From 3416074f9fdd2161f121ecbbc644f393ecfe2655 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Sun, 18 Oct 2026 18:26:14 +0900 Subject: [PATCH 01/12] Integrate remaining CPython 3.15 final runtime and library changes Rebase the remaining integration onto current main while retaining merged compiler and runtime fixes. Import canonical CPython v3.15.0 library sources through update_lib with preserved RustPython markers and explicit migration deferrals. Include the published dependency-report fix from PR #8958, partial slot correction from PR #9127, and update_lib repair with existing-workflow coverage from PR #9131. Assisted-by: OpenAI Codex:model version unavailable --- .cspell.dict/cpython.txt | 2 + .cspell.dict/python-more.txt | 1 + .cspell.dict/rustpython.txt | 2 + .github/workflows/ci.yaml | 79 +- .github/workflows/cron-ci.yaml | 35 + .github/workflows/lib-deps-check.yaml | 80 +- .github/workflows/update-caches.yml | 4 +- .pre-commit-config.yaml | 3 + Cargo.lock | 14 +- Cargo.toml | 22 +- Lib/_collections_abc.py | 13 +- Lib/_ios_support.py | 13 +- Lib/_pyrepl/_module_completer.py | 164 +- Lib/_pyrepl/commands.py | 54 +- Lib/_pyrepl/completing_reader.py | 95 +- Lib/_pyrepl/console.py | 86 +- Lib/_pyrepl/content.py | 136 + Lib/_pyrepl/fancycompleter.py | 213 + Lib/_pyrepl/historical_reader.py | 23 +- Lib/_pyrepl/input.py | 3 +- Lib/_pyrepl/layout.py | 268 + Lib/_pyrepl/pager.py | 2 +- Lib/_pyrepl/reader.py | 680 +- Lib/_pyrepl/readline.py | 65 +- Lib/_pyrepl/render.py | 399 + Lib/_pyrepl/simple_interact.py | 3 +- Lib/_pyrepl/trace.py | 6 + Lib/_pyrepl/types.py | 8 +- Lib/_pyrepl/unix_console.py | 545 +- Lib/_pyrepl/utils.py | 169 +- Lib/_pyrepl/windows_console.py | 354 +- Lib/argparse.py | 285 +- Lib/asyncio/__main__.py | 54 +- Lib/asyncio/base_events.py | 55 +- Lib/asyncio/base_subprocess.py | 46 +- Lib/asyncio/graph.py | 7 +- Lib/asyncio/proactor_events.py | 23 +- Lib/asyncio/selector_events.py | 121 +- Lib/asyncio/staggered.py | 1 - Lib/asyncio/streams.py | 41 +- Lib/asyncio/taskgroups.py | 62 +- Lib/asyncio/tools.py | 47 +- Lib/asyncio/unix_events.py | 78 +- Lib/asyncio/windows_events.py | 2 +- Lib/bdb.py | 13 +- Lib/cProfile.py | 13 + Lib/calendar.py | 187 +- Lib/code.py | 4 +- Lib/codeop.py | 4 +- Lib/collections/__init__.py | 91 +- Lib/compileall.py | 33 +- Lib/concurrent/futures/__init__.py | 22 +- Lib/concurrent/futures/process.py | 188 +- Lib/concurrent/interpreters/_queues.py | 7 +- Lib/contextlib.py | 181 +- Lib/ctypes/__init__.py | 28 +- Lib/ctypes/_layout.py | 3 +- Lib/ctypes/macholib/__init__.py | 8 +- Lib/ctypes/util.py | 61 +- Lib/curses/__init__.py | 98 + Lib/curses/ascii.py | 99 + Lib/curses/has_key.py | 192 + Lib/curses/panel.py | 6 + Lib/curses/textpad.py | 236 + Lib/dataclasses.py | 169 +- Lib/dbm/dumb.py | 33 +- Lib/dbm/sqlite3.py | 4 + Lib/difflib.py | 107 +- Lib/doctest.py | 142 +- Lib/ensurepip/__init__.py | 15 +- ...ne-any.whl => pip-26.2.1-py3-none-any.whl} | Bin 1813144 -> 1816632 bytes Lib/ftplib.py | 8 +- Lib/getpass.py | 250 +- Lib/glob.py | 45 +- Lib/http/client.py | 81 +- Lib/http/cookiejar.py | 11 +- Lib/http/cookies.py | 19 +- Lib/http/server.py | 530 +- Lib/idlelib/News3.txt | 49 +- Lib/idlelib/colorizer.py | 8 +- Lib/idlelib/config.py | 113 +- Lib/idlelib/configdialog.py | 67 +- Lib/idlelib/editor.py | 42 +- Lib/idlelib/filelist.py | 6 +- Lib/idlelib/grep.py | 20 +- Lib/idlelib/idle_test/README.txt | 2 +- Lib/idlelib/idle_test/htest.py | 6 +- Lib/idlelib/idle_test/template.py | 5 +- Lib/idlelib/idle_test/test_autocomplete.py | 16 +- Lib/idlelib/idle_test/test_codecontext.py | 4 +- Lib/idlelib/idle_test/test_colorizer.py | 28 +- Lib/idlelib/idle_test/test_config.py | 60 +- Lib/idlelib/idle_test/test_configdialog.py | 257 +- Lib/idlelib/idle_test/test_editmenu.py | 4 +- Lib/idlelib/idle_test/test_editor.py | 63 +- Lib/idlelib/idle_test/test_filelist.py | 2 +- Lib/idlelib/idle_test/test_grep.py | 88 +- Lib/idlelib/idle_test/test_help.py | 1 - Lib/idlelib/idle_test/test_iomenu.py | 22 +- Lib/idlelib/idle_test/test_mainmenu.py | 1 + Lib/idlelib/idle_test/test_multicall.py | 50 +- Lib/idlelib/idle_test/test_outwin.py | 11 + Lib/idlelib/idle_test/test_pyshell.py | 51 +- Lib/idlelib/idle_test/test_replace.py | 86 +- Lib/idlelib/idle_test/test_rpc.py | 19 + Lib/idlelib/idle_test/test_run.py | 95 +- Lib/idlelib/idle_test/test_runscript.py | 23 +- Lib/idlelib/idle_test/test_sidebar.py | 17 +- Lib/idlelib/idle_test/test_stackviewer.py | 4 +- Lib/idlelib/idle_test/test_window.py | 7 +- Lib/idlelib/idle_test/test_zoomheight.py | 2 +- Lib/idlelib/iomenu.py | 22 +- Lib/idlelib/macosx.py | 2 +- Lib/idlelib/multicall.py | 38 +- Lib/idlelib/outwin.py | 4 + Lib/idlelib/pyshell.py | 64 +- Lib/idlelib/replace.py | 29 +- Lib/idlelib/rpc.py | 9 +- Lib/idlelib/run.py | 110 +- Lib/idlelib/runscript.py | 4 + Lib/idlelib/stackviewer.py | 2 +- Lib/idlelib/textview.py | 8 +- Lib/idlelib/util.py | 33 + Lib/idlelib/window.py | 4 + Lib/imaplib.py | 60 +- Lib/keyword.py | 1 + Lib/mailbox.py | 9 +- Lib/modulefinder.py | 15 +- Lib/multiprocessing/connection.py | 15 +- Lib/multiprocessing/context.py | 9 +- Lib/multiprocessing/forkserver.py | 110 +- Lib/multiprocessing/heap.py | 4 - Lib/multiprocessing/managers.py | 4 +- Lib/multiprocessing/resource_tracker.py | 2 +- Lib/multiprocessing/sharedctypes.py | 7 +- Lib/pathlib/__init__.py | 269 +- Lib/pathlib/_os.py | 387 +- Lib/pathlib/types.py | 98 +- Lib/pdb.py | 291 +- Lib/pickle.py | 52 +- Lib/pickletools.py | 83 +- Lib/pkgutil.py | 44 +- Lib/profile.py | 10 +- Lib/profiling/__init__.py | 13 + Lib/profiling/sampling/__init__.py | 23 + Lib/profiling/sampling/__main__.py | 73 + .../sampling/_assets/python-logo-only.png | Bin 0 -> 12720 bytes .../sampling/_assets/tachyon-logo.png | Bin 0 -> 94008 bytes Lib/profiling/sampling/_child_monitor.py | 279 + Lib/profiling/sampling/_css_utils.py | 22 + .../_flamegraph_assets/flamegraph.css | 1085 + .../sampling/_flamegraph_assets/flamegraph.js | 1528 + .../flamegraph_template.html | 464 + Lib/profiling/sampling/_format_utils.py | 5 + .../sampling/_heatmap_assets/heatmap.css | 1555 + .../sampling/_heatmap_assets/heatmap.js | 738 + .../sampling/_heatmap_assets/heatmap_index.js | 104 + .../heatmap_index_template.html | 136 + .../heatmap_pyfile_template.html | 131 + .../_heatmap_assets/heatmap_shared.js | 64 + .../sampling/_shared_assets/base.css | 514 + Lib/profiling/sampling/_shared_assets/base.js | 58 + Lib/profiling/sampling/_sync_coordinator.py | 256 + .../4.1.3/d3-flamegraph-tooltip.min.js | 1 + .../d3-flame-graph/4.1.3/d3-flamegraph.css | 46 + .../d3-flame-graph/4.1.3/d3-flamegraph.min.js | 1 + .../sampling/_vendor/d3/7.8.5/d3.min.js | 2 + Lib/profiling/sampling/binary_collector.py | 151 + Lib/profiling/sampling/binary_reader.py | 148 + Lib/profiling/sampling/cli.py | 1462 + Lib/profiling/sampling/collector.py | 292 + Lib/profiling/sampling/constants.py | 57 + Lib/profiling/sampling/dump.py | 295 + Lib/profiling/sampling/errors.py | 19 + Lib/profiling/sampling/gecko_collector.py | 1006 + Lib/profiling/sampling/heatmap_collector.py | 1145 + Lib/profiling/sampling/jsonl_collector.py | 268 + .../sampling/live_collector/__init__.py | 200 + .../sampling/live_collector/collector.py | 1112 + .../sampling/live_collector/constants.py | 65 + .../sampling/live_collector/display.py | 240 + .../sampling/live_collector/trend_tracker.py | 157 + .../sampling/live_collector/widgets.py | 1090 + Lib/profiling/sampling/module_utils.py | 102 + Lib/profiling/sampling/opcode_utils.py | 94 + Lib/profiling/sampling/pstats_collector.py | 429 + Lib/profiling/sampling/sample.py | 665 + Lib/profiling/sampling/stack_collector.py | 907 + Lib/profiling/sampling/string_table.py | 53 + Lib/profiling/tracing/__init__.py | 220 + Lib/profiling/tracing/__main__.py | 6 + Lib/profiling/tracing/_utils.py | 32 + Lib/pstats.py | 69 +- Lib/py_compile.py | 6 +- Lib/pyclbr.py | 1 - Lib/pydoc.py | 203 +- Lib/pydoc_data/module_docs.py | 9 +- Lib/pydoc_data/topics.py | 814 +- Lib/re/__init__.py | 54 +- Lib/re/_compiler.py | 2 +- Lib/re/_parser.py | 1 - Lib/rlcompleter.py | 14 +- Lib/shelve.py | 70 +- Lib/site-packages/README.txt | 2 + Lib/sqlite3/__main__.py | 65 +- Lib/sqlite3/_completer.py | 111 + Lib/sqlite3/dump.py | 13 +- Lib/subprocess.py | 392 +- Lib/symtable.py | 31 +- Lib/tempfile.py | 105 +- Lib/test/_crossinterp_definitions.py | 120 + Lib/test/_test_multiprocessing.py | 242 +- Lib/test/bisect_cmd.py | 185 + Lib/test/cov.py | 48 + Lib/test/curses_tests.py | 46 + Lib/test/dtracedata/call_stack.stp | 29 +- Lib/test/levenshtein_examples.json | 50002 ++++++++++++++++ Lib/test/libregrtest/cmdline.py | 122 +- Lib/test/libregrtest/findtests.py | 5 +- Lib/test/libregrtest/main.py | 23 +- Lib/test/libregrtest/refleak.py | 9 + Lib/test/libregrtest/run_workers.py | 9 +- Lib/test/libregrtest/runtests.py | 2 +- Lib/test/libregrtest/setup.py | 8 + Lib/test/libregrtest/tsan.py | 8 +- Lib/test/libregrtest/utils.py | 28 +- Lib/test/picklecommon.py | 56 + Lib/test/pickletester.py | 290 +- Lib/test/pythoninfo.py | 512 +- Lib/test/subprocessdata/fd_status.py | 4 +- Lib/test/support/__init__.py | 207 +- Lib/test/support/channels.py | 4 +- Lib/test/support/isolation.py | 25 +- Lib/test/support/strace_helper.py | 2 +- Lib/test/test__colorize.py | 11 + Lib/test/test__interpchannels.py | 1842 + Lib/test/test__interpreters.py | 1124 + Lib/test/test_android.py | 19 +- Lib/test/test_argparse.py | 639 +- Lib/test/test_asyncio/test_base_events.py | 145 +- Lib/test/test_asyncio/test_events.py | 205 + Lib/test/test_asyncio/test_free_threading.py | 39 + Lib/test/test_asyncio/test_graph.py | 150 + Lib/test/test_asyncio/test_selector_events.py | 43 + Lib/test/test_asyncio/test_server.py | 32 + Lib/test/test_asyncio/test_server_context.py | 314 + Lib/test/test_asyncio/test_ssl.py | 3 + Lib/test/test_asyncio/test_sslproto.py | 44 + Lib/test/test_asyncio/test_streams.py | 28 + Lib/test/test_asyncio/test_subprocess.py | 146 +- Lib/test/test_asyncio/test_taskgroups.py | 231 +- Lib/test/test_asyncio/test_tools.py | 311 +- Lib/test/test_asyncio/test_unix_events.py | 47 +- Lib/test/test_asyncio/test_windows_events.py | 109 + Lib/test/test_asyncio/utils.py | 8 +- Lib/test/test_calendar.py | 200 +- Lib/test/test_class.py | 123 +- Lib/test/test_cmd_line_script.py | 45 +- Lib/test/test_codeop.py | 2 +- Lib/test/test_collections.py | 66 +- Lib/test/test_compileall.py | 8 +- Lib/test/test_complex.py | 49 +- Lib/test/test_concurrent_futures/executor.py | 43 +- .../test_as_completed.py | 4 + .../test_concurrent_futures/test_deadlock.py | 42 +- Lib/test/test_concurrent_futures/test_init.py | 10 + .../test_interpreter_pool.py | 68 +- .../test_process_pool.py | 208 +- .../test_concurrent_futures/test_shutdown.py | 32 +- .../test_thread_pool.py | 3 + Lib/test/test_concurrent_futures/test_wait.py | 12 +- Lib/test/test_concurrent_futures/util.py | 8 +- Lib/test/test_contextlib.py | 217 + Lib/test/test_contextlib_async.py | 210 + Lib/test/test_coroutines.py | 161 +- Lib/test/test_crossinterp.py | 1550 + Lib/test/test_ctypes/__init__.py | 17 + Lib/test/test_ctypes/_support.py | 1 - Lib/test/test_ctypes/test_as_parameter.py | 5 +- .../test_ctypes/test_c_simple_type_meta.py | 8 +- Lib/test/test_ctypes/test_callbacks.py | 15 + Lib/test/test_ctypes/test_dlerror.py | 5 +- Lib/test/test_ctypes/test_find.py | 18 +- .../test_ctypes/test_generated_structs.py | 2 +- Lib/test/test_ctypes/test_incomplete.py | 33 +- Lib/test/test_ctypes/test_macholib.py | 12 + Lib/test/test_ctypes/test_numbers.py | 13 +- Lib/test/test_ctypes/test_parameters.py | 8 + Lib/test/test_ctypes/test_structures.py | 7 + Lib/test/test_curses.py | 2046 + Lib/test/test_dataclasses/__init__.py | 73 +- Lib/test/test_dataclasses/_types_proxy.py | 8 + .../test_dataclasses/dataclass_module_3.py | 32 + .../dataclass_module_3_str.py | 32 + .../test_dataclasses/dataclass_module_4.py | 38 + .../dataclass_module_4_str.py | 38 + Lib/test/test_dbm_dumb.py | 10 + Lib/test/test_dbm_gnu.py | 222 + Lib/test/test_dbm_ndbm.py | 165 + Lib/test/test_defaultdict.py | 12 + Lib/test/test_dictviews.py | 4 +- Lib/test/test_difflib.py | 32 +- Lib/test/test_difflib_expect.html | 332 +- Lib/test/test_dtrace.py | 415 +- Lib/test/test_ensurepip.py | 37 +- Lib/test/test_exceptions.py | 119 +- Lib/test/test_external_inspection.py | 4377 +- Lib/test/test_format.py | 291 +- Lib/test/test_fstring.py | 98 +- Lib/test/test_getpass.py | 168 +- Lib/test/test_glob.py | 31 - Lib/test/test_grammar.py | 9 +- Lib/test/test_httplib.py | 133 + Lib/test/test_httpservers.py | 810 +- Lib/test/test_interpreters/__init__.py | 9 + Lib/test/test_interpreters/__main__.py | 4 + Lib/test/test_interpreters/test_api.py | 2489 + Lib/test/test_interpreters/test_channels.py | 610 + Lib/test/test_interpreters/test_lifecycle.py | 194 + Lib/test/test_interpreters/test_queues.py | 667 + Lib/test/test_interpreters/test_stress.py | 94 + Lib/test/test_interpreters/utils.py | 687 + Lib/test/test_mailbox.py | 25 +- Lib/test/test_marshal.py | 29 +- Lib/test/test_minidom.py | 316 +- Lib/test/test_modulefinder.py | 15 + .../__init__.py | 9 + .../test_preload.py | 230 + .../test_multiprocessing_main_handling.py | 12 +- Lib/test/test_operator.py | 16 + Lib/test/test_ordered_dict.py | 13 +- Lib/test/test_pathlib/support/lexical_path.py | 11 +- Lib/test/test_pathlib/support/local_path.py | 16 +- Lib/test/test_pathlib/support/zip_path.py | 51 +- Lib/test/test_pathlib/test_join.py | 55 + Lib/test/test_pathlib/test_join_windows.py | 22 +- Lib/test/test_pathlib/test_pathlib.py | 132 +- Lib/test/test_pathlib/test_read.py | 27 +- Lib/test/test_pathlib/test_write.py | 22 +- Lib/test/test_patma.py | 119 + Lib/test/test_pickle.py | 25 +- Lib/test/test_picklebuffer.py | 3 +- Lib/test/test_pkgutil.py | 52 +- Lib/test/test_platform.py | 36 +- Lib/test/test_positional_only_arg.py | 12 +- Lib/test/test_profile.py | 6 +- Lib/test/test_profiling/__init__.py | 5 + Lib/test/test_profiling/__main__.py | 4 + Lib/test/test_profiling/test_heatmap.py | 862 + .../test_sampling_profiler/__init__.py | 9 + .../_live_collector_helpers.py | 62 + .../test_sampling_profiler/helpers.py | 202 + .../test_sampling_profiler/mocks.py | 112 + .../test_sampling_profiler/test_advanced.py | 236 + .../test_sampling_profiler/test_async.py | 787 + .../test_binary_format.py | 1788 + .../test_sampling_profiler/test_blocking.py | 215 + .../test_sampling_profiler/test_children.py | 1082 + .../test_sampling_profiler/test_cli.py | 1083 + .../test_sampling_profiler/test_collectors.py | 3543 ++ .../test_sampling_profiler/test_dump.py | 454 + .../test_integration.py | 953 + .../test_live_collector_core.py | 698 + .../test_live_collector_interaction.py | 1234 + .../test_live_collector_ui.py | 906 + .../test_sampling_profiler/test_modes.py | 587 + .../test_sampling_profiler/test_profiler.py | 888 + .../test_trend_tracker.py | 97 + .../test_profiling/test_tracing_profiler.py | 308 + Lib/test/test_py_compile.py | 107 +- Lib/test/test_pyexpat.py | 68 +- Lib/test/test_pyrepl/__init__.py | 3 + Lib/test/test_pyrepl/support.py | 15 +- Lib/test/test_pyrepl/test_fancycompleter.py | 250 + Lib/test/test_pyrepl/test_interact.py | 2 +- Lib/test/test_pyrepl/test_layout.py | 309 + Lib/test/test_pyrepl/test_pyrepl.py | 551 +- Lib/test/test_pyrepl/test_reader.py | 78 +- Lib/test/test_pyrepl/test_render.py | 311 + Lib/test/test_pyrepl/test_utils.py | 48 + Lib/test/test_pyrepl/test_windows_console.py | 42 +- Lib/test/test_re.py | 79 +- Lib/test/test_regrtest.py | 43 +- Lib/test/test_remote_pdb.py | 1754 + Lib/test/test_sax.py | 12 + Lib/test/test_shelve.py | 252 +- Lib/test/test_sqlite3/test_cli.py | 260 +- Lib/test/test_sqlite3/test_dbapi.py | 26 +- Lib/test/test_sqlite3/test_dump.py | 18 + Lib/test/test_sqlite3/test_factory.py | 26 +- Lib/test/test_sqlite3/test_hooks.py | 22 +- Lib/test/test_sqlite3/test_userfunctions.py | 55 +- Lib/test/test_str.py | 67 +- Lib/test/test_subprocess.py | 181 +- Lib/test/test_support.py | 272 +- Lib/test/test_symtable.py | 81 + Lib/test/test_sys.py | 255 +- Lib/test/test_tcl.py | 838 + Lib/test/test_tempfile.py | 84 + Lib/test/test_timeit.py | 82 +- Lib/test/test_tkinter/README | 14 + Lib/test/test_tkinter/__init__.py | 30 + Lib/test/test_tkinter/__main__.py | 4 + Lib/test/test_tkinter/support.py | 211 + Lib/test/test_tkinter/test_colorchooser.py | 69 + Lib/test/test_tkinter/test_dnd.py | 171 + Lib/test/test_tkinter/test_filedialog.py | 73 + Lib/test/test_tkinter/test_font.py | 250 + .../test_tkinter/test_geometry_managers.py | 1001 + Lib/test/test_tkinter/test_images.py | 675 + Lib/test/test_tkinter/test_loadtk.py | 47 + Lib/test/test_tkinter/test_messagebox.py | 37 + Lib/test/test_tkinter/test_misc.py | 1982 + Lib/test/test_tkinter/test_scrolledtext.py | 65 + Lib/test/test_tkinter/test_simpledialog.py | 279 + Lib/test/test_tkinter/test_text.py | 747 + Lib/test/test_tkinter/test_tkinter_pipe.py | 55 + Lib/test/test_tkinter/test_variables.py | 363 + Lib/test/test_tkinter/test_widgets.py | 2644 + Lib/test/test_tkinter/widget_tests.py | 628 + Lib/test/test_tomllib/burntsushi.py | 33 +- .../data/valid/dates-and-times/datetimes.json | 1 + .../data/valid/dates-and-times/datetimes.toml | 1 + .../data/valid/dates-and-times/localtime.json | 4 +- .../data/valid/dates-and-times/localtime.toml | 3 +- .../inline-table/empty-inline-table.json | 1 + .../inline-table/empty-inline-table.toml | 1 + .../inline-table/multiline-inline-table.json | 35 + .../inline-table/multiline-inline-table.toml | 12 + .../multiline-basic-str/replacements.json | 6 + .../multiline-basic-str/replacements.toml | 4 + Lib/test/test_tomllib/test_data.py | 19 +- Lib/test/test_tomllib/test_misc.py | 19 + Lib/test/test_ttk/__init__.py | 53 + Lib/test/test_ttk/__main__.py | 4 + Lib/test/test_ttk/test_extensions.py | 404 + Lib/test/test_ttk/test_style.py | 464 + Lib/test/test_ttk/test_widgets.py | 2202 + Lib/test/test_ttk_textonly.py | 481 + Lib/test/test_turtle.py | 22 + Lib/test/test_type_aliases.py | 58 +- Lib/test/test_type_cache.py | 24 +- Lib/test/test_typing.py | 645 +- Lib/test/test_unittest/test_assertions.py | 11 +- Lib/test/test_unittest/test_case.py | 144 +- Lib/test/test_unittest/test_loader.py | 51 + Lib/test/test_unittest/test_result.py | 37 +- Lib/test/test_unittest/test_runner.py | 52 +- Lib/test/test_unittest/test_util.py | 33 + Lib/test/test_unparse.py | 32 + Lib/test/test_urllib.py | 55 + Lib/test/test_urllib2.py | 73 + Lib/test/test_urllib2net.py | 21 - Lib/test/test_urlparse.py | 596 +- Lib/test/test_uuid.py | 0 Lib/test/test_webbrowser.py | 96 +- Lib/test/test_wsgiref.py | 128 +- Lib/test/test_xml.py | 43 + Lib/test/test_xml_etree.py | 113 +- Lib/test/test_zipfile/__main__.py | 2 +- Lib/test/test_zipfile/_path/test_path.py | 2 +- Lib/test/test_zipfile/test_core.py | 152 +- .../test_zoneinfo/data/update_test_data.py | 3 + .../test_zoneinfo/data/zoneinfo_data.json | 348 +- Lib/test/test_zoneinfo/test_zoneinfo.py | 90 +- Lib/timeit.py | 249 +- Lib/tkinter/__init__.py | 331 +- Lib/tkinter/colorchooser.py | 2 + Lib/tkinter/commondialog.py | 2 + Lib/tkinter/dialog.py | 7 +- Lib/tkinter/font.py | 27 +- Lib/tkinter/messagebox.py | 12 +- Lib/tkinter/simpledialog.py | 7 +- Lib/tkinter/ttk.py | 82 +- Lib/tomllib/_parser.py | 65 +- Lib/tomllib/_re.py | 26 +- Lib/trace.py | 34 +- Lib/turtle.py | 734 +- Lib/typing.py | 439 +- Lib/unittest/_log.py | 5 +- Lib/unittest/case.py | 38 +- Lib/unittest/loader.py | 11 +- Lib/unittest/main.py | 14 +- Lib/unittest/suite.py | 20 +- Lib/unittest/util.py | 25 +- Lib/urllib/error.py | 2 +- Lib/urllib/parse.py | 278 +- Lib/urllib/request.py | 27 +- Lib/webbrowser.py | 90 +- Lib/wsgiref/handlers.py | 3 +- Lib/wsgiref/simple_server.py | 14 +- Lib/xml/__init__.py | 3 +- Lib/xml/dom/pulldom.py | 16 +- Lib/xml/etree/ElementPath.py | 17 +- Lib/xml/sax/expatreader.py | 12 +- Lib/xml/sax/handler.py | 11 +- Lib/xml/utils.py | 37 + Lib/zipfile/__init__.py | 161 +- Lib/zipfile/_path/__init__.py | 27 +- Lib/zipfile/_path/_functools.py | 20 + Lib/zoneinfo/_tzpath.py | 4 + Lib/zoneinfo/_zoneinfo.py | 11 +- Parser/Python.asdl | 154 + Parser/README.md | 8 + Parser/asdl.py | 399 + build.rs | 14 + capi-exports.def | 516 + crates/capi/Cargo.toml | 2 +- crates/capi/src/ceval.rs | 118 + crates/capi/src/dictobject.rs | 338 + crates/capi/src/methodobject.rs | 51 +- crates/capi/src/monitoring.rs | 202 + crates/capi/src/pyerrors.rs | 4 +- crates/capi/src/pylifecycle.rs | 6 +- crates/capi/src/util.rs | 4 +- crates/codegen/src/compile.rs | 1278 +- crates/codegen/src/ir.rs | 322 +- crates/codegen/src/symboltable.rs | 18 +- crates/compiler/src/lib.rs | 125 +- crates/derive-impl/src/compile_bytecode.rs | 3 +- crates/doc/generate.py | 16 +- crates/doc/src/data.inc.rs | 8 +- crates/host_env/src/crt_fd.rs | 2 +- crates/host_env/src/ctypes.rs | 48 +- crates/host_env/src/fs.rs | 4 + crates/host_env/src/signal.rs | 76 +- crates/host_env/src/time.rs | 1 - crates/stdlib/src/_remote_debugging.rs | 607 +- crates/stdlib/src/_remote_debugging/binary.rs | 975 + crates/stdlib/src/pickle.rs | 152 +- crates/stdlib/src/pyexpat.rs | 647 +- crates/stdlib/src/syslog.rs | 11 + crates/vm/Cargo.toml | 1 + crates/vm/src/builtins/builtin_func.rs | 10 +- crates/vm/src/builtins/descriptor.rs | 17 +- crates/vm/src/builtins/function.rs | 6 +- crates/vm/src/builtins/type.rs | 3 + crates/vm/src/class.rs | 8 +- crates/vm/src/coroutine.rs | 10 +- crates/vm/src/exceptions.rs | 32 +- crates/vm/src/frame.rs | 127 +- crates/vm/src/getpath.rs | 1 - crates/vm/src/import.rs | 103 +- crates/vm/src/protocol/mapping.rs | 40 +- crates/vm/src/protocol/number.rs | 32 +- crates/vm/src/signal.rs | 68 +- crates/vm/src/stdlib/_ast/expression.rs | 23 +- crates/vm/src/stdlib/_ast/pyast.rs | 30 +- crates/vm/src/stdlib/_ast/python.rs | 157 +- crates/vm/src/stdlib/_ast/validate.rs | 15 +- .../src/stdlib/_collections/ordered_dict.rs | 4 +- crates/vm/src/stdlib/_ctypes.rs | 35 +- crates/vm/src/stdlib/_ctypes/base.rs | 15 +- crates/vm/src/stdlib/_ctypes/function.rs | 344 +- crates/vm/src/stdlib/_ctypes/simple.rs | 12 + crates/vm/src/stdlib/_imp.rs | 25 +- crates/vm/src/stdlib/_interpreters.rs | 8 +- crates/vm/src/stdlib/_lsprof.rs | 620 + crates/vm/src/stdlib/_signal.rs | 122 +- crates/vm/src/stdlib/_sre.rs | 2 + crates/vm/src/stdlib/_symtable.rs | 53 +- crates/vm/src/stdlib/_sysconfigdata.rs | 7 + crates/vm/src/stdlib/_testcapi.rs | 391 + crates/vm/src/stdlib/_testcapi/call_buffer.rs | 197 + crates/vm/src/stdlib/_testcapi/heap.rs | 210 + crates/vm/src/stdlib/_testcapi/meth.rs | 210 + crates/vm/src/stdlib/_testcapi/monitoring.rs | 193 + crates/vm/src/stdlib/_testcapi/runtime.rs | 188 + crates/vm/src/stdlib/_testcapi/threads.rs | 123 + crates/vm/src/stdlib/_testinternalcapi.rs | 473 +- crates/vm/src/stdlib/_thread.rs | 11 +- crates/vm/src/stdlib/_warnings.rs | 8 +- crates/vm/src/stdlib/builtins.rs | 14 +- crates/vm/src/stdlib/mod.rs | 14 +- crates/vm/src/stdlib/sys.rs | 44 +- crates/vm/src/stdlib/sys/monitoring.rs | 19 + crates/vm/src/stdlib/sys/monitoring/capi.rs | 176 + crates/vm/src/stdlib/time.rs | 41 +- crates/vm/src/stdlib/time/pytime.rs | 221 + crates/vm/src/types/slot.rs | 4 +- crates/vm/src/vm/compile.rs | 641 +- crates/vm/src/vm/crossinterp.rs | 108 +- crates/vm/src/vm/interpreter.rs | 42 +- crates/vm/src/vm/mod.rs | 107 +- crates/vm/src/vm/python_run.rs | 15 +- crates/vm/src/vm/runtime.rs | 4 + crates/vm/src/vm/setting.rs | 8 +- crates/vm/src/vm/thread.rs | 6 + crates/vm/src/vm/vm_new.rs | 18 +- crates/vm/src/warn.rs | 33 +- extra_tests/frozendict_contract.py | 57 + extra_tests/snippets/builtin_memoryview.py | 2 +- .../snippets/import_canonical_callback.py | 71 + extra_tests/snippets/import_lazy_native.py | 53 + .../snippets/stdlib_315_api_followups.py | 25 + .../stdlib_asyncio_reinitialization_315.py | 75 + .../snippets/stdlib_base_n_buffers_315.py | 68 + .../stdlib_base_n_canonical_cleanup_315.py | 63 + .../stdlib_bytearray_take_bytes_315.py | 34 + ...stdlib_bytearray_take_bytes_threads_315.py | 56 + .../snippets/stdlib_codec_registry_315.py | 19 + .../snippets/stdlib_compress_csv_315.py | 31 + .../stdlib_frozen_mapping_integrations_315.py | 113 + .../snippets/stdlib_generator_states_315.py | 33 + extra_tests/snippets/stdlib_json_315.py | 17 + .../snippets/stdlib_lock_precedence_315.py | 24 + .../snippets/stdlib_marshal_code_filename.py | 33 + .../stdlib_ordered_views_extended_315.py | 86 + .../snippets/stdlib_rlock_constructor_315.py | 50 + .../snippets/stdlib_ssl_introspection.py | 63 + extra_tests/snippets/stdlib_venv_cycles.py | 49 + extra_tests/snippets/stdlib_venv_nested.py | 155 + .../snippets/stdlib_warning_followups_315.py | 42 + .../snippets/stdlib_warning_module_315.py | 34 + extra_tests/snippets/stdlib_weakref_gc_315.py | 61 + .../snippets/stdlib_weakref_gc_threads_315.py | 103 + ruff.toml | 1 + scripts/dis_dump.py | 10 +- scripts/generate_capi_exports.py | 212 + scripts/update_lib/deps.py | 2 + src/interpreter.rs | 9 +- src/lib.rs | 19 +- src/settings.rs | 54 +- tests/frozendict.rs | 45 + tools/opcode_metadata/cpython.py | 2 + tools/opcode_metadata/opcodes.py | 36 +- tools/opcode_metadata/utils.py | 4 + .../.cargo_vcs_info.json | 6 + .../rustpython-ruff_python_parser/Cargo.toml | 226 + .../Cargo.toml.orig | 58 + vendor/rustpython-ruff_python_parser/LICENSE | 430 + .../rustpython-ruff_python_parser/README.md | 30 + .../README.rustpython.md | 149 + .../UPSTREAM-SHA256.json | 231 + ...tpython-rc3-concat-comma-diagnostics.patch | 130 + .../rustpython-rc3-fstring-diagnostics.patch | 268 + ...rustpython-rc3-parameter-diagnostics.patch | 120 + ...ython-rc3-starred-generator-operands.patch | 135 + ...rustpython-rc3-statement-diagnostics.patch | 709 + .../rustpython-rc3-unpack-diagnostics.patch | 559 + .../src/error.rs | 1800 + .../src/lexer.rs | 3230 + .../src/lexer/cursor.rs | 178 + .../src/lexer/indentation.rs | 202 + .../src/lexer/interpolated_string.rs | 179 + .../rustpython-ruff_python_parser/src/lib.rs | 626 + .../src/parser/case_diagnostics.rs | 156 + .../src/parser/expression.rs | 4341 ++ .../src/parser/helpers.rs | 112 + .../src/parser/mod.rs | 2256 + .../src/parser/options.rs | 59 + .../src/parser/pattern.rs | 889 + .../src/parser/progress.rs | 46 + .../src/parser/recovery.rs | 219 + .../src/parser/scratch_buffer.rs | 136 + ...ser__tests__expr_mode_invalid_syntax1.snap | 8 + ...ser__tests__expr_mode_invalid_syntax2.snap | 8 + ...ser__tests__expr_mode_invalid_syntax3.snap | 8 + ...parser__tests__expr_mode_valid_syntax.snap | 12 + ..._inner_line_continuation_and_t_string.snap | 10 + ...er_line_continuation_newline_t_string.snap | 12 + ...arser__tests__ipython_escape_commands.snap | 532 + ...parser__tests__tstring_fstring_middle.snap | 12 + ..._tests__tstring_fstring_middle_fuzzer.snap | 12 + ...arser__parser__tests__unicode_aliases.snap | 46 + .../src/parser/statement.rs | 4921 ++ .../src/parser/tests.rs | 623 + .../src/semantic_errors.rs | 2668 + ...thon_parser__lexer__tests__assignment.snap | 17 + ...tests__backslash_continuation_at_root.snap | 29 + ...s__backslash_continuation_indentation.snap | 28 + ...ash_continuation_mismatch_indentation.snap | 28 + ...ruff_python_parser__lexer__tests__bom.snap | 13 + ...parser__lexer__tests__bom_with_offset.snap | 13 + ...r__lexer__tests__bom_with_offset_edge.snap | 11 + ...__lexer__tests__comment_until_mac_eol.snap | 14 + ..._lexer__tests__comment_until_unix_eol.snap | 14 + ...xer__tests__comment_until_windows_eol.snap | 14 + ...lexer__tests__dedent_after_whitespace.snap | 25 + ...er__tests__double_dedent_with_mac_eol.snap | 28 + ...ests__double_dedent_with_tabs_mac_eol.snap | 28 + ...sts__double_dedent_with_tabs_unix_eol.snap | 28 + ...__double_dedent_with_tabs_windows_eol.snap | 28 + ...r__tests__double_dedent_with_unix_eol.snap | 28 + ...tests__double_dedent_with_windows_eol.snap | 28 + ...arser__lexer__tests__emoji_identifier.snap | 21 + ..._parser__lexer__tests__empty_fstrings.snap | 22 + ...__tests__empty_ipython_escape_command.snap | 27 + ..._parser__lexer__tests__empty_tstrings.snap | 22 + ...er__lexer__tests__escape_unicode_name.snap | 11 + ..._python_parser__lexer__tests__fstring.snap | 25 + ...arser__lexer__tests__fstring_comments.snap | 20 + ...ser__lexer__tests__fstring_conversion.snap | 31 + ..._parser__lexer__tests__fstring_escape.snap | 22 + ...__lexer__tests__fstring_escape_braces.snap | 28 + ...ser__lexer__tests__fstring_escape_raw.snap | 22 + ...__tests__fstring_expression_multiline.snap | 23 + ...rser__lexer__tests__fstring_multiline.snap | 28 + ...__lexer__tests__fstring_named_unicode.snap | 13 + ...xer__tests__fstring_named_unicode_raw.snap | 17 + ..._parser__lexer__tests__fstring_nested.snap | 42 + ...r__tests__fstring_newline_format_spec.snap | 83 + ...er__lexer__tests__fstring_parentheses.snap | 40 + ..._parser__lexer__tests__fstring_prefix.snap | 30 + ...__fstring_single_quote_escape_mac_eol.snap | 13 + ..._fstring_single_quote_escape_unix_eol.snap | 13 + ...tring_single_quote_escape_windows_eol.snap | 13 + ...exer__tests__fstring_with_format_spec.snap | 62 + ...ests__fstring_with_ipy_escape_command.snap | 18 + ...tests__fstring_with_lambda_expression.snap | 33 + ...s__fstring_with_multiline_format_spec.snap | 31 + ..._tests__fstring_with_named_expression.snap | 43 + ...__lexer__tests__fstring_with_nul_char.snap | 13 + ...exer__tests__indentation_with_mac_eol.snap | 21 + ...xer__tests__indentation_with_unix_eol.snap | 21 + ...__tests__indentation_with_windows_eol.snap | 21 + ...exer__tests__invalid_leading_zero_big.snap | 20 + ...er__tests__invalid_leading_zero_small.snap | 20 + ..._lexer__tests__ipython_escape_command.snap | 31 + ...ts__ipython_escape_command_assignment.snap | 33 + ...s__ipython_escape_command_indentation.snap | 17 + ...ape_command_line_continuation_mac_eol.snap | 11 + ...pe_command_line_continuation_unix_eol.snap | 11 + ...command_line_continuation_windows_eol.snap | 11 + ...ine_continuation_with_mac_eol_and_eof.snap | 11 + ...ne_continuation_with_unix_eol_and_eof.snap | 11 + ...continuation_with_windows_eol_and_eof.snap | 11 + ...ests__ipython_help_end_escape_command.snap | 41 + ...ser__lexer__tests__line_comment_empty.snap | 12 + ...rser__lexer__tests__line_comment_long.snap | 12 + ...tests__line_comment_single_whitespace.snap | 12 + ...lexer__tests__line_comment_whitespace.snap | 12 + ...inuation_at_eof_after_newline_mac_eol.snap | 19 + ...nuation_at_eof_after_newline_unix_eol.snap | 19 + ...tion_at_eof_after_newline_windows_eol.snap | 19 + ...sts__line_continuation_at_eof_mac_eol.snap | 22 + ...ts__line_continuation_at_eof_unix_eol.snap | 22 + ..._line_continuation_at_eof_windows_eol.snap | 22 + ...__tests__logical_newline_line_comment.snap | 13 + ..._tests__match_softkeyword_in_notebook.snap | 23 + ...ests__multiple_backslash_continuation.snap | 19 + ...r__lexer__tests__nested_t_and_fstring.snap | 44 + ...r__tests__newline_in_brackets_mac_eol.snap | 39 + ...__tests__newline_in_brackets_unix_eol.snap | 39 + ...ests__newline_in_brackets_windows_eol.snap | 39 + ...ogical_newline_in_string_continuation.snap | 21 + ..._python_parser__lexer__tests__numbers.snap | 24 + ...ython_parser__lexer__tests__operators.snap | 15 + ...f_python_parser__lexer__tests__string.snap | 19 + ...sts__string_continuation_with_mac_eol.snap | 11 + ...ts__string_continuation_with_unix_eol.snap | 11 + ..._string_continuation_with_windows_eol.snap | 11 + ...ser__lexer__tests__tet_too_low_dedent.snap | 28 + ...__lexer__tests__triple_quoted_mac_eol.snap | 11 + ..._lexer__tests__triple_quoted_unix_eol.snap | 11 + ...xer__tests__triple_quoted_windows_eol.snap | 11 + ..._python_parser__lexer__tests__tstring.snap | 25 + ...arser__lexer__tests__tstring_comments.snap | 20 + ...ser__lexer__tests__tstring_conversion.snap | 31 + ..._parser__lexer__tests__tstring_escape.snap | 22 + ...__lexer__tests__tstring_escape_braces.snap | 28 + ...ser__lexer__tests__tstring_escape_raw.snap | 22 + ...__tests__tstring_expression_multiline.snap | 23 + ...rser__lexer__tests__tstring_multiline.snap | 28 + ...__lexer__tests__tstring_named_unicode.snap | 13 + ...xer__tests__tstring_named_unicode_raw.snap | 17 + ..._parser__lexer__tests__tstring_nested.snap | 42 + ...r__tests__tstring_newline_format_spec.snap | 83 + ...er__lexer__tests__tstring_parentheses.snap | 40 + ..._parser__lexer__tests__tstring_prefix.snap | 30 + ...__tstring_single_quote_escape_mac_eol.snap | 13 + ..._tstring_single_quote_escape_unix_eol.snap | 13 + ...tring_single_quote_escape_windows_eol.snap | 13 + ...exer__tests__tstring_with_format_spec.snap | 62 + ...ests__tstring_with_ipy_escape_command.snap | 18 + ...tests__tstring_with_lambda_expression.snap | 33 + ...s__tstring_with_multiline_format_spec.snap | 31 + ..._tests__tstring_with_named_expression.snap | 43 + ...__lexer__tests__tstring_with_nul_char.snap | 13 + ...arser__string__tests__backspace_alias.snap | 33 + ...hon_parser__string__tests__bell_alias.snap | 33 + ..._string__tests__carriage_return_alias.snap | 33 + ...r_tabulation_with_justification_alias.snap | 33 + ...n_parser__string__tests__delete_alias.snap | 33 + ...ests__dont_panic_on_8_in_octal_escape.snap | 46 + ...er__string__tests__double_quoted_byte.snap | 290 + ...n_parser__string__tests__escape_alias.snap | 33 + ...g__tests__escape_char_in_byte_literal.snap | 44 + ...n_parser__string__tests__escape_octet.snap | 39 + ...arser__string__tests__form_feed_alias.snap | 33 + ...string__tests__fstring_constant_range.snap | 97 + ...ing__tests__fstring_escaped_character.snap | 63 + ...tring__tests__fstring_escaped_newline.snap | 63 + ...ing__tests__fstring_line_continuation.snap | 65 + ...__fstring_parse_self_documenting_base.snap | 62 + ...ring_parse_self_documenting_base_more.snap | 102 + ...fstring_parse_self_documenting_format.snap | 76 + ...ing__tests__fstring_unescaped_newline.snap | 63 + ...thon_parser__string__tests__hts_alias.snap | 33 + ...ng__tests__invalid_byte_literal_error.snap | 10 + ...tring__tests__invalid_unicode_literal.snap | 14 + ...ng__tests__invalid_unicode_name_error.snap | 14 + ...__tests__missing_unicode_lbrace_error.snap | 14 + ...__tests__missing_unicode_rbrace_error.snap | 14 + ...r__string__tests__parse_empty_fstring.snap | 35 + ...r__string__tests__parse_empty_tstring.snap | 35 + ...tring__tests__parse_f_string_concat_1.snap | 60 + ...tring__tests__parse_f_string_concat_2.snap | 60 + ...tring__tests__parse_f_string_concat_3.snap | 93 + ...tring__tests__parse_f_string_concat_4.snap | 106 + ...ests__parse_f_t_string_concat_1_error.snap | 10 + ...ests__parse_f_t_string_concat_2_error.snap | 10 + ..._parser__string__tests__parse_fstring.snap | 83 + ...__string__tests__parse_fstring_equals.snap | 78 + ...ring_nested_concatenation_string_spec.snap | 112 + ...ing__tests__parse_fstring_nested_spec.snap | 83 + ...sts__parse_fstring_nested_string_spec.snap | 96 + ...ring__tests__parse_fstring_not_equals.snap | 78 + ..._tests__parse_fstring_not_nested_spec.snap | 70 + ...ts__parse_fstring_self_doc_prec_space.snap | 62 + ...parse_fstring_self_doc_trailing_space.snap | 62 + ...ring__tests__parse_fstring_yield_expr.snap | 55 + ...r__string__tests__parse_string_concat.snap | 49 + ..._parse_string_triple_quotes_with_kind.snap | 33 + ..._tests__parse_t_string_concat_1_error.snap | 10 + ..._tests__parse_t_string_concat_2_error.snap | 10 + ..._tests__parse_t_string_concat_3_error.snap | 10 + ..._tests__parse_t_string_concat_4_error.snap | 10 + ..._parser__string__tests__parse_tstring.snap | 83 + ...__string__tests__parse_tstring_equals.snap | 78 + ...ring_nested_concatenation_string_spec.snap | 112 + ...ing__tests__parse_tstring_nested_spec.snap | 83 + ...sts__parse_tstring_nested_string_spec.snap | 96 + ...ring__tests__parse_tstring_not_equals.snap | 78 + ..._tests__parse_tstring_not_nested_spec.snap | 70 + ...ts__parse_tstring_self_doc_prec_space.snap | 62 + ...parse_tstring_self_doc_trailing_space.snap | 62 + ...ring__tests__parse_tstring_yield_expr.snap | 55 + ...ing__tests__parse_u_f_string_concat_1.snap | 60 + ...ing__tests__parse_u_f_string_concat_2.snap | 73 + ...tring__tests__parse_u_string_concat_1.snap | 49 + ...tring__tests__parse_u_string_concat_2.snap | 49 + ...ests__parse_u_t_string_concat_1_error.snap | 10 + ...ests__parse_u_t_string_concat_2_error.snap | 10 + ...er__string__tests__raw_byte_literal_1.snap | 40 + ...er__string__tests__raw_byte_literal_2.snap | 38 + ...on_parser__string__tests__raw_fstring.snap | 58 + ...on_parser__string__tests__raw_tstring.snap | 58 + ...er__string__tests__single_quoted_byte.snap | 290 + ..._tests__string_parser_escaped_mac_eol.snap | 33 + ...tests__string_parser_escaped_unix_eol.snap | 33 + ...ts__string_parser_escaped_windows_eol.snap | 33 + ...ing__tests__triple_quoted_raw_fstring.snap | 58 + ...ing__tests__triple_quoted_raw_tstring.snap | 58 + ...string__tests__tstring_constant_range.snap | 97 + ...ing__tests__tstring_escaped_character.snap | 63 + ...tring__tests__tstring_escaped_newline.snap | 63 + ...ing__tests__tstring_line_continuation.snap | 65 + ...__tstring_parse_self_documenting_base.snap | 62 + ...ring_parse_self_documenting_base_more.snap | 102 + ...tstring_parse_self_documenting_format.snap | 76 + ...ing__tests__tstring_unescaped_newline.snap | 63 + .../src/string.rs | 1238 + .../src/token_set.rs | 52 + .../src/token_source.rs | 276 + .../src/typing.rs | 92 + vendor/xml/.cargo_vcs_info.json | 6 + vendor/xml/.gitignore | 2 + vendor/xml/Cargo.toml | 63 + vendor/xml/Cargo.toml.orig | 26 + vendor/xml/LICENSE | 21 + vendor/xml/README.md | 225 + vendor/xml/RUSTPYTHON-PROVENANCE.md | 77 + vendor/xml/UPSTREAM-SHA256SUMS | 32 + vendor/xml/patches/amplification.patch | 1071 + vendor/xml/src/attribute.rs | 99 + vendor/xml/src/common.rs | 199 + vendor/xml/src/escape.rs | 162 + vendor/xml/src/lib.rs | 27 + vendor/xml/src/macros.rs | 38 + vendor/xml/src/name.rs | 334 + vendor/xml/src/namespace.rs | 515 + vendor/xml/src/reader.rs | 239 + vendor/xml/src/reader/amplification.rs | 106 + vendor/xml/src/reader/amplification_tests.rs | 129 + vendor/xml/src/reader/config.rs | 312 + vendor/xml/src/reader/error.rs | 323 + vendor/xml/src/reader/events.rs | 270 + vendor/xml/src/reader/lexer.rs | 1212 + vendor/xml/src/reader/parser.rs | 948 + .../src/reader/parser/attribute_expansion.rs | 158 + vendor/xml/src/reader/parser/inside_cdata.rs | 41 + .../reader/parser/inside_closing_tag_name.rs | 32 + .../xml/src/reader/parser/inside_comment.rs | 37 + .../src/reader/parser/inside_declaration.rs | 204 + .../xml/src/reader/parser/inside_doctype.rs | 437 + .../src/reader/parser/inside_opening_tag.rs | 124 + .../parser/inside_processing_instruction.rs | 116 + .../xml/src/reader/parser/inside_reference.rs | 99 + vendor/xml/src/reader/parser/outside_tag.rs | 242 + vendor/xml/src/util.rs | 356 + vendor/xml/src/writer.rs | 96 + vendor/xml/src/writer/config.rs | 157 + vendor/xml/src/writer/emitter.rs | 483 + vendor/xml/src/writer/events.rs | 289 + 905 files changed, 196317 insertions(+), 8860 deletions(-) create mode 100644 Lib/_pyrepl/content.py create mode 100644 Lib/_pyrepl/fancycompleter.py create mode 100644 Lib/_pyrepl/layout.py create mode 100644 Lib/_pyrepl/render.py create mode 100644 Lib/cProfile.py create mode 100644 Lib/curses/__init__.py create mode 100644 Lib/curses/ascii.py create mode 100644 Lib/curses/has_key.py create mode 100644 Lib/curses/panel.py create mode 100644 Lib/curses/textpad.py rename Lib/ensurepip/_bundled/{pip-26.1.2-py3-none-any.whl => pip-26.2.1-py3-none-any.whl} (57%) create mode 100644 Lib/profiling/__init__.py create mode 100644 Lib/profiling/sampling/__init__.py create mode 100644 Lib/profiling/sampling/__main__.py create mode 100644 Lib/profiling/sampling/_assets/python-logo-only.png create mode 100644 Lib/profiling/sampling/_assets/tachyon-logo.png create mode 100644 Lib/profiling/sampling/_child_monitor.py create mode 100644 Lib/profiling/sampling/_css_utils.py create mode 100644 Lib/profiling/sampling/_flamegraph_assets/flamegraph.css create mode 100644 Lib/profiling/sampling/_flamegraph_assets/flamegraph.js create mode 100644 Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html create mode 100644 Lib/profiling/sampling/_format_utils.py create mode 100644 Lib/profiling/sampling/_heatmap_assets/heatmap.css create mode 100644 Lib/profiling/sampling/_heatmap_assets/heatmap.js create mode 100644 Lib/profiling/sampling/_heatmap_assets/heatmap_index.js create mode 100644 Lib/profiling/sampling/_heatmap_assets/heatmap_index_template.html create mode 100644 Lib/profiling/sampling/_heatmap_assets/heatmap_pyfile_template.html create mode 100644 Lib/profiling/sampling/_heatmap_assets/heatmap_shared.js create mode 100644 Lib/profiling/sampling/_shared_assets/base.css create mode 100644 Lib/profiling/sampling/_shared_assets/base.js create mode 100644 Lib/profiling/sampling/_sync_coordinator.py create mode 100644 Lib/profiling/sampling/_vendor/d3-flame-graph/4.1.3/d3-flamegraph-tooltip.min.js create mode 100644 Lib/profiling/sampling/_vendor/d3-flame-graph/4.1.3/d3-flamegraph.css create mode 100644 Lib/profiling/sampling/_vendor/d3-flame-graph/4.1.3/d3-flamegraph.min.js create mode 100644 Lib/profiling/sampling/_vendor/d3/7.8.5/d3.min.js create mode 100644 Lib/profiling/sampling/binary_collector.py create mode 100644 Lib/profiling/sampling/binary_reader.py create mode 100644 Lib/profiling/sampling/cli.py create mode 100644 Lib/profiling/sampling/collector.py create mode 100644 Lib/profiling/sampling/constants.py create mode 100644 Lib/profiling/sampling/dump.py create mode 100644 Lib/profiling/sampling/errors.py create mode 100644 Lib/profiling/sampling/gecko_collector.py create mode 100644 Lib/profiling/sampling/heatmap_collector.py create mode 100644 Lib/profiling/sampling/jsonl_collector.py create mode 100644 Lib/profiling/sampling/live_collector/__init__.py create mode 100644 Lib/profiling/sampling/live_collector/collector.py create mode 100644 Lib/profiling/sampling/live_collector/constants.py create mode 100644 Lib/profiling/sampling/live_collector/display.py create mode 100644 Lib/profiling/sampling/live_collector/trend_tracker.py create mode 100644 Lib/profiling/sampling/live_collector/widgets.py create mode 100644 Lib/profiling/sampling/module_utils.py create mode 100644 Lib/profiling/sampling/opcode_utils.py create mode 100644 Lib/profiling/sampling/pstats_collector.py create mode 100644 Lib/profiling/sampling/sample.py create mode 100644 Lib/profiling/sampling/stack_collector.py create mode 100644 Lib/profiling/sampling/string_table.py create mode 100644 Lib/profiling/tracing/__init__.py create mode 100644 Lib/profiling/tracing/__main__.py create mode 100644 Lib/profiling/tracing/_utils.py create mode 100644 Lib/site-packages/README.txt create mode 100644 Lib/sqlite3/_completer.py create mode 100644 Lib/test/_crossinterp_definitions.py create mode 100755 Lib/test/bisect_cmd.py create mode 100644 Lib/test/cov.py create mode 100755 Lib/test/curses_tests.py create mode 100644 Lib/test/levenshtein_examples.json create mode 100644 Lib/test/test__interpchannels.py create mode 100644 Lib/test/test__interpreters.py create mode 100644 Lib/test/test_asyncio/test_server_context.py create mode 100644 Lib/test/test_crossinterp.py create mode 100644 Lib/test/test_curses.py create mode 100644 Lib/test/test_dataclasses/_types_proxy.py create mode 100644 Lib/test/test_dataclasses/dataclass_module_3.py create mode 100644 Lib/test/test_dataclasses/dataclass_module_3_str.py create mode 100644 Lib/test/test_dataclasses/dataclass_module_4.py create mode 100644 Lib/test/test_dataclasses/dataclass_module_4_str.py create mode 100644 Lib/test/test_dbm_gnu.py create mode 100644 Lib/test/test_dbm_ndbm.py create mode 100644 Lib/test/test_interpreters/__init__.py create mode 100644 Lib/test/test_interpreters/__main__.py create mode 100644 Lib/test/test_interpreters/test_api.py create mode 100644 Lib/test/test_interpreters/test_channels.py create mode 100644 Lib/test/test_interpreters/test_lifecycle.py create mode 100644 Lib/test/test_interpreters/test_queues.py create mode 100644 Lib/test/test_interpreters/test_stress.py create mode 100644 Lib/test/test_interpreters/utils.py create mode 100644 Lib/test/test_multiprocessing_forkserver/test_preload.py create mode 100644 Lib/test/test_profiling/__init__.py create mode 100644 Lib/test/test_profiling/__main__.py create mode 100644 Lib/test/test_profiling/test_heatmap.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/__init__.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/_live_collector_helpers.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/helpers.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/mocks.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_advanced.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_async.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_blocking.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_children.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_cli.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_collectors.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_dump.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_integration.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_live_collector_core.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_live_collector_interaction.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_live_collector_ui.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_modes.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_profiler.py create mode 100644 Lib/test/test_profiling/test_sampling_profiler/test_trend_tracker.py create mode 100644 Lib/test/test_profiling/test_tracing_profiler.py create mode 100644 Lib/test/test_pyrepl/test_fancycompleter.py create mode 100644 Lib/test/test_pyrepl/test_layout.py create mode 100644 Lib/test/test_pyrepl/test_render.py create mode 100644 Lib/test/test_remote_pdb.py create mode 100644 Lib/test/test_tcl.py create mode 100644 Lib/test/test_tkinter/README create mode 100644 Lib/test/test_tkinter/__init__.py create mode 100644 Lib/test/test_tkinter/__main__.py create mode 100644 Lib/test/test_tkinter/support.py create mode 100644 Lib/test/test_tkinter/test_colorchooser.py create mode 100644 Lib/test/test_tkinter/test_dnd.py create mode 100644 Lib/test/test_tkinter/test_filedialog.py create mode 100644 Lib/test/test_tkinter/test_font.py create mode 100644 Lib/test/test_tkinter/test_geometry_managers.py create mode 100644 Lib/test/test_tkinter/test_images.py create mode 100644 Lib/test/test_tkinter/test_loadtk.py create mode 100644 Lib/test/test_tkinter/test_messagebox.py create mode 100644 Lib/test/test_tkinter/test_misc.py create mode 100644 Lib/test/test_tkinter/test_scrolledtext.py create mode 100644 Lib/test/test_tkinter/test_simpledialog.py create mode 100644 Lib/test/test_tkinter/test_text.py create mode 100644 Lib/test/test_tkinter/test_tkinter_pipe.py create mode 100644 Lib/test/test_tkinter/test_variables.py create mode 100644 Lib/test/test_tkinter/test_widgets.py create mode 100644 Lib/test/test_tkinter/widget_tests.py create mode 100644 Lib/test/test_tomllib/data/valid/inline-table/empty-inline-table.json create mode 100644 Lib/test/test_tomllib/data/valid/inline-table/empty-inline-table.toml create mode 100644 Lib/test/test_tomllib/data/valid/inline-table/multiline-inline-table.json create mode 100644 Lib/test/test_tomllib/data/valid/inline-table/multiline-inline-table.toml create mode 100644 Lib/test/test_tomllib/data/valid/multiline-basic-str/replacements.json create mode 100644 Lib/test/test_tomllib/data/valid/multiline-basic-str/replacements.toml create mode 100644 Lib/test/test_ttk/__init__.py create mode 100644 Lib/test/test_ttk/__main__.py create mode 100644 Lib/test/test_ttk/test_extensions.py create mode 100644 Lib/test/test_ttk/test_style.py create mode 100644 Lib/test/test_ttk/test_widgets.py create mode 100644 Lib/test/test_ttk_textonly.py mode change 100644 => 100755 Lib/test/test_uuid.py create mode 100644 Lib/test/test_xml.py create mode 100644 Lib/xml/utils.py create mode 100644 Lib/zipfile/_path/_functools.py create mode 100644 Parser/Python.asdl create mode 100644 Parser/README.md create mode 100644 Parser/asdl.py create mode 100644 capi-exports.def create mode 100644 crates/capi/src/monitoring.rs create mode 100644 crates/stdlib/src/_remote_debugging/binary.rs create mode 100644 crates/vm/src/stdlib/_lsprof.rs create mode 100644 crates/vm/src/stdlib/_testcapi.rs create mode 100644 crates/vm/src/stdlib/_testcapi/call_buffer.rs create mode 100644 crates/vm/src/stdlib/_testcapi/heap.rs create mode 100644 crates/vm/src/stdlib/_testcapi/meth.rs create mode 100644 crates/vm/src/stdlib/_testcapi/monitoring.rs create mode 100644 crates/vm/src/stdlib/_testcapi/runtime.rs create mode 100644 crates/vm/src/stdlib/_testcapi/threads.rs create mode 100644 crates/vm/src/stdlib/sys/monitoring/capi.rs create mode 100644 crates/vm/src/stdlib/time/pytime.rs create mode 100644 extra_tests/frozendict_contract.py create mode 100644 extra_tests/snippets/import_canonical_callback.py create mode 100644 extra_tests/snippets/import_lazy_native.py create mode 100644 extra_tests/snippets/stdlib_315_api_followups.py create mode 100644 extra_tests/snippets/stdlib_asyncio_reinitialization_315.py create mode 100644 extra_tests/snippets/stdlib_base_n_buffers_315.py create mode 100644 extra_tests/snippets/stdlib_base_n_canonical_cleanup_315.py create mode 100644 extra_tests/snippets/stdlib_bytearray_take_bytes_315.py create mode 100644 extra_tests/snippets/stdlib_bytearray_take_bytes_threads_315.py create mode 100644 extra_tests/snippets/stdlib_codec_registry_315.py create mode 100644 extra_tests/snippets/stdlib_compress_csv_315.py create mode 100644 extra_tests/snippets/stdlib_frozen_mapping_integrations_315.py create mode 100644 extra_tests/snippets/stdlib_generator_states_315.py create mode 100644 extra_tests/snippets/stdlib_json_315.py create mode 100644 extra_tests/snippets/stdlib_lock_precedence_315.py create mode 100644 extra_tests/snippets/stdlib_marshal_code_filename.py create mode 100644 extra_tests/snippets/stdlib_ordered_views_extended_315.py create mode 100644 extra_tests/snippets/stdlib_rlock_constructor_315.py create mode 100644 extra_tests/snippets/stdlib_ssl_introspection.py create mode 100644 extra_tests/snippets/stdlib_venv_cycles.py create mode 100644 extra_tests/snippets/stdlib_venv_nested.py create mode 100644 extra_tests/snippets/stdlib_warning_followups_315.py create mode 100644 extra_tests/snippets/stdlib_warning_module_315.py create mode 100644 extra_tests/snippets/stdlib_weakref_gc_315.py create mode 100644 extra_tests/snippets/stdlib_weakref_gc_threads_315.py create mode 100644 scripts/generate_capi_exports.py create mode 100644 tests/frozendict.rs create mode 100644 vendor/rustpython-ruff_python_parser/.cargo_vcs_info.json create mode 100644 vendor/rustpython-ruff_python_parser/Cargo.toml create mode 100644 vendor/rustpython-ruff_python_parser/Cargo.toml.orig create mode 100644 vendor/rustpython-ruff_python_parser/LICENSE create mode 100644 vendor/rustpython-ruff_python_parser/README.md create mode 100644 vendor/rustpython-ruff_python_parser/README.rustpython.md create mode 100644 vendor/rustpython-ruff_python_parser/UPSTREAM-SHA256.json create mode 100644 vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-concat-comma-diagnostics.patch create mode 100644 vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-fstring-diagnostics.patch create mode 100644 vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-parameter-diagnostics.patch create mode 100644 vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-starred-generator-operands.patch create mode 100644 vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-statement-diagnostics.patch create mode 100644 vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-unpack-diagnostics.patch create mode 100644 vendor/rustpython-ruff_python_parser/src/error.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/lexer.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/lexer/cursor.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/lexer/indentation.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/lexer/interpolated_string.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/lib.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/case_diagnostics.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/expression.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/helpers.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/mod.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/options.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/pattern.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/progress.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/recovery.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/scratch_buffer.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax1.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax2.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax3.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_valid_syntax.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__fstring_expr_inner_line_continuation_and_t_string.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__fstring_expr_inner_line_continuation_newline_t_string.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__ipython_escape_commands.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle_fuzzer.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__unicode_aliases.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/statement.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/parser/tests.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/semantic_errors.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__assignment.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_at_root.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_indentation.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_mismatch_indentation.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__bom.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__bom_with_offset.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__bom_with_offset_edge.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__comment_until_mac_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__comment_until_unix_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__comment_until_windows_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__dedent_after_whitespace.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_mac_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_mac_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_unix_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_windows_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_unix_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_windows_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__emoji_identifier.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_fstrings.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_ipython_escape_command.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__escape_unicode_name.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_newline_format_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_prefix.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_lambda_expression.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__indentation_with_mac_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__indentation_with_unix_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__indentation_with_windows_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__invalid_leading_zero_big.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__invalid_leading_zero_small.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_assignment.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_indentation.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_mac_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_unix_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_windows_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_mac_eol_and_eof.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_unix_eol_and_eof.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_windows_eol_and_eof.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_help_end_escape_command.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_empty.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_long.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_single_whitespace.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_whitespace.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_mac_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_unix_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_windows_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_mac_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_unix_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_windows_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__logical_newline_line_comment.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__match_softkeyword_in_notebook.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__multiple_backslash_continuation.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_mac_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_unix_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_windows_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__non_logical_newline_in_string_continuation.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__numbers.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__operators.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_mac_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_unix_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_windows_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tet_too_low_dedent.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_mac_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_unix_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_windows_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_newline_format_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__backspace_alias.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__bell_alias.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__carriage_return_alias.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__character_tabulation_with_justification_alias.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__delete_alias.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__dont_panic_on_8_in_octal_escape.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__double_quoted_byte.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_alias.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_char_in_byte_literal.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_octet.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__form_feed_alias.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__hts_alias.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__invalid_byte_literal_error.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__invalid_unicode_literal.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__invalid_unicode_name_error.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__missing_unicode_lbrace_error.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__missing_unicode_rbrace_error.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1_error.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2_error.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_concat.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_triple_quotes_with_kind.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1_error.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2_error.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3_error.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4_error.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_1.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_2.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1_error.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2_error.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_1.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_2.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__single_quoted_byte.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_mac_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_unix_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_windows_eol.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap create mode 100644 vendor/rustpython-ruff_python_parser/src/string.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/token_set.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/token_source.rs create mode 100644 vendor/rustpython-ruff_python_parser/src/typing.rs create mode 100644 vendor/xml/.cargo_vcs_info.json create mode 100644 vendor/xml/.gitignore create mode 100644 vendor/xml/Cargo.toml create mode 100644 vendor/xml/Cargo.toml.orig create mode 100644 vendor/xml/LICENSE create mode 100644 vendor/xml/README.md create mode 100644 vendor/xml/RUSTPYTHON-PROVENANCE.md create mode 100644 vendor/xml/UPSTREAM-SHA256SUMS create mode 100644 vendor/xml/patches/amplification.patch create mode 100644 vendor/xml/src/attribute.rs create mode 100644 vendor/xml/src/common.rs create mode 100644 vendor/xml/src/escape.rs create mode 100644 vendor/xml/src/lib.rs create mode 100644 vendor/xml/src/macros.rs create mode 100644 vendor/xml/src/name.rs create mode 100644 vendor/xml/src/namespace.rs create mode 100644 vendor/xml/src/reader.rs create mode 100644 vendor/xml/src/reader/amplification.rs create mode 100644 vendor/xml/src/reader/amplification_tests.rs create mode 100644 vendor/xml/src/reader/config.rs create mode 100644 vendor/xml/src/reader/error.rs create mode 100644 vendor/xml/src/reader/events.rs create mode 100644 vendor/xml/src/reader/lexer.rs create mode 100644 vendor/xml/src/reader/parser.rs create mode 100644 vendor/xml/src/reader/parser/attribute_expansion.rs create mode 100644 vendor/xml/src/reader/parser/inside_cdata.rs create mode 100644 vendor/xml/src/reader/parser/inside_closing_tag_name.rs create mode 100644 vendor/xml/src/reader/parser/inside_comment.rs create mode 100644 vendor/xml/src/reader/parser/inside_declaration.rs create mode 100644 vendor/xml/src/reader/parser/inside_doctype.rs create mode 100644 vendor/xml/src/reader/parser/inside_opening_tag.rs create mode 100644 vendor/xml/src/reader/parser/inside_processing_instruction.rs create mode 100644 vendor/xml/src/reader/parser/inside_reference.rs create mode 100644 vendor/xml/src/reader/parser/outside_tag.rs create mode 100644 vendor/xml/src/util.rs create mode 100644 vendor/xml/src/writer.rs create mode 100644 vendor/xml/src/writer/config.rs create mode 100644 vendor/xml/src/writer/emitter.rs create mode 100644 vendor/xml/src/writer/events.rs diff --git a/.cspell.dict/cpython.txt b/.cspell.dict/cpython.txt index 7fc991670d2..f05d15774be 100644 --- a/.cspell.dict/cpython.txt +++ b/.cspell.dict/cpython.txt @@ -211,6 +211,7 @@ prec preds preinitialized pybuilddir +pycfunction pycore PYGC pyinner @@ -311,3 +312,4 @@ xibufferview xstat XXPRIME ENOTCAPABLE +unmarshalled diff --git a/.cspell.dict/python-more.txt b/.cspell.dict/python-more.txt index f2383f15f97..5cc93d84235 100644 --- a/.cspell.dict/python-more.txt +++ b/.cspell.dict/python-more.txt @@ -306,3 +306,4 @@ xmlcharrefreplace xoptions xopts yieldfrom +issubnormal diff --git a/.cspell.dict/rustpython.txt b/.cspell.dict/rustpython.txt index 563b58c248a..10475ce07ac 100644 --- a/.cspell.dict/rustpython.txt +++ b/.cspell.dict/rustpython.txt @@ -40,3 +40,5 @@ rustix struc unscanned zelf +embedders +nocapture diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5762703b146..1380fcf7ce1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -573,19 +573,14 @@ jobs: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - name: Verify the CPython reference environment + run: python -c "import bz2, ctypes, lzma, pathlib, platform, sqlite3, ssl; expected = pathlib.Path('.python-version').read_text().strip(); actual = platform.python_version(); assert actual == expected, (actual, expected)" + - name: Install macOS dependencies uses: ./.github/actions/install-macos-deps with: openssl: true - # Keep features in sync with update-caches.yml CARGO_ARGS. - - name: build rustpython - run: cargo build --locked --release --verbose --features=threading,jit ${{ env.CARGO_ARGS }} - - - name: run snippets - run: python -m pip install -r requirements.txt && pytest -v - working-directory: ./extra_tests - - name: Detect available cores id: cores shell: bash @@ -593,10 +588,42 @@ jobs: cores=$(python -c 'print(__import__("os").process_cpu_count())') echo "cores=${cores}" >> "$GITHUB_OUTPUT" - - name: Run CPython tests + - name: Build production RustPython + id: build_default + run: cargo build --locked --release --verbose --features=threading,jit ${{ env.CARGO_ARGS }} + + - name: Record Windows timestamp compatibility + if: runner.os == 'Windows' + shell: bash + run: | + python - <<'PYTIME' + import json, subprocess, sys + code = """ + import datetime, json, time + rows = [] + for timestamp in (-1, -2147483649, -11644473600, -11644473601, 2**40): + for name in ('localtime', 'gmtime'): + try: + value = getattr(time, name)(timestamp) + result = [list(value), value.tm_zone, value.tm_gmtoff] + except Exception as error: + result = [type(error).__name__, str(error), getattr(error, 'errno', None)] + rows.append([timestamp, name, result]) + print(json.dumps(rows)) + """ + for executable in (sys.executable, 'target/release/rustpython.exe'): + result = subprocess.check_output([executable, '-c', code], text=True) + print(json.dumps({'executable': executable, 'timestamps': json.loads(result)})) + PYTIME + + - name: run snippets + run: python -m pip install -r requirements.txt && python -m pytest -v + working-directory: ./extra_tests + + - name: Run CPython tests (production) run: | # shellcheck disable=SC2086 - target/release/rustpython -u -m test --slow-ci -j "$CORES" $EXTRA_TEST_ARGS $SKIPS + target/release/rustpython -u -m test --slow-ci -j "$CORES" $EXTRA_TEST_ARGS $SKIPS -x test_frame test_inspect test_monitoring timeout-minutes: ${{ matrix.timeout }} shell: bash env: @@ -669,6 +696,28 @@ jobs: shell: bash run: python -I scripts/whats_left.py --rustpython target/release/rustpython + # Run all production checks before rebuilding the same binary with C API. + # Keep these features in sync with update-caches.yml CARGO_ARGS. + - name: Build RustPython with C API + id: build_capi + if: ${{ !cancelled() && steps.build_default.outcome == 'success' }} + run: cargo build --locked --release --verbose --features=threading,jit,capi ${{ env.CARGO_ARGS }} + env: + PYO3_CONFIG_FILE: ${{ github.workspace }}/crates/capi/pyo3-rustpython.config + PYO3_NO_PYTHON: "1" + + # A failed production check must not suppress this cohort or become green. + - name: Run CPython tests (C API) + if: ${{ !cancelled() && steps.build_capi.outcome == 'success' }} + run: | + target/release/rustpython -u -m test --slow-ci -j "$CORES" $EXTRA_TEST_ARGS test_frame test_inspect test_monitoring + timeout-minutes: ${{ matrix.timeout }} + shell: bash + env: + RUSTPYTHON_SKIP_ENV_POLLUTERS: true + CORES: ${{ steps.cores.outputs.cores }} + EXTRA_TEST_ARGS: ${{ join(matrix.extra_test_args, ' ') }} + clippy: # Named after the matrix entry rather than left to be named for it: a # generated name lists every value in the entry, so adding or removing one @@ -723,7 +772,12 @@ jobs: continue-on-error: true - name: Clippy - run: cargo clippy --locked --keep-going ${{ env.CARGO_ARGS_MOST_FEATS }} --workspace --all-targets --bins --examples --tests ${{ env.WORKSPACE_EXCLUDES }} -- -Dwarnings + run: cargo clippy --locked --keep-going ${{ env.CARGO_ARGS_MOST_FEATS }} --workspace --exclude rustpython-capi --all-targets --bins --examples --tests ${{ env.WORKSPACE_EXCLUDES }} -- -Dwarnings + + # Load the C-API crate's RustPython-specific PyO3 configuration. + - name: Clippy C API + working-directory: crates/capi + run: cargo clippy --locked --all-targets -- -Dwarnings # macOS/Windows clippy uses the same OS-surface filter as cargo check. # Force with the run:cross-check label. Check names stay clippy (). @@ -786,6 +840,9 @@ jobs: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - name: Verify the CPython reference environment + run: python -c "import bz2, ctypes, lzma, pathlib, platform, sqlite3, ssl; expected = pathlib.Path('.python-version').read_text().strip(); actual = platform.python_version(); assert actual == expected, (actual, expected)" + - uses: dtolnay/rust-toolchain@stable with: components: rustfmt diff --git a/.github/workflows/cron-ci.yaml b/.github/workflows/cron-ci.yaml index 115458ae940..9088241ee9e 100644 --- a/.github/workflows/cron-ci.yaml +++ b/.github/workflows/cron-ci.yaml @@ -17,6 +17,32 @@ env: CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls-aws-lc,jit,host_env,doc,ucd_3_2_0 jobs: + manager-coverage-regression: + name: Verify manager XML-RPC under coverage + runs-on: ubuntu-latest + permissions: + contents: read + # Leave room for the instrumented build and the normal 600-second test limit. + timeout-minutes: 30 + if: ${{ github.repository == 'RustPython/RustPython' || github.event_name != 'schedule' }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: dtolnay/rust-toolchain@stable + + - uses: taiki-e/install-action@4cef1412cce204788f482e778a0b9187f9626a29 # v2.87.21 + with: + tool: cargo-llvm-cov + + # Keep this strict check independent of the full coverage collection below. + - name: Run the canonical manager XML-RPC regression + run: >- + cargo llvm-cov --no-report run --locked -- + -m test -v -u all --timeout 600 --match '*test_remote' + test_multiprocessing_forkserver.test_manager + # codecov collects code coverage data from the rust tests, python snippets and python test suite. # This is done using cargo-llvm-cov, which is a wrapper around llvm-cov. codecov: @@ -112,6 +138,9 @@ jobs: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - name: Verify the CPython reference environment + run: python -c "import bz2, ctypes, lzma, pathlib, platform, sqlite3, ssl; expected = pathlib.Path('.python-version').read_text().strip(); actual = platform.python_version(); assert actual == expected, (actual, expected)" + - name: build rustpython run: cargo build --release --verbose --features sqlite @@ -172,6 +201,12 @@ jobs: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - name: Verify the CPython reference environment + run: python -c "import bz2, ctypes, lzma, pathlib, platform, sqlite3, ssl; expected = pathlib.Path('.python-version').read_text().strip(); actual = platform.python_version(); assert actual == expected, (actual, expected)" + + - name: Make the reference libpython available to benchmark executables + run: echo "LD_LIBRARY_PATH=$(python -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR"))')${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" >> "$GITHUB_ENV" + - run: cargo install cargo-criterion - name: build benchmarks diff --git a/.github/workflows/lib-deps-check.yaml b/.github/workflows/lib-deps-check.yaml index 6f147542ee6..30b17bf98c7 100644 --- a/.github/workflows/lib-deps-check.yaml +++ b/.github/workflows/lib-deps-check.yaml @@ -111,32 +111,80 @@ jobs: - name: Run deps check if: steps.changed-files.outputs.modules != '' - id: deps-check run: | # Run deps for all modules at once - echo "deps_output<> "$GITHUB_OUTPUT" - output=$(python scripts/update_lib deps "${MODULES}" --depth 2 2>&1 || true) - echo "$output" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" + python scripts/update_lib deps "${MODULES}" --depth 2 > "$RUNNER_TEMP/lib-deps-report.md" 2>&1 || true env: MODULES: ${{ steps.changed-files.outputs.modules }} + - name: Prepare dependency report + if: steps.changed-files.outputs.modules != '' + id: deps-comment + shell: python + run: | + from os import environ + from pathlib import Path + + report_path = Path(environ["RUNNER_TEMP"]) / "lib-deps-report.md" + report = report_path.read_text(encoding="utf-8") + if report: + heading = "## 📦 Library Dependencies\n\n" + body = ( + heading + + "The following Lib/ modules were modified. Here are their dependencies:\n\n" + + report + + "\n\n**Legend:**\n" + + "- `[+]` path exists in CPython\n" + + "- `[x]` up-to-date, `[ ]` outdated\n" + ) + report_path.write_text(body, encoding="utf-8") + body_size = len(body.encode("utf-8")) + run_url = ( + f"{environ['GITHUB_SERVER_URL']}/{environ['GITHUB_REPOSITORY']}" + f"/actions/runs/{environ['GITHUB_RUN_ID']}" + ) + + # Job summaries have a 1 MiB limit; keep larger reports as artifacts. + needs_artifact = body_size > 1024 * 1024 + summary = body + if needs_artifact: + summary = ( + heading + + "The dependency report exceeds the job summary limit. " + + "Download the full report from this run's `lib-deps-report` artifact.\n" + ) + with open(environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as fd: + fd.write(summary) + + # Leave room for the action's marker, counting UTF-8 bytes, not characters. + comment = body + if body_size >= 60000: + comment = ( + heading + + "The dependency report is too large for a pull request comment. " + + f"[View the full report in the workflow run]({run_url}#summary).\n" + ) + comment_path = Path(environ["RUNNER_TEMP"]) / "lib-deps-comment.md" + comment_path.write_text(comment, encoding="utf-8") + with open(environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fd: + fd.write("has_report=true\n") + fd.write(f"needs_artifact={str(needs_artifact).lower()}\n") + + - name: Upload oversized dependency report + if: steps.deps-comment.outputs.needs_artifact == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: lib-deps-report + path: ${{ runner.temp }}/lib-deps-report.md + if-no-files-found: error + - name: Post comment - if: steps.deps-check.outputs.deps_output != '' + if: steps.deps-comment.outputs.has_report == 'true' uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: header: lib-deps-check number: ${{ github.event.pull_request.number }} - message: | - ## 📦 Library Dependencies - - The following Lib/ modules were modified. Here are their dependencies: - - ${{ steps.deps-check.outputs.deps_output }} - - **Legend:** - - `[+]` path exists in CPython - - `[x]` up-to-date, `[ ]` outdated + path: ${{ runner.temp }}/lib-deps-comment.md - name: Remove comment if no Lib changes if: steps.changed-files.outputs.modules == '' diff --git a/.github/workflows/update-caches.yml b/.github/workflows/update-caches.yml index ae4e3edb509..60f3b56d85b 100644 --- a/.github/workflows/update-caches.yml +++ b/.github/workflows/update-caches.yml @@ -14,13 +14,15 @@ concurrency: cancel-in-progress: false env: + PYO3_CONFIG_FILE: ${{ github.workspace }}/crates/capi/pyo3-rustpython.config + PYO3_NO_PYTHON: "1" CARGO_INCREMENTAL: 0 CARGO_TERM_COLOR: always CARGO_PROFILE_TEST_DEBUG: 0 CARGO_PROFILE_DEV_DEBUG: 0 CARGO_PROFILE_RELEASE_DEBUG: 0 # Keep feature list in sync with CI's release build in .github/workflows/ci.yaml. - CARGO_ARGS: --workspace --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls-aws-lc,host_env,threading,jit,doc --exclude rustpython_wasm --exclude rustpython-venvlauncher + CARGO_ARGS: --workspace --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls-aws-lc,host_env,threading,jit,capi,doc --exclude rustpython_wasm --exclude rustpython-venvlauncher jobs: build-caches: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a78da8d2e6a..926e069edc4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,6 +38,8 @@ repos: entry: rustfmt language: system types: [rust] + # Keep the audited upstream source bytes and explicit backports intact. + exclude: '^vendor/(rustpython-ruff_python_parser|xml)/' priority: 0 - id: generate-rs-opcode-metadata @@ -67,6 +69,7 @@ repos: hooks: - id: cspell types: [rust] + exclude: '^vendor/(rustpython-ruff_python_parser|xml)/' additional_dependencies: - '@cspell/dict-en_us' - '@cspell/dict-cpp' diff --git a/Cargo.lock b/Cargo.lock index 01c2bc8d576..1543ce33d52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "buffer-redux" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "431a9cc8d7efa49bc326729264537f5e60affce816c66edf434350778c9f4f54" +dependencies = [ + "memchr", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -3939,8 +3948,6 @@ dependencies = [ [[package]] name = "rustpython-ruff_python_parser" version = "0.16.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9729e35cac02839c7a67aaccfea3eba32032f7aeacca2e82ad79e8d7b7fd95" dependencies = [ "bitflags 2.13.2", "bstr", @@ -4186,6 +4193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684" dependencies = [ "bitflags 2.13.2", + "buffer-redux", "cfg-if", "clipboard-win", "home", @@ -5323,8 +5331,6 @@ dependencies = [ [[package]] name = "xml" version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89" [[package]] name = "xz" diff --git a/Cargo.toml b/Cargo.toml index 0b9c6af8d60..3fd55e8823e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rustpython" description = "A python interpreter written in rust." -include = ["LICENSE", "Cargo.toml", "src/**/*.rs"] +include = ["LICENSE", "Cargo.toml", "capi-exports.def", "src/**/*.rs"] version.workspace = true authors.workspace = true edition.workspace = true @@ -101,6 +101,11 @@ required-features = [ [profile.dev.package."*"] opt-level = 3 +# Unoptimized opcode dispatch leaves oversized native frames active during +# imports and callbacks. Keep the evaluator compact without changing dev guards. +[profile.dev.package.rustpython-vm] +opt-level = 3 + [profile.test] opt-level = 3 # https://github.com/rust-lang/rust/issues/92869 @@ -163,11 +168,9 @@ template = "installer-config/installer.wxs" [workspace] resolver = "2" -members = [ - ".", - "crates/*", -] -exclude = ["pymath"] +# The root package is implicit; listing "." would defeat vendor exclusions. +members = ["crates/*"] +exclude = ["pymath", "vendor/rustpython-ruff_python_parser", "vendor/xml"] [workspace.package] version = "0.6.1" @@ -197,7 +200,8 @@ rustpython-wtf8 = { path = "crates/wtf8", version = "0.6.1" } rustpython-doc = { path = "crates/doc", version = "0.6.1" } # Use the RustPython Ruff fork for RustPython public `_ast` metadata. -ruff_python_parser = { package = "rustpython-ruff_python_parser", version = "0.16.10" } +# Keep required parser fixes when these crates are used from another workspace. +ruff_python_parser = { package = "rustpython-ruff_python_parser", path = "vendor/rustpython-ruff_python_parser", version = "0.16.10", features = ["python315-diagnostics"] } ruff_python_ast = { package = "rustpython-ruff_python_ast", version = "0.16.10" } ruff_text_size = { package = "rustpython-ruff_text_size", version = "0.16.10" } ruff_source_file = { package = "rustpython-ruff_source_file", version = "0.16.10" } @@ -288,7 +292,7 @@ rustls-graviola = "0.4" rustls-native-certs = "0.8" rustls-pemfile = "2.2" rustls-webpki = { version = "0.103", default-features = false, features = ["std"] } -rustyline = "18" +rustyline = { version = "18", features = ["buffer-redux"] } serde = { package = "serde_core", version = "1.0.225", default-features = false, features = ["alloc"] } schannel = "0.1.29" scopeguard = "1" @@ -326,7 +330,7 @@ web-sys = "0.3" webpki-roots = "1.0" which = "8" x509-parser = "0.18" -xml = "1.3" +xml = { path = "vendor/xml", version = "1.3" } writeable = "0.6" # Lints diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 996b094377a..0e1d8ccf44a 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -49,7 +49,7 @@ def _f(): pass "Mapping", "MutableMapping", "MappingView", "KeysView", "ItemsView", "ValuesView", "Sequence", "MutableSequence", - "ByteString", "Buffer", + "Buffer", ] # This module has been renamed from collections.abc to _collections_abc to @@ -823,6 +823,7 @@ def __eq__(self, other): __reversed__ = None +Mapping.register(frozendict) Mapping.register(mappingproxy) Mapping.register(framelocalsproxy) @@ -1170,3 +1171,13 @@ def __iadd__(self, values): MutableSequence.register(list) MutableSequence.register(bytearray) + +_deprecated_ByteString = globals().pop("ByteString") + +def __getattr__(attr): + if attr == "ByteString": + import warnings + warnings._deprecated("collections.abc.ByteString", remove=(3, 17)) + globals()["ByteString"] = _deprecated_ByteString + return _deprecated_ByteString + raise AttributeError(f"module 'collections.abc' has no attribute {attr!r}") diff --git a/Lib/_ios_support.py b/Lib/_ios_support.py index 20467a7c2bc..cc51aa50491 100644 --- a/Lib/_ios_support.py +++ b/Lib/_ios_support.py @@ -10,12 +10,15 @@ else: # ctypes is available. Load the ObjC library, and wrap the objc_getClass, # sel_registerName methods - lib = util.find_library("objc") - if lib is None: - # Failed to load the objc library - raise ImportError("ObjC runtime library couldn't be loaded") + # find_library() resolves a system library through the dyld shared cache, + # which needs _dyld_shared_cache_contains_path(); that is unavailable before + # iOS 14, so fall back to the bare name, which dyld resolves by itself. + lib = util.find_library("objc") or "libobjc.dylib" - objc = cdll.LoadLibrary(lib) + try: + objc = cdll.LoadLibrary(lib) + except OSError: + raise ImportError("ObjC runtime library couldn't be loaded") objc.objc_getClass.restype = c_void_p objc.objc_getClass.argtypes = [c_char_p] objc.sel_registerName.restype = c_void_p diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index 2098d0a54ab..17bf5cdc819 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -3,6 +3,7 @@ import importlib import os import pkgutil +import re import sys import token import tokenize @@ -12,21 +13,36 @@ from dataclasses import dataclass from itertools import chain from tokenize import TokenInfo +from .fancycompleter import safe_getattr TYPE_CHECKING = False if TYPE_CHECKING: + from types import ModuleType from typing import Any, Iterable, Iterator, Mapping + from .types import CompletionAction HARDCODED_SUBMODULES = { # Standard library submodules that are not detected by pkgutil.iter_modules # but can be imported, so should be proposed in completion "collections": ["abc"], + "math": ["integer"], "os": ["path"], "xml.parsers.expat": ["errors", "model"], } +AUTO_IMPORT_DENYLIST = { + # Standard library modules/submodules that have import side effects + # and must not be automatically imported to complete attributes + re.compile(r"antigravity"), # Calls webbrowser.open + re.compile(r"idlelib\..+"), # May open IDLE GUI + re.compile(r"test\..+"), # Various side-effects + re.compile(r"this"), # Prints to stdout + re.compile(r"_ios_support"), # Spawns a subprocess + re.compile(r".+\.__main__"), # Should not be imported +} + def make_default_module_completer() -> ModuleCompleter: # Inside pyrepl, __package__ is set to None by default @@ -52,37 +68,73 @@ class ModuleCompleter: def __init__(self, namespace: Mapping[str, Any] | None = None) -> None: self.namespace = namespace or {} self._global_cache: list[pkgutil.ModuleInfo] = [] + self._failed_imports: set[str] = set() self._curr_sys_path: list[str] = sys.path[:] self._stdlib_path = os.path.dirname(importlib.__path__[0]) - def get_completions(self, line: str) -> list[str] | None: - """Return the next possible import completions for 'line'.""" + def get_completions( + self, line: str, *, include_values: bool = True + ) -> tuple[list[str], list[Any], CompletionAction | None] | None: + """Return the next possible import completions for 'line'. + + For attributes completion, if the module to complete from is not + imported, also return an action (prompt + callback to run if the + user press TAB again) to import the module. + + If *include_values* is false, the returned values list is empty and + attribute values are not resolved. + """ result = ImportParser(line).parse() if not result: return None try: - return self.complete(*result) + return self.complete(*result, include_values=include_values) except Exception: # Some unexpected error occurred, make it look like # no completions are available - return [] - - def complete(self, from_name: str | None, name: str | None) -> list[str]: + return [], [], None + + def complete( + self, + from_name: str | None, + name: str | None, + *, + include_values: bool = True, + ) -> tuple[list[str], list[Any], CompletionAction | None]: if from_name is None: # import x.y.z assert name is not None path, prefix = self.get_path_and_prefix(name) modules = self.find_modules(path, prefix) - return [self.format_completion(path, module) for module in modules] + names = [self.format_completion(path, module) for module in modules] + # These are always modules, use dummy values to get the right color + values = [sys] * len(names) if include_values else [] + return names, values, None if name is None: # from x.y.z path, prefix = self.get_path_and_prefix(from_name) modules = self.find_modules(path, prefix) - return [self.format_completion(path, module) for module in modules] + names = [self.format_completion(path, module) for module in modules] + # These are always modules, use dummy values to get the right color + values = [sys] * len(names) if include_values else [] + return names, values, None # from x.y import z - return self.find_modules(from_name, name) + submodules = self.find_modules(from_name, name) + attr_names, attr_module, action = self._find_attributes(from_name, name) + all_names = sorted({*submodules, *attr_names}) + if not include_values: + return all_names, [], action + + # Build values list matching the sorted order: + # submodules use `sys` as a dummy value so they get the 'module' color, + # attributes use their actual value. + attr_map = {} + if attr_module is not None: + attr_map = {n: safe_getattr(attr_module, n) for n in attr_names} + all_values = [attr_map[n] if n in attr_map else sys for n in all_names] + return all_names, all_values, action def find_modules(self, path: str, prefix: str) -> list[str]: """Find all modules under 'path' that start with 'prefix'.""" @@ -100,23 +152,25 @@ def _find_modules(self, path: str, prefix: str) -> list[str]: if self.is_suggestion_match(module.name, prefix)] return sorted(builtin_modules + third_party_modules) - if path.startswith('.'): - # Convert relative path to absolute path - package = self.namespace.get('__package__', '') - path = self.resolve_relative_name(path, package) # type: ignore[assignment] - if path is None: - return [] + path = self._resolve_relative_path(path) # type: ignore[assignment] + if path is None: + return [] modules: Iterable[pkgutil.ModuleInfo] = self.global_cache imported_module = sys.modules.get(path.split('.')[0]) if imported_module: - # Filter modules to those who name and specs match the + # Filter modules to those whose name and specs match the # imported module to avoid invalid suggestions spec = imported_module.__spec__ if spec: + def _safe_find_spec(mod: pkgutil.ModuleInfo) -> bool: + try: + return mod.module_finder.find_spec(mod.name, None) == spec + except Exception: + return False modules = [mod for mod in modules if mod.name == spec.name - and mod.module_finder.find_spec(mod.name, None) == spec] + and _safe_find_spec(mod)] else: modules = [] @@ -141,6 +195,44 @@ def _is_stdlib_module(self, module_info: pkgutil.ModuleInfo) -> bool: return (isinstance(module_info.module_finder, FileFinder) and module_info.module_finder.path == self._stdlib_path) + def find_attributes( + self, path: str, prefix: str + ) -> tuple[list[str], list[Any], CompletionAction | None]: + """Find all attributes of module 'path' that start with 'prefix'.""" + attributes, module, action = self._find_attributes(path, prefix) + if module is not None: + values = [safe_getattr(module, attr) for attr in attributes] + else: + values = [] + return attributes, values, action + + def _find_attributes( + self, path: str, prefix: str + ) -> tuple[list[str], ModuleType | None, CompletionAction | None]: + path = self._resolve_relative_path(path) # type: ignore[assignment] + if path is None: + return [], None, None + + imported_module = sys.modules.get(path) + if not imported_module: + if path in self._failed_imports: # Do not propose to import again + return [], None, None + imported_module = self._maybe_import_module(path) + if not imported_module: + return [], None, self._get_import_completion_action(path) + try: + module_attributes = dir(imported_module) + except Exception: + module_attributes = [] + # Filter out invalid attribute names, such as dashes that cannot be + # imported with 'import'. + names = [ + attr_name for attr_name in module_attributes + if (self.is_suggestion_match(attr_name, prefix) + and attr_name.isidentifier()) + ] + return names, imported_module, None + def is_suggestion_match(self, module_name: str, prefix: str) -> bool: if prefix: return module_name.startswith(prefix) @@ -185,6 +277,13 @@ def format_completion(self, path: str, module: str) -> str: return f'{path}{module}' return f'{path}.{module}' + def _resolve_relative_path(self, path: str) -> str | None: + """Resolve a relative import path to absolute. Returns None if unresolvable.""" + if path.startswith('.'): + package = self.namespace.get('__package__', '') + return self.resolve_relative_name(path, package) + return path + def resolve_relative_name(self, name: str, package: str) -> str | None: """Resolve a relative module name to an absolute name. @@ -209,8 +308,39 @@ def global_cache(self) -> list[pkgutil.ModuleInfo]: if not self._global_cache or self._curr_sys_path != sys.path: self._curr_sys_path = sys.path[:] self._global_cache = list(pkgutil.iter_modules()) + self._failed_imports.clear() # retry on sys.path change return self._global_cache + def _maybe_import_module(self, fqname: str) -> ModuleType | None: + if any(pattern.fullmatch(fqname) for pattern in AUTO_IMPORT_DENYLIST): + # Special-cased modules with known import side-effects + return None + root = fqname.split(".")[0] + mod_info = next((m for m in self.global_cache if m.name == root), None) + if not mod_info or not self._is_stdlib_module(mod_info): + # Only import stdlib modules (no risk of import side-effects) + return None + try: + return importlib.import_module(fqname) + except Exception: + sys.modules.pop(fqname, None) # Clean half-imported module + return None + + def _get_import_completion_action(self, path: str) -> CompletionAction: + prompt = ("[ module not imported, press again to import it " + "and propose attributes ]") + + def _do_import() -> str | None: + try: + importlib.import_module(path) + return None + except Exception as exc: + sys.modules.pop(path, None) # Clean half-imported module + self._failed_imports.add(path) + return f"[ error during import: {exc} ]" + + return (prompt, _do_import) + class ImportParser: """ diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index 10127e58897..e79fbfa6bb0 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -22,6 +22,7 @@ from __future__ import annotations import os import time +from typing import TYPE_CHECKING # Categories of actions: # killing @@ -32,10 +33,11 @@ # finishing # [completion] +from .render import RenderedScreen from .trace import trace # types -if False: +if TYPE_CHECKING: from .historical_reader import HistoricalReader @@ -74,7 +76,7 @@ def kill_range(self, start: int, end: int) -> None: else: r.kill_ring.append(text) r.pos = start - r.dirty = True + r.invalidate_buffer(start) class YankCommand(Command): @@ -125,24 +127,27 @@ def do(self) -> None: r.arg = 10 * r.arg - d else: r.arg = 10 * r.arg + d - r.dirty = True + r.invalidate_prompt() class clear_screen(Command): def do(self) -> None: r = self.reader + trace("command.clear_screen") r.console.clear() - r.dirty = True + r.invalidate_full() class refresh(Command): def do(self) -> None: - self.reader.dirty = True + trace("command.refresh") + self.reader.invalidate_full() class repaint(Command): def do(self) -> None: - self.reader.dirty = True + trace("command.repaint") + self.reader.invalidate_full() self.reader.console.repaint() @@ -208,9 +213,10 @@ def do(self) -> None: repl = len(r.kill_ring[-1]) r.kill_ring.insert(0, r.kill_ring.pop()) t = r.kill_ring[-1] + start = r.pos - repl b[r.pos - repl : r.pos] = t r.pos = r.pos - repl + len(t) - r.dirty = True + r.invalidate_buffer(start) class interrupt(FinishCommand): @@ -242,8 +248,9 @@ def do(self) -> None: r.console.prepare() r.pos = p # r.posxy = 0, 0 # XXX this is invalid - r.dirty = True - r.console.screen = [] + r.invalidate_full() + trace("command.suspend sync_rendered_screen") + r.console.sync_rendered_screen(RenderedScreen.empty(), r.console.posxy) class up(MotionCommand): @@ -369,6 +376,7 @@ class self_insert(EditCommand): def do(self) -> None: r = self.reader text = self.event * r.get_arg() + start = r.pos r.insert(text) if r.paste_mode: data = "" @@ -376,7 +384,7 @@ def do(self) -> None: data += ev.data if data: r.insert(data) - r.last_refresh_cache.invalidated = True + r.invalidate_buffer(start) class insert_nl(EditCommand): @@ -400,20 +408,23 @@ def do(self) -> None: del b[s] b.insert(t, c) r.pos = t - r.dirty = True + r.invalidate_buffer(s) class backspace(EditCommand): def do(self) -> None: r = self.reader b = r.buffer + changed_from: int | None = None for i in range(r.get_arg()): if r.pos > 0: r.pos -= 1 del b[r.pos] - r.dirty = True + changed_from = r.pos if changed_from is None else min(changed_from, r.pos) else: self.reader.error("can't backspace at start") + if changed_from is not None: + r.invalidate_buffer(changed_from) class delete(EditCommand): @@ -431,12 +442,15 @@ def do(self) -> None: r.console.finish() raise EOFError + changed_from: int | None = None for i in range(r.get_arg()): if r.pos != len(b): del b[r.pos] - r.dirty = True + changed_from = r.pos if changed_from is None else min(changed_from, r.pos) else: self.reader.error("end of buffer") + if changed_from is not None: + r.invalidate_buffer(changed_from) class accept(FinishCommand): @@ -450,6 +464,7 @@ def do(self) -> None: with self.reader.suspend(): self.reader.msg = _sitebuiltins._Helper()() # type: ignore[assignment] + self.reader.invalidate_prompt() class invalid_key(Command): @@ -470,22 +485,24 @@ def do(self) -> None: from .pager import get_pager from site import gethistoryfile + # After the pager exits, the screen state is unknown (Unix may + # restore via alternate screen, Windows shows pager output). + # Clear and force a full redraw at the end for consistency. + self.reader.console.clear() + history = os.linesep.join(self.reader.history[:]) self.reader.console.restore() pager = get_pager() pager(history, gethistoryfile()) self.reader.console.prepare() - # We need to copy over the state so that it's consistent between - # console and reader, and console does not overwrite/append stuff - self.reader.console.screen = self.reader.screen.copy() - self.reader.console.posxy = self.reader.cxy + self.reader.invalidate_full() class paste_mode(Command): def do(self) -> None: self.reader.paste_mode = not self.reader.paste_mode - self.reader.dirty = True + self.reader.invalidate_prompt() class perform_bracketed_paste(Command): @@ -502,4 +519,3 @@ def do(self) -> None: s=time.time() - start, ) self.reader.insert(data.replace(done, "")) - self.reader.last_refresh_cache.invalidated = True diff --git a/Lib/_pyrepl/completing_reader.py b/Lib/_pyrepl/completing_reader.py index 9d2d43be514..f783e8db36b 100644 --- a/Lib/_pyrepl/completing_reader.py +++ b/Lib/_pyrepl/completing_reader.py @@ -21,16 +21,18 @@ from __future__ import annotations from dataclasses import dataclass, field +from typing import TYPE_CHECKING import re from . import commands, console, reader +from .render import RenderLine, ScreenOverlay from .reader import Reader # types Command = commands.Command -if False: - from .types import KeySpec, CommandName +if TYPE_CHECKING: + from .types import CommandName, CompletionAction, Keymap, KeySpec def prefix(wordlist: list[str], j: int = 0) -> str: @@ -168,39 +170,68 @@ def do(self) -> None: r: CompletingReader r = self.reader # type: ignore[assignment] last_is_completer = r.last_command_is(self.__class__) + if r.cmpltn_action: + if last_is_completer: # double-tab: execute action + msg = r.cmpltn_action[1]() + r.cmpltn_action = None # consumed + if msg: + r.msg = msg + r.cmpltn_message_visible = True + r.invalidate_message() + else: # other input since last tab: cancel action + r.cmpltn_action = None + immutable_completions = r.assume_immutable_completions completions_unchangable = last_is_completer and immutable_completions stem = r.get_stem() if not completions_unchangable: - r.cmpltn_menu_choices = r.get_completions(stem) + r.cmpltn_menu_choices, r.cmpltn_action = r.get_completions(stem) completions = r.cmpltn_menu_choices if not completions: - r.error("no matches") + if not r.cmpltn_action: + r.error("no matches") elif len(completions) == 1: - if completions_unchangable and len(completions[0]) == len(stem): + completion = stripcolor(completions[0]) + if completions_unchangable and len(completion) == len(stem): r.msg = "[ sole completion ]" - r.dirty = True - r.insert(completions[0][len(stem):]) + r.cmpltn_message_visible = True + r.invalidate_message() + r.insert(completion[len(stem):]) else: - p = prefix(completions, len(stem)) + clean_completions = [stripcolor(word) for word in completions] + p = prefix(clean_completions, len(stem)) if p: r.insert(p) if last_is_completer: r.cmpltn_menu_visible = True - r.cmpltn_message_visible = False r.cmpltn_menu, r.cmpltn_menu_end = build_menu( r.console, completions, r.cmpltn_menu_end, r.use_brackets, r.sort_in_column) - r.dirty = True + if r.msg: + r.msg = "" + r.cmpltn_message_visible = False + r.invalidate_message() + r.invalidate_overlay() elif not r.cmpltn_menu_visible: - r.cmpltn_message_visible = True - if stem + p in completions: + if stem + p in clean_completions: r.msg = "[ complete but not unique ]" - r.dirty = True + r.cmpltn_message_visible = True + r.invalidate_message() else: r.msg = "[ not unique ]" - r.dirty = True + r.cmpltn_message_visible = True + r.invalidate_message() + + if r.cmpltn_action: + if r.msg and r.cmpltn_message_visible: + # There is already a message (eg. [ not unique ]) that + # would conflict for next tab: cancel action + r.cmpltn_action = None + else: + r.msg = r.cmpltn_action[0] + r.cmpltn_message_visible = True + r.invalidate_message() class self_insert(commands.self_insert): @@ -215,11 +246,12 @@ def do(self) -> None: r.cmpltn_reset() else: completions = [w for w in r.cmpltn_menu_choices - if w.startswith(stem)] + if stripcolor(w).startswith(stem)] if completions: r.cmpltn_menu, r.cmpltn_menu_end = build_menu( r.console, completions, 0, r.use_brackets, r.sort_in_column) + r.invalidate_overlay() else: r.cmpltn_reset() @@ -240,6 +272,7 @@ class CompletingReader(Reader): cmpltn_message_visible: bool = field(init=False) cmpltn_menu_end: int = field(init=False) cmpltn_menu_choices: list[str] = field(init=False) + cmpltn_action: CompletionAction | None = field(init=False) def __post_init__(self) -> None: super().__post_init__() @@ -248,7 +281,7 @@ def __post_init__(self) -> None: self.commands[c.__name__] = c self.commands[c.__name__.replace('_', '-')] = c - def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]: + def collect_keymap(self) -> Keymap: return super().collect_keymap() + ( (r'\t', 'complete'),) @@ -257,30 +290,30 @@ def after_command(self, cmd: Command) -> None: if not isinstance(cmd, (complete, self_insert)): self.cmpltn_reset() - def calc_screen(self) -> list[str]: - screen = super().calc_screen() - if self.cmpltn_menu_visible: - # We display the completions menu below the current prompt - ly = self.lxy[1] + 1 - screen[ly:ly] = self.cmpltn_menu - # If we're not in the middle of multiline edit, don't append to screeninfo - # since that screws up the position calculation in pos2xy function. - # This is a hack to prevent the cursor jumping - # into the completions menu when pressing left or down arrow. - if self.pos != len(self.buffer): - self.screeninfo[ly:ly] = [(0, [])]*len(self.cmpltn_menu) - return screen + def get_screen_overlays(self) -> tuple[ScreenOverlay, ...]: + if not self.cmpltn_menu_visible: + return () + return ( + ScreenOverlay( + self.lxy[1] + 1, + tuple(RenderLine.from_rendered_text(line) for line in self.cmpltn_menu), + insert=True, + ), + ) def finish(self) -> None: super().finish() self.cmpltn_reset() def cmpltn_reset(self) -> None: + if getattr(self, "cmpltn_menu_visible", False): + self.invalidate_overlay() self.cmpltn_menu = [] self.cmpltn_menu_visible = False self.cmpltn_message_visible = False self.cmpltn_menu_end = 0 self.cmpltn_menu_choices = [] + self.cmpltn_action = None def get_stem(self) -> str: st = self.syntax_table @@ -291,8 +324,8 @@ def get_stem(self) -> str: p -= 1 return ''.join(b[p+1:self.pos]) - def get_completions(self, stem: str) -> list[str]: - return [] + def get_completions(self, stem: str) -> tuple[list[str], CompletionAction | None]: + return [], None def get_line(self) -> str: """Return the current line until the cursor position.""" diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 8956fb1242e..2a53d5ff581 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -19,22 +19,25 @@ from __future__ import annotations +import os import _colorize from abc import ABC, abstractmethod import ast import code import linecache -from dataclasses import dataclass, field -import os.path +from dataclasses import dataclass +import re import sys +from typing import TYPE_CHECKING - -TYPE_CHECKING = False +from .render import RenderedScreen +from .trace import trace if TYPE_CHECKING: - from typing import IO - from typing import Callable + from typing import Callable, IO + + from .types import CursorXY @dataclass @@ -46,10 +49,17 @@ class Event: @dataclass class Console(ABC): - posxy: tuple[int, int] - screen: list[str] = field(default_factory=list) + posxy: CursorXY = (0, 0) height: int = 25 width: int = 80 + _redraw_debug_palette: tuple[str, ...] = ( + "\x1b[41m", + "\x1b[42m", + "\x1b[43m", + "\x1b[44m", + "\x1b[45m", + "\x1b[46m", + ) def __init__( self, @@ -70,8 +80,52 @@ def __init__( else: self.output_fd = f_out.fileno() + self.posxy = (0, 0) + self.height = 25 + self.width = 80 + self._rendered_screen = RenderedScreen.empty() + self._redraw_visual_cycle = 0 + + @property + def screen(self) -> list[str]: + return list(self._rendered_screen.screen_lines) + + def sync_rendered_screen( + self, + rendered_screen: RenderedScreen, + posxy: CursorXY | None = None, + ) -> None: + if posxy is None: + posxy = rendered_screen.cursor + self.posxy = posxy + self._rendered_screen = rendered_screen + trace( + "console.sync_rendered_screen lines={lines} cursor={cursor}", + lines=len(rendered_screen.composed_lines), + cursor=posxy, + ) + + def invalidate_render_state(self) -> None: + self._rendered_screen = RenderedScreen.empty() + trace("console.invalidate_render_state") + + def begin_redraw_visualization(self) -> str | None: + if "PYREPL_VISUALIZE_REDRAWS" not in os.environ: + return None + + palette = self._redraw_debug_palette + cycle = self._redraw_visual_cycle + style = palette[cycle % len(palette)] + self._redraw_visual_cycle = cycle + 1 + trace( + "console.begin_redraw_visualization cycle={cycle} style={style!r}", + cycle=cycle, + style=style, + ) + return style + @abstractmethod - def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: ... + def refresh(self, rendered_screen: RenderedScreen) -> None: ... @abstractmethod def prepare(self) -> None: ... @@ -195,7 +249,19 @@ def runsource(self, source, filename="", symbol="single"): ast.PyCF_ONLY_AST, incomplete_input=False, ) - except (SyntaxError, OverflowError, ValueError): + except SyntaxError as e: + # If it looks like pip install was entered (a common beginner + # mistake), provide a hint to use the system command prompt. + if re.match(r"^\s*(pip3?|py(thon3?)? -m pip) install.*", source): + e.add_note( + "The Python package manager (pip) can only be used" + " outside of the Python REPL.\n" + "Try the 'pip' command in a separate terminal or" + " command prompt." + ) + self.showsyntaxerror(filename, source=source) + return False + except (OverflowError, ValueError): self.showsyntaxerror(filename, source=source) return False if tree.body: diff --git a/Lib/_pyrepl/content.py b/Lib/_pyrepl/content.py new file mode 100644 index 00000000000..3cb22fee84b --- /dev/null +++ b/Lib/_pyrepl/content.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from .utils import ColorSpan, StyleRef, THEME, iter_display_chars, unbracket, wlen + + +@dataclass(frozen=True, slots=True) +class ContentFragment: + """A single display character with its visual width and style. + + The body of ``>>> def greet`` becomes one fragment per character:: + + d e f g r e e t + ╰──┴──╯ ╰──┴──┴──┴──╯ + keyword (unstyled) + + e.g. ``ContentFragment("d", 1, StyleRef(tag="keyword"))``. + """ + + text: str + width: int + style: StyleRef = StyleRef() + + +@dataclass(frozen=True, slots=True) +class PromptContent: + """The prompt split into leading full-width lines and an inline portion. + + For the common ``">>> "`` prompt (no newlines):: + + >>> def greet(name): + ╰─╯ + text=">>> ", width=4, leading_lines=() + + If ``sys.ps1`` contains newlines, e.g. ``"Python 3.13\\n>>> "``:: + + Python 3.13 ← leading_lines[0] + >>> def greet(name): + ╰─╯ + text=">>> ", width=4 + """ + + leading_lines: tuple[ContentFragment, ...] + text: str + width: int + + +@dataclass(frozen=True, slots=True) +class SourceLine: + """One logical line from the editor buffer, before styling. + + Given this two-line input in the REPL:: + + >>> def greet(name): + ... return name + ▲ cursor + + The buffer ``"def greet(name):\\n return name"`` yields:: + + SourceLine(lineno=0, text="def greet(name):", + start_offset=0, has_newline=True) + SourceLine(lineno=1, text=" return name", + start_offset=17, cursor_index=14) + """ + + lineno: int + text: str + start_offset: int + has_newline: bool + cursor_index: int | None = None + + @property + def cursor_on_line(self) -> bool: + return self.cursor_index is not None + + +@dataclass(frozen=True, slots=True) +class ContentLine: + """A logical line paired with its prompt and styled body. + + For ``>>> def greet(name):``:: + + >>> def greet(name): + ╰─╯ ╰──────────────╯ + prompt body: one ContentFragment per character + """ + + source: SourceLine + prompt: PromptContent + body: tuple[ContentFragment, ...] + + +def process_prompt(prompt: str) -> PromptContent: + r"""Return prompt content with width measured without zero-width markup.""" + + prompt_text = unbracket(prompt, including_content=False) + visible_prompt = unbracket(prompt, including_content=True) + leading_lines: list[ContentFragment] = [] + + while "\n" in prompt_text: + leading_text, _, prompt_text = prompt_text.partition("\n") + visible_leading, _, visible_prompt = visible_prompt.partition("\n") + leading_lines.append(ContentFragment(leading_text, wlen(visible_leading))) + + return PromptContent(tuple(leading_lines), prompt_text, wlen(visible_prompt)) + + +def build_body_fragments( + buffer: str, + colors: list[ColorSpan] | None, + start_index: int, +) -> tuple[ContentFragment, ...]: + """Convert a line's text into styled content fragments.""" + # Two separate loops to avoid the THEME() call in the common uncolored path. + if colors is None: + return tuple( + ContentFragment( + styled_char.text, + styled_char.width, + StyleRef(), + ) + for styled_char in iter_display_chars(buffer, colors, start_index) + ) + + theme = THEME() + return tuple( + ContentFragment( + styled_char.text, + styled_char.width, + StyleRef.from_tag(styled_char.tag, theme[styled_char.tag]) + if styled_char.tag + else StyleRef(), + ) + for styled_char in iter_display_chars(buffer, colors, start_index) + ) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py new file mode 100644 index 00000000000..ac4f0afdbc7 --- /dev/null +++ b/Lib/_pyrepl/fancycompleter.py @@ -0,0 +1,213 @@ +# Copyright 2010-2025 Antonio Cuni +# Daniel Hahler +# +# All Rights Reserved +"""Colorful tab completion for Python prompt""" +from __future__ import annotations + +from _colorize import ANSIColors, get_colors, get_theme +import rlcompleter +import keyword +import types + +TYPE_CHECKING = False + +if TYPE_CHECKING: + from typing import Any + from _colorize import Theme + + +def safe_getattr(obj, name): + # Mirror rlcompleter's safeguards so completion does not + # call properties or reify lazy module attributes. + if isinstance(getattr(type(obj), name, None), property): + return None + if (isinstance(obj, types.ModuleType) + and isinstance(obj.__dict__.get(name), types.LazyImportType) + ): + return obj.__dict__.get(name) + return getattr(obj, name, None) + + +def colorize_matches(names: list[str], values: list[Any], theme: Theme) -> list[str]: + return [ + _color_for_obj(name, obj, theme) + for name, obj in zip(names, values) + ] + +def _color_for_obj(name: str, value: Any, theme: Theme) -> str: + t = type(value) + color = _color_by_type(t, theme) + return f"{color}{name}{ANSIColors.RESET}" + + +def _color_by_type(t, theme): + typename = t.__name__ + # this is needed e.g. to turn method-wrapper into method_wrapper, + # because if we want _colorize.FancyCompleter to be "dataclassable" + # our keys need to be valid identifiers. + typename = typename.replace('-', '_').replace('.', '_') + return getattr(theme.fancycompleter, typename, ANSIColors.RESET) + + +class Completer(rlcompleter.Completer): + """ + When doing something like a.b., keep the full a.b.attr completion + stem so readline-style completion can keep refining the menu as you type. + + Optionally, display the various completions in different colors + depending on the type. + """ + def __init__( + self, + namespace=None, + *, + use_colors='auto', + consider_getitems=True, + ): + from _pyrepl import readline + rlcompleter.Completer.__init__(self, namespace) + if use_colors == 'auto': + # use colors only if we can + use_colors = get_colors().RED != "" + self.use_colors = use_colors + self.consider_getitems = consider_getitems + + if self.use_colors: + # In GNU readline, this prevents escaping of ANSI control + # characters in completion results. pyrepl's parse_and_bind() + # is a no-op, but pyrepl handles ANSI sequences natively + # via real_len()/stripcolor(). + readline.parse_and_bind('set dont-escape-ctrl-chars on') + self.theme = get_theme() + else: + self.theme = None + + if self.consider_getitems: + delims = readline.get_completer_delims() + delims = delims.replace('[', '') + delims = delims.replace(']', '') + readline.set_completer_delims(delims) + + def complete(self, text, state): + # if you press at the beginning of a line, insert an actual + # \t. Else, trigger completion. + if text == "": + return ('\t', None)[state] + else: + return rlcompleter.Completer.complete(self, text, state) + + def _callable_postfix(self, val, word): + # disable automatic insertion of '(' for global callables + return word + + def _callable_attr_postfix(self, val, word): + return rlcompleter.Completer._callable_postfix(self, val, word) + + def global_matches(self, text): + names = rlcompleter.Completer.global_matches(self, text) + prefix = commonprefix(names) + if prefix and prefix != text: + return [prefix] + + names.sort() + values = [] + for name in names: + clean_name = name.rstrip(': ') + if keyword.iskeyword(clean_name) or keyword.issoftkeyword(clean_name): + values.append(None) + else: + try: + values.append(eval(name, self.namespace)) + except Exception: + values.append(None) + if self.use_colors and names: + return self.colorize_matches(names, values) + return names + + def attr_matches(self, text): + try: + expr, attr, names, values = self._attr_matches(text) + except ValueError: + return [] + + if not names: + return [] + + if len(names) == 1: + # No coloring: when returning a single completion, readline + # inserts it directly into the prompt, so ANSI codes would + # appear as literal characters. + return [self._callable_attr_postfix(values[0], f'{expr}.{names[0]}')] + + prefix = commonprefix(names) + if prefix and prefix != attr: + return [f'{expr}.{prefix}'] # autocomplete prefix + + names = [f'{expr}.{name}' for name in names] + if self.use_colors: + return self.colorize_matches(names, values) + return names + + def _attr_matches(self, text): + expr, attr = text.rsplit('.', 1) + if '(' in expr or ')' in expr: # don't call functions + return expr, attr, [], [] + try: + thisobject = eval(expr, self.namespace) + except Exception: + return expr, attr, [], [] + + # get the content of the object, except __builtins__ + words = set(dir(thisobject)) - {'__builtins__'} + + if hasattr(thisobject, '__class__'): + words.add('__class__') + words.update(rlcompleter.get_class_members(thisobject.__class__)) + names = [] + values = [] + n = len(attr) + if attr == '': + noprefix = '_' + elif attr == '_': + noprefix = '__' + else: + noprefix = None + + # sort the words now to make sure to return completions in + # alphabetical order. It's easier to do it now, else we would need to + # sort 'names' later but make sure that 'values' in kept in sync, + # which is annoying. + words = sorted(words) + while True: + for word in words: + if ( + word[:n] == attr + and not (noprefix and word[:n+1] == noprefix) + ): + value = safe_getattr(thisobject, word) + names.append(word) + values.append(value) + if names or not noprefix: + break + if noprefix == '_': + noprefix = '__' + else: + noprefix = None + + return expr, attr, names, values + + def colorize_matches(self, names, values): + return colorize_matches(names, values, self.theme) + + +def commonprefix(names): + """Return the common prefix of all 'names'""" + if not names: + return '' + s1 = min(names) + s2 = max(names) + for i, c in enumerate(s1): + if c != s2[i]: + return s1[:i] + return s1 diff --git a/Lib/_pyrepl/historical_reader.py b/Lib/_pyrepl/historical_reader.py index c4b95fa2e81..09b969d80bc 100644 --- a/Lib/_pyrepl/historical_reader.py +++ b/Lib/_pyrepl/historical_reader.py @@ -90,7 +90,7 @@ def do(self) -> None: if r.get_unicode() != r.history[r.historyi]: r.buffer = list(r.history[r.historyi]) r.pos = len(r.buffer) - r.dirty = True + r.invalidate_buffer(0) class first_history(commands.Command): @@ -130,10 +130,11 @@ def do(self) -> None: o = len(r.yank_arg_yanked) else: o = 0 + start = r.pos - o b[r.pos - o : r.pos] = list(w) r.yank_arg_yanked = w r.pos += len(w) - o - r.dirty = True + r.invalidate_buffer(start) class forward_history_isearch(commands.Command): @@ -142,7 +143,7 @@ def do(self) -> None: r.isearch_direction = ISEARCH_DIRECTION_FORWARDS r.isearch_start = r.historyi, r.pos r.isearch_term = "" - r.dirty = True + r.invalidate_prompt() r.push_input_trans(r.isearch_trans) @@ -150,7 +151,7 @@ class reverse_history_isearch(commands.Command): def do(self) -> None: r = self.reader r.isearch_direction = ISEARCH_DIRECTION_BACKWARDS - r.dirty = True + r.invalidate_prompt() r.isearch_term = "" r.push_input_trans(r.isearch_trans) r.isearch_start = r.historyi, r.pos @@ -163,7 +164,7 @@ def do(self) -> None: r.pop_input_trans() r.select_item(r.isearch_start[0]) r.pos = r.isearch_start[1] - r.dirty = True + r.invalidate_prompt() class isearch_add_character(commands.Command): @@ -171,7 +172,7 @@ def do(self) -> None: r = self.reader b = r.buffer r.isearch_term += self.event[-1] - r.dirty = True + r.invalidate_prompt() p = r.pos + len(r.isearch_term) - 1 if b[p : p + 1] != [r.isearch_term[-1]]: r.isearch_next() @@ -182,7 +183,7 @@ def do(self) -> None: r = self.reader if len(r.isearch_term) > 0: r.isearch_term = r.isearch_term[:-1] - r.dirty = True + r.invalidate_prompt() else: r.error("nothing to rubout") @@ -207,7 +208,7 @@ def do(self) -> None: r.isearch_direction = ISEARCH_DIRECTION_NONE r.console.forgetinput() r.pop_input_trans() - r.dirty = True + r.invalidate_prompt() @dataclass @@ -241,7 +242,6 @@ def __post_init__(self) -> None: isearch_end, isearch_add_character, isearch_cancel, - isearch_add_character, isearch_backspace, isearch_forwards, isearch_backwards, @@ -279,8 +279,7 @@ def select_item(self, i: int) -> None: self.buffer = list(buf) self.historyi = i self.pos = len(self.buffer) - self.dirty = True - self.last_refresh_cache.invalidated = True + self.invalidate_buffer(0) def get_item(self, i: int) -> str: if i != len(self.history): @@ -358,7 +357,7 @@ def search_next(self, *, forwards: bool) -> None: if forwards and not match_prefix: self.pos = 0 self.buffer = [] - self.dirty = True + self.invalidate_buffer(0) else: self.error("not found") return diff --git a/Lib/_pyrepl/input.py b/Lib/_pyrepl/input.py index 21c24eb5cde..2d65246c700 100644 --- a/Lib/_pyrepl/input.py +++ b/Lib/_pyrepl/input.py @@ -38,10 +38,11 @@ from abc import ABC, abstractmethod import unicodedata from collections import deque +from typing import TYPE_CHECKING # types -if False: +if TYPE_CHECKING: from .types import EventTuple diff --git a/Lib/_pyrepl/layout.py b/Lib/_pyrepl/layout.py new file mode 100644 index 00000000000..6d854d1142d --- /dev/null +++ b/Lib/_pyrepl/layout.py @@ -0,0 +1,268 @@ +"""Wrap content lines to the terminal width before rendering.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Self + +from .content import ContentFragment, ContentLine +from .types import CursorXY, ScreenInfoRow + + +@dataclass(frozen=True, slots=True) +class LayoutRow: + """Metadata for one physical screen row. + + For the row ``>>> def greet(name):``:: + + >>> def greet(name): + ╰─╯ ╰──────────────╯ + 4 char_widths=(1,1,1,…) ← 16 entries + buffer_advance=17 ← includes the newline + """ + + prompt_width: int + char_widths: tuple[int, ...] + suffix_width: int = 0 + buffer_advance: int = 0 + + @property + def width(self) -> int: + return self.prompt_width + sum(self.char_widths) + self.suffix_width + + @property + def screeninfo(self) -> ScreenInfoRow: + widths = list(self.char_widths) + if self.suffix_width: + widths.append(self.suffix_width) + return self.prompt_width, widths + + +@dataclass(frozen=True, slots=True) +class LayoutMap: + """Mapping between buffer positions and screen coordinates. + + Single source of truth for cursor placement. Given:: + + >>> def greet(name): ← row 0, buffer_advance=17 + ... return name ← row 1, buffer_advance=15 + ▲cursor + + ``pos_to_xy(31)`` → ``(18, 1)``: prompt width 4 + 14 body chars. + """ + rows: tuple[LayoutRow, ...] + + @classmethod + def empty(cls) -> Self: + return cls((LayoutRow(0, ()),)) + + @property + def screeninfo(self) -> list[ScreenInfoRow]: + return [row.screeninfo for row in self.rows] + + def max_column(self, y: int) -> int: + return self.rows[y].width + + def max_row(self) -> int: + return len(self.rows) - 1 + + def pos_to_xy(self, pos: int) -> CursorXY: + if not self.rows: + return 0, 0 + + remaining = pos + for y, row in enumerate(self.rows): + if remaining <= len(row.char_widths): + # Prompt-only leading rows are terminal scenery, not real + # buffer positions. Treating them as real just manufactures + # bugs. + if remaining == 0 and not row.char_widths and row.buffer_advance == 0 and y < len(self.rows) - 1: + continue + x = row.prompt_width + for width in row.char_widths[:remaining]: + x += width + return x, y + remaining -= row.buffer_advance + last_row = self.rows[-1] + return last_row.width - last_row.suffix_width, len(self.rows) - 1 + + def xy_to_pos(self, x: int, y: int) -> int: + if not self.rows: + return 0 + + pos = 0 + for row in self.rows[:y]: + pos += row.buffer_advance + + row = self.rows[y] + cur_x = row.prompt_width + char_widths = row.char_widths + i = 0 + for i, width in enumerate(char_widths): + if cur_x >= x: + # Include trailing zero-width (combining) chars at this position + for trailing_width in char_widths[i:]: + if trailing_width == 0: + pos += 1 + else: + break + return pos + if width == 0: + pos += 1 + continue + cur_x += width + pos += 1 + return pos + + +@dataclass(frozen=True, slots=True) +class WrappedRow: + """One physical screen row after wrapping, ready for rendering. + + When a line overflows the terminal width, it splits into + multiple rows with a ``\\`` continuation marker:: + + >>> x = "a very long li\\ ← suffix="\\", suffix_width=1 + ne that wraps" ← prompt_text="" (continuation) + """ + prompt_text: str = "" + prompt_width: int = 0 + fragments: tuple[ContentFragment, ...] = () + layout_widths: tuple[int, ...] = () + suffix: str = "" + suffix_width: int = 0 + buffer_advance: int = 0 + + +@dataclass(frozen=True, slots=True) +class LayoutResult: + wrapped_rows: tuple[WrappedRow, ...] + layout_map: LayoutMap + line_end_offsets: tuple[int, ...] + + +def layout_content_lines( + lines: tuple[ContentLine, ...], + width: int, + start_offset: int, +) -> LayoutResult: + """Wrap content lines to fit *width* columns. + + A short line passes through as one ``WrappedRow``; a long line is + split at the column boundary with ``\\`` markers:: + + >>> short = 1 ← one WrappedRow + >>> x = "a long stri\\ ← two WrappedRows, first has suffix="\\" + ng" + """ + if width <= 0: + return LayoutResult((), LayoutMap(()), ()) + + offset = start_offset + wrapped_rows: list[WrappedRow] = [] + layout_rows: list[LayoutRow] = [] + line_end_offsets: list[int] = [] + + for line in lines: + newline_advance = int(line.source.has_newline) + for leading in line.prompt.leading_lines: + line_end_offsets.append(offset) + wrapped_rows.append( + WrappedRow( + fragments=(leading,), + ) + ) + layout_rows.append(LayoutRow(0, (), buffer_advance=0)) + + prompt_text = line.prompt.text + prompt_width = line.prompt.width + body = tuple(line.body) + body_widths = tuple(fragment.width for fragment in body) + + # Fast path: line fits on one row. + if not body_widths or (sum(body_widths) + prompt_width) < width: + offset += len(body) + newline_advance + line_end_offsets.append(offset) + wrapped_rows.append( + WrappedRow( + prompt_text=prompt_text, + prompt_width=prompt_width, + fragments=body, + layout_widths=body_widths, + buffer_advance=len(body) + newline_advance, + ) + ) + layout_rows.append( + LayoutRow( + prompt_width, + body_widths, + buffer_advance=len(body) + newline_advance, + ) + ) + continue + + # Slow path: line needs wrapping. + current_prompt = prompt_text + current_prompt_width = prompt_width + start = 0 + total = len(body) + while True: + # Find how many characters fit on this row. + index_to_wrap_before = 0 + column = 0 + for char_width in body_widths[start:]: + if column + char_width + current_prompt_width >= width: + break + index_to_wrap_before += 1 + column += char_width + + if index_to_wrap_before == 0 and start < total: + index_to_wrap_before = 1 # force progress + + at_line_end = (start + index_to_wrap_before) >= total + if at_line_end: + offset += index_to_wrap_before + newline_advance + suffix = "" + suffix_width = 0 + buffer_advance = index_to_wrap_before + newline_advance + else: + offset += index_to_wrap_before + suffix = "\\" + suffix_width = 1 + buffer_advance = index_to_wrap_before + + end = start + index_to_wrap_before + row_fragments = body[start:end] + row_widths = body_widths[start:end] + line_end_offsets.append(offset) + wrapped_rows.append( + WrappedRow( + prompt_text=current_prompt, + prompt_width=current_prompt_width, + fragments=row_fragments, + layout_widths=row_widths, + suffix=suffix, + suffix_width=suffix_width, + buffer_advance=buffer_advance, + ) + ) + layout_rows.append( + LayoutRow( + current_prompt_width, + row_widths, + suffix_width=suffix_width, + buffer_advance=buffer_advance, + ) + ) + + start = end + current_prompt = "" + current_prompt_width = 0 + if at_line_end: + break + + return LayoutResult( + tuple(wrapped_rows), + LayoutMap(tuple(layout_rows)), + tuple(line_end_offsets), + ) diff --git a/Lib/_pyrepl/pager.py b/Lib/_pyrepl/pager.py index 1fddc63e3ee..92afaa5933a 100644 --- a/Lib/_pyrepl/pager.py +++ b/Lib/_pyrepl/pager.py @@ -138,7 +138,7 @@ def pipe_pager(text: str, cmd: str, title: str = '') -> None: '.' '?e (END):?pB %pB\\%..' ' (press h for help or q to quit)') - env['LESS'] = '-RmPm{0}$PM{0}$'.format(prompt_string) + env['LESS'] = '-RcmPm{0}$PM{0}$'.format(prompt_string) proc = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, errors='backslashreplace', env=env) assert proc.stdin is not None diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index f0116e742d2..7e4dd801c84 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -25,16 +25,41 @@ import _colorize from contextlib import contextmanager -from dataclasses import dataclass, field, fields +from dataclasses import dataclass, field, fields, replace +from typing import Self from . import commands, console, input -from .utils import wlen, unbracket, disp_str, gen_colors, THEME +from .content import ( + ContentFragment, + ContentLine, + SourceLine, + build_body_fragments, + process_prompt as build_prompt_content, +) +from .layout import LayoutMap, LayoutResult, LayoutRow, WrappedRow, layout_content_lines +from .render import RenderCell, RenderLine, RenderedScreen, ScreenOverlay +from .utils import ANSI_ESCAPE_SEQUENCE, ColorSpan, THEME, StyleRef, wlen, gen_colors from .trace import trace # types Command = commands.Command -from .types import Callback, SimpleContextManager, KeySpec, CommandName +from collections.abc import Callable, Iterator +from .types import ( + Callback, + CommandName, + CursorXY, + Dimensions, + EventData, + KeySpec, + Keymap, + ScreenInfoRow, + SimpleContextManager, +) + +type CommandClass = type[Command] +type CommandInput = tuple[CommandName | CommandClass, EventData] +type PromptCellCacheKey = tuple[str, bool] # syntax classes @@ -52,8 +77,8 @@ def make_default_syntax_table() -> dict[str, int]: return st -def make_default_commands() -> dict[CommandName, type[Command]]: - result: dict[CommandName, type[Command]] = {} +def make_default_commands() -> dict[CommandName, CommandClass]: + result: dict[CommandName, CommandClass] = {} for v in vars(commands).values(): if isinstance(v, type) and issubclass(v, Command) and v.__name__[0].islower(): result[v.__name__] = v @@ -61,7 +86,7 @@ def make_default_commands() -> dict[CommandName, type[Command]]: return result -default_keymap: tuple[tuple[KeySpec, CommandName], ...] = tuple( +default_keymap: Keymap = tuple( [ (r"\C-a", "beginning-of-line"), (r"\C-b", "left"), @@ -131,6 +156,77 @@ def make_default_commands() -> dict[CommandName, type[Command]]: ) +@dataclass(frozen=True, slots=True) +class RefreshInvalidation: + """Which parts of the screen need to be recomputed on the next refresh.""" + + cursor_only: bool = False + buffer_from_pos: int | None = None + prompt: bool = False + layout: bool = False + theme: bool = False + message: bool = False + overlay: bool = False + full: bool = False + + @classmethod + def empty(cls) -> Self: + return cls() + + @property + def needs_screen_refresh(self) -> bool: + return any( + ( + self.buffer_from_pos is not None, + self.prompt, + self.layout, + self.theme, + self.message, + self.overlay, + self.full, + ) + ) + + @property + def is_cursor_only(self) -> bool: + return self.cursor_only and not self.needs_screen_refresh + + @property + def buffer_rebuild_from_pos(self) -> int | None: + if self.full or self.prompt or self.layout or self.theme: + return 0 + return self.buffer_from_pos + + def with_cursor(self) -> Self: + if self.needs_screen_refresh: + return self + return replace(self, cursor_only=True) + + def with_buffer(self, from_pos: int) -> Self: + current = from_pos + if self.buffer_from_pos is not None: + current = min(current, self.buffer_from_pos) + return replace(self, cursor_only=False, buffer_from_pos=current) + + def with_prompt(self) -> Self: + return replace(self, cursor_only=False, prompt=True) + + def with_layout(self) -> Self: + return replace(self, cursor_only=False, layout=True) + + def with_theme(self) -> Self: + return replace(self, cursor_only=False, theme=True) + + def with_message(self) -> Self: + return replace(self, cursor_only=False, message=True) + + def with_overlay(self) -> Self: + return replace(self, cursor_only=False, overlay=True) + + def with_full(self) -> Self: + return replace(self, cursor_only=False, full=True) + + @dataclass(slots=True) class Reader: """The Reader class implements the bare bones of a command reader, @@ -148,10 +244,9 @@ class Reader: * pos: A 0-based index into 'buffer' for where the insertion point is. - * screeninfo: - A list of screen position tuples. Each list element is a tuple - representing information on visible line length for a given line. - Allows for efficient skipping of color escape sequences. + * layout: + A mapping between buffer positions and rendered rows/columns. + It is the internal source of truth for cursor placement. * cxy, lxy: the position of the insertion point in screen ... * syntax_table: @@ -162,8 +257,6 @@ class Reader: * arg: The emacs-style prefix argument. It will be None if no such argument has been provided. - * dirty: - True if we need to refresh the display. * kill_ring: The emacs-style kill-ring; manipulated with yank & yank-pop * ps1, ps2, ps3, ps4: @@ -198,66 +291,89 @@ class Reader: kill_ring: list[list[str]] = field(default_factory=list) msg: str = "" arg: int | None = None - dirty: bool = False finished: bool = False paste_mode: bool = False - commands: dict[str, type[Command]] = field(default_factory=make_default_commands) - last_command: type[Command] | None = None + commands: dict[CommandName, CommandClass] = field(default_factory=make_default_commands) + last_command: CommandClass | None = None syntax_table: dict[str, int] = field(default_factory=make_default_syntax_table) - keymap: tuple[tuple[str, str], ...] = () + keymap: Keymap = () input_trans: input.KeymapTranslator = field(init=False) input_trans_stack: list[input.KeymapTranslator] = field(default_factory=list) - screen: list[str] = field(default_factory=list) - screeninfo: list[tuple[int, list[int]]] = field(init=False) - cxy: tuple[int, int] = field(init=False) - lxy: tuple[int, int] = field(init=False) - scheduled_commands: list[str] = field(default_factory=list) + rendered_screen: RenderedScreen = field(init=False) + layout: LayoutMap = field(init=False) + cxy: CursorXY = field(init=False) + lxy: CursorXY = field(init=False) + scheduled_commands: list[CommandName] = field(default_factory=list) can_colorize: bool = False + gen_colors: Callable[[str], Iterator[ColorSpan]] = gen_colors threading_hook: Callback | None = None + invalidation: RefreshInvalidation = field(init=False) ## cached metadata to speed up screen refreshes @dataclass class RefreshCache: - screen: list[str] = field(default_factory=list) - screeninfo: list[tuple[int, list[int]]] = field(init=False) + """Previously computed render/layout data for incremental refresh.""" + + render_lines: list[RenderLine] = field(default_factory=list) + layout_rows: list[LayoutRow] = field(default_factory=list) line_end_offsets: list[int] = field(default_factory=list) - pos: int = field(init=False) - cxy: tuple[int, int] = field(init=False) - dimensions: tuple[int, int] = field(init=False) - invalidated: bool = False + pos: int = 0 + dimensions: Dimensions = (0, 0) def update_cache(self, reader: Reader, - screen: list[str], - screeninfo: list[tuple[int, list[int]]], + render_lines: list[RenderLine], + layout_rows: list[LayoutRow], + line_end_offsets: list[int], ) -> None: - self.screen = screen.copy() - self.screeninfo = screeninfo.copy() + self.render_lines = render_lines.copy() + self.layout_rows = layout_rows.copy() + self.line_end_offsets = line_end_offsets.copy() self.pos = reader.pos - self.cxy = reader.cxy self.dimensions = reader.console.width, reader.console.height - self.invalidated = False def valid(self, reader: Reader) -> bool: - if self.invalidated: - return False dimensions = reader.console.width, reader.console.height dimensions_changed = dimensions != self.dimensions return not dimensions_changed - def get_cached_location(self, reader: Reader) -> tuple[int, int]: - if self.invalidated: - raise ValueError("Cache is invalidated") - offset = 0 - earliest_common_pos = min(reader.pos, self.pos) + def get_cached_location( + self, + reader: Reader, + buffer_from_pos: int | None = None, + *, + reuse_full: bool = False, + ) -> tuple[int, int]: + """Return (buffer_offset, num_reusable_lines) for incremental refresh. + + Three paths: + - reuse_full (overlay/message-only): reuse all cached lines. + - buffer_from_pos=None (full rebuild): rewind to common cursor pos. + - explicit buffer_from_pos: reuse lines before that position. + """ + if reuse_full: + if self.line_end_offsets: + last_offset = self.line_end_offsets[-1] + if last_offset >= len(reader.buffer): + return last_offset, len(self.line_end_offsets) + return 0, 0 + if buffer_from_pos is None: + buffer_from_pos = min(reader.pos, self.pos) num_common_lines = len(self.line_end_offsets) while num_common_lines > 0: - offset = self.line_end_offsets[num_common_lines - 1] - if earliest_common_pos > offset: + candidate = self.line_end_offsets[num_common_lines - 1] + if buffer_from_pos > candidate: break num_common_lines -= 1 - else: - offset = 0 + # Prompt-only leading rows consume no buffer content. Reusing them + # in isolation causes the next incremental rebuild to emit them a + # second time. + while ( + num_common_lines > 0 + and self.layout_rows[num_common_lines - 1].buffer_advance == 0 + ): + num_common_lines -= 1 + offset = self.line_end_offsets[num_common_lines - 1] if num_common_lines else 0 return offset, num_common_lines last_refresh_cache: RefreshCache = field(default_factory=RefreshCache) @@ -270,123 +386,267 @@ def __post_init__(self) -> None: self.input_trans = input.KeymapTranslator( self.keymap, invalid_cls="invalid-key", character_cls="self-insert" ) - self.screeninfo = [(0, [])] + self.layout = LayoutMap.empty() self.cxy = self.pos2xy() self.lxy = (self.pos, 0) + self.rendered_screen = RenderedScreen.empty() self.can_colorize = _colorize.can_colorize() + self.invalidation = RefreshInvalidation.empty() - self.last_refresh_cache.screeninfo = self.screeninfo + self.last_refresh_cache.layout_rows = list(self.layout.rows) self.last_refresh_cache.pos = self.pos - self.last_refresh_cache.cxy = self.cxy + self.last_refresh_cache.dimensions = (0, 0) - def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]: - return default_keymap + @property + def screen(self) -> list[str]: + return list(self.rendered_screen.screen_lines) - def calc_screen(self) -> list[str]: - """Translate changes in self.buffer into changes in self.console.screen.""" - # Since the last call to calc_screen: - # screen and screeninfo may differ due to a completion menu being shown - # pos and cxy may differ due to edits, cursor movements, or completion menus + @property + def screeninfo(self) -> list[ScreenInfoRow]: + return self.layout.screeninfo + + def collect_keymap(self) -> Keymap: + return default_keymap - # Lines that are above both the old and new cursor position can't have changed, - # unless the terminal has been resized (which might cause reflowing) or we've - # entered or left paste mode (which changes prompts, causing reflowing). + def calc_screen(self) -> RenderedScreen: + """Translate the editable buffer into a base rendered screen.""" num_common_lines = 0 offset = 0 if self.last_refresh_cache.valid(self): - offset, num_common_lines = self.last_refresh_cache.get_cached_location(self) - - screen = self.last_refresh_cache.screen - del screen[num_common_lines:] - - screeninfo = self.last_refresh_cache.screeninfo - del screeninfo[num_common_lines:] - - last_refresh_line_end_offsets = self.last_refresh_cache.line_end_offsets - del last_refresh_line_end_offsets[num_common_lines:] + if ( + self.invalidation.buffer_from_pos is None + and not ( + self.invalidation.full + or self.invalidation.prompt + or self.invalidation.layout + or self.invalidation.theme + ) + and (self.invalidation.message or self.invalidation.overlay) + ): + # Fast path: only overlays or messages changed. + offset, num_common_lines = self.last_refresh_cache.get_cached_location( + self, + reuse_full=True, + ) + assert not self.last_refresh_cache.line_end_offsets or ( + self.last_refresh_cache.line_end_offsets[-1] >= len(self.buffer) + ), "Buffer modified without invalidate_buffer() call" + else: + offset, num_common_lines = self.last_refresh_cache.get_cached_location( + self, + self._buffer_refresh_from_pos(), + ) + + base_render_lines = self.last_refresh_cache.render_lines[:num_common_lines] + layout_rows = self.last_refresh_cache.layout_rows[:num_common_lines] + last_refresh_line_end_offsets = self.last_refresh_cache.line_end_offsets[:num_common_lines] + + source_lines = self._build_source_lines(offset, num_common_lines) + content_lines = self._build_content_lines( + source_lines, + prompt_from_cache=bool(offset and self.buffer[offset - 1] != "\n"), + ) + layout_result = self._layout_content(content_lines, offset) + base_render_lines.extend(self._render_wrapped_rows(layout_result.wrapped_rows)) + layout_rows.extend(layout_result.layout_map.rows) + last_refresh_line_end_offsets.extend(layout_result.line_end_offsets) - pos = self.pos - pos -= offset + self.layout = LayoutMap(tuple(layout_rows)) + self.cxy = self.pos2xy() + if not source_lines: + # reuse_full path: _build_source_lines didn't run, + # so lxy wasn't updated. Derive it from the buffer. + self.lxy = self._compute_lxy() + self.last_refresh_cache.update_cache( + self, + base_render_lines, + layout_rows, + last_refresh_line_end_offsets, + ) + return RenderedScreen(tuple(base_render_lines), self.cxy) - prompt_from_cache = (offset and self.buffer[offset - 1] != "\n") + def _buffer_refresh_from_pos(self) -> int: + """Return buffer position from which to rebuild content. - if self.can_colorize: - colors = list(gen_colors(self.get_unicode())) + Returns 0 (full rebuild) when no incremental position is known. + """ + buffer_from_pos = self.invalidation.buffer_rebuild_from_pos + if buffer_from_pos is not None: + return buffer_from_pos + return 0 + + def _compute_lxy(self) -> CursorXY: + """Derive logical cursor (col, lineno) from the buffer and pos.""" + text = "".join(self.buffer[:self.pos]) + lineno = text.count("\n") + if lineno: + col = self.pos - text.rindex("\n") - 1 else: - colors = None - trace("colors = {colors}", colors=colors) + col = self.pos + return col, lineno + + def _build_source_lines( + self, + offset: int, + first_lineno: int, + ) -> tuple[SourceLine, ...]: + if offset == len(self.buffer) and (offset > 0 or first_lineno > 0): + return () + + pos = self.pos - offset lines = "".join(self.buffer[offset:]).split("\n") cursor_found = False lines_beyond_cursor = 0 - for ln, line in enumerate(lines, num_common_lines): + source_lines: list[SourceLine] = [] + current_offset = offset + + for line_index, line in enumerate(lines): + lineno = first_lineno + line_index + has_newline = line_index < len(lines) - 1 line_len = len(line) + cursor_index: int | None = None if 0 <= pos <= line_len: - self.lxy = pos, ln + cursor_index = pos + self.lxy = pos, lineno cursor_found = True elif cursor_found: lines_beyond_cursor += 1 if lines_beyond_cursor > self.console.height: - # No need to keep formatting lines. - # The console can't show them. break + + source_lines.append( + SourceLine( + lineno=lineno, + text=line, + start_offset=current_offset, + has_newline=has_newline, + cursor_index=cursor_index, + ) + ) + pos -= line_len + 1 + current_offset += line_len + (1 if has_newline else 0) + + return tuple(source_lines) + + def _build_content_lines( + self, + source_lines: tuple[SourceLine, ...], + *, + prompt_from_cache: bool, + ) -> tuple[ContentLine, ...]: + if self.can_colorize: + colors = list(self.gen_colors(self.get_unicode())) + else: + colors = None + trace("colors = {colors}", colors=colors) + + content_lines: list[ContentLine] = [] + for source_line in source_lines: if prompt_from_cache: - # Only the first line's prompt can come from the cache prompt_from_cache = False prompt = "" else: - prompt = self.get_prompt(ln, line_len >= pos >= 0) - while "\n" in prompt: - pre_prompt, _, prompt = prompt.partition("\n") - last_refresh_line_end_offsets.append(offset) - screen.append(pre_prompt) - screeninfo.append((0, [])) - pos -= line_len + 1 - prompt, prompt_len = self.process_prompt(prompt) - chars, char_widths = disp_str(line, colors, offset) - wrapcount = (sum(char_widths) + prompt_len) // self.console.width - if wrapcount == 0 or not char_widths: - offset += line_len + 1 # Takes all of the line plus the newline - last_refresh_line_end_offsets.append(offset) - screen.append(prompt + "".join(chars)) - screeninfo.append((prompt_len, char_widths)) - else: - pre = prompt - prelen = prompt_len - for wrap in range(wrapcount + 1): - index_to_wrap_before = 0 - column = 0 - for char_width in char_widths: - if column + char_width + prelen >= self.console.width: - break - index_to_wrap_before += 1 - column += char_width - if len(chars) > index_to_wrap_before: - offset += index_to_wrap_before - post = "\\" - after = [1] - else: - offset += index_to_wrap_before + 1 # Takes the newline - post = "" - after = [] - last_refresh_line_end_offsets.append(offset) - render = pre + "".join(chars[:index_to_wrap_before]) + post - render_widths = char_widths[:index_to_wrap_before] + after - screen.append(render) - screeninfo.append((prelen, render_widths)) - chars = chars[index_to_wrap_before:] - char_widths = char_widths[index_to_wrap_before:] - pre = "" - prelen = 0 - self.screeninfo = screeninfo - self.cxy = self.pos2xy() - if self.msg: - for mline in self.msg.split("\n"): - screen.append(mline) - screeninfo.append((0, [])) - - self.last_refresh_cache.update_cache(self, screen, screeninfo) - return screen + prompt = self.get_prompt(source_line.lineno, source_line.cursor_on_line) + content_lines.append( + ContentLine( + source=source_line, + prompt=build_prompt_content(prompt), + body=build_body_fragments( + source_line.text, + colors, + source_line.start_offset, + ), + ) + ) + return tuple(content_lines) + + def _layout_content( + self, + content_lines: tuple[ContentLine, ...], + offset: int, + ) -> LayoutResult: + return layout_content_lines(content_lines, self.console.width, offset) + + def _render_wrapped_rows( + self, + wrapped_rows: tuple[WrappedRow, ...], + ) -> list[RenderLine]: + return [ + self._render_line( + row.prompt_text, + row.fragments, + row.suffix, + ) + for row in wrapped_rows + ] + + def _render_message_lines(self) -> tuple[RenderLine, ...]: + if not self.msg: + return () + width = self.console.width + render_lines: list[RenderLine] = [] + for message_line in self.msg.split("\n"): + # If self.msg is larger than console width, make it fit. + # TODO: try to split between words? + if not message_line: + render_lines.append(RenderLine.from_rendered_text("")) + continue + for offset in range(0, len(message_line), width): + render_lines.append( + RenderLine.from_rendered_text(message_line[offset : offset + width]) + ) + return tuple(render_lines) + + def get_screen_overlays(self) -> tuple[ScreenOverlay, ...]: + return () + + def compose_rendered_screen(self, base_screen: RenderedScreen) -> RenderedScreen: + overlays = list(self.get_screen_overlays()) + message_lines = self._render_message_lines() + if message_lines: + overlays.append(ScreenOverlay(len(base_screen.lines), message_lines)) + if not overlays: + return base_screen + return RenderedScreen(base_screen.lines, base_screen.cursor, tuple(overlays)) + + _prompt_cell_cache: dict[PromptCellCacheKey, tuple[RenderCell, ...]] = field( + init=False, default_factory=dict, repr=False + ) + + def _render_line( + self, + prefix: str, + fragments: tuple[ContentFragment, ...], + suffix: str = "", + ) -> RenderLine: + cells: list[RenderCell] = [] + if prefix: + cache_key = (prefix, self.can_colorize) + cached = self._prompt_cell_cache.get(cache_key) + if cached is None: + prompt_cells = RenderLine.from_rendered_text(prefix).cells + if self.can_colorize and prompt_cells and not ANSI_ESCAPE_SEQUENCE.search(prefix): + prompt_style = StyleRef.from_tag("prompt", THEME()["prompt"]) + prompt_cells = tuple( + RenderCell( + cell.text, + cell.width, + style=prompt_style if cell.text else cell.style, + controls=cell.controls, + ) + for cell in prompt_cells + ) + self._prompt_cell_cache[cache_key] = prompt_cells + cached = prompt_cells + cells.extend(cached) + cells.extend( + RenderCell(fragment.text, fragment.width, style=fragment.style) + for fragment in fragments + ) + if suffix: + cells.extend(RenderLine.from_rendered_text(suffix).cells) + return RenderLine.from_cells(cells) @staticmethod def process_prompt(prompt: str) -> tuple[str, int]: @@ -396,9 +656,8 @@ def process_prompt(prompt: str) -> tuple[str, int]: (\x01 and \x02) removed. The length ignores anything between those brackets as well as any ANSI escape sequences. """ - out_prompt = unbracket(prompt, including_content=False) - visible_prompt = unbracket(prompt, including_content=True) - return out_prompt, wlen(visible_prompt) + prompt_content = build_prompt_content(prompt) + return prompt_content.text, prompt_content.width def bow(self, p: int | None = None) -> int: """Return the 0-based index of the word break preceding p most @@ -460,10 +719,10 @@ def eol(self, p: int | None = None) -> int: def max_column(self, y: int) -> int: """Return the last x-offset for line y""" - return self.screeninfo[y][0] + sum(self.screeninfo[y][1]) + return self.layout.max_column(y) def max_row(self) -> int: - return len(self.screeninfo) - 1 + return self.layout.max_row() def get_arg(self, default: int = 1) -> int: """Return any prefix argument that the user has supplied, @@ -489,10 +748,6 @@ def get_prompt(self, lineno: int, cursor_on_line: bool) -> str: prompt = self.ps3 else: prompt = self.ps1 - - if self.can_colorize: - t = THEME() - prompt = f"{t.prompt}{prompt}{t.reset}" return prompt def push_input_trans(self, itrans: input.KeymapTranslator) -> None: @@ -504,65 +759,48 @@ def pop_input_trans(self) -> None: def setpos_from_xy(self, x: int, y: int) -> None: """Set pos according to coordinates x, y""" - pos = 0 - i = 0 - while i < y: - prompt_len, char_widths = self.screeninfo[i] - offset = len(char_widths) - in_wrapped_line = prompt_len + sum(char_widths) >= self.console.width - if in_wrapped_line: - pos += offset - 1 # -1 cause backslash is not in buffer - else: - pos += offset + 1 # +1 cause newline is in buffer - i += 1 - - j = 0 - cur_x = self.screeninfo[i][0] - while cur_x < x: - if self.screeninfo[i][1][j] == 0: - j += 1 # prevent potential future infinite loop - continue - cur_x += self.screeninfo[i][1][j] - j += 1 - pos += 1 - - self.pos = pos + self.pos = self.layout.xy_to_pos(x, y) - def pos2xy(self) -> tuple[int, int]: + def pos2xy(self) -> CursorXY: """Return the x, y coordinates of position 'pos'.""" + assert 0 <= self.pos <= len(self.buffer) + return self.layout.pos_to_xy(self.pos) - prompt_len, y = 0, 0 - char_widths: list[int] = [] - pos = self.pos - assert 0 <= pos <= len(self.buffer) + def insert(self, text: str | list[str]) -> None: + """Insert 'text' at the insertion point.""" + start = self.pos + self.buffer[self.pos : self.pos] = list(text) + self.pos += len(text) + self.invalidate_buffer(start) - # optimize for the common case: typing at the end of the buffer - if pos == len(self.buffer) and len(self.screeninfo) > 0: - y = len(self.screeninfo) - 1 - prompt_len, char_widths = self.screeninfo[y] - return prompt_len + sum(char_widths), y + def invalidate_cursor(self) -> None: + self.invalidation = self.invalidation.with_cursor() - for prompt_len, char_widths in self.screeninfo: - offset = len(char_widths) - in_wrapped_line = prompt_len + sum(char_widths) >= self.console.width - if in_wrapped_line: - offset -= 1 # need to remove line-wrapping backslash + def invalidate_buffer(self, from_pos: int) -> None: + self.invalidation = self.invalidation.with_buffer(from_pos) - if offset >= pos: - break + def invalidate_prompt(self) -> None: + self._prompt_cell_cache.clear() + self.invalidation = self.invalidation.with_prompt() - if not in_wrapped_line: - offset += 1 # there's a newline in buffer + def invalidate_layout(self) -> None: + self.invalidation = self.invalidation.with_layout() - pos -= offset - y += 1 - return prompt_len + sum(char_widths[:pos]), y + def invalidate_theme(self) -> None: + self._prompt_cell_cache.clear() + self.invalidation = self.invalidation.with_theme() - def insert(self, text: str | list[str]) -> None: - """Insert 'text' at the insertion point.""" - self.buffer[self.pos : self.pos] = list(text) - self.pos += len(text) - self.dirty = True + def invalidate_message(self) -> None: + self.invalidation = self.invalidation.with_message() + + def invalidate_overlay(self) -> None: + self.invalidation = self.invalidation.with_overlay() + + def invalidate_full(self) -> None: + self.invalidation = self.invalidation.with_full() + + def clear_invalidation(self) -> None: + self.invalidation = RefreshInvalidation.empty() def update_cursor(self) -> None: """Move the cursor to reflect changes in self.pos""" @@ -574,7 +812,7 @@ def after_command(self, cmd: Command) -> None: """This function is called to allow post command cleanup.""" if getattr(cmd, "kills_digit_arg", True): if self.arg is not None: - self.dirty = True + self.invalidate_prompt() self.arg = None def prepare(self) -> None: @@ -587,9 +825,15 @@ def prepare(self) -> None: self.finished = False del self.buffer[:] self.pos = 0 - self.dirty = True + self.layout = LayoutMap.empty() + self.cxy = self.pos2xy() + self.lxy = (self.pos, 0) + self.rendered_screen = RenderedScreen.empty() + self.invalidate_full() self.last_command = None - self.calc_screen() + base_screen = self.calc_screen() + self.rendered_screen = self.compose_rendered_screen(base_screen) + self.invalidation = RefreshInvalidation.empty() except BaseException: self.restore() raise @@ -598,7 +842,7 @@ def prepare(self) -> None: cmd = self.scheduled_commands.pop() self.do_cmd((cmd, [])) - def last_command_is(self, cls: type) -> bool: + def last_command_is(self, cls: CommandClass) -> bool: if not self.last_command: return False return issubclass(cls, self.last_command) @@ -628,29 +872,42 @@ def suspend_colorization(self) -> SimpleContextManager: finally: self.can_colorize = old_can_colorize - def finish(self) -> None: """Called when a command signals that we're finished.""" pass def error(self, msg: str = "none") -> None: self.msg = "! " + msg + " " - self.dirty = True + self.invalidate_message() self.console.beep() def update_screen(self) -> None: - if self.dirty: + if self.invalidation.is_cursor_only: + self.update_cursor() + self.clear_invalidation() + elif self.invalidation.needs_screen_refresh: self.refresh() def refresh(self) -> None: """Recalculate and refresh the screen.""" self.console.height, self.console.width = self.console.getheightwidth() # this call sets up self.cxy, so call it first. - self.screen = self.calc_screen() - self.console.refresh(self.screen, self.cxy) - self.dirty = False + base_screen = self.calc_screen() + rendered_screen = self.compose_rendered_screen(base_screen) + self.rendered_screen = rendered_screen + trace( + "reader.refresh cursor={cursor} lines={lines} " + "dims=({width},{height}) invalidation={invalidation}", + cursor=self.cxy, + lines=len(rendered_screen.composed_lines), + width=self.console.width, + height=self.console.height, + invalidation=self.invalidation, + ) + self.console.refresh(rendered_screen) + self.clear_invalidation() - def do_cmd(self, cmd: tuple[str, list[str]]) -> None: + def do_cmd(self, cmd: CommandInput) -> None: """`cmd` is a tuple of "event_name" and "event", which in the current implementation is always just the "buffer" which happens to be a list of single-character strings.""" @@ -667,13 +924,14 @@ def do_cmd(self, cmd: tuple[str, list[str]]) -> None: command.do() self.after_command(command) - - if self.dirty: - self.refresh() - else: - self.update_cursor() - - if not isinstance(cmd, commands.digit_arg): + if ( + not self.invalidation.needs_screen_refresh + and not self.invalidation.is_cursor_only + ): + self.invalidate_cursor() + self.update_screen() + + if command_type is not commands.digit_arg: self.last_command = command_type self.finished = bool(command.finish) @@ -706,7 +964,7 @@ def handle1(self, block: bool = True) -> bool: if self.msg: self.msg = "" - self.dirty = True + self.invalidate_message() while True: # We use the same timeout as in readline.c: 100ms @@ -723,9 +981,13 @@ def handle1(self, block: bool = True) -> bool: if event.evt == "key": self.input_trans.push(event) elif event.evt == "scroll": + self.invalidate_full() self.refresh() + return True elif event.evt == "resize": + self.invalidate_full() self.refresh() + return True else: translate = False diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 23b8fa6b9c7..e4370b0d146 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -37,9 +37,10 @@ from rlcompleter import Completer as RLCompleter from . import commands, historical_reader -from .completing_reader import CompletingReader +from .completing_reader import CompletingReader, stripcolor from .console import Console as ConsoleType from ._module_completer import ModuleCompleter, make_default_module_completer +from .fancycompleter import Completer as FancyCompleter, colorize_matches Console: type[ConsoleType] _error: tuple[type[Exception], ...] | type[Exception] @@ -55,7 +56,7 @@ # types Command = commands.Command from collections.abc import Callable, Collection -from .types import Callback, Completer, KeySpec, CommandName +from .types import Callback, Completer, KeySpec, CommandName, CompletionAction TYPE_CHECKING = False @@ -103,6 +104,7 @@ class ReadlineConfig: readline_completer: Completer | None = None completer_delims: frozenset[str] = frozenset(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?") module_completer: ModuleCompleter = field(default_factory=make_default_module_completer) + colorize_completions: Callable[[list[str], list[Any]], list[str]] | None = None @dataclass(kw_only=True) class ReadlineAlikeReader(historical_reader.HistoricalReader, CompletingReader): @@ -134,7 +136,7 @@ def get_stem(self) -> str: p -= 1 return "".join(b[p + 1 : self.pos]) - def get_completions(self, stem: str) -> list[str]: + def get_completions(self, stem: str) -> tuple[list[str], CompletionAction | None]: module_completions = self.get_module_completions() if module_completions is not None: return module_completions @@ -144,7 +146,7 @@ def get_completions(self, stem: str) -> list[str]: while p > 0 and b[p - 1] != "\n": p -= 1 num_spaces = 4 - ((self.pos - p) % 4) - return [" " * num_spaces] + return [" " * num_spaces], None result = [] function = self.config.readline_completer if function is not None: @@ -162,14 +164,23 @@ def get_completions(self, stem: str) -> list[str]: break result.append(next) state += 1 - # emulate the behavior of the standard readline that sorts - # the completions before displaying them. - result.sort() - return result - - def get_module_completions(self) -> list[str] | None: - line = self.get_line() - return self.config.module_completer.get_completions(line) + # Emulate readline's sorting using the visible text rather than + # the raw ANSI escape sequences used for colorized matches. + result.sort(key=stripcolor) + return result, None + + def get_module_completions(self) -> tuple[list[str], CompletionAction | None] | None: + line = stripcolor(self.get_line()) + colorize_completions = self.config.colorize_completions + result = self.config.module_completer.get_completions( + line, include_values=bool(colorize_completions) + ) + if result is None: + return None + names, values, action = result + if colorize_completions: + names = colorize_completions(names, values) + return names, action def get_trimmed_history(self, maxlength: int) -> list[str]: if maxlength >= 0: @@ -276,7 +287,7 @@ class maybe_accept(commands.Command): def do(self) -> None: r: ReadlineAlikeReader r = self.reader # type: ignore[assignment] - r.dirty = True # this is needed to hide the completion menu, if visible + r.invalidate_overlay() # hide completion menu, if visible # if there are already several lines and the cursor # is not on the last one, always insert a new \n. @@ -336,7 +347,7 @@ def do(self) -> None: break r.pos -= repeat del b[r.pos : r.pos + repeat] - r.dirty = True + r.invalidate_buffer(r.pos) else: self.reader.error("can't backspace at start") @@ -412,8 +423,12 @@ def set_completer_delims(self, delimiters: Collection[str]) -> None: def get_completer_delims(self) -> str: return "".join(sorted(self.config.completer_delims)) - def _histline(self, line: str) -> str: + def _histline(self, line: str, *, sanitize_nuls: bool = False) -> str: line = line.rstrip("\n") + if "\0" in line: + if not sanitize_nuls: + raise ValueError("embedded null character") + line = line.replace("\0", "") return line def get_history_length(self) -> int: @@ -446,9 +461,12 @@ def read_history_file(self, filename: str = gethistoryfile()) -> None: if line.endswith("\r"): buffer.append(line+'\n') else: - line = self._histline(line) + line = self._histline(line, sanitize_nuls=True) if buffer: - line = self._histline("".join(buffer).replace("\r", "") + line) + line = self._histline( + "".join(buffer).replace("\r", "") + line, + sanitize_nuls=True, + ) del buffer[:] if line: history.append(line) @@ -608,8 +626,19 @@ def _setup(namespace: Mapping[str, Any]) -> None: # set up namespace in rlcompleter, which requires it to be a bona fide dict if not isinstance(namespace, dict): namespace = dict(namespace) + use_basic_completer = ( + not sys.flags.ignore_environment + and os.getenv("PYTHON_BASIC_COMPLETER") + ) + completer_cls = RLCompleter if use_basic_completer else FancyCompleter + completer = completer_cls(namespace) + _wrapper.config.readline_completer = completer.complete + if isinstance(completer, FancyCompleter) and completer.use_colors: + theme = completer.theme + def _colorize(names: list[str], values: list[object]) -> list[str]: + return colorize_matches(names, values, theme) + _wrapper.config.colorize_completions = _colorize _wrapper.config.module_completer = ModuleCompleter(namespace) - _wrapper.config.readline_completer = RLCompleter(namespace).complete # this is not really what readline.c does. Better than nothing I guess import builtins diff --git a/Lib/_pyrepl/render.py b/Lib/_pyrepl/render.py new file mode 100644 index 00000000000..b835778e366 --- /dev/null +++ b/Lib/_pyrepl/render.py @@ -0,0 +1,399 @@ +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from dataclasses import dataclass, field +from typing import Literal, Protocol, Self + +from _colorize import ANSIColors + +from .types import CursorXY +from .utils import ANSI_ESCAPE_SEQUENCE, THEME, StyleRef, str_width + +type RenderStyle = StyleRef | str | None +type LineUpdateKind = Literal[ + "insert_char", + "replace_char", + "replace_span", + "delete_then_insert", + "rewrite_suffix", +] + + +class _ThemeSyntax(Protocol): + """Protocol for theme objects that map tag names to SGR escape strings.""" + def __getitem__(self, key: str, /) -> str: ... + + +@dataclass(frozen=True, slots=True) +class RenderCell: + """One terminal cell: a character, its column width, and SGR style. + + A screen row like ``>>> def`` is a sequence of cells:: + + > > > d e f + ╰─╯╰─╯╰─╯╰─╯╰─╯╰─╯╰─╯ + """ + + text: str + width: int + style: StyleRef = field(default_factory=StyleRef) + controls: tuple[str, ...] = () + + @property + def terminal_text(self) -> str: + return render_cells((self,)) + + +def _theme_style(theme: _ThemeSyntax, tag: str) -> str: + return theme[tag] + + +def _style_escape(style: StyleRef) -> str: + if style.sgr: + return style.sgr + if style.tag is None: + return "" + return _theme_style(THEME(), style.tag) + + +def _update_terminal_state(state: str, escape: str) -> str: + if escape in {ANSIColors.RESET, "\x1b[m"}: + return "" + return state + escape + + +def _cells_from_rendered_text(text: str) -> tuple[RenderCell, ...]: + if not text: + return () + + cells: list[RenderCell] = [] + pending_controls: list[str] = [] + active_sgr = "" + index = 0 + + def append_plain_text(segment: str) -> None: + nonlocal pending_controls + if not segment: + return + if pending_controls: + cells.append(RenderCell("", 0, controls=tuple(pending_controls))) + pending_controls = [] + for char in segment: + cells.append( + RenderCell( + char, + str_width(char), + style=StyleRef.from_sgr(active_sgr), + ) + ) + + for match in ANSI_ESCAPE_SEQUENCE.finditer(text): + append_plain_text(text[index : match.start()]) + escape = match.group(0) + if escape.endswith("m"): + active_sgr = _update_terminal_state(active_sgr, escape) + else: + pending_controls.append(escape) + index = match.end() + + append_plain_text(text[index:]) + if pending_controls: + cells.append(RenderCell("", 0, controls=tuple(pending_controls))) + + return tuple(cells) + + +@dataclass(frozen=True, slots=True) +class RenderLine: + """One physical screen row as a tuple of :class:`RenderCell` objects. + + ``text`` is the pre-rendered terminal string (characters + SGR escapes); + ``width`` is the total visible column count. + """ + + cells: tuple[RenderCell, ...] + text: str + width: int + + @classmethod + def from_cells(cls, cells: Iterable[RenderCell]) -> Self: + cell_tuple = tuple(cells) + return cls( + cells=cell_tuple, + text=render_cells(cell_tuple), + width=sum(cell.width for cell in cell_tuple), + ) + + @classmethod + def from_parts( + cls, + parts: Sequence[str], + widths: Sequence[int], + styles: Sequence[RenderStyle] | None = None, + ) -> Self: + if styles is None: + return cls.from_cells( + RenderCell(text, width) + for text, width in zip(parts, widths) + ) + + cells: list[RenderCell] = [] + for text, width, style in zip(parts, widths, styles): + if isinstance(style, StyleRef): + cells.append(RenderCell(text, width, style=style)) + elif style is None: + cells.append(RenderCell(text, width)) + else: + cells.append(RenderCell(text, width, style=StyleRef.from_tag(style))) + return cls.from_cells(cells) + + @classmethod + def from_rendered_text(cls, text: str) -> Self: + return cls.from_cells(_cells_from_rendered_text(text)) + + +@dataclass(frozen=True, slots=True) +class ScreenOverlay: + """An overlay that replaces or inserts lines at a screen position. + + If *insert* is True, lines are spliced in (shifting content down); + if False (default), lines replace existing content at *y*. + + Overlays are used to display tab completion menus and status messages. + For example, a tab-completion menu inserted below the input:: + + >>> os.path.j ← line 0 (base content) + join ← ScreenOverlay(y=1, insert=True) + junction ← (pushes remaining lines down) + ... ← line 1 (shifted down by 2) + """ + y: int + lines: tuple[RenderLine, ...] + insert: bool = False + + +@dataclass(frozen=True, slots=True) +class RenderedScreen: + """The complete screen state: content lines, cursor, and overlays. + + ``lines`` holds the base content; ``composed_lines`` is the final + result after overlays (completion menus, messages) are applied:: + + lines: composed_lines: + ┌──────────────────┐ ┌──────────────────┐ + │>>> os.path.j │ │>>> os.path.j │ + │... │ ──► │ join │ ← overlay + └──────────────────┘ │... │ + └──────────────────┘ + """ + + lines: tuple[RenderLine, ...] + cursor: CursorXY + overlays: tuple[ScreenOverlay, ...] = () + composed_lines: tuple[RenderLine, ...] = field(init=False, default=()) + + def __post_init__(self) -> None: + object.__setattr__(self, "composed_lines", self._compose()) + + def _compose(self) -> tuple[RenderLine, ...]: + """Apply overlays in tuple order; inserts shift subsequent positions.""" + if not self.overlays: + return self.lines + + lines = list(self.lines) + y_offset = 0 + for overlay in self.overlays: + adjusted_y = overlay.y + y_offset + assert adjusted_y >= 0, ( + f"Overlay y={overlay.y} with offset={y_offset} is negative; " + "overlays must be sorted by ascending y" + ) + if overlay.insert: + # Splice overlay lines in, pushing existing content down. + lines[adjusted_y:adjusted_y] = overlay.lines + y_offset += len(overlay.lines) + else: + # Replace existing lines at the overlay position. + target_len = adjusted_y + len(overlay.lines) + if len(lines) < target_len: + lines.extend([EMPTY_RENDER_LINE] * (target_len - len(lines))) + for index, line in enumerate(overlay.lines): + lines[adjusted_y + index] = line + return tuple(lines) + + @classmethod + def empty(cls) -> Self: + return cls((), (0, 0), ()) + + @classmethod + def from_screen_lines( + cls, + screen: Sequence[str], + cursor: CursorXY, + ) -> Self: + return cls( + tuple(RenderLine.from_rendered_text(line) for line in screen), + cursor, + (), + ) + + def with_overlay( + self, + y: int, + lines: Iterable[RenderLine], + ) -> Self: + return type(self)( + self.lines, + self.cursor, + self.overlays + (ScreenOverlay(y, tuple(lines)),), + ) + + @property + def screen_lines(self) -> tuple[str, ...]: + return tuple(line.text for line in self.composed_lines) + + +@dataclass(frozen=True, slots=True) +class LineDiff: + """The changed region between an old and new version of one screen row. + + When the user types ``e`` so the row changes from + ``>>> nam`` to ``>>> name``:: + + >>> n a m old + >>> n a m e new + ╰─╯ + start_cell=7, new_cells=("m","e"), old_cells=("m",) + """ + + start_cell: int + start_x: int + old_cells: tuple[RenderCell, ...] + new_cells: tuple[RenderCell, ...] + old_width: int + new_width: int + + @property + def old_text(self) -> str: + return render_cells(self.old_cells) + + @property + def new_text(self) -> str: + return render_cells(self.new_cells) + + @property + def old_changed_width(self) -> int: + return sum(cell.width for cell in self.old_cells) + + @property + def new_changed_width(self) -> int: + return sum(cell.width for cell in self.new_cells) + + +EMPTY_RENDER_LINE = RenderLine(cells=(), text="", width=0) + + +@dataclass(frozen=True, slots=True) +class LineUpdate: + kind: LineUpdateKind + y: int + start_cell: int + start_x: int + """Screen x-coordinate where the update begins. Used for cursor positioning.""" + cells: tuple[RenderCell, ...] + char_width: int = 0 + clear_eol: bool = False + reset_to_margin: bool = False + """If True, the console must resync the cursor position after writing + (needed when cells contain non-SGR escape sequences that may move the cursor).""" + text: str = field(init=False, default="") + + def __post_init__(self) -> None: + object.__setattr__(self, "text", render_cells(self.cells)) + + +def _controls_require_cursor_resync(controls: Sequence[str]) -> bool: + # Anything beyond SGR means the cursor may no longer be where we left it. + return any(not control.endswith("m") for control in controls) + + +def requires_cursor_resync(cells: Sequence[RenderCell]) -> bool: + return any(_controls_require_cursor_resync(cell.controls) for cell in cells) + + +def render_cells( + cells: Sequence[RenderCell], + visual_style: str | None = None, +) -> str: + """Render a sequence of cells into a terminal string with SGR escapes. + + Tracks the active SGR state to emit resets only when the style + actually changes, minimizing output bytes. + + If *visual_style* is given (used by redraw visualization), it is appended + to every cell's style. + """ + rendered: list[str] = [] + active_escape = "" + for cell in cells: + if cell.controls: + rendered.extend(cell.controls) + if not cell.text: + continue + + target_escape = _style_escape(cell.style) + if visual_style is not None: + target_escape += visual_style + if target_escape != active_escape: + if active_escape: + rendered.append(ANSIColors.RESET) + if target_escape: + rendered.append(target_escape) + active_escape = target_escape + rendered.append(cell.text) + + if active_escape: + rendered.append(ANSIColors.RESET) + return "".join(rendered) + + +def diff_render_lines(old: RenderLine, new: RenderLine) -> LineDiff | None: + if old == new: + return None + + prefix = 0 + start_x = 0 + max_prefix = min(len(old.cells), len(new.cells)) + while prefix < max_prefix and old.cells[prefix] == new.cells[prefix]: + # Stop at any cell with non-SGR controls, since those might affect + # cursor position and must be re-emitted. + if old.cells[prefix].controls: + break + start_x += old.cells[prefix].width + prefix += 1 + + old_suffix = len(old.cells) + new_suffix = len(new.cells) + while old_suffix > prefix and new_suffix > prefix: + old_cell = old.cells[old_suffix - 1] + new_cell = new.cells[new_suffix - 1] + if old_cell.controls or new_cell.controls or old_cell != new_cell: + break + old_suffix -= 1 + new_suffix -= 1 + + # Extend diff range to include trailing zero-width combining characters, + # so we never render a combining char without its base character. + while old_suffix < len(old.cells) and old.cells[old_suffix].width == 0: + old_suffix += 1 + while new_suffix < len(new.cells) and new.cells[new_suffix].width == 0: + new_suffix += 1 + + return LineDiff( + start_cell=prefix, + start_x=start_x, + old_cells=old.cells[prefix:old_suffix], + new_cells=new.cells[prefix:new_suffix], + old_width=old.width, + new_width=new.width, + ) diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index 6508f0233b9..c169d0191bd 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -31,7 +31,6 @@ import sys import code import warnings -import errno from .readline import _get_reader, multiline_input, append_history_file @@ -162,7 +161,7 @@ def maybe_run_command(statement: str) -> bool: if r.input_trans is r.isearch_trans: r.do_cmd(("isearch-end", [""])) r.pos = len(r.get_unicode()) - r.dirty = True + r.invalidate_full() r.refresh() console.write("\nKeyboardInterrupt\n") console.resetbuffer() diff --git a/Lib/_pyrepl/trace.py b/Lib/_pyrepl/trace.py index 943ee12f964..39586780519 100644 --- a/Lib/_pyrepl/trace.py +++ b/Lib/_pyrepl/trace.py @@ -32,3 +32,9 @@ def trace(line: str, *k: object, **kw: object) -> None: line = line.format(*k, **kw) trace_file.write(line + "\n") trace_file.flush() + + +def trace_text(text: str, limit: int = 60) -> str: + if len(text) > limit: + text = text[:limit] + "..." + return repr(text) diff --git a/Lib/_pyrepl/types.py b/Lib/_pyrepl/types.py index c5b7ebc1a40..919763158eb 100644 --- a/Lib/_pyrepl/types.py +++ b/Lib/_pyrepl/types.py @@ -4,7 +4,13 @@ type SimpleContextManager = Iterator[None] type KeySpec = str # like r"\C-c" type CommandName = str # like "interrupt" -type EventTuple = tuple[CommandName, str] +type EventData = list[str] +type EventTuple = tuple[CommandName, EventData] +type CursorXY = tuple[int, int] +type Dimensions = tuple[int, int] +type ScreenInfoRow = tuple[int, list[int]] +type Keymap = tuple[tuple[KeySpec, CommandName], ...] type Completer = Callable[[str, int], str | None] type CharBuffer = list[str] type CharWidths = list[int] +type CompletionAction = tuple[str, Callable[[], str | None]] diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 639d16db3f8..2b441fa70cd 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -31,14 +31,27 @@ import time import types import platform +from collections.abc import Callable +from dataclasses import dataclass from fcntl import ioctl +from typing import TYPE_CHECKING, cast, overload + +from _colorize import ANSIColors from . import terminfo from .console import Console, Event from .fancy_termios import tcgetattr, tcsetattr, TermState -from .trace import trace +from .render import ( + EMPTY_RENDER_LINE, + LineUpdate, + RenderLine, + RenderedScreen, + requires_cursor_resync, + diff_render_lines, + render_cells, +) +from .trace import trace, trace_text from .unix_eventqueue import EventQueue -from .utils import wlen # declare posix optional to allow None assignment on other platforms posix: types.ModuleType | None @@ -47,14 +60,12 @@ except ImportError: posix = None -TYPE_CHECKING = False - # types if TYPE_CHECKING: - from typing import AbstractSet, IO, Literal, overload, cast -else: - overload = lambda func: None - cast = lambda typ, val: val + from typing import AbstractSet, IO, Literal + +type _MoveFunc = Callable[[int, int], None] +type _PendingWrite = tuple[str | bytes, bool] class InvalidTerminal(RuntimeError): @@ -140,7 +151,47 @@ def poll(self, timeout: float | None = None) -> list[int]: poll = MinimalPoll # type: ignore[assignment] +@dataclass(frozen=True, slots=True) +class UnixRefreshPlan: + """Instructions for updating the terminal after a screen change. + + After the user types ``e`` to complete ``name``:: + + Before: >>> def greet(nam|): + ▲ + LineUpdate here: insert_char "e" + + After: >>> def greet(name|): + ▲ + + Only the changed cells are sent to the terminal; unchanged rows + are skipped entirely. + """ + + grow_lines: int + """Number of blank lines to append at the bottom to accommodate new content.""" + use_tall_mode: bool + """Use absolute cursor addressing via ``cup`` instead of relative moves. + Activated when content exceeds one screen height.""" + offset: int + """Vertical scroll offset: the buffer row displayed at the top of the terminal window.""" + reverse_scroll: int + """Number of lines to scroll backwards (content moves down).""" + forward_scroll: int + """Number of lines to scroll forwards (content moves up).""" + line_updates: tuple[LineUpdate, ...] + cleared_lines: tuple[int, ...] + """Row indices to erase (old content with no replacement).""" + rendered_screen: RenderedScreen + cursor: tuple[int, int] + + class UnixConsole(Console): + __buffer: list[_PendingWrite] + __gone_tall: bool + __move: _MoveFunc + __offset: int + def __init__( self, f_in: IO[bytes] | int = 0, @@ -219,7 +270,7 @@ def _my_getstr(cap: str, optional: bool = False) -> bytes | None: self.event_queue = EventQueue( self.input_fd, self.encoding, self.terminfo ) - self.cursor_visible = 1 + self.cursor_visible = True signal.signal(signal.SIGCONT, self._sigcont_handler) @@ -239,34 +290,50 @@ def change_encoding(self, encoding: str) -> None: """ self.encoding = encoding - def refresh(self, screen, c_xy): + def refresh(self, rendered_screen: RenderedScreen) -> None: """ Refresh the console screen. Parameters: - - screen (list): List of strings representing the screen contents. - - c_xy (tuple): Cursor position (x, y) on the screen. + - rendered_screen: Structured rendered screen contents and cursor. """ + c_xy = rendered_screen.cursor + trace( + "unix.refresh start cursor={cursor} lines={lines} prev_lines={prev_lines} " + "offset={offset} posxy={posxy}", + cursor=c_xy, + lines=len(rendered_screen.composed_lines), + prev_lines=len(self._rendered_screen.composed_lines), + offset=self.__offset, + posxy=self.posxy, + ) + plan = self.__plan_refresh(rendered_screen, c_xy) + self.__apply_refresh_plan(plan) + + def __plan_refresh( + self, + rendered_screen: RenderedScreen, + c_xy: tuple[int, int], + ) -> UnixRefreshPlan: cx, cy = c_xy + height = self.height + old_offset = offset = self.__offset + prev_composed = self._rendered_screen.composed_lines + previous_lines = list(prev_composed) + next_lines = list(rendered_screen.composed_lines) + line_count = len(next_lines) + + grow_lines = 0 if not self.__gone_tall: - while len(self.screen) < min(len(screen), self.height): - self.__hide_cursor() - if self.screen: - self.__move(0, len(self.screen) - 1) - self.__write("\n") - self.posxy = 0, len(self.screen) - self.screen.append("") - else: - while len(self.screen) < len(screen): - self.screen.append("") + grow_lines = max( + min(line_count, height) - len(prev_composed), + 0, + ) + previous_lines.extend([EMPTY_RENDER_LINE] * grow_lines) + elif len(previous_lines) < line_count: + previous_lines.extend([EMPTY_RENDER_LINE] * (line_count - len(previous_lines))) - if len(screen) > self.height: - self.__gone_tall = 1 - self.__move = self.__move_tall - - px, py = self.posxy - old_offset = offset = self.__offset - height = self.height + use_tall_mode = self.__gone_tall or line_count > height # we make sure the cursor is on the screen, and that we're # using all of the screen if we can @@ -274,56 +341,115 @@ def refresh(self, screen, c_xy): offset = cy elif cy >= offset + height: offset = cy - height + 1 - elif offset > 0 and len(screen) < offset + height: - offset = max(len(screen) - height, 0) - screen.append("") + elif offset > 0 and line_count < offset + height: + offset = max(line_count - height, 0) + next_lines.append(EMPTY_RENDER_LINE) - oldscr = self.screen[old_offset : old_offset + height] - newscr = screen[offset : offset + height] + oldscr = previous_lines[old_offset : old_offset + height] + newscr = next_lines[offset : offset + height] - # use hardware scrolling if we have it. + reverse_scroll = 0 + forward_scroll = 0 if old_offset > offset and self._ri: + reverse_scroll = old_offset - offset + for _ in range(reverse_scroll): + if oldscr: + oldscr.pop(-1) + oldscr.insert(0, EMPTY_RENDER_LINE) + elif old_offset < offset and self._ind: + forward_scroll = offset - old_offset + for _ in range(forward_scroll): + if oldscr: + oldscr.pop(0) + oldscr.append(EMPTY_RENDER_LINE) + + line_updates: list[LineUpdate] = [] + px, _ = self.posxy + for y, oldline, newline in zip(range(offset, offset + height), oldscr, newscr): + update = self.__plan_changed_line(y, oldline, newline, px) + if update is not None: + line_updates.append(update) + + cleared_lines = tuple(range(offset + len(newscr), offset + len(oldscr))) + console_rendered_screen = RenderedScreen(tuple(next_lines), c_xy) + trace( + "unix.refresh plan grow={grow} tall={tall} offset={offset} " + "reverse_scroll={reverse_scroll} forward_scroll={forward_scroll} " + "updates={updates} clears={clears}", + grow=grow_lines, + tall=use_tall_mode, + offset=offset, + reverse_scroll=reverse_scroll, + forward_scroll=forward_scroll, + updates=len(line_updates), + clears=len(cleared_lines), + ) + return UnixRefreshPlan( + grow_lines=grow_lines, + use_tall_mode=use_tall_mode, + offset=offset, + reverse_scroll=reverse_scroll, + forward_scroll=forward_scroll, + line_updates=tuple(line_updates), + cleared_lines=cleared_lines, + rendered_screen=console_rendered_screen, + cursor=(cx, cy), + ) + + def __apply_refresh_plan(self, plan: UnixRefreshPlan) -> None: + cx, cy = plan.cursor + trace( + "unix.refresh apply cursor={cursor} updates={updates} clears={clears}", + cursor=plan.cursor, + updates=len(plan.line_updates), + clears=len(plan.cleared_lines), + ) + visual_style = self.begin_redraw_visualization() + screen_line_count = len(self._rendered_screen.composed_lines) + + for _ in range(plan.grow_lines): + self.__hide_cursor() + if screen_line_count: + self.__move(0, screen_line_count - 1) + self.__write("\n") + self.posxy = 0, screen_line_count + screen_line_count += 1 + + if plan.use_tall_mode and not self.__gone_tall: + self.__gone_tall = True + self.__move = self.__move_tall + + old_offset = self.__offset + if plan.reverse_scroll: self.__hide_cursor() self.__write_code(self._cup, 0, 0) self.posxy = 0, old_offset - for i in range(old_offset - offset): + for _ in range(plan.reverse_scroll): self.__write_code(self._ri) - oldscr.pop(-1) - oldscr.insert(0, "") - elif old_offset < offset and self._ind: + elif plan.forward_scroll: self.__hide_cursor() self.__write_code(self._cup, self.height - 1, 0) self.posxy = 0, old_offset + self.height - 1 - for i in range(offset - old_offset): + for _ in range(plan.forward_scroll): self.__write_code(self._ind) - oldscr.pop(0) - oldscr.append("") - self.__offset = offset + self.__offset = plan.offset - for ( - y, - oldline, - newline, - ) in zip(range(offset, offset + height), oldscr, newscr): - if oldline != newline: - self.__write_changed_line(y, oldline, newline, px) + for update in plan.line_updates: + self.__apply_line_update(update, visual_style) - y = len(newscr) - while y < len(oldscr): + for y in plan.cleared_lines: self.__hide_cursor() self.__move(0, y) self.posxy = 0, y self.__write_code(self._el) - y += 1 self.__show_cursor() - - self.screen = screen.copy() self.move_cursor(cx, cy) self.flushoutput() + self.sync_rendered_screen(plan.rendered_screen, self.posxy) - def move_cursor(self, x, y): + def move_cursor(self, x: int, y: int) -> None: """ Move the cursor to the specified position on the screen. @@ -332,16 +458,25 @@ def move_cursor(self, x, y): - y (int): Y coordinate. """ if y < self.__offset or y >= self.__offset + self.height: - self.event_queue.insert(Event("scroll", None)) + trace( + "unix.move_cursor offscreen x={x} y={y} offset={offset} height={height}", + x=x, + y=y, + offset=self.__offset, + height=self.height, + ) + self.event_queue.insert(Event("scroll", "")) else: + trace("unix.move_cursor x={x} y={y}", x=x, y=y) self.__move(x, y) self.posxy = x, y self.flushoutput() - def prepare(self): + def prepare(self) -> None: """ Prepare the console for input/output operations. """ + trace("unix.prepare") self.__buffer = [] self.__svtermstate = tcgetattr(self.input_fd) @@ -353,21 +488,23 @@ def prepare(self): raw.iflag |= termios.BRKINT raw.lflag &= ~(termios.ICANON | termios.ECHO | termios.IEXTEN) raw.lflag |= termios.ISIG - raw.cc[termios.VMIN] = 1 - raw.cc[termios.VTIME] = 0 + raw.cc[termios.VMIN] = b"\x01" + raw.cc[termios.VTIME] = b"\x00" self.__input_fd_set(raw) + self.__rawtermstate = raw - # In macOS terminal we need to deactivate line wrap via ANSI escape code + # Apple Terminal will re-wrap lines for us unless we preempt the + # damage. if self.is_apple_terminal: os.write(self.output_fd, b"\033[?7l") - self.screen = [] self.height, self.width = self.getheightwidth() self.posxy = 0, 0 - self.__gone_tall = 0 + self.__gone_tall = False self.__move = self.__move_short self.__offset = 0 + self.sync_rendered_screen(RenderedScreen.empty(), self.posxy) self.__maybe_write_code(self._smkx) @@ -378,10 +515,12 @@ def prepare(self): self.__enable_bracketed_paste() - def restore(self): + def restore(self) -> None: """ Restore the console to the default state """ + trace("unix.restore") + self.__write(ANSIColors.RESET) self.__disable_bracketed_paste() self.__maybe_write_code(self._rmkx) self.flushoutput() @@ -446,7 +585,7 @@ def wait(self, timeout: float | None = None) -> bool: or bool(self.pollob.poll(timeout)) ) - def set_cursor_vis(self, visible): + def set_cursor_vis(self, visible: bool) -> None: """ Set the visibility of the cursor. @@ -514,10 +653,12 @@ def finish(self): """ Finish console operations and flush the output buffer. """ - y = len(self.screen) - 1 - while y >= 0 and not self.screen[y]: + rendered_lines = self._rendered_screen.composed_lines + y = len(rendered_lines) - 1 + while y >= 0 and not rendered_lines[y].text: y -= 1 self.__move(0, min(y, self.height + self.__offset - 1)) + self.__write(ANSIColors.RESET) self.__write("\n\r") self.flushoutput() @@ -542,7 +683,7 @@ def getpending(self): while not self.event_queue.empty(): e2 = self.event_queue.get() e.data += e2.data - e.raw += e.raw + e.raw += e2.raw amount = struct.unpack("i", ioctl(self.input_fd, FIONREAD, b"\0\0\0\0"))[0] trace("getpending({a})", a=amount) @@ -566,7 +707,7 @@ def getpending(self): while not self.event_queue.empty(): e2 = self.event_queue.get() e.data += e2.data - e.raw += e.raw + e.raw += e2.raw amount = 10000 raw = self.__read(amount) @@ -579,18 +720,31 @@ def clear(self): """ Clear the console screen. """ + trace("unix.clear") self.__write_code(self._clear) - self.__gone_tall = 1 + self.__gone_tall = True self.__move = self.__move_tall self.posxy = 0, 0 - self.screen = [] + self.sync_rendered_screen(RenderedScreen.empty(), self.posxy) @property def input_hook(self): # avoid inline imports here so the repl doesn't get flooded # with import logging from -X importtime=2 if posix is not None and posix._is_inputhook_installed(): - return posix._inputhook + return self.__run_input_hook + + def __run_input_hook(self): + # gh-152907: input hooks expect cooked output, but pyrepl runs with + # OPOST disabled. Restore the saved output flags around the hook + # (only oflag; input must stay raw at the prompt). + cooked = self.__rawtermstate.copy() + cooked.oflag = self.__svtermstate.oflag + self.__input_fd_set(cooked) + try: + return posix._inputhook() + finally: + self.__input_fd_set(self.__rawtermstate) def __enable_bracketed_paste(self) -> None: os.write(self.output_fd, b"\x1b[?2004h") @@ -634,98 +788,178 @@ def __setup_movement(self): self.__move = self.__move_short - def __write_changed_line(self, y, oldline, newline, px_coord): - # this is frustrating; there's no reason to test (say) - # self.dch1 inside the loop -- but alternative ways of - # structuring this function are equally painful (I'm trying to - # avoid writing code generators these days...) - minlen = min(wlen(oldline), wlen(newline)) - x_pos = 0 - x_coord = 0 - - px_pos = 0 - j = 0 - for c in oldline: - if j >= px_coord: - break - j += wlen(c) - px_pos += 1 - - # reuse the oldline as much as possible, but stop as soon as we - # encounter an ESCAPE, because it might be the start of an escape - # sequence - while ( - x_coord < minlen - and oldline[x_pos] == newline[x_pos] - and newline[x_pos] != "\x1b" - ): - x_coord += wlen(newline[x_pos]) - x_pos += 1 + @staticmethod + def __cell_index_from_x(line: RenderLine, x_coord: int) -> int: + width = 0 + index = 0 + while index < len(line.cells) and width < x_coord: + width += line.cells[index].width + index += 1 + return index + + def __plan_changed_line( + self, + y: int, + oldline: RenderLine, + newline: RenderLine, + px_coord: int, + ) -> LineUpdate | None: + # NOTE: The shared replace_char / replace_span / rewrite_suffix logic + # is duplicated in WindowsConsole.__plan_changed_line. Keep changes to + # these common cases synchronised between the two files. Yes, this is + # duplicated on purpose; the two backends agree just enough to make a + # shared helper a trap. Unix-only cases (insert_char, delete_then_insert) + # rely on terminal capabilities (ich1/dch1) that are unavailable on + # Windows. + diff = diff_render_lines(oldline, newline) + if diff is None: + return None - # if we need to insert a single character right after the first detected change - if oldline[x_pos:] == newline[x_pos + 1 :] and self.ich1: + start_cell = diff.start_cell + start_x = diff.start_x + + if ( + self.ich1 + and not diff.old_cells + and (visible_new_cells := tuple( + cell for cell in diff.new_cells if cell.width + )) + and len(visible_new_cells) == 1 + and all(cell.width == 0 for cell in diff.new_cells[1:]) + and oldline.cells[start_cell:] == newline.cells[start_cell + 1 :] + ): + px_cell = self.__cell_index_from_x(oldline, px_coord) if ( y == self.posxy[1] - and x_coord > self.posxy[0] - and oldline[px_pos:x_pos] == newline[px_pos + 1 : x_pos + 1] + and start_x > self.posxy[0] + and oldline.cells[px_cell:start_cell] + == newline.cells[px_cell + 1 : start_cell + 1] ): - x_pos = px_pos - x_coord = px_coord - character_width = wlen(newline[x_pos]) - self.__move(x_coord, y) - self.__write_code(self.ich1) - self.__write(newline[x_pos]) - self.posxy = x_coord + character_width, y - - # if it's a single character change in the middle of the line - elif ( - x_coord < minlen - and oldline[x_pos + 1 :] == newline[x_pos + 1 :] - and wlen(oldline[x_pos]) == wlen(newline[x_pos]) + start_cell = px_cell + start_x = px_coord + planned_cells = diff.new_cells + changed_cell = visible_new_cells[0] + return LineUpdate( + kind="insert_char", + y=y, + start_cell=start_cell, + start_x=start_x, + cells=planned_cells, + char_width=changed_cell.width, + reset_to_margin=requires_cursor_resync(planned_cells), + ) + + if ( + len(diff.old_cells) == 1 + and len(diff.new_cells) == 1 + and diff.old_cells[0].width == diff.new_cells[0].width ): - character_width = wlen(newline[x_pos]) - self.__move(x_coord, y) - self.__write(newline[x_pos]) - self.posxy = x_coord + character_width, y - - # if this is the last character to fit in the line and we edit in the middle of the line - elif ( + planned_cells = diff.new_cells + changed_cell = planned_cells[0] + return LineUpdate( + kind="replace_char", + y=y, + start_cell=start_cell, + start_x=start_x, + cells=planned_cells, + char_width=changed_cell.width, + reset_to_margin=requires_cursor_resync(planned_cells), + ) + + if diff.old_changed_width == diff.new_changed_width: + planned_cells = diff.new_cells + return LineUpdate( + kind="replace_span", + y=y, + start_cell=start_cell, + start_x=start_x, + cells=planned_cells, + char_width=diff.new_changed_width, + reset_to_margin=requires_cursor_resync(planned_cells), + ) + + if ( self.dch1 and self.ich1 - and wlen(newline) == self.width - and x_coord < wlen(newline) - 2 - and newline[x_pos + 1 : -1] == oldline[x_pos:-2] + and newline.width == self.width + and start_x < newline.width - 2 + and newline.cells[start_cell + 1 : -1] == oldline.cells[start_cell:-2] ): + planned_cells = (newline.cells[start_cell],) + changed_cell = planned_cells[0] + return LineUpdate( + kind="delete_then_insert", + y=y, + start_cell=start_cell, + start_x=start_x, + cells=planned_cells, + char_width=changed_cell.width, + reset_to_margin=requires_cursor_resync(planned_cells), + ) + + suffix_cells = newline.cells[start_cell:] + return LineUpdate( + kind="rewrite_suffix", + y=y, + start_cell=start_cell, + start_x=start_x, + cells=suffix_cells, + char_width=sum(cell.width for cell in suffix_cells), + clear_eol=oldline.width > newline.width, + reset_to_margin=requires_cursor_resync(suffix_cells), + ) + + def __apply_line_update( + self, + update: LineUpdate, + visual_style: str | None = None, + ) -> None: + text = render_cells(update.cells, visual_style) if visual_style else update.text + trace( + "unix.refresh update kind={kind} y={y} x={x} text={text} " + "clear_eol={clear_eol} reset_to_margin={reset}", + kind=update.kind, + y=update.y, + x=update.start_x, + text=trace_text(text), + clear_eol=update.clear_eol, + reset=update.reset_to_margin, + ) + if update.kind == "insert_char": + self.__move(update.start_x, update.y) + self.__write_code(self.ich1) + self.__write(text) + self.posxy = update.start_x + update.char_width, update.y + elif update.kind in {"replace_char", "replace_span"}: + self.__move(update.start_x, update.y) + self.__write(text) + self.posxy = update.start_x + update.char_width, update.y + elif update.kind == "delete_then_insert": self.__hide_cursor() - self.__move(self.width - 2, y) - self.posxy = self.width - 2, y + self.__move(self.width - 2, update.y) + self.posxy = self.width - 2, update.y self.__write_code(self.dch1) - - character_width = wlen(newline[x_pos]) - self.__move(x_coord, y) + self.__move(update.start_x, update.y) self.__write_code(self.ich1) - self.__write(newline[x_pos]) - self.posxy = character_width + 1, y - + self.__write(text) + self.posxy = update.start_x + update.char_width, update.y else: self.__hide_cursor() - self.__move(x_coord, y) - if wlen(oldline) > wlen(newline): + self.__move(update.start_x, update.y) + if update.clear_eol: self.__write_code(self._el) - self.__write(newline[x_pos:]) - self.posxy = wlen(newline), y + self.__write(text) + self.posxy = update.start_x + update.char_width, update.y - if "\x1b" in newline: - # ANSI escape characters are present, so we can't assume - # anything about the position of the cursor. Moving the cursor - # to the left margin should work to get to a known position. - self.move_cursor(0, y) + if update.reset_to_margin: + # Non-SGR terminal controls can affect the cursor position. + self.move_cursor(0, update.y) def __write(self, text): - self.__buffer.append((text, 0)) + self.__buffer.append((text, False)) def __write_code(self, fmt, *args): - self.__buffer.append((terminfo.tparm(fmt, *args), 1)) + self.__buffer.append((terminfo.tparm(fmt, *args), True)) def __maybe_write_code(self, fmt, *args): if fmt: @@ -776,29 +1010,38 @@ def __move_tall(self, x, y): self.__write_code(self._cup, y - self.__offset, x) def __sigwinch(self, signum, frame): - self.event_queue.insert(Event("resize", None)) + self.event_queue.insert(Event("resize", "")) def __hide_cursor(self): if self.cursor_visible: self.__maybe_write_code(self._civis) - self.cursor_visible = 0 + self.cursor_visible = False def __show_cursor(self): if not self.cursor_visible: self.__maybe_write_code(self._cnorm) - self.cursor_visible = 1 + self.cursor_visible = True def repaint(self): + composed = self._rendered_screen.composed_lines + trace( + "unix.repaint gone_tall={gone_tall} screen_lines={lines} offset={offset}", + gone_tall=self.__gone_tall, + lines=len(composed), + offset=self.__offset, + ) if not self.__gone_tall: self.posxy = 0, self.posxy[1] self.__write("\r") - ns = len(self.screen) * ["\000" * self.width] - self.screen = ns + ns = len(composed) * ["\000" * self.width] else: self.posxy = 0, self.__offset self.__move(0, self.__offset) ns = self.height * ["\000" * self.width] - self.screen = ns + self.sync_rendered_screen( + RenderedScreen.from_screen_lines(ns, self.posxy), + self.posxy, + ) def __tputs(self, fmt, prog=delayprog): """A Python implementation of the curses tputs function; the diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index 06cddef851b..4d1f936b422 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -9,6 +9,7 @@ import _colorize from collections import deque +from dataclasses import dataclass from io import StringIO from tokenize import TokenInfo as TI from typing import Iterable, Iterator, Match, NamedTuple, Self @@ -16,12 +17,13 @@ from .types import CharBuffer, CharWidths from .trace import trace + ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]") ZERO_WIDTH_BRACKET = re.compile(r"\x01.*?\x02") ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""}) -IDENTIFIERS_AFTER = {"def", "class"} -KEYWORD_CONSTANTS = {"True", "False", "None"} -BUILTINS = {str(name) for name in dir(builtins) if not name.startswith('_')} +IDENTIFIERS_AFTER = frozenset({"def", "class"}) +KEYWORD_CONSTANTS = frozenset({"True", "False", "None"}) +BUILTINS = frozenset({str(name) for name in dir(builtins) if not name.startswith('_')}) def THEME(**kwargs): @@ -59,6 +61,21 @@ class ColorSpan(NamedTuple): tag: str +class StyledChar(NamedTuple): + text: str + width: int + tag: str | None = None + + +def _ascii_control_repr(c: str) -> str | None: + code = ord(c) + if code < 32: + return "^" + chr(code + 64) + if code == 127: + return "^?" + return None + + @functools.cache def str_width(c: str) -> int: if ord(c) < 128: @@ -226,8 +243,8 @@ def gen_colors_from_token_stream( yield ColorSpan(span, "builtin") -keyword_first_sets_match = {"False", "None", "True", "await", "lambda", "not"} -keyword_first_sets_case = {"False", "None", "True"} +keyword_first_sets_match = frozenset({"False", "None", "True", "await", "lambda", "not"}) +keyword_first_sets_case = frozenset({"False", "None", "True"}) def is_soft_keyword_used(*tokens: TI | None) -> bool: @@ -242,7 +259,7 @@ def is_soft_keyword_used(*tokens: TI | None) -> bool: None | TI(T.NEWLINE) | TI(T.INDENT) | TI(string=":"), TI(string="match"), TI(T.NUMBER | T.STRING | T.FSTRING_START | T.TSTRING_START) - | TI(T.OP, string="(" | "*" | "[" | "{" | "~" | "...") + | TI(T.OP, string="(" | "*" | "-" | "+" | "[" | "{" | "~" | "...") ): return True case ( @@ -276,10 +293,71 @@ def is_soft_keyword_used(*tokens: TI | None) -> bool: TI(T.NAME, string=s) ): return not keyword.iskeyword(s) + case ( + None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":" | ";"), + TI(string="lazy"), + TI(string="import") | TI(string="from") + ): + return True case _: return False +def iter_display_chars( + buffer: str, + colors: list[ColorSpan] | None = None, + start_index: int = 0, +) -> Iterator[StyledChar]: + """Yield visible display characters with widths and semantic color tags. + + Note: ``colors`` is consumed in place as spans are processed -- callers + that split a buffer across multiple calls rely on this mutation to track + which spans have already been handled. + """ + + if not buffer: + return + + color_idx = 0 + if colors: + while color_idx < len(colors) and colors[color_idx].span.end < start_index: + color_idx += 1 + + active_tag = None + if colors and color_idx < len(colors) and colors[color_idx].span.start < start_index: + active_tag = colors[color_idx].tag + + for i, c in enumerate(buffer, start_index): + if colors and color_idx < len(colors) and colors[color_idx].span.start == i: + active_tag = colors[color_idx].tag + + if control := _ascii_control_repr(c): + text = control + width = len(control) + elif ord(c) < 128: + text = c + width = 1 + elif unicodedata.category(c).startswith("C"): + text = r"\u%04x" % ord(c) + width = len(text) + else: + text = c + width = str_width(c) + + yield StyledChar(text, width, active_tag) + + if colors and color_idx < len(colors) and colors[color_idx].span.end == i: + color_idx += 1 + active_tag = None + # Check if the next span starts at the same position + if color_idx < len(colors) and colors[color_idx].span.start == i: + active_tag = colors[color_idx].tag + + # Remove consumed spans so callers see the mutation + if color_idx > 0 and colors: + del colors[:color_idx] + + def disp_str( buffer: str, colors: list[ColorSpan] | None = None, @@ -315,53 +393,18 @@ def disp_str( (['\x1b[1;34mw', 'h', 'i', 'l', 'e\x1b[0m', ' ', '1', ':'], [1, 1, 1, 1, 1, 1, 1, 1]) """ + styled_chars = list(iter_display_chars(buffer, colors, start_index)) chars: CharBuffer = [] char_widths: CharWidths = [] - - if not buffer: - return chars, char_widths - - while colors and colors[0].span.end < start_index: - # move past irrelevant spans - colors.pop(0) - theme = THEME(force_color=force_color) - pre_color = "" - post_color = "" - if colors and colors[0].span.start < start_index: - # looks like we're continuing a previous color (e.g. a multiline str) - pre_color = theme[colors[0].tag] - - for i, c in enumerate(buffer, start_index): - if colors and colors[0].span.start == i: # new color starts now - pre_color = theme[colors[0].tag] - - if c == "\x1a": # CTRL-Z on Windows - chars.append(c) - char_widths.append(2) - elif ord(c) < 128: - chars.append(c) - char_widths.append(1) - elif unicodedata.category(c).startswith("C"): - c = r"\u%04x" % ord(c) - chars.append(c) - char_widths.append(len(c)) - else: - chars.append(c) - char_widths.append(str_width(c)) - - if colors and colors[0].span.end == i: # current color ends now - post_color = theme.reset - colors.pop(0) - chars[-1] = pre_color + chars[-1] + post_color - pre_color = "" - post_color = "" - - if colors and colors[0].span.start < i and colors[0].span.end > i: - # even though the current color should be continued, reset it for now. - # the next call to `disp_str()` will revive it. - chars[-1] += theme.reset + for index, styled_char in enumerate(styled_chars): + previous_tag = styled_chars[index - 1].tag if index else None + next_tag = styled_chars[index + 1].tag if index + 1 < len(styled_chars) else None + prefix = theme[styled_char.tag] if styled_char.tag and styled_char.tag != previous_tag else "" + suffix = theme.reset if styled_char.tag and styled_char.tag != next_tag else "" + chars.append(prefix + styled_char.text + suffix) + char_widths.append(styled_char.width) return chars, char_widths @@ -379,13 +422,35 @@ def prev_next_window[T]( """ iterator = iter(iterable) - window = deque((None, next(iterator)), maxlen=3) + try: + first = next(iterator) + except StopIteration: + return + window = deque((None, first), maxlen=3) try: for x in iterator: window.append(x) yield tuple(window) - except Exception: - raise finally: window.append(None) yield tuple(window) + + +@dataclass(frozen=True, slots=True) +class StyleRef: + tag: str | None = None # From THEME().syntax, e.g. "keyword", "builtin" + sgr: str = "" + + @classmethod + def from_tag(cls, tag: str, sgr: str = "") -> Self: + return cls(tag=tag, sgr=sgr) + + @classmethod + def from_sgr(cls, sgr: str) -> Self: + if not sgr: + return cls() + return cls(sgr=sgr) + + @property + def is_plain(self) -> bool: + return self.tag is None and not self.sgr diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 46c6030748b..3768a22ad16 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -25,6 +25,7 @@ import ctypes import types +from dataclasses import dataclass from ctypes.wintypes import ( _COORD, WORD, @@ -37,20 +38,29 @@ SHORT, ) from ctypes import Structure, POINTER, Union +from typing import TYPE_CHECKING + +from _colorize import ANSIColors + from .console import Event, Console -from .trace import trace -from .utils import wlen +from .render import ( + EMPTY_RENDER_LINE, + LineUpdate, + RenderLine, + RenderedScreen, + requires_cursor_resync, + diff_render_lines, + render_cells, +) +from .trace import trace, trace_text from .windows_eventqueue import EventQueue try: - from ctypes import get_last_error, GetLastError, WinDLL, windll, WinError # type: ignore[attr-defined] + from ctypes import get_last_error, WinDLL, windll, WinError # type: ignore[attr-defined] except: # Keep MyPy happy off Windows from ctypes import CDLL as WinDLL, cdll as windll - def GetLastError() -> int: - return 42 - def get_last_error() -> int: return 42 @@ -66,8 +76,6 @@ def __init__(self, err: int | None, descr: str | None = None) -> None: except ImportError: nt = None -TYPE_CHECKING = False - if TYPE_CHECKING: from typing import IO @@ -126,6 +134,17 @@ def __init__(self, err: int | None, descr: str | None = None) -> None: class _error(Exception): pass + +@dataclass(frozen=True, slots=True) +class WindowsRefreshPlan: + grow_lines: int + offset: int + scroll_lines: int + line_updates: tuple[LineUpdate, ...] + cleared_lines: tuple[int, ...] + rendered_screen: RenderedScreen + cursor: tuple[int, int] + def _supports_vt(): try: return nt._supports_virtual_terminal() @@ -149,18 +168,19 @@ def __init__( # Save original console modes so we can recover on cleanup. original_input_mode = DWORD() - GetConsoleMode(InHandle, original_input_mode) + if not GetConsoleMode(InHandle, original_input_mode): + raise WinError(get_last_error()) trace(f'saved original input mode 0x{original_input_mode.value:x}') self.__original_input_mode = original_input_mode.value - SetConsoleMode( + if not SetConsoleMode( OutHandle, ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING, - ) + ): + raise WinError(get_last_error()) - self.screen: list[str] = [] self.width = 80 self.height = 25 self.__offset = 0 @@ -171,74 +191,124 @@ def __init__( # Console I/O is redirected, fallback... self.out = None - def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: + def refresh(self, rendered_screen: RenderedScreen) -> None: """ Refresh the console screen. Parameters: - - screen (list): List of strings representing the screen contents. - - c_xy (tuple): Cursor position (x, y) on the screen. + - rendered_screen: Structured rendered screen contents and cursor. """ - cx, cy = c_xy - - while len(self.screen) < min(len(screen), self.height): - self._hide_cursor() - if self.screen: - self._move_relative(0, len(self.screen) - 1) - self.__write("\n") - self.posxy = 0, len(self.screen) - self.screen.append("") + c_xy = rendered_screen.cursor + trace( + "windows.refresh start cursor={cursor} lines={lines} prev_lines={prev_lines} " + "offset={offset} posxy={posxy}", + cursor=c_xy, + lines=len(rendered_screen.composed_lines), + prev_lines=len(self._rendered_screen.composed_lines), + offset=self.__offset, + posxy=self.posxy, + ) + plan = self.__plan_refresh(rendered_screen, c_xy) + self.__apply_refresh_plan(plan) - px, py = self.posxy - old_offset = offset = self.__offset + def __plan_refresh( + self, + rendered_screen: RenderedScreen, + c_xy: tuple[int, int], + ) -> WindowsRefreshPlan: + cx, cy = c_xy height = self.height + old_offset = offset = self.__offset + prev_composed = self._rendered_screen.composed_lines + previous_lines = list(prev_composed) + next_lines = list(rendered_screen.composed_lines) + line_count = len(next_lines) + + grow_lines = max( + min(line_count, height) - len(prev_composed), + 0, + ) + previous_lines.extend([EMPTY_RENDER_LINE] * grow_lines) - # we make sure the cursor is on the screen, and that we're - # using all of the screen if we can + scroll_lines = 0 if cy < offset: offset = cy elif cy >= offset + height: offset = cy - height + 1 scroll_lines = offset - old_offset + previous_lines.extend([EMPTY_RENDER_LINE] * scroll_lines) + elif offset > 0 and line_count < offset + height: + offset = max(line_count - height, 0) + next_lines.append(EMPTY_RENDER_LINE) + + oldscr = previous_lines[old_offset : old_offset + height] + newscr = next_lines[offset : offset + height] + + line_updates: list[LineUpdate] = [] + px, _ = self.posxy + for y, oldline, newline in zip(range(offset, offset + height), oldscr, newscr): + update = self.__plan_changed_line(y, oldline, newline, px) + if update is not None: + line_updates.append(update) + + cleared_lines = tuple(range(offset + len(newscr), offset + len(oldscr))) + console_rendered_screen = RenderedScreen(tuple(next_lines), c_xy) + trace( + "windows.refresh plan grow={grow} offset={offset} scroll_lines={scroll_lines} " + "updates={updates} clears={clears}", + grow=grow_lines, + offset=offset, + scroll_lines=scroll_lines, + updates=len(line_updates), + clears=len(cleared_lines), + ) + return WindowsRefreshPlan( + grow_lines=grow_lines, + offset=offset, + scroll_lines=scroll_lines, + line_updates=tuple(line_updates), + cleared_lines=cleared_lines, + rendered_screen=console_rendered_screen, + cursor=(cx, cy), + ) - # Scrolling the buffer as the current input is greater than the visible - # portion of the window. We need to scroll the visible portion and the - # entire history - self._scroll(scroll_lines, self._getscrollbacksize()) - self.posxy = self.posxy[0], self.posxy[1] + scroll_lines - self.__offset += scroll_lines + def __apply_refresh_plan(self, plan: WindowsRefreshPlan) -> None: + cx, cy = plan.cursor + trace( + "windows.refresh apply cursor={cursor} updates={updates} clears={clears}", + cursor=plan.cursor, + updates=len(plan.line_updates), + clears=len(plan.cleared_lines), + ) + visual_style = self.begin_redraw_visualization() + screen_line_count = len(self._rendered_screen.composed_lines) - for i in range(scroll_lines): - self.screen.append("") - elif offset > 0 and len(screen) < offset + height: - offset = max(len(screen) - height, 0) - screen.append("") + for _ in range(plan.grow_lines): + self._hide_cursor() + if screen_line_count: + self._move_relative(0, screen_line_count - 1) + self.__write("\n") + self.posxy = 0, screen_line_count + screen_line_count += 1 - oldscr = self.screen[old_offset : old_offset + height] - newscr = screen[offset : offset + height] + if plan.scroll_lines: + self._scroll(plan.scroll_lines, self._getscrollbacksize()) + self.posxy = self.posxy[0], self.posxy[1] + plan.scroll_lines - self.__offset = offset + self.__offset = plan.offset self._hide_cursor() - for ( - y, - oldline, - newline, - ) in zip(range(offset, offset + height), oldscr, newscr): - if oldline != newline: - self.__write_changed_line(y, oldline, newline, px) - - y = len(newscr) - while y < len(oldscr): + for update in plan.line_updates: + self.__apply_line_update(update, visual_style) + + for y in plan.cleared_lines: self._move_relative(0, y) self.posxy = 0, y self._erase_to_end() - y += 1 self._show_cursor() - - self.screen = screen self.move_cursor(cx, cy) + self.sync_rendered_screen(plan.rendered_screen, self.posxy) @property def input_hook(self): @@ -247,49 +317,98 @@ def input_hook(self): if nt is not None and nt._is_inputhook_installed(): return nt._inputhook - def __write_changed_line( - self, y: int, oldline: str, newline: str, px_coord: int - ) -> None: - # this is frustrating; there's no reason to test (say) - # self.dch1 inside the loop -- but alternative ways of - # structuring this function are equally painful (I'm trying to - # avoid writing code generators these days...) - minlen = min(wlen(oldline), wlen(newline)) - x_pos = 0 - x_coord = 0 - - px_pos = 0 - j = 0 - for c in oldline: - if j >= px_coord: - break - j += wlen(c) - px_pos += 1 - - # reuse the oldline as much as possible, but stop as soon as we - # encounter an ESCAPE, because it might be the start of an escape - # sequence - while ( - x_coord < minlen - and oldline[x_pos] == newline[x_pos] - and newline[x_pos] != "\x1b" + def __plan_changed_line( # keep in sync with UnixConsole.__plan_changed_line + self, + y: int, + oldline: RenderLine, + newline: RenderLine, + px_coord: int, + ) -> LineUpdate | None: + diff = diff_render_lines(oldline, newline) + if diff is None: + return None + + start_cell = diff.start_cell + start_x = diff.start_x + if ( + len(diff.old_cells) == 1 + and len(diff.new_cells) == 1 + and diff.old_cells[0].width == diff.new_cells[0].width ): - x_coord += wlen(newline[x_pos]) - x_pos += 1 + changed_cell = diff.new_cells[0] + # Ctrl-Z (SUB) can reach here via RenderLine.from_rendered_text() + # for prompt/message lines, which bypasses iter_display_chars(). + # On Windows, raw \x1a causes console cursor anomalies, so we + # force a cursor resync when it appears. + return LineUpdate( + kind="replace_char", + y=y, + start_cell=start_cell, + start_x=start_x, + cells=diff.new_cells, + char_width=changed_cell.width, + reset_to_margin=( + requires_cursor_resync(diff.new_cells) + or "\x1a" in changed_cell.text + ), + ) + + if diff.old_changed_width == diff.new_changed_width: + return LineUpdate( + kind="replace_span", + y=y, + start_cell=start_cell, + start_x=start_x, + cells=diff.new_cells, + char_width=diff.new_changed_width, + reset_to_margin=( + requires_cursor_resync(diff.new_cells) + or any("\x1a" in cell.text for cell in diff.new_cells) + ), + ) + + suffix_cells = newline.cells[start_cell:] + return LineUpdate( + kind="rewrite_suffix", + y=y, + start_cell=start_cell, + start_x=start_x, + cells=suffix_cells, + char_width=sum(cell.width for cell in suffix_cells), + clear_eol=oldline.width > newline.width, + reset_to_margin=( + requires_cursor_resync(suffix_cells) + or any("\x1a" in cell.text for cell in suffix_cells) + ), + ) - self._hide_cursor() - self._move_relative(x_coord, y) - if wlen(oldline) > wlen(newline): + def __apply_line_update( + self, + update: LineUpdate, + visual_style: str | None = None, + ) -> None: + text = render_cells(update.cells, visual_style) if visual_style else update.text + trace( + "windows.refresh update kind={kind} y={y} x={x} text={text} " + "clear_eol={clear_eol} reset_to_margin={reset}", + kind=update.kind, + y=update.y, + x=update.start_x, + text=trace_text(text), + clear_eol=update.clear_eol, + reset=update.reset_to_margin, + ) + original_y = self.posxy[1] + self._move_relative(update.start_x, update.y) + if update.clear_eol: self._erase_to_end() - self.__write(newline[x_pos:]) - self.posxy = min(wlen(newline), self.width - 1), y + self.__write(text) + self.posxy = min(update.start_x + update.char_width, self.width - 1), update.y - if "\x1b" in newline or y != self.posxy[1] or '\x1a' in newline: - # ANSI escape characters are present, so we can't assume - # anything about the position of the cursor. Moving the cursor - # to the left margin should work to get to a known position. - self.move_cursor(0, y) + if update.reset_to_margin or update.y != original_y: + # Non-SGR terminal controls or vertical movement require a cursor sync. + self.move_cursor(0, update.y) def _scroll( self, top: int, bottom: int, left: int | None = None, right: int | None = None @@ -308,7 +427,7 @@ def _scroll( if not ScrollConsoleScreenBuffer( OutHandle, scroll_rect, None, destination_origin, fill_info ): - raise WinError(GetLastError()) + raise WinError(get_last_error()) def _hide_cursor(self): self.__write("\x1b[?25l") @@ -330,7 +449,7 @@ def _disable_bracketed_paste(self) -> None: def __write(self, text: str) -> None: if "\x1a" in text: - text = ''.join(["^Z" if x == '\x1a' else x for x in text]) + text = text.replace("\x1a", "^Z") if self.out is not None: self.out.write(text.encode(self.encoding, "replace")) @@ -342,30 +461,33 @@ def __write(self, text: str) -> None: def screen_xy(self) -> tuple[int, int]: info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): - raise WinError(GetLastError()) + raise WinError(get_last_error()) return info.dwCursorPosition.X, info.dwCursorPosition.Y def _erase_to_end(self) -> None: self.__write(ERASE_IN_LINE) def prepare(self) -> None: - trace("prepare") - self.screen = [] + trace("windows.prepare") self.height, self.width = self.getheightwidth() self.posxy = 0, 0 - self.__gone_tall = 0 self.__offset = 0 + self.sync_rendered_screen(RenderedScreen.empty(), self.posxy) if self.__vt_support: - SetConsoleMode(InHandle, self.__original_input_mode | ENABLE_VIRTUAL_TERMINAL_INPUT) + if not SetConsoleMode(InHandle, self.__original_input_mode | ENABLE_VIRTUAL_TERMINAL_INPUT): + raise WinError(get_last_error()) self._enable_bracketed_paste() def restore(self) -> None: + trace("windows.restore") if self.__vt_support: + self.__write(ANSIColors.RESET) # Recover to original mode before running REPL self._disable_bracketed_paste() - SetConsoleMode(InHandle, self.__original_input_mode) + if not SetConsoleMode(InHandle, self.__original_input_mode): + raise WinError(get_last_error()) def _move_relative(self, x: int, y: int) -> None: """Moves relative to the current posxy""" @@ -386,8 +508,16 @@ def move_cursor(self, x: int, y: int) -> None: raise ValueError(f"Bad cursor position {x}, {y}") if y < self.__offset or y >= self.__offset + self.height: + trace( + "windows.move_cursor offscreen x={x} y={y} offset={offset} height={height}", + x=x, + y=y, + offset=self.__offset, + height=self.height, + ) self.event_queue.insert(Event("scroll", "")) else: + trace("windows.move_cursor x={x} y={y}", x=x, y=y) self._move_relative(x, y) self.posxy = x, y @@ -402,7 +532,7 @@ def getheightwidth(self) -> tuple[int, int]: and width of the terminal window in characters.""" info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): - raise WinError(GetLastError()) + raise WinError(get_last_error()) return ( info.srWindow.Bottom - info.srWindow.Top + 1, info.srWindow.Right - info.srWindow.Left + 1, @@ -411,7 +541,7 @@ def getheightwidth(self) -> tuple[int, int]: def _getscrollbacksize(self) -> int: info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): - raise WinError(GetLastError()) + raise WinError(get_last_error()) return info.srWindow.Bottom # type: ignore[no-any-return] @@ -419,7 +549,7 @@ def _read_input(self) -> INPUT_RECORD | None: rec = INPUT_RECORD() read = DWORD() if not ReadConsoleInput(InHandle, rec, 1, read): - raise WinError(GetLastError()) + raise WinError(get_last_error()) return rec @@ -429,7 +559,7 @@ def _read_input_bulk( rec = (n * INPUT_RECORD)() read = DWORD() if not ReadConsoleInput(InHandle, rec, n, read): - raise WinError(GetLastError()) + raise WinError(get_last_error()) return rec, read.value @@ -508,17 +638,20 @@ def beep(self) -> None: def clear(self) -> None: """Wipe the screen""" + trace("windows.clear") self.__write(CLEAR) self.posxy = 0, 0 - self.screen = [] + self.sync_rendered_screen(RenderedScreen.empty(), self.posxy) def finish(self) -> None: """Move the cursor to the end of the display and otherwise get ready for end. XXX could be merged with restore? Hmm.""" - y = len(self.screen) - 1 - while y >= 0 and not self.screen[y]: + rendered_lines = self._rendered_screen.composed_lines + y = len(rendered_lines) - 1 + while y >= 0 and not rendered_lines[y].text: y -= 1 self._move_relative(0, min(y, self.height + self.__offset - 1)) + self.__write(ANSIColors.RESET) self.__write("\r\n") def flushoutput(self) -> None: @@ -531,7 +664,7 @@ def flushoutput(self) -> None: def forgetinput(self) -> None: """Forget all pending, but not yet processed input.""" if not FlushConsoleInputBuffer(InHandle): - raise WinError(GetLastError()) + raise WinError(get_last_error()) def getpending(self) -> Event: """Return the characters that have been typed but not yet @@ -558,7 +691,7 @@ def getpending(self) -> Event: # ignore SHIFT_PRESSED and special keys continue if ch == "\r": - ch += "\n" + ch = "\n" e.data += ch return e @@ -585,6 +718,7 @@ def wait(self, timeout: float | None) -> bool: ) def repaint(self) -> None: + trace("windows.repaint unsupported") raise NotImplementedError("No repaint support") diff --git a/Lib/argparse.py b/Lib/argparse.py index a106a718a99..bae204ac5a4 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -64,7 +64,6 @@ still considered an implementation detail.) """ -__version__ = '1.1' __all__ = [ 'ArgumentParser', 'ArgumentError', @@ -90,8 +89,10 @@ import os as _os import re as _re import sys as _sys +from gettext import gettext as _ +from gettext import ngettext -from gettext import gettext as _, ngettext +lazy import _colorize SUPPRESS = '==SUPPRESS==' @@ -157,6 +158,17 @@ def _identity(value): # Formatting Help # =============== +class _ColorlessTheme: + # A 'fake' theme for no colors + def __getattr__(self, name): + # _colorize's no_color themes are just all empty strings + # by directly using empty strings the import is avoided + if name.startswith("_"): + raise AttributeError(name) + return "" + +_colorless_theme = _ColorlessTheme() + class HelpFormatter(object): """Formatter for generating usage messages and argument help strings. @@ -171,7 +183,6 @@ def __init__( indent_increment=2, max_help_position=24, width=None, - color=True, ): # default setting for width if width is None: @@ -179,7 +190,6 @@ def __init__( width = shutil.get_terminal_size().columns width -= 2 - self._set_color(color) self._prog = prog self._indent_increment = indent_increment self._max_help_position = min(max_help_position, @@ -196,15 +206,35 @@ def __init__( self._whitespace_matcher = _re.compile(r'\s+', _re.ASCII) self._long_break_matcher = _re.compile(r'\n\n\n+') - def _set_color(self, color): - from _colorize import can_colorize, decolor, get_theme + self._set_color(False) + + def _set_color(self, color, *, file=None): + # Set a new color setting and file, clear caches for theme and decolor + self._theme_color = color + self._theme_file = file + self._cached_theme = None + self._cached_decolor = None - if color and can_colorize(): - self._theme = get_theme(force_color=True).argparse - self._decolor = decolor + def _get_theme_and_decolor(self): + # If self._theme_color is false, this prevents _colorize from importing + if self._theme_color and _colorize.can_colorize(file=self._theme_file): + self._cached_theme = _colorize.get_theme(force_color=True).argparse + self._cached_decolor = _colorize.decolor else: - self._theme = get_theme(force_no_color=True).argparse - self._decolor = _identity + self._cached_theme = _colorless_theme + self._cached_decolor = _identity + + @property + def _theme(self): + if self._cached_theme is None: + self._get_theme_and_decolor() + return self._cached_theme + + @property + def _decolor(self): + if self._cached_decolor is None: + self._get_theme_and_decolor() + return self._cached_decolor # =============================== # Section and indentation methods @@ -522,7 +552,27 @@ def _format_text(self, text): text = text % dict(prog=self._prog) text_width = max(self._width - self._current_indent, 11) indent = ' ' * self._current_indent - return self._fill_text(text, text_width, indent) + '\n\n' + text = self._fill_text(text, text_width, indent) + text = self._apply_text_markup(text) + return text + '\n\n' + + def _apply_text_markup(self, text): + """Apply color markup to text. + + Supported markup: + `...` or ``...`` - inline code (rendered with prog_extra color) + + When colors are disabled, backticks are preserved as-is. + """ + t = self._theme + if not t.reset: + return text + text = _re.sub( + r'(`{1,2})([^`]+)\1', + rf'{t.prog_extra}\2{t.reset}', + text, + ) + return text def _format_action(self, action): # determine the required width and the entry label @@ -661,9 +711,9 @@ def _format_args(self, action, default_metavar): return result def _expand_help(self, action): - help_string = self._get_help_string(action) + help_string = str(self._get_help_string(action)) if '%' not in help_string: - return help_string + return self._apply_text_markup(help_string) params = dict(vars(action), prog=self._prog) for name in list(params): value = params[name] @@ -673,7 +723,43 @@ def _expand_help(self, action): params[name] = value.__name__ if params.get('choices') is not None: params['choices'] = ', '.join(map(str, params['choices'])) - return help_string % params + + t = self._theme + + result = help_string % params + + if not t.reset: + return result + + # Match format specifiers like: %s, %d, %(key)s, etc. + fmt_spec = r''' + % + (?: + % # %% escape + | + (?:\((?P[^)]*)\))? # key + [-#0\ +]* # flags + (?:\*|\d+)? # width + (?:\.(?:\*|\d+))? # precision + [hlL]? # length modifier + [diouxXeEfFgGcrsa] # conversion type + ) + ''' + + def colorize(match): + spec, key = match.group(0, 'key') + if spec == '%%': + return '%' + if key is not None: + # %(key)... - format and colorize + formatted = spec % {key: params[key]} + return f'{t.interpolated_value}{formatted}{t.reset}' + # bare %s etc. - format with full params dict, no colorization + return spec % params + + return self._apply_text_markup( + _re.sub(fmt_spec, colorize, help_string, flags=_re.VERBOSE) + ) def _iter_indented_subactions(self, action): try: @@ -690,7 +776,35 @@ def _split_lines(self, text, width): # The textwrap module is used only for formatting help. # Delay its import for speeding up the common usage of argparse. import textwrap - return textwrap.wrap(text, width) + decolored = self._decolor(text) + if decolored == text: + return textwrap.wrap(text, width) + + # gh-142035: colors inflate textwrap's length counts, so wrap + # the decolored text and re-apply colors per word; if textwrap + # split a word, keep the plain lines (colors can't be mapped). + plain = self._whitespace_matcher.sub(' ', decolored).strip() + if not plain: + # nothing visible to wrap (e.g. an empty interpolated value) + return [text] + plain_lines = textwrap.wrap(plain, width) + plain_words = plain.split() + colored_words = text.split() + # Drop escape-only tokens (e.g. an empty interpolated value). + if len(colored_words) != len(plain_words): + colored_words = [ + word for word in colored_words if self._decolor(word) + ] + colored_lines = [] + start = 0 + for plain_line in plain_lines: + plain_line_words = plain_line.split() + end = start + len(plain_line_words) + if plain_words[start:end] != plain_line_words: + return plain_lines + colored_lines.append(' '.join(colored_words[start:end])) + start = end + return colored_lines def _fill_text(self, text, width, indent): text = self._whitespace_matcher.sub(' ', text).strip() @@ -750,7 +864,14 @@ def _get_help_string(self, action): ): defaulting_nargs = (OPTIONAL, ZERO_OR_MORE) if action.option_strings or action.nargs in defaulting_nargs: - help += _(' (default: %(default)s)') + t = self._theme + default_str = _(" (default: %(default)s)") + prefix, suffix = default_str.split("%(default)s") + help += ( + f" {t.default}{prefix.lstrip()}{t.reset}" + f"%(default)s" + f"{t.default}{suffix}{t.reset}" + ) return help @@ -934,15 +1055,26 @@ def __init__(self, deprecated=False): _option_strings = [] + neg_option_strings = [] for option_string in option_strings: _option_strings.append(option_string) - if option_string.startswith('--'): - if option_string.startswith('--no-'): + if len(option_string) > 2 and option_string[0] == option_string[1]: + # two-dash long option: '--foo' -> '--no-foo' + if option_string.startswith('no-', 2): raise ValueError(f'invalid option name {option_string!r} ' f'for BooleanOptionalAction') - option_string = '--no-' + option_string[2:] + option_string = option_string[:2] + 'no-' + option_string[2:] _option_strings.append(option_string) + neg_option_strings.append(option_string) + elif len(option_string) > 2 and option_string[0] != option_string[1]: + # single-dash long option: '-foo' -> '-nofoo' + if option_string.startswith('no', 1): + raise ValueError(f'invalid option name {option_string!r} ' + f'for BooleanOptionalAction') + option_string = option_string[:1] + 'no' + option_string[1:] + _option_strings.append(option_string) + neg_option_strings.append(option_string) super().__init__( option_strings=_option_strings, @@ -952,11 +1084,12 @@ def __init__(self, required=required, help=help, deprecated=deprecated) + self.neg_option_strings = neg_option_strings def __call__(self, parser, namespace, values, option_string=None): if option_string in self.option_strings: - setattr(namespace, self.dest, not option_string.startswith('--no-')) + setattr(namespace, self.dest, option_string not in self.neg_option_strings) def format_usage(self): return ' | '.join(self.option_strings) @@ -1662,29 +1795,35 @@ def _get_positional_kwargs(self, dest, **kwargs): def _get_optional_kwargs(self, *args, **kwargs): # determine short and long option strings option_strings = [] - long_option_strings = [] for option_string in args: # error on strings that don't start with an appropriate prefix - if not option_string[0] in self.prefix_chars: + if option_string[0] not in self.prefix_chars: raise ValueError( f'invalid option string {option_string!r}: ' f'must start with a character {self.prefix_chars!r}') - - # strings starting with two prefix characters are long options option_strings.append(option_string) - if len(option_string) > 1 and option_string[1] in self.prefix_chars: - long_option_strings.append(option_string) # infer destination, '--foo-bar' -> 'foo_bar' and '-x' -> 'x' dest = kwargs.pop('dest', None) if dest is None: - if long_option_strings: - dest_option_string = long_option_strings[0] - else: - dest_option_string = option_strings[0] - dest = dest_option_string.lstrip(self.prefix_chars) + priority = 0 + for option_string in option_strings: + if len(option_string) <= 2: + # short option: '-x' -> 'x' + if priority < 1: + dest = option_string.lstrip(self.prefix_chars) + priority = 1 + elif option_string[1] not in self.prefix_chars: + # single-dash long option: '-foo' -> 'foo' + if priority < 2: + dest = option_string.lstrip(self.prefix_chars) + priority = 2 + else: + # two-dash long option: '--foo' -> 'foo' + dest = option_string.lstrip(self.prefix_chars) + break if not dest: - msg = f'dest= is required for options like {option_string!r}' + msg = f'dest= is required for options like {repr(option_strings)[1:-1]}' raise TypeError(msg) dest = dest.replace('-', '_') @@ -1862,7 +2001,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): - exit_on_error -- Determines whether or not ArgumentParser exits with error info when an error occurs - suggest_on_error - Enables suggestions for mistyped argument choices - and subparser names (default: ``False``) + and subparser names (default: ``True``) - color - Allow color output in help messages (default: ``False``) """ @@ -1881,7 +2020,7 @@ def __init__(self, allow_abbrev=True, exit_on_error=True, *, - suggest_on_error=False, + suggest_on_error=True, color=True, ): superinit = super(ArgumentParser, self).__init__ @@ -1963,14 +2102,16 @@ def add_subparsers(self, **kwargs): self._subparsers = self._positionals # prog defaults to the usage message of this parser, skipping - # optional arguments and with no "usage:" prefix + # non-required optional arguments and with no "usage:" prefix if kwargs.get('prog') is None: # Create formatter without color to avoid storing ANSI codes in prog formatter = self.formatter_class(prog=self.prog) formatter._set_color(False) positionals = self._get_positional_actions() + required_optionals = [action for action in self._get_optional_actions() + if action.required] groups = self._mutually_exclusive_groups - formatter.add_usage(None, positionals, groups, '') + formatter.add_usage(None, required_optionals + positionals, groups, '') kwargs['prog'] = formatter.format_help().strip() # create the parsers action and add it to the positionals list @@ -2437,7 +2578,7 @@ def _parse_optional(self, arg_string): return None # if it doesn't start with a prefix, it was meant to be positional - if not arg_string[0] in self.prefix_chars: + if arg_string[0] not in self.prefix_chars: return None # if the option string is present in the parser, return the action @@ -2546,7 +2687,7 @@ def _get_nargs_pattern(self, action): # allow any number of options or arguments elif nargs == REMAINDER: - nargs_pattern = '([AO]*)' if option else '(.*)' + nargs_pattern = '(.*)' # allow one argument followed by any number of options or arguments elif nargs == PARSER: @@ -2699,14 +2840,16 @@ def _check_value(self, action, value): # Help-formatting methods # ======================= - def format_usage(self): - formatter = self._get_formatter() + def format_usage(self, formatter=None): + if formatter is None: + formatter = self._get_formatter() formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups) return formatter.format_help() - def format_help(self): - formatter = self._get_formatter() + def format_help(self, formatter=None): + if formatter is None: + formatter = self._get_formatter() # usage formatter.add_usage(self.usage, self._actions, @@ -2728,16 +2871,20 @@ def format_help(self): # determine help from format above return formatter.format_help() - def _get_formatter(self): + def _get_formatter(self, file=None): formatter = self.formatter_class(prog=self.prog) - formatter._set_color(self.color) + formatter._set_color(self.color, file=file) return formatter def _get_validation_formatter(self): # Return cached formatter for read-only validation operations # (_expand_help and _format_args). Avoids repeated slow _set_color calls. + # Validation never renders output, so force color off to avoid + # importing _colorize during add_argument. if self._cached_formatter is None: - self._cached_formatter = self._get_formatter() + formatter = self.formatter_class(prog=self.prog) + formatter._set_color(False) + self._cached_formatter = formatter return self._cached_formatter # ===================== @@ -2747,12 +2894,26 @@ def _get_validation_formatter(self): def print_usage(self, file=None): if file is None: file = _sys.stdout - self._print_message(self.format_usage(), file) + formatter = self._get_formatter(file=file) + try: + usage_text = self.format_usage(formatter=formatter) + except TypeError: + # Backward compatibility for formatter classes that + # do not accept the 'formatter' keyword argument. + usage_text = self.format_usage() + self._print_message(usage_text, file) def print_help(self, file=None): if file is None: file = _sys.stdout - self._print_message(self.format_help(), file) + formatter = self._get_formatter(file=file) + try: + help_text = self.format_help(formatter=formatter) + except TypeError: + # Backward compatibility for formatter classes that + # do not accept the 'formatter' keyword argument. + help_text = self.format_help() + self._print_message(help_text, file) def _print_message(self, message, file=None): if message: @@ -2762,6 +2923,13 @@ def _print_message(self, message, file=None): except (AttributeError, OSError): pass + def _get_theme(self, file=None): + # If self.color is False, _colorize is not imported + if self.color and _colorize.can_colorize(file=file): + return _colorize.get_theme(force_color=True).argparse + else: + return _colorless_theme + # =============== # Exiting methods # =============== @@ -2781,9 +2949,26 @@ def error(self, message): should either exit or raise an exception. """ self.print_usage(_sys.stderr) + theme = self._get_theme(file=_sys.stderr) + fmt = _('%(prog)s: error: %(message)s\n') + fmt = fmt.replace('error: %(message)s', + f'{theme.error}error:{theme.reset} {theme.message}%(message)s{theme.reset}') + args = {'prog': self.prog, 'message': message} - self.exit(2, _('%(prog)s: error: %(message)s\n') % args) + self.exit(2, fmt % args) def _warning(self, message): + theme = self._get_theme(file=_sys.stderr) + fmt = _('%(prog)s: warning: %(message)s\n') + fmt = fmt.replace('warning: %(message)s', + f'{theme.warning}warning:{theme.reset} {theme.message}%(message)s{theme.reset}') args = {'prog': self.prog, 'message': message} - self._print_message(_('%(prog)s: warning: %(message)s\n') % args, _sys.stderr) + self._print_message(fmt % args, _sys.stderr) + +def __getattr__(name): + if name == "__version__": + from warnings import _deprecated + + _deprecated("__version__", remove=(3, 20)) + return "1.1" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index 44e14771b11..cbb052630d7 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -12,13 +12,16 @@ import types import warnings -from _colorize import get_theme -from _pyrepl.console import InteractiveColoredConsole +try: + from _colorize import get_theme + from _pyrepl.console import InteractiveColoredConsole as InteractiveConsole +except ModuleNotFoundError: + from code import InteractiveConsole from . import futures -class AsyncIOInteractiveConsole(InteractiveColoredConsole): +class AsyncIOInteractiveConsole(InteractiveConsole): def __init__(self, locals, loop): super().__init__(locals, filename="") @@ -96,7 +99,7 @@ def run(self): console.write(banner) - if not sys.flags.isolated and (startup_path := os.getenv("PYTHONSTARTUP")): + if not sys.flags.ignore_environment and (startup_path := os.getenv("PYTHONSTARTUP")): sys.audit("cpython.run_startup", startup_path) try: import tokenize @@ -112,13 +115,17 @@ def run(self): if CAN_USE_PYREPL: theme = get_theme().syntax ps1 = f"{theme.prompt}{ps1}{theme.reset}" - console.write(f"{ps1}import asyncio\n") + import_line = f'{theme.keyword}import{theme.reset} asyncio' + else: + import_line = "import asyncio" + console.write(f"{ps1}{import_line}\n") if CAN_USE_PYREPL: from _pyrepl.simple_interact import ( run_multiline_interactive_console, ) try: + sys.ps1 = ps1 run_multiline_interactive_console(console) except SystemExit: # expected via the `exit` and `quit` commands @@ -152,24 +159,35 @@ def interrupt(self) -> None: parser = argparse.ArgumentParser( prog="python3 -m asyncio", description="Interactive asyncio shell and CLI tools", - color=True, ) subparsers = parser.add_subparsers(help="sub-commands", dest="command") ps = subparsers.add_parser( "ps", help="Display a table of all pending tasks in a process" ) ps.add_argument("pid", type=int, help="Process ID to inspect") + ps.add_argument( + "--retries", + type=int, + default=3, + help="Number of retries on transient attach errors", + ) pstree = subparsers.add_parser( "pstree", help="Display a tree of all pending tasks in a process" ) pstree.add_argument("pid", type=int, help="Process ID to inspect") + pstree.add_argument( + "--retries", + type=int, + default=3, + help="Number of retries on transient attach errors", + ) args = parser.parse_args() match args.command: case "ps": - asyncio.tools.display_awaited_by_tasks_table(args.pid) + asyncio.tools.display_awaited_by_tasks_table(args.pid, retries=args.retries) sys.exit(0) case "pstree": - asyncio.tools.display_awaited_by_tasks_tree(args.pid) + asyncio.tools.display_awaited_by_tasks_tree(args.pid, retries=args.retries) sys.exit(0) case None: pass # continue to the interactive shell @@ -182,20 +200,26 @@ def interrupt(self) -> None: sys.audit("cpython.run_stdin") - if os.getenv('PYTHON_BASIC_REPL'): + if not sys.flags.ignore_environment and os.getenv('PYTHON_BASIC_REPL'): CAN_USE_PYREPL = False else: - from _pyrepl.main import CAN_USE_PYREPL + try: + from _pyrepl.main import CAN_USE_PYREPL + except ModuleNotFoundError: + CAN_USE_PYREPL = False return_code = 0 loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - repl_locals = {'asyncio': asyncio} - for key in {'__name__', '__package__', - '__loader__', '__spec__', - '__builtins__', '__file__'}: - repl_locals[key] = locals()[key] + repl_locals = { + 'asyncio': asyncio, + '__name__': __name__, + '__package__': None, + '__loader__': __loader__, + '__spec__': None, + '__builtins__': __builtins__, + } console = AsyncIOInteractiveConsole(repl_locals, loop) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 6d1f1f84323..7b6a6702338 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -14,19 +14,21 @@ """ import collections +import contextvars import collections.abc import concurrent.futures import errno import heapq import itertools +import math import os import socket import stat import subprocess +import sys import threading import time import traceback -import sys import warnings import weakref @@ -289,6 +291,7 @@ def __init__(self, loop, sockets, protocol_factory, ssl_context, backlog, self._ssl_shutdown_timeout = ssl_shutdown_timeout self._serving = False self._serving_forever_fut = None + self._context = contextvars.copy_context() def __repr__(self): return f'<{self.__class__.__name__} sockets={self.sockets!r}>' @@ -318,7 +321,7 @@ def _start_serving(self): self._loop._start_serving( self._protocol_factory, sock, self._ssl_context, self, self._backlog, self._ssl_handshake_timeout, - self._ssl_shutdown_timeout) + self._ssl_shutdown_timeout, context=self._context) def get_loop(self): return self._loop @@ -380,6 +383,7 @@ async def serve_forever(self): except exceptions.CancelledError: try: self.close() + self.close_clients() await self.wait_closed() finally: raise @@ -507,7 +511,8 @@ def _make_ssl_transport( extra=None, server=None, ssl_handshake_timeout=None, ssl_shutdown_timeout=None, - call_connection_made=True): + call_connection_made=True, + context=None): """Create SSL transport.""" raise NotImplementedError @@ -949,7 +954,7 @@ async def sock_sendfile(self, sock, file, offset=0, count=None, try: return await self._sock_sendfile_native(sock, file, offset, count) - except exceptions.SendfileNotAvailableError as exc: + except exceptions.SendfileNotAvailableError: if not fallback: raise return await self._sock_sendfile_fallback(sock, file, @@ -1211,21 +1216,28 @@ async def _create_connection_transport( self, sock, protocol_factory, ssl, server_hostname, server_side=False, ssl_handshake_timeout=None, - ssl_shutdown_timeout=None): + ssl_shutdown_timeout=None, context=None): - sock.setblocking(False) - - protocol = protocol_factory() - waiter = self.create_future() - if ssl: - sslcontext = None if isinstance(ssl, bool) else ssl - transport = self._make_ssl_transport( - sock, protocol, sslcontext, waiter, - server_side=server_side, server_hostname=server_hostname, - ssl_handshake_timeout=ssl_handshake_timeout, - ssl_shutdown_timeout=ssl_shutdown_timeout) - else: - transport = self._make_socket_transport(sock, protocol, waiter) + try: + sock.setblocking(False) + context = context if context is not None else contextvars.copy_context() + + protocol = protocol_factory() + waiter = self.create_future() + if ssl: + sslcontext = None if isinstance(ssl, bool) else ssl + transport = self._make_ssl_transport( + sock, protocol, sslcontext, waiter, + server_side=server_side, server_hostname=server_hostname, + ssl_handshake_timeout=ssl_handshake_timeout, + ssl_shutdown_timeout=ssl_shutdown_timeout, + context=context) + else: + transport = self._make_socket_transport(sock, protocol, waiter, context=context) + except: + # gh-153133: close the socket if the transport is never created. + sock.close() + raise try: await waiter @@ -1270,7 +1282,7 @@ async def sendfile(self, transport, file, offset=0, count=None, try: return await self._sendfile_native(transport, file, offset, count) - except exceptions.SendfileNotAvailableError as exc: + except exceptions.SendfileNotAvailableError: if not fallback: raise @@ -2021,7 +2033,10 @@ def _run_once(self): event_list = None # Handle 'later' callbacks that are ready. - end_time = self.time() + self._clock_resolution + now = self.time() + # Ensure that `end_time` is strictly increasing + # when the clock resolution is too small. + end_time = now + max(self._clock_resolution, math.ulp(now)) while self._scheduled: handle = self._scheduled[0] if handle._when >= end_time: diff --git a/Lib/asyncio/base_subprocess.py b/Lib/asyncio/base_subprocess.py index 224b1883808..0423bc100b2 100644 --- a/Lib/asyncio/base_subprocess.py +++ b/Lib/asyncio/base_subprocess.py @@ -22,11 +22,10 @@ def __init__(self, loop, protocol, args, shell, self._proc = None self._pid = None self._returncode = None - self._exit_waiters = [] + self._exit_waiters = set() self._pending_calls = collections.deque() self._pipes = {} self._finished = False - self._pipes_connected = False if stdin == subprocess.PIPE: self._pipes[0] = None @@ -166,6 +165,9 @@ def kill(self): else: def send_signal(self, signal): self._check_proc() + if self._returncode is not None: + # The process already exited + return try: os.kill(self._proc.pid, signal) except ProcessLookupError: @@ -209,12 +211,19 @@ async def _connect_pipes(self, waiter): except (SystemExit, KeyboardInterrupt): raise except BaseException as exc: + # Close any pipes that were already connected before the + # error/cancellation to avoid leaking file descriptors. + for proto in self._pipes.values(): + if proto is not None: + proto.pipe.close() + for raw_pipe in (proc.stdin, proc.stdout, proc.stderr): + if raw_pipe is not None: + raw_pipe.close() if waiter is not None and not waiter.cancelled(): waiter.set_exception(exc) else: if waiter is not None and not waiter.cancelled(): waiter.set_result(None) - self._pipes_connected = True def _call(self, cb, *data): if self._pending_calls is not None: @@ -235,6 +244,7 @@ def _process_exited(self, returncode): if self._loop.get_debug(): logger.info('%r exited with return code %r', self, returncode) self._returncode = returncode + if self._proc.returncode is None: # asyncio uses a child watcher: copy the status into the Popen # object. On Python 3.6, it is required to avoid a ResourceWarning. @@ -243,6 +253,13 @@ def _process_exited(self, returncode): self._try_finish() + # gh-119710: Wake up futures waiting for wait() as soon as the process + # exits. + for waiter in self._exit_waiters: + if not waiter.done(): + waiter.set_result(returncode) + self._exit_waiters = None + async def _wait(self): """Wait until the process exit and return the process return code. @@ -251,22 +268,18 @@ async def _wait(self): return self._returncode waiter = self._loop.create_future() - self._exit_waiters.append(waiter) - return await waiter + self._exit_waiters.add(waiter) + try: + return await waiter + finally: + if self._exit_waiters is not None: + self._exit_waiters.discard(waiter) def _try_finish(self): assert not self._finished if self._returncode is None: return - if not self._pipes_connected: - # self._pipes_connected can be False if not all pipes were connected - # because either the process failed to start or the self._connect_pipes task - # got cancelled. In this broken state we consider all pipes disconnected and - # to avoid hanging forever in self._wait as otherwise _exit_waiters - # would never be woken up, we wake them up here. - for waiter in self._exit_waiters: - if not waiter.done(): - waiter.set_result(self._returncode) + if all(p is not None and p.disconnected for p in self._pipes.values()): self._finished = True @@ -276,11 +289,6 @@ def _call_connection_lost(self, exc): try: self._protocol.connection_lost(exc) finally: - # wake up futures waiting for wait() - for waiter in self._exit_waiters: - if not waiter.done(): - waiter.set_result(self._returncode) - self._exit_waiters = None self._loop = None self._proc = None self._protocol = None diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py index 35f7fa62140..d0a54f3625b 100644 --- a/Lib/asyncio/graph.py +++ b/Lib/asyncio/graph.py @@ -58,7 +58,8 @@ def _build_graph_for_future( while coro is not None: if hasattr(coro, 'cr_await'): # A native coroutine or duck-type compatible iterator - st.append(FrameCallGraphEntry(coro.cr_frame)) + if coro.cr_frame is not None: + st.append(FrameCallGraphEntry(coro.cr_frame)) coro = coro.cr_await elif hasattr(coro, 'ag_await'): # A native async generator or duck-type compatible iterator @@ -154,7 +155,9 @@ def capture_call_graph( f = sys._getframe(depth) if limit != 0 else None try: while f is not None: - is_async = f.f_generator is not None + # gh-156988: sync gen should not clear the call chain + is_async = isinstance( + f.f_generator, (types.CoroutineType, types.AsyncGeneratorType)) call_stack.append(FrameCallGraphEntry(f)) if is_async: diff --git a/Lib/asyncio/proactor_events.py b/Lib/asyncio/proactor_events.py index 6b94975d004..7adb09f3fce 100644 --- a/Lib/asyncio/proactor_events.py +++ b/Lib/asyncio/proactor_events.py @@ -62,6 +62,7 @@ def __init__(self, loop, sock, protocol, waiter=None, self._closing = False # Set when close() called. self._called_connection_lost = False self._eof_written = False + self._empty_waiter = None if self._server is not None: self._server._attach(self) self._loop.call_soon(self._protocol.connection_made, self) @@ -331,10 +332,6 @@ class _ProactorBaseWritePipeTransport(_ProactorBasePipeTransport, _start_tls_compatible = True - def __init__(self, *args, **kw): - super().__init__(*args, **kw) - self._empty_waiter = None - def write(self, data): if not isinstance(data, (bytes, bytearray, memoryview)): raise TypeError( @@ -465,7 +462,6 @@ class _ProactorDatagramTransport(_ProactorBasePipeTransport, def __init__(self, loop, sock, protocol, address=None, waiter=None, extra=None): self._address = address - self._empty_waiter = None self._buffer_size = 0 # We don't need to call _protocol.connection_made() since our base # constructor does it for us. @@ -575,6 +571,15 @@ def _loop_reading(self, fut=None): else: self._read_fut = self._loop._proactor.recvfrom(self._sock, self.max_size) + except ConnectionResetError as exc: + # WSARecvFrom() reports a stale ICMP port unreachable + # notification as a synchronous ConnectionResetError when the + # same socket was used to send to an address that is not + # listening. This is transient, so reschedule the read loop + # instead of leaving it dead. + self._protocol.error_received(exc) + if not self._closing: + self._loop.call_soon(self._loop_reading) except OSError as exc: self._protocol.error_received(exc) except exceptions.CancelledError: @@ -642,7 +647,7 @@ def __init__(self, proactor): signal.set_wakeup_fd(self._csock.fileno()) def _make_socket_transport(self, sock, protocol, waiter=None, - extra=None, server=None): + extra=None, server=None, context=None): return _ProactorSocketTransport(self, sock, protocol, waiter, extra, server) @@ -651,7 +656,7 @@ def _make_ssl_transport( *, server_side=False, server_hostname=None, extra=None, server=None, ssl_handshake_timeout=None, - ssl_shutdown_timeout=None): + ssl_shutdown_timeout=None, context=None): ssl_protocol = sslproto.SSLProtocol( self, protocol, sslcontext, waiter, server_side, server_hostname, @@ -733,7 +738,7 @@ async def sock_accept(self, sock): async def _sock_sendfile_native(self, sock, file, offset, count): try: fileno = file.fileno() - except (AttributeError, io.UnsupportedOperation) as err: + except (AttributeError, io.UnsupportedOperation): raise exceptions.SendfileNotAvailableError("not a regular file") try: fsize = os.fstat(fileno).st_size @@ -836,7 +841,7 @@ def _write_to_self(self): def _start_serving(self, protocol_factory, sock, sslcontext=None, server=None, backlog=100, ssl_handshake_timeout=None, - ssl_shutdown_timeout=None): + ssl_shutdown_timeout=None, context=None): def loop(f=None): try: diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 7c2062e3dd5..326d64025f3 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -67,10 +67,10 @@ def __init__(self, selector=None): self._transports = weakref.WeakValueDictionary() def _make_socket_transport(self, sock, protocol, waiter=None, *, - extra=None, server=None): + extra=None, server=None, context=None): self._ensure_fd_no_transport(sock) return _SelectorSocketTransport(self, sock, protocol, waiter, - extra, server) + extra, server, context=context) def _make_ssl_transport( self, rawsock, protocol, sslcontext, waiter=None, @@ -78,16 +78,17 @@ def _make_ssl_transport( extra=None, server=None, ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT, ssl_shutdown_timeout=constants.SSL_SHUTDOWN_TIMEOUT, + context=None, ): self._ensure_fd_no_transport(rawsock) ssl_protocol = sslproto.SSLProtocol( self, protocol, sslcontext, waiter, server_side, server_hostname, ssl_handshake_timeout=ssl_handshake_timeout, - ssl_shutdown_timeout=ssl_shutdown_timeout + ssl_shutdown_timeout=ssl_shutdown_timeout, ) _SelectorSocketTransport(self, rawsock, ssl_protocol, - extra=extra, server=server) + extra=extra, server=server, context=context) return ssl_protocol._app_transport def _make_datagram_transport(self, sock, protocol, @@ -159,16 +160,16 @@ def _write_to_self(self): def _start_serving(self, protocol_factory, sock, sslcontext=None, server=None, backlog=100, ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT, - ssl_shutdown_timeout=constants.SSL_SHUTDOWN_TIMEOUT): + ssl_shutdown_timeout=constants.SSL_SHUTDOWN_TIMEOUT, context=None): self._add_reader(sock.fileno(), self._accept_connection, protocol_factory, sock, sslcontext, server, backlog, - ssl_handshake_timeout, ssl_shutdown_timeout) + ssl_handshake_timeout, ssl_shutdown_timeout, context) def _accept_connection( self, protocol_factory, sock, sslcontext=None, server=None, backlog=100, ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT, - ssl_shutdown_timeout=constants.SSL_SHUTDOWN_TIMEOUT): + ssl_shutdown_timeout=constants.SSL_SHUTDOWN_TIMEOUT, context=None): # This method is only called once for each event loop tick where the # listening socket has triggered an EVENT_READ. There may be multiple # connections waiting for an .accept() so it is called in a loop. @@ -204,21 +205,22 @@ def _accept_connection( self._start_serving, protocol_factory, sock, sslcontext, server, backlog, ssl_handshake_timeout, - ssl_shutdown_timeout) + ssl_shutdown_timeout, context) else: raise # The event loop will catch, log and ignore it. else: extra = {'peername': addr} + conn_context = context.copy() if context is not None else None accept = self._accept_connection2( protocol_factory, conn, extra, sslcontext, server, - ssl_handshake_timeout, ssl_shutdown_timeout) - self.create_task(accept) + ssl_handshake_timeout, ssl_shutdown_timeout, context=conn_context) + self.create_task(accept, context=conn_context) async def _accept_connection2( self, protocol_factory, conn, extra, sslcontext=None, server=None, ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT, - ssl_shutdown_timeout=constants.SSL_SHUTDOWN_TIMEOUT): + ssl_shutdown_timeout=constants.SSL_SHUTDOWN_TIMEOUT, context=None): protocol = None transport = None try: @@ -229,11 +231,12 @@ async def _accept_connection2( conn, protocol, sslcontext, waiter=waiter, server_side=True, extra=extra, server=server, ssl_handshake_timeout=ssl_handshake_timeout, - ssl_shutdown_timeout=ssl_shutdown_timeout) + ssl_shutdown_timeout=ssl_shutdown_timeout, + context=context) else: transport = self._make_socket_transport( conn, protocol, waiter=waiter, extra=extra, - server=server) + server=server, context=context) try: await waiter @@ -275,9 +278,9 @@ def _ensure_fd_no_transport(self, fd): f'File descriptor {fd!r} is used by transport ' f'{transport!r}') - def _add_reader(self, fd, callback, *args): + def _add_reader(self, fd, callback, *args, context=None): self._check_closed() - handle = events.Handle(callback, args, self, None) + handle = events.Handle(callback, args, self, context=context) key = self._selector.get_map().get(fd) if key is None: self._selector.register(fd, selectors.EVENT_READ, @@ -309,9 +312,9 @@ def _remove_reader(self, fd): else: return False - def _add_writer(self, fd, callback, *args): + def _add_writer(self, fd, callback, *args, context=None): self._check_closed() - handle = events.Handle(callback, args, self, None) + handle = events.Handle(callback, args, self, context=context) key = self._selector.get_map().get(fd) if key is None: self._selector.register(fd, selectors.EVENT_WRITE, @@ -582,14 +585,10 @@ def _sock_sendall(self, fut, sock, view, pos): pos[0] = start async def sock_sendto(self, sock, data, address): - """Send data to the socket. + """Send a datagram from sock to address. - The socket must be connected to a remote socket. This method - continues to send data from data until either all data has been - sent or an error occurs. None is returned on success. On error, - an exception is raised, and there is no way to determine how much - data, if any, was successfully processed by the receiving end of - the connection. + The socket does not have to be connected. This method sends the + whole datagram in a single call. Return the number of bytes sent. """ base_events._check_ssl_socket(sock) if self._debug and sock.gettimeout() != 0: @@ -714,6 +713,9 @@ async def sock_accept(self, sock): return await fut def _sock_accept(self, fut, sock): + # gh-153761: _sock_accept must not scheduled with already cancelled future + if fut.done(): + return fd = sock.fileno() try: conn, address = sock.accept() @@ -773,7 +775,7 @@ class _SelectorTransport(transports._FlowControlMixin, # exception) _sock = None - def __init__(self, loop, sock, protocol, extra=None, server=None): + def __init__(self, loop, sock, protocol, extra=None, server=None, context=None): super().__init__(extra, loop) self._extra['socket'] = trsock.TransportSocket(sock) try: @@ -787,12 +789,13 @@ def __init__(self, loop, sock, protocol, extra=None, server=None): self._extra['peername'] = None self._sock = sock self._sock_fd = sock.fileno() - + self._context = context self._protocol_connected = False self.set_protocol(protocol) self._server = server self._buffer = collections.deque() + self._buffer_size = 0 self._conn_lost = 0 # Set when call to connection_lost scheduled. self._closing = False # Set when close() called. self._paused = False # Set when pause_reading() called @@ -869,7 +872,7 @@ def close(self): if not self._buffer: self._conn_lost += 1 self._loop._remove_writer(self._sock_fd) - self._loop.call_soon(self._call_connection_lost, None) + self._call_soon(self._call_connection_lost, None) def __del__(self, _warn=warnings.warn): if self._sock is not None: @@ -897,12 +900,13 @@ def _force_close(self, exc): return if self._buffer: self._buffer.clear() + self._buffer_size = 0 self._loop._remove_writer(self._sock_fd) if not self._closing: self._closing = True self._loop._remove_reader(self._sock_fd) self._conn_lost += 1 - self._loop.call_soon(self._call_connection_lost, exc) + self._call_soon(self._call_connection_lost, exc) def _call_connection_lost(self, exc): try: @@ -919,13 +923,18 @@ def _call_connection_lost(self, exc): self._server = None def get_write_buffer_size(self): - return sum(map(len, self._buffer)) + return self._buffer_size def _add_reader(self, fd, callback, *args): if not self.is_reading(): return - self._loop._add_reader(fd, callback, *args) + self._loop._add_reader(fd, callback, *args, context=self._context) + def _add_writer(self, fd, callback, *args): + self._loop._add_writer(fd, callback, *args, context=self._context) + + def _call_soon(self, callback, *args): + self._loop.call_soon(callback, *args, context=self._context) class _SelectorSocketTransport(_SelectorTransport): @@ -933,10 +942,9 @@ class _SelectorSocketTransport(_SelectorTransport): _sendfile_compatible = constants._SendfileMode.TRY_NATIVE def __init__(self, loop, sock, protocol, waiter=None, - extra=None, server=None): - + extra=None, server=None, context=None): self._read_ready_cb = None - super().__init__(loop, sock, protocol, extra, server) + super().__init__(loop, sock, protocol, extra, server, context) self._eof = False self._empty_waiter = None if _HAS_SENDMSG: @@ -948,14 +956,12 @@ def __init__(self, loop, sock, protocol, waiter=None, # decreases the latency (in some cases significantly.) base_events._set_nodelay(self._sock) - self._loop.call_soon(self._protocol.connection_made, self) + self._call_soon(self._protocol.connection_made, self) # only start reading when connection_made() has been called - self._loop.call_soon(self._add_reader, - self._sock_fd, self._read_ready) + self._call_soon(self._add_reader, self._sock_fd, self._read_ready) if waiter is not None: # only wake up the waiter when connection_made() has been called - self._loop.call_soon(futures._set_result_unless_cancelled, - waiter, None) + self._call_soon(futures._set_result_unless_cancelled, waiter, None) def set_protocol(self, protocol): if isinstance(protocol, protocols.BufferedProtocol): @@ -1084,10 +1090,11 @@ def write(self, data): if not data: return # Not all was written; register write handler. - self._loop._add_writer(self._sock_fd, self._write_ready) + self._add_writer(self._sock_fd, self._write_ready) # Add it to the buffer. self._buffer.append(data) + self._buffer_size += len(data) self._maybe_pause_protocol() def _get_sendmsg_buffer(self): @@ -1107,6 +1114,7 @@ def _write_sendmsg(self): except BaseException as exc: self._loop._remove_writer(self._sock_fd) self._buffer.clear() + self._buffer_size = 0 self._fatal_error(exc, 'Fatal write error on socket transport') if self._empty_waiter is not None: self._empty_waiter.set_exception(exc) @@ -1116,12 +1124,15 @@ def _write_sendmsg(self): self._loop._remove_writer(self._sock_fd) if self._empty_waiter is not None: self._empty_waiter.set_result(None) - if self._closing: + # gh-156512: don't let _call_connection_lost be called twice + if self._closing and not self._conn_lost: + self._conn_lost += 1 self._call_connection_lost(None) elif self._eof: self._sock.shutdown(socket.SHUT_WR) def _adjust_leftover_buffer(self, nbytes: int) -> None: + self._buffer_size -= nbytes buffer = self._buffer while nbytes: b = buffer.popleft() @@ -1142,13 +1153,16 @@ def _write_send(self): if n != len(buffer): # Not all data was written self._buffer.appendleft(buffer[n:]) + self._buffer_size -= n except (BlockingIOError, InterruptedError): - pass + self._buffer.appendleft(buffer) + return except (SystemExit, KeyboardInterrupt): raise except BaseException as exc: self._loop._remove_writer(self._sock_fd) self._buffer.clear() + self._buffer_size = 0 self._fatal_error(exc, 'Fatal write error on socket transport') if self._empty_waiter is not None: self._empty_waiter.set_exception(exc) @@ -1158,7 +1172,9 @@ def _write_send(self): self._loop._remove_writer(self._sock_fd) if self._empty_waiter is not None: self._empty_waiter.set_result(None) - if self._closing: + # gh-156512: don't let _call_connection_lost be called twice + if self._closing and not self._conn_lost: + self._conn_lost += 1 self._call_connection_lost(None) elif self._eof: self._sock.shutdown(socket.SHUT_WR) @@ -1184,11 +1200,18 @@ def writelines(self, list_of_data): self._conn_lost += 1 return - self._buffer.extend([memoryview(data) for data in list_of_data]) + for data in list_of_data: + # gh-155888: an empty chunk can never be drained, so never buffer it + if not data: + continue + self._buffer.append(memoryview(data)) + self._buffer_size += len(data) + if not self._buffer: + return self._write_ready() # If the entire buffer couldn't be written, register a write handler if self._buffer: - self._loop._add_writer(self._sock_fd, self._write_ready) + self._add_writer(self._sock_fd, self._write_ready) self._maybe_pause_protocol() def can_write_eof(self): @@ -1229,14 +1252,12 @@ def __init__(self, loop, sock, protocol, address=None, super().__init__(loop, sock, protocol, extra) self._address = address self._buffer_size = 0 - self._loop.call_soon(self._protocol.connection_made, self) + self._call_soon(self._protocol.connection_made, self) # only start reading when connection_made() has been called - self._loop.call_soon(self._add_reader, - self._sock_fd, self._read_ready) + self._call_soon(self._add_reader, self._sock_fd, self._read_ready) if waiter is not None: # only wake up the waiter when connection_made() has been called - self._loop.call_soon(futures._set_result_unless_cancelled, - waiter, None) + self._call_soon(futures._set_result_unless_cancelled, waiter, None) def get_write_buffer_size(self): return self._buffer_size @@ -1283,7 +1304,7 @@ def sendto(self, data, addr=None): self._sock.sendto(data, addr) return except (BlockingIOError, InterruptedError): - self._loop._add_writer(self._sock_fd, self._sendto_ready) + self._add_writer(self._sock_fd, self._sendto_ready) except OSError as exc: self._protocol.error_received(exc) return diff --git a/Lib/asyncio/staggered.py b/Lib/asyncio/staggered.py index 2ad65d8648e..845aed4c6a3 100644 --- a/Lib/asyncio/staggered.py +++ b/Lib/asyncio/staggered.py @@ -62,7 +62,6 @@ async def staggered_race(coro_fns, delay, *, loop=None): coroutine's entry is ``None``. """ - # TODO: when we have aiter() and anext(), allow async iterables in coro_fns. loop = loop or events.get_running_loop() parent_task = tasks.current_task(loop) enum_coro_fns = enumerate(coro_fns) diff --git a/Lib/asyncio/streams.py b/Lib/asyncio/streams.py index dd8f6618623..d4867424098 100644 --- a/Lib/asyncio/streams.py +++ b/Lib/asyncio/streams.py @@ -214,9 +214,11 @@ def _stream_reader(self): return self._stream_reader_wr() def _replace_transport(self, transport): - loop = self._loop self._transport = transport self._over_ssl = transport.get_extra_info('sslcontext') is not None + reader = self._stream_reader + if reader is not None: + reader._replace_transport(transport) def connection_made(self, transport): if self._reject_connection: @@ -271,7 +273,6 @@ def connection_lost(self, exc): self._closed.set_exception(exc) super().connection_lost(exc) self._stream_reader_wr = None - self._stream_writer = None self._task = None self._transport = None @@ -477,6 +478,10 @@ def set_transport(self, transport): assert self._transport is None, 'Transport already set' self._transport = transport + def _replace_transport(self, transport): + assert self._transport is not None, 'Transport not set' + self._transport = transport + def _maybe_resume_transport(self): if self._paused and len(self._buffer) <= self._limit: self._paused = False @@ -669,8 +674,7 @@ async def readuntil(self, separator=b'\n'): # adds data which makes separator be found. That's why we check for # EOF *after* inspecting the buffer. if self._eof: - chunk = bytes(self._buffer) - self._buffer.clear() + chunk = self._buffer.take_bytes() raise exceptions.IncompleteReadError(chunk, None) # _wait_for_data() will resume reading if stream was paused. @@ -680,10 +684,9 @@ async def readuntil(self, separator=b'\n'): raise exceptions.LimitOverrunError( 'Separator is found, but chunk is longer than limit', match_start) - chunk = self._buffer[:match_end] - del self._buffer[:match_end] + chunk = self._buffer.take_bytes(match_end) self._maybe_resume_transport() - return bytes(chunk) + return chunk async def read(self, n=-1): """Read up to `n` bytes from the stream. @@ -718,20 +721,16 @@ async def read(self, n=-1): # collect everything in self._buffer, but that would # deadlock if the subprocess sends more than self.limit # bytes. So just call self.read(self._limit) until EOF. - blocks = [] - while True: - block = await self.read(self._limit) - if not block: - break - blocks.append(block) - return b''.join(blocks) + joined = bytearray() + while block := await self.read(self._limit): + joined += block + return joined.take_bytes() if not self._buffer and not self._eof: await self._wait_for_data('read') # This will work right even if buffer is less than n bytes - data = bytes(memoryview(self._buffer)[:n]) - del self._buffer[:n] + data = self._buffer.take_bytes(min(len(self._buffer), n)) self._maybe_resume_transport() return data @@ -762,18 +761,12 @@ async def readexactly(self, n): while len(self._buffer) < n: if self._eof: - incomplete = bytes(self._buffer) - self._buffer.clear() + incomplete = self._buffer.take_bytes() raise exceptions.IncompleteReadError(incomplete, n) await self._wait_for_data('readexactly') - if len(self._buffer) == n: - data = bytes(self._buffer) - self._buffer.clear() - else: - data = bytes(memoryview(self._buffer)[:n]) - del self._buffer[:n] + data = self._buffer.take_bytes(n) self._maybe_resume_transport() return data diff --git a/Lib/asyncio/taskgroups.py b/Lib/asyncio/taskgroups.py index 00e8f6d5d1a..1d3d26a5a56 100644 --- a/Lib/asyncio/taskgroups.py +++ b/Lib/asyncio/taskgroups.py @@ -37,6 +37,7 @@ def __init__(self): self._errors = [] self._base_error = None self._on_completed_fut = None + self._cancel_on_enter = False def __repr__(self): info = [''] @@ -63,6 +64,8 @@ async def __aenter__(self): raise RuntimeError( f'TaskGroup {self!r} cannot determine the parent task') self._entered = True + if self._cancel_on_enter: + self.cancel() return self @@ -113,6 +116,7 @@ async def _aexit(self, et, exc): # can be cancelled multiple times if our parent task # is being cancelled repeatedly (or even once, when # our own cancellation is already in progress) + pending_cancellation_error = None while self._tasks: if self._on_completed_fut is None: self._on_completed_fut = self._loop.create_future() @@ -120,6 +124,7 @@ async def _aexit(self, et, exc): try: await self._on_completed_fut except exceptions.CancelledError as ex: + pending_cancellation_error = ex if not self._aborting: # Our parent task is being cancelled: # @@ -148,6 +153,9 @@ async def _aexit(self, et, exc): # If there are no pending cancellations left, # don't propagate CancelledError. propagate_cancellation_error = None + elif propagate_cancellation_error is None: + # gh-155433: the remaining cancellation is not ours, don't drop it + propagate_cancellation_error = pending_cancellation_error # Propagate CancelledError if there is one, except if there # are other errors -- those have priority. @@ -171,13 +179,29 @@ async def _aexit(self, et, exc): self._parent_task.uncancel() self._parent_task.cancel() try: - raise BaseExceptionGroup( - 'unhandled errors in a TaskGroup', - self._errors, - ) from None + # If the *only* error is a GeneratorExit from the body + # of the group, then instead of raising an + # ExceptionGroup we raise GeneratorExit. This ensures + # that async generators that use TaskGroup properly + # swallow the exception on `aclose()` while ensuring + # that no exceptions from subtasks are swallowed. + if ( + et is not None + and issubclass(et, GeneratorExit) + and len(self._errors) == 1 + ): + raise exc + else: + raise BaseExceptionGroup( + 'unhandled errors in a TaskGroup', + self._errors, + ) from None finally: exc = None + # Suppress any remaining exception (exceptions deserving to be raised + # were raised above). + return True def create_task(self, coro, **kwargs): """Create a new task in this group and return it. @@ -203,6 +227,9 @@ def create_task(self, coro, **kwargs): # the current task too early. gh-128550, gh-128588 self._tasks.add(task) task.add_done_callback(self._on_task_done) + # gh-155418: an eager task can cancel the group before joining _tasks + if self._aborting and not task.done(): + task.cancel() try: return task finally: @@ -278,3 +305,30 @@ def _on_task_done(self, task): self._abort() self._parent_cancel_requested = True self._parent_task.cancel() + + def cancel(self): + """Cancel the task group + + `cancel()` will be called on any tasks in the group that aren't yet + done, as well as the parent (body) of the group. This will cause + the task group context manager to exit *without* + `asyncio.CancelledError` being raised. + + If `cancel()` is called before entering the task group, the group + will be cancelled upon entry. This is useful for patterns where + one piece of code passes an unused TaskGroup instance to another in + order to have the ability to cancel anything run within the group. + + `cancel()` is idempotent and may be called after the task group has + already exited. + """ + if not self._entered: + self._cancel_on_enter = True + return + if self._exiting and not self._tasks: + return + if not self._aborting: + self._abort() + if self._parent_task and not self._parent_cancel_requested: + self._parent_cancel_requested = True + self._parent_task.cancel() diff --git a/Lib/asyncio/tools.py b/Lib/asyncio/tools.py index f39e11fdd51..2ac1738d15c 100644 --- a/Lib/asyncio/tools.py +++ b/Lib/asyncio/tools.py @@ -1,6 +1,6 @@ """Tools to analyze tasks running in asyncio programs.""" -from collections import defaultdict, namedtuple +from collections import defaultdict from itertools import count from enum import Enum import sys @@ -27,10 +27,10 @@ def __init__( # ─── indexing helpers ─────────────────────────────────────────── def _format_stack_entry(elem: str|FrameInfo) -> str: if not isinstance(elem, str): - if elem.lineno == 0 and elem.filename == "": + if elem.location.lineno == 0 and elem.filename == "": return f"{elem.funcname}" else: - return f"{elem.funcname} {elem.filename}:{elem.lineno}" + return f"{elem.funcname} {elem.filename}:{elem.location.lineno}" return elem @@ -231,27 +231,38 @@ def exit_with_permission_help_text(): print( "Error: The specified process cannot be attached to due to insufficient permissions.\n" "See the Python documentation for details on required privileges and troubleshooting:\n" - "https://docs.python.org/3.14/howto/remote_debugging.html#permission-requirements\n" + "https://docs.python.org/3/howto/remote_debugging.html#permission-requirements\n", + file=sys.stderr, ) sys.exit(1) -def _get_awaited_by_tasks(pid: int) -> list: - try: - return get_all_awaited_by(pid) - except RuntimeError as e: - while e.__context__ is not None: - e = e.__context__ - print(f"Error retrieving tasks: {e}") - sys.exit(1) - except PermissionError as e: - exit_with_permission_help_text() +_TRANSIENT_ERRORS = (RuntimeError, OSError, UnicodeDecodeError, MemoryError) + + +def _get_awaited_by_tasks(pid: int, retries: int = 3) -> list: + for attempt in range(retries + 1): + try: + return get_all_awaited_by(pid) + except PermissionError: + exit_with_permission_help_text() + except ProcessLookupError: + print(f"Error: process {pid} not found.", file=sys.stderr) + sys.exit(1) + except _TRANSIENT_ERRORS as e: + if attempt < retries: + continue + if isinstance(e, RuntimeError): + while e.__context__ is not None: + e = e.__context__ + print(f"Error retrieving tasks: {e}", file=sys.stderr) + sys.exit(1) -def display_awaited_by_tasks_table(pid: int) -> None: +def display_awaited_by_tasks_table(pid: int, retries: int = 3) -> None: """Build and print a table of all pending tasks under `pid`.""" - tasks = _get_awaited_by_tasks(pid) + tasks = _get_awaited_by_tasks(pid, retries=retries) table = build_task_table(tasks) # Print the table in a simple tabular format print( @@ -262,10 +273,10 @@ def display_awaited_by_tasks_table(pid: int) -> None: print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<50} {row[5]:<15} {row[6]:<15}") -def display_awaited_by_tasks_tree(pid: int) -> None: +def display_awaited_by_tasks_tree(pid: int, retries: int = 3) -> None: """Build and print a tree of all pending tasks under `pid`.""" - tasks = _get_awaited_by_tasks(pid) + tasks = _get_awaited_by_tasks(pid, retries=retries) try: result = build_async_tree(tasks) except CycleFoundException as e: diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index bfb3da0debb..67415bf4473 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -212,13 +212,13 @@ async def _make_subprocess_transport(self, protocol, args, shell, raise except BaseException: transp.close() - await transp._wait() + await tasks.shield(transp._wait()) raise return transp def _child_watcher_callback(self, pid, returncode, transp): - self.call_soon_threadsafe(transp._process_exited, returncode) + transp._process_exited(returncode) async def create_unix_connection( self, protocol_factory, path=None, *, @@ -360,7 +360,7 @@ async def _sock_sendfile_native(self, sock, file, offset, count): "os.sendfile() is not available") try: fileno = file.fileno() - except (AttributeError, io.UnsupportedOperation) as err: + except (AttributeError, io.UnsupportedOperation): raise exceptions.SendfileNotAvailableError("not a regular file") try: fsize = os.fstat(fileno).st_size @@ -639,7 +639,8 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None): self._conn_lost = 0 self._closing = False # Set when close() or write_eof() called. - mode = os.fstat(self._fileno).st_mode + pipe_stat = os.fstat(self._fileno) + mode = pipe_stat.st_mode is_char = stat.S_ISCHR(mode) is_fifo = stat.S_ISFIFO(mode) is_socket = stat.S_ISSOCK(mode) @@ -656,7 +657,19 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None): # On AIX, the reader trick (to be notified when the read end of the # socket is closed) only works for sockets. On other platforms it # works for pipes and sockets. (Exception: OS X 10.4? Issue #19294.) - if is_socket or (is_fifo and not sys.platform.startswith("aix")): + # On macOS and Solaris, the trick misfires for named FIFOs (but not for + # pipes created with os.pipe(), which have st_nlink == 0): the write + # end polls as readable whenever unread data sits in the FIFO, and no + # event is delivered when the read end is closed, so it can only + # ever report a false disconnection (gh-145030). The same XNU + # behaviour applies on iOS/tvOS/watchOS (sys.platform is not + # "darwin" there). + is_named_fifo_without_close_event = ( + sys.platform in {"darwin", "ios", "tvos", "watchos", "sunos5"} + and is_fifo and pipe_stat.st_nlink > 0) + if is_socket or (is_fifo + and not sys.platform.startswith("aix") + and not is_named_fifo_without_close_event): # only start reading when connection_made() has been called self._loop.call_soon(self._loop._add_reader, self._fileno, self._read_ready) @@ -887,8 +900,8 @@ def _do_wait(self, pid, pidfd, callback, args): pid) else: returncode = waitstatus_to_exitcode(status) - - os.close(pidfd) + finally: + os.close(pidfd) callback(pid, returncode, *args) class _ThreadedChildWatcher: @@ -928,6 +941,49 @@ def add_child_handler(self, pid, callback, *args): def _do_waitpid(self, loop, expected_pid, callback, args): assert expected_pid > 0 + if hasattr(os, 'waitid'): + # Wait for the child process using waitid() on platforms which support it. + # WNOWAIT is used to avoid reaping the child process, allowing the event loop to + # reap the child process with waitpid() later in event loop thread. + # This makes the reaping of the child and notification of the return code + # atomic with respect to the event loop thread. + try: + os.waitid(os.P_PID, expected_pid, os.WEXITED | os.WNOWAIT) + except ChildProcessError: + # The child process is already reaped + pass + if loop.is_closed(): + # loop is already closed, reap the zombie here so that it is not leaked. + pid, _ = self._reap(loop, expected_pid) + logger.warning("Loop %r that handles pid %r is closed", + loop, pid) + else: + try: + loop.call_soon_threadsafe( + self._reap_and_notify, loop, expected_pid, + callback, args) + except RuntimeError: + # The event loop was closed concurrently. + pid, _ = self._reap(loop, expected_pid) + logger.warning("Loop %r that handles pid %r is closed", + loop, pid) + else: + # Fallback for platforms that don't support waitid(): we have to + # reap the child here, which is racy with respect to send_signal() + pid, returncode = self._reap(loop, expected_pid) + if loop.is_closed(): + logger.warning("Loop %r that handles pid %r is closed", + loop, pid) + else: + loop.call_soon_threadsafe(callback, pid, returncode, *args) + + self._threads.pop(expected_pid) + + def _reap_and_notify(self, loop, expected_pid, callback, args): + pid, returncode = self._reap(loop, expected_pid) + callback(pid, returncode, *args) + + def _reap(self, loop, expected_pid): try: pid, status = os.waitpid(expected_pid, 0) except ChildProcessError: @@ -943,13 +999,7 @@ def _do_waitpid(self, loop, expected_pid, callback, args): if loop.get_debug(): logger.debug('process %s exited with returncode %s', expected_pid, returncode) - - if loop.is_closed(): - logger.warning("Loop %r that handles pid %r is closed", loop, pid) - else: - loop.call_soon_threadsafe(callback, pid, returncode, *args) - - self._threads.pop(expected_pid) + return pid, returncode def can_use_pidfd(): if not hasattr(os, 'pidfd_open'): diff --git a/Lib/asyncio/windows_events.py b/Lib/asyncio/windows_events.py index 0bf7732136f..c905db10168 100644 --- a/Lib/asyncio/windows_events.py +++ b/Lib/asyncio/windows_events.py @@ -408,7 +408,7 @@ async def _make_subprocess_transport(self, protocol, args, shell, raise except BaseException: transp.close() - await transp._wait() + await tasks.shield(transp._wait()) raise return transp diff --git a/Lib/bdb.py b/Lib/bdb.py index 79da4bab9c9..50cf2b3f5b3 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -269,12 +269,9 @@ def trace_dispatch(self, frame, event, arg): is entered. return: A function or other code block is about to return. exception: An exception has occurred. - c_call: A C function is about to be called. - c_return: A C function has returned. - c_exception: A C function has raised an exception. - For the Python events, specialized functions (see the dispatch_*() - methods) are called. For the C events, no action is taken. + For all the events, specialized functions (see the dispatch_*() + methods) are called. The arg parameter depends on the previous event. """ @@ -290,12 +287,6 @@ def trace_dispatch(self, frame, event, arg): return self.dispatch_return(frame, arg) if event == 'exception': return self.dispatch_exception(frame, arg) - if event == 'c_call': - return self.trace_dispatch - if event == 'c_exception': - return self.trace_dispatch - if event == 'c_return': - return self.trace_dispatch if event == 'opcode': return self.dispatch_opcode(frame, arg) print('bdb.Bdb.dispatch: unknown debugging event:', repr(event)) diff --git a/Lib/cProfile.py b/Lib/cProfile.py new file mode 100644 index 00000000000..cc6255f61ae --- /dev/null +++ b/Lib/cProfile.py @@ -0,0 +1,13 @@ +"""Compatibility wrapper for cProfile module. + +This module maintains backward compatibility by importing from the new +profiling.tracing module. +""" + +from profiling.tracing import run, runctx, Profile + +__all__ = ["run", "runctx", "Profile"] + +if __name__ == "__main__": + from profiling.tracing.__main__ import main + main() diff --git a/Lib/calendar.py b/Lib/calendar.py index 18f76d52ff8..7aaaf73bfe0 100644 --- a/Lib/calendar.py +++ b/Lib/calendar.py @@ -14,8 +14,9 @@ __all__ = ["IllegalMonthError", "IllegalWeekdayError", "setfirstweekday", "firstweekday", "isleap", "leapdays", "weekday", "monthrange", "monthcalendar", "prmonth", "month", "prcal", "calendar", - "timegm", "month_name", "month_abbr", "day_name", "day_abbr", - "Calendar", "TextCalendar", "HTMLCalendar", "LocaleTextCalendar", + "timegm", "month_name", "month_abbr", "standalone_month_name", + "standalone_month_abbr", "day_name", "day_abbr", "Calendar", + "TextCalendar", "HTMLCalendar", "LocaleTextCalendar", "LocaleHTMLCalendar", "weekheader", "Day", "Month", "JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", @@ -139,6 +140,24 @@ def __len__(self): month_name = _localized_month('%B') month_abbr = _localized_month('%b') +# On platforms that support the %OB and %Ob specifiers, they are used +# to get the standalone form of the month name. This is required for +# some languages such as Greek, Slavic, and Baltic languages. +try: + standalone_month_name = _localized_month('%OB') + standalone_month_abbr = _localized_month('%Ob') +except ValueError: + standalone_month_name = month_name + standalone_month_abbr = month_abbr +else: + # Some systems that do not support '%OB' will keep it as-is (i.e., + # we get [..., '%OB', '%OB', '%OB']), so for non-distinct names, + # we fall back to month_name/month_abbr. + if len(set(standalone_month_name)) != len(set(month_name)): + standalone_month_name = month_name + if len(set(standalone_month_abbr)) != len(set(month_abbr)): + standalone_month_abbr = month_abbr + def isleap(year): """Return True for leap years, False for non-leap years.""" @@ -359,7 +378,7 @@ def formatweekday(self, day, width): """ Returns a formatted week day name. """ - if width >= 9: + if width >= max(map(len, day_name)): names = day_name else: names = day_abbr @@ -377,7 +396,7 @@ def formatmonthname(self, theyear, themonth, width, withyear=True): """ _validate_month(themonth) - s = month_name[themonth] + s = standalone_month_name[themonth] if withyear: s = "%s %r" % (s, theyear) return s.center(width) @@ -479,30 +498,29 @@ def formatday(self, day, weekday): """ if day == 0: # day outside month - return ' ' % self.cssclass_noday + return f' ' else: - return '%d' % (self.cssclasses[weekday], day) + return f'{day}' def formatweek(self, theweek): """ Return a complete week as a table row. """ s = ''.join(self.formatday(d, wd) for (d, wd) in theweek) - return '%s' % s + return f'{s}' def formatweekday(self, day): """ Return a weekday name as a table header. """ - return '%s' % ( - self.cssclasses_weekday_head[day], day_abbr[day]) + return f'{day_abbr[day]}' def formatweekheader(self): """ Return a header for a week as a table row. """ s = ''.join(self.formatweekday(i) for i in self.iterweekdays()) - return '%s' % s + return f'{s}' def formatmonthname(self, theyear, themonth, withyear=True): """ @@ -510,11 +528,10 @@ def formatmonthname(self, theyear, themonth, withyear=True): """ _validate_month(themonth) if withyear: - s = '%s %s' % (month_name[themonth], theyear) + s = f'{standalone_month_name[themonth]} {theyear}' else: - s = '%s' % month_name[themonth] - return '%s' % ( - self.cssclass_month_head, s) + s = standalone_month_name[themonth] + return f'{s}' def formatmonth(self, theyear, themonth, withyear=True): """ @@ -522,8 +539,7 @@ def formatmonth(self, theyear, themonth, withyear=True): """ v = [] a = v.append - a('' % ( - self.cssclass_month)) + a(f'
') a('\n') a(self.formatmonthname(theyear, themonth, withyear=withyear)) a('\n') @@ -543,11 +559,9 @@ def formatyear(self, theyear, width=3): v = [] a = v.append width = max(width, 1) - a('
' % - self.cssclass_year) + a(f'
') a('\n') - a('' % ( - width, self.cssclass_year_head, theyear)) + a(f'') for i in range(JANUARY, JANUARY+12, width): # months in this row months = range(i, min(i+width, 13)) @@ -560,29 +574,48 @@ def formatyear(self, theyear, width=3): a('
%s
{theyear}
') return ''.join(v) - def formatyearpage(self, theyear, width=3, css='calendar.css', encoding=None): + def _format_html_page(self, theyear, content, css, encoding): """ - Return a formatted year as a complete HTML page. + Return a complete HTML page with the given content. """ if encoding is None: - encoding = sys.getdefaultencoding() + encoding = 'utf-8' v = [] a = v.append - a('\n' % encoding) - a('\n') - a('\n') + a('\n') + a('\n') a('\n') - a('\n' % encoding) + a(f'\n') + a('\n') + a(f'Calendar for {theyear}\n') + a('\n') if css is not None: - a('\n' % css) - a('Calendar for %d\n' % theyear) + a(f'\n') a('\n') a('\n') - a(self.formatyear(theyear, width)) + a(content) a('\n') a('\n') return ''.join(v).encode(encoding, "xmlcharrefreplace") + def formatyearpage(self, theyear, width=3, css='calendar.css', encoding=None): + """ + Return a formatted year as a complete HTML page. + """ + content = self.formatyear(theyear, width) + return self._format_html_page(theyear, content, css, encoding) + + def formatmonthpage(self, theyear, themonth, *, css='calendar.css', encoding=None): + """ + Return a formatted month as a complete HTML page. + """ + content = self.formatmonth(theyear, themonth) + return self._format_html_page(theyear, content, css, encoding) + class different_locale: def __init__(self, locale): @@ -653,28 +686,61 @@ def __init__(self, highlight_day=None, *args, **kwargs): super().__init__(*args, **kwargs) self.highlight_day = highlight_day - def formatweek(self, theweek, width, *, highlight_day=None): + def _get_theme(self): + from _colorize import get_theme + + return get_theme(tty_file=sys.stdout) + + def formatday(self, day, weekday, width, *, highlight_day=None): """ - Returns a single week in a string (no newline). + Returns a formatted day. """ - if highlight_day: - from _colorize import get_colors - - ansi = get_colors() - highlight = f"{ansi.BLACK}{ansi.BACKGROUND_YELLOW}" - reset = ansi.RESET + if day == 0: + s = '' else: - highlight = reset = "" + s = f'{day:2}' + s = s.center(width) + if day == highlight_day: + theme = self._get_theme().calendar + s = f"{theme.highlight}{s}{theme.reset}" + return s + def formatweek(self, theweek, width, *, highlight_day=None): + """ + Returns a single week in a string (no newline). + """ return ' '.join( - ( - f"{highlight}{self.formatday(d, wd, width)}{reset}" - if d == highlight_day - else self.formatday(d, wd, width) - ) + self.formatday(d, wd, width, highlight_day=highlight_day) for (d, wd) in theweek ) + def formatweekheader(self, width): + """ + Return a header for a week. + """ + header = super().formatweekheader(width) + theme = self._get_theme().calendar + return f"{theme.weekday}{header}{theme.reset}" + + def formatmonthname(self, theyear, themonth, width, withyear=True): + """ + Return a formatted month name. + """ + name = super().formatmonthname(theyear, themonth, width, withyear) + theme = self._get_theme().calendar + if ( + self.highlight_day + and self.highlight_day.year == theyear + and self.highlight_day.month == themonth + ): + color = theme.highlight + name_only = name.strip() + colored_name = f"{color}{name_only}{theme.reset}" + return name.replace(name_only, colored_name, 1) + else: + color = theme.header + return f"{color}{name}{theme.reset}" + def formatmonth(self, theyear, themonth, w=0, l=0): """ Return a month's calendar string (multi-line). @@ -709,7 +775,9 @@ def formatyear(self, theyear, w=2, l=1, c=6, m=3): colwidth = (w + 1) * 7 - 1 v = [] a = v.append - a(repr(theyear).center(colwidth*m+c*(m-1)).rstrip()) + theme = self._get_theme().calendar + year = repr(theyear).center(colwidth*m+c*(m-1)).rstrip() + a(f"{theme.header}{year}{theme.reset}") a('\n'*l) header = self.formatweekheader(w) for (i, row) in enumerate(self.yeardays2calendar(theyear, m)): @@ -810,28 +878,30 @@ def timegm(tuple): def main(args=None): import argparse - parser = argparse.ArgumentParser(color=True) + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) textgroup = parser.add_argument_group('text only arguments') htmlgroup = parser.add_argument_group('html only arguments') textgroup.add_argument( "-w", "--width", type=int, default=2, - help="width of date column (default 2)" + help="width of date column" ) textgroup.add_argument( "-l", "--lines", type=int, default=1, - help="number of lines for each week (default 1)" + help="number of lines for each week" ) textgroup.add_argument( "-s", "--spacing", type=int, default=6, - help="spacing between months (default 6)" + help="spacing between months" ) textgroup.add_argument( "-m", "--months", type=int, default=3, - help="months per row (default 3)" + help="months per row" ) htmlgroup.add_argument( "-c", "--css", @@ -852,12 +922,12 @@ def main(args=None): "-t", "--type", default="text", choices=("text", "html"), - help="output type (text or html)" + help="output type (`text` or `html`)" ) parser.add_argument( "-f", "--first-weekday", type=int, default=0, - help="weekday (0 is Monday, 6 is Sunday) to start each week (default 0)" + help="weekday (0 is Monday, 6 is Sunday) to start each week" ) parser.add_argument( "year", @@ -867,7 +937,7 @@ def main(args=None): parser.add_argument( "month", nargs='?', type=int, - help="month number (1-12, text only)" + help="month number (1-12)" ) options = parser.parse_args(args) @@ -880,9 +950,6 @@ def main(args=None): today = datetime.date.today() if options.type == "html": - if options.month: - parser.error("incorrect number of arguments") - sys.exit(1) if options.locale: cal = LocaleHTMLCalendar(locale=locale) else: @@ -890,13 +957,17 @@ def main(args=None): cal.setfirstweekday(options.first_weekday) encoding = options.encoding if encoding is None: - encoding = sys.getdefaultencoding() + encoding = 'utf-8' optdict = dict(encoding=encoding, css=options.css) write = sys.stdout.buffer.write + if options.year is None: write(cal.formatyearpage(today.year, **optdict)) else: - write(cal.formatyearpage(options.year, **optdict)) + if options.month: + write(cal.formatmonthpage(options.year, options.month, **optdict)) + else: + write(cal.formatyearpage(options.year, **optdict)) else: if options.locale: cal = _CLIDemoLocaleCalendar(highlight_day=today, locale=locale) diff --git a/Lib/code.py b/Lib/code.py index b134886dc26..df1d7199e33 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -224,7 +224,7 @@ def interact(self, banner=None, exitmsg=None): sys.ps1 = ">>> " delete_ps1_after = True try: - _ps2 = sys.ps2 + sys.ps2 delete_ps2_after = False except AttributeError: sys.ps2 = "... " @@ -385,7 +385,7 @@ def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=Fa if __name__ == "__main__": import argparse - parser = argparse.ArgumentParser(color=True) + parser = argparse.ArgumentParser() parser.add_argument('-q', action='store_true', help="don't print version and copyright messages") args = parser.parse_args() diff --git a/Lib/codeop.py b/Lib/codeop.py index 8cac00442d9..40e88423119 100644 --- a/Lib/codeop.py +++ b/Lib/codeop.py @@ -66,9 +66,9 @@ def _maybe_compile(compiler, source, filename, symbol, flags): try: compiler(source + "\n", filename, symbol, flags=flags) return None - except _IncompleteInputError as e: + except _IncompleteInputError: return None - except SyntaxError as e: + except SyntaxError: pass # fallthrough diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 0aeca1ed7e5..5dbcac19e7a 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -32,6 +32,8 @@ _sys.modules['collections.abc'] = _collections_abc abc = _collections_abc +lazy from copy import copy as _copy +lazy from heapq import nlargest as _nlargest from itertools import chain as _chain from itertools import repeat as _repeat from itertools import starmap as _starmap @@ -59,8 +61,6 @@ except ImportError: pass -heapq = None # Lazily imported - ################################################################################ ### OrderedDict @@ -328,14 +328,14 @@ def __ior__(self, other): return self def __or__(self, other): - if not isinstance(other, dict): + if not isinstance(other, (dict, frozendict)): return NotImplemented new = self.__class__(self) new.update(other) return new def __ror__(self, other): - if not isinstance(other, dict): + if not isinstance(other, (dict, frozendict)): return NotImplemented new = self.__class__(other) new.update(self) @@ -637,12 +637,7 @@ def most_common(self, n=None): if n is None: return sorted(self.items(), key=_itemgetter(1), reverse=True) - # Lazy import to speedup Python startup time - global heapq - if heapq is None: - import heapq - - return heapq.nlargest(n, self.items(), key=_itemgetter(1)) + return _nlargest(n, self.items(), key=_itemgetter(1)) def elements(self): '''Iterator over elements repeating each as many times as its count. @@ -779,23 +774,27 @@ def __repr__(self): # When the multiplicities are all zero or one, multiset operations # are guaranteed to be equivalent to the corresponding operations # for regular sets. + # # Given counter multisets such as: # cp = Counter(a=1, b=0, c=1) # cq = Counter(c=1, d=0, e=1) + # # The corresponding regular sets would be: # sp = {'a', 'c'} # sq = {'c', 'e'} + # # All of the following relations would hold: - # set(cp + cq) == sp | sq - # set(cp - cq) == sp - sq - # set(cp | cq) == sp | sq - # set(cp & cq) == sp & sq # (cp == cq) == (sp == sq) # (cp != cq) == (sp != sq) # (cp <= cq) == (sp <= sq) # (cp < cq) == (sp < sq) # (cp >= cq) == (sp >= sq) # (cp > cq) == (sp > sq) + # set(cp + cq) == sp | sq + # set(cp - cq) == sp - sq + # set(cp | cq) == sp | sq + # set(cp & cq) == sp & sq + # set(cp ^ cq) == sp ^ sq def __eq__(self, other): 'True if all counts agree. Missing counts are treated as zero.' @@ -908,6 +907,33 @@ def __and__(self, other): result[elem] = newcount return result + def __xor__(self, other): + '''Symmetric difference. Absolute value of count differences. + + The symmetric difference p ^ q is equivalent to: + + (p - q) | (q - p). + + For each element, symmetric difference gives the same result as: + + max(p[elem], q[elem]) - min(p[elem], q[elem]) + + >>> Counter(a=5, b=3, c=2, d=2) ^ Counter(a=1, b=3, c=5, e=1) + Counter({'a': 4, 'c': 3, 'd': 2, 'e': 1}) + + ''' + if not isinstance(other, Counter): + return NotImplemented + result = Counter() + for elem, count in self.items(): + newcount = abs(count - other[elem]) + if newcount: + result[elem] = newcount + for elem, count in other.items(): + if elem not in self and count: + result[elem] = abs(count) + return result + def __pos__(self): 'Adds an empty counter, effectively stripping negative and zero counts' result = Counter() @@ -990,6 +1016,22 @@ def __iand__(self, other): self[elem] = other_count return self._keep_positive() + def __ixor__(self, other): + '''Inplace symmetric difference. Absolute value of count differences. + + >>> c = Counter(a=5, b=3, c=2, d=2) + >>> c ^= Counter(a=1, b=3, c=5, e=1) + >>> c + Counter({'a': 4, 'c': 3, 'd': 2, 'e': 1}) + + ''' + for elem, count in self.items(): + self[elem] = abs(count - other[elem]) + for elem, count in other.items(): + if elem not in self: + self[elem] = abs(count) + return self._keep_positive() + ######################################################################## ### ChainMap @@ -1177,14 +1219,14 @@ def __repr__(self): def __or__(self, other): if isinstance(other, UserDict): return self.__class__(self.data | other.data) - if isinstance(other, dict): + if isinstance(other, (dict, frozendict)): return self.__class__(self.data | other) return NotImplemented def __ror__(self, other): if isinstance(other, UserDict): return self.__class__(other.data | self.data) - if isinstance(other, dict): + if isinstance(other, (dict, frozendict)): return self.__class__(other | self.data) return NotImplemented @@ -1205,16 +1247,29 @@ def __copy__(self): def copy(self): if self.__class__ is UserDict: return UserDict(self.data.copy()) - import copy data = self.data try: self.data = {} - c = copy.copy(self) + c = _copy(self) finally: self.data = data c.update(self) return c + + # This method has a default implementation in MutableMapping, but dict's + # equivalent is last-in, first-out instead of first-in, first-out. + def popitem(self): + """Remove and return a (key, value) pair as a 2-tuple. + + Removes pairs in the same order as the wrapped mapping's popitem() + method. For dict objects (the default), that order is last-in, + first-out (LIFO). + Raises KeyError if the UserDict is empty. + """ + return self.data.popitem() + + @classmethod def fromkeys(cls, iterable, value=None): d = cls() diff --git a/Lib/compileall.py b/Lib/compileall.py index 67fe370451e..812a496611e 100644 --- a/Lib/compileall.py +++ b/Lib/compileall.py @@ -165,6 +165,14 @@ def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0, stripdir = os.fspath(stripdir) if stripdir is not None else None name = os.path.basename(fullname) + # Without a cache_tag, we can only create legacy .pyc files. None of our + # callers seem to expect this, so the best we can do is fail without raising + if not legacy and sys.implementation.cache_tag is None: + if not quiet: + print("No cache tag is available to generate .pyc path for", + repr(fullname)) + return False + dfile = None if ddir is not None: @@ -223,7 +231,7 @@ def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0, cfile = importlib.util.cache_from_source(fullname) opt_cfiles[opt_level] = cfile - head, tail = name[:-3], name[-3:] + tail = name[-3:] if tail == '.py': if not force: try: @@ -318,7 +326,6 @@ def main(): parser = argparse.ArgumentParser( description='Utilities to support installing Python libraries.', - color=True, ) parser.add_argument('-l', action='store_const', const=0, default=None, dest='maxlevels', @@ -330,10 +337,10 @@ def main(): parser.add_argument('-f', action='store_true', dest='force', help='force rebuild even if timestamps are up to date') parser.add_argument('-q', action='count', dest='quiet', default=0, - help='output only error messages; -qq will suppress ' + help='output only error messages; `-qq` will suppress ' 'the error messages as well.') parser.add_argument('-b', action='store_true', dest='legacy', - help='use legacy (pre-PEP3147) compiled file locations') + help='use legacy (pre-PEP 3147) compiled file locations') parser.add_argument('-d', metavar='DESTDIR', dest='ddir', default=None, help=('directory to prepend to file paths for use in ' 'compile-time tracebacks and in runtime ' @@ -359,28 +366,28 @@ def main(): 'of each file considered for compilation')) parser.add_argument('-i', metavar='FILE', dest='flist', help=('add all the files and directories listed in ' - 'FILE to the list considered for compilation; ' - 'if "-", names are read from stdin')) + '`FILE` to the list considered for compilation; ' + 'if `"-"`, names are read from `stdin`')) parser.add_argument('compile_dest', metavar='FILE|DIR', nargs='*', help=('zero or more file and directory names ' 'to compile; if no arguments given, defaults ' - 'to the equivalent of -l sys.path')) + 'to the equivalent of `-l` `sys.path`')) parser.add_argument('-j', '--workers', default=1, type=int, help='Run compileall concurrently') invalidation_modes = [mode.name.lower().replace('_', '-') for mode in py_compile.PycInvalidationMode] parser.add_argument('--invalidation-mode', choices=sorted(invalidation_modes), - help=('set .pyc invalidation mode; defaults to ' - '"checked-hash" if the SOURCE_DATE_EPOCH ' + help=('set `.pyc` invalidation mode; defaults to ' + '`"checked-hash"` if the `SOURCE_DATE_EPOCH` ' 'environment variable is set, and ' - '"timestamp" otherwise.')) + '`"timestamp"` otherwise.')) parser.add_argument('-o', action='append', type=int, dest='opt_levels', help=('Optimization levels to run compilation with. ' - 'Default is -1 which uses the optimization level ' - 'of the Python interpreter itself (see -O).')) + 'Default is `-1` which uses the optimization level ' + 'of the Python interpreter itself (see `-O`).')) parser.add_argument('-e', metavar='DIR', dest='limit_sl_dest', - help='Ignore symlinks pointing outsite of the DIR') + help='Ignore symlinks pointing outsite of the `DIR`') parser.add_argument('--hardlink-dupes', action='store_true', dest='hardlink_dupes', help='Hardlink duplicated pyc files') diff --git a/Lib/concurrent/futures/__init__.py b/Lib/concurrent/futures/__init__.py index d6ac4b3e0b6..5d53a5ac509 100644 --- a/Lib/concurrent/futures/__init__.py +++ b/Lib/concurrent/futures/__init__.py @@ -17,6 +17,9 @@ wait, as_completed) +lazy from .process import ProcessPoolExecutor +lazy from .thread import ThreadPoolExecutor + __all__ = [ 'FIRST_COMPLETED', 'FIRST_EXCEPTION', @@ -40,26 +43,9 @@ _interpreters = None if _interpreters: + lazy from .interpreter import InterpreterPoolExecutor # noqa: F401 __all__.append('InterpreterPoolExecutor') def __dir__(): return __all__ + ['__author__', '__doc__'] - - -def __getattr__(name): - global ProcessPoolExecutor, ThreadPoolExecutor, InterpreterPoolExecutor - - if name == 'ProcessPoolExecutor': - from .process import ProcessPoolExecutor - return ProcessPoolExecutor - - if name == 'ThreadPoolExecutor': - from .thread import ThreadPoolExecutor - return ThreadPoolExecutor - - if _interpreters and name == 'InterpreterPoolExecutor': - from .interpreter import InterpreterPoolExecutor - return InterpreterPoolExecutor - - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/concurrent/futures/process.py b/Lib/concurrent/futures/process.py index 122be582793..51df68540b5 100644 --- a/Lib/concurrent/futures/process.py +++ b/Lib/concurrent/futures/process.py @@ -119,10 +119,11 @@ def _python_exit(): # On Windows, WaitForMultipleObjects is used to wait for processes to finish. -# It can wait on, at most, 63 objects. There is an overhead of two objects: +# It can wait on, at most, 64 objects. There is an overhead of three objects: # - the result queue reader # - the thread wakeup reader -_MAX_WINDOWS_WORKERS = 63 - 2 +# - the SIGINT handler +_MAX_WINDOWS_WORKERS = 64 - 3 # Hack to embed stringification of remote traceback in local traceback @@ -269,6 +270,20 @@ def _process_worker(call_queue, result_queue, initializer, initargs, max_tasks=N return +def _spawn_worker(mp_context, call_queue, result_queue, initializer, + initargs, max_tasks_per_child, processes): + """Start one worker process and record it in *processes* by pid.""" + p = mp_context.Process( + target=_process_worker, + args=(call_queue, + result_queue, + initializer, + initargs, + max_tasks_per_child)) + p.start() + processes[p.pid] = p + + class _ExecutorManagerThread(threading.Thread): """Manages the communication between this process and the worker processes. @@ -321,6 +336,15 @@ def weakref_cb(_, # exiting safely self.max_tasks_per_child = executor._max_tasks_per_child + # gh-119592: Needed to size worker replacement, and immutable, so + # keep a copy rather than reading it back through the executor + # weakref. The rest of the spawn configuration is deliberately NOT + # copied here: holding user-provided objects (initializer, + # initargs, mp_context) in this always-reachable running thread + # could keep the executor itself reachable through them, breaking + # garbage-collection-triggered shutdown. + self.max_workers = executor._max_workers + # A dict mapping work ids to _WorkItems e.g. # {5: <_WorkItem...>, 6: <_WorkItem...>, ...} self.pending_work_items = executor._pending_work_items @@ -357,12 +381,14 @@ def run(self): # while waiting on new results. del result_item - if executor := self.executor_reference(): - if process_exited: - with self.shutdown_lock: - executor._adjust_process_count() - else: - executor._idle_worker_semaphore.release() + if process_exited: + with self.shutdown_lock: + broken = self._replace_dead_worker() + if broken is not None: + self.terminate_broken(*broken) + return + elif executor := self.executor_reference(): + executor._idle_worker_semaphore.release() del executor if self.is_shutting_down(): @@ -379,6 +405,71 @@ def run(self): self.join_executor_internals() return + def _replace_dead_worker(self): + """Spawn a replacement for a worker that exited at its + max_tasks_per_child limit. Called under self.shutdown_lock. + + Returns None while the pool can still make progress, otherwise a + (cause, message) tuple describing why the remaining work items can + never run, so that run() can fail their futures. + """ + assert self.shutdown_lock.locked() + cause = None + message = None + executor = self.executor_reference() + if executor is None: + # gh-152967: The executor was garbage collected; nothing can + # spawn a replacement worker for it anymore. + message = ("The ProcessPoolExecutor was garbage collected with " + "work pending after its last worker process exited " + "upon reaching max_tasks_per_child; the pending work " + "can never be run.") + elif executor._force_shutting_down: + # terminate_workers()/kill_workers() is tearing the pool down; + # a replacement worker would escape the kill and run work + # items that were enqueued before it. + message = ("A worker process exited while the pool was being " + "forcefully shut down; work that was still enqueued " + "will not be run.") + elif self.pending_work_items or not self.is_shutting_down(): + # gh-115634: Do not consult the executor's + # _idle_worker_semaphore here: it counts task completions, not + # idle workers, so it can hold a stale token released by the + # now-dead worker. Trusting such a token would leave the pool + # a worker short, deadlocking once all workers reach their + # task limit. Spawning from this (manager) thread is safe + # despite gh-90622 because max_tasks_per_child is rejected for + # the "fork" start method. + if len(self.processes) < self.max_workers: + # gh-119592: Spawn using state owned by this thread and + # configuration read through the live weakref (which + # shutdown() never clears), not the executor state that + # shutdown(wait=False) clears concurrently. + try: + _spawn_worker(executor._mp_context, self.call_queue, + self.result_queue, executor._initializer, + executor._initargs, + self.max_tasks_per_child, self.processes) + except Exception as exc: + # While other workers remain the pool has merely lost + # capacity and they keep draining the queue; with none + # left the failure is reported below. + cause = format_exception(exc) + message = ("A replacement worker process could not be " + "started, leaving the pool without workers " + "to run the remaining work.") + del executor + + if not self.processes and (self.pending_work_items + or cause is not None): + # No worker processes remain and no replacement can be + # spawned: any remaining work items can never run. A spawn + # failure breaks the pool even with nothing pending; leaving + # a zero-worker pool alive would hang a later submit() on a + # stale _idle_worker_semaphore token instead of raising. + return (cause, message) + return None + def add_call_item_to_queue(self): # Fills call_queue with _WorkItems from pending_work_items. # This function never blocks. @@ -455,10 +546,11 @@ def is_shutting_down(self): return (_global_shutdown or executor is None or executor._shutdown_thread) - def _terminate_broken(self, cause): + def _terminate_broken(self, cause, bpe_message=None): # Terminate the executor because it is in a broken state. The cause # argument can be used to display more information on the error that - # lead the executor into becoming broken. + # lead the executor into becoming broken. bpe_message overrides the + # default message on the BrokenProcessPool set on pending futures. # Mark the process pool broken so that submits fail right now. executor = self.executor_reference() @@ -469,17 +561,34 @@ def _terminate_broken(self, cause): executor._shutdown_thread = True executor = None - # All pending tasks are to be marked failed with the following - # BrokenProcessPool error - bpe = BrokenProcessPool("A process in the process pool was " - "terminated abruptly while the future was " - "running or pending.") + # All pending tasks are to be marked failed with a + # BrokenProcessPool error, as separate instances to avoid sharing + # a traceback (gh-101267). + cause_str = None if cause is not None: - bpe.__cause__ = _RemoteTraceback( - f"\n'''\n{''.join(cause)}'''") - + cause_str = ''.join(cause) + else: + # No cause known, so report any processes that have + # terminated with nonzero exit codes, e.g. from a + # segfault. Multiple may terminate simultaneously, + # so include all of them in the traceback. + errors = [] + for p in self.processes.values(): + if p.exitcode is not None and p.exitcode != 0: + errors.append(f"Process {p.pid} terminated abruptly " + f"with exit code {p.exitcode}") + if errors: + cause_str = "\n".join(errors) + cause_tb = f"\n'''\n{cause_str}'''" if cause_str else None + + if bpe_message is None: + bpe_message = ("A process in the process pool was terminated " + "abruptly while the future was running or pending.") # Mark pending tasks as failed. for work_id, work_item in self.pending_work_items.items(): + bpe = BrokenProcessPool(bpe_message) + if cause_tb is not None: + bpe.__cause__ = _RemoteTraceback(cause_tb) try: work_item.future.set_exception(bpe) except _base.InvalidStateError: @@ -502,9 +611,9 @@ def _terminate_broken(self, cause): # clean up resources self._join_executor_internals(broken=True) - def terminate_broken(self, cause): + def terminate_broken(self, cause, bpe_message=None): with self.shutdown_lock: - self._terminate_broken(cause) + self._terminate_broken(cause, bpe_message) def flag_executor_shutting_down(self): # Flag the executor as shutting down and cancel remaining tasks if @@ -717,6 +826,7 @@ def __init__(self, max_workers=None, mp_context=None, self._queue_count = 0 self._pending_work_items = {} self._cancel_pending_futures = False + self._force_shutting_down = False # _ThreadWakeup is a communication channel used to interrupt the wait # of the main loop of executor_manager_thread from another thread (e.g. @@ -762,18 +872,23 @@ def _adjust_process_count(self): if self._processes is None: return + # gh-152967: A forceful shutdown is in progress; a worker spawned + # here could escape its process snapshot and keep running work. + if self._force_shutting_down: + return + # if there's an idle process, we don't need to spawn a new one. if self._idle_worker_semaphore.acquire(blocking=False): return process_count = len(self._processes) if process_count < self._max_workers: - # Assertion disabled as this codepath is also used to replace a - # worker that unexpectedly dies, even when using the 'fork' start - # method. That means there is still a potential deadlock bug. If a - # 'fork' mp_context worker dies, we'll be forking a new one when - # we know a thread is running (self._executor_manager_thread). - #assert self._safe_to_dynamically_spawn_children or not self._executor_manager_thread, 'https://github.com/python/cpython/issues/90622' + # gh-90622: spawning a child via fork while another thread is + # running can deadlock in the child. submit() only calls this + # method when using a non-fork start method. + assert (self._safe_to_dynamically_spawn_children + or not self._executor_manager_thread), ( + 'https://github.com/python/cpython/issues/90622') self._spawn_process() def _launch_processes(self): @@ -785,15 +900,10 @@ def _launch_processes(self): self._spawn_process() def _spawn_process(self): - p = self._mp_context.Process( - target=_process_worker, - args=(self._call_queue, - self._result_queue, - self._initializer, - self._initargs, - self._max_tasks_per_child)) - p.start() - self._processes[p.pid] = p + _spawn_worker(self._mp_context, self._call_queue, + self._result_queue, self._initializer, + self._initargs, self._max_tasks_per_child, + self._processes) def submit(self, fn, /, *args, **kwargs): with self._shutdown_lock: @@ -890,6 +1000,14 @@ def _force_shutdown(self, operation): if operation not in _SHUTDOWN_CALLBACK_OPERATION: raise ValueError(f"Unsupported operation: {operation!r}") + # gh-152967: Stop the manager thread from spawning replacement + # workers before we copy the processes to signal: a worker spawned + # after the copy would survive the loop below and run enqueued + # work items. Taking the lock orders this against the manager's + # worker replacement, which runs under the same lock. + with self._shutdown_lock: + self._force_shutting_down = True + processes = {} if self._processes: processes = self._processes.copy() diff --git a/Lib/concurrent/interpreters/_queues.py b/Lib/concurrent/interpreters/_queues.py index 56c947f7d2a..5f3ee0934de 100644 --- a/Lib/concurrent/interpreters/_queues.py +++ b/Lib/concurrent/interpreters/_queues.py @@ -1,6 +1,5 @@ """Cross-interpreter Queues High Level Module.""" -import pickle import queue import time import weakref @@ -225,7 +224,7 @@ def put(self, obj, block=True, timeout=None, *, while True: try: _queues.put(self._id, obj, unboundop) - except QueueFull as exc: + except QueueFull: if timeout is not None and time.time() >= end: raise # re-raise time.sleep(_delay) @@ -260,7 +259,7 @@ def get(self, block=True, timeout=None, *, while True: try: obj, unboundop = _queues.get(self._id) - except QueueEmpty as exc: + except QueueEmpty: if timeout is not None and time.time() >= end: raise # re-raise time.sleep(_delay) @@ -279,7 +278,7 @@ def get_nowait(self): """ try: obj, unboundop = _queues.get(self._id) - except QueueEmpty as exc: + except QueueEmpty: raise # re-raise if unboundop is not None: assert obj is None, repr(obj) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 5b646fabca0..efc02bfa924 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -1,11 +1,17 @@ """Utilities for with-statement contexts. See PEP 343.""" + import abc import os import sys import _collections_abc from collections import deque from functools import wraps -from types import MethodType, GenericAlias +lazy from inspect import ( + isasyncgenfunction as _isasyncgenfunction, + iscoroutinefunction as _iscoroutinefunction, + isgeneratorfunction as _isgeneratorfunction, +) +from types import GenericAlias __all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext", "AbstractContextManager", "AbstractAsyncContextManager", @@ -79,11 +85,37 @@ def _recreate_cm(self): return self def __call__(self, func): - @wraps(func) - def inner(*args, **kwds): - with self._recreate_cm(): - return func(*args, **kwds) - return inner + wrapper = wraps(func) + if _isasyncgenfunction(func): + + async def asyncgen_inner(*args, **kwds): + with self._recreate_cm(): + async with aclosing(func(*args, **kwds)) as gen: + async for value in gen: + yield value + + return wrapper(asyncgen_inner) + elif _iscoroutinefunction(func): + + async def async_inner(*args, **kwds): + with self._recreate_cm(): + return await func(*args, **kwds) + + return wrapper(async_inner) + elif _isgeneratorfunction(func): + + def gen_inner(*args, **kwds): + with self._recreate_cm(), closing(func(*args, **kwds)) as gen: + return (yield from gen) + + return wrapper(gen_inner) + else: + + def inner(*args, **kwds): + with self._recreate_cm(): + return func(*args, **kwds) + + return wrapper(inner) class AsyncContextDecorator(object): @@ -95,11 +127,41 @@ def _recreate_cm(self): return self def __call__(self, func): - @wraps(func) - async def inner(*args, **kwds): - async with self._recreate_cm(): - return await func(*args, **kwds) - return inner + wrapper = wraps(func) + if _isasyncgenfunction(func): + + async def asyncgen_inner(*args, **kwds): + async with ( + self._recreate_cm(), + aclosing(func(*args, **kwds)) as gen + ): + async for value in gen: + yield value + + return wrapper(asyncgen_inner) + elif _iscoroutinefunction(func): + + async def async_inner(*args, **kwds): + async with self._recreate_cm(): + return await func(*args, **kwds) + + return wrapper(async_inner) + elif _isgeneratorfunction(func): + + async def gen_inner(*args, **kwds): + async with self._recreate_cm(): + with closing(func(*args, **kwds)) as gen: + for value in gen: + yield value + + return wrapper(gen_inner) + else: + + async def inner(*args, **kwds): + async with self._recreate_cm(): + return func(*args, **kwds) + + return wrapper(inner) class _GeneratorContextManagerBase: @@ -469,13 +531,23 @@ def __exit__(self, exctype, excinst, exctb): return False +def _lookup_special(obj, name, default): + # Follow the standard lookup behaviour for special methods. + from inspect import getattr_static, _descriptor_get + cls = type(obj) + try: + descr = getattr_static(cls, name) + except AttributeError: + return default + return _descriptor_get(descr, obj) + + +_sentinel = ['SENTINEL'] + + class _BaseExitStack: """A base class for ExitStack and AsyncExitStack.""" - @staticmethod - def _create_exit_wrapper(cm, cm_exit): - return MethodType(cm_exit, cm) - @staticmethod def _create_cb_wrapper(callback, /, *args, **kwds): def _exit_wrapper(exc_type, exc, tb): @@ -499,17 +571,8 @@ def push(self, exit): Also accepts any object with an __exit__ method (registering a call to the method instead of the object itself). """ - # We use an unbound method rather than a bound method to follow - # the standard lookup behaviour for special methods. - _cb_type = type(exit) - - try: - exit_method = _cb_type.__exit__ - except AttributeError: - # Not a context manager, so assume it's a callable. - self._push_exit_callback(exit) - else: - self._push_cm_exit(exit, exit_method) + exit_method = _lookup_special(exit, '__exit__', exit) + self._push_exit_callback(exit_method) return exit # Allow use as a decorator. def enter_context(self, cm): @@ -518,17 +581,18 @@ def enter_context(self, cm): If successful, also pushes its __exit__ method as a callback and returns the result of the __enter__ method. """ - # We look up the special methods on the type to match the with - # statement. - cls = type(cm) - try: - _enter = cls.__enter__ - _exit = cls.__exit__ - except AttributeError: + _enter = _lookup_special(cm, '__enter__', _sentinel) + if _enter is _sentinel: + cls = type(cm) raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " - f"not support the context manager protocol") from None - result = _enter(cm) - self._push_cm_exit(cm, _exit) + f"not support the context manager protocol") + _exit = _lookup_special(cm, '__exit__', _sentinel) + if _exit is _sentinel: + cls = type(cm) + raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " + f"not support the context manager protocol") + result = _enter() + self._push_exit_callback(_exit) return result def callback(self, callback, /, *args, **kwds): @@ -544,11 +608,6 @@ def callback(self, callback, /, *args, **kwds): self._push_exit_callback(_exit_wrapper) return callback # Allow use as a decorator - def _push_cm_exit(self, cm, cm_exit): - """Helper to correctly register callbacks to __exit__ methods.""" - _exit_wrapper = self._create_exit_wrapper(cm, cm_exit) - self._push_exit_callback(_exit_wrapper, True) - def _push_exit_callback(self, callback, is_sync=True): self._exit_callbacks.append((is_sync, callback)) @@ -641,10 +700,6 @@ class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager): # connection later in the list raise an exception. """ - @staticmethod - def _create_async_exit_wrapper(cm, cm_exit): - return MethodType(cm_exit, cm) - @staticmethod def _create_async_cb_wrapper(callback, /, *args, **kwds): async def _exit_wrapper(exc_type, exc, tb): @@ -657,16 +712,18 @@ async def enter_async_context(self, cm): If successful, also pushes its __aexit__ method as a callback and returns the result of the __aenter__ method. """ - cls = type(cm) - try: - _enter = cls.__aenter__ - _exit = cls.__aexit__ - except AttributeError: + _enter = _lookup_special(cm, '__aenter__', _sentinel) + if _enter is _sentinel: + cls = type(cm) raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " - f"not support the asynchronous context manager protocol" - ) from None - result = await _enter(cm) - self._push_async_cm_exit(cm, _exit) + f"not support the asynchronous context manager protocol") + _exit = _lookup_special(cm, '__aexit__', _sentinel) + if _exit is _sentinel: + cls = type(cm) + raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " + f"not support the asynchronous context manager protocol") + result = await _enter() + self._push_exit_callback(_exit, False) return result def push_async_exit(self, exit): @@ -677,14 +734,8 @@ def push_async_exit(self, exit): Also accepts any object with an __aexit__ method (registering a call to the method instead of the object itself). """ - _cb_type = type(exit) - try: - exit_method = _cb_type.__aexit__ - except AttributeError: - # Not an async context manager, so assume it's a coroutine function - self._push_exit_callback(exit, False) - else: - self._push_async_cm_exit(exit, exit_method) + exit_method = _lookup_special(exit, '__aexit__', exit) + self._push_exit_callback(exit_method, False) return exit # Allow use as a decorator def push_async_callback(self, callback, /, *args, **kwds): @@ -704,12 +755,6 @@ async def aclose(self): """Immediately unwind the context stack.""" await self.__aexit__(None, None, None) - def _push_async_cm_exit(self, cm, cm_exit): - """Helper to correctly register coroutine function to __aexit__ - method.""" - _exit_wrapper = self._create_async_exit_wrapper(cm, cm_exit) - self._push_exit_callback(_exit_wrapper, False) - async def __aenter__(self): return self diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 7223fc49977..0b84a27f8c6 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -2,15 +2,13 @@ import os as _os import sys as _sys -import sysconfig as _sysconfig import types as _types -__version__ = "1.1.0" +lazy import sysconfig as _sysconfig from _ctypes import Union, Structure, Array from _ctypes import _Pointer from _ctypes import CFuncPtr as _CFuncPtr -from _ctypes import __version__ as _ctypes_version from _ctypes import RTLD_LOCAL, RTLD_GLOBAL from _ctypes import ArgumentError from _ctypes import SIZEOF_TIME_T @@ -18,9 +16,6 @@ from struct import calcsize as _calcsize -if __version__ != _ctypes_version: - raise Exception("Version number mismatch", __version__, _ctypes_version) - if _os.name == "nt": from _ctypes import COMError, CopyComPointer, FormatError @@ -211,13 +206,13 @@ class c_longdouble(_SimpleCData): try: class c_double_complex(_SimpleCData): - _type_ = "D" + _type_ = "Zd" _check_size(c_double_complex) class c_float_complex(_SimpleCData): - _type_ = "F" + _type_ = "Zf" _check_size(c_float_complex) class c_longdouble_complex(_SimpleCData): - _type_ = "G" + _type_ = "Zg" except AttributeError: pass @@ -381,12 +376,6 @@ def create_unicode_buffer(init, size=None): return buf raise TypeError(init) - -def SetPointerType(pointer, cls): - import warnings - warnings._deprecated("ctypes.SetPointerType", remove=(3, 15)) - pointer.set_type(cls) - def ARRAY(typ, len): return typ * len @@ -681,3 +670,12 @@ def DllCanUnloadNow(): raise SystemError(f"Unexpected sizeof(time_t): {SIZEOF_TIME_T=}") _reset_cache() + + +def __getattr__(name): + if name == "__version__": + from warnings import _deprecated + + _deprecated("__version__", remove=(3, 20)) + return "1.1.0" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/ctypes/_layout.py b/Lib/ctypes/_layout.py index 2048ccb6a1c..61b4c6c70fb 100644 --- a/Lib/ctypes/_layout.py +++ b/Lib/ctypes/_layout.py @@ -5,11 +5,12 @@ """ import sys -import warnings from _ctypes import CField, buffer_info import ctypes +lazy import warnings + def round_down(n, multiple): assert n >= 0 assert multiple > 0 diff --git a/Lib/ctypes/macholib/__init__.py b/Lib/ctypes/macholib/__init__.py index 5621defccd6..f00e55f8131 100644 --- a/Lib/ctypes/macholib/__init__.py +++ b/Lib/ctypes/macholib/__init__.py @@ -6,4 +6,10 @@ And also Apple's documentation. """ -__version__ = '1.0' +def __getattr__(name): + if name == "__version__": + from warnings import _deprecated + + _deprecated("__version__", remove=(3, 20)) + return "1.0" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index 3b21658433b..015c62b31a1 100644 --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -1,8 +1,9 @@ import os -import shutil -import subprocess import sys +lazy import shutil +lazy import subprocess + # find_library(name) returns the pathname of a library, or None. if os.name == "nt": @@ -421,8 +422,7 @@ def _findLib_ld(name): result = None try: p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True) + stderr=subprocess.PIPE) out, _ = p.communicate() res = re.findall(expr, os.fsdecode(out)) for file in res: @@ -442,53 +442,12 @@ def find_library(name): _get_soname(_findLib_gcc(name)) or _get_soname(_findLib_ld(name)) -# Listing loaded libraries on other systems will try to use -# functions common to Linux and a few other Unix-like systems. -# See the following for several platforms' documentation of the same API: -# https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html -# https://man.freebsd.org/cgi/man.cgi?query=dl_iterate_phdr -# https://man.openbsd.org/dl_iterate_phdr -# https://docs.oracle.com/cd/E88353_01/html/E37843/dl-iterate-phdr-3c.html -if (os.name == "posix" and - sys.platform not in {"darwin", "ios", "tvos", "watchos"}): - import ctypes - if hasattr((_libc := ctypes.CDLL(None)), "dl_iterate_phdr"): - - class _dl_phdr_info(ctypes.Structure): - _fields_ = [ - ("dlpi_addr", ctypes.c_void_p), - ("dlpi_name", ctypes.c_char_p), - ("dlpi_phdr", ctypes.c_void_p), - ("dlpi_phnum", ctypes.c_ushort), - ] - - _dl_phdr_callback = ctypes.CFUNCTYPE( - ctypes.c_int, - ctypes.POINTER(_dl_phdr_info), - ctypes.c_size_t, - ctypes.POINTER(ctypes.py_object), - ) - - @_dl_phdr_callback - def _info_callback(info, _size, data): - libraries = data.contents.value - name = os.fsdecode(info.contents.dlpi_name) - libraries.append(name) - return 0 - - _dl_iterate_phdr = _libc["dl_iterate_phdr"] - _dl_iterate_phdr.argtypes = [ - _dl_phdr_callback, - ctypes.POINTER(ctypes.py_object), - ] - _dl_iterate_phdr.restype = ctypes.c_int - - def dllist(): - """Return a list of loaded shared libraries in the current process.""" - libraries = [] - _dl_iterate_phdr(_info_callback, - ctypes.byref(ctypes.py_object(libraries))) - return libraries +# On platforms which provide dl_iterate_phdr(), dllist() is implemented +# in _ctypes. +try: + from _ctypes import dllist +except ImportError: + pass ################################################################ # test code diff --git a/Lib/curses/__init__.py b/Lib/curses/__init__.py new file mode 100644 index 00000000000..605d5fcbec5 --- /dev/null +++ b/Lib/curses/__init__.py @@ -0,0 +1,98 @@ +"""curses + +The main package for curses support for Python. Normally used by importing +the package, and perhaps a particular module inside it. + + import curses + from curses import textpad + curses.initscr() + ... + +""" + +from _curses import * +import os as _os +import sys as _sys + +# Some constants, most notably the ACS_* ones, are only added to the C +# _curses module's dictionary after initscr() is called. (Some +# versions of SGI's curses don't define values for those constants +# until initscr() has been called.) This wrapper function calls the +# underlying C initscr(), and then copies the constants from the +# _curses module to the curses package's dictionary. Don't do 'from +# curses import *' if you'll be needing the ACS_* constants. + +def initscr(): + import _curses, curses + # we call setupterm() here because it raises an error + # instead of calling exit() in error cases. + setupterm(term=_os.environ.get("TERM", "unknown"), + fd=_sys.__stdout__.fileno()) + stdscr = _curses.initscr() + for key, value in _curses.__dict__.items(): + if key.startswith('ACS_') or key in ('LINES', 'COLS'): + setattr(curses, key, value) + return stdscr + +# This is a similar wrapper for start_color(), which adds the COLORS and +# COLOR_PAIRS variables which are only available after start_color() is +# called. + +def start_color(): + import _curses, curses + _curses.start_color() + curses.COLORS = _curses.COLORS + curses.COLOR_PAIRS = _curses.COLOR_PAIRS + +# Import Python has_key() implementation if _curses doesn't contain has_key() + +try: + has_key +except NameError: + from .has_key import has_key # noqa: F401 + +# Wrapper for the entire curses-based application. Runs a function which +# should be the rest of your curses-based application. If the application +# raises an exception, wrapper() will restore the terminal to a sane state so +# you can read the resulting traceback. + +def wrapper(func, /, *args, **kwds): + """Wrapper function that initializes curses and calls another function, + restoring normal keyboard/screen behavior on error. + The callable object 'func' is then passed the main window 'stdscr' + as its first argument, followed by any other arguments passed to + wrapper(). + """ + + try: + # Initialize curses + stdscr = initscr() + + # Turn off echoing of keys, and enter cbreak mode, + # where no buffering is performed on keyboard input + noecho() + cbreak() + + # In keypad mode, escape sequences for special keys + # (like the cursor keys) will be interpreted and + # a special value like curses.KEY_LEFT will be returned + stdscr.keypad(1) + + # Start color, too. Harmless if the terminal doesn't have + # color; user can test with has_color() later on. The try/catch + # works around a minor bit of over-conscientiousness in the curses + # module -- the error return from C start_color() is ignorable, + # unless they are raised by the interpreter due to other issues. + try: + start_color() + except _curses.error: + pass + + return func(stdscr, *args, **kwds) + finally: + # Set everything back to normal + if 'stdscr' in locals(): + stdscr.keypad(0) + echo() + nocbreak() + endwin() diff --git a/Lib/curses/ascii.py b/Lib/curses/ascii.py new file mode 100644 index 00000000000..95acff33925 --- /dev/null +++ b/Lib/curses/ascii.py @@ -0,0 +1,99 @@ +"""Constants and membership tests for ASCII characters""" + +NUL = 0x00 # ^@ +SOH = 0x01 # ^A +STX = 0x02 # ^B +ETX = 0x03 # ^C +EOT = 0x04 # ^D +ENQ = 0x05 # ^E +ACK = 0x06 # ^F +BEL = 0x07 # ^G +BS = 0x08 # ^H +TAB = 0x09 # ^I +HT = 0x09 # ^I +LF = 0x0a # ^J +NL = 0x0a # ^J +VT = 0x0b # ^K +FF = 0x0c # ^L +CR = 0x0d # ^M +SO = 0x0e # ^N +SI = 0x0f # ^O +DLE = 0x10 # ^P +DC1 = 0x11 # ^Q +DC2 = 0x12 # ^R +DC3 = 0x13 # ^S +DC4 = 0x14 # ^T +NAK = 0x15 # ^U +SYN = 0x16 # ^V +ETB = 0x17 # ^W +CAN = 0x18 # ^X +EM = 0x19 # ^Y +SUB = 0x1a # ^Z +ESC = 0x1b # ^[ +FS = 0x1c # ^\ +GS = 0x1d # ^] +RS = 0x1e # ^^ +US = 0x1f # ^_ +SP = 0x20 # space +DEL = 0x7f # delete + +controlnames = [ +"NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", +"BS", "HT", "LF", "VT", "FF", "CR", "SO", "SI", +"DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB", +"CAN", "EM", "SUB", "ESC", "FS", "GS", "RS", "US", +"SP" +] + +def _ctoi(c): + if isinstance(c, str): + return ord(c) + else: + return c + +def isalnum(c): return isalpha(c) or isdigit(c) +def isalpha(c): return isupper(c) or islower(c) +def isascii(c): return 0 <= _ctoi(c) <= 127 # ? +def isblank(c): return _ctoi(c) in (9, 32) +def iscntrl(c): return 0 <= _ctoi(c) <= 31 or _ctoi(c) == 127 +def isdigit(c): return 48 <= _ctoi(c) <= 57 +def isgraph(c): return 33 <= _ctoi(c) <= 126 +def islower(c): return 97 <= _ctoi(c) <= 122 +def isprint(c): return 32 <= _ctoi(c) <= 126 +def ispunct(c): return isgraph(c) and not isalnum(c) +def isspace(c): return _ctoi(c) in (9, 10, 11, 12, 13, 32) +def isupper(c): return 65 <= _ctoi(c) <= 90 +def isxdigit(c): return isdigit(c) or \ + (65 <= _ctoi(c) <= 70) or (97 <= _ctoi(c) <= 102) +def isctrl(c): return 0 <= _ctoi(c) < 32 +def ismeta(c): return _ctoi(c) > 127 + +def ascii(c): + if isinstance(c, str): + return chr(_ctoi(c) & 0x7f) + else: + return _ctoi(c) & 0x7f + +def ctrl(c): + if isinstance(c, str): + return chr(_ctoi(c) & 0x1f) + else: + return _ctoi(c) & 0x1f + +def alt(c): + if isinstance(c, str): + return chr(_ctoi(c) | 0x80) + else: + return _ctoi(c) | 0x80 + +def unctrl(c): + bits = _ctoi(c) + if bits == 0x7f: + rep = "^?" + elif isprint(bits & 0x7f): + rep = chr(bits & 0x7f) + else: + rep = "^" + chr(((bits & 0x7f) | 0x20) + 0x20) + if bits & 0x80: + return "!" + rep + return rep diff --git a/Lib/curses/has_key.py b/Lib/curses/has_key.py new file mode 100644 index 00000000000..3471b28cbbe --- /dev/null +++ b/Lib/curses/has_key.py @@ -0,0 +1,192 @@ + +# +# Emulation of has_key() function for platforms that don't use ncurses +# + +import _curses + +# Table mapping curses keys to the terminfo capability name + +_capability_names = frozendict({ + _curses.KEY_A1: 'ka1', + _curses.KEY_A3: 'ka3', + _curses.KEY_B2: 'kb2', + _curses.KEY_BACKSPACE: 'kbs', + _curses.KEY_BEG: 'kbeg', + _curses.KEY_BTAB: 'kcbt', + _curses.KEY_C1: 'kc1', + _curses.KEY_C3: 'kc3', + _curses.KEY_CANCEL: 'kcan', + _curses.KEY_CATAB: 'ktbc', + _curses.KEY_CLEAR: 'kclr', + _curses.KEY_CLOSE: 'kclo', + _curses.KEY_COMMAND: 'kcmd', + _curses.KEY_COPY: 'kcpy', + _curses.KEY_CREATE: 'kcrt', + _curses.KEY_CTAB: 'kctab', + _curses.KEY_DC: 'kdch1', + _curses.KEY_DL: 'kdl1', + _curses.KEY_DOWN: 'kcud1', + _curses.KEY_EIC: 'krmir', + _curses.KEY_END: 'kend', + _curses.KEY_ENTER: 'kent', + _curses.KEY_EOL: 'kel', + _curses.KEY_EOS: 'ked', + _curses.KEY_EXIT: 'kext', + _curses.KEY_F0: 'kf0', + _curses.KEY_F1: 'kf1', + _curses.KEY_F10: 'kf10', + _curses.KEY_F11: 'kf11', + _curses.KEY_F12: 'kf12', + _curses.KEY_F13: 'kf13', + _curses.KEY_F14: 'kf14', + _curses.KEY_F15: 'kf15', + _curses.KEY_F16: 'kf16', + _curses.KEY_F17: 'kf17', + _curses.KEY_F18: 'kf18', + _curses.KEY_F19: 'kf19', + _curses.KEY_F2: 'kf2', + _curses.KEY_F20: 'kf20', + _curses.KEY_F21: 'kf21', + _curses.KEY_F22: 'kf22', + _curses.KEY_F23: 'kf23', + _curses.KEY_F24: 'kf24', + _curses.KEY_F25: 'kf25', + _curses.KEY_F26: 'kf26', + _curses.KEY_F27: 'kf27', + _curses.KEY_F28: 'kf28', + _curses.KEY_F29: 'kf29', + _curses.KEY_F3: 'kf3', + _curses.KEY_F30: 'kf30', + _curses.KEY_F31: 'kf31', + _curses.KEY_F32: 'kf32', + _curses.KEY_F33: 'kf33', + _curses.KEY_F34: 'kf34', + _curses.KEY_F35: 'kf35', + _curses.KEY_F36: 'kf36', + _curses.KEY_F37: 'kf37', + _curses.KEY_F38: 'kf38', + _curses.KEY_F39: 'kf39', + _curses.KEY_F4: 'kf4', + _curses.KEY_F40: 'kf40', + _curses.KEY_F41: 'kf41', + _curses.KEY_F42: 'kf42', + _curses.KEY_F43: 'kf43', + _curses.KEY_F44: 'kf44', + _curses.KEY_F45: 'kf45', + _curses.KEY_F46: 'kf46', + _curses.KEY_F47: 'kf47', + _curses.KEY_F48: 'kf48', + _curses.KEY_F49: 'kf49', + _curses.KEY_F5: 'kf5', + _curses.KEY_F50: 'kf50', + _curses.KEY_F51: 'kf51', + _curses.KEY_F52: 'kf52', + _curses.KEY_F53: 'kf53', + _curses.KEY_F54: 'kf54', + _curses.KEY_F55: 'kf55', + _curses.KEY_F56: 'kf56', + _curses.KEY_F57: 'kf57', + _curses.KEY_F58: 'kf58', + _curses.KEY_F59: 'kf59', + _curses.KEY_F6: 'kf6', + _curses.KEY_F60: 'kf60', + _curses.KEY_F61: 'kf61', + _curses.KEY_F62: 'kf62', + _curses.KEY_F63: 'kf63', + _curses.KEY_F7: 'kf7', + _curses.KEY_F8: 'kf8', + _curses.KEY_F9: 'kf9', + _curses.KEY_FIND: 'kfnd', + _curses.KEY_HELP: 'khlp', + _curses.KEY_HOME: 'khome', + _curses.KEY_IC: 'kich1', + _curses.KEY_IL: 'kil1', + _curses.KEY_LEFT: 'kcub1', + _curses.KEY_LL: 'kll', + _curses.KEY_MARK: 'kmrk', + _curses.KEY_MESSAGE: 'kmsg', + _curses.KEY_MOVE: 'kmov', + _curses.KEY_NEXT: 'knxt', + _curses.KEY_NPAGE: 'knp', + _curses.KEY_OPEN: 'kopn', + _curses.KEY_OPTIONS: 'kopt', + _curses.KEY_PPAGE: 'kpp', + _curses.KEY_PREVIOUS: 'kprv', + _curses.KEY_PRINT: 'kprt', + _curses.KEY_REDO: 'krdo', + _curses.KEY_REFERENCE: 'kref', + _curses.KEY_REFRESH: 'krfr', + _curses.KEY_REPLACE: 'krpl', + _curses.KEY_RESTART: 'krst', + _curses.KEY_RESUME: 'kres', + _curses.KEY_RIGHT: 'kcuf1', + _curses.KEY_SAVE: 'ksav', + _curses.KEY_SBEG: 'kBEG', + _curses.KEY_SCANCEL: 'kCAN', + _curses.KEY_SCOMMAND: 'kCMD', + _curses.KEY_SCOPY: 'kCPY', + _curses.KEY_SCREATE: 'kCRT', + _curses.KEY_SDC: 'kDC', + _curses.KEY_SDL: 'kDL', + _curses.KEY_SELECT: 'kslt', + _curses.KEY_SEND: 'kEND', + _curses.KEY_SEOL: 'kEOL', + _curses.KEY_SEXIT: 'kEXT', + _curses.KEY_SF: 'kind', + _curses.KEY_SFIND: 'kFND', + _curses.KEY_SHELP: 'kHLP', + _curses.KEY_SHOME: 'kHOM', + _curses.KEY_SIC: 'kIC', + _curses.KEY_SLEFT: 'kLFT', + _curses.KEY_SMESSAGE: 'kMSG', + _curses.KEY_SMOVE: 'kMOV', + _curses.KEY_SNEXT: 'kNXT', + _curses.KEY_SOPTIONS: 'kOPT', + _curses.KEY_SPREVIOUS: 'kPRV', + _curses.KEY_SPRINT: 'kPRT', + _curses.KEY_SR: 'kri', + _curses.KEY_SREDO: 'kRDO', + _curses.KEY_SREPLACE: 'kRPL', + _curses.KEY_SRIGHT: 'kRIT', + _curses.KEY_SRSUME: 'kRES', + _curses.KEY_SSAVE: 'kSAV', + _curses.KEY_SSUSPEND: 'kSPD', + _curses.KEY_STAB: 'khts', + _curses.KEY_SUNDO: 'kUND', + _curses.KEY_SUSPEND: 'kspd', + _curses.KEY_UNDO: 'kund', + _curses.KEY_UP: 'kcuu1' + }) + +def has_key(ch): + if isinstance(ch, str): + ch = ord(ch) + + # Figure out the correct capability name for the keycode. + capability_name = _capability_names.get(ch) + if capability_name is None: + return False + + #Check the current terminal description for that capability; + #if present, return true, else return false. + if _curses.tigetstr(capability_name): + return True + else: + return False + +if __name__ == '__main__': + # Compare the output of this implementation and the ncurses has_key, + # on platforms where has_key is already available + try: + L = [] + _curses.initscr() + for key in _capability_names.keys(): + system = _curses.has_key(key) + python = has_key(key) + if system != python: + L.append( 'Mismatch for key %s, system=%i, Python=%i' + % (_curses.keyname( key ), system, python) ) + finally: + _curses.endwin() + for i in L: print(i) diff --git a/Lib/curses/panel.py b/Lib/curses/panel.py new file mode 100644 index 00000000000..067afef730a --- /dev/null +++ b/Lib/curses/panel.py @@ -0,0 +1,6 @@ +"""curses.panel + +Module for using panels with curses. +""" + +from _curses_panel import * diff --git a/Lib/curses/textpad.py b/Lib/curses/textpad.py new file mode 100644 index 00000000000..c58a7174d19 --- /dev/null +++ b/Lib/curses/textpad.py @@ -0,0 +1,236 @@ +"""Simple textbox editing widget with Emacs-like keybindings.""" + +import curses +import curses.ascii + +def rectangle(win, uly, ulx, lry, lrx): + """Draw a rectangle with corners at the provided upper-left + and lower-right coordinates. + """ + win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1) + win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1) + win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1) + win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1) + win.addch(uly, ulx, curses.ACS_ULCORNER) + win.addch(uly, lrx, curses.ACS_URCORNER) + win.addch(lry, lrx, curses.ACS_LRCORNER) + win.addch(lry, ulx, curses.ACS_LLCORNER) + +class Textbox: + """Editing widget using the interior of a window object. + Supports the following Emacs-like key bindings: + + Ctrl-A Go to left edge of window. + Ctrl-B Cursor left, wrapping to previous line if appropriate. + Ctrl-D Delete character under cursor. + Ctrl-E Go to right edge (stripspaces off) or end of line + (stripspaces on). + Ctrl-F Cursor right, wrapping to next line when appropriate. + Ctrl-G Terminate, returning the window contents. + Ctrl-H Delete character backward. + Ctrl-J Terminate if the window is 1 line, otherwise move to start + of next line. + Ctrl-K If line is blank, delete it, otherwise clear to end of line. + Ctrl-L Refresh screen. + Ctrl-N Cursor down; move down one line. + Ctrl-O Insert a blank line at cursor location. + Ctrl-P Cursor up; move up one line. + + Move operations do nothing if the cursor is at an edge where the + movement is not possible. The following synonyms are supported where + possible: + + KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, + KEY_DOWN = Ctrl-N, KEY_BACKSPACE = Ctrl-h + """ + def __init__(self, win, insert_mode=False): + self.win = win + self.insert_mode = insert_mode + self._update_max_yx() + self.stripspaces = 1 + self.lastcmd = None + win.keypad(1) + + def _update_max_yx(self): + maxy, maxx = self.win.getmaxyx() + self.maxy = maxy - 1 + self.maxx = maxx - 1 + + def _decode(self, ch): + # The text of a chtype cell or input byte, decoded with the window's + # encoding. A_CHARTEXT keeps the character byte, dropping the attributes. + return bytes([ch & curses.A_CHARTEXT]).decode(self.win.encoding, 'replace') + + def _char_at(self, *yx): + # The text of the cell at the given position (default: the cursor). + # instr() re-encodes it to the window's encoding; inch() cannot + # represent a non-ASCII 8-bit-locale character on a wide build. + return self.win.instr(*yx, 1).decode(self.win.encoding, 'replace') + + def _cell_at(self, *yx): + # The cell at the given position (default: the cursor) as a chtype + # addch() can write back with its rendition. inch() mangles a non-ASCII + # character on a wide build, so take the byte from instr() and the + # attributes from inch(). + return self.win.instr(*yx, 1)[0] | self.win.inch(*yx) & curses.A_ATTRIBUTES + + def _isprint(self, cell): + # Whether a chtype cell holds a printable character; _decode() drops the + # attribute bits. + return self._decode(cell).isprintable() + + def _printable_key(self, ch): + # Whether the integer keystroke is a printable character, not a key + # code. 0..255 are character bytes (decoded with the window's encoding); + # larger values are function and navigation keys. + return ch <= 0xff and self._decode(ch).isprintable() + + def _end_of_line(self, y): + """Go to the location of the first blank on the given line, + returning the index of the last non-blank character.""" + self._update_max_yx() + last = self.maxx + while True: + if self._char_at(y, last) != ' ': + last = min(self.maxx, last+1) + break + elif last == 0: + break + last = last - 1 + return last + + def _insert_printable_char(self, ch): + self._update_max_yx() + (y, x) = self.win.getyx() + backyx = None + while True: + if self.insert_mode: + oldch = self._cell_at() + if y >= self.maxy and x >= self.maxx: + # Use insch() in the lower-right cell: addch() there would move + # the cursor out of the window, raising an error and scrolling + # a scrollable window. Pass it as text: insch() does not decode + # an int byte through the locale on a wide build. + self.win.insch(self._decode(ch), ch & curses.A_ATTRIBUTES) + break + self.win.addch(ch) + if not self.insert_mode or not self._isprint(oldch): + break + ch = oldch + (y, x) = self.win.getyx() + # Remember where to put the cursor back since we are in insert_mode + if backyx is None: + backyx = y, x + + if backyx is not None: + self.win.move(*backyx) + + def do_command(self, ch): + "Process a single editing command." + self._update_max_yx() + (y, x) = self.win.getyx() + self.lastcmd = ch + if self._printable_key(ch): + self._insert_printable_char(ch) + elif ch == curses.ascii.SOH: # ^a + self.win.move(y, 0) + elif ch in (curses.ascii.STX,curses.KEY_LEFT, + curses.ascii.BS, + curses.KEY_BACKSPACE, + curses.ascii.DEL): + if x > 0: + self.win.move(y, x-1) + elif y == 0: + pass + elif self.stripspaces: + self.win.move(y-1, self._end_of_line(y-1)) + else: + self.win.move(y-1, self.maxx) + if ch in (curses.ascii.BS, curses.KEY_BACKSPACE, curses.ascii.DEL): + self.win.delch() + elif ch == curses.ascii.EOT: # ^d + self.win.delch() + elif ch == curses.ascii.ENQ: # ^e + if self.stripspaces: + self.win.move(y, self._end_of_line(y)) + else: + self.win.move(y, self.maxx) + elif ch in (curses.ascii.ACK, curses.KEY_RIGHT): # ^f + if x < self.maxx: + self.win.move(y, x+1) + elif y == self.maxy: + pass + else: + self.win.move(y+1, 0) + elif ch == curses.ascii.BEL: # ^g + return 0 + elif ch == curses.ascii.NL: # ^j + if self.maxy == 0: + return 0 + elif y < self.maxy: + self.win.move(y+1, 0) + elif ch == curses.ascii.VT: # ^k + if x == 0 and self._end_of_line(y) == 0: + self.win.deleteln() + else: + # first undo the effect of self._end_of_line + self.win.move(y, x) + self.win.clrtoeol() + elif ch == curses.ascii.FF: # ^l + self.win.refresh() + elif ch in (curses.ascii.SO, curses.KEY_DOWN): # ^n + if y < self.maxy: + self.win.move(y+1, x) + if x > self._end_of_line(y+1): + self.win.move(y+1, self._end_of_line(y+1)) + elif ch == curses.ascii.SI: # ^o + self.win.insertln() + elif ch in (curses.ascii.DLE, curses.KEY_UP): # ^p + if y > 0: + self.win.move(y-1, x) + if x > self._end_of_line(y-1): + self.win.move(y-1, self._end_of_line(y-1)) + return 1 + + def gather(self): + "Collect and return the contents of the window." + result = "" + self._update_max_yx() + for y in range(self.maxy+1): + self.win.move(y, 0) + stop = self._end_of_line(y) + if stop == 0 and self.stripspaces: + continue + for x in range(self.maxx+1): + if self.stripspaces and x > stop: + break + result = result + self._char_at(y, x) + if self.maxy > 0: + result = result + "\n" + return result + + def edit(self, validate=None): + "Edit in the widget window and collect the results." + while 1: + ch = self.win.getch() + if validate: + ch = validate(ch) + if not ch: + continue + if not self.do_command(ch): + break + self.win.refresh() + return self.gather() + +if __name__ == '__main__': + def test_editbox(stdscr): + ncols, nlines = 9, 4 + uly, ulx = 15, 20 + stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.") + win = curses.newwin(nlines, ncols, uly, ulx) + rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols) + stdscr.refresh() + return Textbox(win).edit() + + str = curses.wrapper(test_editbox) + print('Contents of text box:', repr(str)) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 242a6d2ffc2..863e4101f98 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1,13 +1,12 @@ -import re import sys -import copy import types -import inspect import keyword import itertools import annotationlib import abc from reprlib import recursive_repr +lazy import copy +lazy import re __all__ = ['dataclass', @@ -178,21 +177,16 @@ def __repr__(self): return '' _HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS() -# A sentinel object to detect if a parameter is supplied or not. Use -# a class to give it a better repr. -class _MISSING_TYPE: - pass -MISSING = _MISSING_TYPE() +# A sentinel object to detect if a parameter is supplied or not. +MISSING = sentinel("MISSING") # A sentinel object to indicate that following fields are keyword-only by -# default. Use a class to give it a better repr. -class _KW_ONLY_TYPE: - pass -KW_ONLY = _KW_ONLY_TYPE() +# default. +KW_ONLY = sentinel("KW_ONLY") # Since most per-field metadata will be unused, create an empty -# read-only proxy that can be shared among all fields. -_EMPTY_METADATA = types.MappingProxyType({}) +# read-only dictionary that can be shared among all fields. +_EMPTY_METADATA = frozendict() # Markers for the various kinds of fields and pseudo-fields. class _FIELD_BASE: @@ -217,9 +211,8 @@ def __repr__(self): _POST_INIT_NAME = '__post_init__' # String regex that string annotations for ClassVar or InitVar must match. -# Allows "identifier.identifier[" or "identifier[". -# https://bugs.python.org/issue33453 for details. -_MODULE_IDENTIFIER_RE = re.compile(r'^(?:\s*(\w+)\s*\.)?\s*(\w+)') +# This regular expression is compiled on demand so that 're' module can be imported lazily +_MODULE_IDENTIFIER_RE = None # Atomic immutable types which don't require any recursive handling and for which deepcopy # returns the same object. We can provide a fast-path for these types in asdict and astuple. @@ -763,22 +756,16 @@ def _is_kw_only(a_type, dataclasses): return a_type is dataclasses.KW_ONLY -def _is_type(annotation, cls, a_module, a_type, is_type_predicate): - # Given a type annotation string, does it refer to a_type in - # a_module? For example, when checking that annotation denotes a - # ClassVar, then a_module is typing, and a_type is - # typing.ClassVar. +def _get_type_from_annotation(annotation, cls): + # Loosely parse a string annotation and return its type. - # It's possible to look up a_module given a_type, but it involves - # looking in sys.modules (again!), and seems like a waste since - # the caller already knows a_module. + # We can't perform a full type hint evaluation at the point where @dataclass + # was invoked because class's module is not fully initialized yet. So we resort + # to parsing string annotation using regexp, and extracting a type before + # the first square bracket. # - annotation is a string type annotation # - cls is the class that this annotation was found in - # - a_module is the module we want to match - # - a_type is the type in that module we want to match - # - is_type_predicate is a function called with (obj, a_module) - # that determines if obj is of the desired type. # Since this test does not do a local namespace lookup (and # instead only a module (global) lookup), there are some things it @@ -804,22 +791,26 @@ def _is_type(annotation, cls, a_module, a_type, is_type_predicate): # a eval() penalty for every single field of every dataclass # that's defined. It was judged not worth it. - match = _MODULE_IDENTIFIER_RE.match(annotation) - if match: - ns = None - module_name = match.group(1) - if not module_name: - # No module name, assume the class's module did - # "from dataclasses import InitVar". - ns = sys.modules.get(cls.__module__).__dict__ - else: - # Look up module_name in the class's module. - module = sys.modules.get(cls.__module__) - if module and module.__dict__.get(module_name) is a_module: - ns = sys.modules.get(a_type.__module__).__dict__ - if ns and is_type_predicate(ns.get(match.group(2)), a_module): - return True - return False + # String regex that string annotations for ClassVar or InitVar must match. + # Allows "identifier.identifier[" or "identifier[". + # https://github.com/python/cpython/issues/77634 for details. + global _MODULE_IDENTIFIER_RE + if _MODULE_IDENTIFIER_RE is None: + _MODULE_IDENTIFIER_RE = re.compile(r'^\s*(\w+(?:\s*\.\s*\w+)*)') + + match = _MODULE_IDENTIFIER_RE.prefixmatch(annotation) + if not match: + return None + + # Note: _MODULE_IDENTIFIER_RE guarantees that path is non-empty + path = match[1].split(".") + root = sys.modules.get(cls.__module__) + for path_item in path: + root = getattr(root, path_item.strip(), None) + if root is None: + return None + + return root def _get_field(cls, a_name, a_type, default_kw_only): @@ -857,6 +848,10 @@ def _get_field(cls, a_name, a_type, default_kw_only): # is actually of the correct type. # For the complete discussion, see https://bugs.python.org/issue33453 + if isinstance(a_type, str): + a_type_annotation = _get_type_from_annotation(a_type, cls) + else: + a_type_annotation = a_type # If typing has not been imported, then it's impossible for any # annotation to be a ClassVar. So, only look for ClassVar if @@ -864,10 +859,7 @@ def _get_field(cls, a_name, a_type, default_kw_only): # module). typing = sys.modules.get('typing') if typing: - if (_is_classvar(a_type, typing) - or (isinstance(f.type, str) - and _is_type(f.type, cls, typing, typing.ClassVar, - _is_classvar))): + if _is_classvar(a_type_annotation, typing): f._field_type = _FIELD_CLASSVAR # If the type is InitVar, or if it's a matching string annotation, @@ -876,10 +868,7 @@ def _get_field(cls, a_name, a_type, default_kw_only): # The module we're checking against is the module we're # currently in (dataclasses.py). dataclasses = sys.modules[__name__] - if (_is_initvar(a_type, dataclasses) - or (isinstance(f.type, str) - and _is_type(f.type, cls, dataclasses, dataclasses.InitVar, - _is_initvar))): + if _is_initvar(a_type_annotation, dataclasses): f._field_type = _FIELD_INITVAR # Validations for individual fields. This is delayed until now, @@ -982,6 +971,29 @@ def _hash_exception(cls, fields, func_builder): # See https://bugs.python.org/issue32929#msg312829 for an if-statement # version of this table. +# A non-data descriptor to autogenerate class docstring +# from the signature of its __init__ method on demand. +# The primary reason is to be able to lazy import `inspect` module. +class _AutoDocstring: + + def __get__(self, _obj, cls): + try: + # In some cases fetching a signature is not possible. + # But, we surely should not fail in this case. + import inspect + text_sig = str(inspect.signature( + cls, + annotation_format=annotationlib.Format.FORWARDREF, + )).replace(' -> None', '') + except TypeError, ValueError: + text_sig = '' + + doc = cls.__name__ + text_sig + setattr(cls, '__doc__', doc) + return doc + +_auto_docstring = _AutoDocstring() + def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot): @@ -1050,10 +1062,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, dataclasses = sys.modules[__name__] for name, type in cls_annotations.items(): # See if this is a marker to change the value of kw_only. - if (_is_kw_only(type, dataclasses) - or (isinstance(type, str) - and _is_type(type, cls, dataclasses, dataclasses.KW_ONLY, - _is_kw_only))): + if isinstance(type, str): + a_type_annotation = _get_type_from_annotation(type, cls) + else: + a_type_annotation = type + if _is_kw_only(a_type_annotation, dataclasses): # Switch the default to kw_only=True, and ignore this # annotation: it's not a real field. if KW_ONLY_seen: @@ -1209,23 +1222,13 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, if hash_action: cls.__hash__ = hash_action(cls, field_list, func_builder) - # Generate the methods and add them to the class. This needs to be done - # before the __doc__ logic below, since inspect will look at the __init__ - # signature. + # Generate the methods and add them to the class. func_builder.add_fns_to_class(cls) if not getattr(cls, '__doc__'): - # Create a class doc-string. - try: - # In some cases fetching a signature is not possible. - # But, we surely should not fail in this case. - text_sig = str(inspect.signature( - cls, - annotation_format=annotationlib.Format.FORWARDREF, - )).replace(' -> None', '') - except (TypeError, ValueError): - text_sig = '' - cls.__doc__ = (cls.__name__ + text_sig) + # Create a class doc-string lazily via descriptor protocol + # to avoid importing `inspect` module. + cls.__doc__ = _auto_docstring if match_args: # I could probably compute this once. @@ -1344,10 +1347,6 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields): if '__slots__' in cls.__dict__: raise TypeError(f'{cls.__name__} already specifies __slots__') - # gh-102069: Remove existing __weakref__ descriptor. - # gh-135228: Make sure the original class can be garbage collected. - sys._clear_type_descriptors(cls) - # Create a new dict for our new class. cls_dict = dict(cls.__dict__) field_names = tuple(f.name for f in fields(cls)) @@ -1365,6 +1364,11 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields): # available in _MARKER. cls_dict.pop(field_name, None) + # Remove __dict__ and `__weakref__` descriptors. + # They'll be added back if applicable. + cls_dict.pop('__dict__', None) + cls_dict.pop('__weakref__', None) # gh-102069 + # And finally create the class. qualname = getattr(cls, '__qualname__', None) newcls = type(cls)(cls.__name__, cls.__bases__, cls_dict) @@ -1385,8 +1389,11 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields): # make an update, since all closures for a class will share a # given cell. for member in newcls.__dict__.values(): + # If this is a wrapped function, unwrap it. - member = inspect.unwrap(member) + if not isinstance(member, type) and hasattr(member, '__wrapped__'): + import inspect + member = inspect.unwrap(member) if isinstance(member, types.FunctionType): if _update_func_cell_for__class__(member, cls, newcls): @@ -1498,7 +1505,8 @@ class C: If given, 'dict_factory' will be used instead of built-in dict. The function applies recursively to field values that are dataclass instances. This will also look into built-in containers: - tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'. + tuples, lists, dicts, and frozendicts. Other objects are copied + with 'copy.deepcopy()'. """ if not _is_dataclass_instance(obj): raise TypeError("asdict() should be called on dataclass instances") @@ -1554,7 +1562,7 @@ def _asdict_inner(obj, dict_factory): return obj_type(*[_asdict_inner(v, dict_factory) for v in obj]) else: return obj_type(_asdict_inner(v, dict_factory) for v in obj) - elif issubclass(obj_type, dict): + elif issubclass(obj_type, (dict, frozendict)): if hasattr(obj_type, 'default_factory'): # obj is a defaultdict, which has a different constructor from # dict as it requires the default_factory as its first arg. @@ -1589,7 +1597,8 @@ class C: If given, 'tuple_factory' will be used instead of built-in tuple. The function applies recursively to field values that are dataclass instances. This will also look into built-in containers: - tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'. + tuples, lists, dicts, and frozendicts. Other objects are copied + with 'copy.deepcopy()'. """ if not _is_dataclass_instance(obj): @@ -1618,7 +1627,7 @@ def _astuple_inner(obj, tuple_factory): # generator (which is not true for namedtuples, handled # above). return type(obj)(_astuple_inner(v, tuple_factory) for v in obj) - elif isinstance(obj, dict): + elif isinstance(obj, (dict, frozendict)): obj_type = type(obj) if hasattr(obj_type, 'default_factory'): # obj is a defaultdict, which has a different constructor from diff --git a/Lib/dbm/dumb.py b/Lib/dbm/dumb.py index def120ffc37..a080f4e8655 100644 --- a/Lib/dbm/dumb.py +++ b/Lib/dbm/dumb.py @@ -9,7 +9,7 @@ - seems to contain a bug when updating... - reclaim free space (currently, space once occupied by deleted or expanded -items is never reused) +items is not reused except if .reorganize() is called) - support concurrent access (currently, if two processes take turns making updates, they can mess up the index) @@ -17,8 +17,6 @@ - support efficient access to large databases (currently, the whole index is read when the database is opened, and some updates rewrite the whole index) -- support opening for read-only (flag = 'm') - """ import ast as _ast @@ -289,6 +287,35 @@ def __enter__(self): def __exit__(self, *args): self.close() + def reorganize(self): + if self._readonly: + raise error('The database is opened for reading only') + self._verify_open() + # Ensure all changes are committed before reorganizing. + self._commit() + # Open file in r+ to allow changing in-place. + with _io.open(self._datfile, 'rb+') as f: + reorganize_pos = 0 + + # Iterate over existing keys, sorted by starting byte. + for key in sorted(self._index, key = lambda k: self._index[k][0]): + pos, siz = self._index[key] + f.seek(pos) + val = f.read(siz) + + f.seek(reorganize_pos) + f.write(val) + self._index[key] = (reorganize_pos, siz) + + blocks_occupied = (siz + _BLOCKSIZE - 1) // _BLOCKSIZE + reorganize_pos += blocks_occupied * _BLOCKSIZE + + f.truncate(reorganize_pos) + self._modified = True + # Commit changes to index, which were not in-place. + self._commit() + + def open(file, flag='c', mode=0o666): """Open the database file, filename, and return corresponding object. diff --git a/Lib/dbm/sqlite3.py b/Lib/dbm/sqlite3.py index d0eed54e0f8..c8ee6f184b3 100644 --- a/Lib/dbm/sqlite3.py +++ b/Lib/dbm/sqlite3.py @@ -15,6 +15,7 @@ STORE_KV = "REPLACE INTO Dict (key, value) VALUES (CAST(? AS BLOB), CAST(? AS BLOB))" DELETE_KEY = "DELETE FROM Dict WHERE key = CAST(? AS BLOB)" ITER_KEYS = "SELECT key FROM Dict" +REORGANIZE = "VACUUM" class error(OSError): @@ -126,6 +127,9 @@ def __enter__(self): def __exit__(self, *args): self.close() + def reorganize(self): + self._execute(REORGANIZE) + def open(filename, /, flag="r", mode=0o666): """Open a dbm.sqlite3 database and return the dbm object. diff --git a/Lib/difflib.py b/Lib/difflib.py index aef2b6f0fd8..95ba8fd782c 100644 --- a/Lib/difflib.py +++ b/Lib/difflib.py @@ -33,6 +33,7 @@ from heapq import nlargest as _nlargest from collections import namedtuple as _namedtuple from types import GenericAlias +lazy from _colorize import can_colorize, get_theme Match = _namedtuple('Match', 'a b size') @@ -637,15 +638,15 @@ def quick_ratio(self): # avail[x] is the number of times x appears in 'b' less the # number of times we've seen it in 'a' so far ... kinda avail = {} - availhas, matches = avail.__contains__, 0 + matches = 0 for elt in self.a: - if availhas(elt): + if elt in avail: numb = avail[elt] else: numb = fullbcount.get(elt, 0) avail[elt] = numb - 1 if numb > 0: - matches = matches + 1 + matches += 1 return _calculate_ratio(matches, len(self.a) + len(self.b)) def real_quick_ratio(self): @@ -701,10 +702,12 @@ def get_close_matches(word, possibilities, n=3, cutoff=0.6): s.set_seq2(word) for x in possibilities: s.set_seq1(x) - if s.real_quick_ratio() >= cutoff and \ - s.quick_ratio() >= cutoff and \ - s.ratio() >= cutoff: - result.append((s.ratio(), x)) + if s.real_quick_ratio() < cutoff or s.quick_ratio() < cutoff: + continue + + ratio = s.ratio() + if ratio >= cutoff: + result.append((ratio, x)) # Move the best scorers to head of list result = _nlargest(n, result) @@ -939,10 +942,12 @@ def _fancy_replace(self, a, alo, ahi, b, blo, bhi): cruncher.set_seq1(a[i]) # Ordering by cheapest to most expensive ratio is very # valuable, most often getting out early. - if (crqr() > best_ratio - and cqr() > best_ratio - and cr() > best_ratio): - best_i, best_j, best_ratio = i, j, cr() + if crqr() <= best_ratio or cqr() <= best_ratio: + continue + + ratio = cr() + if ratio > best_ratio: + best_i, best_j, best_ratio = i, j, ratio if best_i is None: # found nothing to synch on yet - move to next j @@ -1094,7 +1099,7 @@ def _format_range_unified(start, stop): return '{},{}'.format(beginning, length) def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', - tofiledate='', n=3, lineterm='\n'): + tofiledate='', n=3, lineterm='\n', *, color=False): r""" Compare two sequences of lines; generate the delta as a unified diff. @@ -1111,6 +1116,10 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', For inputs that do not have trailing newlines, set the lineterm argument to "" so that the output will be uniformly newline free. + Set 'color' to True to enable output in color, similar to + 'git diff --color'. Even if enabled, it can be + controlled using environment variables such as 'NO_COLOR'. + The unidiff format normally has a header for filenames and modification times. Any or all of these may be specified using strings for 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. @@ -1134,6 +1143,11 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', four """ + if color and can_colorize(): + t = get_theme(force_color=True).difflib + else: + t = get_theme(force_no_color=True).difflib + _check_types(a, b, fromfile, tofile, fromfiledate, tofiledate, lineterm) started = False for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n): @@ -1141,25 +1155,25 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', started = True fromdate = '\t{}'.format(fromfiledate) if fromfiledate else '' todate = '\t{}'.format(tofiledate) if tofiledate else '' - yield '--- {}{}{}'.format(fromfile, fromdate, lineterm) - yield '+++ {}{}{}'.format(tofile, todate, lineterm) + yield f'{t.header}--- {fromfile}{fromdate}{lineterm}{t.reset}' + yield f'{t.header}+++ {tofile}{todate}{lineterm}{t.reset}' first, last = group[0], group[-1] file1_range = _format_range_unified(first[1], last[2]) file2_range = _format_range_unified(first[3], last[4]) - yield '@@ -{} +{} @@{}'.format(file1_range, file2_range, lineterm) + yield f'{t.hunk}@@ -{file1_range} +{file2_range} @@{lineterm}{t.reset}' for tag, i1, i2, j1, j2 in group: if tag == 'equal': for line in a[i1:i2]: - yield ' ' + line + yield f'{t.context} {line}{t.reset}' continue if tag in {'replace', 'delete'}: for line in a[i1:i2]: - yield '-' + line + yield f'{t.removed}-{line}{t.reset}' if tag in {'replace', 'insert'}: for line in b[j1:j2]: - yield '+' + line + yield f'{t.added}+{line}{t.reset}' ######################################################################## @@ -1615,16 +1629,13 @@ def _line_pair_iterator(): _file_template = """ - - - - + + - - - @@ -1636,13 +1647,36 @@ def _line_pair_iterator(): _styles = """ :root {color-scheme: light dark} - table.diff {font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; border:medium} - .diff_header {background-color:#e0e0e0} - td.diff_header {text-align:right} - .diff_next {background-color:#c0c0c0} + table.diff { + font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; + border: medium; + } + .diff_header { + background-color: #e0e0e0; + font-weight: bold; + } + td.diff_header { + text-align: right; + padding: 0 8px; + } + .diff_next { + background-color: #c0c0c0; + padding: 4px 0; + } .diff_add {background-color:palegreen} .diff_chg {background-color:#ffff77} .diff_sub {background-color:#ffaaaa} + table.diff[summary="Legends"] { + margin-top: 20px; + border: 1px solid #ccc; + } + table.diff[summary="Legends"] th { + background-color: #e0e0e0; + padding: 4px 8px; + } + table.diff[summary="Legends"] td { + padding: 4px 8px; + } @media (prefers-color-scheme: dark) { .diff_header {background-color:#666} @@ -1650,6 +1684,8 @@ def _line_pair_iterator(): .diff_add {background-color:darkgreen} .diff_chg {background-color:#847415} .diff_sub {background-color:darkred} + table.diff[summary="Legends"] {border-color:#555} + table.diff[summary="Legends"] th{background-color:#666} }""" _table_template = """ @@ -1692,7 +1728,7 @@ class HtmlDiff(object): make_table -- generates HTML for a single side by side table make_file -- generates complete HTML file with a single side by side table - See tools/scripts/diff.py for an example usage of this class. + See Doc/includes/diff.py for an example usage of this class. """ _file_template = _file_template @@ -1892,8 +1928,11 @@ def _format_line(self,side,flag,linenum,text): # make space non-breakable so they don't get compressed or line wrapped text = text.replace(' ',' ').rstrip() - return '%s%s' \ - % (id,linenum,text) + # add a class to the td tag if there is a difference on the line + css_class = ' class="diff_changed" ' if flag else ' ' + + return f'{linenum}' \ + + f'{text}' def _make_prefix(self): """Create unique anchor prefixes""" diff --git a/Lib/doctest.py b/Lib/doctest.py index a66888d8fc9..7311ce4c926 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -102,15 +102,14 @@ def _test(): import re import sys import traceback +import types import unittest -from io import StringIO, IncrementalNewlineDecoder +from io import StringIO, TextIOWrapper, BytesIO from collections import namedtuple -import _colorize # Used in doctests -from _colorize import ANSIColors, can_colorize +lazy import _colorize # Used in doctests +lazy from _colorize import ANSIColors, can_colorize -__unittest = True - class TestResults(namedtuple('TestResults', 'failed attempted')): def __new__(cls, failed, attempted, *, skipped=0): results = super().__new__(cls, failed, attempted) @@ -238,10 +237,6 @@ def _normalize_module(module, depth=2): else: raise TypeError("Expected a module, string, or None") -def _newline_convert(data): - # The IO module provides a handy decoder for universal newline conversion - return IncrementalNewlineDecoder(None, True).decode(data, True) - def _load_testfile(filename, package, module_relative, encoding): if module_relative: package = _normalize_module(package, 3) @@ -253,10 +248,9 @@ def _load_testfile(filename, package, module_relative, encoding): pass if hasattr(loader, 'get_data'): file_contents = loader.get_data(filename) - file_contents = file_contents.decode(encoding) # get_data() opens files as 'rb', so one must do the equivalent # conversion as universal newlines would do. - return _newline_convert(file_contents), filename + return TextIOWrapper(BytesIO(file_contents), encoding=encoding, newline=None).read(), filename with open(filename, encoding=encoding) as f: return f.read(), filename @@ -388,7 +382,7 @@ def __init__(self, out): self.__out = out self.__debugger_used = False # do not play signal games in the pdb - pdb.Pdb.__init__(self, stdout=out, nosigint=True) + super().__init__(stdout=out, nosigint=True) # still use input() to get user input self.use_rawinput = 1 @@ -928,7 +922,7 @@ def find(self, obj, name=None, module=None, globs=None, extraglobs=None): # given object's docstring. try: file = inspect.getsourcefile(obj) - except TypeError: + except (TypeError, OSError): source_lines = None else: if not file: @@ -1173,6 +1167,32 @@ def _find_lineno(self, obj, source_lines): if pat.match(source_lines[lineno]): return lineno + # Handle __test__ string doctests formatted as triple-quoted + # strings. Find a non-blank line in the test string and match it + # in the source, verifying subsequent lines also match to handle + # duplicate lines. + if isinstance(obj, str) and source_lines is not None: + obj_lines = obj.splitlines(keepends=True) + # Skip the first line (may be on same line as opening quotes) + # and any blank lines to find a meaningful line to match. + start_index = 1 + while (start_index < len(obj_lines) + and not obj_lines[start_index].strip()): + start_index += 1 + if start_index < len(obj_lines): + target_line = obj_lines[start_index] + for lineno, source_line in enumerate(source_lines): + if source_line == target_line: + # Verify subsequent lines also match + for i in range(start_index + 1, len(obj_lines) - 1): + source_idx = lineno + i - start_index + if source_idx >= len(source_lines): + break + if obj_lines[i] != source_lines[source_idx]: + break + else: + return lineno - start_index + # We couldn't find the line number. return None @@ -1283,6 +1303,11 @@ def __init__(self, checker=None, verbose=None, optionflags=0): # Reporting methods #///////////////////////////////////////////////////////////////// + def report_skip(self, out, test, example): + """ + Report that the given example was skipped. + """ + def report_start(self, out, test, example): """ Report that the test runner is about to process the given @@ -1380,6 +1405,8 @@ def __run(self, test, compileflags, out): # If 'SKIP' is set, then skip this example. if self.optionflags & SKIP: + if not quiet: + self.report_skip(out, test, example) skips += 1 continue @@ -2277,12 +2304,63 @@ def set_unittest_reportflags(flags): return old +class _DocTestCaseRunner(DocTestRunner): + + def __init__(self, *args, test_case, test_result, **kwargs): + super().__init__(*args, **kwargs) + self._test_case = test_case + self._test_result = test_result + self._examplenum = 0 + + def _subTest(self): + subtest = unittest.case._SubTest(self._test_case, str(self._examplenum), {}) + self._examplenum += 1 + return subtest + + def report_skip(self, out, test, example): + unittest.case._addSkip(self._test_result, self._subTest(), '') + + def report_success(self, out, test, example, got): + self._test_result.addSubTest(self._test_case, self._subTest(), None) + + def report_unexpected_exception(self, out, test, example, exc_info): + tb = self._add_traceback(exc_info[2], test, example) + exc_info = (*exc_info[:2], tb) + self._test_result.addSubTest(self._test_case, self._subTest(), exc_info) + + def report_failure(self, out, test, example, got): + msg = ('Failed example:\n' + _indent(example.source) + + self._checker.output_difference(example, got, self.optionflags).rstrip('\n')) + exc = self._test_case.failureException(msg) + tb = self._add_traceback(None, test, example) + exc_info = (type(exc), exc, tb) + self._test_result.addSubTest(self._test_case, self._subTest(), exc_info) + + def _add_traceback(self, traceback, test, example): + if test.lineno is None or example.lineno is None: + lineno = None + else: + lineno = test.lineno + example.lineno + 1 + return types.SimpleNamespace( + tb_frame = types.SimpleNamespace( + f_globals=test.globs, + f_code=types.SimpleNamespace( + co_filename=test.filename, + co_name=test.name, + ), + ), + tb_next = traceback, + tb_lasti = -1, + tb_lineno = lineno, + ) + + class DocTestCase(unittest.TestCase): def __init__(self, test, optionflags=0, setUp=None, tearDown=None, checker=None): - unittest.TestCase.__init__(self) + super().__init__() self._dt_optionflags = optionflags self._dt_checker = checker self._dt_test = test @@ -2306,30 +2384,28 @@ def tearDown(self): test.globs.clear() test.globs.update(self._dt_globs) + def run(self, result=None): + self._test_result = result + return super().run(result) + def runTest(self): test = self._dt_test - old = sys.stdout - new = StringIO() optionflags = self._dt_optionflags + result = self._test_result if not (optionflags & REPORTING_FLAGS): # The option flags don't include any reporting flags, # so add the default reporting flags optionflags |= _unittest_reportflags + if getattr(result, 'failfast', False): + optionflags |= FAIL_FAST - runner = DocTestRunner(optionflags=optionflags, - checker=self._dt_checker, verbose=False) - - try: - runner.DIVIDER = "-"*70 - results = runner.run(test, out=new.write, clear_globs=False) - if results.skipped == results.attempted: - raise unittest.SkipTest("all examples were skipped") - finally: - sys.stdout = old - - if results.failed: - raise self.failureException(self.format_failure(new.getvalue().rstrip('\n'))) + runner = _DocTestCaseRunner(optionflags=optionflags, + checker=self._dt_checker, verbose=False, + test_case=self, test_result=result) + results = runner.run(test, clear_globs=False) + if results.skipped == results.attempted: + raise unittest.SkipTest("all examples were skipped") def format_failure(self, err): test = self._dt_test @@ -2444,7 +2520,7 @@ def shortDescription(self): class SkipDocTestCase(DocTestCase): def __init__(self, module): self.module = module - DocTestCase.__init__(self, None) + super().__init__(None) def setUp(self): self.skipTest("DocTestSuite will not work with -O2 and above") @@ -2875,7 +2951,7 @@ def get(self): def _test(): import argparse - parser = argparse.ArgumentParser(description="doctest runner", color=True) + parser = argparse.ArgumentParser(description="doctest runner") parser.add_argument('-v', '--verbose', action='store_true', default=False, help='print very verbose output for all tests') parser.add_argument('-o', '--option', action='append', @@ -2885,8 +2961,8 @@ def _test(): ' than once to apply multiple options')) parser.add_argument('-f', '--fail-fast', action='store_true', help=('stop running tests after first failure (this' - ' is a shorthand for -o FAIL_FAST, and is' - ' in addition to any other -o options)')) + ' is a shorthand for `-o FAIL_FAST`, and is' + ' in addition to any other `-o` options)')) parser.add_argument('file', nargs='+', help='file containing the tests to run') args = parser.parse_args() diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py index 7d13414ff82..bc2fb742cb9 100644 --- a/Lib/ensurepip/__init__.py +++ b/Lib/ensurepip/__init__.py @@ -10,7 +10,7 @@ __all__ = ["version", "bootstrap"] -_PIP_VERSION = "26.1.2" +_PIP_VERSION = "26.2.1" # Directory of system wheel packages. Some Linux distribution packaging # policies recommend against bundling dependencies. For example, Fedora @@ -131,6 +131,15 @@ def _bootstrap(*, root=None, upgrade=False, user=False, Note that calling this function will alter both sys.path and os.environ. """ + + try: + import zlib + except ImportError: + raise ModuleNotFoundError( + "ensurepip requires the standard library module 'zlib' " + "to install pip." + ) from None + if altinstall and default_pip: raise ValueError("Cannot use altinstall and default_pip together") @@ -169,6 +178,8 @@ def _bootstrap(*, root=None, upgrade=False, user=False, args += ["--user"] if verbosity: args += ["-" + "v" * verbosity] + if sys.implementation.cache_tag is None: + args += ["--no-compile"] return _run_pip([*args, "pip"], [os.fsdecode(tmp_wheel_path)]) @@ -206,7 +217,7 @@ def _uninstall_helper(*, verbosity=0): def _main(argv=None): import argparse - parser = argparse.ArgumentParser(color=True) + parser = argparse.ArgumentParser() parser.add_argument( "--version", action="version", diff --git a/Lib/ensurepip/_bundled/pip-26.1.2-py3-none-any.whl b/Lib/ensurepip/_bundled/pip-26.2.1-py3-none-any.whl similarity index 57% rename from Lib/ensurepip/_bundled/pip-26.1.2-py3-none-any.whl rename to Lib/ensurepip/_bundled/pip-26.2.1-py3-none-any.whl index 24b5cc90ace64391f8ff6ac218837e03f980d485..bc442f6b99306b37cbafcd1abb573c6c429e5c2a 100644 GIT binary patch delta 821454 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

tD}a>Ib74JK`?<&qIfMkntJe~ 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[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∈-i_>tEpTYzpOfP5kKLS|%X zkuig)WdI45P4R!b;saw^N5-@j!$_vS4AtS6#+(2fnMhEMpY#X$Hi4-_Aq-nQ>Pe+B z%+CA~_4NcImBW$q`_r-jbt{92hp<>iu!x!*5c9Y=e}CrgFXY=U{ha0=$eTe&>?YI? zmuhjNU0h>USP!IIrIpFJ8WS^ZidRy+k@XK!0Cnyo4YavpWL;wL0){J`4qGFW7Tck?gonSCCr zAZ~lSvz=ZxRM3TcdGh6_XD?qQ-+r4s`v$GVFWcXI{qMMPf}|te^(V|7sx^ILeI%Tj>Ui+qXkv2?Zcu* z>vnOQ-)M<|gUB7w)_aFqm83pX!LkG9s!)W*u<5IA)JGJ~WlPQ*g8T%w$idYS)0Zmi z1x+px*_9W58XH|it$mWbDStky7j&Ut^`@=ReOWdzCO~nm3Q;Uxs&_b|saIF)j1#La ze=Tq_wYy13ALph+ayj)i)|Z!kU78H=0hqY$(t0uVe4Clf33m+DRRjd2d1(6+xI&s~+ z_CRKnF6icXQs;NQ5I>7cbga;Cyu9Ypf4Z&w^jiug zy__-qd@UI}-4>nxRe}d>*f6h9O zCsybMfLJ<@jm$^mA_mi|LCDSdkB1Q;L(tx6>%uDM&TT_})Un+Ex&G!>h%uZzfA-M3 z(LleblkxAod`5fJ&Fxlnvx3X&jcp-kEk0$-8nLIZqM}yQY9@%khmSF1r}P+=n6J4NB4$m%6ed55rZhn(rGd z+J?@4I%<-4xq14D1FHlPZ1QY-O00;ntvjknYzQhA=0}h0< z1YJHA7=IQU007iplR&-{mrlq77k_iyHj>}B@J>+0LHp@+v!7 zXG>O591=6bh(j_2NAj$q|9<@dKmY{CN@9JmB@$>f`rVBN*L}N-vTVH{_PxlmsM>XH zKSX)cv_n2rZ6i-k)U)-zDTcPKrGC(w-{ismwP>4lwK-WM=%TG_Q4pXsUw;)E{)@b> z^HnXD(LY2Z`g~~nW%PQ0-{2w7xo5PY(<6xGTH9?LJb}cUDa&#ps$<5 z2|w?uF3qY205^G^iVub82p2u_Td~QD!^=tzxQKk+$N?~0Mn5+d|N31eB|v^7incF9 zFzdF+9grI_WUH#lx?ZfS4}VMZ_g;!V1YjqIyv&DO&;OrX3Ph-{R(k{-nnTE@MZ4?r zLC<^l`umqZyn8!8Tx>R3T{YKwg8n-LAX_G>-po9Zy4`FnO2g*9g(m{IS2d+*1^}^{ zpS`Q35rJG@FQcaIcX?g?TM!bNAEMs?$!cARGD6T%yXJRQEo4;C;4)erX!;#!scb<5VcC0y zu#DDiPg6G4jcB5)s}cn9>MErfZv@x~kaK)r!*c|WplL0saiagl2SJ+yDz!32)~3I< zRg)+xI0RVGj6)^r(#)er)f&XZ$Te>oe|o~hnzfGa^u&A6aeomXd6(|;rdkU*#Ci=y zc>{hr@077kyTh&8UMBE{E5bOO}QQk7vBT zGR8W+?ek71>xSu!$S7Q%=KV&VM+D*N)9YLM)e}7*{J3i=1Z+c!0mobdqv6+NVP2yk zRiLC`7GS@;7Jo0Z7-47*;5cM8j3uzn0KHh6mt}%O7n9e_0ep9IGD?WrCve~^Mg9}D zdp7N4>B#QL1m*mEVtOB{hxGry2nm2o3gWI3(JDj~Tq#%~R=Fl3*tj(34n?~ONQ~_==g4wK)=6~ff0-_NxoC)s>@a!f2(&8X| zC{ayIZ6Z@%+PJ?TVfD9SAlfgYA-_gLlJ~~6WEsFDmz+fkc?IJ2THyc+M$BEeU`xTU z=amE?c@v4g2d__qasN@*f~#E-`fGFxL^$^Du+r=A-~ROVx8J^cLt9jfY=@zf=VxbK zUR>wsmwx~Upk$M_{pL(|q5xy4-1AvaE*!?3s=bNwl;;U~=5SJbK@e)>mT~d)GMC`T zhjEPB9g9WGQmnwoj3juazJe_)XdtG2R*IqqGM1TosWb*-VG^Us^Ax-UQ0vB8 zMs{^IB>1bV$*2@CY~5U(pZ@*Qc@;tw;nI}PB3!nJU1$Mx9|)8$*N6A0(cpoHT&dx%YAT3?qGrEaL3GhFW!bAXq7YK%z(i{j zntubf4O)>wO;PL4@-uvXhMG>(^@E{>-$;3e4}c0N@u~%6#_7wb7Wob6b_1aVJfqoi z3*iu8MC3vZ;isarIV!qwY^KPfiQu>f4vIzSwUJjM5xN3gxMV6Xqib<^4gr`YT*bbJ zaB%TuF10=HWvNuz;}+7OMu1BN1z8D|MSqma zN&K`0TZ(gcIhw(u-j}1tLsNUZ6|4jBCpZz%LMb3xXbR!#p_&U}>W%1?s|61L=Jq;u z97qAiB-`bvB6pg*nCam2q+6Uv9Sb_i4T8T{1l*~uu})|(OkN1+wxYO(<)3TSxbsZv z;^Onm5wf11-sJTjUCjNcMat{eC4V^ca#kCWd5CvLX7XuHxe9Y>=>32^n(WjuvQP7c zrQ?0e;hx#l2n|F!Zw|=>8ctcH;QRW4Rr_{9pl%V4*^1|U9x?Y#d=qVRkfa(kvOIvK z0N=M@CTo-xdm2hv)nM$XpMbf|DDs*yY4)84>p11cl`fxqv@2oOSPbVLXOgIKUdU=uXsEl4_twSrnp zwlG3V%;uxh5jl_A<~bPV^?zFQXb&<)D>L7vsgXi5lqBPyAw$W8J2ny|0yNWTc$#G! zF<=6dWlIaAMfCd!G-OR7Y3TMthUqwrpSP;RzQaG2OfHRdhRiIDMJ|1T!zBI&J`nN? zlZt@F>*}1s5vpn_Gm9bsUct80(zejI?H&Rtu+OEa1=uf0U5E#;0DlHX6pC|<4Wfg5 z0xE*JLuMmChqgPd!SvRaI;%89O=j(mo;-cRQyJJmLm(mmW&_5u+BDVrz$l^YR>KoX z_<~oHv^I<1=sz$zf&rzwGbP01&bAmlkx`M?Ro979$5#0Yz$x2Tmjcw+xC?EF>rBZ! zn+u}aVtk1JUEBO9dVdKt>>+Lx8Y)qWu(H>33=re&Jh37!3lGO0PlNozkMfPhoS&9u zAk;&aQH}t<7J_ZeR8BG7i?u6sp=xrWI!(hQ2vaPxO15l_U+6`qy+@hZJ4e;?{fjce zqm$Ca)6u?qV#rU91x+{Q|ku=JMaLF_yQ$6t?JZa(66W~%|u4>GzX~YV=qfsB>`GohAw+KI*q+1&E z^;rJ%R!@TKXegCLxCsHDPRAcqgh>Fw<8`6Q7mW#=naw_m-U%`iq~jciR%{H?=<5>8 zP{??lkp*EGm47CARr?570%q#(V-QL7W6mx)sR>HTz2j^wn9Ff#@Fqo_gJixPa=8_y zU-B`I;cMGztI#@XJFJP(u?Lz|1fl&urb}FM0(%N$z+fN7a+nI-cVj9^zWE&d}>d9*QrCe9u`0RrUSYn}93T^{222cy=aV((sQg#8 zfq$ljs5c}3wI?F#Ny4U;HgAWSdICCq{rlg>*~Pv`v*6`+PQ`-Us-tjA4QcT}z5L_a z=%`Voe3RM)r(zYPBj`x95gk3k|5DK+l7}50;DctHY^06yn-}mc2Z2~7 z(Te{^$s_NXLJ$EVZIT!#oq1V=i@Xy@G9k8mxZs-Y76@L)x z2HP)Wt5@|VuT*x7R>TDibn15wi7eIOP6&_ezG~?gqgFxgwX24?sE`j+sMzhJjt?N}@ zTp!uy&kzxcz%E03j1bmjqusbe4u2$_O>s|4ikWy4f5$*UtRBnv>IC+&jF-{V&QdD` zsu{K%&ur#a2zpnwY$;azO%lHmU7Z)W^kZqQ>MjyY7rn&L2pDRRXRBuPSO381ed7+C zj1#r9F5%J?5U0Uby6Vhz#qM~8J7OpGb$NQ`0oiVyU? zM3kpdp9!}d1PoNkIKqIL^nX9Gi~Oq34C9cRJ1~)iZtosS*uI9?f=81JY$mL;fP!eK zImo3gB)z>TT-S^-kKrpGT zq$wt;wF@(&9bz;*Hc{36o*yUfSnHrH>#!O-;JI8V40L7QHqTsnkAHsZ_X3UC*nY=G zRdOQ6MJKa{9ZImUpK-{yjK0Y+XlMaWe_KgA-?LYJ6y?`BHop;9ml23Gu@)I;_E=&& zZ_3BNc8r8U;9Q5K?=bsud%nu^+2wemcZ3_sh{(bsI@ZrZ)ONL!6RH7>;Y=HfMHLLy zGy=p6%8b~8wh$cY1Aq20bk|Wh)39j=Pabhyh^Q(&Prg;%6}LH;OGa-#LP6qk8AXNb zbEz|@Ek|gk^2AB1jTkMgT`P|2aTG$bGYaz3o~LY-5&gg<%O(c;3j+NJmUZ+C`b<^3 z>r6Gb=G1s_8|}L&U*j?1ld;Q-L%-p}@C3CXPvL)b)9VExKsiwfJHSHsGn+fvNXuPMq~vlK_ReuZJy;@=FhzwzlY1 zOlPL{M$;;Zes!TIp-T#QssK(i^NEOF2iikK#lRoKYqE}t%=vz3GrFJ9@3`3-5G8}T zjBZ4~YSAIvkAK7Q_!~~!*oRiwC~XP()fM+Ms%1FjNFb~fR-NbS%0>mIfMlYHT)1UU zrI~hEF&nr~*b=|Un_Iq&d#M^BIL=gB<#jnw5eXpgx`Bvbn*~SL2^-Z~ zk?iC~xq{iA4%aH-kFpj4JApsGQu;)MCH;1mqq<_a8kRp6*%{W_>v{sCHleBSVH7&b z^fkw8ig7eZLkgA4UpIPHET4*{>2q;AoValWOn>L5Rdtd{VNN%=%ptE*y&z_0=(Ohp ze~g60??}*8yGFdU;V))~Q{>J#B;0xck_t%?#O|Ou)Pk@uzz~CnfdZ0K1rqxuIY>t! zwtIG>kRWp{_*m@(8DgaL{yt_M7xozf16l12J?N63mxVk8_;5efyS+qN_a zQpSKpN{U(@Ej)#*Vgv`>-O)=dB!$4V$tdTcEwC=k$>kJZ7^Winy)BBW$jdo#HRww; z#{`wo0|jZDs>tgH3TgyIOswlL_%jwaGM`Xx9+fz+7ry!$s@8v%4B$>41Fy9W6@Sf5 z)wc~6B}sGsI4O>8GhhNtW#!Kiv|7*86jb%jRZQnEb24#fu8pU7nn&c1L#{LVt>-;u$w~1@n$=?;wWAN!{P|9p;ODJU4Jo0mE2Xi z6L}*oyO2{n1{)q5MkBkR58P=iaIgaP7S(jQKN2N3J=j&>Vc$_TfXZ&F9)MI>bB;OUM+R-a%UgKj4pCJ6OjBKh?)F7 zuqm$-*tT~b(1kDh-@N|iyI1GjNRdN?CI{7yhPr@fvMT^}?M-S6V60?&bb5;I*|j)T zj6D?}_$v_g;S^FJkdX51^px)jYj5m>TcZp)4{2#8KZ|)14`5vBrhft!okLyI!P9X3 zp<`eWSByaTYO>}nURlbh1RYNY@j4TT>c|*xS%7Ez1}l9tJ!sC_UiZSdQ9ok~&J18j z)pKw!AdJ^SU<;0X&eheRxEbT9{GOQ{UspU>iKTp3bo;^G54u4;yUn0QhMtZM%=e-B+ zmtTHC^l_a4tHn4Ts*s+hty3~b|U(IpX}^P(|A9|#q>8QQdlfL7U>75L#Kv5 z#>*&9&*JE_34c*Oq8Jp;63Q5EOm7S6;_mEMQv%*?-duU8czKAgoK9X6Ykvut2hpeY%xQ4A3~zy&4Djv? z^LI~-f`Qaiu=7w%m$P=smHuVm^4B_k1xW*?s&_u&@1q1_`8#{5`$a_UlB1!H10YAE zZ%iRXcX#e-WRyVqf9Ifkzli^szD{N3)7qX1_+`Mxd=eAm*1^r5FJz|bb9@pKCKqq@finKOoB3>Ca_rn_I)#gA ziIdgR6$}&UsPCW2=lT~ja*`QPmog19K`p0w8kYO9xd)o#V_#(pSRbI<7kLpu) z^i5DltPKBb==^zznM95}d>9vt37-%T>Yj1?)2UK4!&)*UnqL!fnu@cW9xr;s0{zOS z@qdZo2Re(LzOT84V%6-W$GR`|SG_*gM~nwqE`)0RY3RtUz_O%8_1Uze}|;NhFB>Go8rKI?=1d~J`G!RSq; zkLXK*Twmem7eMs)gAIy1x2#iZbOq*faDUv%sVjrjNh>L}f;Dd4kBw-2tVd1`Qufsi zMXY;C;i??4owT(t5CD2(;)s7)um)^a#7L$&)w@u*Qz**NYe9w4UEW_qvL=DK*Fezo zb=$sQ<-G+BzmDNRNu$@ePl$?5z=Y+mO@qy^KY#y1ceqjVcYs076T7>WK6tUiYkvhm z@Ft4iznA!Fr1x^0_aaWCe?qv2AN9<49ep{62J~jc`#5TC^!(R+n>}_u-=f^mSE z2X|{|?6B2v%(47p-^4b;aQfe-CVr1<6(bJxosycrxzh75>Vf~|Dbdh+_HdPqqclhd&>rEVdb(0_~>WJ-PvRRkx9B}><8%Q2< zH9IW9w=)t9GI!g9Q(>WF30FDRV&wJ~noez7GLLm#s0&|Oc}*vf?oFcGd`r?`?)$|K zrH?5u$9BGD)7SM{b41RaP_1cz!o%@l2Kry2;)EoVE2MPGT_M1ldt&;_9~MpbmV zIP`=Qr?y)bKAuaWM5rkUOCHYldwzab3pWs$2!lm#b?eIjvFTV0o;;B+37xC^sV;Wg zBDRLodfEz}`b-72G&f;(<8zOva};c_wZFiMHUWPi-q4&|61@svVub?tv$EVliho>9 zHa9lI7rbzbhg&^&aLxtts_^I)_oz%4lx!fZum%|`tD(?`BGnrZW-F3(&48j>zGP18 z!V$wqq35o@46EYvh)4-hGSk4fhP#KWi?ex=_^6na+pg(oXk+y(!m?VsS>8L^WWDb{ zu3$;lEYyx3XV>n|8h*Rse`>w~Pz_511QY-O00;ntvjkn2-sA&E5rne@U3)~CVHO$y z04`nt04tZV;{zO*$~zkpmt^GwBL`E#ATV6Q3zv@N1A_rwmlEa!mIU3^?VXpb<^wK& zR^tdP&+-S@d)H^DqP#B<7c&sKSnq4us|$lOm!f9oJNwLCsBQt5wrH}lD1n4}@Gl1` zn{V(*YUsr13HXvY^mnO-{|40H&C3Q^aDLsY0c1{AYZ~xvt%kndk#xW5hF8Q#dh?%! zI(*mnT_60Z_-_q|_~ZKLP!Th)UCb=1q8~h(UAaeWM)jV)OF||QR%6pjdP&9RwgZY5XgES9+`EWkT z8IqfYlpGQ${v?O=Bx4z~9Z=b&IN4J$FY5%BZ~N|{p5Y!qbCj?7CO^mq7-vn2eS@>}W=1p^j$9@EzqK^v85T1<$HRz%=5N5f(2e( zU4f#M=oMu3tz+$BmSa{wfH;Z&B~@RG1~ltK%;Jw9udc3Oj>s9E3WkL^u@K{%O{AnO z$SwsoVi-@2`~cfu3-h0kJg&Y2s>u-kM(n_%1MaViMxoq`+rJUk?26HU2jTyJg9r-H zXY{7zweD)L-+~R5!2z9^5ixMHk2N^6f{-^T->_3$Xey{%2CxvIhNH}`=uO;iD4PXFOJ1;U4MJ5Tl#O=cN3aJ#{!VK%gxc}7qjV3 z523Z99Zbv8PDH)GM5XqBW^f2)woSo@n)L&OO8W%Z2@My#%gJl;{s3&MtGZL?wnWQY zf>@q0f&xfDS6tvH5|j@v3g5aq{GxF?N*>SY%S{!U^!2mXNDej7_a_M>Do*@-3&~i4Cm1 zm#%Vb-9PLwZ(BA3%U-k3n>a!n{8U<8*$QTlMc)ExeWVsTv=A?Pae#>h@|WO=KqEHd zg<7ly6TEmN^SQ=!Vgwg(|Tyq+RWa!nl= zAxydMjsNiS5AZTYdp2uZ8%qDkbw-+F&^X9DcK@Fj5{3e=#RBu!+j@VC7|RNu4Bf4Q zwblN7i!T>{UlTZt!X+osm_aES4LKz3`Vt3eFyZU*bk`S^G*WPX-k|+=_;$``gR?BlK5Ei~_ z@fQih2x7ZjWs{g#r3YOCmmT4OT8JW)CFJzVl3*WVh>qA_JFyLPq-T}6k3MYv`f-{9 ziQkg|(((H;7`VFf6e7!GK*5t>f>9hCX!1vIfX5nQ=dWOui2B=zA;GG? zRN?Y}{u6(ALVI9J;%Rc4HICazDUuW7xGDW2AcCv^NCEfnfQ_Mrf#Kn!kn`1)as@`> zIl0&p^RJb)x7t_)=2rAwN9h+polDr(I&PaBYffUJjg?hkzbat0!TL6kr(&=U$@6U$ zsGudlOLV~stnIA?H(UIxDgxMTD}|m7tx;@$&upE_e~rJ3 z6|E)AUU^frgQ%+9-PO?H#+JcGvD*H-V~(%I%W@bgZ8Rl2r2ZbFW3?tp(gkITp={wb z6F+p2%mL#M4lJiITT)>gm4J4GVtJ5%MMW}SWBvdfEx@igv-4AT8bL}Wm7JC<(j=u- zsbtf)lY30uAMrs;#s;`v){5%q78KU!V69eAjT6ac#lW#Zmu&$9r`BYF3iJxhkIK`&x5Sf)P3|`_ngMTJJ4IuSPmI7@NbI{{7>wVGDIpwr}n<#^H z24-yF(WZxQ9ol5Y36*dO6INb?;*9aZbB^P@zH+n_Q=w2xh__&enzC^TMr({H*#GHl{>eLOW7;?NTc1Zw0723x*=eFtv?13fCN z#e*){KtkV>t#eGA=2|RqI715^a*hWu1Au}OE1m`6D19^0KGc2J5_gY9e~-l{yBKTf zMu3FXX#5NRD442_V6Bnquu<5ftf}hv;Um}u-`)k1RCD2_!2OpTK!A<<;E3@@U7t-A zSqum}og5klD^!WxNf0%EW=%3ixE8Njxb>DHIwEwA#ZVsBwuX$S&347iHF{h~nXt`h zYOdieL;eKTU)QlHp^8kdEoHp`7iKWLd7*AFh`Z5PigP#iF8hM=6`|?i7gw>u5qc$m z&?+E5S?v#Co8EW<@I+93oXFgrf~mE31t6s42r_5lPY2RvuFnyF$%{P*G6pK}e@F%@ z0vWfT5LggH2Og!9#b+A)SSV2s`k;{_!U~j1h$cO#y@^C_7;Y<;j9eMG8w$ z7{iBItvIIiMS%&nwG@Yso*woteAOcgiC)G$ERM{k`8(ae`Fy2nOS(a z#3~LUn7PqOdMw&X_}ec&Ggz~=FOxB_0;9=7Ji;awfQl8vY!QLl^vjxJ+3n|0?!M~3 ztQLXrISNgu%a{N017{@e6`clAc_3*?UG`f2)BTTg>KT%Bin%;}w^ff$svN-O`s~V= zit+<;JOXdF_V1eaU$142bKpR|#_Y|G{%MD7Df!trG+IxE## z!#wDqvmmuZGO-AvG)rB@;VTwFrvu1m(APgFb_*cbG8WYc-Ik5Hy2-bcWK8`Wm*VpS zGJh2!3}G_P)V$+gPbMOK+-TqF7FGB4$sXlejv*tbeuWw4*?P0bLhkb9RNeujlS_)= zV9^tDaGFMGF$H*~nXThPB;;JHX?HG^+8j6O-Be#s zdjRE=Lz#(}tqtU=yuBzY!n2G~+m^kzure=c8~BN(ezP7aOW&yDm04$ zt2YqYD2&E{5RZp5r6+|lf=!j;H(piTwNb|CN8Lp!GwkQ+zR2j}SiR3Vv0BEKWfdBq z0D>#4&sg7}9=0_3p$we{COv|`dVjLu5-m8H%#;fc07l2%&vt={njzz}Fd6bhsN9c4bECBFp@=!d}gisS4kjIoe@Ih^$=4os)BDt$;U zd-)&H_!CG{*k<*IG(XaM3V%*Qq2R8Jr{hVyN*2W=T7aQHTD8!Szl z>0)ONe9lFCAG;oMZd)G2BXa!^Y|-N1Ta03;#`rg1{*US|sulCLet+OJ91qXvpt-Hf z4zI%wy}pid>g9tBD!)8+@iPzpuBHD{SzFu`s;ui9fsaZZb4VJ1{V#n1p1zL@BG(v) z=zjaZu##}RLwkdme}KFOBlq@};9FQK)|CY|5?DC_DdG46ZMtcW6Sf78PuNpMSf;Qs zMH}(i=iQfow5eI|Y<~!{lSo9(N^G6SH(%z8JopQ4X8_+;QR@qdjMYiML+VfDcHgyX z>~#bQ5?HXLV*M1X7nwfo4r{g6qbQi8SIIbZDg_?FKS9#ZQ82~Ww@#Gp9mof@QX3DO zvZLu9@nUiRNLCYwgYVAv5u{AZ%EA2gerh2$*N1X!K9baafqxEet!*#1quA4GY8Z~& z*66W5iNvaU18!Q0-B>qxkDD%JtvM*LC(L>|AYbccywr8)^mKBek$puaKY?`jc$rtFgx{|LI)-9HA?EM~8 zN(ykQk%AjURDT>h*il{T7R~x7)oiFe$w;y-eR^x0L)!E7UgM&PE8X614ITB}>lzRE zoN@scuv1JhD4Qa}S}fAIfhm=rIJ0Ta8X}Mx=PZ0l$ze)=Ham5Yr^82Qh;P4fk*z_= zuLYWW<8)G#?%}W}_jokF4Nm`4l|!Jei>^b1WQz(HIe!;FV;_VkV}!WAKjr|WZKT=` zCX(a7yS~1w+oH**lLi+AUReeyjSlUML@^rNtB|X1#AHcJnOP`Azy;5X&+ptUW3<)W zV~An+!D&<{#2-W)d}EU3n2Ao-6^}&-15!{1nEj^QD!v8c%v8P4h5<73QUri3r8W@s23iboCls*fX4C zcnjNel<$jsx-ysOA3#ZD9p{FA>iv1S!_2lE5TL%Y#)vytn+w~P|4{Tb*1L53B=#*{ zw4So6hr+u<$QGBR5mRNZC6o4(p{=Kfd(#Ru(p#K21Ca7^@;o##RvxBDM@c{PD>$pr8;ZXcRE=mrO zAcw~;yanHiUI2AKnQOPdO<#)gf;%rAqLSiv26Y@B zPJh2CY&IbPRtzyjEP-oy%x|v5)^5?lUgf4l*SXDUqBebu&00fi@dP9l*2ppsF+f5W zluv!P1NlAU7cE5D$O2vJc%t4r!JpBYE{CY|o@ z=tBguFyEAzN_vRwbe3@oX-Qb>z81^te}Dd$DDiw&f${fe6%d~<^+HFwJ`95;JS%^5iL1}GrJQBqwHq!!s#=;^xEC$4<`8gnzj^ z37#X>*PE@U^eU#@C7UYoBXBCi+OG0=Dbh{If4aDu=ATHumynmz;nJf%*Wl}O^eq6L z@Vc07oVneO%-_;D)RiqhuhB|jISAn_Dc{2b85m){!{xPQfb1PHamJnz=Nkil>uz*v z7xV-LFHJ}>(+@OzZZU--`qa`v^M5UU_QQrbB6&B}^+|r{HCJc6cFq_tf7u}Q)fETK z-1k@bY6njAKUEWc=fO(JlOODHcYEQ(9m#9W{`mCe+wFxLyKZJ?b9+7T$%|{2QIfO> zCoR>uqmbBD=%;QbB`Qe{H=p?ajB6wlpP#Y7qPxViU!ZY7+7Hl(;GWU-|9{ViXv`1j z7F={r-+;#NmS`fc!b5!D>xAvQ+fYHH24lS3Ru*keq@V^)9Io4S3_?%ch%>*yBg9@O z*l|1{*uXaUJDw1V**1J|=~4-P;~=0I%8^HexiDbn&iRJ`;74T!39TLdIWsqxeqi+J-1V^ao95CYNTM3v!K7TUOf4~f8DHg*_zdwIQXlq(N zlMwXv`NWFimR{1Qmhe7hZiFc30n1gqdi(_APK^^LwGnz3>7D$hmq)Z03L8wP0-{Dy z_ktn^@Uq|qs$clDRev-r`K>GWr#Cmps#xF5CnNq4w=OY6{L=-7K9gJQ*pXRwC0Cc0 z3A7W&k2dH9WN{3(O@WGz2Pb!A(?N{H-FEMszDgW@B85MG8YY5&Ft_^#l3rc(@*3o6 zNw2jqHGsSAP8~=zMvkCVo-wD-$3~R{?I7)`qHYL;fDK(LeSZc_l;;vp(h96P0i0b8 zwC&G)quYug#t-xbiMG?7e0O8f7wsVDjaR=`(NS(J(|9f0n##)YoJ=K{B#3rIn~(EHz=NYIE}wVbxYW9YIyi00$Mu9 zOMtudsg=YmcNx5z6c>!RN_w&%=b8&J&2ikvL}Y3x1QS;A`X&_h3%Sn6d~!_`t;pUd zx0hZ20~`c|vjkn2eg6YX5rne@UHW<+gy{tU01OuZ04tY48U!bl1POm_Z`(Ey{_bBv zsK6>?ntp(R4h4!8D^RpU+if2VLt&&-%!U?Kk#g#0`R}_UDT?w-nzSy483}BW_xjxN z?x?y`8&(u`Z~6|3f{9J5I>UI=D8r3Vjh@YFqA8V>P}-H8UzP*mD+3+Bl;DUeZg?rV z)}RN1u;eJrt`*JIu=#(sC4pRe9j$2dVhc^By1eD(HNQgIT(?k)T0j?Mz8_ZJgSn5h zH$n!cH>%rkDZW7UhHv0Lwk@hcvj)b$++%E`XwYr6JKv}Zq|T*it_Skh_&Z~?q6_%c zi!GPXm}ATxe4>9`(1uGHAa#%Lh`EE!31;hCzUF#8V*R^)n$3S+@TL+7JiJ!wx^LMt zX8Kmba)5i81d3#p9Z|^sLAV-N&1SO-YE~0;rf3kiwC&VK%!9=qo*9GBrsI0S(AF=| zXfVqu#cIxe`ITwYEoKZqqGp(41D9Q*PY0NY4f9&;vLh+rbK9XONtWavm1t6p9z&J7 z-T8bLao5SM7jS>Kyp7oJRtb9aJfvjmY<>2j>tQ~#OY8+EI5Hs4&Zv-mi)q@?o;#Zf+J`)jqbWHvowtSD;`ZrXcp4cUNMtI94=2w>BiU0O za}v*RT3A_s1#P6#jyL6K6@|{tKTX7c{IJ{87McGykR+yg4RVL7Zum}oNQZ!X`@Sh-42fW(RB;;y|CPX4BOsXqUX4bD8)yoP4v&C*BBvt~1 zN%bZY7x4vGwTGGY8mmyHaKTM5ErkF#Hu-*LOepOb{`z@jY{72) z!mk3lPIY5v2-6gOY^IxTzbPD#?=(D`{fr$tVW}N7Z=YuDsUNaX9?_o+^W8i-a2jVU zH%mpu-|2gfV_6HG*#+PHoaScB;Ma6=9BoaK?3Ikc*3Dp64sv zYk6lf;*ww$MOf8zp)txbkR)=$rL6TWkDVUbxIop@p81>2iR68M=}^~eYmma!>O=*} zFx?$sKgE2KOCD%z7s_aisCRU4I{?dj+<{hq5*0oK`Z(lY3BlN_TqqDpDg_5JTnOki zo({v=0sZdsJt>Mohz!HrScyV@*Eu8cki!zD17^kd5wW$9?!4suBeiXx~QW0 zmuKIt(mI60X;DE7IPNy3!27G~FgL=i3*RyI!n$bn53ggFM_%Tg#UScVKAuI?u}1_> zzDzue`XAb+{smA=0|XQR000O8gtG(=T{TU89@P^70B}(N05O+A8U!boGzJ6_e;Y@V z-}xatY4W;V%UfrIY2SZ}Ocgv;7G$G*kFFvcTJQ<`Wv!%Pn)b9MUfSC4*F&x4X} z8FyGLKiJ}QcXf4jy{m@nrrxMDUGLgmqtjFsn{C~+Dyyox&Dx@_%w%GpmG$LiQC+&P zW^X2IT)nExQm<%kl3lDEh_hBVf7wN;c~ZOI!df@>`ful_=?_1h{_vk?FaBepo>lvW zdQ$;}g<55%o$#7%u}#t&UFCI?Y_rvMhTA8N{%uz@dZVk>_`Rx|tSs_ZVdxNqU0alX z>5IDAz}mm-{6)6WxPMhw#fl!K6+Aiw=|(pOuy(Nio4-o*iwek4Wo4p2f2{PDh~#F^ zCJH`3W<{y<*Y$a$w^^fq$f~@^vsN!?^ry^Roi(*m8kBeZ-mnGYRdV`sSo{5doFIe=-3iydM#xTel^ zX_B(2uHCYq;qQgQCnm)Ue}gUK3+pbvz0$gLtN)5$h9(1FBzUg2*{#~T>5#c#ili_r z#l@-4ix$~)F+2?D=@ByEMmW!|2|P$&hk4L1Cf-YOn6YZpT~nr4y4>o3tr&55T#v)q z6voS2^e&nx{ z32F%4sHIaC$)#?8rf0L1z^3VZGC{WSD8OVA{I>*t`mAhpmhV0P`gv2nr|GG^w%!18 zm3;VaGC5MuZt5Zj1y!xf0yGpTpxdIoQtg#i=lkQ)~Ze*^$`J;*n^gp1YD$V4dPN%N{_hwhM#YQWqe(H6+CV_c0Ki)B2m6NISQKkHpiFk;Vu=Lzy z*E$^%+iN}>{&m8-hQ7CYvn`3(eL>|JmYOF{ja$kCweUYs26#99#f&+gf`qX3J0)Jl zsHuAfL{aYX)Dg;C5ku=~Q#gG91jmaXT!C3gYc?l=a!05fD}{q_$zV(r7>Ju9*SWeW zGR4XfYFPC?g zhWAWVT>@F$m_ogBzC7_V`y6`TRf5Y}-7Of0)+yQH(X1Cpzh0d3L z*XVJWy{b3b5YR9+JPY#*Q{$Kb=$=Se?#d-B_bkkl{Ine8vIyq7xJ-?1kts|BSPn$m z0I#ZU3NQ*;NkR%V^b4bbFyBs#ALDl0FO0e;T-VI9p~0>2zIvNA5J`~wywIr9GIm;R=tgsNbyGBT#gPm;OO@8SQEd%N z)LOUTdgS!HaGq|W(3eJpF{^=~hVfN|r`cl64Em9Lr%es`f!mEcmm0!~y1D*0_*Sq6 ze_(3r58E2uZE%m8dfOD_|5Y4ip}*JG&cUinV0TNGlWo>y8*7yiL$ZTgv@f|^GD6ti zfesw%wg68KWPy<8Jpecninhpw9!ex)rrtFxop$geD1i;8)~2`GOeru*_`WTDSmmwP zxL8C+)~j_B6byE0eX8l=pTN-S8wm!h;M zrb))m@{o8hAX4301ZFz9Oc=%Oep`UEayd^N=T8nOZ)Q%DdVT}p`!NGeXcwVbfWe|o z)1bkOF4qgG&!QNHW=QhKLnzAQ;SC*+BlQy(vWF3JK03*;ZUE%wJ`^aC0z%sqe*#x` zwj}L(bzwxD4DD0+)|uzB4@-BP&bYG`sMz8WJS2=zf+HSrA=uI#E*{4lMx1EwmNRv; zc?SmS?GuHp7tuprH*g2sHclt{z}H^f3uckPu~Fr}gDn)WP<1{QSjusBnkFs}b3eO7 z4L>*fS=%Y^+!U6&F1B0WJ<-F9yK`QS;qX-h#tyQM|Ai?0yqO(0X8xK5IlG>7}gW4i2B2GnB&zjt@E{6a+)kWek)Ry8zAVy8$T>DV(vB1{D(y z+f_EvDczvQ$L43@zf@z&|L`$q5e|pcikfz&&dA)|9lMZF9s84lLGw@1tyjm#_ub`p zK_-$l7~FK5fg|T>Mb7~ae-1H>@Elc;12h+cZFO!cy^?VJuIUxD$bgICO4>8HWmAHc=T(!ye>NaCvnmlL- zLmIRSzRL03MU(hxf5Eei4pL%%gAR)Or*h+2XUt`)Q_iEz=5t~4xpq2G<;8UI16)l@ zX5uo5`g%T~T^^%QdS@JR4P!OcJBKgo4xoRe&Y2Qo7G4^JCAum1l!izDmNM+CY=^f| zv>aV^09le6)Hp4Wm9$Ts(I+zkTw)By-Ehn1s=kc^A_i4Mf2aob9uxPNys@DbW>S)9 zCYLIfLU#Ih)4$cxnl{nr!z!jw?7D@WWVU7?2sRh7|Hr;LdJfqdGA|U$u$a z5Tm(_knS&75fg3@8R_j3?i#5C8Y~B8VOEz>5CkTF*78=WA@nu0FVQ<)Yqf?Kh)(?kcQ7Sd6jL#lUtXlmf{p%;m z7d>3}W-0u?ObkVdpD5!)UPlV3%6t#Dm86u{OU3w1fLRdQkq;>$jixKOfhEcH#qM%8 zePht#(Zby35t+hYo9RLVOyY>&T?KWF#FL_bTpf%b=wc|Ng5<&xcj6$CwrF|0)8DO&tDr80+9$;;u3@3XaV=5{b|Hc$FS4nMffvWzTS2bHBnE53L*?y?`q zTWhjhcTFCC$N}!BB|1tt3_eA&%C2jC*x>A)8|bW!DiteV}70_6p|z&pA4BkGtILxz-(Sgl_|f zj`phdx)M5dSK`Rd1q_tu4m0XVkIs`Y?O3_gW|wlAW{awvhdPV!AHgdf01uT>gQFA3 zgr6f<$ydK(@273`2Fikn*-VmuSzr_BEmeuYWMN;YhR030Pa-hdG6>ue9hA%W;J)b( z8KpJQt{F;<8$pi?O^GkqKtMCPRZ*)8*!Ba&i4b}D{N;)I1zUnJlahh$r8ebUVOMvJ z4AW@W{EDv2ZhP5exyEuX5cX1>%3K%Q)Ph*5sqIqG`$m+7s z-*9F`EdKm*vIE;IB=?K5^##^EhU>Bvlk{q{qoO}trj08;gZjwDA6&HcwSHC@V;3o`BP6<2?*+2*e6`YrALE>UF2Z9{Z_URTF(9qYC06R}`_ z=Y*R3H@%Hx+S)3{LV-)=&SK9AP6Re`jvh<>6}%37O^~J&II#T?N)s(Ah>F`{U91XN z*CDfo-(*{kpQmW@r-9DCO;z_+a_gdD9%-rP(rDMAxUS2xzNL4~vOUNeLHRBLd8JFs z)UGOi+iArH!M4_apmvFf`kMPGci2BFn?xNWVF?Z_qI+j11A(EWKQ`~=9v4rly#kMA zd&Q2*_X)o!bgPdWja*HP6Ls|*o0tdOw%HsBj*?oe_x^^sM(nttQ((sWb6I*9>4G4@ zb*fCFS66|M;tGN||Ds`317&TJYtI#KOy2;1xHpAKO0t{fT z$Z-ZKsy7=%!*kMi?|h2!yX0HK+mC(!j$pa*?~`v6^%DzDdsR!s95b*#TrUNvB`lk; zVneQIiyIApPBb~qG2V~|I1MS|4{t)$fSVm)v@i;5-!qX05%b$blu%iA2p`#8A zEf^n(1AL|42VgGCaQH4k-H zt-kW!Y!GTR#?GN-IJVh|eE8xLG=wHAExVvBIM93|I&h)8HON8~68gau!#__X)Wf01 z(?E*q0ATm}+Dd;cSaJv`P{N!FOK-eY$28ckl-sPS;I8{M+3D9teH~maAE|_h?D%VQ zJl3I2h!1yZ%d}O0lt7R6=EYf~4s{}b!8>qrkwSV*@~D3R&L#~XEFF5i*L@^XgJ*oI zgwPQ(I|q_ha{0=YmWHsbA5FR_^%&fLe0w(jIgX*<@~|OACW<3N z=_=RI$o-6uuEDWOLezgu5suaIb|3wk5rj|>SnId=7PU%}J*J~{x+-X!wT+mqkF>x2RQwxVg`3_LLLisOhC3RHxI7^ab4073qkE5)Xp6j)0*`v`KpGt`UulLwox}1x2D0V!|tYGn%I- z78<>kyerD~8(7ArpCrP`KeMP=L3HLSfVU#WlzjB7l(yyeE~4>MzeXuxi}2%#{O<7| zr26#2&d0<2IOq6tUtQ~5+`zqk8TZY-Irn9ngh|o;FHlPZ1QY-O00;ntvjklUo|mW= zBLDzGjh8+e1V4XV_(pOxT-!)+IURJl&1M}h;y4R?6UUegXpYnx(L<3kNzGW9%zwXn z^!p)c?Ige*j)1)*vRPeST~%GLZm#=oo0a8yKkj=`mRY^sb^VxCZQG62Sa?`s8UbIWWu*L)4RUjtMGPvG-^Y(T5>a*9ce*fl+-_7XO z7u8m*-tBh)lYYPLyRid2_}9C`PW-v*`Ny&v#@Vy!GlsvbcSU(G+Ev#VyJ~q?;V_CG zaHxB+743gGsJ`E{_f=D`zBG@HK^S&oS+8r+>-WD>cfJxw-`tONqhI=>>$lMB-^2<> zI0NL{uC14Jt8C%cF-Z5&J4}K%fIsuyStkDoA@pMCntK6ruU`EU@9c3#-@uJwsA2LV z@72pqv90>M{Z73ohGn%AHmz&mtG=z8Vp%OW!tj3v{CM4t{b81Uy%D0J8?o2K4@%Q-Lb(_c`pNiF6qMPKOKZvnfRb%z0 z1wj}9G)%YugkI;He`<&Q4v_&chjG)jipc&`8XIz7WMp-_5yNUAR-IAyctRcLI)llN-%B8t28KqdGDl5Zb zkc!h_23Sl{f#@4=+I1%(#Fn#(#C_M)%X$o$*hMyYtQWKNldo%db3Zb5f@Yu-fk}sT zjRbX`!pdB)-8Fvxj0LNyh5=`L4LE;~ud4kZCTgLd&Z!Ar)@bMHap>l#Y_O#!+_PuT z(3}C~yHIvb0Zjcl-J6uO?3N{<`3f3H(_)gp%BRn0(Sn;RK<^4U0C@8nz$wJZEE20P zyUr%~FfrDP9ELq%^Q-|qer3II&D79b4^5~?_-P9RHv=N|B##0Xv6bDDnkawdKCMup z9=Lm0CcsRr+j=a^i6+z_n)OWo;`M3nu1w~!)bIT~rrJ%jZFMI~OEu4rs66u)tu`eY z5Od31$o>xYQY%b57<)l z6wT8i7^5AM%VQid(((y-x-DLo001}qO|*j;uUt3h`g-Ossv1PJcWu;=xuaR!v3Dj@ zGmWsmfX5q(xPvI#C5x2RJefLeeY&ndV-JX_(+V$qQfM!Z){;5h3|ayU(*w}ch)V%% z-+Nss<9?Vl9YX8c=CFVGv}y+7qG#v@wz`;~hx!xK>%VtBn(gC3uMD1SO%MX(4^|RrA|UdH4=2; zOqJU}D`G@9D@JB$CqUMh+?#TtvQvimNB;DnmIKVrTdNn~jrVx-Zf4PJVg8J%oRgSASIa<& zO3^eAJQgwFXu(F6km3@2HV>l>XEVwuw#d#R#v2wlMED?>H}Vb_r`EWuhz34${$vNq zm2rZgXL?A$lWbHkNJAdgy5}GXT_Xm*(vuX9Xd1VtZSU}%3*m=tbX(3G8?t()o=Je_g5DID{YL#u7vp>U@3dG@u?7=eNXY{&>Oi^+|E z#Z6)cSgkt21_VJZJYc`=+6#Ep0pdNXPz>Bvw^iL1F-AfvIe4o%E7S;GWYx&Xx^7Q7 zErH1!sC1i|(txh8PgIYA`!k6`!>e&vF+fBKXR?2Vt=iMAka-s*-^nJDbr<(`#=yU( z!CkpfO`?ekKS?poKygIqjTeWy3UuGo)XHITUsnAJx#jAn*^7)qv_M30{lRec;(DH4 z9YoV~5BL*!Sd7^nt-k+(|bBStEo8J4wcv9G3WGCKF{9ud+~HGHSvmxqUJ0S zeIJR0R6wX*w;TrTq5^Rv8H8%P%OmH zi==E1%>@C5B3^q9(oKI+xKrgT z2C$5>vCD4jdoqwJ3s9~!LSAH_iC$z6BKvMXjM;M2bzo1SCAF@5xM3wf5LhpEC?QtH z=6Qxe-j1xVZDHO1tlNq8zG)5w?QP$ECzfMIDQdKrfV_Q=cgTR+)&m)4s9LFWs)~Ox z;1s?X7Di%SePncXH4LI3jWvWB!7y}&?~aXVa|u!w;5KL!48V!VP_QwN)94D^muT9bPY z%YZX5`=v_Oi~fe$W$OJ0ONlE+uS1$Z4bLcMv2fHff^b5S%U??%5IoXsIW+d_jgbxBTJ znetZ+`R8Qp0lv)Ii!l~hmOkma>D(_E6?-aiIIQjOE*MP z(w%*eIo#%OX`ST1Mce_HdeZaGk!ODpmMXhzyNA}Y zmZ*-Yu(+rm0|Ba66Afibiq$u^&K9P4Y@Em=kVh#A2Wr3~DLMng#F$>-)?9_PIM=?l zHS8NBmolY<|CBcZ)NUj{qplqrHsulDNb~wa{}nc3HNV(+mTWjrun^Kx&Nvs<4vU6d zmv(sO%`B}+|MAH@`=)JY`h%N14|ZxJ8W^1`_A^!t@;YjVqMEsM`T=0)r^E zJh`u{>|(LFz?dlfySSJFq-rEn??!lVhOQ9+LslK}>R)9C2RWCour5J9U*L}klA%yJ zM)`zRbL!0 zjE?H<9DqL~fJfk=Qmxygz|@)y`g(Y%$IT=!KFkxk^(4q+_{(QxvAFZW6M;5; zI4RSBhwa)X2^5rg1)e6IiM1;Qr_jcB~hiNyr36AOyEPQ-~+jEL|_ z28zN+$DtODB)2vWkYk1+CX;O1+fPAvA_74r&B+MqO_}}$a!8d z!E>xKz%KCx&qs$G;!%}jc|LdDjD;+~ga==KwO9ClLK30y zu~4)@Q%L$#@K)VY>;jxG?p_bv?(HIO`dNM0-VZ7v>34=BcOW%gcemdWPt3BC7XI(4 zzQu4aH}QYGatv6TS9OXKRp}R$!Q?T77J5`oVdm=s=!RaxZU@Aa)G_WDSp6g0Hl!7Y zO}7VWt(u4GFnFKEFd~)Gs_0)#Rb|IH~U)(#y{X47gwG3M2o<$*q&Hpcj+R1 zdh@m{@u9d~mbaZ68usYAtE_-dC2!?aIbOFBa$kS8t;*qK%v6(hj`WX3DCc(FeGoM} zQP1&{<=q;CCD`vMGujJkm#=|dc`SO_-Joy{Y%QY10u3?P1NPyvfjKeE(_sl1G|HK` za*Vbh>zV95_^%n-GGK((w{=@Jv?O9M0mYcs=%_TlSImg!*vtu+7H6o7rc_kxw#jPl99v$D`G;xbgBAFpFphEh4 za};?|nO1H^23`T5c!Y?Su;3M44&eP*sS|%*@1Z1R6^G&j8%mgfHL%_Hje;eA{frb; zN}amahuj0piJquQbsftmVT7a>(f3^y8_hJu_4w&@)=+9+y1Y5e_4eK?8{wG=>t4?Y zLSXf!5>?ODh8fn*(z3|VZ0@(5cG8xBO~%4fv4L^RNjImK-PN=*Sx8y7NTLwhUtNDY z%0pUIHU?K_y1k$eFVhtLe;=dDw;4h%jSRJ@RvZ-zz;X8B7&xK=#(BU~r+~hb6MVCF z0p1mv(^G*ZK!p@noTye#{jAuUqqC6Sd!^D=lQy{yls( z|8ivGHoh!!3F72f#lShsYNCGN97lgUAxg&)zPe5XkIMHIGIgCL9lG&RDS5eCooBvc zfgkSN$W|R^rn#;V%u+B-Er2pGj-AfHDy^3rlN)!PP})8qO9r*m1!8Lf(;Yx#4O3k7 z!k|W_fehs`>VD2Fc#IDqzk!irb_dOj9yczu92KJ|iQ~z}{8dSRZ;g*8mVSSNB%S3D z1+#6u&0_(}U?uf0W3kDqf*&b0GRfrc`G{rUKw*1x%b^LC{H){#6FYFks&(9ip+jIE z+V}$8v)lkZmsmG3Lq4*&{GkT880kchiBxgu^+yX%z5*nuID~g`A$m^IcbNIPAw4ro zvI^Xl1-su?E0F=)F7FVXLq>mZ_sHHPb{tBWOf#;Au$WQygE2HLWWX#FmnS;xS`tU;^>Zm0-CMlu?RHQ)|UtY&*z{U|9uN zCu*xSY|0=Vt6264uM;s&e|}E(p~^{o7sU_cdgVCIFFk+t5sC&z7BPz~ z-Lx@vBST;kS%`W@H9!bjQzFW1Ms*^RHBZ>1R;7<$({I1RdeQPJJ z0j^cKS>lm|oeI2&#vUN;!ijl<(tzjOqAV4&ZD8j?aU>JX(UOyAfJkfk$eZcLsV&T> zBXgPteih0~${2spi3DaxPgrI~`6aJ9mENgG4`6i<$j}BLX!5NWB>I*&qXz4|ig4i5 zsy%ohF`(S7gCr6^v~ovi(n}mMB!)Sr5epx%SOPSg0Qp+kgK$B~+{TXzP-^m?8Ktz2 zXyPgMxe;eNH*Dj=FklvW)0!iIu!I7^i6V&N6ib1)qOOVnj2&Z_QIYT-lRwGI zLCr{sGC=Aux*|!^}eNNo^xgwNNT{>Kf2ptQe`{tnZl65paj4DcE zS<)LPm5Y_y(uzs;o$`tZ)o{=Hk3==;h9AooaF~BKUeX+o8(GG*JpHolyUqzF`pJQe z8k~qX8*4dvdX#lCf@%U9Jbl>I)%7fUe8T^{n)I12OBd~ai?+F#7#yc@!@#Z%sDKcbXCe434LGHv9yREMr*f`yFBv7N?bt3;jPmHUR z?yiJf-k}S}ktp0c zNTC3=#a_OGyB6@IJc4?{07K!m*raobANqdSLYkXRoe;_NMkRYZNaeBpdZ*LR^nrMYEp2XXK*4{q>Z(EB8}K!j_*s= zIau}WUe(wcXF$1C5l2FZc<|FSR!sKK3M87HLx8;%H`HMNIq^fYgYO>@Hgtc1h0#J2 zBaFlZBcb>Aju8ftz|ev?H!_%r1V*fbpAs451oEkqEY0=-`{bNJ@XxkG$lM4dAR(6O zgi5Dq;k0aqJKIT)LXmW~jUPKZGn!9w_EvdE&rp#jWti;^d>r~>9@dABUb5B?(pq@P zf!Vup8UuMxfelUswr4Va5~_bS3s_TN9idtxpB{pv3TN{8B0yDARBm-ur`-p11`hnZ z>+Z7s&O{>R26fKSQg>F#>zj4!f0meNVb&)i15BQ_^^(ItJ<=VE6HF?u z6=Ee#R=vsV*ZO#;Uu#Imwl-5XUL%<>%U~k;j8$)|g2_5#G*=JZIqhO~#*taHhoRTI zp0$B}+jKWz0N`211#f~?#dLN@7!R;OVRW~3F$0mnGMgHsprC&yBv{a*=!OyanHFn% z$$f4bxLu-g00OiWD{^=`6i$X3>fi93<|yye>9!{+qVXv}LvNv9j*UmLEd2Q^>XwlB zN8vEjMw{8LZWqVcz8K3Na*&w-9>>?@$D!6$vqr`nZ&rDry(*2cCm1_y#RKEvObZ~F z51R_~XpZ}zv)_LJdi)!ae<0od(DinG2w)?>pkSvM2enU8V8|=$_M=10)E5{goXAzQ z#_^SMjiLzIrI-X}Ybvg)0}*^2r6}MqyH+K{=|xtTWP1brgTF41-<`tTUo@=*Yn^?Q@Bo z4;xsK-_*xt4Bcf^ks;2q+_RkITr+aYyP3Q1BR@DnfB3 zJsvzFS#)}O^4@3SevX6tYwJ)ko9OKH%^`b~m_1E%U$oAor7d^tcRRt9r4*_l`GVz3 zPTf-Q9S~`^7(tAEV#@K57(~ht{2uL+ouuW7k_LaP^*VoqUqCmqoF6S7N1~Y!3!ptj zkCQ;p(yHXc7;xMe8t!PHWqEUmd>X-l>nk|*COCBi9!Eb7l*&*JV&vTyavsCI7rc?Z ze2+JDK^~3X%TNvG^oFH>Yy)QV`TJH>8t|#(Z^+p^vb8>fDQ~w>D)OA%#j&>o^5Mo4 zZo#b?b=Y&g^F6g7_kh>T`>?I?M#g?~crR~4Njo2v!nFDL^14wx$g8YftQ z*bB}9?lHCKm#}QcaW~9gytswMX@7%Xv3jvP?5Y>LdiSCphP@bGeEjjh|KgX==?6SU zslEL87ax7{tB-#9t6zNL`bu1!h4FYyl%;vS-*t8C zQ;y0HaxC%9Q>7&6IVQy{T4DCygU~Zhk;2%LB856!3e^mz2L7LF;OQ|Op@C(b@D7Mh$UEu@`S|Dd)L~GI>xnP?7M#^ zz4^ldc??8hTB=cn(--D()2i8JA~^^PoqAv$meewuPE2b}t%Sc~Bnx zy(tNTSadx})C5VC^%Z|=)W6pI#|RP<#BVt*U6g480IBx}%1~8pc<>Xe!AI3aNtVEslXK%iEH_yK5_RQ!bmK#u7 z!P(uUG*pA>c{=7HB-I6%QikYc6W-glq|YRCwr*Nb*$RG@F1rY^+7E;R%+SSUf59IC zun-!J(&S7wGiYS`0%TGvs`P)EVjjw5oqy4B!8u~(oVv*8iJ}T2A0xMYs!cVNReP{q z2X>w86^c?Dk>ktkV7XrF&7MQ7E3mTurd(R(^JE{CK8c6m<%scgTi0pDWjVPmUtRVX zq8zWuzIb%UdvzT-COg;)ws?%RG6~FuRrEy3u2|Jjumg&YqKtIz?goF-WeKxv$>(~f zB+(1>+X2UD$jYd+d9;)^x$)mPsVBc#X{Yu5C|nTm5`_+V*^SSQchQDCYd;>9ClAuj zxWM|7=AKsl{6SDkLdIgUzZBnC>g_qJW~>0fe`&WX_FP!J>blE}HF{*u%+PJ;juY5X zqYeF3nz?od%>M;@~xQGj}}0qc>@O1;4h z0H(TO72mTB|fp`Ez2GssN;fsS;S zl`YK7ymgy8`w+zB!%XFo_>4_V=G&PN-5X^-=xlc;n|i}Gz@@vPcsdq4ra}lAQd@xh z{V+~&{Wk9e4w2sPp=%hA)Vapy_gDbYLdTt&-_xA5-GiDJDo*RxuJdD2jb|rDx#;Y? zgmK_UGZ7?5Y{`GZ*@$Kb%Y$x+Y8;QjD9Iqh;~Z68Io3>(Muk7ZFN_G;I5q_qov01g zmBxg`7=qkve)%!<`133$y^_OUd>?MR=jpD*0A}!wD8VM7&npK}*aYHSoFjvw*6QU{ z!UPPWs<_cg)fL@VaawBd>)eUEE*9#iipl3kl(N^!EK+{~7bzoG>91zdK`NpC4dM^1t&$-+|&Ok*<)--V?Qjk<{De{FeP zXRsN?VB~if=DF>9! z2Y!+?(&bZ#qs$%R&&ZgpFPcoVN3ZXvJPb8!OIohPn`-5rSYmt>Wt=BCuuL-_-}6{V zw`e=dCB^eZmUjM%I&~^*A`bX+`ebV(2jV<3&7o-u_k(-H&VOyg_yat6xmwleQ*qHB zZNq=mp$o1{9E^+)YykmXqp+ee2;ckn_r%Ympkm_ZJ}PWQ>=0_|ht-d2Sf5}~Vxiz+ z3E>YMYlbnP&DN~&eNq5tQpr^C zHtyxX=x?^Gcl3iJWcVk0w*fH3#;V9wQ)_5?|<*q)lP{N#e0Zm-F zAH-j=WVR57RbZO1sJj=UeStfThVjMN?ds+5!VKfG*L}S3AU$7+3M;@aF-mZ0y`q`g z9{B$NP)h>@6aWAK2mpk$1YOu;??d1Q004ED5IzJ|6tMvT)^5PMy<~wxOQ)EKEUF~s z#Fu$K1VAu)%QC9G?WO!by{J$Y|GqeoIld67whoGQMFzWc8H-CNRC&=5nJxqEWvv?Hb((tScSNkN0I6Kat#~AJ(o~F1SkRhmw7(~Fn`-y zj*;Jz>Y||33&e7pw)@_~Nx3Z$*GtFOrOjIWLLX~Fbizh=tT-%<{Oj`A*ifk|P7YLN z+QgMi?$uPu|LlzW6|6@NvSWkwI(uFEpl*x~A`psS&U>npC%e zbAt?I$qzeVTD7uxt}XE55yXfJm*7*8;U)_tZFWTBSqHBui+hKx*XIMP*neK2W6z+6 zZY;1iWuOE}QV4A_WEr3MawdLmT6~E>k`wM9#nG zkIFmP6N&ETF8pzg8T<)=o1#oeD`>1EAS=F?riAy)(K{cB z%UeWKexkbYAU#pl~K!2q>s8!7gEjev|FBfxV&+WCKVr;YvW1;xtH$Cwl4#LMy0YV_YT@lKk%&Ue`q3${oeH2lS- zaBwt}@Q^3(mD_o|S{yo2*1J0ShWqcTA`|d7xQ9`Rsctihp>dYz&}SX4$e? z8epNYlS@ZzM>UPpelrWq41oMODk`#m_mhp8QA)xQN-9GY$d$5yye^A3eHO396UZm} zOzdOGa(zd6MahM4ezD=8m!0)qQy*b2jqiW@;9*?d8ktdybEx-rd>dUo_iAK;!fZp_ zA(<;sitMfJRRdKt?>f9qqOn(n(_<1&9df$fL9NjMj z(bjq0zGfhy4IJ~T(2Ob&KFk`Ohxp5!bD6vJS&;#3eevMvu|P@psO4X72lGr^=8pyq z*R~}GU)L?MxWIPt@aP$Jh*uY_5#!TOvD29(ohe>@NyPD#=mk7sKr2t8C4}*02)U6y zbC=UdMp0TwYk$RrKbGx8HK3kV$33sZv}qd|CXLGIg92#`$4fn;m|@ytmf}VMWYo0( zkPVa(8bi+}H#ztu*-H%HFlcqdv+Ph|TbbLm@3P*vYOQVix^N0CA)MNC7Gm%YqpM6) zvbMPd@RnCsu|1xJ4DB)Cw_ImKUZ$@0^FDX7Je~x0-G8(5G@R1IwBq&HS~;H2hLH(5 zKA|;OB{F62`keOICOK+*j`(76lWYQFs(KvXn!d&;;PgSXAfb4~u0+9@a zw^|oNEofIN5HU_qRAvJ|;i{XHR-2Laanf0hRP3H7GJSL|PNaX3nOO$e+1gC+*jTTA zyfoRt&T0mio6ukje+_R5UI2@GT$I0`fU6Dv0^!R!zBfbuY>QWziY$({`OyRN^_#Z> zs88=&h=l<;z)ozv&_Gk05uul~J6#vy2b?X1dLMSQNooWsPbka0saQeH@Mk7~(}F%$ zJz*QEFt@$emybjQjsXvs9z_Hn4urD=U7}jSYgrEf02VNp;a~(Rmvu!1HGib+sMqV? zzUKu%0w5_n?cMICyF@Ywym)`V&kJy^${kD7^}g9Fk)}-Us!}zK7e(3dMwUfAnb@%% zZ?=;)&0m#yE>`+!yC4&5zR#OXuA0G|gfCa^yo*LCzRX2zel?|vT%BWVFF@DrYumO@ zZQHhO_tfscwr$&XdurRZZJxIGyw7uUllOj`WRm$Z$=q-L&Q4d{h~hF zFC(hz*WpU)$?i>)VGW&6KsDD~ws*5OdDr8Pn{MZOUUOhAXpYsMRU~9fi$^ zR;XuHsBQe^yLi*_#=~OfWV{s85SxmbypFM_3JKy7iu%$bV3wZYZW z8vmIBRRRYi-bg1$ov@X3`$a>VwLJD&N?r z5sWo>`^US(IoRiVN;TNviW&}ecf?c z_uR#j4DF`sN0Je+aiAQ5I;||41{ZMBNv|^BP<0{+z`)R)IGlB}z=$C}oVl&%-L~BM zs7``~sh<2td9XD?jYjK7Lvl;BYe;!QISZUt-yAuvfR$Jgj7);$vhem~N_mXl#Kt<$ z!LR65dCkTEvsPu06J+aDS&Dx$G@bCm^UHJFhk3%c@PKi!*g<7xAThzno_!L=Y~~a@ z3z#D-ejREDBy^@v51YvBEMIA^akvdHp9%eLr<-p>ngW2Ln$ya2nke{o1y-+NGUK9;Gnb`DKIR6NxH=lL-hD#=MwHb zdp0rDYbZxOx85;`Jsj}M^3qU6snt?>P@hWdatqVKh6TE93;VLddC*j|#`arGmTy%V zuZhE~zOT1*fQE6U?m658k!BCw>%ZX8?~B+cI~5wy2af6(D(0r4(%YF{fj?Y7ug}Zt zVVl$}Eel=(8b>F9F*UOI7pMs~@#7KA;s9kWW!H`(&22@R!oLU-6$M+tJzx>qButw? z=e*^#eO`J}q6zEb&_)s4#NpPFwxd426EMaMX##`7X{TvdlL9|#k0HOGbI*{~k3TmS zm|K~I*T?D<=njY6W!5A&fr8Moy+T1X6vkfaE7sa>-(UpcE+9mjH>j&eBR1~Vao%4?8NDifSfvi?>l z(9ekci|j@Kz?I*Y{jODyR$e~x0qpT|n1oD>|9KN?K2k~f)4IrmLOHPuIV%W(TW)_y zsXj@Qr$PcGX_5XhUfPBetb#=~Ce6(zC`H?xCuHRlW)ClE+#k#=zQ}g;o^mo+hRyCq zLWUA{z1ZsLEZhg*9k9OPyTJ~5Bee&!G@D_4n3)XF;c#E_lXTw0#Z?I-u(OF!DOnnOb z_yvMK|JTrv>JevJKZwxV#ALOTXE-pDgTTnrEgIp#Tf=&@Z!`jgZe-TdeCwW&&hRpO zQN;xyI@J6nt~ZUH{IKa40l(gZhKMHJh<5#IPYdc~iQ74PnL8V)d00%ie2=Ta<>R`uqD@GwIPDIK+%q!F=S$&kT#*q9 zxGK1;WZndYax8uB1wq8ylKG<&Gh0L6fyZwv7xGtJ*dkB6TuM5wmx-ebMzGvMT67y zer;OJanQpeoVb)-28N=&>b?rwnA%-bnbLa=u@1Icv~_TuV_y~CI-RMwPRd!(sfl@w zj+}MMBNTE8y{!#Kr|NHSCTe%Ge-$AzvQto69*2CW$^Twwjp(p!j+0&{Jvs#(hVB;B zY1cusK)^IwLL+Giubx>DL9pQzRjdp*5L(z7J@5rLLO^qc|MiyuojE&U8Dj|jTS z!K`Cbxm=xyttC_7d#1hynLC2-b)i;Dd9Pfz`VIh+0y6-7ml2K2afZg9 zy__P11cNw`-iiU~KNon3bCAi;6VMjQ>YvUB#s;U1Del*g5%uoVtLgD=4~ZF73w!XEV)3iCvFvp_ zg8=sL4f1j&-Bh0t&w6yj!R=N0hjl~vxs81 z?6LG$j#Y9anLH=fQSd3{Eemvv2Sjii8dwh%-JkD6c}0ok)eE10%gKpzSM}B^f0yi) z$sBU6lUh-6M#JxRQvCiLXZ*luh^JIcE4w-cNd+Ku38t8OpD}}b#}lTH>&1G2!GuEr z18cDpm>_wV6~4~Uyy3v@P42KoXmUJS zD;GihD2Rz5JBsPvI!`;#sM0$?-4{CqSD4kqNSVEFOmay?7+E*k}Xk*9r`nu>a%ed(B%ZK60M&8{A%slh7WfCGm zYG~VxlY5~ngF^aCKD{@5Dx3SZ`Z1gmr2I&GReCEyz1#u}XZ9*c< zs*gwVs}6E?^DM0G2x<@akQt|VZXU*SR8Ek=`Y}zqdul#r{Yb#Jtdp7=e?qW5+$Gm# z5aO?iBy8=p&`ls=+}wiy8s(PUGfMp%ZqS84562;@l%dql0s`8c zrAJO{_-e;?p<+kz39WS@Q2q~ybh;l~*$-eil|{PdY3{*rrx6J9`UW&$ zl=GazlNMbSp$|6aQ-YM&opQNCYWs2C+0JQwY&QCdUmL{YX|vl86lH6jZ=oD+&$pbF zdB6}L$D3}9=gDbr%ZG;;jn02_ZfaVyV$FGDHQFSyn)AjG@i%s7NbSeuD>Th?AT~(< zTxejg+73Gi++v z*xUR4wH|oow?Y~sDD!7qz{$dUf0w_lgAgIWV>^hOw)^eTp8DEGbZ#ehY<}Ewiw*O{ zt_pn!&a%b3lLJ(H=crQz1uy-iAN1mH?qs%0cCfMd6L+Q&;!|Vd4AFB%srPFLmTq?T znjD60&ZFEG!7{{ z>~<~jCUl<-8=n_Rkx;w<0>C>tw^u9m67>;f3d$&FeZ$oKZ%^L01ufl35z8oZz)yp_ zw=;ZuZuwR&1#fSU$aI4qpcs(>=V(fuDpE!+G5$t_2w{1mPGMgS>m?s%A%UU`wS#$5BR=! zkaJDm&R%}^-yr?dq+gGr`PfVjHabmD72>(pfn8JSp6=QEHRkEvVXK+G6h*NMw7dn6 z47=BSm-BL62ZyWdqaiQGlc2<|IZ3fS%yTtf;tWFR1H_Wv22xd4YF3E#fok*yGrG6A87V2mJ82>~8G_jID(n3v!ZDFUFy4YOkcVfucEUm>y-m|zS}d~y1TV>V~t9WC(4>XN|oPe0}fY`q{YY% zy{~_Abu#|_x@~07KFumr(gIXHIN)j#nlPauxv7jZkDI!w`h74+luTfh?&&x3#r&SixFh`TS%5)O8R^MVgqfyksvnC#m#v8^JmQ&YN%?(+64HtJD>X&%qf1 zSU{{kc`;hO(pyI5FHdE~7_v)45PcF>_e_{f<#Q-bH)R-yqW8NoxKaR`_k$w9HGky# z=N6l_M%>v5{~dYDAqyCaaTvT48Cu~s$7KKTaCR0^gjBNUJ0a(MdgX1E#ZF&v39-_m zj)CTEh^8|!TURi`{iuFG3vTEyP=rtwphe@Y)v6vA);K<{UE5WYKBg&4P~XLF&ssOQ zIom%d;uEQT@qZ|!*CM=M$oK9Ks5Mas2RlDH&63XLXZMxmra5@&>T%W!E zepnl5Jq%bsCk@)1L5ve+Z!Wq%a~JSrQ(ee*YjfUV(*>|A@ngu1gvwBaw&$#!{02sl zh3dY~=e-LixWH?Ti%H2Ng$gwn{zu&;shWDg5GyHjccd0x#`xJYD7*#J zsA|@XU8&+;$M%IKjWi!0ayxxgvvJ>MQ#X{7CJKN`GOLugD4U1WwNvK10bN|Pfd76) z*qc9ViJN9Kyc3)E?LR)$^i{?YvevNz1-GW_u4dnw*SG9%>msbfKfMx`%ovMSS)Pak z3S`!caM(d0?YS}~r1(US>CB@ye@6Pi-=Zx*novdm2M5LgV{Cx#F`KLE^3)@cH71eELNXnn6C!;KCu>~Ujz;RYy5O->3k7>?3GcNNbz{sJ3XS5Bg^thQ`*)Zy#;bDRIGh?n$rsF5 z$h8Se+~AgzrFA@EU9is>89VCXaB!}ry-!xiKX0i0n>Is$q*#{dcun|u#JyyE`bw=m?ZlZnW=SB-o<m>r0j>#>?-QGtpk47tb3`{wfEXWfZ?6N)0M>&PMs!{HIL9)Q+wtPi+)MPd&L zkoE}tkfk~?DuAc|S;Np%3xa2sJF%>&xZliV0WU1cptZ17 z{XraM?<8fRiMoiucYc9}wlMfhDDN=upSH6rK&U3Z>B#Izmx}Zqu58iRTmzaYSKBiy z7a~Hq2_aj))EnCq^~1>24h|r#@2ASoY!f|VWNmGipDnrVIA%c>$l{6%uJjC_d%`vb zb)JJ8iM=t-bbugEGLkh~1Dq5M`rzfy3ciIFpoHqlgj2H50fzZU!Pi|w*R0zf#aTyQ z4fl5j=o`J1-`SEvfw3g9i#;7oFB$7DZe|5~J)WgaCDwsLjKFQ~odIg5+!~L8P6a%t zL>A6U_hA*jxl_QS=^T^bj*F*BP{6GPLTJvuwbFEtQ1#VbCoz^raaYssr2O%EJ}W-a zXAC`dfZqoE`lJ*(_W`Ewj?cTxxWDKQtjeIEvVRXl{u+%_!TAMaNa;xqAQ@>LG_~0A z!%y3k3eU)shjTawJObkA+IWc@BqVc)_e`S%)Rwln^GV8kGw<6gesN84O+rxj89}qv zg+l!GF_+>;rMrXfYqsIH2Sm^5?`Gw_b$WE&~_K@~7y2 zFPB~_ED#^D9q?Tx=2=IYX? zRah=ZKmlgkRL!aG1CPt`CHPYB#G{AVwdhqO{5hmzydj4hx?KSphy1i+xGg2L=Cza8 z;A7k9foU$++;q!(k}e9@jt=sM970c7`%zSgV{O|llZV`)8Of&Y#K;17e4K|y`lfcx zAl+d9M5o{D=?9A7&q^lp<`Qd?we7zo+KFR7XaN`<%Flto5d0@h!Tr`?c$A(6rWM>k zx7u!6FQT^%FJ=@kfQPSnv9e)^~>)X>F7LMV}?sIQRKSS9f*!R`X>O=FRYBOZ6e zi#^CHdD-|>LPyCVcJ~eV{ONwBz0h#Dn_1fz=~#mJrYYunA zS^&Y!0}4^XRan6+lkxi^Z-?^f14z|5mDTYg8Qen2N(&<+sz|z+rxhn{u+&WKVLKY$ zi2t=!nSZNOZTj1={1ErJ!%oMBG2nIByZ3g_er5W;is*T zSL!h;NAk|9VSZu!@LQw%aa$aAJZpAwqZmn697cWN~;u)iz)0%AOM)g2Ng?& zH10y4N4DQ-okVzrov1-O`?()Gqu6~L-2`020-5ebR$0Vt2`8y!uL}dC%dp`NMKE}R z>QN2dM6^^WtFT{x6jQ)ki#2{hD**zXxFYR-qqT)NU?F$3HB{5)#;?gzt9QQTgl@U0 zj}QV>s-eCOtoXnxPpZRV39xKlrAtbUt6Mq};i^)Q6UO*>3G*ze z2<(k^%D7v|cVhEJ=YbAWj$+5I7|4gMg3_=I5xZV~m_=RW=p& zcc&y)L^X9Eos@XQc=c(+sz+=3PW+R2Q5!9za7vO(tA0HnnHu zw*~>w>|Q=Z1WE?p_5dWKZofM7I`(CigO-m2e#vh&sTdzGndL zo5^xUmt{x0B{#WRmh+6L#26&W749Q%?z;G5Ix+nPMvp?0Kx!&RMj0uK4e8Rw|! zOsdRhY`eyTc(S@EtAk4OQ*@?@$%)Ks5<}TUibOMV7qK~v4d5cfI!P*$z6HpvTeMIw z*vW}N>v#;p_lTB%4y|F}4wB;`J>}OTq}J_6#vK0R3S%RTFl~7-Q8cy-G9-^;jtfEk z`*-e9H$~e_-~n}57@BX?@d|m0M8;=#;95bHGVUw|0=2u#L3EQsVqM93&JP{el+Hu; z9Ll&|cH@=x1pxVXsn>lDbZ6}8GRAySDM7ucI@;OWF;cAv#5BXgaejOxYJUr2<-k&Y5_$C_`fsS9O_)6G#qv*dOfaxxRlEuJI1HoS!=B>?H{8-nva0o zykG(9 z)QWv|07vOw>tdVefp?EUJK83gcxgD zhN7OB{aPlNT-m`dp6`}Dwv}_eZLFf{e!z4!d@4bP_L9~2fLevz23F-Qvl#smL6z3` z9kq9R#*F!Ee>MEd?%i=tA5(H9To_pV&ZyzdO1BwuXOq?4&%6_Cl-L>>BVsQG>{@LH z0OVyL*i@!jZeyoYmVv$(*MG6SOib#gr)l`0v?Z6N=nhfuO`H!w5jQr++M^@RWwQ7f zZH6)6+(RRVCgCJbq3!f`k3;XFxoYeuuV2M2M{X~{;R*Kj*ht=_ynZ^h+5@5nM&44y z%EfL!{uRqs1$CvrpuF5}(M{8-fCl~+P)F2(H!wVG_>lRi!YD@h`cpGO`tZ;F!#&C;CEf zS{!Ps+`bs^%j5k8j=aFKnqjxQ2k(cGzyRV0F%dm`3>~9V9K9HujP0zH;@*lA;3`PT zTaFpd?bH65gEq?Zwp_#yM`yU&nLz&g%5RlOjz`nz({{bK#vElV%oal&>SW1VmDSK^ zMGbO8uzv{`QYN;%sc{!;Adb&@hHRtI#q^!EYx?H4$XGGV3DqSKN2Jy4JTx zj@+$4zk$)R76O7BNEkdc)JlpgV2WIgOR}b@?bD=PL(-1x8azbsgmS5cj+>6zJBTG6 z;E}x}2gvY8#YN~0G@fpVF~F&4H?wJN_{GA6a4(#Fpkn?)12|GHArO927^ajTfZri2jd4G_J#^I>t286XSQ(j$v zv-zr{(Zdy_HFRCE^nO0X1XGkfP(0Z!P%!(?^G6A#&l7w$L?C+VxgzR`YwSBX2t|GZ zRC{{@HfLkc=9=sNnK34T#DC8Ob6fnn8%xz5aNH#lNtTI(6~jLfiX1V(su^Im{$qzE zJW>BCUu4BRql`(;^brkN>^**u+%CKXOKp6MOc~1W_gCh)zU2q_f7in{nm{m4$p2PB z^9Zng;Qv&?EqqHy+kfiW4&)@kUbHkraa@QrhbS-%z>R^UqT&((9{8Gs2|W_)IZnPz z5QPp0T8N-bb4>~bTVkV)$M^r_7ezFtv|yz&B#a;S@VdX=rIU@H;jmYhgF|+iV@$}A zzlF-!^wE$@cMSaWk?Z7xS(qd8l#A#d3ptY&!A=P=Bm%cOXsFv`W@fCxy?O zjo&fFr5EW|A!^u0X-eU55m+a+FplO_%!%q+M((o2{9)SxKQ26=lkkDcOj&ivl?(o+ zxVf?79Bqh%_67z5+dY3~{XGA24H`Jy_=Rx+$mVC@kkXXs)0yrR)j9Aj34QYq?*PoW zz~3p%n6CXSMbxCcZ@AIRDs5r>k0}eE*d@UL7?ixPlSZ^`!3a!ku!)>8d;m$2qm?cxh1J&K zI9X^1YYA)Wc(%JCd|1S_0qaaLiIPuyhJZ522A-R)ntUR;_@GUPVF)E^gn(G}R*S27MKn2&CcBeLg0)|u` zL?sPFPGihw0wv~gA59UJB*mttSt^S{nc~<<$C{k_>yXU5-Fqz8o-nU*^K%#Jw-YPP zd}yl5Aw3M-hH?jI$+&X`AxiXtpUC<}*mp-(FO zhb$v4dt7&nOJ3Ll&*JMKy67T6y!%e{GpLL=X0D5Vi@=Xin|MMbJTMt0NH0U1T$kWZqY$s!eS|!rB8>RrWk{^jop0DK*j_* z%Z$!hng+FGyQ~N+2t&1#$Xd3_0CGS%k(DlUxrNS2s?YNSe`p9B{c<}_lDD^lJl+{Z;OO{f(z?4 zc^gY)WR?_|RjyaBqT~sv5H2C68`H-hC7o_I>Gg&$NOc_E@D<%AWsUsSwCldbOrhal z!Q0B;cZtBs#{i{0^_B_1n2(pf>-D!y%R14n0SZ2dT?kJkeF40#zHwjL{?T8gD$Y)u zNJ~2VhRy*|_+)vF`I*B-7`^yPl}RC4;bVQAZyW1X9geL>lnu&hbB?8;R)H57lBa8NjP|OE&3vI5vI_+(*Z$fd4QI{Bzae@Qy}LS~ z^bkPvOQOH5U5%zSTIBXM5RdJry`7LOmlx247dhM#acS#+A{F0n_5}$sv50T%$7pDT z*)pZ$N7yw4do{}e(iwfsbT745-4^XwmF*QVw#Qubtm?c#A5ZgHhCa-X3VX}ESHaHv zwa`I=q2B5KTt3#wSl)#LOhjC80#LDGgZJsOrP~QR(I{|o-ci2Dkf=<3`)3k@^bNYs zH=qW0Y@4a9qDN~joF?QJVGYOBZ~hbO?AvqkyvBUL@61a9`YoXFWmHm!g%!WN*ol)~ z;6)Soa7~A*z66tO%F)=-r-c%bwD|EBk9br^$sXj8eiU5?yL!vR3*Gu)NZ|_D25sTnsTbEhC8SU_6X1j3g`HFC(@O$3ut$z#C z+HA3yb=yGf+)!k==OxVx{O@K3(^i@QrV94oZuOl4)(`rhZq>SCZzuULZ|^VM|Laz5 zrx{>|;86b~L*Bp6)J#*z18V}zmM}S4E$RN%Q>#qaVbAe7UasNrT8|jvz||U0iBxXB zlRTn(?1SG^aS53+S*xF?^uw$`iRuE?v}*8N-fE4S8D2P(>np2Mr$7nNyY7nfmoY;Y zMlI5(=HarwaGS&h`l}mlhJ(fNB}r4;hMp1xfH+j8Y@0%5`#cxb(yb&MS+fQ05QMzH zDqY@KeAqAX#$X$fwh__U(BWh+Pomb`s)iVYY@idy>sY%@_qv{YNdKC2K%$XH*CmnPoJTLHuEY6`FIPkNwKj)N~&Z`&I-@4 ztcGU~sYPlA5%}#zTX+_4y_~)cZ8ByE4!K&XRDE-1u)i@7S@UAE>E-x>sD#rY9oL*- zJ^JnV+a#5={jfOhULaY$t)sHX^Cp%Zb%;6ir_2yZ+`z(tj_++T?%M;c{}KM*PUS5RG*Z&k)I%f=kOWs2htRm@R=4KcfxKlN; z-$688au9e|Sy(3Ab*b1;QdS&N)9x)Q2jZ7Km-`geuDwRxh>F!z9AB`oZXb4b&XmlO z*l%qDznQOwwRiXD7Hp7!yQ@518r=%B3<#+_991;oX6GuzlDjn)V6^15Id6c7G8lL$T zhEh3#Ckc_!%vz&E`8gc3ooughEJYclsm7iG97q`hvT+Kd&tyhEv&nskHijhHp~IVT z7~E`-U52S(@$bXGb^HS;Ugo0~WEFPcKjettGb0Ef6=qLXX%Ww4jOu0Gm^BKHOHvkW z73@4IKy_NCY9aP0E#^S5n$Z`wB;ANqL`Q=YDpHD)FVW4EsM;BCmMqT2JL(HGi4E8K zPB8TJFU_b=@%TLXs+u+25Ugp%Y%m zjuHbXy(L0Na%Gt=B6Fc&B4%&cxeoa&c{(CuEfD^b+`*l7TEv=kyo+}|bQW8cd{B>h<|8S~s`uOf4sLDjAjr8Y(eySj;P(S`+iHa0AV}$yv}*CKOh!SocHRAc^+D6hIWwAbM)2CQW zo5c!NuY6p#tgE(uD;)ZGH}!0PxqTlUeE#A6m-rh8fY)B?H(ly5WY?@X8h@uMDkKE- z$i&5Oo@>Obpg=^h8tAB+EY$GwG{4xg;|irK6&;zf?5B^v@cCFE#7Gg84QajQpv&cEdPu=Fd%{TgdVBu+RnN;Cq0&j_au zQ9&)46d8Uy8WiCsrTJIQQ>xJB^`}0-3WKT&fwfJKc2N6fWG8SFj}<*6GSVdo=?SD8 zP^J-3xE#VJUdIYLe*k+or6_j&?aXz+LMsj9<5#Oc-oG$z$kxczF8>L23)dot-$=h~ z{drE}_s@ZAax6LHc!coS2F;Z*B(@}gt82Nm%*$*M2c@?vaD(vKaX#{ZtYVe`Yfe`Q zN#_#HMp&Tbi^2vfmN;1%VV$1&gj^piS#Euz{Ax!anYwt7a`` zZufe077Oa4_1q9j@tZGNI6-o-*d&k;IUP>TZ47Wj&4oPZks#_fFiZYBL~`<*QG5qmLcaz7?>^vx!^8@}>3o4=aqLsg%H^+x6G$xY!d-xB-XsI~h6@9#Srn+gZ z?C_`ABC)?eyX-XV6o%(!(_(TI=`ngp^BZVwe0GgCn|rdn$DsdewP^V4&WpvHa+-s~ zyP(_s=XLlo*qBh2wXG}RxRVl|qkNbdQf*O^ATz)oV{h1s9YDTv$?A$r!bxLeJKk-cS)$U@hn4xv zJxngObDr1>Nfbi6LwZ>;Z0YHNAj?#mZ{GO^xta*ZtxY3(%r`dxag;;cV``Us)WvVz zpi*D&K8-;5&r}EL7c*D8NWRTt0OYsaAhEd9+>(6ovWbqrra&P&VaT}=-8+_W3Ny-s z?|`NN956H#y~WY zFT+wWz&2HMF=;fQII9_a7mYHf;j6#okq3L_0+YKF&eWWc1dWT}YUkQq9$&>r5U$;G z^j?v+#T{}b)UOP7)^RpC0xF0~stHi98Y5TL6|j__O%X-Wo=n+kn$vKG=?pehds^!pMl zefV%Xx+)D_aMH#G^`0V*J@%NM1V@q{>bc@%l96&u3P<{`^p9B_bR##){3V;QaJRMV zQnyt6$?qxIYkgPyrSO z{NKmhMTKxB?sVsId8T;yMLi$`F12kOtGHZ zub%(Awoxe`x$Ep}wo6Zb0#RWUjL0RZg~rqmz^*qUAsF?9d~53s8->u`{=P4u@3B(1 zfh^;neR%|#ky8tORpxymm5u&nqhe74w#;N9hPx}hg`23Vn$kvEw=S(|!&%RWjJ|0{ zc``o3HXv!EbJR-mVuBlU{j)@1$vowxRGTmK9^YL zbHA|`=;Qo7U9TTmepW!$rw0QfUTAs7m+o=Hud>bNc2cPUzsV%{how&3Yec*D@JJm^ zOKUhqHjOD>Dy5^|W&k-T&xDWj#b9RDWng9v8~_x^GIO=`@@3t16RiieeAI#+;2YNw z*k*~SEYqQRawmW{MdG${^uFolF*`tD_q(huv&b8RszEcN&@Fhi?vrA5GgUe5?Lb8> zuv-m$_OLXyBNDZ5c;@T<=pgKIGKoni`~yFKO{lDmo=(H&2I=Fs*UQDD@4{T-@X2h0QPxS)Tnd_Z2Dg|hP;2(XE z5>?G%= zrygBc?fwZ?JDTB>J52`gMM!@GN#s^7X~ES@Xh=uaiJ@$AWpb9lRG>-{zIuAPdzQjM)~b@X&~TI3E#yR5v|zkfOYx%f(n3TmmfYQyIF z?XyCw+P$+f*%&@)eXmQi9F7V2yo^fAW4XVq`+hjU^`>H6QLlHOxywAAz#E8`B!`V} zB0-m&X95))Sd=h)W+_&Xb;}tK1cW@HU82Thcn%Kccl+#m1+Veedh{z{CxlTOB&jgb zB1B-$gc`I_V1ujwcps8oYBHGf&L)geQNl2ROmIAMEvV)D-AkWru1OyNd(U3FM7+Rc z!)m6{f(3k{&&6U{;ZA&*-jxnIgDKfEI;fzH(tcM1{SIJPQ0~$l$hYQ$( zfquga<3)NPgR)zWq=Qfbk4751so`M#ZB5P~2BrsPDj(%vu#G8SI8oE$28?I_S^5nW zA`Yz{9swr`CQuqo#4D&0~KKDIbhry zHxBE+V7X|%CA3t#kWP492>{b$2#D~IY4K=>Mi0Gv2pAQlk)Xlj%hULf!LLdu@KrS zd{*Ckc)R>P45sT#0wtk6ZQ9>JbGnPqP2?BDE<3*Y2msIF^jM+v| z`DBEEn7K3sb;&O5Um~ljvXd#zy}MqRKQ!Jt*e`m98b75{dcR9Gp>(>5CO~0|w*+VP zgv;QM9)&0JfQ9m%1AK>wHD&gB!~fQQxiYl*`e2XuBjoDf$e(u%BwmxOkEWJ5^uGvc zNMMpPL@*z)MaT>a27)FwLkq3V|GPGI!1{9uWnHE;PvJ+y5UdlTSNPJG^O1PNKp2AK0b_!LMF?!;^b8l}Bc z2RC_9!$r(lfUHjuj76^o^=%wkjKSImhT{Y~mZH-j$yk)gm$^Gi8>Flnq+DN4YD=ON zkjvGDAmzbVX3!ermJ)WCC4YEd>)|_I1{uJg;@!fUW!?Z$G^n9O+v#IJI_(Kj5U}^M ziNK;Vrjw&<0<;ugXUlkJI8n52$a$gQKOZv_a7iYy)%6X$-0?1?gTO`XyI#sTYx#2u zgDv|T2b~H3`6LKNs zp%JGWRUC>&D7aH~mBce~7HPH(M5Y&gJy1Uqld@Jw287KU4H#H9EDIwX%K+P40wYk5SXSXm#3$nVF*3nbA!>YXxn{aKB4ZQNKQbd>p^XU4K~1JeX5Vg3u01L z7mtkA$S4wk!VN~=Ahm=qsb7~&pyWYFUuj>*U`0BJF0G_8d5xw)Hqo@yuE^$YVS-$e zh}v3;&D?WQ9n<@AM51#v9*na5y-2@gaowh506;C}!??u>rgz_N^t_8Wv7Xal+|0es z1^?3PO98*zUF1WkkMlzs`k^sH*55oHf_GYc0xF0o)4bGO8LEjFuF5KBu-`eC*ll}e z*maMnrX$soINR|ir@+5iU~bgyEgJwE5*Oehm<+8#)wpEdugH9Y(XU~6_@#x4h?f%0 z2yka&G>KuLR@^$caSQhflC=N9E35%U-D-eKzJQyPd3Hl0%tFO*USKcIbN;*qFc!T1W!k=5 z>oCeHrro8Gvoce%QJDVyznFkH%0E7we>_~d9M#S*bt^w`y2s!@)yhx>H!rgS`~l9I zHjqO$le_k&$S0@YN^trq@s!YO^c-Wj;c$}Oo zAp&R}0;bb%{nJDOg|RANi%p`_)#KFzF@WpPHj|ensuAt#|ALg_Cfk5J8%s#b=JQWh zyq-n4-bB#5<5H|w)|5q-Id7$F1On*D%1MSqGmfGPRa<1SQ$&yr=2XDFxLXmih?R=3 z-V4-Kj<=5aag5?M(bOb8nm=5x^AbGjKuouqVq-4NbE0aI;l{;0Ft`JgJfjGJci*E0 ze1A`ENY*JVxCty_V+j70ymy*<`nTI#RKU+Xbo_w?Y#deIa&UF04 znSr9u+k(gxY7)AnS(o5nKg4wN8D^kF_2)ZnV3W$OA7UZ}jy~&DqCSDT)>7fa_ zyXqRX>nT7}HVhEpD7)aQ2s9pr>IH z77pd1Hin(h#wp;e?bx!b#^|?k427{L!;8%5{A~ zKUfc+yCZr;d5DO(noi|rtE^EZ{enBH(Mv|p0amLvZlMjX+ec2jjI2ymRb0iK$)<{+ zgSpc#Sr$ziGhh+*PMiqLqT)^EP9SZz7h-lsRM3j0zIc$PP~)S`M4Xz$m?si=>VcjL zT^10^A^}!tLBn;8wB@oBY|rw7LPzAQK&CQz89R5=>0Kanmcz;C#=FnSQ^zkj3wK2PrUqZg_c_KQ8htq$`r%Ps0= z*hO=kZg^U`T*K6-3@#f9RHH$%Cjv1&RdftG(f}@o-BO1Af_72(B>m+nE{6L*SP8+W z#lRQMByLMNF$_o{ATtAoWH<`eP@M(Mo0S&~_RUtcDCQ{dyCG-6SFEYbG{hV81pNEX=subD69O#_MMy0{u<&8_JX(eLl^PIsB!onBKZ6@MIhX>j{7g|N zZ77FLC}(*_l)j{VW?-f6r{Mc*!D%&ai~vG3>;!dgbXCRR3!c}hcDBq_D*TP>T{M;y zDwR_)4W!{_nZE)bk%Y2ZvkBYXc@RvNp0_IHj|Vcg8=yc-DPS27;I89=URaoL`!O0L zejsXiW)7891g{$MN}3IehC(@Uzf7E;7Q=W1N?FEo_-LN84Dt=|^T|e!ucsN=bO9Yn zL^4DRZEf9mdNkOmo3-HBWpf&^2){9lgD1WXfj{xE9?|3=S{hSpbG-QT!0FK062cuh zp!GJBt}8vYWfhq(%RS?qz?T!KqYK|r($F%YE9~N{*hfzY-7?RHJByK|OwnF~`#}{W zMs1=>$NttgVPQXD`w#>Ol#<+{zys`Ug_D0=Zf%?!)LWW(+HyZ}MBpb}@%Qly8O{^^ zd)i`ZOEGkZsFHZ5EnA9~{N{BX-Qaxo(4UHQg5g>0(9xZsRly66iVA~%gOhg&LH~Mb z=>qp-b24q*#gsX?q07RNh+j>#_aux7-%WT$p!h^F@_72ZQVjT;B&2*^!T|8Qzk`{t z?fD`y&WP^6$c4{g_J84fpko*O*%E%joj~#-(7ydlKJvu*3=_+c$M4*{d5-7f__vFg zIWMFYw+sr2N6gWM5sbFZ&fzv*B8I}FGkXN!%e-3?Pe9S-*69;}tQwsZx66%T)1RY% z;bgb}PX%$lapd>2JKS^nOc#Lsd)Tx8;bMI)lC*JPZ0k5ul99tJ#gSq9T`%(3EBQOt zvyZzFiRh-jMuMrqMo7VUQQl8m$D8&sRBs>Z(ykR$^9`AcY4vo&m6KbkJ6}HEgyWkF z=bMG&@>Z^|N3Lu3)yZl(ax5J!QaP;0GrNd@4qxi}i4IL%doN#=NWnm!U+}~Hr$Xk~ zt9#w;BFqI^t;1K}%7yZTfTc4%^*V%G0BPP*YdrGx-w~U9u6DsC@-zMx zb#_lp0%Y&CSG}W4oX+WKQn&Xw?}h)YQ7~TM6&U8HPk6~yIcMp-*y(}HS|%>a*vmVK zS*c;Lkbe}LCS@7*$V8E3VmMRvrk7SHmsN%vZ}g-4l~BGti8gBXgM{tneymKeEEzVH zNY8_f(5Hf02|C4E>6FQX(o=9ZqP^=)YZMOES7K)bmX z@4%lQoa~l6JA6Ux%N<`I5daU!Ps%cw7h@l+0j5dN8HnAs&^1+tQA^jMnTA$Ci5#`$ z045t7#(FM$Ypnhalb0BoB5$iJYi+aB?et9}artq|zcQU-_t;_B$mMxO)$yhVQ~lOYF><(Ws%ujVI-kJeML#!8(M4aT|t z{2A3CD=$d5<=+Z{En}3t)`6{;j-3 z19E2_b5@?{f(d)8rv{%{!U_)|rTC{*MVgIOvy%Q{eX67e$`K{Dd&(IFiUnLTAg8!_ z2vMrWRUK3SBr#A<-eO6WtjP8l9mQw;rM2RRUE*TdOku!UC`2ZmAw@mdQ;+{thsZ%= z#27fEI8x1nGw*T<0I;UqzmvfA|;eR(NlkmI&SX+$`1)#H$oPQfq;3(bD0X#lid% z;&7LcD%up8Y)!vbm3C=sfzklb^JtC#Gyj9l|NZD7d_TGAr5wv)6u_w8E8q{E1N64n z>3+dnF5~)x)i@^m+jFk>!|CH~J>x5`Z;94R=Odf0PUx*&EoUApdia-fB;2l=etD=b zdssP5vUM6`|JoRyp=sdHZD2U$JzVv$4fbbMLmH+D{N>A!5FO$)N$%lG*x%)a6kxn!#je;VMLbU_}&>BBw<2q?2^gcpz*TQNkURR7Q&SSs-0dt#CEwE-#g^H$Nr|v~0(Fk`LHK;5Z)H@TC?Y0$QP?wtu%8>lp@8GPBDvBXOZ~3%4 zkrTFH5EOw0ZApAJeP3)@*#@B``H*^(^e!x|*N{Ok2w)dikcFcum6a^Pqz9W}a90?94V|`q z$l$&ue}iXu^F|?QKkL%DhNAjL2O3U+ZbS&mOB@n7^UX~>n?Dx#BEfg1*`Y!61+w?q zu%fr_3;;P2Y8Nz>hGGGLe^?*`1hV+waimV(|qL{UwYSYXE}fXm9J2lxOJUWQEq`+Wt&qUTj zaAsoUIHlRt2kf|$>;8wRg-iMm3o6a9y`9;G;&zxA@hmlJM$knD4SIX_H9X|s8p*dA|zqcy#&*6GgfMFJ} z=IjsAtx;Vf#T$5yW}MKr*&{PfbE$;?Xry`S+wja9^{Ys^6TzMt4L$aIW|l6x>7 zhF)dPqbp_j?z7e?q%bS07Yc$1fM#=lvJn*(Zpmb#@ybTcA{z&{*`DOjMJlY7=3|UW;zpSi;215IALX`*N#AUEhxqMk(%6ftDARI*S0CxnaCc@~4 zXZE)#y$ms|=I%OXIv_?0r%D1qlt2YB`7=oVY(d+HFnK-?d->vYGCdsz( zW&LSB;KU5dN<82hY+;3$JDh#N3bJ;%8G7NZdr@nvt*FqvnJPUL*z2K-DkeBXHhn=| zS~_tAyI%_rE`t0aJp_TWfE(S!LDe!AwyGv90_PHYJ`6o_1mUbU`<}3fYel98`G$th zjUCgOtGkWS5AD06S;T&5Ms4W{cGPr5?K7nQ*&-?)2~BvS6Ly@vQ+e~S_KK8=IWL3B zlh(OJE_SLyi&2C9GPqk8h?{{xi;>Temukna3@khm(9Seo0d>s>z}aV;XbdV4tDl=X zz|`+JrqwLRgHa#FP_7?@UG;LHEAEuY0nakRT#v>eCl((9MI_$1M!c3Gy0Q=+21?f^a4@_gULy+L6Y~ zf*lN8FS5lZIm+$aMpVPlDPS#VlEXL4lkjPWuLcb zogkx>In{Fg2yr8s9>4M0PfP=(zMmR`{IlWwMo<<37U4(_7IUY@wox1-T@~q@4wh#> zUi${1ccOwp!@4cQbwl1q6a@xZl;@*bHQm#AMPEoUzt|q2t#=&M1`L`LJ&@eLN_HJZ z!Ng)4CKR-Gx2ZpY+KZk%>^09GB*ap#)RK6ExI|36AkN`K8nF7ruPh(~oL+@+)s;4D zcvn9FXmliW1=x_{6lv{=)Inii{X{oyG`H7*loy^dNS!e0kfN!w2ojJgZ^47aOe)^n z2H+Ne*z)naW;|Oi#5*+aaaMk5Tk=gHGMye?sT+su;CLf&iHE+jkCeQVJEKYQpu~i?x(~Y@In9MhUEk>vK{CHf-OUWwX>tITLXQ^J;wBd03_bK z%1^}N!G?n)%XB`-ihQuM)2SuQuOIOjr*ABtRf$Sib9vbAMKnXyr@D>xmP6H)T*ASS z=FdAPjSO{hUPefT}ZctkZi=y>V-U$N>hx)13!u!FB zS=iu|Ze^=*MNG%^koqnEYCG{8d*3|*^3Yr6Tza$CXI+xOTW@cW*osyI>n|C2opV#Z9Y{+z><4L&` z`4l0m6J-!MBx`zOshL%3r_!j(qq<9oOmXPmUl=}#f!kh8nxPSJR~{g2Y~Cm-;ni{V zpjJZnZHKAziR>pvi-*Ce=W&XFP!s%>U)?*78S?eKlye-5)FdhR#;Y&Kx<4{C4_HDE z;ToZnPq{XRfVFyXVQT1Pk(PnMa84gbU1OZCPWYQUFjhiQtyE6f#6@Q~EwkHOX}`8pY@_lRfpYUfuTx{4n%nOQKKUqO zyV9#Q2G)8GidpG(sjl+2Lr^j%$vE^bkOrjlGIwJR+l=>$HPB}e5N0@Dg(icevj%;? zY!Jh$=RGK{ANVP?h4P}g4@=2z3Tdd;?6RGIhmdd!tavfd5eUdM*>a;Qq?s5LaR%fp zf8i3`7c*?L-SM<80y{?laNyf4DpWQDAi|xksNjPW$6dc9ur5~Dl|SsGB)fj`0UZ(> z&SO{>2P((d7*p@eg8{Zs4r>UWJNWAA2vJWF3%I(XiR0(^X{ma2>&7u~zV>!!(vTx~ z?Tg(zy8U$5=JlRJI015QWtpPT0VSn4>ZIsF)_cLU1a79_wA>H?*EWjZ5nfsjMutrI z)FMN(;>bkc?0-oY9@`kOCxtzrL+#xKQ9WDW@RFIL!WY>-doPFR#lc#(B$mO}K|C`1 zmoksqMCp1Gc?+`Twxr|Kb<$mNwEp~AYIeC)$1yGlc{7BDp#z&*7lOn>=~U_$(kG{v zWRY55zhR20%RMguP@*=?W6$(|L)p|`3=xfTP}A&2kF=aQtB_c|SPc)kAllULKCYk- z5kNvqR4Y;mK#>OWsk=#cDU04(E+Ao28cL+FgDttqhe-V1%&4JHetpNiH$9s@u(z3F z9wDTyWS-}qm)kpD^ke}33$zolLaa)4dF9||qiwR#-SzVUgt3N@LX)$I(`N9%r16z}Z zTSKO>ZQ$6WI%cUbT4MiRASj2dEU^lwYGa1>WrsWTBF+zW_ejI861zK(*Bo6;@!R3X z-1CEIcR~Sxa>`!3&tRB5VIlq$k3`{+_Ctp!Zat1SmR24I@e7I(G^j18oRu(O@RbJ} zfljp;Pbs=MtDV%rgsk=;X-fTtz#5vzLRBrrMB9R^{n}d)nJvU~ni4YzK5A-?b7lc# zac~EFEg(FeCOB(7g1@X_nB43otVxDl8`kKIQ^y0)=Z5!6%YjhJX2L+z!s(6|w7Mku z^RAN-uENla(ttEtUVhjRg9sFcTE{`BTUhiP>IYmEROhaq(uKLN z_h+e{Jbgb4!u0jc?H7q7H6#+ZAHgPZ+;gC0Q7Z!%vF<_?fUO0C3)aQTgPRJ+&@FqA zbhQ}(`43uI{~KnB;GpJsciarN&Iu8Pzy!UUfT4TC$8=|~kl&m4-0QtA5}Oe`2zQR| zokKL9z(+FPnAP}0Z8&8~AdTEIy}ZE3824_H1~U`?xwwa4Wq39T>7<&t(BXd7^)Bw4 zVcA-BUWu@cUrZrGq7^YI44C#0Sqg*gQ;E42(v$`QL4qRIx2*MU>>nEB7ZUe3NHsOM6 z=rrK#TrQy`TDk|BSp_1f#}2hjFu{*tFQ_uQvN1`{Yp4;;NSIZWhrDCQU5j<=I}C)Ygn$k>RnN>G9>k85lQVCjXq~JvbTYv zAr1$c<8iEiLR922i5lvkCHyK8Q2=np3J90a;qs8DOY1*X9QN;VO`1o|xpsy$+9lQm zl-e8Ka4Fm64X)59`XV=Kx<+ls~QHtC!-; zH6S83VJr?h7{rt5XM+_x@qyll3?XF02M=YPNrzC_=h^H04}^s=CmbXL|H)9E~89EX6;A3gFyc=^uy4&h`)DYZ-zDW}2o5)v8SN$8sj2XIrwij@zs* z?GxG>XDw@UU~Wd*l8W)dU_2D80J+6UwJw_O>)z{Ee<2LI3Kwn(U?PNo8=x|**lIEc z)zJ4d?Tz3kor5By${f^&*{vNz%*4%d>Y|-Xoh0u3u$4ySzd3I25-A1X@0ChAvYnOs z=Pi$uD?oabEbnipxkGfS%v1o?MRP~4m~?lFjTiWsMBiOvaTf*$(L}-hH6hm3oOT0| z{L^b;K#ey1Ur|Bs$d=VhqKvL_|kOr8$}tg7YV1{)$n4BD($cYd3sW=~|(vpsLDGi4xX=29GyIOUsWl5{O+ z4z#!LK#M{aL6ezKrfin{Pr}ttpe)k7!>XbhzDb@8{(E8NmsA;$lfc(WvV5!l3+wT- zsUH%GJU>X>h#?pdyRPoPC?8%lAiwsHj5&ua&0LBXY=xB)UgIfsL8T@nVn}O`DzgLf zHY6oo_Z0JE$Qyqsw(R4%E%UO7TGYr>%!ti%Tk8fllPk3E<4(~?cl?$vq!CDjvcbS# zb>+Aa9<+0R;!VmnDqrdd9;>=YilkJ+CtwzT@X0TtR5Jl^N+3wL4Yzd^<(hV{gfM`C z)=`z}ISBAlD)&d$DhLlGU+mP_BIGJpZJH*L#0B37cNn7Wj84fV#+Tc?0RM=d4Ht=mvwi3Z-JG_`S!87r? z4zfN8Sgk7c~qtBceUWwg-ns{WV%>eFJSs(rpf-aa6lG&b5+DJ6R`^K9^Ma3%~ zpZoHtxvjn58{TtSV0;yJAi6732`&O>f#~nh;RXPCuplsOty;rC>SUX#gNWT@3Mgqb zR@A+Y&Ss0(+k8&9W{Ss4DqrlQ!ok3bPP$u|oqob!D;r(O=nM{fDU4yl^Y4<7x4Oh5 z|58Ij$F7wpU#8BJ1Ju%`j<(5{js~DssboY(P))c8pOlZapSDpL8%x57$wPi=^4nP; zi6ce!xUioJQni*?_LUHIzD9eukKp@W$xdm%RDM(X9j- zEe-*Aq=alXzDKdYl2GXS+Cc#D%Eca4Syn6-vawM(AAKYHAC8g4NqcI>ybv zKjThxd6(|~KG0cX;T+lWn3+HSiCG0)Egz(uVIA>YE{=;0|4}R|?O?gwAf_=Zuib1A zPH2BxS@UK(_>JOS@nqBw-Tzp~6H-3<@BopQhZIITF|!j{j=eLtcNshX=XL7xkAt3C zm&($?rct7JY#9Kninj|JE!x19o?tJ>ONsaRezsm+O-J%4labjw22kI7x|+6fl5Ovq{$FJ zta6+BvNqVj4Fc;E0m?xASLJU<8^ch8kY5h}mu)Y;dn9Il=TBPQZ!H&o9|5mS&YgGH zoM43nKa)AknMxAT@-cloN%m8Kec}qCdJ|-~EZ466yV(bO>-@;}-6_qTJUP-jNZ%R6 zn7n+m*_;*<9H1^676M7WRQ5xSXWjF`pxyMDmLxl;# zw81K_f~nPNia*GvU54W%zA~{RCo$NW%9bP1g&`Ya^NaALy+vL42sv8P&W>?6E0xYb*gU3eqPl&tYjA=1Im}REk zzvB$l z$An!^_|PF<{Pbjw4tOI&pw9H8o4Mmp(~=4Bq4P2pZ_royIzK9)u82LcrGG~G*; zM&Q|#`_zza{~JKHbNW?ZK36nJo&64bHli(@IZC)3wu91Qk4?OdeepTtKwa&hdX*4^ zDk%)j>}E0Fk#R9jt~vt>Of}uLq2k4;LlbU6Zm@)_;>^A%SCoLq7Aj+&z|gCOa*|(s z(h@ni3Y+x!bOgR10mvT{7boVJoF0!1&Qr6>D3hM9hzqEe3k_78d-cg+&_p%Bk4#mW z&{fKJoy}RUx!JMvSfc(nVfTgBvJZDDKM@f6y%vNfLdkBP2%A85pHuXJAAWMz0H*sU zp2yJ*&Lvyx(!B`OH!8G{zYjh_m*FdyrB7q1&?W&VEg&D5m0xcxCFmTh;j!mRg`tkC zv0U#f9Ro~cVJc`e50rGYNkEv(IQ@ctrgEfVZ&}=8uVrm}ihs*1-P9{tVo0*qaB#Tm zLE~Z#Xl|`}Dq@Ifbg4DtoeU42Qb>f7DrQFS}qIL54@L>6i ze)ZlruUy~9hx1No`{$+}dzalCC*Igmk~I;Wgb>r^ynNJ%2?lp;-V#P`?!_|X)nKUW zX@W;EQtX)TmTU5Z;d$TxEM;qRI)VWE53`#Fw5dXa0|8;7`hS}aIbbt>y&Vd;q$_$?&+kcFkCEai<(I{v+tNo_!<93K; zfLHj^i3xM7=TmEk7(3eU>i9%~iru*ULS0T^^f&akuhtAfj#q6- zZM$7T|1C7BKu|RIw`Gw6_qAny?!K>8gv~yrwQakU!NH64956qjTBP-(>6QD`A#~o* zjPa}PiCI;%)$lBhGxS9>GEaB!cfOSWg8SOoS?NVx${7Y%*-{uMqE|*(ndW7uHe08q zsSn!*>x+PRCnp+Jl|A%?r}o0-tWEj&oKqfltYTJe2=g74|0uK2Hig7cQVbWc8S_ZA zd<#?iWLC4_0bs-HY+Z zF+l)D)}7Oai4&7rB_QR&j7ZuIn}jRA;s$(Gy1GnjFI~J5WQVs%QkBva<{HyFMS(aX zgrf+GYg2~pn;KMkA_XtxP$5fsxjX|OcD}{CPoUGKnJ*^_YdZc{k@N%nl^dClx3TTwe%?$l$g1J!p_ z7E*7b!#o=%7SwNtUct4Rsj52bN8T0v=oM8gIe?%byDbd&;b3-Quvv3YFx;6+b_Ih<7cEPNve1=FfR;~XTwN3$=bdh@OX2Uxzc z)XNNk(W&6`D3SmC#}fyxbYNKiZO6|a%L@|iRiM4XukW;9?<0w5vxzNR6v5nM5Ex>Z z4uCFo^=@ht(e@qfSYt+_iU%4hKv*PkI@##0(1C3euGvIjz(z+@4zJuAm*%jMQTcIg zxBJrL@5(hbOBL2hlk-=Knzrpcs&=b3GgB8_Tseh3&#KNX+8KX_uMEghqf&@b7F$0} zvsr!9!I%ZLIyP6#qPWD|y^rZl$RGW|3IHn@dmeFdmJI88?b||>{r6n1YTRC(zd4Vx z%UH|=G9|3kx z&015^ZKpTcCTs;DL~X@z3zYHH8s5lH%c&H}QXFJ~(3Wz$RQv;Sa<+c52tdWcMsstH z>o^#68YkVowcjt-RHHcHlZDWDxu$Rm>pyQv*$=h^L41}-Iqq~JzK&1S7Liw0Ft^5O z9<@oaBZX$mTY=J?C_hMbWyR0(6>RS4j0KCnF@w7%aTyXmFOgq`XEdO;WkyXOk)iW8 zKzl`~V`Nyx{cbf}A;>qq00_>Op9}n{gHGn*QVu^L9|jxhC(`1tY{0Y-p14qhm|j!i zNA`Wc#G00Cbi`x8bh>4!1%DSb`t{G!7z2~7z$!<7beF?4)8;_PwobrN5LnfTC3{d2Nv5e4}LiFB# zR&|L9D1A+{6TLaHHhoZYSbs?TR1b!zpof?f;WUi)6+k)gw@UV4bfp z!zJ5ghiX^bj9hy?|Cgebg3}a6&T- z9}K#`s5sRXWKpe|&CCmiboNDX2tF1Z0@06IokPl7uJ~01LorbS&A_C&Khh(FIcy4= z#ic@bv3sBW2kMNH9&)6LxdRJ|I(eXJ>&6faf@k+Pz6hwlZUBIcoPhC^z-iG#DfeIX z2>wzGBa#8!fE7jlPzi-&xSK2%T*S>$}ElFl#5bRDle z-;KSTL{pKhWB^g=dX^7Z$}&T-PB$vkX6yD5wK4C6fqP8csHT7Cug>z1Cn(O&s87^8 z^!3CKce;eWq4YNDbToA0t;Z7OuC|TIolkeV3&{7w;bbgX6z@S)mJ75j3aZ~b@oBuP zj-iMQz0VSs3+pBydu?QdwczmY6-3GH(<7Hq&O2r_3xJ*qG6&!41@;tpLb=DR!;TPBcu-){X4Lydeq*9_T@6L!vM5i zPvg2_ezR?soiO?t3YLZmE8LUkj_2M4?5#BDTO8!D4-JEcm^Gs(4>Gmv3Me;CZ8;#3#1vqGD%4h`{1P(;Rn;+S}OP%v;j=aZ&$s^PJn z&{*BFW%y35y?DvG|wwL-ziOMhYHn1&nKUbC@eAiPsyX6F2yod;%+dcwzX z==&v#*=_&3_aM}g3)`|NA*M`W!|4O;%ba+*4-lGq ziMOu3+qO%#S~GazO$d`(8tWqUTXT}XRlIqDwENB@^Qw#P&EUU_v<)pm4uF5A#@Y0` zI|yL_Nxlv9Doc`F7N9TLl-EreHKLuDf4k?oWch^ftTT7Rw4zq{sh9)F5xZHG^3gEa zG_GZVC}(+sqDGLcS(!{kIuN=c$5*KIrhw`P_v%6t&Ukan?8462fg$D`7gnwWcQ?J+ z()Opy=gsHo?&$rD1Gj2CT)HdfV6m&-n6|0WY_~i=;xrV~4+)xtX@DDxT+wxeY;c&&Qx2>m6^}LC%fg&v zF0zz63#qoO{DoMdo6vR8h)c5kP}I9umh%J4l&J1rFM<=y2}v1yRs{&e3BU;D+Ns0k{VglUvIC;xxwx_wHTQAnd?12318aQ z3scCVWz<*uxAKOFL&(&|o86c|aIS>2I*Z=O;N=_N-u{aqU1}rrTI~>S_is8=OJCsH z0~butD~UIb_n&kA#3@aB=1RFgbGbTJGK>zq4HUq;q>fx+DHF0R=0w62*3bP?~a z74zwF!Uu$1SIBxi>xM!0rAo$ZF6USI>J3xb*4^qV`8^XJ<o*P0Zf^aM}fq8dP!Mfqk9)V7qeXZtUX3wE35Q%}>G-ZJQbZKwN{t3fy;*8r6E;TOLN6Or4$DkcPmI*Yo@!I}2DkZgDGs^M!Mo_4~ZKICPEdao-a%m;$JX5Qv;E5?7dx-NwFLV zgsi`@uOW7iF@4%F4@prA+=PtqfV5?tu#~iF)JeSa5^~Lzzb=$ci;dN6m;gT#eD=D< zwj5JEN}R#rm7t*gU1rUsjj?(pM5BqMamN%(^jyV?T1oWoqVza*jN;}+wi17*btAh! zUd`mEfw?V66Vs3ukd0$J?Xgk92%5+}dx zJt}9Lt#g#mk>AWR)9mkuEdY2APfDnkQ0wGt!jgvygAyD?Z`^yU==7LM0%>Zd1OECW zGyhw>eIk`>KB<%>R3yb=7Gkkv3tT7HsAfhxM@V{VN-C`#@(KJ_{xrVx<73yUSrKGH zR$L$&zF&2gjU3c&%PKSxQrJ`<08=Sl;%+NVsoro7=E%xvc%^q7FazYl|1gCMt4G?SJ%Ii!~sScKQ!o9caD4AsxoI7_D$7MRVv~c$($0S99UYI@0bSSSB(3KU6<9qTkQL)Z zN;g{8NFACECi%{z#8s4Aae_yQxr7Kma`pI$fm_lqgbw%#pGnZ3=xT7AOVr!kx?tfz zwn(&bH6naG^+77q!h|>S8*1_nF)mbhBPF0)m;+r zv^_sUu_gJ~N-#FIt7EzKq@lr>1(Pj}ha+fMn-!d+Rrl#qYI)Rgf{f!s{DMA@!RS1d1zbFm< zOK)pKciZ#VKdB(oF6`#g$sM{5JkzL&_>J`bRDhI@-m`b#N#)KWWb-_|4#GNWfdw>q zV5>>0H9oA#AEmV3%dlo}i=kB+(lPM>lu7NMA%l-67g1S9xano@5VU~z+b+JDbk$}U z0*UNd-OAXjDjZz1U9eU-g=wX%L4el*i#KOR&zl7kUIvkPy;GDIg*PFxw zWB%asd4FC>%Mm1p3o+^kJL&}wgQZ>kpyYKf-2Sa*7VaAL;%8%|@&+l11fM2ePLTEr zhXKB&H+!M{IW!M0r-laH^3#B>=Q?O4DhsFD;LEKDN@E#3?F1Eb-ILD9=EQ{_2l=6% z8BZb?T@xx8?GQ{$2rWby*@8VT5pAGhQY_nYxPS3=S*41E4y4_+Kw{&8v$X98%z=Wj zUsP`*jLY^2{=quMfDb7CsdJXl_6|34$XBT83XK56%fv=)ER7Agc=V4mt>xR8AIb41 zX(o@%mZS*o`Qoi1s!95+;KgYmj@E(fX%KkTbLhOyUsDF1(xOABu|!v{F_y0t(2tX< z|J5S~F3(f;>iYimc2D1#6|_}M+9&H9hi7Z6ev`fhF;lza;EA39nb9|4jTpdatKT`w zrEx){bALCtykQUUn~53oZJos-=t7Wm;|ko>|H)Es0)nOTE=jJ5+Yc;I##Q|=y(-vY zH|D$$+KZ-IH_wPtwJ)_@WHvTF!>mc4OEYi*SxWOMPkHBVImLgDQRFo z%LGafc`bdUy0IPKvZc@2Sryw|oM#!6PR^3@Ik z>u+ETw8P&m9Y#5Wc8J-`r+C^LqFW4%S>awY>!Za;XbPJ_q$Icd?U5rSGQgL>P^B#2 z;NHcZ8^e~)S!8w{BDCr8x{Z#5U54rl!;Z6Sb=4}h*lJ9i_jKc2;eqxs|Du)=78tNQRx8bio(b;y0tcY&N``w9Z|s=)vJh3Kiox6^!AspSy`61% zkzGNGB;pg=ab1N5OkujDTwQrfN3ZCUeA4Xb%Bc)9Xs95_q<_-&?^&gE8`Gqd9&~vUccMU zOs^-@4}$Mo;apM`%wTHMUZ=~Ikt{dG@nr(j=l*sAHq@BD4GdJ!VcOB60qfxkcH{%y zS90z7xc;HoLpwk86uwk|G|%UkSfT0{*D=0O5z6goa)_?8 zS8FXVoMdt4EwQ*MJ#q~btmoXd)NrWSAnyM88tdH#xteHqDSh;0|=LX^vI z2uQ80mrFNQvTNsVBuS7mQh5?llPn9N5(CQEWvCCZv>op#lNx^4&g;~lIUW8{DPc8* zplfU!@{;HA%uD1KIGeu7J9S3Z;RooIhOEkBT;Z+Ii<6iLF(;1*&OpXGzhpLLjD3>i~qHu z9l=87GGW$WS{45m(IEi^d;aa`BJBM7=fahco^+6**ogC*MFQ~l$ll4~<7CL{30 zgv5C9*6C=4b$CW8s`4Z+MY*ucI4RPEVh8|Qe2CeWChioH_#|^ta*32TbDY0`BrCi$ zypU)&oVP7y7ho(=V)=l8Ce%r%qp6f?YRzWNNF<=Z+VvOl|G2ux=E?$f3Ba-KiET~H ziEZ1qjUC&zZQHi(Ol(_sPSyEx?vL2@?$xVTKViyYk{k}eJWg)#Vusci=kQRUU!6I{ z==Aa^i$Mmox{L+A`zoMt_M1ZngzL=UQ|+j5g0olsSUjp}2__6{vO7=!k*st~gr$qu z+mW+mOkhRBzOO&cHSxdB1(}}DFJ~%*u=A+nA#$NZMJkiH%le1qF;rE(#XP10KkF^! zyM-6(0~$S)I9*M!e^&<~%O5W%(W_@-P?`yD7GAR0v}MH>-Ne z`;fqfY0g;JSk-BZpV=b-tY+_?7*1e#X0Ty{WR-*kmtLI__Id$x~ z+<4Jd;Mtx@50QN;`#j=eJ;*q`Z7l7N+G_EXay8YwEC!$R88{&T{9$Y>7{XInhts-U zevD<&b%Gu|fk%E`Bssii3!Ega(Wou#xcTV@$i6jBvq%BZ7gDxhLrTAgUE}hxB#|>~ z1uuyrd+uWj5RH_71JgkZti<&PsEU?ArKo#G{|Oax{w*IWy|YA=voVXxHI?wMygn5$ zgnCk6(<`B8Cj@=~sHXenjwmGsk{0v|X3}*BlE@5su%mz-!>y=Xl{n_2?0Z0+oYgkW zo}O|XO5gB%OsMNct0MuS(kaP(r?wf3hUG6Yqi&;|hY959MD_wuM<5MT-|EI%Oq#Vw z)1-itjU^a|MXBR0Lp_aFW?-{sxX~UEsW}`ekT+mKYI7D8@LXlwIwu!w(Mdi!S4opD zwaN|dS5w!~ai(2uH5Cf&E4f=zIBK9&C)q^1*Po=#z^G(Pwib7IKqylP6x2E|Jv;;x z1hOS_NwEB9GWHpN+?(OzMNh!l5^PNtf9}uJ3UTUDJ#&TfQk4i1-i{WnDIJzV2Qcc{ zVkDzbq>L&J!0KMV*ZFiVsR5ySYZd=|H_-3a%Ac`9&$pP4u9QA+c?ODtH4+0_ zq?!u)Z}4~=#KicC!f8wb`2t47pDp&$J~yPX7ve+Zy<$38lMx{`z~^W)@TLA%Jq?9< zIsTSl)?)$K@7uAlAZVJCIhH-?lX>u2Cxgxmbn3YmK)29NcE9mqv4Fwg4r|axJd1%D z=a1B0mV(<nrzJlUp<1yXXi9_NcCX1oO!F-`i278$NwKXnT^{un5a})kEA2_C{>U zG+s@Pex|l#Xj);Q3@zi6F>_GaXZNxqPn^FvU2MWrqbz&O6!sB7Xzuy_PNzM5#)&u@ z+_0m$$itb73w=}$QAQ4Kbyk&_MXM*ueGhw z->L3jpU;xGO>RR)2U z02sN-byx4Gw!_Etm`e?}2;N^MrNTA>>_vLQw3DI&?dGGNs}>pL>J&Sq$jP-S9EFj& zLe=AW6WVpBw>hQ*W&L%tPY4NTDLBD*hc}D6ufMn1br}GN{?CLes2WK_E5|?_s%Y{b zj$qz;l^*ZaDby+C62AtFv6L- zFawwdF~lnpRiIbkj8HakE%L;wa|Iyj3}b1OO*?Gqk+-k8hJ{I)M7}r}MeJ!XK=xy2 zh+0o3`CUK7sJv@YS4}>MD}GGJutyNuB>TC^s|U~ssM$dQ=DpFFoYNpw}T;23<;6 zpGew{JXC;g|HsC9F`Dypn|asCoFPx!e$$^IltiXVF`J2EkwP$_t0Nee0rWCZ1P0+N zej`Un0l{1! zT%M1=jhAtB8QF{?OjHs~g{+P`>Z8t<8uc*uGmw1Iyck+OHE`YiH!H zYxibe_wVmjz1fdXylBf$1FN6XZXZPyd~>eV$*ueakAy+ooh4*O`xh`|Df;XnN1e+QAw<6IUl^t{jyTl9)R-x5 zB(2_FTR3%6d)`}NAZc!VUm zj7!OHRzWFlT(38WJ^$X~>*L!DHOB)pdz6jNUS3PN{(Kx70PeJ~frFua)AJbqkJjRk zqv?-{%y*+GxTw{M)ZpHN)rI6cvYJ|TN<0a1p(l%jq5~x=`APH@kueOw+b-@W%Fuv& ztFKsN=*8r7=oKZipe<7{J$8_++Gm!3G-Ks$>20VEv-f zVpbmX19~+~!s#L8pN6?U;Q4b5n7RZ0Nb;0|&V#s#ldim_9-cMcc)2_C$$5KYj{O;! zJJ|i-0J=6XNSXiNVCpa@5D;OifhQPdDwZe?9N=%;tv0z<^e5(^7$T{2VI4p zrG!E~ABtIn{49Ps$?tA}rcB4U1<=8FvVWZYZ5AR4ZZhNUFQSOa2r{WW4*yIP4g;%k z#gJb~W|T$V;Tf3P3SsviR(+aFIc%>O)bGJ)=lk%UI54&&V?FolwCQ`Isz_`tP` zwZ}Hj1}FR3$j8rk0#bFXB5c)}us_X!Q}^uLl{72Thw*nnzesUleDn2C5HR(iYRQ`R z8Dh%NH`bbx;8hOkyp$SUZb-}|vFh>9^0*Diw@^rKCNAhlt5?3YH0=GW1}}nv(UFf` z&ISsdC@unS8p`sp-NPhXBaS@~yoI6H6g!@p;pr}pe4Sey z?2ce9V><+;{|CF?yeL4JzXrIhCdxd_+sci20c*-#H83VQnpV~d1CWu~J<&yW9;#Vl zti~029ABUOBbumI`RBS4osO(1!wLFDrV5=@6geF>jpDM*DMTe1>969V?FUn>o4k9g zco6l52yx!-iGs5%0>(xxFMp=?5K3cZnGhC|5&wKhDxtz8z7euUsT7&gJ8}!;RaL$5 zFAe>ksC57GZpLp3EkOI5Ers6qC%9TSUViq~3Dj?C4U2@pI_@ianb{YAensnQ%j$^-Jb-Z(NALKgvyn34j**_j z%h#6sxt9m5wBj6|5po7~cd)@;&I__I6XMCfVC6=?U zrB*9wJ$V8fRs7i{;Hnmy9EakMu6TjXT92bw)p7J!0D&aajzrZ~PNjI*sD-8Xuav&B zKA!C)=zPKzMUsPlZ&G6Yq&n|qZ*kO!dZQ~O6d8Sf;mDE#mug%vjq(Z<7;&+q zjnN=^6ri>7_iBkfvhx8>0!c`Ho;hW2{~BhlUVRlyr8cEAD^zD4Qvp1ceXt-CU-cGi zWjITbIoigY_q)xp+W|(-><0S^eiJ@ zkefF2iD_9{U{P-53E}2f{S(D5wdyy}=Vej>2 z#SXqg;R}DG=?mC{q7o6Z8PBy2zoK|b>rh2J{1sn2y0{Ty!ZcmC_d1Lx90h~fB#v0r z9`uU!m6i2;K>lTJ|M{psCNcbPxjYm;V+L>`$oJ>kh2o1w^+)h$!f(&;Cv}S^kc9B( zzpGhZpy>n2zWZ<~D}uZk#I$fWuWQI% zi#|PMj_xK}P!r&8Pa~fVvPD_+(mDuTns9VlzU8<(wh^%tP0MUwez6=-+cpJXqXr-q zNtdu9N+6XDupY=b$s z^txf!r>|lqa-d4kNzp1lUt2qSJ&pbs1;oE19cK#?5eh6DRjTZNR!@tb{kvzmJeaZ9 z0HQ-vP<$)q76oAK4E9{GO8f9yVHf*`YFyO5M-(ls zhERRCXPsBd2wdVPk##)~4?>FpdI=q_o~Az**S8Wr$mw+ktF)CXw?*@EfN__aloj5BaUiX$3eNI^Wt^xNM`F(%5|9EC%2E)9=Tyu0_g? zYCQ1@0@jnqWQy{UdYG+$>5E60q%L`9F>4|Q zx@V`4IOkhFFD}2=xvj#>KML@O9;P1h74-CBij-Qfb$>oA_?EyP3|K+FkmgDK z7v{b}_2b*{mss!HCqe1cSfAHKO^QsOq4ijM|G?mHLe~Ra>!}PEe_SYG>fOd4XWIvi z^Ha_W*L^zh)SSD|g@0wT&q25*n#VcKLFhyCR~CeNI&?5O=0S#8Wt{lm`HQi9I)8ib zsQc1iN5qqeTEHvrNDg>{fHdlG;bno|*f7)on)`!bp68*gj+n_M!^^5C%JS}$`h+U% zuYnfG%3+z^*e|(pW)Wmit6OQ}7;VLAGCn^EH8{nd0_hkC9vocd>oVi?e_5S3F<{`UMONd=~XVrBlJZ{KZQ#Hfmio!DQA}_AY`Yl;--< zL0k7akdq03)^iogBa}xbbP@9^VHyptm%~K|4bqO;Qvqk&8*2cSQQgNm3#rZLZSxKT zjcZK*DP*oI%v4*1bSv^rQvdY!H24eAK9b_k$zE3@wJ@gJC=D&CC*FiV#s_)mI&e@0lt_}J4f5}; zEeA8aKC4%y5Z#c7n?Oj*A7|7CLZOQ3)(ITek$`tbApPzHlMI%JKa2QfiEg08x#|RV zCbzt3)WI?`S99&1w(c$m6$%=jp5a^N5E8c&v7WxJPKQd^B->?si8#a|KqPd?d@Y1T zmkRwf8m>1$4retQ&=JpSIpLVJfz@HnimO?jT9LX?+1%k5{SEh+nM^w!bkd#Nlt3=0 z9e}(3=_mz;!BWk~#KYWq6Xs1BsP$Q!zAwjLM)H70h^=yO1InPB=ufEOQ1vFBG&o1x zsb18NR1~kKLqUW&*1i-rEJtXoRkk^!z&wg;-)G~HaOnWaNeA7O=5c8o4@DXmLPt!K zo^qq%{`ax-xnv5;KNwc+7H~7SNu`=#*8qW-O&QA@b3XQA9I@6cNIis1@N3R&{P(GJ zYFT(bI7DIIEz-)b%9O7to21mOgQoGXYLR zc$wx|?@nt*MnNTJIfQMj)qe~C8FBtv0~EFkGF$mhayUm>HBFyWnF%%zxm=_rA3xCR zx!r^HEF#cwk-cex!R2aECO)+?45rJvu%lm02L(VYIgZ@`|cBMsEe z%~gpX;|@IjqRr>^E-Cy^PhspVOVKp}b9iSrpX5N_(srOi)$G=Cg=m*y_}HW})+%R} z5lJ|C^-7>7;$MZARm-d5QP(;Ocj(=lmPVcILcGh_o%g-+zg8sr++T%3k1Y?XT7HX7 zzjnQ>^)B4n3kV=$ahUE4QUI=X7;esJw8CCJ>8(7X)MB`a4}AZdLbNGq+yj9E0onZnV{w3hfb3IM-N8^(V{yS@0CG6Lk-Bec(=wz~ zgW%;6oQnxn^2No&B+()&FhoobY__;^hM!SHRDQh7x^}$f5iW`DR=YeNK1V#~^I-k{ zEN+iMmR`;Ju5cMW8qJHJp!eacdnb@-XW`jQv=zaRBUQk8tp0c z3(la8iN%({v(z!CO`4(C=V-!NuHaD|_hWLGyGl8om#h3ScAyD~l86ov3l^_$Qb(nSL;i$bX4-x)r`#;T44cm6Wxfe`pvn3{)F zs0qL6AFU8%2as4bQnO>Ca`5teF@ni0m08M1l2`G!7{this?^6^`V)nh!X3%7?T++T z8I%`5N<4I4NK}+MR45qB>fSk>p*^Gs6#CE&Bff)OmL@07R?6U0u(JMDv(S7R=XB1j z+^@yJxDu%1JC`MIdyGp6u*2zbJBMpRI^Ga6GAu^|77s(5DZ;BCUHM8on1TRhr#MZ&iA5+%@^Tr$3uRViK2-JcAB&# zD>2EpJ?E$cA;(fe$}0R)R@vT&2!6s#8Uc}b2Yy>3*_E-c1QSC@E6=My+J*PM%MhQS z{Dt>78t_{ZgyNcXCRHgL2P0eOeKjO|k@}lQ_7JwR2;AcmFaNdb$~}A5>|B zFvYS!^Bv3OnFk>?>q6vH{IpbtB+eDW>Gqxu`!`rEAa*hl>kkrHw`>7!cvc~~`54Ye zU(7ImUp0CTrM_W2YZDOG=JHc`QB!mgt~yAaYaurB=4X<}l8GP^>#QqZ zFFFaH>n=>7z@$h2R@pdT>Xs}aHB{MJ=F?32+?p3K(vl{aCX-ndmV8kwD%z?#)J!@k zqPG({726_(;2*B}HS>K=ddW1%*JBo>3ofTW0GnA;~_>|476Z+hPFwJ`&jE%IfclH#q% z+$gIhI)m~hNa+u;zAHKB;L@A9@@tjC#EOR&6^vJnME5m(wv-ZR87igi!^v4F+v)*W zl|KQqx4IISncZTlSg&S4A1_>lML~Nw0EFOqo~rziB9~%NW_%Q7=s!|e13ozCOw!vx z?ZqU~lzz@4{8eqH9x)?++r&x2w`Xvo&7FV*G_>U5OSUg9hjciR1;!BSgK$3Ur{B&; zzO(v7m#VDGEfj_Skdey4rL>28jwcaoZ2HS6DSPueB@rQybPmRaMn(zph@5;30|c(^@2drQ{#5GwnKBc&&4Ad}0Y zBzZXz@Oq4RMbH&G#Xyy5R*x#-9TJp%=$%qbbFiMORCO}GpO(Cs6a{*DgJc;z>b59P zJ?!mm*dP2nW$b}}k)2||uDXyc03~TY9?5iep2|;y!&*W2&nyc>Lq>-x1Z*7)+iCst z)-8O!G8d{*s;U(X;sLQ+biHmzl>769l3-b&6Km+n@eA>J=u^^a9C3Gj(5Z-my;+Jh zlp^XIzKWCn##on!OER{{DX9{kw>j3daAdE+nPyxv^}0Ph#;I3b&~ z;iZa6O6%Z(NI$ zxa<(#?z8D!fgU`M4sua>dC+8*pf^mj0J}ztR?jFJMh))^QP{h7lxfnU)d}3xML>b* z+X9iXV~@C1hd}^X);As*!1k1v7nJTh#D3`er#tn1Z3r0dE$k6!-gQs>r3AMoN3ya+ z)(~(NpE-4cj^+ly8O~_LxMDG(}&FNXa4z>p4EZwT5wLUeph6c7+r>OL(vI>5}rncm)GTHEWNn%3~Mn+pa;lrX;*d4sguQ9hd6iv{2SH2}4lhCtMkSV*dE@Jb2?X6^T$(t8JKYp7 zt`AK6e789~@Ib5cdp8%;BOZF!Y~C{4l=DC$;g~0OLHPRW%*>%fU3h%pQ@XhI_-)~H z=*EGC8NkPE(1Wf4H=A!mKD>h)3hOZwm?lL$KWR;O`@sL_gc~6PXYw59nLc=gZFQR|fTr|!>I9n}NFK*|MdRVoH4{GWeiPZDJUDmUnL2q5U(OXs@ zwWNKr#wR1HUYS)QIR5E`a^#a*+!}@cs1u|5Qm!@_8RT`dLv#o6j&NkFwrMuYF$yF; z16UR8US3`0yNAPNm~%d05dWm-zc94A1 z&Y&-lmIWLWMi_=#d)jp@Hr;_YW#cNp0igz=w2}dBPzqHZ4JH@TQF~HjE9FyS>0#ih z0TTaEIMUQhU7%rOJ~QB1u!F{?F<#@R%)&}`ok-W6?c4cJ^T+qM(_FXQpZCv6H-x5; z{#b^!JxQ><4O;P44%>`U0qjm`dV!V{M(|i@yZw3}{k7P?sCv~#1>EVSXYM1+0K}Y| z8{mArDc3Bw$7w@xA`!MG${VG?{;RJ>f^7ol1Dbdn%j%S2IEfn?d<5H_XbQL4t#Di4 zZeaw2)Oau*(m*5f*um)Lp@k^k=ax9mGHOWp4#CW+Of2rOc&=@K_5ZwUUUY=eqB9#r;Vca3UFG3&EONMrBgu?{6x#KqLM}`nnvx3fm zVcFCEyaZUgV8JjS?IJPNlrOZSz>XfPk^U!6*UKWu_$3!qG9mXr2H0Ym_AHK zJ|}r4Z&6QJywGUhmq|+XhUC3E)1@-6x^*TI39%y#M!67O0DM(*Nd)=hJ}YlIFGQdK z>eSo^#A&(%W!f$+nb@4jJ9N9#jAIW!X5TvPfkfTs%|z<%Ly)&rpTXcV5)Z{aMtnMy zdp<_I6^aq*uOg8sb0S3+Y^jw86~dkk$}=G~zG-g}sGy)O5h|*>3aIP=#OngHUE7@9 zb{U7!ZM%gXK;=3EV@VH$9t-xkoi0#Qn?E>Q`iV!%7Zan*WL>pLb9SeLv(!lqJw`_} zq7+m~Oz>xxCwCvuOV5Pu3D|xwGn1Gz$VvRiy;_+vI0-2I!Zv6j+p!Z?AG=L$WS1Zb6Mc-yv#;C=%@< z2vqNUUc#7Po-U>1Qf{1Pr|yZpTpWO;U1xfM0|rQUw%=DQtaAP>a+z!Qe0sznSxWTj zn9L;-fHqAHItGcQkclDmtBv;sD<~r>)F6oV*8;W}!7q#YAw3K#y3(>f4gyv@FyP=I zHy{f{gGgg~_pt>|Ztpui$$A4*c$)lhzg>TR!39Qr)Z#-}f!POnV~t@PXMVy)_Lymu zA=<*&SAgb^&N}v513ja1;PkbcsG}VoKF8M{facc_`1skL{i!br*^rz)fo$}vOF4z@ zlX7hzXnE8MDBb?6|MUo6VIDv610=#{$^n_Zc1$LrXeCNhTe!%A`2@L}mrSt`r!RlD zB}Qse`Qx3j7frI_N`qn*(fV~~_Iq$0fsCWzRlhI|Jjiq} zAYC!a&xUz|*1TA_8TT`EZFX1c}J;7@lQ-Z<`7jVw1X8|&!>3%NYIJaW0?cbt8xwC+;fbz09)VquE83BAJ z_&CsVS8$Tp!KqzS(g{~pXi`Z5_GuD%)Q**YA#=($nj@1P%n~B&QQL?LYgi#57Fj5_ z1PCnIQ|DEMl>5_LqnCTj;-!|2r#?H5$&+pC-h0a0PHt4r6kT`nquqkJ%;ZLH?UnvX z)I+@OxJ8{(=B$ry*d3!?v)N3cVP*e4nqUXE-^{V2Ir2t!2RF?H0L zZ-xWw*GFt7q+vqVvewOqrHV{u0@25D_nvL#w;L=>OEB|vW|JomI)))f`1ih>X9^p< zm-P9YS5QxqsT&}-s3aHWW6d%!-UHYT{6MqX`0ln4$f+Y?l@d|$V%xZ<|1AJq?hn;K z8%?XD7WbjV>0sh^(ubN2kj|Q(!*W#SXm;2Zs-#@T|MwjsT-o3?wWSsOcLW$eJmMEW zsW=u?AdLdm(cgW4t*p}-M(Fer9EF0ciDtH=97o(|jYxDi*{VjEX7MnaE%DCWUtl{xy5z?wg2Zlz^c4=a@{>?klq5ZE&gv#Xwq96*hvsM-X+Uj3-05Dr9^O*pSE(bQY>G>^d5NMJ zMwg_#AYBh|+Hy$-BesqCoL;m2sR|(z3`qb>2#m2bHOIyd_(IzK6OP9rTpJa9=Dmv1 z#-qaNzUo7XWM3_T&*0I3^h5|z70<@8u@k2Q+$halr-%UK$C6xbxVq%w;zOp|4OGSg|zRs2giTr~k_ zV^z}m6lfpuRbzr-CQfG)x&)f0SF3eqO(#`^B?t~pq_?giz6IJb26&06Fn6L9rP+TW zqj=zK=BWa<`4PK@;bi>VY$9j%#R%Vtr=MDhf_h#W0O?XVKT3?!&Ri81-X@Ch>a`|- z;I-*fbZP-6L}?gMH4fzp@E_+a#ZO7w>dYo~TkrMjizMRy43$bt<%uX~D%YybF=~q} zoV-~4_dn$zcXn3xbM++kdvfvtkel`9jO6P0b^(p2NM6pGE!#CApei^)Yj_3yUeB6$ z1~tb8@OO-Y(RB~~F@J25RS;uRpZ{%{2H{x69b4v9xVWqtCf{HvQ>4IRs>Q~T07N#C zwQQJ>5`eR|sIB(9$VsaGqC6!V66&>F<7Y+d^OWSeCABb>B-2XPu+=V&`AmW@4jYTzfycw#1v$(%Gc5tz6fsS29 z_oA)=ot_eN^|AkHS&I{OW^8IIXT_kPLBWTx zA=#n93#82kRBO2B0c>C&DG=4^-YTJH8g(8i1DXpd-zak?OAZss^2EERKfn47ta5G@ zKmmhDgX)XhIE62h&j!`&x$8>YaD8lhvY4WqCITl#x+=ai+){6w6+jjGnOfexuYdk7o)ZW`F5-vZiS6jlt)m4~EC!UDvgxdnrQdQ%M1O1;;E0+i zUr8qL-uBwk*AWw|nalGZ0nWk9=!kLlN64}qhr4duWsyEL?SVFU97wcI06-&?OS%;l}Z&HYS zIY)^KduI_X5bkQ*n&W0KRa31{aht0UBQ)Aw)+IbzDT!KDBzbX|EvwnYh|)n!%|1)h zRXV#OmniRj&Y`77v4v*{Pzm43YFK9xOiCj>>I_A^-VYqd%|TcM$P7HnS&}nB;B_53 z2G5~20)xFnH*d~p)vl0E^2s)_)-fxe_waW73& z^Mb$KLRGsQ=Qr;#vmbQTrlnMU&dNHXFPBQ~z!_0gn$xRK(=}UgT}-%|4F7R-pLEu* z+Q(Sg-eGIFbMoGTQ>1m_ zR*H<_Q9N}RvpLz`C``)FHw4=%ZsV;H8{ISu<~nm`iEXE!_Q8%*;6m_@J)Crxl~I%VH%jW{~p4 zOgkLmbk_z+B$=e1(7(#GJh*>PHvRQnmM^{7(wr2BK%OLd3LW2f3aYsA^EKYn{YJ2Rt?sE#q>qCFzSqNawbLgeWHwemDSS_WT7 z!YL|Wr8;B;P?H&cIe@OCTx;CF@G09tN6@lZdvF55Gj1w;w4*{rpG_qEP(8ubU^eCP zq9JoDwb;8znEk!#{nvTlXgnOshd)hcXM5m#B%jT8`p@4#;@sBm+Rfs0gAGw|`o^Q@bP6syyDUZ0H)(`NL2Tc0%^Zs_FKn_vN3_L03<4j$ z&2!L4z?putsL%_Tt$WTv%i&G^>OggS(ky<0vXcgU?aeLmDtZFzrv={0#?kGuptqbv zSI2dQNi1D2UoYe+KAmRim9ebDM2?z76mpMeE&CD?Hz zc)l`9lw56{ZUw%wC5nLskBT8$iS5#L$OD^ZfR^fu`O{ZQZo#L_Zzta8hCD9W2-S8A z(7DQ@AmXUi!*v{g|MTtv>bx-bh|azt#$2I3pV>xv%Urq5@LIBEUbIGhXVs5E`1i5_ z*^n=Y$i55(I29zlTUKcZ#F|?$4^`T#5>RN$MO?%K1A8DWcVNqKq9wzsp8dO;w+l!T zz}E?)z0gP~;Ok>Ihp$xtxxoNRdt6}cbmGeq3fOO~MF@(QV>?$3V8S0PYf}^i#uFdJ zl~p)746hQI0EJlX11;EDbc;UPo?eHn3r5r(?y~=e^;ql+7a!);kh+MGZU!G?c>4;n zSwM=SvC155MQ3)swNucJy&7w$?M2)SU~hW)=Ax&3SFIEk!RMI8#Vm1Dx2hJDTfQ_^ zeCk$8Tmcm2^H%y_D?X)dsdN@<%x@63h6`TeIWnTjyTL(st+t9{k}b5zT$BX5wGeWcmLZnA`uNKDIDYV^OYL#-Wr_;x=Q&<@VU zXepQ^^?*U}Cei#g;hU2_3yKv5$haIbc(u^3D*$RbXnR8xmRjD$Ij`zcc_Ooss=0P7 z918)NI;K{vd!;Jo>?@3jax^=h1!_jT#0%uZIN#t&pgvfcDR%kY_CS4?u2b@)B4AKm z>TBwZbmN6|IaP(D<=$vz=BTI!`@s0f?oSF@#)JhLkjfgj7V#>FN={rMKudP(KRo$* zLcj>k#=1^cdWy~-O|2!s>eMgkpJ8Fi zCGK73RpC>s%(0&rkE~L5?qydBOp47vWLaRzLk9(%eG*8`%r<2&HFE4ieNWwU8a-oW zs>}ge^O(GFiDqW+8YH&1=g%XbXbawno%yvm9~b60AvG4~GOm;=GeL@TUBdG zrX6a$HxV*}spnb@9b_LZ9Juye*lOrL3T(kib&R|te>59xrZXj7E@L^y9wlb6K6&k8 zrQYvlqUSP?)&&_p(!z4pMW%943+1H#@4E`%{b{lSvfds0lo-d9~z?w;WRyOro@_d33m?>aNPs|~5= z`z4gS@S$B5+)teqNBLASOA5T}%jmQII6JkQ1;5(fb)@)(iJ*Dx?K^yS9b%R;hSdQG znL-qj^d@1gx?d01q6g|Ak6TZwh^+;im_E7{Gad$BH<4~zJmj`VoH4WO89HRG#nL-D z+1dF#_XzkF{ZqE?Kk&R#4_$Q6gfooyZ*w=g&04@;@B4GNH0y4cxtHAY0Zr{UP<4N` ziooZnhWBZ#b?Y7&)wZmx5MUNZ@mMR-1ZKjD?zp9t@+hYv1;A*+Uq=QB+2vnwj)rCX@&oa6GLKmy z`Av`x(vx+&}#bv~LHE zh_*Yiu|VhVgIJHFLr@9Fp%lV=jTBHfy~?mrNZSry0$%|RJ;nlNbcCDOY}6Kcx@+lN zMDMsR!m@4qa%7CrloB@@{`1w3n=k<>vGVx31HwbVpD$nayZ+#d4?Y0$UmymVB#B+8 z6L^-N`~18LQ~e-M-m^$VATBm@Z8P4#2UY5H)mu=eVhT%oj@ImL?z(5Ze~}ribpFB8 zmI6#A_`{_dORPgMq6%pR8$N`M{pyp)6IR`H=Y1YiADf`oN*0t>HNMxYRgorzP9^hE;3fjg9X_{^dMal{RhTfrJ1zp z4bR2)5yr<24~ru-@NGUaw(3Z z;a=LwOSlS89}#QFc7SKWWn%W%&Wx|5uQ!I^ps4#ROIF*{X#@f0QIX#Lfk7Lhb5qK9 zLcoMPME@07I4b6Dgnv+ul5tZWgB|Q2jUi7t@NSjnmzY82oel{rU2;##52sznqki4p zLiVCrzYsU_{__S<5QMbKS!ktyrab+qte^DUJAFZq-3{hDzA7Cj1iSAGz-{Ou*N#p} z3Pm_lA$3N$xT2f~vueao}# zl}DG7JhnP@IK>2SqM<}>!Q6v&1awhl=lq1i22aa{Cab<^g`qx;ebN6uK2H_=X5Bb= z%1;d7rX$JE-5Y;kdmC^_^&#wM zq?sH$ueSeXwG6WRJ?HbOsI>R%(lxH(+x#TxDGwOF1dcXgCkg_~@~v)Oh;C>YNm$N- zO*ahq3<^jT{p=#{?(~wJr6CHCBLHix;WgXV1?)p1xA3|D+g7*xDHsJGDaRBUUxI~I zzF3wjAZ{Ko2qgjX$FqluR7~Eo{MN()1Ml$AMv(JGUP)$@hdICq*Fxro@?r`f8Vugr z&pvYwW@%L9uPH@t`;nVweLB3bdQ|gV+UG6?8dcI?dls!8TrMWsrtM9Xf~4s=t1nRB zOFY>)PGzC-Ho54$=t5CH61IHgr@M#X<*nR{Fk4#`y_<4mGv zZ-f@aZ%o^E7&3qErF5ici1J@SHi3g!an9A`|G0X`;7p#V|2wvA+vXWiVWv2lsX}@vY!wuCa z3_hs0ihxR&p+3Tb-IcV(q?W%QD1^*fX(^nWTd3#lz-MjG!SY(YdokkTTuVwPr*1S$ z3L7l+t%67~@?yO%W2i&=7=;i2sb|Zmudsf@5P$#HU%u>B2%T3WpfCiqXe+|y|5RD0 z(bO!|zBw=~3!H#HS3dc>_rRsSx$0K)R*e7q=qfv^P z7ZtR9M~-OZ7Gg;^Jz+ojo~3!0#W(V%onzi1LQB@NGy5#`YwxX;r#BJp6hqI)9v+6n z>X0Q%`z2ylxcLf~pk4mt}6an zgG5O|CIRY^$hm@6vKNA^okBiy!A7*!GU3cr2MqY>V$f_<*yqmeu)lDTN<{98s!lwh zHqO;^FMrDv&4He5tb7e=sA(O9)K$Ud5bn7A*esgxP9zhE572xV*^xL^0l4PQ4?tg7 zI^T`_)m%okKxiD+z{X12ed9j{6l?)Bwda)LmgUs z^G1%z5{G%9qLJXRCK^lc*rN#u4=zfB-A|LYO9Cu0p)|lRgsMx1qNMl4?-Ql2*ucbTb&Cts)T|pa3MD;@_w(UhfL1up8wQ#uu1NHmNYR`kT9S=%7Nk|Q{t?A+Qp<3{wQ- zT0CQl22`|^Tv#E!hXoFd3-O4))2>5HbeRr`hnt)vK~v(sI?{DmO}oDK1$FDCd>uPR zRtHq8hHR-yCy5=NT?2v8=zr`LuHWXB05E+K?YGenWy7KAlJE*5gkC578AJ~76j)5` zMYqqq)N~_R%2LZ9u_At|RmIYyLBsM`T{wG2V@r`(-`QwS!*L5XBQZi=;PUP1_%2L` z=#}*&^C*jjp9CrB_eel}T9W#56GV!Oz>N$_GAjm<(tk^GQeeUROTDcb>X5d?LHkbw zjK;_&BD(H;8)5Ra`|Ra`;cQI~zFhR1!V3DrvftGVOtbSIE@kRN{!6e7kJeH8GuApN zEBB5h$>BzEg~!J@3|nEtQ!}yW!Nh{DXH@LdJ_Z@jyk`dQ`&Pd zf=2TO3)}>T9Dw~e&BPRMRYS877%HLMO2)Z*O5{jo8bk4EkPKKQhMeq5u#{iG?dqQ? z(Vb`wjAq;rtQFKR-J=<3!_a>_k`KcgMH7yFcojzm3`C^`xTHDvy9GjRqwYR>2iTkugA{KS}cUtG@ zNl`s)5Xb3Y-yH0%@81vjks-WcA-O~Ve>HLO0ne_#Vn^Nd6aa~T75wyM{_V>=E)W86 znMl4Jq=7d)(7?(rIRNB5H-1Yuwtnk9gYcB>{dM@RMQpkF+=>AC#W1d95<*+l0bW>7 zLjDx5NoJf%o69s7>M-j4l?x|L7T=ASdlD#-F}uo!s})Bn zuVn5S-_Ve$tZJntBGXauhO?$L+`Luh%dxbs5s@DHZ5UYind_dG1lA6%m5LTe-TWBTA}`~_xct9TFiOgY=uIl6h>O1! zCV-V^%Rj450iON_9yvR1zrrF(r}qUEBMD-?{mL1YV(1GCJv1HO&!C8Y^vt&iiDcNT;p#PNP zw8ASzVEu3Wd2HjgZb>7cNR8f`0b3QezWI$e&4nd9HENG80u+`tG45OXLbDew>s?|ns4JqHBex~sB*aAC8 z$zD0ZW%fmtMMPDRPf)k_=qcJP|b&{P1|2x^DK1e351c z1JBRnhJB0?dk!aSrkeE=wXeh@9a&qAb8S=A65w?bg4__A2t|hd-(L6clw04^S)3&3 zdOOZ7e_gz#BRxTY4R|6g*HTv3C;+J)1mhNvDfaA2;bvgJ`+j}IvHZ%ZGvf=@i#c|B zL`s50RVAh^b&Ni@M2-Rqi4{mUCbvmjA2}!8=Y-@dmMF_H8v_CsGpXS)w{vG!wXA=+F}N1NsFb8v~|mDZXJ zcwrVO$f_NN++0UVc%~6|VBO21O!-op<>z&@bB0bf;y0O_Lqy|aokRu8Pg;#TtTRIG zFod;Fdd9lW9l0-m8JugcIPoY^^D~@QB|!?!&fBY!U6W53%PuCCm&DNlR(HM%qrVp}QXtBD z6_XK8YM3zdAePyFlRvNzmiIe(p+#`_?Fz?Dnf0WYv%ceoyMzE%4Cdgx=Stc=jv@1;KWaBueWG1#A&~MpQF@eUMKH;Vrc>u%)VZL;^ zfSs9>X1qL#)qhPl`1#0QqEbFhPb#k)g{b0EHNNJiSTvctVkrk08gwsYd$z-vVpi0^ zxCM14Y9=u`4l7)%45-H1RC~dkimcU5rQz!L9`D?A$E8}VmMY|fOHu*QiffSL zjKOeqp5#58N^Mp=r8#{|^{w<@r9%N8LQaC@%g{;}p>ZI0QcQO8yIUML#3-6>%co6K z5gWPGE_94+U4|kE3vu}!kYF+z=;X)luK9wC9#{8|ZO_1LA`*$xhsG0fjBn6_?UN>- z7IvLdv$MjJbn(3LxX&Ae>lv%_jJ*BD9wsr=#NIC`=|~4;CR$;!a($`tv|T_;xQ*E) ze?)&g_Ol`2l^gDx*IN#4Y*l{au)byxr0$1_ey`U~2I3DMSJld+@#Y9FHM%woOdj&L zCN(wKs&L>BtXhr(xcH2Cy|iv~r!Nvh-B4E|nzs69sA|1{o~(@R&KX$oL~2x3$CYvL zw$HLnAmIbFt0^%>$ZTx@! zLC2~AOvB9C|?4C;kWV(x~)bKoi^aN zXxd8_j<6b7e~D6MzmJ`^L1dw{(Td(hF)IEC6jO^Aw73DF1^%&|wcNUeA!@Pa-&!ft zP@}$5QLv0D?e0R%ODW-JpPCR+bO>3+5Tq^h$)VIIB+t>AFTwgfquXT9O$1J=#7=L3 zj*zPpkD5bV)R4_qN4iX6LWks`2{N$fP0h`e+^H*y4pf2o$Lmgi#eRw;Q1zE@98EYy z_pggOZ6l53RB3vn_U_Nqo}6B3m6yLvgypF@-zX@avO5e{G3@>C_dTC%dj57Nmpmbk zaP3$`w2z*Oc#hFxL*d!3Yju{H2Wt={TcAlCMOk9!*od9^4&wq4Wr+oAcFMp`ZF}d0 zUbj=(D^mZ#tSQgOYz=tB>04b1#^-R(Co!oPvI)gcj%5A-gQDd=U%=uN+_pd5pnEXp zMD||>6v85(THjR+P3USouH4q7&z~KunqjLqn7o1^quLA=fGIYg{TyJ>-S_{a9g-G( zUD`wf0fB0Bq(vhb|0Dtbqr)?|V+Obw+Zfq8 zFdNufGZ`AWF3#$>toki@|zbd zem$@C-W|puD0zVye3MBruVn3hG zJzc07LL+{u_RueNxlP-};YC>~9Ta}EBHi<0W{q1``k@KW6eV;RdfdgCQ)M%GBV5)F zd~VE*s;*l$4@$CUqp5DhWr#3MuCLgR?Ag{JpI_e3)-|tlm!*6rFM}jEM$2UpuM=Ec zF|VCtr{AqR3hdhT$7Mdq2TEy`fDV|ZhVA{PP(;}*@Wi-sdYBp#zJvkOCoAk68KbrKgj_SA$2?$uj18+WB7vpCc$V|kpZ1#Lq7Dc}e z2}xMRf@&Y#1v?_+2U6bAaGQj%wf~gUL^14SKNm zWRz6>Ya&LpAFN^)iS(xcC%Nf19>VNMQdlxRoD3dTRnm=?aj)NPl7JPS z$}*5`e373QlW!~I8^qHBjl)hCxh+A46>Wt0p6Z&pVG#w}sV^GQFJ&?3B#$5=DbTzO zo%r}o&}-OT`26uE%Uoc{DsI`FoBSaqf*pw1Hion!)zc(-*`p+@91VT(f{Vi8q|d@v z^9CSNCX1l+K%BpjRGEv&zm4cdJ)+ehvUIwihZI9!^tc}$waD$;EqTjw!nwpjlj-kAJaoo{g=C#|y}mb| zncVOWtv-j@z8l!jgj78D{Qng^Z}-rh*)#uUoInb3BqSA3P>dM`aYc56N&S!l13d#u zOJ(%(;3;V|5i`eUE5QlgUWWTeYOR}XFQ45&JbubY*@fPPe3|=oFWza{wDe078Gvh0Vtz}pKe2myLESA-Dr@Ut=vU^ohJiRl zjovZQ)Hv#&&Ya$E*B@o0fe9{C<%#=L<<;XB-Ga*kgv_V2PBp=Y)Q?RIn@Ehe8n|_m zvsQZXWW!yh4VBgayVp=?iLtO4wOZBCKmJm5r!+Kv zMM(yJcgAI6Lus$>dYsbYP~s*YP$nM1N7CB=Pa))#%4Nvq|LwjY;AuDa2^7R30UU(r zzwR+pnJmsj)OFV5iKh62Wd z3#==I5-{Q7=8;sisZ>%>l&pA^y=sBExNyZdk-@k)1bT3T)MZzHy;_l+q2Hv7_|Y~o zhobvkWt#_L3H>fZy(H%Ne{2~KM(4ZQcZ%NWC|&%v5-8~>+ivdbj2XTse1Vd4ach>@ z)5*Tt0UB5%%eg{y%8U{UU)35(9P@Lmv#u}=xxn1b5}lm2Fp@2_LkC=yxc0DtSqh;Tq)=Ms-0XStKz>?( zj)c`)c<+fVt5GJIdf#?ovZ*H3cxRiA%ZN{1Bhqw5ndzpTyz2<|aU1*rZ{XC#caMgdLWWETauR>`&h&Z+q6hrgkD=i|HiXqo* zqCILhC+DMz=!pqvO(}yY<;Z55@Io70Ir#gG?`7PMG@&kdaiu!Dr*i8omyabVHYEdC z0iKUY$r39~FKr>?%;xZp`AyCV5AY_e%MfT_zP!m=M2{ocfu-b+A%YV-45CE zvN2KPDgmEi`O%bwUBn_p1^xyYp` zq?EYp7$dXKvD44v)`gt*n-6u+o+*C#bZno=$+h1`{x?R|X*urfYuohNc^B|ELAoKl zCPc_xrDJB`DNP{LMMHFb5D!=Ciqt{#xGVAE*qMf(<*K+**_ia*dgS4SbN9YnU6F`} z!Sve^m3D5wxl!9Ru0~CANy>?Y%T)4tk$SAT?b@9EoebnBX)_5&!JIDeR_Wr zI&d^Qmf^svwuo)fBu*xwwFW3f75W}OtHsA2C zaQceQKe+jE`n|vIMj$lio$*vv8{l?({diy*JOgC$+ouVd=Mf=0 zh*6J2>%dv1T)@X(VH$VAZ5uCj#u=U=6uGW9P%GqT(2>wc|01RDHA9-`uUtW$cnj~$ z2)xxnxRzvT-REap9hyLbnjAQvL7svd_D)@86*7c`g$cf>cY~CUysGzt?ElN^W=#*h z9U!0Ik-CBTiWK4sV+Mq`BVRQhMH+X3Y>*M2?h6}hQlNLd(SfrQuRhT`0n~{k{`vD` zmbo&v>n8y$OMWT?!~K@r(*guKLEL_cFQ;l7dzSqk;Y}i6G(uQHad7uZI>RR`lYGLN z)9$O=PH*aICq>SqP;loK#;^Rz!R7nV=%SEpy#iEL7A0t6@M9o{KySc%YUw#!986}E zOOn$iio~3Iex>js{jSr@4iBGBOesV+8?tJ>Ce7s%;%@#snE~8U2dhiE_%~XA_XNl7 zg#D?Q#{tng|4D^3YlD-b$&cIPIYM&mzh4eZLcVuw__xMyeMazS=aUI0n@}Y8#`rt1 zPCDI?;na}Kv!=jj;^nOL&QelpjdXcvc&<%7w36q^dB`W@9HeSkN}5)|Tc0tv4`W7# zJNq3oyeciq0=eQw@99Jc1a1h_0&8csOUmr!@mD>l2TW{lyM*62ju}(ODTfW?raP&_ zYOdS7X6MGq?PgIU0Zl*4Qw7jh+sX~dvXFrS^e>rq76U*{ZFi^uJ4SZ&iMnW-x=KWG z>vSCne>w}2FOeWxF)$d(;cj21DZ222iNp2x`zob0ObSv8M993zm z%8Sh28i*fj+5;xUy+dMUPX5Ng%PB1)Z*?zz`vo*BMRd5-C|?xc{wc-z_XJG=W%v(i z#3`VDP(4KK(zRB3dj7h3K6nT-wY*9jj}R($dIe>Re|(ATC|^D@1C`|9OwaXo`Q>UN-0ivnSd=G}%K7YTD7@b!ib2hqrY6KC2^r2>6V+bQX zpPsK{znO_Yg8nTddTXR2cUb(DVWbr)`aujs_A#EwiLiV3SvtS~=4ple1b{3|#BS)! z0<}z~UQkwkG(cGP7oyzKZHz<$??X;uLz}bhRGal8AHc~SK^a&m%8Po-C22bNw z`dDa}wO0sq1Gl~z2fc$#w+n~GFBiX81We@E))5E0gJb>hLE0=_(^E2mA7g81feDfy z87d%n-#c^y z_`yH9zG-+cVl5qu6BxoAR`aJn;^D_4l7k&`?fGLMnu6+FHtbO%5<{|YXu@U05Zzba zzg(}ntJyH}z4%^jO9BSA=z0k@bnl~vHjciXT+AW~Uv>1AMmnf<6PQL0aPsGo;hLIj zS0>~))8%LW6zI=}^Y^UrfO?>+lKa{70L`;I&SnFc+xF?}bF77{TSHSiX~$TpZsOE^ zTgViy`)u~(DW1l>oyU)(VS@;msQ3)C4ldPya1EX+A|d3s#9K#Zl~Q1hMJ4Dq^SUQo zkrcMFXF2oR<*HlWy<8Z^!YF+|@QS%lHCXjtrO=D=k7FtXcx1Z!`8)8a;7*H+e+N{h zw9*yyX*A5CQ>~3|L7@O(JwDZk$<`HnRJybmeJDtZw6KpkdVuQ!h0ZI9KclI1Au3eXWRRVM{mb2WKwvj-+1;F1 zf>%*rSRq@?>N%ji%mpZ^yvt~ZL2Vdc;ie4JwykXZNFrY4+i=&)b5pb-j|{Pf0E)ap zK0e%Q4!3c14^4&sFDp6i0zWI^cQ@m)PZ9NRPB4E)m(hnQhsi4;KO zcyfN6Iked1ig$t1W-pXV&SiacIcP=QYO7GpgTL;^t8Ev8Qd}V{a0;y>jBN;u0-Kx$ z-KPx3{q%OpCv<4~dC~I2Wb-NJYnR526{IZfcs~F;JfOX4GP=KkHD&sDvVe4_vp?&p zvVcppY*B*uAq4P+jI4pi>Kgyg-!*^bLYpm|b@s!cN8!hk{DFwLe3%o#h%2xwr0x))B zO=BFyi)%dE0r*rfK_yMbBN9E6fVRto)<+OZ7wj3HlbmO9Z|Lg9-$*If26*U|NKgI6X$ z*OM3>)*IlgY(11Ds*xBsORZ$P5^7^qk7zAzy@y_+wt^T1f4bX#7KHdaHHLhGj>yGt zr^n+8LkSm}rZopP-B&~p7&CZ0ut`65(B}x>?nC-heT$|(-zkdLa5srAPSBvM9jg?! zbMYB!hF8p>paHPp6UVQheyEY|Z+i>%*Uqn$Ja3@tXaY0HAG;UN9hBD;cRR(M7N$9z zTx;mgw3J+g4!2Xf*2qEUax33%42L6FJwG8M8J2{eJ z8v*=$`|7g&SN=%(C`~y-hu8o7ucimlN__ZGTlIYvdcFbPrsYsBY0E+&cF}p(K2|!f zU}uy{@^K-YO@Ri~YNGlm?o>`_UdDU9pIjCNGK@1qc{n-I_=#vyd`2gu`OTpwJ~P#} zwt_%#PRCLW{8hHikyGa{qljFQ(F$w;4!=Yik6a`nFZJ_x^N zyuSVqg6W9E;-**SG)j*+Ywl_zTcYxnS<&9$Y|1+mitOBh-^9M5!&jx)$KcAcS6@JP zVMOWBWVXcV*~!(Bv3;^kR#HCT@CNiJIp&)%^^{#DHU7zfiAf5zXeQVwB9n?%eLDK|P5|yWUk20t zq5D`Q{218#KX^$FZDNY=b~A1IDJ3u~jCUy!Tsm^hNH?#T6_&fny59^rOmJq86Bqo= zjoQuCD|5qSz&EBoQ|x_fIEAv5s{OKs98;mnTZ=zgR5@7;C-=fg+Z)%f~!ZC=BQq!nRIcw(Il-hi&&JmE*>E`nl z1nSQkpBtysz>i-OC$f4Y$CYdRBx2r7^QDgk1u7iTh;M}!5!FOEWv4dKQIgljbBCwc4>rFZ~_z9_>>AQV7QYW$*m@KCBMX2ip)(fNFv!sEwugKPn zq*}^wA-xC6jC$R88e#tj1V=6@!ULTSx+lUX5(GLwj!s_+>vB9 zyGNsnst$}<>W^(0wq+%jj0Gzw13iZ`iC;~b2+W<*KI2m5M*ghF_{FIoZIpS+zhtRn%+N5t z5R#k_J(wz9>mf0}CG&yaTU3b+O{J0xwJ*=X_W;5B6m-$^v~i`X9%2>sV}a6}{=w?& zb1P(<4LV?*Ft+Z2+fagTdmy)&?Bd{KlUjx|zM@C^0F|wy#MbhuZpr%1klrg5`3Gee zZ-M3F>nk7BliVps)Z!~wbDr8CT}>@s_hz^4ZBK!&g2^F)L1SPgeWU}>Os86m4=Zl| z2Y<+Uyml?cHC?om1&dw+oRN0)6{XDsQEN8;HHk-7A ze=Zi2fdh?L`ajN&kRiXNzM$FTmj5Uc)#8vOLyD%S>+A`(pUFGVch8L3$w~Won zP%XN4W}t)@wk|Ncfcz&r@!Y(yh{~jYYiw#BQD_|B{L7xy0Orvvgqe#>JUThh74BOZD(^1T!cZh z7uhKW`oy5tKUFuacJexQC#te`F6aaU1Gk739-;D-=nvzd%SXL<9Xc&d@YSj+gqy=i zQBM3O*ph$&1g9p4W5en@$6tRawKF_j0hiopc@Tz1jfdn0yql=uM4S1W#aN+M%<9EW z`fw>cX83v>bXE~R4E+8QYsWS^X^SVO<`i+-oc~LTr!Os|izxd$kBzTBe5f*vN}YQN z&ZmRJC8)h#xUacyAds-KKP8Ba&{&viaC5l3=JgD;tNJT<<8KJdD>!2u5tcS-&n7a- zN7y)Qrm!q)a>Rpn%|yqTMxcA24)6Qx7eh=!l{jNSkryxgh%j-?$!g;?nn(Q59Ysh1 z9oeOnjsDDvS7azx8n3zajhp#&35=xfHmCrI2YMy}n>$lD@)dg*$RMOf6V=qd=(n8i zb44TY3gFN?lMeAE9%g6f;V_zAEqYc>>)9U*81xNH`ynNSMmED|214l%7^l6fj?{n$ z=T|VIcQ(6)DHr);ehvfGIba6@{ix@)N{$S_jrl;2`|j5QI^J4F!)G?c^J%=Qj$5&=1v zAzKhNUwMm<#>wPU&8OSH2Lps!@bYu!bYKW2OY#c`+l2-n!feZ>uEBUHSTl4o5hVl9 z8SPC~@IyRR2St{S-;8I-#dRb1ojW$CdEJ>J@vlX9KPKo+6$X9uophY{(Ex17P2#jI z)qjo20LdR3sK1Qk#r~p|l))02b_4U;=nqX_5pa{T)EVr@cE|1J;6?};^ZCuD;v!>a zG1#*s2`>vcH)IB^G+A$jJ0cGJXg!O>sz^806@j0sG&!LP$?yNz(Y8~;gj)$nb`$ZhA&ucwy2VQP35T`Njvr7x1oo3A{uM8Rq)sQ=he>6;Rwol!odgEcKsU|s zW&CTe32tbe*xdsOrk6NWB%8iRMj|Bd)&JM7VW|#$CXaeLsB!j~*oSm-k>IwboPV2L zc)KbYG;^DN3?Lsp7RN|IEqRLi@Pj9RVsnR=u`LyO9-DffJdZKdu?U1GbYxWEc#C*9 zn(K0dUQcVC0NJJ@94#I=wg?Bq^SshQSKKhR3*BcL<|?`Gc+j8dQHz*6p_cLp9KD+k zC45qLhbTAKZ;GiV^lnD)^hA!$+)Y90w|Pd$TT{I4ye$GQ+nqc2=vpqwT z-{LmbqEE-XceH|dtMc;g>x?$OUu!f?br;~&*fuO$A(B$SCW(`!Q03soe+K*_w%F9( zNZpe9z(+2?0eUggar5;B2b4iK^jskx{3z2Kj2)QweXazvel|%Y)F9*eu^vU7mnNy3 zQE-MnZ13r4rSAxw=XS>2f`j6wI&+I(36PI=nKnl{beTth4I0a~5Vg*KvUyCP#}5+v zQ*+0}L9x!vH9^8Qvh((3KMmKA&yB$kQFshmw__o%E_}DTkLT0PH(1v<8pfc|RaGMF zF}^N~yVq%{zA0a(Rt1+BqF4NyTC~4v#rP+pQOYH+!!8H-5bPf5p0!jt=oG1t8|o}% zak5m~upd+P^iXSM)*6 zy=h*F3>5&4C9TyF-7jWcChnjIHzKqPue(eCRZmPo&LH(ZVRq-iw^F<6=oSsG5VL~n zHyB1mM#_rG;)o-i`Xc3%{Q^lU7toe|^l^p6S%e5uW=Q5MwXOlcNt1`9&MS}-UJn&7 zS3{B6WEq;xI*7n{h`!1p=hgS%z!6!2Ht^%EJD368f0}OAxd&RY(dHIZ^D=k}*rJ60 zV*!KZ!ID9zumqPfTv(5S)DJJST1~*OQ{&%)0l;k#TEwtQ`^Dv22z@}76WLv<@urKZ zLUBCAh*@qlFE=(-V1Q1tMVx^312JRjz{#~uz{`jI;;#1Cu`jRRzRxZdDGO9A|( zZglW5B>C+&Ymca9kcn!}*%Leq=DSBL-0tx*PC>A5hRNt+OC~|NKlxY_n!C+M=W(15 z{hc!BAl-^a5+s&6%2p}4mVHD#^gJ5LHC;f`Q`dAt4QJ4(ss){fk1>9opC4!?E8+93 zZA z$E+wkC-~i86>dS@$LBe2ML_tPnP;z0c-<~Nr4nX0@7FYj$=nocGJa#45JZgeL3UtV z8j|+(T}kF|>V!`+>NQ)WH=>rA8n<`)4H4qbp{s<3k~ANghJc4i7;!NL7{6_&NUFYhadcE_p8PEjf`+X3k1BiLKMJhyP4%sl>2Nf1BsIMo$vNMcc>|S-=_qQ#*k+cU5|0nbXELS6tBvuy$u4)Nfbc(|3MeQeqb)9 zK<#tWL)kLYaz0u0AuHW+Cng$K4y;>xlvvO|P>Gw5cj?$NKu*P2UClZPkRp4#k3g5U z0VJYqS_f$HT4livmnLjy7dDj8(IPRQ4gaAmynHgMw9#dc!mjZm;EZ8rOlZ0yUFd%?-9ZTrpTtnp=T#&XMl7P#sG6^8 zdS?$@-(9<^^5NDzURoYy`tZwpOeV!b{ybBVl#R92`o|@ML@B5!Dk@uS8Gd2v`$Gf{ z#ePGFI%nW57u!_mE!We>>BOeos7p2_GuID*tC9CFRxBeHhZkBwTtG-{pJD7;|Li zA!t*#YJAs|e|1@a(z9I7wpoLfo1lFxkl}YEU!>GqMFr)`pa#{%3M9Yx18Q;j_au%Y zi5~fQ8_|JUcxUW52fq*hXp-h!__S@=j_vB- zQqb~n*|0^5D9z2dh0F(0@$$eJ-+QFfK^+G<(yNS_%a*<4k};!kr|O_+;t%D{dsLz~ zh^~<=H9s_5kiYD%-xo_;0H_P!#KgOCWy2eRGqdMavr)%|J(|pMM&~%7fN?^ZK{~ z4Zm4P7exDRC4HC1Z~B`N?wyRsWF5O`hqg&It)%lbZl)*89ZX)QC2z;t|93mKa*Jen zRhu}X?N6~sr)*>|=ss?Yd1L}%E*Y=P0t5WTRP&xJ4C1JH|dz$*omVcZWJsRi|TvHaQgb?&y3&%vhL`1Fr*6bbAEm?aVmG<1{RBg4{>W(d#Xz8Q!+De%Xy4M8Bp< z>B3W-G$Dh`^qOK1;?WPMEe?QV*0p;$IZyRw4cusgrNo|BvfSx-A%P&eAx5(D$UQOD zsgJ*wE^^h(59~C7eW{uGRGM|K0%jQO@~3^ie>lqbro(*^X7ix4^?}rUp#p%`0}wl> zPIfuv2h9?V9uz0%?jq}$>D~5N#KkG{>GwtQQ7E}dWEv3P7H`)w)+=}L>4!UVx)isq zAkrOna<18n&Q}TXpdZneMe8~99T3jCge6U>E5~=dJFSz;Db~Li>@uz`7zh9;mMjoH zX;+?bQ7xr)N zpJB6qSOhUYldEtTO;&&%*2$9$^m^kg)%O!m&iQv|7e78SFnW2KddO0jl;lcT_zfl) z?W6WNEId||jwTBazlS2gVFYIE(w(I2vq~g5kPje^x2zwbgt$|}TLZBG+IyEe@ybEp1fcI?f& zmTc_hvas?B*U85q6`dzN{ z1__WeSkGu~k*EV;vw+};_pbpiSmo61m=lQ6$BYhJB&eq7#xu&oGNCsC50$rVyg1!i zNccgtyAQa1NIdX^#qX#UgB^jrL!xLKqRyv#2x=2;v*4FMwNbZW78CY|33qvO8a`|J zc>6~wDm9?8w=#O03gP>$HW^Li!{zPOr6 zKY2yzub`)EHHf<&w2Fg=_$p8hpom(~>`kOY$_!OnNprSH1p9w>c6iOZiB0Wmg0HJE z!Qk!&8VVqJT?F1!Hh}^X`+ZeRvi>}UM>_Lm1!UQP>m=pb?b#^yP=X;Wg&+!E#6^Ti zCh;}OuM)ET@dmQFxpp34!zF5WD(hISL?)wU!rJQ=-KcV`v?1ua)%+UMtiDlVrx>t<}{+4I~b%}!nR z7`nlZ^OhX(Tnwmq%$WXlhRa!R{H`(_(kGqx_cTn0E5JHAJ3>DyvSyuasW>`lDJOte zA&&i?gV4THsovL*PalV+7?9O&$B6Bk?A->7K~10*D$d8gBjvN}!UuBttN##y>vnRm z>fhI4r~(%vxPe4XOWGSkeRdHFte)f*&qrszqW9dGKm3&x7c?~0t10P#|GrIA*Y}ST zBMkH(2cXWCzdE4IElZ*+If&g>+kijw3l=xQ;ISoFjkn$(`NfRjff}GgjaXB*>#s{b zdY(==nEniD2cUn8btZD|uhh{YAMedCw!|bmS<+0BuX;Sct);J|k;+?kR7o+f2j6$gR30SfI`2KSl(7a}rrg-&yXNp6%tf@+68 z727{EA`B}Jy^3R}lJ%HoBy@=1(_H$Fhgf|!Hi;jx-qhOP3${dr&Y5rqeg43YdM|*5 zTQcszarw~k5%$!({M_gTIa;n(ld=*sal+sgmWQl1)6bxG)FYNuQriM;MmqB$za*qi z0sdksJ+C!LXD;ERO|o5D%{?1yv~G%BR~vD=r}!H}Q}Wt?R#YCDnX|ODZo^0Ha0I-^1U50+A>%dkgN>o)m3s4|nH;}ybdmR!F zsnnIB>DRPuA@#+I@aA1CPT?!W#Uk!_9Q0n(s?Z`M+eC&QfVoI43A~Gudj#p@jg-N| zy=2cI5mtBq>U>m*TWxq6PRoCrj~-Zgdb__m*aOb>Y$CH?G$u*zVe#Ji2(fC72h(zBhm>_iGH6>ciV|L8w zNIpugwZjN(Bi13?ScmGGF#E@JL7sGn|&(QW}-nu?TI`1%z!{U&1urVZ6PwH&?bOt)-+C5BddJT16+0}1cZ+f%*4RQAk7!SUi<@uc@V+uZwT1CEhkWp*ih$s zxv{cSeWb5&&c|jr;PTO7v82E6Q~JGBD@GAhOZv1iyNB&NnJ-U7rlJj;j*{h;6tkC)S{B8Zxzn#C#NPL$TE1>s|zr7c@YtMI0?7Cm^bU6QD6nba$erhEI`w-@s$ywv~vd_ceR z-n&=@uW(2Y3V)ZvF#adGDFXnZ*LK0rbK*zR96Oc)sQiuBo3}Fn03w7dL#cPX)$rT7 z`3vnAFt_bJ+rYrh)Q%w$*9k)#UyYG^c|nG7tNNP1(?#G;G*6Qi_cFPx5R>$&^=$JH zc55AsS+3glcZVBE{eJ*IK)}E2I!(Zf&QNx+U{$h+)l((FwK*{T01h;8znLsN`bulH zM=X4sGr+lL5+MjMwGH&ey}iSKpDO@=GXeP#-DE?^4)s?s*yJm`@GlARhUO}qj-2t@ z>e160(UyrXCw1-*Pod$f1;dDx@kk0iM zl;C7|JH-ihx(2tFe5B0~U$0qkXYc%FM5OZUK$XSafa{Z4AyWv2IQYxPAkiGJ^+9^J zRtSbTu}Cgc<(}o7r;?*PkW~@t=UC~adt^g>(-o~zP0Czu>+Op@Tx?;lnm75{4hut^ z;^s*wujHU?5nZ(Jc3B;N$$?MJU@PAd&zqolvYg3-Y3YuL7N5phF@6)?{3c?_-lW?h zYq?WJJF0>UL^?wDUeqaJ+5a=({e^d5Nfqe3@TO_sEO9VeX-sp!7)ygc8^y`)Q>tx8 z-neN8bkMzPl;=IIyt1nVrM>nNn!#J-O?TJD=CM7VJ9<29`}wkejE0BOv$a(enw>i? zk>Ca3A3gaGEqPUFzCWZ;e=ZBz-`MBP^R=mp#}ZHt$7vz*IGnL!+;E4bRzga?QuDz_ zOOnaY5B_YU1Cpi3OGQKXE!xz*R}Qki(Wi@I?YSG9JQ6)T0@c7`%WREwK;G`1D~p%e zr5Zn^w3|!7-sUWSNHc^4XPzmImf^NTVO^py?zWUV5SO6XT4B%MO5dp4hmv|1_ZfR| zUZTw986MP$Ra7_KLSECkTyQq7)DbJ=Fl+RFIrL&*=ozDJMt107?6-{=o8IHL9^ZB+ z@9tmo;v@L@We4x?*T1uW-@@S0q6rjcNa{N;_Mf%%g#KgSsR6u zT}-cOMDo3VrWNQm=q)%w9KStbv0fc$ZAEOcR@kmN$6pdnddDrvKirubPQBa0hKSSC z4`=SiUlsX4Z(f8zGd8UGWeq1TK(2`_JxtG>ROj>hP280`VrxpVUDlZxDNk@lO;3=y zn1~I4WC~~0TK>cxJ59M0S@1D>T>%o`w&xTP@b|QTgWe|UjXUuL8kyhAP&Mjgw(soV z^xd{^y1(`w`cJyQ_8$6Ay1(}Ry$a<|SBxs(p-BE6iY!I;N+;sgyPOSs+sbA%1-kWw z0{>C)?VwHP66Z%X?YU}uP9o(Iuv^SUdS$8w1PmXwF5nFk?~~J+KVR7h328M^y90#B zGp(9``U_Vrx4x|Pqkv%ZmUR`f5gwkqgv|rnKNaO1DwvlOJS!L1TFjV0yc_VsK)Q~V z^$o7?(y49wQ_v8Wpl3MYc6DR&Hn|iWBO(QR$aEowEAyp4ic-2J_5iHxtZt!_PnT#S zvATR^+>)v@x_J3HtzF?DE#~xS?(mIDGUil&BE>Nj>(G5R?qlEVGZr~shXfj4z8aB= zS-Vg0TTSR*8;df|6lL;mJiDhl`cbx#iPqSF@~7lKTruua=H}@{UD!z zwTgjn2U2~!M7AJLM|lRo-ZRUxmG@7X4sS-3tNfrmJXpj7A893~eC3cXZ6;D+q=nu* zFX)!4EDMXS;d9VDv3rp8B;NMkb&tEeQky|o1tRXCwuuztbh=x(1U*Sp?&um>a;HFE zsVp3BVI~Du%ng9T_xhwEcU+6giExyEaDoG3fTayp7sP#I;A7H31(0R|9kG_g_R*g8 z0!2KS#&F4Hn`R9CewiP@3yMS|kt9F!0oFZ)$9~&{J&K9>nSz?V{+dEGyRvsvXD+k6 zkB|=jLt8W^5X0YS3&}Xv!k~6ob)kUGhVrGFTJy}aP2&7?^7TjR6~MZjq@E3bf_tsn zV_oEOa);`|qHKi^W=G|%JNx4mOMaYY1v-;8pd8bcN#-wJFsqS$%<6p_oW%NqN~JEG zY2V6dM9KU+*C=Y~6E`G-DY-EoX90HMnu4Kbv>KzfE~A`p5-MTEr7dqa71mA`65PqI zi($LMTQ7~kJ?73CkZ`dcMH@zc*u-cdEKt;^ReI#CdBftDWkYQq?Bu5#8Aby~b{n^y z^p}ole5}vqGazELs<$ywbMPLGpDi9jo(a2B(y@9t!=&0^`zC(eQG-z~MxH9Aqli_4 z4)fVIt{oKyn?qX^2oTr4Dxcvq(?Hj$<$2eqS-s;+plghgV^LS4Jo`$2vZLUWbIECK z1y3ors29|43z#U9@rG|>9-rp}bbxW-wS971&kd>^qdrP;7^4@gB-PU9#p6z=EzNQ!YiOH$`7o6I zo((j-edMf)HK)sV$y+$N2hfRMG>r<4b4RpW#=PT6Yd@9E+3{q@Ipi1H-QK!~UBL3` z$>sAc>&R{Vic69LvRl&v$e2f0n*%HtUv4}I%OX{ow3lukci2vUWsZ(Vc;B=y#~n=O zImt(#(oUh6ZY%|yD~x@}==Qf$UEzxh2TzjU8#9R;JvB?}V3J8lZ_qgt^h4E_|H51- z?lE82^+G>Z4Mo2aWnbQ5y}vOuhL9MBelP6)<%7Ryxt|>PO}_a@k_{){DOvQro+q)Y z9-w`I=mI*3cu#_VXOcCTfaB{o=#K;HIJ1M}H78BsYp7xsZt*%RX_bBST0;iZ9#B#1 z0;0(pH2^XTLD!@&^Rs#IO#EKF1zxMgfYPJVH6jl58j-EbjplDH34~Y*lqS$(%$gHe z=U9MU%k;>AtN|a!R30p8++|#-cnI!&sHYYUiwqGu$e_YH( z#nuH{pwBCRva^wGu+up($_DKrUnBl8RbAv!91=_4EvM^3C#7I7rmZp4cmcC2RH zxb$#wT#4&7%`F~=>C#@y9b=1|XA?zI-gOkuCDvBvTXY)KDGx|vbhCICG{5Y zH~*G@Zbu`?OP_l+r0^66ICR3oUn+)jPLOlS>O)tQeR~-1_x36=0Xe|_-5aBvQ2MU0 zLj)lQL(7XIo`yt#QQyDS<#HGwbbIEYcu1`W_>dL-h|5cJ;oJPQ&+&@X_lUqHS-qLy z<~rRJmJKBw=}ShNx>u<#uGAxGJ3zsRiEh7tP4C727-D8lo*badw?5W1`CZzNu{+nr z=42OieURZiYUWl>hiI@#_ zmoJQh?6D(v=9*ijoVTNib(QNu`>kIud#yS%3UflAhu{Vvpx;@CpNk!Fzfr`12|bm}dL@nj~`uNK*IIX?V)e855KWmjd@|S4OgFg197j*0zICYIWZ*OlQ8jd#ogvrLf^L$x_Jf3o_t&;Max7( zdQrn`kCW&Ku_*jVJ@Wao?R!tbrWoh&7QW5-3y3x{J2;8K4o_!{vXdgJ&7y|l)ikeV zbz|h)3%f&h`2;M3*^?YY^?Wu&dV3%A&hO{` zd@3kNPfyf=M~NAY(K}fsVk=8`r`;U8YTDk~+tD~?LU%T|>2T9jZk~vAOLzI;%&Pv{ zJWxnbJ>F2c!8!KoWIx)N*__0G@u4Fj32v!UYGsLf=7F?15!ud>U=$kpWCo^rlI;u! zBc#xt8ju7g+%Iu9Mz+DB&EtL;%^StuZVLtnF_-8 zQtU_5tit=ppf?Zd+@2HmaOG? z>$*GXXQhtFVu>30Arf7i}aYhol%f4n+fysqN9{-urgT(^&y6ilww1M6SrfY)k( z^xXiBtV_No?}^##si>cbr94}k1JWvauQf!)YfKA!|H%&QotB}0viDN$@T$Gm>2>J4 za2{XJjRDn{uEp};$2i|1@6rPXd)E!Vr4*k1|3xg#|3oY`C1Ok8iKUNi|LtPQ{5Qpt zeu(*N5Cyg!(k+=~4RF=nEumcweXx9d4^?#WTxHbgBXlz6n0XFip4HCrJkhWMOIVyR}OU+c0<{FXO;&2s#n# z$9~e;C!AgDs>ZW$YhbTr4xiwQZ6Cv`OIC|w0T7pm+V(B*7q_7`p>^iNKwHPI?8tUar6qKbc z3s%>sO=*e`>2aC^O}Xrg=y7$Uau4Z@@ItEvjFXM-McRF~xaN3AN&%^F4Jp%H9A5g7 zPO?E4PljaLp~0HQFg4=Hk4B9B>tadF@uk?d$%ewm$Nweoy|x_Hx-8*)o}%tGx`a39 z4uq9|@F3hB-UtE02=nwmD9v|HewoKqSM@(e_ZX2G3WV82pgreWd#yR=H(=)kc8xBY zF1*=)N|xvgT>*xD=N`(H_MhwxL{@&AV-M%_ zDyjI^DW+!ulF$$5i^SZNlQ2ni`if_>v$vJ+Rk6u7E5C@`N-<*03B}qf!R4}yWDOyW z)ZRJDfC2F;&zRvO$Srz4uKVbrr5C_ZFu-f3=bU(l%p~v-q-?a}S(~A_)+~R?E`@P_ zf=Y1+0S&Vd(Y-Wdur)Wg_S|l=(}yw(v+nsrL3gj73^elwVZ2R;d0q;5?19!!t}cwK zEpGrskkI1K{dm2Qa`=cZ_#8vwwUs2AgWl^>ttY~&IMr(X7(Yqy&59s+tT?i9iVeXh zAbF~{^ug2*G(*Mqq7;)WKrN}Ad1df_wcPd1SE)Z#VzcYq5qgMhA5YpXS?eZkxbonD zMi zsd^LAjJj&ei)D2-O|?xTACMx9_~n*Wj63!tG5L)*6+0+_@jWcs7id?YG`;}zoD&nZ zs1R(g_lS7DUSo*c;>7gJ_FmNT(w)UAi2p$$=l`d7}9^W3kuIj{Fvi>-|I_%10-s?blH zKLsC)ggb@F(vLA(cC;42#HSuK`+4;Yc2r;z@Iiy(=vZIexmcVWOO`NlWV47b4mZiy zRYC|lMErMNmi)>;CrdtNKH*VV3Vj$NnjLzP>2LS!aWIf zB+an&&_5v3Lq%Y}@Xq5hOtXSwKMulF2>HE9*OBsDnY;M^87%0Ozyq_|FQ&Y z6{$AH-&M@;QDfF|!>pmF)`!*2H>VqkOGbYOLvf=M;G0(r|Hp&xpVo(?6VJsCBeUNX zzbJ~x8>1sQL*S@2;IGVoH$~uYWbPY{`dy#IA;(Xzntg}ARE{cNwpGVc;a6X^pWWZz zxjo>|?(gs19`I-P_h+~Ft<)6wt6EdqxKK%;&whD*UvEU7HsO#(NN`nMbUbe=Kyq@9 zBcC)apA_Ko9E5#O zKZ%ONs@ubm@UEeMh@^U!A{N5h4TsjN3W@=ajhd4;7UhB|+k5q>09->W5ohGDMf54A zfx&LE)Ki9!gS2R1f9J2yaanrEbIv$J6eMe&-F(5fZ~O)LGXnD~jsAa?npzM*pA%Sv zg-cl6UIMT8^eWunXF^+lMPPP*<5b9dkNqjQ(N@5f@#%4YgZ(R(o#;rI@A(C_QzMc* zBgzJL@}7^^HKbywKtix-t|S(Bs1%AwZU*d`4**E8cnf>JuZEGX_pqxe{4x~d++Z8r zBiw>`1K(MjJ;j|!4gE?@={Vi<^P35g*+oDA&!2BX*C1?m)2_g8cSv*}sHh}Y9iEcc z^J+L?>U1Z6gT*h_>YIu{6gnu1=1fCS=Gg!R8K>Odsf7CqhZAY&Y1BuQC`pe@-V~Bi z0yz*grF`d)RH(3Un9QE#O+34++9W+|Af?h$4?E zB_v1HzHFLZOXy;WM!0COu2rTC+5vW_vmI~T-G8EgX2%I(&K! z4$b0UKq7f0G{WdmM@H~t#RNets zS}gW|@nqlu$T<92Ci=7rV(6z{6#LZV;71B1MGqAMDm@NKkG_keAU6CYHIAM}>Oe(| zIO=0#=r1j7lmil%?~uq|e0}?=bqxppv2E~S^1u%kX`Jks>!(8VD=hrczGa{3`Kxn> z{v{6sM4x;Hy?vDSS?Q~N%QwfFqoU?8Z&M(D^{eB|kK2Hs4eOtb3i#Quem<(7*NFl@ zFBH{QYoKuRL_yD0$F=h8-Z}TG+Px4`?-M*T?MtIs&fFi>J6&e>y=!aM-R_id_whJ) zUgz}gRl}`9Hj66^J?}HKcI7LKup9@f59i9lQkHvn6b431ym?zqGd#6VeQULpQ)e)L zM4+~|Yw4OLR;D0bY^LJ{%_{o$ouxX@tj}hwi?mOfz-sAj{KCOE?_aY;mPJ{3-vau! zI-bH(8YoPVs5;$^2$qexh{mf26^^*mYZ(lSFepIHznog_jOSZCch>k+Li?)S*i1A} z=*1$o?o>4Q*W`#NM$pIdF_T@m*aD4zb7k0IW(S;$CV@qj%;T4fw8S`U5+C>mh)ogY zEEmW_!m~{5U=n}#iMh~bd!W91;1KYak+hD4ykcdFiWYAZyJ*?k`>T3Ib>U;d2K;*@ zePO^ag&r9riLxSzk|GIoE8=-31$ZQwMa;Hu88*B4=~TcWvzuDjL-xKrZSoU;tCI`G z+7)y>ftKT__p2?j22GF(zLcRG`fBh(`y>4Wz_^(Z3wKTmf{JNEtID^5NMI}9U(40{aW53JPcQV%UoO6o32h@ zW0l;H0o&#NQVkpRubh+E5OZFYOo)pCN3S+> ziyrpns#gArZ}qqlf_c9Sh}rNS5ylEUqbmvMlKTwj_Q|Ff`zc(eZ15j7+7%uqXhA1k zi!D;iLn-PVm=Mq2cPyrV^0Tbm#L+IEh!0%bVunsHbF*=59WF*x_ky<*a0uE??MeFY z0BX+p-@@u&9{$&G{q@j)L+qb6`tO()<3|m{;p_d;@;{{_8aDyX}Uo9x4AFmwh zsMy#~M8+RA0{npJ{m6Z6hkMSawFLf@UhI+aewm#x?C~n}r6~kG!r(pDJ(f4ZBLLpt zN(Ky2`*h|Sag9IHaqm+jRV2p!R)M+29A%zR$q1@n=K( zyCVaBHMC!k?4JmK*j$^XV9j|dPl$56Xm4-kHUcZ(*M`cw%u4gF0qQEhQU#;c)^&UO zJFr+t!(?+1uQ#OXa{A5ARDa`#6BS_B`!tfu@_^g?bc=HRk|-d6*gE8oVzHS`M-+u_ z^W}sVgv;`S*MaTjPKnql5xHf@~z@A;(;;m8iMo;Jpj)GTy9+1jO7pi8t0~$d{sj&Rp=H}xrkL6C@q`0tS0GUh|jFaKLPa8eBo?2Yz7?<2!OYJ91k)a*M4baQi=|XsF@v_yx3*dT6Z=^pEZh+kJdi|h(wBvtXD=dx497&lM=xY|_fc?+o zJQzE&1Ex(?f6!m@u66RHzDi7BLwMK_b75}Ym`m|s+GGzNk5MN(h99E^{xDovU84*2 zoq%}A(Z*|L{$~kL%@k-qu|uvN?7-gk;TIEd;Qp^we`t{K#!$am_fjK6&bx~;p`UcU zP48BJ)E8&J1CWVbAjN-N(uUcfdo;3auoeA@sf8yW8Y4km<@ZXr*ZWRYssK^7;(1V~C)%w~!O; zSR`|m2QJnmB;J;JaP86RIKj7Il^&z9hvU9~GoBDZr&t5JOzFcUNU=5{P@y_XW56?e zwTUJKmM#@QNoFzbI-YyB3^-J~P6@4ELg*qj-bGL=?UF!ZrN5$Al{8qbCmhU_J|iQ+ z%L8uC{C>AQGx^&YZ=js%2o>58TYf3d`&K=#lk=Xb6{T14)hy8>^VZ*&!QEk#Er&9H z{{p7wp^EqA=kPy=A2s&=YTxeyJU{IBhjcQC5io**I0z#Yf*>Re{WQZ2<40S_;cZTS z{N9M)AmR{tN3kPh-D6QGJfIu=G)#g&@6X%=TO2yp=F?9jWRf1S?T({pbOh=933hx9 zu_Ks8KWqL6@DoQqnD|wOIfakt9SskEpV~bTKbH5w1D`YOgZ|J*$pAdy{^-M`4o!r^ zQSXay5Pk&zhdcboLT_guWO!t^ceKyQqhse=Gt-}}_J;@9K*GNqVXwD+_MPT(;H#g5 zwDcVbfiH4As{Df}+$DbU;64@X;X?3x!MNN09=C7rf|V)cMwxbWf6V+hh4+PjVGWD< zw!@co#GW;1e^B0Y5#BsNOND`|Ef1|ucQMO{;t2!rEE!qz9b>Xg-<4x*NS7e-8!0*0M~ zc>5;7H0OAM8x0R179`PDh`*m8+!C09bDx#4#x_1KTK<${^G0N@LoQ@fWP3ordZ*%n zKIGb0QVvdO^Y*;qF%!5L=@&3psOQ(bNA;cca&l6&Ib+#GV_MCQHI1s*07Jl?K06Eb z4Vr4y(lYmymMyJXF0`S4&18v#zRj85%enK0&9|;1KDWwdh%sriQVL&i(KIA<6eXPl=K54x)jTv9VV7ij0cWf84Tx(xyS%egqn40HZMc!R+<{(?0> zQ5^dBKGiRz2KY07Pxvut(4M&5sZ`0K{55V5%3t3?m6TloU&u*mFL$PB(R&J`L`#p0OQ55$_5(usSasm?}qhjdPOlsh|BKn z^)$5kTI4(H4vRwjMBr1SRvEEOsgcKx|H=TN?d)zW3D|ajoWT_Nm!TcaTv%~cO0OXp zs<>L^sT~4hJE@PIeJW>j0H2mdDIMEABA|XJ#l*h38xf$9Y*f*ir9k4ECgv%q!*Za; zzF2IYI8nSXP&YCl%+itDD+O!w1HXm9DXlHJ9dqvWCMlP3iG_(v(%7lV%DS+jt%OTL zk2908%T$?v>P{Hmy`r+Q9iDk83TwdT$7N0Pdz-y=y0VAi5xWFsdfjK8_RYyY44f zS_GVYXhI@b&Uz^zWC5XRln@}_3R`1+VuQEt@Q3wRznN-?8;uTu>Z~p8f zoU#TK2jh9uHts2Exf4XPJs@DY9`BLZ(DuqK-F0G1T(=A}fxc6eKE492XStyHQMvL?sG! zy0RXBF4D2~+`w|3$OVwmb>MpBZCony=BC+DsCrP^+}v3xkhDHsQl9D_5mMa&49 zFTJ*ktn1WR{`(!omy2W4QUDz-zWeKAnO(n0$IBl(g#UEU?;XJ3_WDD*f`my7+<6g7 zpd?P=B=~bBfAC-uM*(DdB=UD|aTpS!hwwvx2pu(t$FetkqzFj(H}Dtw!N_qa5gz4} z5cbK1WJew4{v(_nnFVy;)|dD-~>9lmXC7E!|C_0=#dC+mkSer zea~G;-k>4fLp^sI{WF=g*{J@dX#~*od^PSW>(p1qa@yqwO(VksYsLUoS24i(@;#Ga ze%&_Az#r`wKlzGdtsMH~CJrUvEjjWPo5CKp_T2Yv3MVaGMH|3(SHwHNhZKQj-0N`X zGRD^#o4d@jc<071qpUbpp}&`Ja>q7*!1;0E-BJF5qv9n3Ug321?=P=kPhQYiYI9MNoa?1cB0PBr=WMCMBMY<)l+OC2^tlH+pN)0t|(n9#cz z2!!kII@imTaZKD=bC!&1t{@}`pRXtN1+%SenZDZs0$jFwT7jmpQf(~DG=VFBdZEyT zO5H4lt)#qfaXW1UZe1=0SUC;J*~!Bzn6h5gjs|K`@3$QDbXsGy4!pHa!iM4EMc&g$ zH5VG$w}03S-flwE;wer)&>FN}6A_}U)E7M<(d2yNEhjs%)0TJ7CgEw>Mav!o!`Zk8 z;PXtA-5E^@s zSRQh3gmP8ib(`J;(W)N^a#i5vj1mBP<7x&h`_-?(^IYn=e5UKCw-M8SB8M`Zl!~$p zAiLno8P?<}##H_cX^#IyHXPCw4R|81iOHQM7xf9uJk>24a>^t41l?K-dvHirVWTt} zEr^w6b`h`oi!}y8JdOA9W_2MDd)*@(caRnob)V3#ln-VP>9O+XVl#-!ayw1%lc-3H z{*}4`TyoTc&qfX9U6^)%dJRnX!87R7GWHFGJkBvHYlF7z(T8N-3>Xh2h3;abr-N^d zXMdd6BXv2%b7uGwaGU)@kW}11>s)LzZ-twM%Vqz-7`JS&ZAB!siwY8l+jDR%m*mS&mi>8m75BeKY9K zEgrXE(7bY-x&C^E_BeAwGc(P$v^>*qT8!yrW)S*fFdk5Dv!!OsHW{^%y_zaA(VUg`j@w@Bct(4{61-UIi2=TYH5+6{>~o7(Fb*Xn$@jp92ly`zP?<&>HxE9%EeNh3?MH`K~tBj(XVTDH8X& z4aSSECY|a>$*l%q2*%i)u~2Pk8iVYT4qyq8GpXkZChi^Fy@USs44_0Im8%Zz_tl>1 zP&IY?#&-^>P)k|jIdZyW{>1_#g3Ry0b-i=}XP@OUfL_l<_4kL-x~q4e>dQ^;p4gq@ z()t~Lge}(_s&hUW+#W@45^*ohj6FR8S~+t!PuYV2To7zLA&Tcspxl}8QL4H0G@Im9 zFexM(Ax_P2CHjgXVK!*0qQWZeSm5oPu433Q!%oA|2S{JW{(6Pp4Tmu?rr)==c1A>_ zueNnsuJzC)27O(GD{?c6tf`*^FgEzuK*lV8FYdE?p5I(!w!0SlVg9sknA%ug9A6cxX_i)KF_0#&gcO0(?Oc zPLj=q=|m=f=PvZbP|lRG0+8^n>nuy*{L9^a-OB0SSaY(O-3eScYfXhPno!3pX06wM zS>Jx}L|SGyFLkWDKnxoV6wpG$!|Ldem#cA#7BUWcF@)zczY=Zs^cHv;eNVWK=jR;s zotR(6W_=wMuF-Z{cpyPe{uOIi&+4l}BlxG_zw46TKSdRLrPBG75kVFsMuUsD(wK(;e4X!a&xohtnH>;3RO!8ejdKnerMJN!Ld@E{jYS937SOUnS}j z|4c6SOF{1MI;#GzK~{-u?Pa=KkdZIs3ZSLq#qZML$mzo`6iIB%}*3~ zaQQR{}behHul%)$9{c(eOSPQhp|Y8efr6Nv)^#+ zS4ue)x!97{enB+0RsOA<#=LuE&vHo=a5`8{~TqGN<8b_7i1XPFfLC>!C> z(L_&?hyDDa*Pi|cMSq!_{d5?u-|gEktc?CtVd(yYAf3a<3vlrN?}<&v+yjt=u}g?G zeoy}wMf_Jg6BjE?`$dgsk1_y%hfj`t?AHp{`FDL!UjvbfhLtLw)k;E@P5pf!vZ(eQ z15|qtHhAs_@pf~cucVp#rrkc*_dO`_j5|6SAh-O>a%Hdq2KuV&y1yZ^bof0N3z^=& zEjQui?LYb9yZ$T4{UJC3{vtY=ndB|u=*C5DTM#IwHX?Tuq3cYz@TIwbW;_6ai)P*% z+2C#vSDAwlU%`&$v%sBMJ+DH@_yuO|Aeba)$o^CTIK1OQ;Kw8f3df@c1 zudOFW^e<$uR1fH0wQYVb2!8PWNswK0bBAgszf@t|C~8>YAqp-fP=~U%QLhU&$Dj~I-EoKDO(B-f^<{<^J?|5FXKW0+WfNm*xO(a;cWEA#lFV{u z5m#Ay=g+fzdGOJE?VIUDzSkG&l8jM-{2cq~YDX~#ynL%tUZ;t84qi#zmh*Ef9?~kk zu8Fh_g}R;uqZ0>-blTsu)A{~NloPv7 zvB_U9+zozz)?JK9>btT}wKR?MfmpsN8?J12y9kYWz=HBEdU5J!jhF2sWHm_Uz4&iO*Z?-PnY-2u;5dW<~D z%$~!49GrtG?aERjIPyNR+zWY@sOSApJQWgB+7y?JVlW$9@Tm5HlF zDi5M?t-*#(dkuxR8*Eobe|0yk0_G5+3R)pKu@nTPsTN&dnd?38OYO4A3Qh~RGgkQz z)JgGW!HpzwG97b@8_5w{>TWXLGoJ zs1t`@%+Bfd7YX^%`uU|q5Og%M@0^SvKSk&tsp2#`GQLrG{78I+kkgM5D;XXgVR3jc zF%UZF7WGLcA4)`@CU_7%D(Ik3Njkg}Gx-1APAlohPAhOHY2g1gomSF6>a==!R2zAx zVtb@s=;>C8J+hjF3&*GL2|&NB?Flu1o7leI>J1^;SPm?vnL_`3mGrrI<#9knOxz2e z=$*hzCXS?RzTK&R(sNN>0KT>Hpr@s!HmW?e)#?5?Qx9rV!MxSzGyk5FHr7F`1`%t) zO7ni}_aIiOGP@RW=UM_#akITXUh0R$pOCN-Uph3uw=Nx=;pOocMo9-m&xta?8 z$hD&<{&#g+odMUPVOo6V?d95kSE{%A$qkj2>0D52X2ioHu$={J}d}(TzSsL}Qy6%)0bqJnlL2^r-z5vsEDWsFXJvRmq zx1daMkdsi;4^lb0p?!086vVI9?5(zpUp~vOOctza%WUA^V&Kex#I>S-s-9ox#ig;R z_tLS55^v_yvKF()rBzH7+3qwk^1PUqsELBHiFD8tl-`abL^zN`-YZk2t~B3WMCclt zyWDD#r&MyvXsEN<@*=zu8XqwWvJ$MWJ)YXyCOEXnbY0a|!y*6mj~ zcEFEH@BLEnFWZZeLr6JF51khjJ&q4Q65xke6kK1 zc_`Ez&NtbSp9t}z(+c{c(*M^+Ge3y)OCGDsez|1B_HTVdpG1Ssa_JtBeqY^m4DVR6 z_&Sjb&f`mt;3y=2<(3~;H4}l_eyv-1L%HO%{)f>qm=OuZhzYoW=9XFuqQ8CTSBhhdiPd3 zK;1u_!figi#s;-R`XoG3?0xMs@=YlEBWu$xx?Cj`qHYg=6^v-iau`UKRa{;oAwY z0>0S6@xT)a+kspk+h^YR<0MIKj zI3b5fuba4k2aAJ=z@RAC<<0fmwa<#j*?rWs7EIoSAJag>TgW)8O}YuB$c~^4#2vg( z@4i^uWXrx0KDkpi)L7woF{UcNnHv)E&joXm>w-#=xQET0+>o+U7kNhVMhT?t{2e|r zn<0Kcgr+mXMZWo0Un9)=RL^}s6N0-%lKo4cm>*Vuux@G1^xV+pbxw?Xyy{)1H#-E8 zIur_%f9a)?Xv5rb>v-a>kjN}5JOq}j zZnNyjc{X`(o7tMjslte%f;1u^E8rbuTL^7$;tkQ=cq$$^cPn1oTT&T}jv$m;8vnZU zwtZB8?3dCg*&BlV3bsx$V`0k-4&#Ec3l+x+X0h$9~9u6SLtzAx;+5jFDpjzR&P z)AdUAhOWt88K`r;;ZcNRT?{-!k-w+}g^j%f)g8|&7BctuJ3$!cc}ATv^B|ZEDI9t= zB=46md#N48cV03qG{fOk^DOY#7p|!BF7SeXi$c0m!hjf>hme}A^h~FlcxgBaKbc{t z%Fz-d0`g?eQ>Nnhil@onh9YBXoiHhf z@2P9(KZHZ;;`?@a2ksqIet@NaaM(A9`spEmEWjZ^l*0Ef7{_r6#y|o&6!<}ez(E{; z!%!SXL4qV;5JdK=0{ba`eU$dG=x~$C(nFGr`UsC5%J$$Vate=F=g{Qe(JlHZe(e#_ z4jpNDlqXa8Q5AE|rk}un?x=2tkFPy)+{cVU9~&Fu(^j;<$c`}`p!}Ek z#rwyqGFX1YuY1ZDnfw9=;r_1FIfhh5X~!VC2f@Fs|9yJ@X6pyrZ{q>}uIjh7Ii80i zC@6wM$DUid$gRP^H9j!(t+>YKo!{;BuT*rd9A?zp_hk9u{JHEQ3!<{-fo~vx@Z&~> z`@x9&aaDg;A${W(#!Otw^`F z2$kao8+r3aZult<6fD;E>G!4b`^N>qNiUmbJf0<3M9~~A+zi);v_}~&X3WyxFdZ6# z8+g9x_C|T!6>RdRTBFNtp8~D608o9#UbP-(-j3yYZNGVtXOvT6wI?}$X(hGIDr;Fx z#KmZsL4`=qBm7=t)e`#j`Q>%My%?=Akgzycu6_^i*bz+K2-b71K9Nk}z&l&o?Icjb zxz2IADR`8dGuL?AG_`1_76_vmed5-m9{Cb}HZ&cC(@L7As#^$X;Rmk< zoF1=Kf=u0R9pZHjs#hI<1IXZEkIN3)oU z@y9jf3^BEB^P(B$^(+9lDe|b`wYd!XR-}g{Kvp>hc}36#B3_-u-#JGaELr<_na_zV zUXO=+xs{Y}<;&_>BT!^futb6krguYmKv}ucxXwK?1$6>8dCM1n&tdm~n+Oq}BGofz zi-^#!gj9Qt9OE|)upi*#1Q8EL##RCM8~I*`Pp|hp`jL;w=|0`h3u`WE$w?6xWz8d* z(nvR72s#T-S{s4wQoXx-w%zByU$Ff98UoHTL zK2N;fl`10WZCGg~6~v9MpY1D8ZT&)QG;={mv!uFUP@?L8Obq&{-)fnHFXdeYUfysK zZ;hcrq&!O;uDf>e#^cfMA$3vnZYd=nRNNuXZxrG59g3czUg#rF0A6>Z0 z`B(kSOVy;((S}(eN>5Luxxf38iS&~LqUGZ;S0`k_mj+Chb`dzJafJB|M6id!8!|}g zl2W=QuTQ3bc2s@|bQ|kl8l!Cf7H5}7%2QhJD+N-S*D5W7{)((g-NFD_=wN@MuM5n* zB0bBS5GRU9+eRxfjbfudSe+e^2&Xb;XbizPb+|!w%esMCI4|c1pjwPg*7nYW1-8rO z@@ntrx24f*G~-@FmqkG->m*`=ZzNE7H8xJ%DU!{9zLV(czSIf`@*Dj&?zje~F+tN$ zTX&^GCX@_up(J#R4tPN*PKe7MVZ=mn$5TFf zg8co=)?VjBWIf>SijiwwdA%`^7my;Hu10TlWGGkZ>GLDd6r^ygW^0rN{fSEt@)oL4 z)8`g{5j^Ur<`l~Hwwd#EmEz6UJodlNs^I?`tNQg}zhqVL53CBIKnTM@f5+PCj?3xshJMhO`EMa^^g5%4-FOnUH+BZr+FiGiI%?J@orv%V-iUL3^g z=UD-eI%*a0&sskj9bZX&WU4Xjh{*};s5J(E;jh{#DRiWxGwipR{0+f>#j3K8VoG%I zvFzjLjwVOC9Vd?RX7D4sAALm54rN;6V@Qh}=iM*7lf)$X4gNR~hTw7FevjD4J#gn` z5V_yuKW0@XAA?tZV#;8E$N@)h+Zc6gG$&U6{|Boo|D9E7>>X3K>mOOww~hb9tV;TS z$E>R8OLwKsDt6HPY))^iKgqp>h~;zSi=gB);PoP#d+JQW65VAFG6r5n-d>LT+V!!A zhh)*cU7^Lt^FlfqvK zWj7JUn=lJ`DkLQQ8rI|eVGSo4n@-e!`UGN3F#OPG`3nG4K&!vIVrW{GH+XEN?cC)< zeJ{O+&qZe?aws>}N;|U!nk}Ih<8MI=bm&F?5tj{p! zf7xrSm)$sLI@J!SYqte>qu6`@;cq# z1q12wji*z!tnb$HY@kUBS{YK)LA|{IDHeRJVygi1#3zgoLRdK6<1T;~1&2>#y|YJ? z%V@$TyOCmTE>7(;&G)c(Ln~i$Hh@eGe`B!R2?}lmM{)|vHsQF>;$t}ML9|q8r~+Y| zn588@O{^qkCb&LK=eDyS9YxXtc&m3)Kz2SkSGi%lS$x^=_GScdclhBQJay0Q>ff;e<;Us zPVMqE+Jn{VM!ri{v-+TA0f;Xs)7!HH{dch{B2C^&{W|;Fa&^hDPKa^k8zEX)@K0FP zRm^ghVlN)H%a@;IeEt6x1jljstYqi+i|2 zJXAyS_yxkulF_l+Za-s%~zh$gm4P-E^fD0~A8QJqLl{SE=LY z$lVxGk8aZzl&J-SC6;8@l$IXOReP6yDy{byEvCH@RahZcA)y**1JrNmf2p!#u~WH) zuNRT2%_}W-KG)MX0`;gN%D2RQ*&-wF_>a9s?|c*Z0S>zWIbB zHm!$y+T_2*MGWsKJrn`nt0o&sHT7B?+`FHpmChe?3sw7HMRb682$xK#iTQko&3GxI z8dGY;N&6|+ymmTMB$5;1e@>M*eR7!U#1h0iYt7y`sq<4E6CjBs)iqL{w-gL|i`mQa zb?8xv1MZiUtsK5xVuMV$z&P(S<=+EIy@GkyNXek9u9^e4i(CzX%Wl5=L@ZLr;KYjh z;azweWHoxT*aZEyxShP6$#GV%bp4jDX;eCF1*Lwf0HULF00=Lc1J zBm|fD8d8nHU}>rQWRd2i{MT6(@?T?Bzdr1jtP1&oRqcccq(B0PAOhU!)RCIP5o+gG z7z|+q0w0c%5JG??e+45HM*g(G;GjLpvD^|S$6`JHWn}_7=n(l4dIOQ)DB|d~kHFs& z;^!Vjc=}?vut!Kc${=b~Js#>3-#3u`2S=YdyqX z)1#q&-*vzGL5Q&9^*)%4JhExTPWs|ck}Nwe4`m~8eA?=%W7#JmkDM7! z9TlPIU)x6h$g1SG8_{Liz({|FLaifukZl#x3X6ZA2>%bTs`cMk)uHJH|H!JoZ~Px- zRmwkRRognFMt~A8{+c#Q+1{KgIEUOO4ls#Ji)8}mr9OA@Yan_RZoIxLm#GgoF*@a= zZQss(XLgt_e|5?k-?D3hX|OM?7tl`EaZ>u5IstsE5Z%+1m9`=H!bh`dWYJLtt>Ic; z8s&!EpPNJ7=1c1)vAv}&`KFb(!e|!53Y%vFcS2i;!L>tQI`RFzHD6+iw~s4v*HGpj z-io&qm;@R{GgokOL%kBIT?-n!#I3Ckrx@r%h)%>ue@|B=cs{pz=pi=o6rrit8%3oN z2@O{Ne(x;4ZX+ap%B^r>K3GgBSHKZ zZn<1L72%dL z$+`-he}&m#H6v|uIG7PwU1HR{%|N0Dzk7YSeN#oth7 zrP1=~g5eZ5HF=?xA_#E=HP;##tXHWd*ptySI?;*+IohP-)x8!sxd@A$MN;CP0{FRhYxTvnE55ngy2+UQSCF2!uRw`;}rg0k_XayydT1;Idc}8S4I0POi5x)6hr&e`kudk=^*V zS>u_GG0E>pg?NX8Zj9FD#=An8wV^*Mf6XO%zm=(?-R-6pOJmrW`+fuM;=SlIYL~G6 z07=opC7ZAk&al1uiQ_#*N#G(j^u15EG+5MqMsbR_oCOarSp=WYf$7Zn`z#q>&Z#cd z_h#k>CbFl1O^oCTtBp*t6B~EW_tn(97c!nTtm^sr99W({w4n}wbakFY{~nD*e|yvvG(MI4T|MshH43d z&V9q)G^Mj=+Mdf>N3nBBuut|OYT}#pa^|u>5$m;ivV|Vi0IBdMH_)<}8x_l2wSA$k z%e^13?2dRCYHz~ z7=fh|pErp%GE4<#+L%62%C<_WJMCmif51Y|k7r=rA6M69eOGXu>Ta79e?pzEJLTwG z7Gl5^*1KhKt`E*SL;fkKz`w61Smxy=?Z^M1_icTe{D;JTp*_MEnW3-mN=|PEU!Uj?m75qsVhBn?I7p)4PPr(A#8Cu95FFmg7fOK` z3Q<2}Uk70#5B7EN8RSR>f06L92zN*l5{LTlPM8iwedw?#qLQDoubr%+pH`Zk`0UK+ zPy`5%jMCxp_z^MODOn1Bco%r|^i#+oM1&>3A=xk3*G>p``gA0&@MAptFsSTMBBapc z$m0UkA^V?xT)MOHSarmZgET_u$K41%mIhJk^MxH9G~vew3I2F^f0Ni>R=U(jUuuJY zV_(JvQ(|rOvEa0a7{r+iiv}7Tx*L%rB3Rf$w-YT#kK2ZLR2e$dRqrb)pZoXq?p-$R zEZiZ6>1g_GEGnm?;+V!U5S3R)ab547C>~f~B}u(s5r0v1f7DRw;e@QSp07{1;JG;} z6J(@i#>#FdGm<%!qr?v2@`|QqI=<~ubAr(D6DPFP&*HpcpBK-Z9ryWe;WB$uFq}sE z+IV#5WLAI1oPs248i*Fee0SO&TIH_^gU@l@xv@Q*Aze2wZ?q+FeS(AMo2E3~m$UWp z5{?e1tGKc1e~kMR;8)r{r(O}0@0;9c$qqE8G|JD6yu_`P6#`C~)qo7L-lOLyGHcDc z}8(Z(#%dNrmS6a-2%j zCBSg8?usW6my%>r1#hrJgp|#bFbK=X^D$|{8fAuke=kFKf5g+2oQQSfDe^b=KpxMLzI6Epq#n0(81$HzP zf5ac{-a|H!JoIhSkDuKSg5bYl{BUYn`bh|ohr`{S6axO6{8i5b@uhde#+{83~S7f0?q}KhyiCcenZU_QkUR|BXXDJPYvOIK;!V z07r)4pE|^2*T8?{5RY90|I8t3OZMr_O&`FQ->S?orTZ*Rla@=prfD<6h;_8YVA^)u z?q6qp^zy-CK|v@kFZN8DnR0@{Rw;m}M++=l5-Ye}HZ5}40oDWavd3b5PPb3Ef3@#M z<5X&W>&2HAat56hUN)RqpQsCnFagV^uYM4k&>lOIERyRlWmKK+FFOg~XI^4=5*P3S z4`@^hZY=0Ju5x*b>yrJvRX@C;0D95;9cJC=hHeL05a@2SZNvi|vTRUh{p-OXDs zi%}#GNPH2A7%We_~C4-|_yJl#B)}f1`&bBIBcQjiCpr z9fJ>PQWXDGsk0+*dgN?B6nYf>`KZ)S+^`2E1$9*0v(NNW0{>iu9}`IkbUE{24@&{2MF&G^%JmQAPZl0j0O+8+)YIBW4LzwBM51v^x(veqf9b=P8+ z0r+gpWb;sN&Kt)bqbPQ->yt(9L3z7bm!a-@!oTCy2^B0zrv1^qD|2n z=uPD3tSDE^f97$>D<6#`!Wx7I|7cRZsrfwmt*%lZ!_>Oj~yK1g3e_o}sI|;bd$?20SPp%2PWTRzA zt5>hlCNL&t^$AzKZAEr0{kc#Ar$)*>c@Vv}LvmCbsvpvpz+cT%dU3a^!8a+xyEFuLZ%AD=|6=#&;Wdp+Af^yb}c$eY*e`6|E8^>q|^O;_z%=GWb6zfeHW1sKjwo%wu87F~fYym&%j(gK&&vAXgs<3VT5 zwep~}uFj*v-1W8)Cl^fci-Mf%RmE)TrdY9)1H)yHnRGBIJhf=rmaAp)*4K%b;7G=$ zAyVYv{yTfM*$$eCX4T&U{UA(11ir@+$e|fdq6m&c=ud-(9r*9~JN``bjK5#Cy;N}&ICn0VA5Ij`>TfxI$_j^a3 z6ZmiZ-r@Fu|HkhfZV&hyes9yH@Q}=r4tuyY;Gyy%04mD!G%IsqN%KftL^Jw8f7$bR zDOSlQFZd}vYg=GFNZokm?_nlEC4fn7Gg)G&d`J+1VnC<;+YvYLAb%W0&?aFHF@~GA`e_1aN#+*R|ANsr8zHJdOyjhzgxpPd;#!#XE0Ke zJY2i#oz=5`lQiDfhp_HQn+zAWe+=cX_s$=$OH9*CWXf-VR$-FtmNS2QS{JA1*s=@K zvn}iQRaS^sSIdxD#aVNT&=u)ohdWE!Xm(e`U!SLLIsOsAs4#=1n9pu4(Z1jpo;u)~|L7fBi44=095L z4`%a+Rlc*EC!Zj&yy3 z9!dVA>*$NH1M(ql9OH?89y4Tzy*;K7Wk>FRH%f79U07f4DeoC7vFZnE05>Bac!X1e7>Agujm120RQanSbJy_~3-> zPrrvLUzs7FxQL?uO_G1g1GybM5TyPeyBUp%hvA0oTx!G2sR7BLfcr6KSdN&X{@;oj z_(l3{2t)OIYxY}X@9&Z>=HE%W)c;#am%qN{W5<7oNOHyoW{Y5XxaSnzY~`*p1(8xzhx z4hNLEUc%$Bxed>il8T2FK$0{TDEuSaQjEtT%}ykm`%M zBt*@5z-rT3xZb91e}3oASUV?HsJhL0GHH|(!Wh4dm$H%YsPjxQ217^mg>0);=RtYl z3AF)m?2I^gy1-sCul0r{-yTmpEpHnqboI#?=4x5;NUe{z`zq0w~1HP?y!R*_`ygExvuVyudxRG*E%G3Trl_NJnzd z;c2M?(kxwndX4cc*5H|iZeG#b9C^10VUbU}??F{%f9N4hyuCUGoB%58hOt6g@t@Lj ze~icoy=clwc?Z6wAuHBjPdVPHXv%@)j{%wXKSYwtRKWo}Bo{PzD}1uxVol3P6N%;3 z8!6=3bJ#f=)Gq0jT-Z&6aK2%cIe6s=1GWe8;fbAkB@Cl#Y&ZwPa36X?t%HKi#+V>G zOCATgf3MXvh&k;1_W35o{vp;kg<3P_tTD;K9qVz^b|5Y_VLLSz+~} z6XA7S)o&+om~M4>;eB-|9Y|S2oMVv$?-Q{cCAW+KpLN9eX3! zHV?n2ym+}9Pq34L=h?8kB;K%&zk+v{#vnSznAz*Cu9~Cu-4Q%3&1aJXDK^n=>dU~m zQ!~7Ww9=J_3jyb_&0exsOU}i1q~MmTXrNj!u7lpp~H+ zobfNqrvG(drMjroc&hLHzvy|Y?$3H!chL3qB$oaKS@$0=^4kckKV9Jap;!=^VUR+QgEj&oID)|_c@)kN=reWU53mhN4w#jE3QFi_YV;m19LDUM8R_E9y7$pa`IqZ;6&8delX8kQF?{_E4i4u584eo6iwf1O*zk&@nbo_xew4>2&glbq;+0*O7oG{pW7sov$I83t582(((D{1!Jl z`V#(?4u8wm$UnyE$))29J1VQ%L(n8`zG$@qp9>GMR#+ILj#?v*j}PUFf3G0QGC@jr zKGl=o(Fdf% zfCJC{fb-BpIP9Bl*q_}Z@MrhNDl`)N!4X5t>euw}X%lf38;Q)ka9ud%{I3sYn={6?-8AVAzmmq^*j0kyc9wjAs+UdvU`Nf@J}Uu}-Ii;D?&J7Fc_pig&+< ztQ1Fcew|Oxi+-INtV{)UI$^m!pY{5VDe?`z>Cf<4dB8b;GB0t3f9tbmG|}_c!SA6f zD%9o}0g91|bKXLDUDmec`qin?AWd9rHDDEDQukLmCrUTGa;zH}iGxSLX>bF$Up5cq$;Oi=mWa~l`fkLD z3Rdn!;vT|xI3;wsf7*;5o=C3O^fz%_4E5N?&rVu|toULdJBQk^#64YeWXgecg zY1Z!G(>J)kH@A07Mm%>37?7SHm+0*du_v>@OcjMI4xaTqf0g&Os*3$>v*7CE!io=z zMDINv3EA_+ifj%-F%GT}*TrW`t9pLHwDds4zzibjf;Y2K^zebewuyX2-NG9ihE zsXsmmHCCrhNR4igP)*1DEG)&xd5wqq0i@4&U{A`WPe${6VKg%jCdq+vRe{s(a@ySN z(OV~<|DhTFe>bQH99VD0sT18G`0(P%O2#LmsY61)t=eS%T&eAM@-D6DTCcrYU=f__o%Id+)l;J?>y*ugS4NwjLR^n}bmF7SaCtuR z%P_2C0!EhjuJl-qCnmIG@1X%`mYxRYdA}@I@Y(Shf6t7ML7wn;feORCyQHx>^$+~2 z5_hUAnlN43(CYB6zdC_J86E&PIhR(Olb+bx!fu=)Af6rEXw}*zLK#bmPH(Z7p<~mN z#@#Nb=uI{WIp1KppSC^*gm*o^oJEPRC%)S@4=P6vh4Ngu78VYkvU(h0%k z`Mi~LfAyZL`-5Nn(RxGxJ6Fy{**9q3!pwZP59xK0i8Nm7T3Ft%GssRgfm;L{iD85; zE}cSmrW+bLaJk)ZsX%g<#Hvt-fkk>sb#FYMbL)tJP0)s;87J8L{(vKpfiR0AOkG`H zT!O1N=KAvdF1_VIbGuRV$yMf@x!RFT33r9wf3$b>ov4Vn_iEJRl!akQYNeTXU#r6N5d<&C4Z%suuclLc!Xc$N0G zw&cNKS?ni(X;gazn`id3r?XYm&uFnC&s8JdMKOW53s@+#MvpOf!(hUaNGfmrv4_uZ ze;8D6=-L9H#==s9;?7{#5R8p1sj9{GUXrA9!%|RxXtQ;D3S^b9kg)p#?tbaFl#nlR zWEHE#0J>)g%x%be945paHvMH_=-T_^ABEQc4C|aK|JUc-&x~q#juu0PYJlc?W7c7>wa#{`igu@$?`E7sNq5&OeH?S^TxH zjQmwuBqt6wa!MTl7A}q^tvEjT0QpCZXa|Y#M~rBPwEGF<2bbdLQE?&Ae^cpEf1V*f zM-u2bf@cSuOp${dkS05z{ZpKX9Fzd^ca(qvnzZZMtgUz#iT90bT<-EKoLK+R;^leT z@1~cQ4tvAeY-Q9aP_`(zy#?-2fc!ey6dVqguHNQ0!^MGx{jSkU<=>tgi*D(8y5P2! zz5nmZ{Jxl!z0z)cUmAM+4JRs|f9F`9@=cxfZ(+qRePVxt75~$10)OQ`|I=*(f8{>^ z(`^F(=|2AoUIe}ZMw;*3KraanUg`~0-vJjS(`FZrqe5m0OZB5>bk>G$Y_=J7bk?qM zKT_kF0g$WYj9EnJYiX%q5gTbt8dV1dcuW-$+=QSCXD@Xy*4a84$_q3WfBzWWdz~-$ zOuL1^q)2YqIZ1(QA{mxpJka&^vYq%dD+$sI#+Z`<8J6{~gx)GEYehrX+rgK1#!iW{ zodID0! znjh!hGU7CQHX`dFqa$(pxzZklcH1T^SG>MHp_KPRHBh`=#pS#ycJ$O>1W9fM4(uzn zMTxi3lOGCB#}oz@Qy># zNVc^G^Yev258Ukvn`(}~6|D`nEnfT~sA6urR6RPEPM^M$a?-W&1SMyR*guW<(ikg9 zzc$ay=vIOGHWd4lt6iCIRmohYI$A(+%zz^b=m)g@F z;+;=H?)ahQ?h)m4UV>@Io40Vw0lOeFJrHZb&Rt2Pe=2m1i3Hbs#TGJb0yB2S?&3DH z&(q39XyA5&0FFr)m2!VBkTL=SsZ~Q3w@nq(^z}ZjX;!Z^mQh=jtn>cfHboGz?ouzM z^p)aLC|@Y(`XH|4DnK1|1|Bw2%;x^#it>1Jf_Q=Rr2-#=s-R8tqEZEZRU?1im=j80 zCM&AQf5|qZ;6d?4Ky(%Zc)_asrs^TSnk~&JY~QFVDu> zMv&0*ZJuyh$_f!^nl~3$a4+f@b+wXr`0CZ0f7($0059?^`~M6t{ZduE-9w1|9lJ z|0a=xT9bZAqAB$Ag7Al%nZ_SVJbXwXADYd@fkSr?hZ9E~c*mQ?XO!)+YI>05kMXtO ze=iUtS{w{}96Z)SlMe{QKR~lMwpx7J=8oyJ;E@eIrXGBBERoNn=!c@3WCzIHuiDSS zU8W}A^1ZR8;BL)qJMBz^-A+R_?tCsTgi^j{4@)IM<`Byp8QPx*)5W+@N-X)Ch`d zgJ_su6CbejXr@8~PrRB^^wzhHf9LD1Dqr}@toI<-U z``w`a{s;H>KOOe-o2<(J_b&_k!C?R6MZQ_;|8Um#BlZXqBPbZ8_CFv75-^P8B#2Wa z2pxiX6iOUDROnAFH1#pbyCH=>3_&PAV&$XTh&ZBQggheVqd^J&Ok4SRf9>bs`JhL2 z1_6({PK+Lm!@M{|SH6t!i$6v)%MOW^J-R+9iOKO$9RI>XL!Ww4@~J}Y`$4iJKL8e= z`ci&02_xvCaku-8LsOAGZ z+4QtOGo)GC!kgc?g@XiqoqOWMem4K^LGF0)yT=$b;2eJ4?dQR4{Mb}YxCa2|XdjxR z;n17rn2QeGKU@2&I|F{TwZFPE;Aiy3-?JAj&^6lEL630CRyNiq7>l`P_D&`)kUjv@MW!}CcPF(YFu$9+?A@7yN-#erua`1jN zd+7FgUjRR&FVcoLfAC{F!({lL%a%c-0zIAK80|rO%^@<1Q_fs3 zOt2c$ic$-Yu#}fHQ=tah9>z)7-<1A6i)3OIHp(_gJ!927Ybf*E?G50)tmFU-16ia5 zYCkV0`?W;_jr?UhC}(;9bt2Wh>+)w>|86wsF}&lq6MnFLe}C)3Z}#p#U-TU$MPP~~ zVF*M}3dIo=Ms~vogCKZJ1|cY%L?{&gDMc|sj#Bn9?Fu2#e^!XLLQ4pt#q)%C4sICw9bdAt9tMt|Z5OcZ1S+SgP z+%pk1Iwf9K$WO+c`L`=MR%t@y7LC;dyw*<@%Bzmp*zjKXjY{3;*#%B{m9q18?sWB) zCI;wYf0R+3_D}o;8J$gDYJLoYw%}dz0@FjT!@m4{t!5G4kzygr0kFXqy()THvlp_< z?(nLqHN?JMT1=s&H_mk1EQ5};qbhg#_$>8@8wsy~nog(r3UUyTcz6DsqTpIPX~t?v z;OwtUi}17^z?u%SL3A!5H>W0JOzz2~*jZipf6Ha7XY+P5tQv5@GxJ18BV+PA7^rgG z-x)hyL96dYfso>kK_$N!AUX6}Bx3ySH9rTR&E5)vxli^2yut!&*0Eu09X8JChNd@~ zgnP`!FQxN*pKe>X+wqJ~CE~7 zng?kXraBTLAhs;?aR>Q+x0d;IJWybf6=9d z=5?vDlMAWZ)GiW$-)p=JC$rUaw$!PLHCg9r?jMPfV8|wS^9xh-7o$#z`pQHax`p6r zyfnn3o!;cUOu)r@X@vTSM|>#DYMczx;ICe;wiTXcN#kf5RI;*Rf+*&aF4Qdo1kxbe z5O?piR1^aAbaG$fGz>h|Bho%oe3_xPs6Kwl|;R+$J-XcBgxBGafglf&5Dhtpd}p<3-a+I%_3mx(?aVIs9-Km1H}39 z4!BYrtVu<)1-&?1`gosAaq`sOAgol}G~#6iC#J?L5tse}u)5Ake_N+JvPq}=^KW9S zzjo~(;ngp$`*VZ^Q3Sq+lq8B!2(ky5KSNjuaU}Nk;41--T-qLRQ6JfjBjuJHj6diz z#AXi^v;CZ(H_z>-!_?8_>b;E$>Yb^svwGvDT@+GdANRPie@ z&>avRFp4^ogol>LeJHekrpfu4mt4k3+J;Z z`x^UfcGfF;CxHBRA2s`-H3>T`oA7){8C^a+sVpGsa2!!l)|9u1|v}Y*L>3+^)LQ5*rnMwa)@*zGerq%#TW z$n07Ia1Nb|UbBpE!*K#bvMoxgqf-tTQ}&Cui^obmOXTv(LM~>S9f74y9PTmY7QZ_! zMJxbmaEa%VYK|?OfEjZfU+T~%rH-GTI-_AqUSiQ~e~0*al;VF$(s-t`l$2tp2rA9uAWqZSGE7#*rpvcW{3P$~P$RA)ok3QNpty?8a{0pfeD zF3&&`f93>zG^@RPLYPiPs2X5}TSl}MoI5_P`PKGGcTFZJ9Mh9`dZAQI?1oqK{T@!k41uEX*wWzwyQ^vKD*=8iNFIBe z%e5+`9V9;xIiTs-u=4eSHX2n>_sH7uY&u>Ge|bz+Z*sB#^H#!#3JNSaz&|5``rzPn zHF%yG8SHN&E#Oxv2^5jaeyV$O>XS4Qo~?3yF8I|F)bFQZ0sqrozgr`|z!CGY@DYzb zT_~eYuF*nfRNiXIlh;S2^r8R4Wjgi3A?n^TWH!JA-!oY#TH03_fzlKh2`))%n^}Rz ze?7{p%Y+2f(wjaadl-W%aRYATD#lplot~cL0i4B6g?pi!6G3-nPA!<5SN(yjdPBq2APLU0&shiq5=kU7flQ{7Yh3TUz z(Es6SOJ;m5qVEfD$PJf9=xoYRFVlG2~~&81IIm%*^{Oq)Dy?q>UKD z)ZynIS12gw*QeFY@%AJIQGj&8S$v?YT9Z*8Yu8b24JpM{D0!6)=&0l0#r`4Al3Vj-aBL{R5e5YRoC-(E!nVr*u^f4RMwTam`XaNSNXHj0GQUCn0m*bx<@>?&*fA1FfPM!xO zFbspqpSRPZ2kuDlqxuMckO=WXE<2VvZC%@hfi)yBTAG0e z#v*kLJwHl{zlq>*9R233+tHuk`Ldu~9q3!PtbBCKq8g_y`bNJ$f4QpW_6D@(n8E(_ zVDrZ<{@D(JKilG;?GQM&_}9B2xFGOfI?s^>g$==ni`TLw%gi3k7te+1G@z~)4I6Fvl`zJ^2h-*ancFEi9n&Z_4 zzanb0De@b7Jl^|_f1jm|P&K}@f6kRw=5g(-p;k68w`kFVF8t*DS zsaHP=UaDU_S9C&VjopZikH@!oeI^JAtDCg}{9v6Ca_Rb)f3OA~)A}JbjA)i?-I){R z2h@hAC2xw{zq!PFODX~`EF&`lAJJ>kEhnIQFX9QYLz!l&fsV%v^QFpbol``)15^(xZ?0B3iaWM1cds}4}DYDS18Ixifld3@PyllBc zcBN0f!e$&xyz2&wALXJ*W8nc;^*Xhrmh0C$dNNA`eIJ0!{-&*1Y;k#oK;$CY1oVuBLi0_p2-cR+p^FblvNc}h zXuo8f=3FC`s?!%>N*?fiw9m}rE;P!OxNLvDm6rKY&jLN8`-{<-EaBtRXD}-Ew4={HG9w%cKp&uSDlNeh`bu68O!gYm|^CDLgjRJ zuP5iRzhppTY?Iru>p&+DXMTb4yQlE88>6iIf9R%7GMxljB~z%l&NH&`V&Gdk2LsBF|=bdk$DHb`-VS{dTn1RXMn ze*)g}sPh)(tZ#f3i0Ga{&l1vJGJ_M4QR9YA6ejIXoEqes5#`sieWGseD|e>pV~NTD zwkC)>m>SnOZ1OQ=-so8lp43zd@u{wDTsu7q)RC?JCD{(j!6L}M3%r%)%DUv3Wed9kXc4pIc0M0v+6>nPH?SOQ>WCGJVgLa-)Z(O zdT<*FG+L@XT$oVMy&p0}Cnmz?Hx4Hu>Ske=i%WEbpJLZ@Mtkleg~;g@0a%~Be?ZnC z47|lg%4O9QR_<7MwVR-KZWlUBL1{aMBaV#oUxr_XY&-nFeA&*`eEnXli8+!^#{d2H znBjgsS-8)A_617ueRhA2e`u}!f41m1c=jg?elJN&5IBkLuLp!0GVI7)UDdF09v?4u_O9tA`U+Huv-hYuD9nnJ;k zED3p3E_Wn`#~&UWd6XGr@>q$=Ke+2NGAI6kuKZV-G5o`T1Ia^y3jfT=DTsqsmwtE~ zICf;}jveCvhM}WGnPVSK_V_rb_#nz1{~hWo(1BAy=#vm5K9h3xgMT_Ee}|h6JU9H0 zCT;fQOSgBHD$K<3TsGn>VV6J1qkQn)^kZg>v$_B2EoCcB^1#-K+^NkSzTN}rZ|sN2 z*^hg?TLM-`K}lzQBXbG+_pgIK7IFMtW5dq{^jj;wUGUq2_7|Bc;P*OHX3cP>^F?A& z=lJ#DVCVzMCd8TuzJc#&>gcj&(O<~bAI?P<*q+|nu!m{3yvX!7 znUwarA9pRA5rG8p=13I4*r(BXdgE4*9(rcm&;}lYfjCDNP&y0}el-#y*f6WSZ^bj|ZcnD)9 z+pmxV@xp(JZ@`}|3~J;?JqgYZbA&*ecC59YEAW1?u`GUEBbksOf=j`QnNGP-KumqB z5&3FYc{K$9BtYhw7h^ z-Td1RWrsVde+o@Ex>T*_Az!Zu%JU&{?<@9ZWuRw1b63ptXcGJL8HtvQf4`$o1BhPk z)g<9wg)$-pqsODhZ20O-s1HwS#Gv-&S;_dLsq+~eC`Wvh(vjkDNm+8Ngo6ibdv|c~7p=sPQ`~*r?!1zZ5@YtO2E+KHrbuPS z*ZpEBJ|^;F_~8h#kBsB~5B?A+bK*0;7eBVTfA60{kEg^w^u&X+o*$$g5IvN`vi$Jd zY5ecvPsur4dX$dZ$8U4wuJ<8hPHzuvF5Yx9;9!xSmu$O1^B+e)edfryKPWZ0{PI>g zy8AjuPLsfC4gKMj!F*{4Lmk??KR|{t_H`VHd~8UNtda$7Hf{8Mgiv28(i#WpvFS#N ze_J!Q15)nCd3==^{`ffOwgCEOZrb($azKs0MQicf!EeVj;YY1SM*E%HQaOgY0BI*Y zK0DxBe2-}kZ;K!0qP+9w-RB|ucAd+OpEavogz{U`gL3?vAljZ>0`85vdI#as`7)X0 zuMH+)kr#)V5B{YC<|Qz~Jone9UBTs2kJks3BYz>Cq041M;CnSLotvBGHFPzoZ5r%s zK12nH!5Hm`d2#^cYMW4{tDHiU%h`#+JF4HBXF4X*c@IvwxrU1Ja${faBNLYsxR4hn zTCN$w%;_~T762*f{J~k$j6I*{a&%u#%)=#EzRp9k;x}JbK`b=TSC%Awi*4BT1)Ut9 z(tj_flg#-~96$=5N@}*fGb-o=EVfPAYM`Anpe!%3nUdZ*=)BEdKT6t4OCwI>Dmx_o zh{qC^B^bciquzqda=kpp%zLDe3ukosv~6taY%r7r*q?fMm1%vc>zu11eGis60Sld# zSZ|UJ1mc}uxdm<7`gKeu`MZNjfKP|ncYm7m<}5r~RlEe3?dt88Uu%MMm0|d{tUm#a zz_-aHQ*cFT>K*xS2*zXWkz@3Iydks(o*vy(+Wn*1pYhJ`QcOfeh;&pL9q4E|+PWA% zU(OZ^p3{4Q8E5#Gtdm0<+}TLl2eS}Q$=cw1ubL|=Q+%jiz~@}353<5KLj?NZ7Jm`v zoZvX$T+rv_EN5tAF44f@#QnOItw&vznzUq8H!?H5zOa`yG04Ev&S~w~D{#N9=rcW6 zH~OqU%cm3sGvewOH!Pl>kiCK)%`DHDTlDl&s+ewNt{|Eb)pXgZuCllS8HjO1SQS;5 zSJylS15t#p)9W;?jc_mXc}kyZV}IXm0xPDaVQ;HF;HR^$%kMeTHoF8aFMnXSvm4~n zwNlQtm?}j*g{3s>+w^w^lN?F&x%*9r@@u2gm%;DG|8@I%%cuLloII`idwVbIeEfK$ zBc%I!>0iv<|1UnzZ|VJCKgW03{vDQ56bORQ(L05Z2uK~|3Ia!W?}pxU8 zKMolAguVs(QQ!Q`C`;mxSbuSRgu&ov)at%w1|HEP_~#N$;NcmG-#nu!XNkEZ$d)5- ztA1V|L8a>WA6oChFRk|__eZa|M?K}Y>8hf)yO&^SGm$Jk(Q9H6d+iZlAN$^RI78s@ zhRU|PtgpJi((RL!hZ39l$oj^9G=%WCxJ-aDareqy3+}P#hlvk-jejkZPgjxTO|tNX zED`x;>2w~L!PFYBpGJXozaZo`dHHbtIU3fuasv#nW$6HpxETo04Lg6Rn2}R zrf0di=b0(&vEP(N=%#GK`}v5ZZgwxN%+CotOvD_u863^DP364Fjy(XLYFxXFC)RrO z_L27o>Dw|C(s05PB|qKSk_{^F@#wQ_Aih7GFghpXk3Es&z%bQA=Q?i6P$J{rl1MUz4iUuyPAsAS87iqoA zYB@S6WA&ux7z%IRS$})64A%<85xG<63(3>kiwA=hglk}Y6DHF>GitbAJ;aZw;vtG% zhK|I^KQ~VL$}`)W{S6B|0s=*g$=+F<8y@Ex*jYAwz<-E5B9WQcdF$(Y$|yA$__f|w2c`MJgT2Ae_G|8%PdJv@7T5?}VOyF}GOwFV57%bD z1$ur{ihoGWzL6Ekx?Os+f?gU^G3AVE&;(&UfZU3)bDQ^|^4|FYYvCQmJeS0TXZH>< zp>q$FZ!vw$XbH3H+1%y?Ws}Rwdyzpe$Dg+ z!?aMg(5o%npx4Am@A8U~EtDoO1q3<@RvX5QGy%_9cxElb7ua*&RZu*8iJ?1>B|sQG zn18i_w6%NMcJPB#wW$98nR~NlH_>HV^qpT(@7!}l-&DkT00amGNFYGK8-3q{5c>5O zG;NpdHNC66*1l)o2+tWZ6FwSAnIkhtj&#FlU1}gI?#lTbb?vnz>Dx8R3>I2F=4f}P zW+1y0%$^iTfrsXzJD{J_@=L7lYiQk<^i}DFY$CY-DxxEUGXG(Ayg0$&cyNY zd^H;C61P(TJ&lVNlnxaqQ_J_bWJ|G~#g{`708A@4i`fJVYA9FJjlo-O=3*A~yzWT} z*ASb%Q!crw4LUlG{UWX&QRh}tcRkaTV-H*pb5TF=m)e}TX7pCf?OOS8DvzYen}26J zD-XHDGhwfC_ihfe=X{~t7YBoM%z{Vk+yVjV6M>{1BMLc}S)4W1gb4*KV1>4#u~4sF zW3Hi9A9T27d4Ha!Iw%y(dkiO$%9;Xj80D;J+2fWb-fB9&esb35IrIV?=Xg1T@$Rm; z%4^!bdrm%*b}c;bReBZ3CKR>gaDN5x3wGd<(~QvVlqE-P=#W)pDq+FTS(qi1LHa4T zzMfAFKl)78JFmwQ)mQSxxMMd4IdB~A_#?kv|1q3A#Z?tIdH4qt`cYhOFZ?F@!-&V& zAHwVpsTrqyns)>@zBBtfHiKY%gMhztbI((} zcoBz8lzbmp6zsk$J`}1jJ=kr3ycHW(261+!M?NnjpJ^AV5OyS-fwKsPq zd(+p3n28UzaJ;8-5Pxr=LVwukA!MA4qZm%yvM^c9|wH_;vgaZhrnn>WxhPqe4~wR^`)KoaCCMC>fA zwN~P*c|%p*2F>1RGh9CqsPbcX`g8L-;I}q@4a%xOd{)5*;Wl_$xPLoc_rARrg?WQ# z8x+zi*O3c#ZS{r+qIwgcr~XWeLiGs1#XaS0vX_jDjCeel9K zeRV{;lGY(Nqw6`>93oSBViC(0P!dV=1+m8n8-4aS{n+zp{^ARR;$`kK%`}<^Fs4u$ z$9XrSM)=Zyf{<+seO-9|8UpNc=zV%gl+;^E$=~87eL`SLP(X8GXth?sm{~@cYmE@% zK7q2pKY!)Sd|#gm4ZB&@LVh@Q1ccLegxE(E%9l1X{*p7pobF^`8%m$Cu2Gn)JB4?x zf+WD1>~WQ1dg5@ADo$8(>A1;L?{>6iNA{c|wE!OB%ncD^eS(=#OlwEw?%~S(JH zpkm@;XBQB!gb>-sVnTChS1LzX2jqA^_NFD<#D8fCq}$09H&h!P&RBUB+UKG2507>N zXyqW&mKD5;R6iH-Irc9Qwp3&0`sM+v?=qUR1(Fm-V_3yhYJM!AX{1Gq*YVgnwY zlw)`w9!Ogd+RegBy>&-J!5&8!%d0_bSWp&4Glr<6np>K2(}!v>M1P>q8%*|rf)Yn@ zw0|24p&;ndyA@Yz8N&CDl(#+e}@^PItyVsue7&RH-(D|zn zO6cz21`}Dy)3XR7|KYBq#1KO2%@RQeY_Mb_pm3K#<+qSj??XNCgO)A*(wHf;X)#(5LM6e z6}cY_s^li!X`N@}oi(>c^~l~}E^V8xcJ3TCU5@>uyo$aokzUc9&Ol7&A~Kw>wSS34 z`~uzhn6&!Cu@fAZ!!O6A&zbm+h=Rv+T`7m!U|(i-ic`&Ag^e;UsIcVEZ918IM{hQEci)O&j9h8SV;J)#KNGoUfNBU>oiyU#X2 zyZ7WHJ5Y?1Phl;c>@$T(c%MB=zkk)KHZ+>N2Nmu5qU64O5bnTp!^rzA=U~ry2Kyw` zU#U}Jd*|7PPIrZ=T{B=ORO9|GSg_%67~hk!2)Vb`p~PDdCEW3CK)(x~{*JKssn_&g zG~IUI)lkAc>AT+x^(Tt{t@g>X!`ia{9@hRS>$5?){u4;MBRJqcfwVh<1Al%O!Wl=F z`-JZJWL)TVLBgU$-r( z-@a>^z+YsZ=%(s#>gS#ZJ@JdjM&brC8}+-(Zr_*v{Z#|MbJ-s=kbX?Q0;;7i`q%91 zYm&VCbKL8=?#i+*B9z`8-hW?bL9K7d)qZ(*VReyopT_js7KVW6ozlf$5^o>%^@AOx zLwvf+cWEYz>}`JRx(811-5Y|nPpQNBKJAK9`DP9$se8V9Hw-uSFN{qXv{!Rn;H_q}XSh!+$+ z2RQ8P$qavKlt+1dAO{c+(-JRxm1#we`?&Y_!AQucqRy#eauI3?Yx`$z_;CkRH~nOoX@OtxbR}j*vM9 z9d86&3Tx3sAuf21lnKpG$Gc#74PG1b(NCzqDHb7$k9cOc&k@q^;%yc>p_DIS`5 z+M9ysx02vD8!Ha&-2L2x{M$!=>q7qS$RBu>BtlUHjZg$ZlYcaXkpx6w1PLJ+ibEJo z&=3K`$ftH3ddu%6^e&nbeMncKd-5QT_6A-o+MAJgeHUmaXkeeGCpOa>L~oV`z3WRs zZ&D=+?`Z@$+KupLY*BpAdq8h42!`$%ihW_~7j~Q`cEcaPa~{ZB+-0wop>L7tkFI8a zud?m(hxpDA*?(T|mAK8yL%ZqU{1EK#y*QhpkK?^g7rq%RQL>l*!oN*V2>x!z;cw{) zgOMctMILYZMe_{tn$F0D%a>2tUJGEp{#AnSY%nB~uoyvt@ zJE{Q; zehEXa%r2Ufw%j`;Iisz7xJ*lQXB8A*l{O<|?0WQ zGUi$YXn&oH0e>^6K`0J}e)CBxf#&vEuL1}_rb^9a0i#ZnjAU`(AADAq4>oVTr*Cz& zSFhKkcrFhD>ZCMy;p(wY&t!hXK(uxU3t;erY2Xo6TWMi|DK?bUIl$BzvMQ&LSZJEs z2XxG}c+nQgm|&`4D6j=*H0p{Ctlt5XI$wBe0)Jo2qdkx|syuFM3=OHtO4aeiY0O%u zn5~v1d-ceyKJbcmE{>qQ#RUf1uP1=TO~^%0qcY2nJD#(cTZP)G8l#yaOHs!0X)f!W zs-F>~V>%fl26qu2GfEQ$`UEX*TOhw`X>Q}7gjOkpL>@^VoAdMJ%@lK}WH!oKRKG*# z$$w)Be_^A}Iv?pGfdlAvdRY(P%tj;B6>QH`WrW}-jT|n6P{qCsAHvC(pSs7MP29Uq zP)*-VnfD{Vg`Ug_^0PX)H^8M5=B$|G#5p&7YGd~p0xc)Cx#e;#BIdyc5*R~mctcfz zf4oFuW_2EY9!vE0S{t(gii~2nN43Dh^ncU_{WOmoY7m~%gh^z5_Gnk?^LDMdm(tVe zq6clO9(Dh8XA?eHhF}5u(<)vdzrAt2gr0mq8mPqYEn0LB!{w@#=$H;iy=@PNB)~G8 zgE>W)VKmzuj*Fw51Pm5gBko8%C#bRRqG?SCj!8TceLb$?6- z70w8+bon&)GgTYr*#9QJf^H(O%_r z;?lE@0~dpNNtmo&Wl$WeyL$?-E^C*(#>w1&P0pp27jp}npID=CX`}_0EPpD-=JDzu zrpRaW1pkC~7gSaRumTU@cxI&eF-7DIYR?#F?$8p&Mx{~O z5^+GGUHPhe*>zy`m~8nepU>v8JiW%}sT0ojnd~u;KyNCn(YOI?L3Gc?FyaExW8{|| z0_~ckEQU6PdC~Fpl7KK5#((#x2nV`7;@+HWkFnaYo0UTcxO!yZQgGQ7EBo_BM*3$D zU`E=Hes*;2hk2qGvT%Y`iE}NAC`UyW+B!_+3JJ-9UUNv~G0gL^K|mCZj$qilGC*yJ zb7P&@u(|V(RA%*xZe%{UAa8q8X?WN^C)d<549w|K(t`?YAXE63@_+nmc``T|0r6%u zpK0hmn?r11*x@W=JUfDweKgQVyvj9LIyy}U2z&sG40qZQW*P_2rD=#oF8qfCr_28l zHlOyt3{KsNM9dbLGLrYl*KOaUslVOxD{}hzPd_k=D4Ze*6rzv~HDMIB;ioME#xa6I zHXyaVAn{LmMKSUxM1Sv`)GbVn&^Nw-cSSt-UC`M8$R1#ldyDR_iTrs>$_8=pcl*lD z-a+=DbK89jMhWDtLHC{vvSA%O*yq8~^e&{b#jM+JV(&gJ}|_KDStmdK`c5=h$b5^B;l8&SzyZ(JWz0vE`#_ z@}A0m0WQm;>z0~Yr62?Moc(U6H=B03?15%cZo!xJX7}u5T6G`ByKg^!8C<@d{igu* z_s_nc9QbRe-+w?rO7~1potoA70a#gWT2zldF`QGZiz<<$UYA@a4)H0!=7C)gB;L%I z96w*W)sF7QDbR8PwTv+7^)vxkHZ1Sw7QJOff}4$HA5dqxtp?+rwlAW7T!ey(pkypw zIHZ&Ps7bJPIeASn9HrpJyny>j2&bz(1e4BMN@^zc^MBkzjUlnk{7{)r6z>iCARnmS z>rx#09xm^d?ZkX>)HQnwz{DOMpWlK;{z%|x=)|+D6o?&Mwc59)8Gd-U_cPk$k~g6l4lI6z8DcUl72?5an9o>VI?t`=Je5tZWH?J1YV+3S_j5zu)w{{5x zM@(yC$qoU$+dA49P18(l!5Gg~6;iNJWA6eJpz&xnvk&(Ji|gamhI-Mt3WWP12@RBX z*MHS`0<2z=aS;>^tWAhDZ5&rkhsBfw!S{10fWIQ3)K!<%{nwNM@Q-r_*NA>5C{M_3 z5FxjQHjwnCfG5C}yGcEMU@M8yQT1po>f-X-f$0XO29+yCjvGZrw*+=GI6?-u(aBgY zIrWIp2nCe-V-B`g<5R@pRk+e+C~7a|^?ymd`3tmSarTTJ)WUomk3b^`RgEusy8+8A z(O&2Kq#e>z-s{hT2`Jtp?ollqXvVk_q}=6%8-P=-+=6noK#GL{!+BQ6Yrbj}I36ib z3l616V~6qdkeB9hJkJ*pJ)kZcRSzU+i`RfitJJC5rS30+=&FEXYmkjtF&rq7gn#j5 zcXtaJ3FQJ5(7wECp3O_7Ex3o--Nlkl*;%(K$0xfBQC=t`0Z3AMRT=M2RJEasB5MgV z1hWE%$i=K*6`?7EsV5IYZQFiYXOTnd<3%C0JjayqjA($|Df8)JvFBy*;4Th=nQKL3 zAtZNHPbD`oU=n!^3C}l1W5E0lx_`0&ADgsg*a*gp8c00638{;Jerzt6;0fVEK@UN? zl#RL;>+u@p#NpnuzUV7T8+ z8x`m^$&b{_t>IFPG`DFFW)DT1u>s?U-U<`V0(6;gcN1PMrylw9!y*~2#_N8W3-P9x zZ2u?*Gn|E9gYVS9BKCw-8C714=(3rmr7Z!Y=cR!Te8Z^ku%JeF%)lx2h#DZM6qF?n z9jv+?dN5KGgQyzeIh%@nVt)!CKY2Qbgn5`l(=4LM)iIn!qs#WK6=?dmJj@Ec7~ zOzpqg<_F2Uk4-=2ss5{%)Y=9yfAXgv@428P-sv((@+QgYE)fo0+y3t#xh1!+b z(P&p}*$W294%2*eM}8r?pJv;LzLi;E;w?6u?1*YR$F3anp7s_7dzE9mf0ykBKS({q z-?udBZ(vb>1z7uN?Bs>lhk|Kw?7?Drd9AC$3)W9`U4YGK;-6%Jt~(<8k_8g=JDkNe zJ*!5!2wziAPnHet0s-t~t-f5$PiAoZMP6z5%E#n7 z-6JUe0$AHA^siU+rN*GAfA4(y{v2+<0$BV9z|wbswfP)H#?sU29BWPigg$*r1r|!~ zqzhviLd#W4!)fMTD=1_5$G}KaHeFJEiw(!#)U}~yA1xZ()bu30JQbiXgd|ZRNW2nw zG8*i5&lhxx@u)~6*CWb9DYoJIsQy~vhpF-T`<{8G{ zV9;-r4S>B3_Xi9h>F!IiAW~V>O4esisKX~I>1NtWpT!Hn-gU<4oJ$pvuX|i3j+>Xf zrKy&ymtl6>nHaF@j7!~-J&1>;EUXz?&!R&Q!82}-5@6w1f0g5kqUM~dM3^|5pUgzE zE-z3@y0W&0tse|Vmj301DgCuhZk!EG%!8h2()v3h6^6fN~!miv}Y8( z_Lg&sW1g@NR*%@!BR4NJp*X_he6QrdJyRA9Jsv=Nm=j&LC)GA7eY&2Ej4FnxyxvZn zk2p3KM#8GUe+)RNB5z6MP_CKowW^-b5|U0h7J#dx1g@%~gW2)y$a(8mk&_lyc_uJK z#jIswZK@5Br>q?##LE_?Y4s>}Vv#upWw!!D?-)tY>P_p7BG%(~^SZ^oH~sMt@qgE$ zApg*zApgLjAfGrC3ZW>RCQuxQDVRdB&AmVbPSFSse?gnO*g$cPRLK@_L-?RpyBNLdV441&WWM>{PJpjO`T+ zd>1_p-xJ)iU~>z<8rTuM6=zwZQkJCyZDhXQpjoORUs2;Rl`nr+|o`Kyc_E#pF&cSt-aj8sIB z$BV;BP8S$a*@q-;;iw`8nSMRq>FG?U1y-H{e-F4rhr+z1qE@2nhAHWz%naqEZ*ev^ zYLJ0vuE-5`i((9_Hom7?P0;H&g0h;R)0L9~aCEM3+l^{qb543ZtSnfn3vj!?ZpvlH z)0FHcmKS|g=#dR-Bvz6hy6D7}l}-0Sp8=|wK~2ARd|?(8VnA-b&i>MWi^yhcS@i;nx0ZtUA_cC{ z9!Ahv#8ekBF7;dl2fXNo_63mY1nIbg2V!;8#;^+xPn&_41jIqw03XfGifs-ee`G1G zWbA5Zr^og&X}dLOS?>Uc1OPhL+Cq3jUZB>UDlI%HYHCC&8KMKK&CM3igUo=gHj`P0 zK%cnV;@EueLbjP%_fP_$8J?4xIzTzX-fvaC;tXCHd>nBJh{p5Z_6`G|H4ihX_*3uj zZ8=_9RlVxMosp%3d9AFTROX~p2=N6z)yOdSqjNY!;GZ-EJ{e>~$T>z;{1 z5#}GrYc+>ej<4)Ub^~ls(^Vg zC_!N_YrrRwKw)%s>gz*0w%N(*Z%`wa_vb*$mrzHUJ0IMAJy0xapMWv3FG~P{#9FS& zNyg)<(VViq4D#I7Ck;;&6M88%#xCo+ffMYBfKuym4SgQ)2NDuAf7{w_Mxa1LV)S^c zA0ENqs4CS(TRp1mjEQZ7Gz#J{7*TME8s+QhIcbOTq=|{AOCnLd?mT(n0g^aZj`T#& ze$+1FJnP!Ioi2k$Nw9K}7q8C7)>d-4yd2Jv%Y&zfT*_+b4iI@TaO?!ogEJmO^H>uG1Y0L{035jGJPhfH(HuK)Fl=Fng#00SGD7)hFZwJ=s zKg2=kKbN251}uLGxdWAmdb9Q3^C9<8`(1}fK4yY_KFDWh`C)rg^afeb@QXJ6_7A7_ zHW32b5B=zILE?SYy;lX@OCrRtv_jtfFMIQI^47#k;9VkhmmWcO1`76;#fsiUk1g)s zLT41-yPbC>uwaKjk-t;>VEjEd4d3e;*hgt2+E48NEe?NTr``YC^A9)(c5aSVyNAm% z`$(L}QF>i(KaZCz82mGt`G1Uob_fJ~hk;(di-CUV2>Ev~&<=rsHwJpK+fM2a1hub( ztu2&0F_6TtTvhs6rQ>KJiL&xoFC<(Fc+l*(!#aUim zXkW5+v)g}2mK%0N8$ue5ucY#pV(=!S0Dec(;uEhl0Mqnv*bYO7BPX)Busrb>F9rg( z{LI5lHJLfGB9rr57*!guB`Ozr{I&YXR>f<_(-{)T$kIh2h z(N2E|!`=c%yJ3KK>e;Tu_f`f(sXcAC$4>PA8+*rrTig}Co92@DIIx{ShV0#JyIl(mkg6R<@f<^Q8$+Jrl57spITRpE_LP4ji6O)bUFQi&9jn=iTe=Htw(qq-k~6_E zg5VIRdJtbi>~f(U%*ZK}EdiA{Dl$hebDL-9o`pe4oZU3wHyeWodA*z-?d+U?ouvXk ztyKM-rP8kB^VL?RK1lezu9V|TN6RI)d2#Mq9nw#7DJ7sr2wzsgx*Z(;oq-J#fr5PtOTpupb3aU0&+ z%sb}4rD&q}44`*jiwgGK+6L)jf3Jxk^nRLMnvU41!`nJLY$W&6%iow=-rZRy(EL}U z#lI-keDn`PKQ&v}g7gIbl3{-f%_9_5Zqro~2c!!BNgu5 zSkAvcJ0NhhD&SaT+hBS3X%3L*!>_AB;)T_9_9g>)a&N2>IWinLR;q0K*_JHZyr8Hz zXaUa47cJ7&P~1BV?=;{qn)JiA0KOY_^S+Pv1l|; zsp(-BU(^FEc!Gb<4t0~Sg43Lagh66ikr4-Q{ZOBUyPhoAB)>8U1rr)6u^R?Kwv;Lo zb~#c<0y)D}Wm)P%9;tS7XQpo1!Lg{k1CZaOmje0%Hy~BdjtDwkLq&wh=kr3}Z|aE8 z_mZYlg^YV$=WlHl%qARhjHY;E%wP$?$eM`7WCzC2tMGqgbL|iPvYvT3 z1qP49WTnp&U{mQQ;RP@LG8FklDDp$(m1_@J!mmY7X0`BCoRJ<*bt^;px#Pzuwo&+H z*Ec3R^(M~LIIkkfIZrZxImO^jP2pY=6I@Bh)uAqgtD=oX2lr{)tf`)TGdBp{m?aP2&Gqv|nno1F3 z7#go1y#e$tQvM!_ zLGMHn^0i<_Cws`XMSmNf*=OM(d&cNXJqv$*cV;4c5C%tYD(c^MU<&zOq(Ql_z(7j$ z+BrPf6VbGvcI&v`0R&)22s(sq&X8HY3cQ}J$kr|VB=N@8EW=cX?3>M3;?2_7UDRB+ zZR^I>LYa6B~@APWb6_bUsUSe2XX98yM2hieu>$jeHwrB z9-zUhsXjIPOVDOYuWkG0hD-Qjw?AC+&sPlm?j`?x#lSzlWJ%&a^LHOgq3PSlo|9xH-Av5QEJ13_^NbzLktFZ~ zjk$E#3`3Vpp4lfu|E z=Zo-6k;F^tT<#as)Mc7m@q6v|BO3=zh9iyD)qrZ9lI!6iurn1*4dxp~fO?`zVz1Jb zz^I7~(~j);rjZEfXJc($B}7hr{pm2QC&CEe&7y%I*l7_(Mj518!JbP;^W;^z7`R9v z&*QXE^?Iw8Ml(>zU9K0hNe_SboWM>~)n0*DI;sYdRM#P=lQ0f6pUtXQ$3{k?o^cO7 zcT#qmYCFo=##brr5gqQQrRwWhQA1QL0R@F_sS=qg@5MLx<{gnl?!sb~RFNZP$UrHH z1D`Dk#ZX$$bmhsCe!LAi+8m#v7;``&p5j?~84CU)k2Euo#)QnVj@y4pfwcyL^)17g zlG2O~CC|#Tv>c_;_Bk#d@oijMX8S0!R<#7_s3+cYp+nDssKBlq{DM?PTrn0-**AiH z(pj5fbhog%;0jVKL}=ZoP00b@6I*`B0$db%d*ig%ocpcwx*iL{|^ z8xl+9MA)P0U6yU3g!rb@t)ns+=P5x~z{^EaJ+1sksQ8 zGwO~xj6tOvwvq4BDzC1qwzuT4Fa%HbzL^-;nKc$^IGxY<@p2A@h33F0Ih^&=Xfs5Q$Q9>~ZjpTu^_(9TLd}4Gk~lmLe0GWCj2qVKq13|(L{Y;l`1~cgOj7&DBd1_Hw)5%s}t?MJoYxg8HscoZtKwj-rcD=-# zXNm~ep~^xbzEOt+RfG209?vzhtRDmkIvExa|{Z z_6!Qw4qZFE|NgTm|9`U3H+BAhxR>(r1|kXZ-~Yn5>O~OlmmTs3AAf&R(3^eg@9-~( z{`B8E`q+DXU=aL@#)|KNZi8hw@g4*X{TakNf{foH$sxR#1UKLXy%*iEnosSSnr-{X zylC<*&c6%I`8yumxkt%+_A|1lr3m_Off~BAj;L>=2&H&k1L)~9YFB;=)RN;*K<%BX zE;i%(25PCw+C~+N!hh?#88D|U0^BUC{fb2Tdx`rquxs4!IT9VA*HEO_FFL$W)np!D z75dNb&>t@X1p3BM`l?L~d@4@7K_d(L+LP@r>J}2IX85|8D*hqp#(&!eyf+ps^-1pk zqB6@z@`+glTeLCPQJ?&m;kGOjsw|W}fE|?Wm;6^YtKEkR?0?ycV}Q!b_VY2Br{JF| zM}1^DHZN(xnyS>=0|;*4Mn(UZ0}&-bHuX=snSfuCqpsx@_0=H>ANu*R8AR+}<0}@- zO_E`U`=h(Oh+Bmiz*C4${xd^;G*}Pbc9eeLgbq`@QM*?%=A@W&vFF1oY+<4MuyW2e zThE9tdeEdM?CE%spWxN?#!jc1tXVi(SU7Vnl%Wxm<7<%A17^sk z(;t*U0Nf+$C2V1jePc#WdJJ>rd>Pe*Twj&aH$;=Kh-C%zuw3s%&9+2q-L-la2!mus z8s!05TYs5zE3kp5gX`rer`zYGsfa^DSyFwLT!ojJ@lZW3T%?%o5xTm<36nC4$X=Jp zu|vSRVc{92{4g&XMsbWt4IEqsCw2o`*2{tq$D~{iUL*`y7ShCxd*rE$^iPk zfM?$W^kHfF&E*fA<#)N=t&}{i%`3m#pfg+^4}WFMDfl^gSMH%wStFIm*tpN;dbpG| z0IJ=yMnUsX7){0#9&uP-F;l)mrK`Ipm_+nCh`psfImwh{=xY9BU{*s{rYXPYc?M%Iz~ZvL{#`vV3_i|(qd&j=c{`Tsbl zoqvwDD)m-&&U}%W>gapk-aQWd+&-RtHkS1ZkxF7W=)pCl+(F?f!xuLZZ6NHlr+kLn z63#DbPX!~&9Jhx#MABWqI$2LEwbFSZIT^VzKZypk*q}@*yY{J=kJu16`B9CLqQx%D zdhvr^Bv~9`kuc=VBA-wm^sFQ=vg36Tg@4Y9o2%uokBgQ2*NAMibOf$xo2?x58kB>O zdgU`x1vm`v;FvIlk=!Exuy`(x6c5JSqLQ(i;fs7EFN~$(0$CadpvMi&lM9p1izuVB z#dA#ZIFu$qgjd%u*J^ylGYA(Zo0GkDL|eg_B`JGGM$yNE4gulJ6fzoxQ+MpM+<#_z zka``dIY|d1m#zK%*>UD?DsJ5~bIXn-u zIjjqcTu6~P+sn*g5V0<2ws1X;u6w)};G9Gt@0}0VzZlwsJ`MMVsY`>sS_dWU~BkhK9Rh z*?$W({MACg1r4`Fet?D;jgtsU!6-tL2uxteryWik@Y-iTU^^*+Om^-zmF(R~8^qgb z2gzP8gUNkp6YBp-{X%^Q-G9*+8SZN!?+Ma4_HOSEb`IBu8aGr%<9n_xM)uqu6z`ce z2-&dTFQ6g%?)VOZoz=0oL_u$whQF^!6FV1!M&1=e|IP4-_ln=HV~p%eWB#6f-2S>N z^n6^wt{b$&((v72PT+s5qV}Fro!!1c!zd1tl@i)JkuF^7N;?2~6@SofCVzm1J1PYJ z6Ii&TLf}7vg*z$)K7oaT4yixE!t|*fnb(i7@M9bBpTNQ$6$1YWEZk8c@Q+~Ou5b4G zobdsCqkQo8+}lUK%T=VQsCM%MN|npym3|UMhN_%$$-~=?D9RdV+38eO`Ji}?1dtvk zX<^zBSgl%cF}UpjQh&mD^Nj^UP7Us5i0clpaK7y?D>`W*`Xoe(&)tF)qf4RdkjJXc%w|9j9wF- zJazvmh{m9XlFsHcNgi>q&{({|kNOma1L2&4@3!tZ`D z5*MC0%dxu2TrEm+=c zc>?WP>2$D{Nl;{04N0)wcJB3-XqRsK1|xs|XeuIhyG_1DMhJMXv}{+tt6au=td1mm z%pbigJ9}&&@08%p1<>$bjlsjejpT`)i$Q-AOOx!u?Ox`Mim(4NQR5xSZx;O9;5y)A za-D6e`cBe(+dJvV*p}yHS@`;EAngW9PdAa?2jJ|c>l^I=;L_!~+pf)AJ;H|FEbf0t zV7>*}i}tma!Qp$fKf4G$ZD#BKm9@4DKms2XeeLTM+wT=JQ>)qwAkx>Sq+NetFU4T0 zslPP%U7lHi_r~M5jX&JqKe;{Nzq-GFa(lpkb$|cl_JCg%b3Uu(6fdvRFQ>a19-Zk@ ziAeqE0ToI#^IFkKK+uYbE(ls=S?_;%b6Pq61$M6eQRjH^h_4wdvxg$g9Gx)+(rK7t zGc5^liLckEMT%t&)z4-oN4jkqy%!0WN7;m2LgzFt&sXOXk|W6ILqrlYvn`I9Jb4YD zK7a)$GR$yn4Sr+Pu8H?!ZUU8d#)WYY#0$8Z2+!8w14$FVm3W6ww-Y6H*Fw`fk+$v7<8O zL}L?j!;ni})2IAphcgcq3Ll?|qCig3E6vOyUZn%;ly!>Mn+rf+<$9f2-3G4rbC?yW z$cXL@7b@glT^Xj!bDo=8<1}C55PRJkzrSPrT_o>kL}Qp3#`B43?pc4WUVtvW+qa-? zb1XN6J~QcgZ1`J2k`;3;KI+*KYHMq3a)6foN|Rfk>!qpsWLCn?18Ci6WIuW#kk|Dx_M3$t_702~sc;=E7Xw&6Ws&r50z(R4nO0-?h2&mcj=K8d9iq1DwE3-NZ7XBAZH z%N^-v^_rwE7lp^y2@%uzcxIW%u(ae*PL>LgbT2k|;}B&0@GlZBcQ?XU;zLSb`?wBi zw~l8ywpZe!l;jz^(d9UQT;)SI^2gUjxV?Zw5Vfl|hy~13<6|~Z_SMulnoSG57T~#j zo+}KDM{7{MBOA)q<(9h4c;Utl5+1cP0aZm;(eXucrZva-Vjhg~>V(mhn%(_KEhOes zoU)!rW9-E1nF&qJ<0k=|9oDY7n6H6zLW))`5~FiHEXx|=_h(;!9Vu%qnS14$ue)E5 z4)n}Q1T`+ynpgUgcXiYl9DyUz`3&fRR&($`CbvsPW&LuXi6F)G88Qz^ImE`WSfm%m zX`q}mH&c!!`zYiPgB~J{Mgy+{%OMoR@pvClBfqf`TGqAh8n3u=S;&?s_oQB549y)n zUW2nrb>p<1Ib%V88vI13+jkJS(fq~YoM>HCRiNr4!( z+-+?l`W0^6z#+0%boLgoXy^0pbfIwH5|jHC2KbIjcg5T{pO>O`Zq9}k@po@~^lt0I z-j%3?e&;TK$oDkL4@F+;Z^Qg*FBxRt#L^{&tHb*TOfFv?V zh#+xB&L9C2;n#nl?rL|ptEX+xc$Ld81n5?kwC}xtudpxqL+bb6XMm1w@fWi|pU8dS zPaPg=cd>BpCLUg|31XydVNJ^BxGoDV>aL2nR6}I1R2kith)QPwR}Q0tbx0X4XK(cy zMeJ=sbI^zN^~h40EFpr&!UCPTljMLpY|3l8`UhXjV0Fx^C*ZYgz&;U>QpIT*8AS~~ zk^9GgmwJ6(6Q~m(YmklpQACid8rGnH0)AEG`Lo>jsC$CPQ(wE`HlO%I zI{WC&%yu)k8^!57ipWdW*t(wqIj%PiDZ^5K-+T1BT9Qr@ioG;qw`qZce&Lbz>EC)K zCrX?J*`n#@=<;_`^3ci8^{LA5DLKrOyel|slQaZL+U1q6Kt2AIK}fKF zqwU+G>2rUn_o({qIw;veaYMcUe$SeKcSMBW&#(`+NcZ~6J?M(Q{irtK5C005qdjPw z?Bngm+CS?u5r?I@4F&DCjYPs8KL+i3LN<*}Xf#W8(v?sz+S2Jt&M zkp7j#{7(j+rj-OFgX!ypx%I^i50)!`{4DGBUDn&YkZtT6`xtn8wUGIBYmS41)NS|& z%CPSqgz`2ED`S21nQ>J-gou&@;nBGJ8>ap7p#JT_uaA92*Ym8G9|oSx^u_Z0JOe)* zELmN@Fq4#hZMT?XDHQ4A=FN8C#oxNu_U1M1!d{G0u(y{Wd~|-OKfHwP!H>s(nEB_& z{z#7lzx57L9Ep%ls{d+-qkbn(r^w(=B4nM1A?qy}n4e>KQVISkY~V3pst`Tg)nOF} zIq&B%P@pJS4zq1K);TAIJ{I9#guC=<+nvik;R^7x=Oxqc0@|96{W9C)N}m!@mlsw! zhmO){5P~O{+;H-TDs~(y-UKs$9WLriv~<~;c_1(%`Xu+qp$Xked7%0^JieaFsAoYV zy&9Y#9_3R5!K^XP8|vMP_)zg^3lk4HRYzq4wD#4$6q5|yQ*n_NU0@>YfCNj796`){ z=qL37-B00KJKcpvP`cDqWu>=W|C-94beRB{(+L`eh~=S$?YT{^&UvhVP9nRy$pvf) z%3-Y1FzCLT3ouxN#Y`@P!Urg2f{4T#SD+%pWO$WD9>#0eXYixCE|>x#oku&x>$tcL z_AGSQI#*}6Hi>e!3$KB2)5}JQnr;Yixkl4V5>Pw{2l<@C2h-1wC5^&yW7XJM4UIEr zUv1`SqO_Sld8)>o?tGblIIsoD?y$X;v**HTM($_zV>sUkfBR=TG4LU31676s;TCTc zzYmE{>&(hmb)G1Y1UO%MA|AXK8@5b^zL&hts`BZezw1DT; zn-A<^5o5dKzbNoO1o-6%{D+f&=`)fo*O4$Jx^^`KXa`2YLR8hjW&9Nh+Mz7}t9O25@{DY;ERMzcF>g9OQ&ArL@1XtAV&1 zaZ>x*kU4w`jJ|db3x74tn0nCyFIWvVY9;V;Zj9JK+_P|h<1!FtCx#s>pA7_WJaXa8o54}xQ_XVkW+zzk zViZ#oId0Ch2wc_5BzhQt=l3~$a2<8iI?KifIMc3NlklwDD|eRp>vYVNFShV6Prf8- zvi$3R3qATmjYMu|N9O;1Jht(xFDDeNp0z)W;V-$?-6Kp7GJA`8>0pkdcf0{rK=-oeHlh4T88I*qWpOo6`DG7S#Vf5RD zWFHB4|Zs0YZSKkpP<;TYr|w0yBlGD3*WkS&+WYD&}>X_Pk^VpAl@EAe?{T; z+Dd2_#qfL37=pbG)q{5$9(v2_WKa3T;cjd~yr-bS@7xcvI~?qUk|Opb4)$)PP0?Lt zLkU{9bY;0J{}%a^;yt7O2dBV&bB-xUx+{K<%b9d{)Pd?!Wu3;#!d z24>?!k_*+g?~HX~3&o~rKB%5=Z^!q}604zMAZ)CUY{PN;|5wx+^#+%n&fPx9Q~Ghi zw;NvAugq-v=4K$B7nVPN?NK#W2G;p%GVSxmMXfJAs^!NqXFFyDx(NMj;%>;_1q4)K zimw;fs4s}SVT3%%%m**5Pup?Fy0Bt@h`LX}7eBPU`}8*f93gI{U$RO07z{Sk9~XSP zp)7o5sob{&dd%;9SC?*+z}%#-@r>R5`lw=pz;H5u%NW5n>+_`FPElS^$VnsytSx}V zd6dUT+`F6|I_cp-GgUQBL(x*>L%~#*k2R)Wr7D({=Tcn#Qu1Odew~|#qRCf(%meO- zd4|f?8E!FloePhLnb5GKQ!s=PVj0F=Ce8*J(B3Lq9ekdg@<;ar;KQi<7s2u$;@=lJ za(eT`w|6!($5bs6(gyn50y*OQwJYj$V_8PzjM-to+?e?(#-sBA!%Y5Jo|GZJZ#vcm za8hd*X3o2h2^bZrGDTyI91&q@km?DGIVn)O{9Wu$xwHu!?VIQlH`_LhaDDowb4DUU~HP zc7my0i*Db8F9E@Ksd(^?Z*I>J5_@2g`c(#-e(T*0grhHmzLGuqh><&%Z+F@2(uuuw zFntd_-{tHZUr~5}?l9he=VEMdy$N>|-K&ri{JpYZfBH=aqrcB!Tf2N)h;Mj#jlmhI zA0_D`pw~E=e>TtLXO8haL;q~v!rP_r(FC+-Z)MlR@2;f45qh4*mGM6Uj6&`5=MDCN z58Sg>`Ad|M{|b2}vnQLs&8T=o9xp8K*8FdCE1;jJW*FvAb1U3`u&JG>b=Q~|LY=KX z;(!;|*~IvUTEJJ>lJu`C{qLlM=j7G{A-cYl%xODJsF3EMF=^vBk7}6;Rk}H%Wl}Fb zUF}@zBD6~W>c(kxr3d;rCoVxsiQUOm z*=Jb5pdbhv{HV2m&)zDG@UW~6;7Si?;yRbo8)J|?p0umHEqUSjz$O!CPI2`g={{*n_fGHM0J|&VYPb;s8G3G z$mn8FIW>T=mYJdfmU0P<4TxS8M;;u@%x@FH)aOj2!GjA?q4d9_sMY$Id_VZc|=rF^4clN{U5gD52Ya}$1 zvZM|1wX4pT1YZM2zy??~!_rmCO*^*}f!8zqbmi_u>|vmao7e1OTJlh(hF#oYwS6t} zD^y?J<}|Ir;{(<6Du(LSLxhDsP>PZ(osAE z@=wO8r?ou+CUJ&}Fvf*~mgXe6*GNm<+qfNLnxwAx`j}X`+c-aZR(}`01%B83|6?7Q zB$WCJ<2--3pQml#om{|Qjt~=XO8u89UdHw>8eA67aofwZ16g zkw%6Xet<$SGtshRZ+sCQoY&LohI`^it(O~r58Sv)t1_^s7z|j2R!|e~oDC0Jh#vVI z!Z)dOjIGxtI`WZ3FRVO~4QL3LL2hH)d137Fs61rL=q94Sv=~UfJWO+e(-@42qJ>v} zT@e)}p5l0z`CB(;vRl;Wq1Bjcgpp^1(4CV~Y%+Sfv+gswIL&;e(h^uGs`YOed?e$4 z>%#N?*?r92m{|Jaby2!g zd{E|GZy&HO_Nq+fnO}$G5gabH&^%1y;#FO6J6v%W99h|~!ugQ_yza=ifnYkCUrDh{ zUgb$T!s8*+E#20{CYO8OBr|2*NaQnrFH83rA%}5zr9zUM^hSmo9Q zx~o08KzY))RfBo=aDOPzJX@;T7Cn%QL6@xgiyG2nUo4!s0JlQi;G0{t#6iN9w@f9*Kdk=03djO~W?kmb?eN?&fy% zyDS@*7!d~!f4}z(_jbkvsoxlrP0M|`ZGa1JCiZIG{KD7>$Y+dcBRqeHF)4ez=PP59 zj&H_v1QN-rH)UD-9P8b@`Tk9=eTnCP8;ZIyrS-=IK{jt{Vg|lT5J>-IHNf}F`Q2)= zOfG)j3;^c*WjEkAJ++;nGTpv2aChz;b;6`>w1VA!e{(p0Z13yfi&<o$17UdKThHd*`-=eiK#NIX8FgV+kJvGn9K*oXh!gsEwl3!4l_6~D`nmLsQoD9hUG z0t_edgsAh;dVvog;h-4BAJRGn8;cW8=ZMPBhybxP!oA|h(V!P>wI~o3 z6+UNwNwmmRE}F|9j01IO%BIFTLJay7f8{E zo+qCbr+Z}QQDPxojO6qJ-gQg8-eo&^Iy1@|J(9XkC^9l5u%pz;_d9z4>?i+XAyMj) ze@A`oUgQdMOH*H76V?zOhr+s2%tkBYX;le_OVip?Td{5$)>`E`7%T$AjJP*9_EO?7 z9qTdoL<(_OK||E3udQ3a=ajtKH^{fx!XG{6I-j%a0_mNUsey4D02r)aDy^ujf1=uk z?)?KOQw~uF&i!Ic*T8jU6Q*}#2;f%wbo(1It(WqW>aE9b(k+_z-1#}$opBMBRz0?B)XPREg=?%kwbO7 zm@g5`ZS_UPqD@DBkDbp+zWP#iQu?e}WgLiz;Q@QCoG3!0U(sS-`T4+FSUk(7aIoX! z{c^Yf;;GcenT)xY6qEoWI+(gjlK1CvLg~ z%ySw!iGYlL@fT!Rz=1G%k3l$KwrJ5w%t+K1qruSOl8mrpc)EIQ)tI+~k{R;HOg=>L zDjh5boY>?6F2+--LJfR1e=`s@<9%IJfkWg|Z&W^`T#C3{9nJ4YR>4NLkd_PO~M!k zQxpl|8;`;;g5GEq4B;4xZZr#lNSuH_Exmx=d<2E|*wgMs8SeH6Vf3D;lI#b=x9e1l z?~dQ(=MLmZyqinn?-IC;G|}(zVD!5Q4uW@60t9*ok=~x-6uv*t_LbrrllwKRqT+X1 z1-dh)INYOP0k)5Ke@XDY_Kqg^z!UtoKPKN(UFdjkzSynH>76>^!OjP1d=F*??}Cr* z4)0XKzlq;g#EIU@ajfv&*m=FbimXWi&PpA93syg&a`e(`1Oy!VqWlvT^KP%`I_ z$}rBcy1;|+aQ9SPg-B<+j|rys&V>NIU?7thAfwd3%uMB(f7STj_jez4`lX|w4IAN) zsV!ea^uU+El(1c_FM1h&8MOM-G5y&6*c?&$7u}NgATbYo30T!aAl75z(NM5`!X%yv zzB)YW!h4vQoEiMyDRq6hI zPAhi^hMR5oK)6Gbi}1^zAy5Jz_A1xe{iQKAk*%&Hn0^~{Ik3E9CVTA zsZ)>8OL60xrIc&07$2s7QF+ zd*0^~3ZB!GfxH(b*_b-O#5EQ{i@PmqD$TUF>S-qD0Eg4BI}d`bdn#5GxogsV6Ypt$ zvLjR!e{UUB@ki(O(5D489W1Ln@jM*|?|~UN^PHoCaYvsWIzQL=1;-c~^Fl5)DMV)&{j@P-%?v(l(Elszlm(N5^ zqK-zA*HC#DOAC+6P`hw+;1gGtIlqz=4z2*jdp5Gyguw_yw)eayl}JD46tCD=F4qv( z;_DFsiAV0ZEI2@f6&^dh^z{a3uOw&UKt7e+%(Qcj-5#od7O~C;v%H2VsOCqwHI-ux zf9di4aIo1CiPt=MO4Rj(iD5Deh`U6yewn}d?NCJ+q1KATKJaCI&I+7@ zyK@>vZtPNwDUW7X#{r+coQteoI#dIQaEwp=7@b0)J3KAhwN9tbXF?{2Q@^??fBI79 z{N-}p)a_XU@0E)OIBh)9blY6=`9a`^iX*OvxkyfLKunt>c4yQ1k?E`WU9x#9qXQWE?Ab%Ene%kBbSi z$hA%s+1SJuLtxWJbHO2RBNnI`eCV2{g_vH&1<)apAu3V?5`IrRc^)l>UuJ6i66^;pG%@8`H_+o=NM2Oh zk{?m&krlEZQ|qfeEENo7r@1=gc7x6H>-B(4wK08_s=++Xb}Me<+K*@p2)! z*Ari*@R?bg057-Q_wWMJAS&ody4h)?KnxMEZd0}QeahxbjWoQPYOkct<~8B+9hEar zQ7Wpu1h=t%qN`4zfH9@CGB&sTVjsMV&NojaZiBMAmZb^9vJukqAq<6M0JUauKZ?=8 zMzjOhMQCLD!IA?T$Gikre|&puuaZ2=_*DtO-CXX4A=&OB0%TGU7zQ7WO!2*X@%u$1_8~qJvMDWN??Mc28h|RG@mZ z>!dGXwbY=$9{fw1pK5i*-GM#PpK;y?|FV-P-)+~kA{qXr6-_ZFfALO3zg~4x<)8TL z|KN5XWcHI={lI5YoJL`aAP9;=VS1y(8!LvePg{ZMXs-_5i$=&jTo=7(Mg-*Et4qJ@ zZ9`&@8}5O^-RbM|+TmdD(%7vN(mf~_M0-RodiPzB_|7JS_b9;h9ng!?y+34oh)VuV zMZdCThxTHXAbn52eO@Q|#{wZMj)j zeGmRxt;=c`Tc~wT|0qa_dl4x28$_R&-+gal=0bevGmZ zeRCfN{)+v0e+4~B!V6UL4u`OPu_1OyCNN2!;&}y9#J~d0HnjE#!}yTR6?A`Tes`05 zRQB(l;r46XZzRj_4;AcdcefHdUx(Bf@(jr}0jd+B8TZZJs$8e)!IQ+ok&e7nVsM*M zryjkjJqDORq}NAuI*jWqkhPARdLG&`#5{nQG+!r1e;7g&@iqD7+f8#DUdibkGD{*# z9`7D_@S!kO4o64OpHAhq?fR6vJ=Ols=Q=Rj6&!*cAEX`Oip4RE!$pHOig4f1Yb@R1 zaq%Ear!*-)6a3=><*|p`PH<)?u)M4zV8%3u$p#x6V7W%BcoXstLn_T>I1QKlyL^Um zyFAO+f86F)O+@=RrXsoDZufJ27E2a(|}6wo6gq;l!fq-2JJ41o%7l z-Y-fzh(ITkZ?^2sKS`|IV0vr8`L7( zJ{>7RCJsc)+$4ZV$Fjm$W{muSD}`>&mUJcc(zvl~>8*l+K3NUp^0S~GiMYbbq{tl# ze_~%q+j&{I0M_|=1|MYp!qKI#BkCRyi~G80zQf-Sw#aIyGtR~3GawS^oE%3j+sgXC%U~BY@cVhZHwE=1M0m?#(S`8TT`%Yg73?TfAF_8 znD#Ee;%~7W^>n&P4=z{^#Ku|9BhRcR}D6+hF$5pR;X)*Kar6JNuR& z`*Q$m{pe!M_Krnt`|tXZMl<#;@cU!skIUaz5BxXF-&YU(H_P8w55(`_SM>F_EVFFp z^x65$$eB?v%(=Xu7fiD>SUs8Ze=wp2ldDWy*6GDT(&genfOoZr=X8s3id}lJcxvF0 zb>+x;2#_DJxy|_PFV9ldQE2^Xw^6li$HTy60{re3De|&p~O~!pA zTg_e?`Fa`oI>~3rySOZw-NLRovKNr&*yOzAplX|$A)=4Dk3AOPob+W9+k04=qb8I_ zYf3NsP1^A89@JVTOmxKC8+&eFGAQzX>57_bzsOR0_5Qi#0Z<7IizqlM_uY80Kq2Q` zmRrhyE+@F}`M=yoe@{L9fA;EMiUsZmu|Od>4)5N-1dO6E0YMmz;}HI74MF_IKs?^# z(g?i6+=gNs6%V03yuIO1jJz=}*y{|4&#Nwzx3);({fbQvY#0d9dqRKH1^C-BD8_eu zMah4M@E)q(6v+mG8=OMFLde}RnAq{2SwkR`9toP|*k2XuYZ2 zQKe8Fa&DF!HTQ)=H&og&&SDERxBrsx=hnuDC0od_am%ve!9> z>yRKUh=G$%1U)f(=c2=vEAPEXmoy&8K)0u8gUPt)%o<#tf9Ine_tFM~0Ll+{_ij7} z>i9745KTwo)jVb!OPwqXyg<(*a+Gc-Fga41Wa~NCJ)&`j{BDch$sCB-NH3b z(DoTuOYeH}`UsFJWKX?>xiTIirvsB2Y?yExvtNDYqH)^w);`hO0t3NPPiKO?bKnK% z|3fPL@Q|{I#U9LBU%W~MuZnm~^5Oy5q4Pf)Kcp+F`|4czI&|H$t)=vZY0(+4tD|l% z(J+hC?RewGUZpqery35_k-?-$;82;@i>>-X?uO_rf7}nmXoK2|O&D-!kLJrI;jZN2 zyMExxjx$OVv0AyhjPO;`+~SS_)x$UmrGOaE-Q$H9I)phX1dO z%#E5m+c?t+guV3Ikn^0=i@@z{*B9FYAuniNHq1MRz9yU7L{A(+ zF~!TCe+9lOvS^m>FH^Z!HUQ4jqbX!vuv+PwKUs(8D8uW` zoqtS}ab_D?*LB9ds7beN_G{i3YJp{UfVn#;A?f6BvR++uj91For{)c35qUm;EIN2?=D+(ZYLkz`tXc4<-j4f8#w? zwU2cT-v%S;{}zlg4o_M0hITf&tzY;HNRT7#t%D{PHj>hD9nrCemg) z*r(q$NdrIlh5u@H?3sfQj^c_GL2OYa&Jbqjk_z`D6anZM!00hJc>)_~F_zh!$}e}b zOP|=~tfyWM9WWHzZo*EEe{Wh;-RV}V@s`&dwR=V@05tJbm#)=4r4M}^O^_a=DM_#_ z>`V^FIJ6sK+#{Qv^WP}`mAW>owtxS1>W1-$Dfd4-@)PL&^{YOB?iW}5&{#^qB(&H2 zQWQ>NuwA^ktj;S2!%k@r}ziMJKd(=gEdHWe<#WivRfag@g9WO zOM|JME=O;l<@EFD#&*iat7B*nM<8$K&m988xAzjdBkArR@^yCe9@_~O2$NFheEj7vTN(J%htOw7HEJohr{3&0V!k-MC zF2e9#waWHRcJf!)MEelar@so99UH!m8C8IbxE);fp`3WDTes=>F<*3o<^5QIydOV@ zr~&KQoeh6kq!Lx`aGRIf8f3GetF3IqRbKLy>IJ>$jAOT@WTn*cj1q1iN7mz*{fWD zuNB8@VWf{WqcXcE8#psYvTmuVPE*w@@@&GuK#x05nWCHURL;|a!>qTE!_frdq=E0? zp(bq4$Mq?~f1xogZGEG_hK&RP3EP-mKC3i`n$h9l^O6@J;}(U0a^4ebWWep%2-KkA zWFB=FD@cfJT#|Z8^w`ZYS2TEiQD{e)m#5Q+1HxevADBx+!sip*gm)L9J=?l5#9BBF zRut3`>a3$GEvMkfT$0nYq5Vml?pc(pW-@44c{loKf8jQ*(%?s_Yis+F zu*=ki(rW9~$Y}a{Fue5Gg>&+tVD%|n`$fzW{tP)*u9Z+@6-A}qfR|>A98+S4AlH3m zMwk$KJ-lh<>Fl6t85Nw5T;AgMDXPTf=@M3Ry-%ZmK5rUp1nm(BjpU$Yg4N}>`=uEm z_RbFif2|@Qaz~(Y9Vz2BqKV*=su^m_s+@{f71*ysRCu$|`5`c)bYH+ucp04RW+Tr9 z=5vmJfP+kzXIG!NV?{MAj6NIFN`~(2h}?lP5~{u>r<%HgK#g>4ts0bvIk8$+1a!RQ zkX_Gh3guw9{XtAEV>K8_Y9f)kkwF}{2#tWf|0Ldy;Y`8L1S56c0{WF$&TF54kKw1|y{YQMNM_PD# z7wd+K*;8w@Tky0dZhyTH^))hh(KpMy_CV$nuTE*k*P~+nVZbDNP0#ZIvONNN=COE?P&&tsc#uIW zf9PHyd%-i!i0$BU#nQF{ME#1vcA+cfN>fEyko8?DxHU~|#BA>j^>U7?Il>`*e}6!0 zN!7YUEomV0wZTWp<9GqkhOkR3YYwhAyl!9!4d4sMZo@KDN=O*5$IW&kk~X^QIkXES zqlU?Rv|!b4Nc+%~1~Aw1fcWQ$ZdKv6m&!2*FFpQ;&;rm)WO9B`>TT zI~fNtEtZQp5dlhSuO}BvpnS!L1wl4!j^<&=?Vd*)C%Wa4&_8SM$v*otOKQ=B=D51O zno!MyO_H7f4L`K;c+Az;kUv-H{J7K7U6YF?zY)$c6+FmjXU{DdsA37|f8-;lBIzS= zzD@X%R@ng1lwq8Mkm-Y5qPbW%8R#iO73sX}ZqvuT%asmQx|fkx`CC(0ydp*|o<3qW zq@`I9f#&{#{xa9oi>U90E*-zqo8PQ6lkbc(5eOzXF89azzxMR}U)}SAC;qMb{m?ar z5)?{9n?}Y^l7Jz2t2ZznvW6 zzZ&d;?-{U-a_(aJy|6vawXKTWi#`(i-G7hl5$5g8eJC8fC%!g9e;K{2v^Nb;yzTH} za@Wh$d)f!_7S4N;4f(BV_I6*A_zzza|0UBsmL>&xR5v`l>Su`~|E!+tpM6Po^_*;? z=7TZ0wpW)?L$4v?Lw|NkzfH#1>SA74U?cF1Wg~tszdgUT=;vJi}jde`4}vtnXuc<38k-UzYv-s)2v9?C)0%{F7zxtNuuZ0iWl>Z0E&8 z+_`mGVL2HYa}k+ZcmD+c^AVe5wxUGjq0OLUZV&=aH0=GgS3R2v8#9+t;CL3^cV>Qm zr%gu3@NCVL?2W3>2Y;&5>l4q^XdtNZ<>}qoKcTA&Xp}myiurW3LD7d$B(J1m+|L(W zERO6$4xvr2e{Z_oC#(SCUw7|fTq>Onti%|dQgm3Lc*bXckmqr2h$q_>EJRWPeD>rM z)sIQ6^8(V?=-(K5{s+XpXIuP_Vfeqh=bvHtFWm135f9NMj*;Xh;SmC(X#(0Xd6Vum zvCmc7lsf{U=%;Qcdzf<5$M5e^x})2swIg!RZAEXqfBX^w+>S%(hNhp->p=Ht;TIT3 z!}oA=boUq8Z~TV7eQXB2M+7$cy@BDT;Dh8nA@f%w(syU(x9Rf!q~V_R+dz4f>D%70 zkMe-`u)uZ#g6vuxNB8gB3rWIvJw*Dknl~;Z@9Mx!(^L2!nb_8vK)==QkG)6_|2881 zERE)se{&DFQ8T27B_6MqJ`R6!JDK)kz53_g-M@{*z|WZW0cl@mQT!g#fDcUj5s773 z-yq;8B;K1v)}N6W_@^7l$EtyUvh0sl@6sIjoi_j4fuedO;po_OuYJMe?jCI+8e62M z4;UjYR(eyD*ZdJei4~l()`yu@3d9Bk`T9(le*#lS?A4#;v#i*HwjAbl>?$qbwB!l_ ze$4Bk)#`OxlfCe8`ZJkaAU)2f)GGoavi$=n7Gd>4gs-f6TF#5QNQI{)D1Oz<)BB3@j zu49Gc#Z()2Bew_8jm}*Ur)!&^LaRtpf9atlmTXq{sCNa@&`y+HPR~FDZh>{twVIFx z`o*ugy{x(}EYA~sf6(fXje|AQ7g$m)`Y0=?66fH{wGvr|th;0H?iIJMOnT#sR9cMqQfv`xhxGuKzBE+e!DMzH;02gTKW;PsglPmD5NR`+T+ zYdzkoMdRI((yGfLo4FGOn;eko#>pJwr(ho%Y!0maG>*AML9vP`cZ$Gae>zEX@p9Ry zK;EBgTc^<{_q?ml4S%`8lK@8F68mmGAgXd!JU7dPFx01QGuFIYd_32va;iYnaE~^E zg^{k_S&a>D?h{4q*}>Z!xU|j&0L}?F!iYH>ebLONB2=#4{enE=?5fVOf?q>zas%Hi zlkJ(Rq4Gr=i)p=EV+61Re=!9`D3Rh5L7rZawf%X*=zw~OQz)q1=P%>>T=`KE&?t2S zvLjEJAvMaB@C(yOC~$vGr{_FfIRa;q>q0BUiPp;As_@Gk%}`YkbSQ71#1V-%o$Ce={pp@KCxQkqB}MBsw`1GiXni9Y%a6#jb7`Tt$kB5S*ES&$z%SrYwA8{_})=PdJIyyjb8`LpwX z;At?t$z){rSs|#s1sKNBy(^f6KTWK}$)4~{iM?@`!gkWMRL?AXml;LL?n;7FyWoxA z)|~X`MNymf-FtT5GcT}r3-G3xF=|f?{)pvz7631NHk=V@xgk0PtR=0ssODNt~%?yPq>{Y z)2)uq<>-tXmx4|H44HN`OC=M1^w}OgXLQi}-v0L5SrG6oky0{y66GVAvJ-!_ji@oG z{zo$9hhrZv<2&olfA%tf{dN2|FXM;I^p}^x`Z}xskeUAY3v3&g#6U8`Dq(cG^|wJ9 zU%*@>prvs00N*ykf%A$41^Kak-jHIMYeIrpSC$^dxesBx8~en(bIt8YcY5!M=NOa# zZ<)zC#i?X`e0AIYMIq20f4x>_`_2BzZTs7~ zz$YjFD3voq+2nx86aq84X$)g`ms`U;uWSM~>JBKwGLo@}%E#wdn%q}Xqm59_IQh(L z_walHp9xIpG*}(h)wt0wyS`JSHk-q1ULN)617MOy`d`jEoBy{zdXaxc<6oP$G@bv8 zHf8z#(K1bY2P+Zz9{&E~`5(~t$D?1NPegv40)%dw6r%_Xf>8*<$Zf~}G%C0krNDb{ z*WUT{HqYGj9F6Vqw@nTb?~y(fxx)>;egB2)!P^IuMt7)8zL*tIdvGp%`wU?3CbWcp z7sbTynw@yB%}Ms*K)-*0yzP1S$wSGWQQj`zr}=JDm)d*6khke@ioR!5B76QA4ff+{ z^d1rPu0h=}ZaW8h{}{!)wNyyF%Y=g6j}!XsxZk6-=aQFiQ9(sGGtgL(qESG|XC`{O z@H6Dq%1O7*lkm;vcjPTsaU7_Y?1Ht=_IGXv^CstZopv-73&MXgz^Uky`>vN^rS0SX zfmn;q-zL+RQGInk5bK9yAFKESvHsyIfPEQ1uHuhj62F}#q4P%|wi+vSEGU*!kT0Je zYGmxD;GZ{KYB}U!n$!I*wkwAbAXj$b_1e)_g4S~{g>VCsC*W1{k1+%9g^@iqugY;+ zKt%zktJv|b$QXYzx{RME2+3U)cM#_!Ia*y()DZ&7h-Z5rc*-LtH)$1wY&Y9$6&~** zPN5R`JP4%lq=Uz0I7CzDB;aM_n*OnRfo<9Xr{9|Ol;43CyhIe&+SgMn9t$s#DLtEP z=yY0hW;hX?#TO4Rf2aXs+)VB4LgdW~u0!IQgf15907QTH=pe&QxLmeh{5d|Y(xGsv z?h;Wnx2x@ol5uw^xQ&fU08HzaKe54u@YXt92es< zg$JPuhD3iK9Cp2Cj~LX5fO73QP&^Vv%EVW|yToBl?E!VyhpfVW>MjnDb8dbgr{EF?b##!7=FuALA| zc6_5dGr7wipo2inOSz;EpN7IaW|jUBW(%N2gTsFx`CEo~Jh|z}l^Tv-wGyHq_izo_ z5T!(}HZ2jsVsv^OrQw<0gCLd1)pC4-0}{{5R7(fD1AK~_M~{tm{p2iH6D+Oha-r29 z0oaV7eyhG@&Q!WgQ&xJC!S5wgmiYD;t6N6#J_nKo(siza5xzwo=*#> zB@`d<9;#vA38>6#dd%T~kRUO*lnzQ%tBanfkGM%MP!kGn;#ZY!q5W;W>+7}jEE?7! zO63-)xrYxsy$Y6ou7O9vF$hhMlOr{bQ{*1qVaoOftG!@UqruY` z+>-?Hl-ztB*b3Qcf32ZV_I`sEzd<(8plT95;b-ohxIL;|N z!sLDhoSxUfmazYywf|~&6WyAH;d6dPpX=LO(VtB}O$RVOOg`g}3+R4+m>iIdhz`|Gh#s`~n1cRmB-S3t1O1lB=+J>N zi=Ct(na8VZw-}(goV38O4e;N#+CEc#q5acTA0#buL6=`Ws6+>2+|soqPIh-(q?@j` zUj?HYt|D+&7ito=MJ{Y zp)usvebP}R(+~KRLjvJ{X>asxx?P>6mV1lA`>8(T&zj)|(=LN>14{RuoL|g#z4f!X zqp%B|iLs~1!JvZGIaDd;aYYlFx|er24CB{nz3Dr0wHlg{x~qQ!cIayr9;YYjU0Jye zqvTN^+2uvtNy52MJ`r0xBABBv-<}g^b+8ILUSOhrMC#GdcEEIhA=L{a)cxY08$H4{ znXzM~DQiMDJqH z55i}hXmj;&wEgCXCTUF6E11sD!6Ek`pe5r z6~w?S@7}x9w>|EEOf2B63I|u z5E2yQ_gX3Nb&>!C7d1seddlt@I#4&6P~JY*|81=l@C&ijzd8b}a?KSzxw(dbB#Na> zmy)(PB>;) z?hR$OmJgE1teM?zMZy7YZ18p$vH*58jTFtCx+OLqLt7S#b1g1yeg|bF-w$*DoG6({) zw#bqj0ldy6J9W@pYOR9AkD-E2VK$emks(^!FJa_tYgaa>U@BfP>%G}$o)Tp~=>vmZ z><0;dyq?!fiFpQ1;3vAHdBmchC`=|#*36gDzm7%)UCy+K3-w)#GeSZIU1w|5v<5c4 zDkh*X_L;RFYy7%LUfx7c$MU<5mq&!R$oCmOujxbUPUz625@L5{6!c)J&5mbZ&cmtq z08C1eIJrC^Bz7TA@yJPCY8Q?WGZ=2CR*=qr521bTKe%(0)T7_+G|hK!+>?R_9z9)k zAbG%H5gS5bhX{Yu6IZB_qS)M*0NZ(#4%8beQ&N#bw$97frfE`WmbX{w1jy+;juk+8 zUi!?E^>1T9`tpZKOTiz!rrQ6Tj?A3jLoF(=viHOg^>~NESKBi1ycz`)FIUa|JsJ)lT4a@v!O8j$zLPg<52h!3$T(j7d)x#e;c_& ze;&Exo$W_kbhE@X_X1f42!0pD-K&hFRdMZZN8mQ_C2`w)u>0M5$9TN6LZ(`OO?V09kC`^ca@+jt;=sOo?trf4mKC?-!+in9Hl4(EO3MDGY_p{UaAE2e zZz}YsuNya^-#9iUq;J5?xJN}NBa|70x{O=05l2!%(n4}v4 zYgFHcs)1Z~b>F66ASJ2_W01ToGYvtLIJ3pmCdb$`W7B4gMy?zzaawj`bfdwf3!oY- zZq-Zv6mJ{xe)@jb=vPvI37%art0p}4@(ydjvw%|K=65l9#?+C47MZ>$FD{o?U^@K+ z2L5wowTa?C246SR|LMcuhh+cnPx@Uj_Wpz)YAJ@nI7HwiiQ+pY1~HVNFp|Ux3_=c{ zJB-A3Sct+H@k@*5ox|>A76FfPvvW!uKLorKdhkyC)6Im@qsh8|m)%3ymi=1zhI|Uw zM~fDR2VG9~Wj+IJ2z7Kp$j{93US#*e8XZp02Z`OELV>??xXX^F#$Lqn4*M7gk>JNFFFaoCXs|%Xyz_qM{?%j54m{|78XmP6M?Uk;M@ZNqnv4HB9s8%? z3yVvx#F)f|AY-(DH+PAWu$g?%%%6!HOFJ~6a{f<>F_~A*a^}C;!ZAPQQwtcZc-rHu z3k2||4a6@)tewJu|161^dblnBnQ`%_*7wk^t-yB&XY(hquTeT57v-;dUXJ@Ab~}$6 ze*j{b`_7VCwK?#|ll~;;md5)3DEOj(i^hs8T7^aL!C3u&Rt)Z0;`-VfB5gw z5P(ULc={2Z-*{foMCh=@GDfPRKw(I*dIQgfLP!Z#BWC(eIAtP5W0Etw3n4cuwJ`w@ z2p2CUOT5j0>U}zAS6?bpxEF3vN4Obfkok%5_;O`UTYJmYMcBFK5T5mSmI-{9IXgM) zkcmW1MU?hJiK}?r$suP=cNyUt!smwQ9x`Q{f&%owHukN9*MBy_eoV1*EP z5V|eMmP=D{wY0Lz&9t#Rg(^O0ZbEH&L0;SnaVoBVOKtWPH`(@rwN_ZJE{f^kkHZ^3PY#1R}+FQ}gH3l^=OIZ|xs~f-h9B_&caFFoG z=&P`}pY>Xwk!L2x8x^ReDx)21yrp`!*1&MLR>1tS^}qu6!FtvR^3sd1 zr0P@Ggd-V@Up1=ZUa58$t!pVVQduGE-Tf9(zm=eS$3b#=Hf0wA%*wxW{~RrRgCkFc zX5_8tsva&+IW>9RqcH}!Hzu*kkXwDtg?Y+)b1ne-qua+{&bs|P>IQtJ zyEL@EV^k13b)8?s-9d}+Ze=82?|9<31Xf&7fVlY=MH!;(CB1iQ7EeQ-w57D!Su?#Z zEnx~}`qnDG1yS-v<)stJjwoyTqEkePx;Y?^t!(KmG?LjX)UHHW?B83l9=WiX>kO%X zL}H#gb@6Z{;-oMT9mv%}h2&p4;mO_b1mIFdtdk-P>3z+Y5?`oWWCy5}PPcK}u;&Ti zT3_AF1?S@&^f)u#HXiPs)i5r%K`Mb))4aqUe_|vb@Bx{i98ZhOje^Th96RS znH`FyEMc^*b+K9h-rN9mD*YvDw`Q>lhKI2@+Y5nKz$(dyvu!DvJSFA!Za7??{ophe zr^#PD4^`pFNrV^&G$f5N3NBfHBw&N~#geZx!E!R{Q{t0_DFV83R3fMR0OpvH`1x6* z@&5d7BhtE#4Tk_zJ9@pBFE(*&hFrC`%1Yi(H`qiH^5z0DIA=*OFZ@4-P~3k4p?>?M z-$5wu*AR*V5rm|W9gQFqgpnu$lQ4==IED}egu?q7zw9zZ*oU;?2WL!w(!++3L=M7u z&_?($14GbbhAfPaAqxE05NgLs(MP|Zd}5<_Y_mTjjgAF&ii?H^zugx`kdJ#;c6gaY zABIW(R)6q0;|PE~PCBmeuslo;kpTuCzzBhkaXBhGt_r0Nx||T7RtS8&YK$Kjv~%J8 z`uBgR56pr-$~5dCtba*=NiWB|M+)blxUJ`EUS@7CnPMn9z&v%QzNOz=Hn(gt_S4mW5{{_P3(Y|IOI6Yp zvzm+6N^`&xvNSoU9i6~@7>0|Us@A(*z$MMnqJe;IHv%S#ADSdwse!Nn$ z_#l_?#|sh9ey_dCqNACb#-Hsx=#bCurEo8N`;(7}W9U$Jpn1l@riC?Gs}kmxU45-UV#({~lVK9iJo=#};NOirID|>xwH|%Anxf)6!>L78 z3IXi!9vu53c#mg+)BS(>e2J1*6?mV zf(ugy)(?&0;v?evb21P3V$D7)K%n-WjZdv$!WqBNH%JfERHGIhjKiK)fXx++Wsi+lf$Fdo2%EBOj zy$?>IygWCZF;4nw0x!r^-MMj-`o`melM*%5ZF8NRn&tMs)6&@u19oxx6tC$HT^kTYifaa$~@Pkgl)Ss7OpXU(VeU*FmW$%jLRX{2B5Skaa8B z%%VAVXY87UXTW;u3gwcFPhG`$eL~iM{Dw}~@uG;>CfibSfz>OIa`c%9U~f#p-eIp& zG`Tr1Ru(TefTGEdrx9x`iCq$s;bLszoGTk~bURe$%DhiTjG0DC+5tlw(v*36gOrMD z>$S|f5dt)r=+I=)uRDo_nJqWuM-sehKNry9Jd6C+4}yr#m!-Vb@9xUoR96>&3ustP zQ$<&d0YkGeDbM1RGYqBc!Za@G0kh~8%a{pYkFdC5RwDS#F_+3QR9FIQN7 z;7;$U9P-nMUhGj!@8eollAT@C&!LdgO5ry~+h`s52bB4j<9vrvK5aj6?AeET%~InG zH*{DnveRwK6@pM*6AZ8-qQ3nQ}pY)Ed~A>m_X~}KDpx*Wk_pLnI=u)LRtXt8FN{Tmi>`Z?x6xZXOY%Y zW120CN-av4E)sDz+q}OTx+1be!k$nLa|9*Kc-kx|nu?4PfEXloWM$_Sk zdyhSB%_|nJ3yo~wLMs#|7`U4brsLZ=(P6Ibcu+5!QZ6nLX0F%nTDoc*24&tF669Ox zEz)iMN=(D3m5s^-PwIJpd&{c~IxyitcN9wl;Jmfat#ci#k^>87BVxi(&ZDjfV`9D% zyaW0c@&4`6z6n^u5`(16P3!hw@~5|rj!XgYmY)^=+2fq@V28ri!FkNbnpNt4ffGo4 zl+;jR+-RSqHnyERn91Au>AV>*F8wu=fYU7s!t-pYt}o5Jc6noeo1r2E$xQTO(yBwc zMMu`H)|tYZ69g~3_kvnAaf{{oa^xurp z{URLlzq$7Jv60`e`va-Q!NYYF!;g>w0>UT+qfp|IiS1OHLMRNRU=;tQk@lak5jk?M zaqv({K=7Y7|LDiA1o>zlcKCK^{h?oi%0tcYnZ!drHrC01$5|zW4}iJ<5ho75yW>La z6QG@92O`|hJVx!3-w7z9qZhn0a}ql`v>0-L$1ptTZ~`5o{QY9S0LCOdh75NmPkbT> z(9Z=!&_~Tr9MX>++#WWn;0J6X$6R0bmu7sI;6NkkHzRE$mB_}EILmonDn_FRBzkSs zzuT&SKlgZl4V&e@*7^fn`G%)$JItFT&U_@oj+KyVO9Xd_SH@OKZd9#!2IQ}KHw*Sb zUCAB%2TqNU*-L@~Th~~eDw#*YqK#d@dSYfTi|GT%YR&n?f2Lp`6}P>?f-H;{x846PgVJUr`3P-S|JG#Awg*8_b>`k7(woTBK8|8 zibD{L;1~v>zZ}2Wx$|BlWB3q;W$97T4pDD*Waok(;=fl}3_ZyC(R!tRS(@X|2I;}S z!A}WIk4?q+BbwW5V?-R>9sUgU#GgG$6#HyohI^U*9p6t6qpSUHff2`C^zcc$xjQ__M9KpL}dCB*#wZ{&XxlPL4juLG*Z=;pYaDC4b5H=Yt61 z&wqKXys5xZ3OQ#Jm6@`)>zp9M{u$eXht~@Jv)9T$F3-S!TGPa@4XTg9bos5OIdS72 zuTcIw*FVZM2dlPp>`H83%QW!A!Gghm|LI!-es#u&9%Nw1bft z!dR7Zk*Xe%*Rup~n2k!}!{`8Q9;KVw&6HYP+7Kt_2z>>R_dH*rz`Nm;yRLI7As50< z;Y_ZjA#vEc0A1Dw@$UNI0aA8#*)5YTc)UjHrmB6d5vX{2d$2qbubd-aRdr8ua%o^1 zikxsz8?+OK{LEarr;Gq1?QZCQqgsrf?Gq;qOMb7-E&6^r{>(^d&_A+b>S{cLqD7`} zUjO9UBu8O&{~1G$5CV1gM75|@^-1Q0^&(NEQ$ZUdpc}>_es)^)H{@)X8e}A4^#gjZ zd(WE@aRw>9es)iQa1%`$WuY#}TWK3*=6XdTI|#VE=Zn?%;Qp2d8l*daIDTEVoT1E; z8y@f}nXuJUEgo2hD{Uks-(7F+LuPK5^DVz&eE=4zd^^j8^H^1C6p&a3$ucM7+|Zx?&Oxq4V;1{2Yf2EMRPMw zqy;@F4hQT$fd|`_h95_-|JSYhKWV&rS{4YHXvJT77$Z5}FFr2W=|+%L7u)B>CO zZo*_0n^*!Nq%lDxMZ2o<&o8h(6;I*zg0BzQ^e{8V(W%zPRNYclFkq!f22?WvOtn>s za)e9|OppXYHD+Od^W7`&Y^FHc>2$C&k~#$3D9X80c@g1^6*+}To zWz1Ovdm}D?QE%6KZSP2tCEpW~*S#odyd`33w(+ZzB%mfNzkW6aTY9Y)l! zqrIw_%>zG8DCn5{y_EVlde?)v+)D}%-jn1K7BZi_aj@IJi5$#j6MFB5D?BiPA?16a z;VEs=G0l*mIw$PabY%#pRgWFnBKq4P(H@%*!1^A4yya;Vr+;W4WENKG4;`5gTH~r6 z0lwSj*^TK>knq2;-d6zl^IAX9a&W(cpdfaP1Rg+-KoJrKDI9`v6x$C_I7&kMgTpN! zgo$4^eaKJ8B%%&4yd5ujzysQW*>U>L zp!WxV)F&_GP`YKu=pGtyYjug&uPpJa7^E{7hXHt6f{vQqL*aL0RW;x5m;ZC`=3~GINSxd6s1S^6Y79Mp z9tvzOP?`9v#LjR?JlPOC0w6+Ic(5y^sPBg-@8x4pS^K{PwsIiZuh5Xoyi(|0-DKhHERvN{icRL`?w z#8-urZ1!SVvPd{LIH?eGd0Lgg#0yrkw;O>}VL~}qfu6gXeY?-QYl+dLY8>EAh9u(i zBSQ_jKM_4A5MfA*V7`p6TPH^nm6XeHj@t(DEuy=Cm52s!QZPpoyFPswNPRN1UuDv_ zq{=?F^Xek>IiQW#n?pzv#AN}02Oq&@zxS1Dq1_{>FBZ2T!(6KHE|nrct+@U+DOq|B zlJr>y#pp?s9V|}>Qs)PF-Y@sLZP09ofl*()SUT6}o#Ui9qHd#PN-r`_&pa#{+n4UsNm;QiBw@a=xH)|u>Y?~Q?kf?Nj^Ul_llbFhX zKdTKiBoBcbbK|wE*ef)8b)K{3dUSbULUQ<7WIB>prKlW(8 z$7_aAzxvEUkcA3u2}J!<>uJGoOE^HXe+T$}YVy!wSTB^FWg_%a3;R*vj~J>!fC7!j zZ#H^l`u3-Vg=(9 zCBBJjd5w=qB8mD2ZvF@uTq&|{lRlTrY#BR`!ML+UWC`68Xl^~?OKJgxE>q?7Ii zn>Hz@TI18_k>o@GhVK*h>ess0oRT!+i>h%aWGFc2W`>W~BW$Xn+*wgk#{6u`uP})O ztq>pQTSt%8asw89MAU9WlRM`@3muG^XkAhYH(pS5&nIVjx%#!=f1j!3mOXOedEmc?IQI26BVCMvYYCY_Y?tWqw+lJ;KUke3Ro zOli_@aS$-Ti@{a!$6Ik>5NK66V^}%<)mKsa1YM6m_u{&N2rrK@WxZ(QjIeM%;^fxk z?d(fL1T-$}bPAMX@Rd$lfn}w`|1yx#e6zai`R#k+&Yk3cXO|^$AgmeRE{*m~c*>}V z>FgW8WfJDXHQ9o;@OC>-XyJ(`&(5RGWI%=50yoZ2!O*02(8?T62N7u0Mi7woeHzt3 z1jaLLq|(5|D?YjAIo2_Gjau6V1zu(21$Ix;@USFT%aJ!7Aa7WoxQE;jGreEwSQJ1) zb*w3kscRK~^b;!VlErjkW*0t$*(>%BLZ3x?CtdU(wqkwfZhTne!AIPvm{wKVJmWv; zI84%de-JjwA7G#Wij|vH#gb4DZ#UmjE zqYwhb(H$nj_zo9g3MLM19{ejTh+~IAJrx}qHWEL7CK3aj!>lJFxw;Hj}HR*u?9pB6bRyn4_}fVa1i>SyYvI+65?=3#(&qVNj^bvU)<}H zFS&5=0aY{VP~t_OqQb+gcFc#dO z_}3eMg0BO0pzF5{K|StlQKwjv4E2)Cz(6X|>{o@9W%{2t1djo>A3&8Qxcv_8b_S;I zcLD7Y2<~t^_V3uSzf}P->^|aJYbCB4W5j$VqWeKu@p=Fzu2e75S5OsM9B*09F$83) z&dOL0cx=c0D_w3q!sEV7Odk7t(xThktl7?grhl23gg#&2z8uq_c8=H`&i*j31W7-^ z*hdu!%Z>}HLgM(sznfKb-yaM77+e@D@U#l>J-G-kj2tx@7>E~A% zD}WEiDXYU#%dvU&n^E)k7KN+&Pv&@}>4M=@1=O?KRDG$*N@K9KO>U!93YV43ki=Yn z=N-enA1QI)?^zg8%0}%4BrneW$>fXRd9!m4KwaT-#y4vannW3Avjx2|gnPnPPHGVv zX=4f)ZWmaZqiU^7g_!WVPub1L@!Sa>Cm^KS3?%R7lOM}xpqP0Ys=E8kp}Jzb5cZww zF~akk2uZ3vje6-7FFwX{5wFNgJfJFn@QU>8-Id9(z}s1R1=xNQ4nhP{Wusuu`h_oh zsFoLdT+dgGe+LT+CTIB>oYu9>>uCnAkCT3ZsKw*g(P;YD(-##Q*MqfvrWcTw4f_^h z)dN3=co6Ssf~9=>(Qs2zH)N?H5un@&lYC8(P>*S}+|_hqlJnW>Y*^}lXK5}o z4duu)bKCB3(gaP3a9QbvGUYq=0eW;y&{rbfU{&AFm}|3!XmY`OXa~dXwQp<@J*~GV zJu^~#Rb{y;T?c0OPv<)5ddC2?pgNg+01k(L5SwZJ8slzvEf-w%?wzjfS(3W?5e5j=MWm=JWArk% zYpWR#`d!&*ggTsy(vtWF?r>4aEuyB-Vp5FJ8wwiw41~uumx!=BJg;xPE$_zVw!Eje zW?ZW`)p5CR4xwSl*Zl_CnrX<2D1HJpa6yDDD1F!Uo4~>~Gj>WsC5qpFS#2HtJ>ZuO zKKdm|c#ywj>8hSO`%bzZ{&cm;RK}lSAMod}?{9J6?Zxch4eZHVZk^7XTS%kaB-vo+ z+0E;6crs*#wW<`VIm@cOM9oEU|G<(x8atP$XbOmS zwle}`SO9aXE4&gqw;&dO)a>4;c(*gmL1gNj9OqKZ+%6eA#AZQ%lKzsa&G{KkOn}^g zZOeF2oI;(T?+<#$ODa6^RDZkWXpqv4*ctLFl@p>NA)dq*PeZQ)ig<{Luhb>DH9otkDrbZV}$a6hBMby`&q@B2D+%OXrO_?|rOrxwNTi=u+SS;0hV zFPYtKb5-fl68$)TuJ;?4yzyRViicNiw|CJ#opdsv#BIB7s_04?J}8o%5-%HI1c_-f zk+x67LmwwHsXnYKR&*1oxv5|ZthK}Ul_~_qK$o~-Nksgdj{}cT>F*Ayf;qdsQ+}&~Aj=w1Q z*b_WV9>`#7HxPvW2PJ=(eUu$gEshURiiV#!kWWlUmLBssByqqz z{IgTJpRg|tec&sB94%=0)9wYc103$J7l)tN;ZGQUDD^Rz2LEys@Zug&BKg;lPJmcR z;bp!Kzawd~&-e6t-hLjH*qHw3A)S8=`VPzo{D+|LTcR-Vzkt4@xx1)8g1+O}th--8 z-+}pn{}<4AU_RhK0e#=+Bmw^deSZ-(%;wVhE#%c8jO)ps!9OY>wAf7sG_-4 zdfe83P_D<$B3?a1carzWR@cu+RiQz&FnxFFIOkN(aM-L>W3$eFKL9LSRBH~)IX{6w z$({9|QkKs2y3&5{}5>|7+o0`gr(VRONc4S?jkXCkrsBizx zDhZ%-LHKv0g_u?aZk_c>QV9!7IyN zeHr_m<#;ur#5QWb`oFKM`Mth{f4=MR^=`AY|1?PcxJdjTG!ElCas3Py{-C4ze^}># zn{MVmUE_z6OA$vFF#?k$M&Jaz6YpP@S2R7U-NCETqsZ>X=&+Ce*xsk;Cj`Z$vzQ=!)V-5&^M~@1-pGJJtKSy&WK1>;iFUiWE@){>c>*t{5 zM}YfZl~;j3%IoU}irOPJ{&w0eeCJc2MPixu0Aj zTw8Ud+4$;*$X4GZJzo_)z`sd)o{SKB^5A>S8o|MS7x>a+kvL60G(lP+!RltrT9N;= zOrokDb$luaGL#^62aWK^vd)lD!0=!ty23YOeRDS4t?Oe+uJQ}z%-8Ggrqf_C7fkAJ z*g!?ET(^|7V`75hsSSt1hM@v~aoAIihu#>9Dj0WrO_({&R(+WjJ9HwJxhdmtNK)2` zSF{{0JL1ef#Ej~A7VOhZ02E3Z?mmQHO8g$W(xodxe+R2LFR7}woaM4e&JfBCSZrY^ zi-PAm18;R?f@hQ7AnXZfG&M5?nhX3}rw}QU@2j0Kmfa%y!tPa?yJ&%bx6-`eWJ4(7 z7T3R$v*y41mqbeH`2l!1nNJ-9XGl~LOegrZdcu$qyy5h%=&o2;pRmUoOvZhcUKLA) zwW?5odKZ@Z){k%m)MRZ?2B#uRNwFJ>N@O_|^JdZ$8o5cJgAz!-!Ba_|@##9KeB+f; z8xNiuS@9}gRvV_k`t>o{{M-5umUiA1f7Zqs&|eWwFEa3WsLHi&Lp& z<19v$c(#G7L@sxCvRC6;54>`XUKwGKjwIKoE2T_SvpzdEad~ooQE@RT=v1PZV4KrQ zu*P(rcmUT*=E;^}g$AXOIK*RK##4S4CMS2MtFi)fw{D^US_Pjv@ky5E@vBb*Do&~-_CK~t%xX)=w`QXBts&e*rDI?aHIaS1N@b(1 z?^`98!`5@ZmF|dtfl(!){*s?Sb9sQydC>3w<^sbz2_VsjbdQ{s*_S>+MTR5pPr6I+ z15893S5^v1ka!I0YK`vv9M@F61D!98+Y43z5e!Z~!fYx(@Av9{e^rNPX6d5xbZX~& z0TVbno|%dkNP0&sB`r9||NG9=Xeq9tBw>76W84LtAmP$0%=K31Z;73gJw*`eqf zoV#BcBFk@2RzZy-;^qRqUOA|5FWUtYX%Gs-!q)g5YEUw%(-ececn*4>S9$+PFL|E@ z5sHLzWgr)SyJYToshwLPVk1R*sA-u)K(?+ZdqA7$&rdUyw33jC; zl7uQ=zSB>284C}8;@6?wPljl`-A2Z|y8skAy>vM>^`#iiY+FVtrxl61z?JAWx?_hT z&l~8NURI8@pqWRQE~_OXoU9{e8yoOebfs3`<@@%9SMP3ce)yU;@|@iS9W^PhqCjtLzZ4KJ$0T4h-$;m0jL?S@vontl!{!IVi$C5{22!3dBDCp2mL-@hocK{e4FY-Hz4t~-@!au{7VB~1hz~QI+ zf*zCFI~pXYV|pmX4si_%ADWu|%8wU+_)PJn(Pzg2`50)2N82gJ4gh)NNFjeo(b%` z^_ev`;KwVfx!i!sQQ;OaxS)$r(Eh(PBwp}2QQ}WHLdzS$kJ@uu2;G_qq$ zP~-o!JKT=}-yNL)ldA!~zns6k8qq&u_{2ZI2ghxJ2d*f`(HDvJbD9fW=K5|ItSWcl zL>Fv}E84lL?uM69^2@x83!+`LVe6{_BFWRvIu#I@&X0%Ox;@moQQWZBIT@aVM%hmS zUQGV+H1b9~HB`ju*~z#eG_0zBZ*@%O*E_AH zcj>?8nBh=gFcd$p%{$FvcGiQT*2-^-697TB^?z8>{&_k3XQk{HWegz+j6gB)s96+= zpvYdth`p{+1WF!`g&>6z5ROvd$E1+_<+x!)9@4ra<1PDG6h=pFAV2(a|GiH?-hf}c z3J+iW_}4D}=;6PLQ%C84*{k7BJNG{xkpSpthc7uu@ZrV(85*QMW{2@7aXlh_=i(n8 z?)!U*#j!*EvR5Pg0q$Ew9KEPo@)>AUe>olabK@8Ix6ZEuxtSo^BLi}Ne$U_aio}Y^;APKx5Bgm4 zZT#HEIcSI8y81DHAidwNAuFRwNX0#_^@%USCq@`TCQbJ^RDGz!8(dX-MGrltHAhBy zvC4Fd>Xm-1r329Gs>$+O3M3s0yV+LQ+tN^T@7r^DLm0QlLE34~H%4hr+SKh#ARPS( zzuTO4a!u<52=AK}#n&mRLpNNzxe`;}%Z;1=fQlqb-WWN5M0LaVw>sv@8!GNa`*ziO zet8&;f(8^oUb6Ij3Qj6mG?xYQK`VYcGv-;f=k1J=_HT!mr7uaV4XIO*ryCl!6 zA85c!n8q_@>f>++(H4_-WDs03j)!C1?6d5R+trPKs!7hkUM^*$JinvU9;!iDLbu0N zHvp&WHu~k}Cu31}emz2Fter&O8}@s_HN`3~Jex!@dl%@%F5z9kpH_j8 z`y=Ilb^L}YNbk-W&{GFZKkwtP7v3BE{AoB4_%=U+gYpQs?owsD9J7cl6?h21{tU*d{loJt&(N(5|wc+k$J9|=nE5cyP#cTpuU z?3Uc=y;vf6?@6(v34NK84maWDqBGiRAvg3^SXUy^X%pu@L$!L?k0f~G{LD5sX=ReFz?7dY-aa^tw z&t{BU=7(};qfhvyyur9#2q{s+{hQZ+=G``;Jr_|xL_wKTxsX3yyjI*i{RUP$$;j)4 zy*-H>^(ulNuKq>qxYS#=8~O^9{rg7MSIj~)jRBl#d@0TK-mF>CH5zqd+9GIrqJ!_V zn)CZ77p2;gRIl6pxeQgq70$eQ{>Z=epedmnU_U`Z-)vRm$%JYBB$om^w>?+<=|71xoz(3%KcNs#jB^7kX}zPM+B2&8YL*tIh4o1Zw; zpP&7~r@lY)dpbo^I0IyP3`5ftOfv`q1Lu5rfbrzyw?*5Z$F&)lIwYHqaz=om=*nnt0x+7D@~o$S5?f@^S}Zc_ zL2B~^+g$PE;#Zyu444Lhk#P>D88Hop2`dR(OVYIvB&cmYg93j=vd#Sq2dPKdR?DIV zn2LM}+g!`U^-EaE+tRb`{1B1(m&1=>cG?2pC0wYI&lWW zRi{qxM7>_zgTAhSmb-2Yp&jzl4#rTXp&<6RFc437le{DaGe1N5 z=n_-v&p2yc8(4?&-K_I?z9l7)B--yNa3!a&}JGOu)|E6I8`2>)Ac2>o0f=2&eyH7|}gS9Y%5 zA+?y(J)_`#?72aFx@eIX8rsA?5w2j}EQC4@e`Iz%h$VsC6^sV8VYv^m#PF1J{J!w> z(jQd#VUQl-lYv970f$-LiBDV=?P6KyAW=mKDi~a=|bFQ`73W(X~lhd`FWVQs36_N5B zl$QBs901^dg#xuA$^di%6V(hH{}K0GH+)JfUC;n-pL+3cLmrpmSMK zY&}2S+Ht^p00x+f$6yx?WCw3C>TtSgR#0G@utFi|gE8Apr2kkh#-Ln8zB$_4T86gE z{TS89gFtG6k=>j8Bz&q!wtiM5`@tesIxl{RQ{uIML`0H%_RLW65=h$?*N}WI)ReVk zgtmUu!k)ivpWUvFFLvtpDcl+e#r>M)wz8sGuawc;ELoix`V!_Q&OmfxcwfWZ{)To= zq`Ug4*dJMLjS$O0e@3quwAPLOIh^Vf%5)%pPAkXD*Ag<(x!Z>{#&xCIcH!IQI|=mq z`gGcV9v8~rsId5z3e)W7d3!l-w3y+Dz5mc>LAGv#W57?w9-)`P_byYS7#7Iq!J&!e zCd&6_R17sO)HQ{X`H5!`OG5$jkMA3!c3Zad#wCpD)h&g+@kpM%a^ZdGB5E zbQ%r0X6P$%&h2>rzV6<5RET}a`a6$5Qs|X_VSy^0g~tNfEp>ie=B1VM=&UX*bJTm8 z@a2{(3f*W~Nj>Y&(?P>*r}Igj*|T|z=stGohg)r5&d^cOT)TQz=)r0LSU{)0^HY6s zB|%9Wf7QlV)4D9X*{-t~Mc<8hF0!jz(z-amar!tpYhfqv_#vf3mkeT4v!5&ATJc#H8B}#q}x<^K&?Q z_wty-lQS5#%g6|EJ|y+>(hqcktxpT*+&V~8(v#c&VW4XC(LU?yQ{>yo<9s@vyd>-F?q!@~RVqC@dMHpP2>r{C&{U=>wd=BA%` ze;BT~1D!7z%gM#LsL~^B1j7_Z6T1&INfKX=qqOf&BviD@zQ`$Bo9rF&j51%^VQ#)Y zz(n_C~Y z9D>QLi0^l=EOVyk`h=>;vRTP3G9jL2e_luN?jGBTlS_zNjAK*kYo&|Gs8GlWNy7!a zxr!6}%&fU}(%ipN$Q6l*58&Z#xpk+eOK5~E7k}u{ z{(jii+My~+=U}#^;1ssssuWJEOLaTrTy<&5!xBU1)`J_13nfo)!h5*FOD4Ime_k9X z{PA>X=hoaF*;%&+;Y8I)ZmD&v_rz!+5ULl#-#1#Z3Xi?@Q30p2uxt5ygGd{Dqxqra7bH|6f;2!n|)T-rf z|J=iQJ?z9gck*tn&JV;`o?q;gf2&XKfLPZjq!F?8?$x8Lj-kLAgQ*l~Pha4OPy|1! z$EMqB%(*u56fuWGUf3}4*2?y@lbWT{-UrCYX(GOf>w8XmKHo=)Lt1iP>sJf)#fzBE zj?4#Mb$G-NClSQ}A>Vufnf8Ae_Y5#x!qp+YKyy{K=`@elZKm0%ca`0a}-~ZV~K5@eT z`BgtU;sf>t3<408W^kIuQH&umieT2C#!u_Zm8`6sXs!N09f@oSJO$VQTNxNt0L9tL zY*wxW7vQhIXws7jpgjo;MuF=!C(@SAVGRd5D|j`8LY1?T?*Q{YyM418#w=)3;nUgS}XFG5Gu29<6y-nwy_%%ZjsC7C$QAJdCIAM<4s2 z26PG!gGiuBTv+F$e{s*>zPQapU+w0<`=I{qgI_=PwNbB3x+g9lX852{uP>Jo(!aRm z13?=Wes^oXduPzEZtZvOEL^K%=tn!fkAEng<3@vLSr?xk53fSUT4?CrbZ#k2y8XTP z32wZF^WB`F{*>TlU2p+S@Vke3Wzc}%PZY;$YM+kOewyW4e>%D=D-!oF!zGsIn0;S1 zLVptV^6^{@G>&@vA&%d;J+vE}en;WU?9J8H@PC>qDuT0K!eNz*a%151U9YT6zCVs^ z$OT+6A~*#l_XA8(I5%nd$vxE9OSLjlKF$KkV6iH@`1)Q!O*Ff_aO|$lRRvG_%d_8! zxu@!Ld;fiifAZX~5o4f;TOs{Y=U;5(HlO%1&D(!z8vipU{q|YkKZL`V@5X%_;;* zzwD<1eJVzS3w)`{kS%z7E$>zg0zuyi(6(mG|Io;`n3op+JQ)N{t!4KE=-g0x zYB@tKrM^7;&1HE6WuE+5Q|p^yI)ByA4$2H?bgGrZhi?clSOY>?pd6TVdl7x;!(pDC zbj?A)f9-t)mwikH*NUT3<{!c4uVY*C#tJs_Petc%-wpKr?fkpD`Bm~Z^jY+_XdgAX zH?JAdQkb^$t#=+YFiSrRl;1`0%sthK8DhQI4^IfCrkB>_(G#Dvojagyu_6zLPP;UW zuzp9-FRK~2qZ+;Pm;V}YWiGTvlhPA2ME8hVf4>Vg^^T7TJk^aX_^_?-46}92YUZx3 zyIW}fECBk_sID$Q6VJ6=;)3gYmp|py;#VW&r5c|<%VSTyt^ zXs>gZa5uBdt~>SG9M}aN^Vs!|N7yRnq=sH>wX6FJ9jlHIjoQSHY?ls2EJN)Qv~TWe7r1xvbkv1sq0y$Wa7rz8iGK zN(@ymMWZ*xKB!vDbay<0YNqL=3<`}V!XEdCCs6zY@mixjy-JqBr*}GG_qVOqT5F?s8hGa&ot zfkRpTWO;Ap{dk>>>2j!TMS%XB;4<(ugTUjAIIgAULyV8`<#BZx_gpRmt=zoS!+#xI zrtV&LnIsTT^t=5;hPM*mJ@3IWe{M=rX**{0-lxmuDNRFMV=ZT1(KGQZ?)OlgU%Mo_ zpUmPyYGMxAH*w*Ylv$qjct~G}daSINid>tQPRp#o%Q764r+1lb@>m>Dg%JGP(zH9N zw)3RxXXr{6G`)Xuf#Z~XeQ%VDKUq&Lq`EFLv~j63J(6~p$NEH~yz&(1e|2-~I;l1G zGV`=_9HL}x6r(Vk7ssvgGa65YgyHdfEiu(27Thlmdu{sXjydA>9vcI!C3V%?A5RbV z$jr0~oiBxAO$$vM-9a~)veYF+ATZ$pzw{1`Mj!Ld4nFp|B0m*`%5P6P&9stT zD-}q;6q&Sh_?48gPAIXRf8Aa}x5kH}4rbIkSmFYA!+xdq^b`~KT+PI{2S-hQQIfh<~-aHH(DFq1hbZ!hm$oX%XKFLrBO4-T7TFlH*x#lIHnp-3_h zB724n8~YgXT;big>{O~Kw%?nl1l1L+a*a{DR2}7rUam6ledpR)fB9~-*5+Qrfqk4I zSS07?kngCTI_;S;)-n<|9&)*|9jxXf$CZub=|zv0s2^D>p3Sn8z0TmP0iI^=e236Z z(JiB2s`h>rPOOzrXY8Qu_vmn3||<)W^$*zxkJlY~dAogHKAJ2A0H zeX!Kgu$T1i+z`wte-!6#1;#__gME)(?pXRCa3$3?IPnl9qUcAO(BiZ~fpEXoRkhC(=>|Hlbs zKqv!ooX62_Og?$Ezn)|Z-it5sjpF>hV1}O!^+%&wf4TAg$Ab(<$QRFnZzqJ%xUrha@ zwa}-CHA>ZRnqFzzaP}v1h5ze_W&i&bV%h(MSl*J!<|9_R%ll^HMjCRu+-dF^qsCA+ ziH?wla0pJ$1$p0uEZyho&EsNnS^8@Ba%L-6yMyxh?(W;$Q z{|2#0QP^J?T!0vL*UQoCp}p^ew~vLD>UURL%;2nkQS6?7(^($WM2a}mNhvQHv+$@! z9X4eGEr~SL?A^H%n|z8qSAAZuWRVb0ahHoCue$~ULnpI-ihCmKrA42r`9%1{(c zA}B^6C`zHsrdB5~;?wLJcw4NAQ_x3VIm(JHGyzmo1rB;ppy!ij zjjiPAORfw7155!)t`u0kUO5tzf}RykZ;LefrWHuGWcy$Fp`kc<1yEbzn{Mh$4q~!( zs0z?-rxLIVa!+l!vgkjn4>HgPN51WC zTWpvWDwFK;-nzXtTn)!C|G5bGP_<3<3wit^B8V0{viP z`{|whdxtXUN1?iATVqb+kHLu;VNLpSN3Z+mp_bwC@m6rQf4`n7a^YUk zTJJBJbltVDy^Ms%C9fEJevYWaI~80dji5xRjU|D1JY3RyQ62@eo)-x>OQkZjD+8^7 zSlTM~V&qnMhZWsE57yn_VDgF8MT%4-X`xu7@geuP)bHUi9dAq%x#9k`C(8K6K(&+k zsC*S^d%_N9?Q$27kH&~!e~PLZW6Ur0TOqIF(I~PoAdDm-Lrn}dgP2Z>XLzOnJ+ScV zm)X^{4rl8`*tmuemYKEnK;)>c)}2D6Fekzog6{oSzT&H~zx(0&konQfHXcMt)^d+9 zJ$8=Lcr8ZuOzyAR;`nWw-0q3l9IX18>_v2Zgv7HBk91WP2376xe@AVE_!AMD&hoe^{pHO}gaa@vRBvF^!4!t9v_~3^%^TL!M~WsSxX3 zWkh-7_m`IdM;yPLP8P~uLP>j!U1E%abH|Yz6OgsL&lTgE6pd?h(u;&b4;Hy7%nVX7 zwF-NWvEyF$lpQn9f4%A!iF8y`cAMyR<>}%;@7@PKDLBl5U*P(GXvVn zXDXcLorLl>@sp^1hTf*VsPE9&;N+3tr={PIEeQtK^dWf0i()aaX z5e4^p`0WtNdIxzkCdvW@x6AH*uyIQArDLNXBJqBnJgf0=qtr%Im#ZOKBfHzY5COrO%0}>~|bAwX%PX8GYMvt*oDN4n1Hf z$yX=0KC;l8vwEMi^Shq@z7#?DKc3^e-oH5K8}&heH5k0IrS)T5aVOmq`6q7m2d91T zf2&`f@?%mU0mSSm$xt-LpcqAxI89OvO%pT-4McGqWEOn78b|=1g(q7bl>tMte50O|z>+yRJD$iPV+lj>$1k2@I`VN0G6Wj|ref~T!PlbyomBqm38LM7b7lO_mXEy9G)EyAF09$|W-O>^b!a1Xin41eH-tBrPj7Gv~rG(|*?Jv9S{ zEr&XusJp?xsLVRK&{`>W1UcL+e-Yn1Y`Job^E6XC_g=U+WSQ*XaI~){1Fxm*@tj6g z@({#OqI`W|GSfK2kR2R}q>p<0wLX$;c)VOh^|x~SWYZCk!yXC}BQDrmjU-2sKXUKa z#B`r;ck$fxMXFb<74=S6BuW>^3uGNx@>*S=ne2>aNe?GXZMD1IWnccqfBWPfz#3}I z2@e*qz7*;as?HGxzU@u{ozO#}2lV!R*Y4{J1&(;i`6WA@mqc=|sqh>$)M?UtPyq|3 z+$FwUNV^j?l<|zdfJuUHM;t@ z^Nin?%TBtoSLlXA%w=``$RdF*d8g^8lKxl=(#GVu6^H&9`lpS?Mb?R zetqtVDk)hcU7z@Zc4>?7z{vrWg+_8v!&Dw+3pIkd89!Eo{{ZL3f8amD`QJG0XPp1r zDL>#mO0Fd}MlmFc07V%>lOzdaTlUOaRTB)eJ_49eUBPX_vK6bzgiar$dXx`OBW z|6-GB0B}?`f4QB?_^!U)lwLn85ABV; z@bIZopDhutG(^^fLZ7m1D)^}!9z&m`{#@3_@v>_QET~=gIiB~vSO#Y3`nTIN&0;A{}oF zZ98Q*H3myL>cAdt<O$Z;n#z0SpY;)n51n`}I$o+t{c$-Q8ir!OzWoJ3?{9PX% z@FoEUzQQ>e09nn0#7RvlpNrZLx8dS%KyP&YJEl!|GcycCP3$J`aYY_ zdG8!A7@C%ZbsaU1fA;k9H`q5n)AvO>=YPPy=}7e>_Q9;hZh?*PX~rkDVs9S`UDlYDwN#yUUyHU`t6B?h;f)Tn>i-`y(KWaMRR{`w93X;M_}lemcaT!e5SNhBYcdN zCU4k<0qgDQk!e$brHHo-SuLwBM5PL~GZ!*PyOUzh=Xd*{W6ePNtW_!f;xRrY@mJ)o ze~U9E!|BnIMB6TFy0I@N&T9<>FTrzZy6SY*Lpq{1sM#NoVBaEI3)jgY=`1cts?0$*5N@Fl1(rzI~>=Mrn0 zrA(~qW~a7P2j?yx%zM2XB^(l>vrzGbH)Bg7aCzvnf3QvQ1Nb;Di^B~0A0ggvock%@ z{dC?Bc!w~+yp2RD3MXiWg2B`XhgX~xszs6L827PT(M{S2|*PJ`q-&@h5i$ zeRf;Fdsly8`pYk{<*B(NW&2{fU-Cjna+jtQaRLDK{sl^@*Yc|#-!4S&t)WgvGtcOzC{^D`*0hbm0iHp(Y zvUkoB5iaisE;xC(8*kc<8Wg&F`rchQW{20MfyPUln#Nq{QI-yKUhVn3?dG1z)Hc$J zBZt=Lh`V}ZJ+DWxv~057e`VN3k6K?nKDo0FH4^HP9!*kJL$9nZV?@&03vD$^D2`vE zcrk}|Tx1v_(^%QM}!-P2|P1)J!FOOvP4se>1nCyz3~dbW1P} z^kY993R^$KEPb{3?ocsQdFAwy&KD-roKqR} zh~gIHL&I*PdhW@1hW8I|5D&XFgS26o7c!yo$!_cBa;-GiA)R{t4k+VsJTJZxp3E_6 zCj6+qN|tkxvTt)g;IITy`7UyU4!o`JP=*RhlblTE^~W(uPK3VL0+jp>s!{hje_Mc?Lj z>ACZD-cLfh6C77Qhvv=FpziW?%;cqu=qPn($|A=fGOmBvf5P!SJ*B)e`<@C!m4$u~IZOQgjdVr8yi^qK>{n`6hAZ#k^@sYZN|-eXmU^Pp z0Y1TKDqhv=G(u9OG$^g4huLpS5Nh% z_83}q^V5bLv6HyPsVGk(IFqdI#H%N&1J10seDeBte<*qT9#0>qGUPC3zH0lVYxWGe z7J0pHl{bjZjt4WKx%+v=kh&WsKiOUH?CyP9aVUcc)n!fvHMp52V9u41O#glI8DY_? zwU)}90oq%}gJ5$Db5Cddge1F{>51o|nT59ygk^l|4{lafPds@sJ9r;Zl=W~GeWDfR zic|3pe`?5Xpoo3p=``0%>PuO$2k)b-gtcR%TN#JZ@bI$l?QrHu_Lku@e|lA!wO_#z zFY6J~{dAuhtbZ7u*YctT<>l^YGc|{9sTBv_bE05>6h)ljv0u?R8J>knEYc2A9NeCD~t`alUWuJ29#y!&mE1;|cZ zKI-|q{jP7vej-)>^x6*+^|#mkz?^8B#xM*a5nyA45(u?YCyZFX6$D9OIE~RLgCd{G zf1B5`4K&U0&Dj4sYo$ml z{UWz*)Aa=_Gy5{bNmEea;pngZjn?vT6UcxjBe?}RtYs{`F`$*}0lUWps1eBo^a)7{ zTz#!};mwhfKtPfBC5;e9HzyJp80)Uwe~{c%jqyMF8yy_bA)LRJiVmyb)^7c&PQz=h zBOZ*1Hqq_aPqPys>*qY`wJ;MmW!=|h&W)frSD<#uJu%j~%ZDP*0ks&ENrxu6PsHC= zz}DXlVSU+mGzqYzAC6Tl^BH+_K?V1fCaGWI5Wakp@3c5;MR?el5p)XcK9RqnP7Zwj7#`(URH5PMpBYw1{IL(nIU8{3-Oj#;48buP&$7K}LecG1&?)Ki9 zgl>D#@N%A>@#JIf3=eND<4os$>|jhr*%sYL*fW}gZk)zzNlj+YxD35`_x)WPE>xi> zc?QLY*=>fXrpG7T6%6(4UHO^Df5TxE29}V!mou5Ehq)y>tBhPq#Jy3UI*Gt<{(avk zFof__l@2#_>f4~3L){$4T1fOVoe%IVzQWU4L$t9yHHF*ZJ;4q3f!H;%UO6eRYaRi; zu}fTOHo6i=nTrIQ5u~-4l@HhQOte=?iP~u7YtL}wc+WPA>l}*?$Ev}pe~B*54g{2P zlN2FxGkZlEVMLR|a$&5@&%{0Wd3qj=2FDgjb@9{9I9fHSP@-XZXuLn895v52G|HXm zy*m|WiDS?l>6wbM+{x8jMqJCtbzi1FUlYQ-#*P-I?R$z!GYveOt@3ka1}a4MoS(jI zhbw8V(hte-4zKOO=pWkke{yq}bG*N;)Wts5RMbw<`X=JK+J86ttv)+O%&{wheI)N% zMa>x=ZM8qbq8fHuuCTEyn}`{%2pd*=|?y1gvW)t*PI@1UF&~sWG;HDL`?IPQT7Zh%gn)VyA5|$wMFZDF67=OKp+AP9Y zad^^37=|6iUyc@6f2HRagVfmj8r&A7Be*ELlh4JAM=XQr;MR_wS?dZLICgfBDzWSu zg?$nA%9a@HP2S46WUBcZg%h2OR=ChBDxXA2%EfrCgplz)-=gMi+W4UBI_jSH7t*GA z=fGn1Az;X6kDAoWZ~Ubh1j6ae>UgWpeiDrJ(RgRj+wmB9e@B(dC3?fw5sNu33ZD%p zwXxd-PwE5**02-FY*vOyW29a16t0fJNAx@QnXH7m_=Iq}D({I0b2XTB>FfN$Upe}L zJ>*6>dDv-psYYXe^o8A{jZxpvPsUrUh6jD5YYM7U|FtJyDuyeQfN6VyFCzX*gzSOM z1y$hccXT=^e<9V3a5xqhS><~28jCS!-`TziDrolJt$JqmkK}c8rZ912VTx}#DdVruomJFm6W)E(jL zO7G7;?#s1=w36)$QWL37FBkq2Jp0Ez(`IPq$K^&Sf8zv``q|U%GgmMwmxu5PS=f1-*eFa(E53Z)Q?S(y=v6BtEOFbe!137DWi?Ii)C*etXXgIj`!{0uJzh*pK^3)p#p9{b*P>&m9M=c;%bLX|KR!?;KBnC1>N=|2 zq>q;#6{nY^c=pNXS{v(F%$*Zi3VD|F){>69p^$Byn(KI@Z_yhaIK9Gx+T;$DHM2@8 z9V|}Is#q~=vC19vv(07bSF+B}j5z4|e{z~p2cgQ_i`=2Tf?ROunh%O{Nq=STKe3u* zhNU31*ShpFa`YIchu4COJ~O-cx!j??BZW9^nwGpBx^wIv@Pdc?x1H0}$qqXDeNjfI zqh2$D#+0D(XD1##flGDEioJ&^L0ngvrR^xjQs9_pIFE=QawIyjF2SsrWM1E;fAiar z^vz9{f8Ou?qfBFv2l~z4hi3uqxFT=rAA4JK+vWH6U%dJM`F}J|__;m)zj3_}X#3}D z{kS5A<0MI8ID^6z2sR-Jyfp*VrU?)Sg`}!M1T~4mCtu9 zXyEsQZcX;&Bgqa+fPN$|zz$f-e{3Zl1po&O3Ns+|z>)RWuT*~&@E1xl&`u{dMIs0& zz`?=<5QijN0A36L7c6(sV2(*|jzbjwwS&IaoL~u~03etX;QsI}C?y3E2md%0G(qi> zd~1`dtZ~lnl!0Gd>iu&Vq%Qp!J_oV4#@1bbR6d+yr^$2tm4&<9hZ4lge|84+JrM0)ciZv!b+b$mXVv@8QC3J$q{~90`Ix(ZZxIACJ-=q1pt^9HAP|tJd;a_- zS6;`#n9vly9UD(SRikdre%BFydXRiD4mQjf-))T%DdyBk1QJBI>;m86@#(m*%?CCr zwNan2V5JZa0RdacBt#I9e^GjhoF+l>@1jK=kfQoen`Q2;37PdfUFGcjyFzI#2x>56j=>r9Mg)L_9V>5kevMa?z>YoV0v~H zP9cSs)9?uG-ifkqw#O=LT>L_dx2%gSM)juV2>IG)M)0mAo5gM=f91z=tae9ECWn`C z-$~qATmB4Of8H#6vMb?aD>jIHh1qLK4>3zW8GV^Ets@=9+X5f1!tt&*i-=C0{3?4J z#+v|9jsvE(4ue+3ze1gQ9> z1BT)N5?8dLH^1!_YKkr8g!$YR3FxUewHlQGSY1JgVn8=+e+9V}sM5^_3D~ve8yUmE zV%tj9P!PoOD+62%@W+)Kg0;0A%q3Tby8aoZK$ug!IV7z_a0TKO4_6FK2;j*^6@X(h z+ol_}MNuKemIn=kKcguC)%9NE{7+ZV>>y-nobt{$>6!4^p|x7BRX#Vs^?z(v7{5NiqKKL?_Jv4g;OMaFdl=d3&MD2-q9M>&Uge{Tk-h#zip{CBxff6_;@4tHL| z&d}rp*BQ4(I}REHOS3WIs9kGduF87p-E(?*wv97<%1>2Qk0R~h@+`cLC*h3mjU9W2 z+D4e2>+H5(b6tiP<}|o)?b0RbG0ZtlsKF3@-N+b%;qU8I1mzO-SM@Ll%yb!PlYj_1 zd#Jo&f1SprV(w?gMZ$-LU2kr9bL$|SHy(T3acMZeMr7b#7?JL z_b654xy#vtxjE{-mD;qlF{0NyP_py0Wg~Fte^hy2_xG|2bq%{q3>>_1?*%>d;>g(4 zi>~43iZe~R+;>{(42~jgyVyM!b5Vr6wjaXrHi%W&Aie(FOJJ)p{s!b{}JPG>h+ ztn!JKEoGiD1J)d6MlWY42o{DJibG^Ke@ZKhURGSl(>)+hT!>LSD3mVpE3Zj3c|7+B zTfFEaJnGbw9AIlWAx5uF(+_>LA6sJwh}vN~s=(LjR^4zW^|eC&Wl?HIL@Q;Ur#D{zRyvE{fQ(E}?kTMw`Bx5AUjG`fW}RPB;1mUA=Ae z(Yfi6!55^qROxs}f_IIU#QM$aN#<^mJw*{cv_l1SA3UVvwcRW5%U1>VF)R1=sbt^o zZOaU|eUP`Ld7!&;%pSRXA;>*!e@e#TNS{2Kh9YV>XK%+F)O^`L8(l2%H_a3#9&39n z>c-vDU%l9=*a60C!ryZde~B5Uy!fWp9c~8XJ}3X7ht%b~H;OnK_}S0RQb=TLEbdau z^PRIeoR#>e6>@AN#93~avB}=lomtMzCuE_QDO!K#raf0YLNPzE^}RDygDc-Ve@xjSl+8eSu$*L4Tyo~l`|Ru;$fm{gBgkBcjMwg@&QHQO zrLiphy2c1O=6INrtz%0Rj%q9@bb7H*mnNkg7s&`w9pU_+tfFst5Lz z!1Uwu&f7M4q{+?WKLv7RG=}P|3%te_RIALcz*f0or+M^_-Af`ws(4Z0NS528b92RI4D~eKQLHW$Qn7 z-gp&IhPeMLfPhPtBD7zlMINNXOi^t%`(y3}s2B7H+-v@sd+FX~hV@?xP>5 zC!}oQiuPUS>K)3NZm$;ICa)@>?MKDVqCbf`Q|MwliQUU>vF5ttR@6Q2pxv!;dVxRg zb2*nd<;dJ}jF|Ghiobo^pF>OHSN;MA^6?WDtDzpDHaP6LL2xct#!Twn`?8-DMB|-b z3U7WA=|JFXfBm_a@lD0bz37lBcCSqJg|Iz;q>8 z6p$2*o4YoCW$aR|?+s;Q^+CTPmX>$)#g9yTJYJJiCLDa>!CVW9gK(_Eo9?U)=U%SH zeT~#+_*xdlHkvDgm=!brfr`~#v8=p2|7G<_U8b9%e^UPb7u$E&{Ly_h%f1W+a1wOg zzP?^7?`8Kz@BU-s?!Ua+cWt}hztRuW3YwuA3ZroX!w7`JQG6|5DX`;3QZR$8Zu&qs5xWwjQ> z6alm$f1tINfZ+lXgBBg|gvvq1$^5ElO_@y#vaRT@z=v#FS7f_M6a^Rx4D=Fkw=oJT z*7a7`nlvUh(QXco7u&Kx@g+m10FMNW2BMA?v*~}%DmH=iHT^E1YS47_+BF936o-O9 zjvS9Of8X#beg;k1uEdkhoj%4D)|N>n)sJN6fAly+noJ6An}5^C*kVSubSO9uvq3p< z$mu>cr|?Y6v>ZU^0d0EN6yT9t|L7=h6vJSdy?oCn(-7d>8d`zjA(K69eJzq$84{rO=HQ#M9 zf1|IF(hhk(pHC5O=V{7A;cyd;y$>61hb<5KQX$8RRUR!8=VcNMl}jAGry?Ni*-PoQ zE|FWT-+hiQ}MXN8EPbd4Ss2Q8XU_^ZJ}s&9>P-#4f4tT3S8@O%Ba zulHaD?#5Q^gL*kJ^x`Fo^z*1z?jSa-f6M*7GMtS(~wbWA`JS;pr$!{l^E&!Gb;wFSB z;=8PLO#klys`I}JsK2_}p98A%BcLKH1tmxZLueXCF$_geiX<2qUD+p&G3fd`e~nS_ zr}lXmx^?9kF!}}JePT=b+Z1RZ9E$*>jsyW1%V57s{EM@#_r`#cagKv2?+TDr|DIN z`n$OHq|hBvu#nWn>XkEJNqV((da~;(cXehepwi5Xz^D~ky$Z-ZVIEV8+qBR5_7LH- zZXFItaxgP(=rDLTo`S*1f1w$RSKs4>R>C4k7s)Urr;5v@>(gO>*9bU(=iEQa82Z+OYt4Eo z`Rey~B)i>x+AZ)~914UTFQ<`YF_U$n;KbieG=|Mn+Rc_=!;aSwe=K4M!#N8u?O;xm z95h&o6g_u3fmSnb8;2f4CI`WciwL>uo*ms%>C9U?zw(I=Vl0PiJfAudI>e2}K< zH22$0s%W^#9Xk>)vygXq(VmMQ+MP4}c=QxU)tbUR9h&+&-?5vAa);vG`uM4Gp6&BA zRzqo@&iI^IzHGE;f9Xk7TUJR2(S3Yg%e-;KX8IvK*v>q>SAJhBNO2aQLhTkDBJ6es zIn7DHm$2e3biuy&6xO}7Vy8iJ%$~V;Aa{Jv+5H7ypGVYdfEnDfju=cv_5A4Q=^B!8 z7{32s*4}K(Q3P2QedjCsoqG=R)N7pw1|dL9Vum+ni9sMhe**FK7nE~mWM}2>-PJ2A ziV#QI3hlhmjHx_2oVvuH7NLwU;yJ%d%U4 z)QZNNvieBpcejFLC}UFKA*&u?2O9<+pW;oTnpNC zLE%m#QK$Ywpg<{v%!Q3r?G-gi+JmM~WyDErjkI-H94nj^~(^ zNW{K2>m0Fw_++TUy5xp+s$GHrwOT&+*Q2#Bwu;#6fACa54bH2AXpV$lr%FeSp}x*u3~AFuaIeb z@o@#5e=O#dbJAz`Gs|z7Xm$JqHxBp<<($j)!k?|>;DOg}h$j($ZB;@a!N`@UBN+z* z_#l8xx)IVaXWooR1Fb!8cy{c;vGq&F4=L{&9&r`~wcEi~Z&2Twy6F?7W?1Qfh4VrZ zfciXHmMEP)8rF2(ljQ_T4v^R?u5QCO_PTv=e~N`$5cz~uX{#ND8NaQQau<>pVpu~D zz(GX9-dYoIqo|&X#U>jYeF=J8E_p`VDhRLSFZO9dAk>A^$s#J~giVS$YQ5F6A#lEr z>nR!7Q&KzE%#g2D!x{9A=^t>x9JvZ(&}ZvWriI_ZCB@$aba_7ED|`6#su5O$Kf0i`_{ ztJpV3Hb4YpJG{b>T^O;!rR`!&_V4s}e^hths4I4ahrT&%g1^01lDBczn{;F3-YeXb zc8DFMW$)q$2z&Bu4&Zdj#+P>bA-neJle%eO)Zrw!y1a5I$cz(R&e?hfq?Yv2pd38CIskeCce)P0xvu9zFPa z8v&anC^ex8D6y_sA5rHtPCJ7=rqAch5vgK?B0NAT>|0{Mk4iGz4y*;*e_W1n_9#^h zFmKBtz%`wQq)a~6KJN)vMD$Ty=3ZG#XcmOf? zFcfhKN40oClxe`=fQL*!DTslK$yCEMu`U#QRAE~<3M7v_xy-Po$(o;Q9~^58u+Vso z&C)xWJOPtuZxws<(I|7be<&iB#-#In^01kA?lqf|TZ>fYLRH$qcMOs(C~5|>f#BRl zUWcZ`&-Z5_tOQpDp`=aza4jMuV%p_=bF1WO>gPeFy~1-YOG?dx9@3s_17OJ4v{B6l zKH}@iIo1+w-b_@5W79-C;})mQS)aU=%jDBJaFTF2U~}x3O+Drne;3+7Mv|^Bjd?*U zEo<2K61<=46K3eOrNf@oKRIZP%Yd1sbD>xb>-yBaY(y3#9t6*KM}VE&O5#kP%$~_I zaO4XcZ!PpJ5jU(Ui&I1R(QqM1Zf~QZf8H&;SL|oL4iS+}?G-J6aw`wfs!}#vLa6QI z9Pd8A%wym7p}%v;fBDqir~r;DXR%{l@P%X4!#L#E^s!nsGCJE^gRu_hq5iQuZH^Be zq^gV5-P93|06jUZg$cRV;Y9q#W8t^6+bV1(;Bs(7+6C9nV7gvovq9kL2#x1RP!S zc5=_J<%+vRf2vg|<4AC1k*Z+lxeu5)7{lwcK4e#wWma@*Iq!IiIkzw`ch0+$Agj{c zc|nF&vz&W((5v}j7Xw32BsF_4pc#TgMMUdjT>O-H(Xl6107O8$zfXg?Ne3+sur!jE z3YU1d2i-P0C9!SBCqbtUWgTA=&1mjGqMoZ7br1JNT7?VZD1V2>k>Ue%bFcOxSD5X$ ziXaqv-N+Gsv+&{s;g`qKIn!|HA`_hfwiKj9=C4mHI?F8cDm_SROpt2xn5?zu9ZW-Q zzEk2c#n;ZC(g#M+x|jw-!9v4`-GPC)jjCvYm4-hKO(av>N7aP;Jk-@R6Jok|_3AY< zNjED#AK|rX9Dk2xxPtd39l^1>0@|e`A>5T;p;6FvFCRn;(?Rt5Jn$gFaWDf~$%DGB z*P=DgStSwTM$|$Ul+=q}>!u4Zv@v%9d3fD6_G-Vjr#SNY%`)0WYbV`1M~#IKE&CTu zw(lU)_aUNx4b1-0$sYl;pHKWAn4u&I69~ePD2g#K&VL{ji4rhHpd^JbIE-T?j-ns# z&F!A-)aRZ(@s=T^>D#I&-@V!q=Dko%?Xpl}`_{(|Gp5)_zS%oul)(4dX+iAm{#{G@ zmX=cQOhDwF9l!gozxRXlJrMqPBM59alPj{f?;W}K;;Gq1(`3VX_^P0Y71~Pi{kh#AF(5d~+N6g2^;^-%v}}rP8?C{+Kn!)mw&wDzm#Lx0&&W(O%`Q!%|^l)HwR3B zsYS6)B6eFE1~ZfOv9(=NM1~CyC`7i7Ud96gqSOw#Vf3*4clHC zw~X~l+x~8TGZZM^?F?Hx0Q)bfQ{Z8i{eGVQvQdNa3G)AJ>q6c3?Sl zoPXu!knRHyJ}RQMRB!t4g28_?(-nhCZjv5A2)XtTblwQEDX=m+KhZH06az2DwA{Gn zYCZ$<^@MZ`o2e|%GrvQ!MiFs#pXug0kic?TZKc5RZc_%s{j%MvE#E#Gjc{B}@Ki%? zSOn^HPocvgrs#>iG}yWj(Tyz0@-IdZ?0?japM(*4_Nk-)xlrvzYcF0~!P*yv_s9KI z8~^guFO~5ho%W-qjHWOQ-wGlElQ2o3D2!7GN?;5^Z@hNnv%3QSqY}9f3yOCh^P5%f z{yofIZt`s34%s;4R+x4-wzq?2{BgPk^X7%ro2%mQ3Btb4g6)23yGEbdIWdX8=YI>u zu6K{$Q`TfV^1F14?R!PMD<-gfU$xn)TlV(YWZt@c_&sibk$qVirQR#V**ouo-p@+k za`yB$f#E7pHeQiV}RzQpARarc9<8nV#hCgGKwq038s~w{5P#8`Xc537G5C&dr|fd zizDyOpVeUz99I2OPr1YU0zXA(SmO8MGvFijS<^+~Nk~r-!w>Z;~F2y22Jqss(9S$VcD48g%CHjStZ~NEN8tzoSY^p zTfX6HL_Q<-83))(xzW+tPCzJFGW-EqE&I8v`wzs@77CpBz%O#HZEu}2wjEVfW}r{H z*;OA0Z~iPk13s(IW}m2gntzy=scVKrkC_Y)whfmQle9Z=+3oSOd|i}(8UDXj-X{+)g7DuTQNsIhmT z;Z}>VcgXWrGq4{`RcgyO3{H$L=rxc6dXO4Q&3|kZtcK z!QZ45E)$r_9gfW7GbP0jn%@A_8rfW7Gb4{F|`IiJL}!295* znzv{|^Vc;G*oxk}Ir!H#Z@X8>6{`b3{$dWFI|nrT&UY(M?|&}B)Un1QwG0<7%y=>~ z$5!+g#BmLbL;$W!px3_Y?7f6A9bUG|I)PF>!?E6W6Iv*2;?>4p2Nn>>a$E2xGNln zcD?}Ep#h8W{;GTweG=NU^JcK3hpCi_5YMJy#a+};m8EA}t?p|1dZ?E3Q47cBq4^I} z0N^FR-I>~4A!pVXC1sgSVv+l8enxh zx!f)GsozpZ%)ntt*Qp^;-1pUJ0`=FFhXakHt^7($q83uqcxDE66sqL*%vTOb$=vnF zhrQiVTUHb2&d0}7E`}bs#D&rfN^GBvPJm?vjeplMoxnsj<5G{BLn@+Xvau8 zGYNghr31fl2bTTpV){=M+CX+_Be($JyuFB7s)LihYHPLKnac7vRN<$60)hRsPr#np z>WpZ0*eny|Q2dIxY@&JoRjm!o8LqoT-|mZ3Dc>Ha5othu^l`Se0i3I(T)9#-xbW`t z(SP@{8aUbY1R0hA#gFqMToA3Z3y3o2(=4z0$e!yXo!w5*C63Gy=wSCLDiGrEeRIZS zjiF7v2{#glyJa{buFJq9#$CPG1qBP+N(^$r4(01gBJv{Ds{-6LN~CDh8gg!6XBJil z%at8JV6&w-Li_zcAXWc@<&561x@N>wuYYH?XX>@} zUnqxC24?hmPNmFoY*^-U4zf4?R$^H0!tsecKbgBDjurrKc&Qu6$0~~H%y@1-zaGd?A2VFI zQga%|+7h`j0tUxycx2Bj2P?^gJ&3;0=!E(RfsQ)7D7~$a7IbBZrlwcZ5M|L*R2Y$u zf;Tw$byZmU@R}a00S?(5!WnLv>3_rKE@n#rnc(DE4ce#1SQO`$m?qSi_s}5xM-DO1 z?IUdq8*73`@|KGS4ANp`u4t7AvktAY9MI7CIwxkhJ=+1D#S-B0f59!^sD~Y|@lV2qe;xNY>2Ke4^7L}+ny$R&<9~-1xc}9K zzJ@3M{vtmJDX0x_kr;v#FpQ%#fl&y_FeCxLMHDoG;Ts=C$n5}1Y~P|j++}-HMI`xm z@?JCB0MNdN_TDtx5EEYP>$KTB)H=&Itn+dH1Gc-DB;+n}*mxkC>}|a50`wb4A?Xgq zHjKADU{4Q1cG8IAJHShSn}72I@8}Rkcin29?BHpCj<>PNMqLYLAAxSTkAQb(oW8}l z$U9y;dFL;Fc7S_(18jhjczdb9pJFLCH1#J`%ExRDteQIK8Y`$$d3UA(A| z<(gcUdj55b1lTR!{OSYo!Z%I5CVj?b=Rn_c4c*AsyQF2t1kHLN3V*b3j=2JR{Q{qk z$PXt;$&tB+{`@^6#C*GH4LFa7g$+cJ} zM;nWT{mYVH+1GfQ&PO@m#`yj;O`mu!=c0{xX?>~vtsyQoi+=#Om+&;fhiWaLS&e~P z4dM85&j*~bgv^0FlcdvCB8~yM77@pvC^(sGlQq@CmU-&X1NS>9-s>j3t4!4iSBn_T zHbbieXm`;?9SywjpV$1=s0dqT9B!WjEm!p0X}!KKz7w#(i$jhID#jJm=rE=b(SJ6N4}tgdHg1y$KT z>g%eTFYt5Y8?wSR}kEd93P^n@{c zL|RbpF!&%J->xOaSKUdXj~roBb3ZhFA6{Le9UY)x#M)fZG(99u*LjpLpKq9Evu2ye z?U={f8J{U549-drSvkx#_d6t*x(fRyc0ei?c$g*`F=$fw$O+nhE0gU~(6ZbF+`al| z->O{^D}OUybma7$7GQo}9*<^Y1LgVC3LDU&+8q8)zWQ@q<3XqiQ?97BI*lhHC?NT= zZ+R5=kF4bCY3KCnu*C5c$ z4xd^s8VoVBe8YU{%qte3cPS}3I-n>5zpprAmj-()O`YKBQJvKOxk{^#>Qhw#<$vj@ zu2EA9^yz-5q!Z(B?{^$McuKn_BWp*~Wj>gPqweU!U6~(`%XwKZ93;s}Rc&Mz0HtDV z$>*Y?W^`_#GQ!2WE{=f;Ef3`xuApuppk`g~t7s`_g#>kEh%#?-96^qfz5~RgKPvMC zU!N!Ge*xX$|6AzpA6)3q=nnrL-GAW(LhYS31coUDpjOX$n=Yq*$n(Q$ZiLA!2cB4>B6VZ8}I|NtCU}n-9Byj4YJ#T z9q`{EyZs#CKO?*IfAn%C3KDpG>EQCf5@N)>=jeCDIB&-+wa8i@|?fb~^c# z-519AnxI~g@&?zVl8l~d`pkc4RPh^u7?;zJ@wgu9MIJMN7TZmpHQ~;Fp z9yu2WmmINsj$g*Bn16&a&1g@_lL^Q7|0Xxa$(y|H+JAjDru$)?g?q!ql&{C8)jkCU~w?-%s$hJU`%)|h!uINphjD6*H? zEZzqVTOq{h{i^vp6lUYvZ)d6Bdlb;`Qn|7C?EUP!RYJZ`G8lT#{=zq+9q-I~tI69J z+Y8<(|8~nD;WuxGcexn-&SS*Vy;g7cfPL1Syxr6PY@v64@4B>K2OD~I_baVQ$EMO@ zR?L>%3qBa+R(~G_D&92S{j%$lTSpJcBz>!0?q0*j$e;G*k?-B|B;Zrh;wL}7Pty@) ztIJChzxQ9h8P)x|3-c*-5%`w6xG%!6n7;k{bLDaf_t#JJ{bP-Tiw)uL8sI4Xo ztTjY0&Gw8fH`6!tY_%)J?grFwx zo{WEI7Cv5+)jGr{CW2M1Hb(EL2vIk}+UXh$W(^s@l$}fg(h#&ZRJP;61Ub~wnHf&> zy_j3(tUX*zR>Km)OI<(_<-v@{Ue3>T@4$Q}m_R{aS)M)yBQ+uledCYh2WAF_{#uWu z*Sh&a41fB?(Ixc`8swUh1@KSJ3_kWUNLfHTo_S63>?@VmhXeSqFT6T-R&stw?WqcR z^v*qw4&6MNKGHg>1_>dpQO6h(i?dMS3an(IN~JvO@KK8%NQC9(7FLAbv;uP+<*LcW%a9kVk=wN|4Lc?>nr}pviif- ze!#RiNs%bZ5ZJyRv%y-7VQ6~8xf|A{VHzPQlp>f9^T^)`-y2FL-kzRmwpTw2dHWN; zaUzwyJCyNW7>keR#l(B&ykW=PLm=B{vm5j*$h{iw`i*=a-llJ_z5Kl;6KA^)WViCe zet*}eEZ%C9bbn})?A3Jtg51Z!du~^@n-`$i-oM#Z~hPr6h5~DpWUPT+pl^&Wo#`r5%4=T6fE1 z=`Ty`chV1bL$ZRrelD%6CTK51IJ`6`gn#uN0z3iQ|vu?5IG9NHDPd{T1!h|fYcSB3+C}BTbfEw9enLEj(nJ0*Nt$In(4sHdKf1F;P3!S-+Po?y(v4Beo^>}{>hM2BOWEao6%v$4E zJ$fQt&(`7w2$G+ks~-;s{K$h9uGl_ldKgSokt09j^1(Zk)O}nz^2u$g3*Cg19bJL z!;}}<6K@j$_|=U!?_gQolRLVwhnzw#-B zhu78DQ8GQm19hKW7&VpW(m;u)4=gmOV_g@R_Ah%3r-CvC91dQ8B z8wj=HV=;?j(Q_n9Ll&Bi%aBC1d!5`-w- zi?V&+fJ&9uYRpviIF1@yxkP8Z1i%;Q*w8(`$t?AwCgh{g;~$63(>)J>vO?fX^u$aB zblTn{GVi1%Zu-Y9*w0I_D9{r*Zw9v5LMeSCjJOFJ#O;>J`ly6`)_;WDwdK+e1aiB0 zXP`emsk_99h1!FoH_-meB>7u`x6+Xpj#wGg@x6x5d8e6|nwiugIkiFjn31EZ2E)_> zjEy}Sn5DzVlp`0%uZ)v_g~h&JgZ2?MLpazM5e?zKovLMM4En0}K8yJ0Od_|MvLQtp zrXDDKG47<14uN1lpMQOk`%8-_5B$L~lbOCC`*fhgK+&;VKRF;27#2Ksv=x>@1{{y^ z1r5e3fQ|;@7oDf~iP4T|0=3+@!66OHSkOD&vCI}$+{4@G?mPnhcCJH9&Ib_S?{#u) zy?(^OhqzTg8;M_P(Kat*roy{9#SCX4F01q$7$WPvshheaRDX_Hy=jeAWCE6x>FZH}$-0pLmxFCCcNP`UmGg&>MTO4=y zu&|!-fhsm|IiC6TmdUF8dQsYj&cJ!}tcoixw+C)RuOT#&+kK2MIz9m^zFij=UG zGnG7!cudb`k$)0kpA3VlO^$!G8R2SDNeHqyOEp-<;7AD3psIsS1q3LC@xcL|FG}V? zsyu1fN|kDH$d>!V%MX&*=#@sv+a>tv6~_n8TAsP01HDP6WouIo#1`qT6Q-?0M&ai+}vmmo2bWm-cM&;{)WuMjm&;GUh|2`l78Rq@Pd0)ZYFX#NQ z+)Tg}N#hJn;uu9>G>+3KNuxN1!#Io}3{EpBihh`wk?dZ{8`{}`4T0`z+8tdnyY^y3 zP1)PsWPjtA#M|DT_}GGK!)d#x%G+UTLuDAf6JP3m_HLCz@3?7uk_}3z95$-e=xXO(C7HdEBD{mQ^u6!58wSSvLHr$+jp1)T z{&sHul-xo5dB|M7b-DgG3o7;iSDMUz{hzbGmVb}aSI<9#H`n_qHN!iIiU*5^>gt?k zebxq?&+1{$-iv_1+wjgB=i~WHoW^z$cz4FrqjT-q-}Ur9M~m%?e!CVVL^i5P0H4fJ z(oY8+_lr5o`JcWr;CC1rvf^zz%g z(SJ-hbrbCRSspfiYD<3hI-R8Ail839-Bs2c!%iu+!7^}M9qc(SiwMNew5v_=;?b_Q zob+>$QSOwE(W~Q$EXDhEI*IaQ=0kcpXuJ@Uy_k59a6{F87L6$cV0dz}5%YkC=u656 z9OE;mgNsX;F3gk&+bc4xB_-B`rXB~5j(@ok>J?NFo#j?{Z1_B*;!)W!wXX@Xu9ieNLMy~46jM?04nTSTUOO8S5R+XbvMQ~ zX0#}Qa5J9PUO0NxK=apR!qpR$LcTVG1xV!iXuZ}(t-d;>4~%4Wd#Xzmtz7g1WdumywIuuL5m zF2e?;y(<0f3>jdLwz<74uEOBUt6E8ylDycLL{HfV*5j@^VlgU^1;opvs-$3t$U1Tw zuKra`DC`AKz;tXJ+3^CTYk%~VV)_c>t1x}V=t9OgFW`y#NaP;LF2XTZiIiH;L)Hp>YLdlg| zU*njc^c&!8Z_D;+7Qu6(nee7(N_IIMJL!arG2D?D@e(Zgh}U=de1Ge_e7rYas4{Ex zdiEWsP!nMB4v`&LN?whj+<2c-pU}j!5pnyAOw?lusu!*kx9XB94d)DZs7EeOIfoTRVM=EVqETv>t9L_R>p~1w@CdMoqRuPV5O*kQaQSs};fX;XDhG?)Q zE{P1BFR*(*s3=_X$A9iXSFb)2y_P+$N#?O}$Ti0ROiEXI40e5gs83%d83jo~3ey)K zKCb}X_b+nbac zmfRL*e;6Je7h}ylrBZ^w3}3p?_wp7?jeL=B0(@p9f4w8xNmWsXxMR$@AJ0)wiX<=* z2{BvD)xRqO2XnbH#v>IIvpD+q*X?w262WA{Q7};|_`5Km+`g8#uc;FI%$I*9`7piH zdX%`2Z~x-E|9|`c{Y#y)55s@{^86pdettUl$3;MbVql8K2?S+OW`k$UhSew-q4uZ? zjG|$TCJ~ZmNQT(GLO+cDL3SAh^%i{X@~-zVmB@EJ7yKT$?v|n08w92Q&OX-YX4yLm z4c;fS6!Td-v1@zrw~Oc7CWR^9!Vma8m`>jIpBr-eU4P6gp6ud@U0DXdK^C*mfcIU( zceHB~?~Sw=*+n1dd;VOcI|@VJ-Y*->B;q|_Eq|ZB;Z2m-=Na42NFx8vcMSPQjF#?m z3*`Q75m29Ef#(cTPy39%oE1`u6$YN@ar}4@(ET7>M_$>>mvEh9trKDEy|T9?;#}IT zIEOx7o_|sL0jNBuWyka5dk5pgVtwvVc~0zSkMFZBzKn=9VNW?LXJD)B=-1^x=hMM2 z$29Tq(};KvD$-{8{3WQU7l7PX_K34ju|J3Yk{-p!E-hBCbLmJ{tyYIjZ>EQ4~}HWK@Urmh<3?fiS~V zBqomIx_9qk)0}zowCH)(jP3CSkiD|R`MIYLAXlEx^mz9YMDsh}&nVAYlsk6M=0~TW!541BySDA*z=nB?o0X(dpYvBNsPCh7jTosVE* z9BRd}0-RWn7MHOPZ|+m}48R;mC$tyZiGMvO{SX&XfUJ7Q4^%l8L2Ev1a{=wLtImx8 zvQ-w-S5@WhrSfFy8C(ul#;K> zV59f1tIMqqLuNU^))FZ*o#UYCtZhY?jDJ0=BaKkO7_#Dsn#yt((z~rK!lKm8gkf&2 zNdC0}MEme!78aF4MwVocAQL+Jnw<7&= zFI44d;!Bk@ihH7><6Y=)X@Aq0RT!E_apChQ*Vg@Z~xn6DE563gzt=x!ATTku6Os|GSeXvPUQn*Qb4hEc?v-LzrkO`e~G{T zc>aIDU&IeOSd@fmj6iUL!Kn>{kqpi-BuO9$PLU)+;ro~qCVw#+K`5GlKU~#Auw9pX=6^z<<$os4<{<=5fikCo|nTyg>L@&s9 znJTy$%uvkiK3LWJZ?M-6y@0>MUe1r$%Mi2|pgYkI*lRzyANwcRYlmLIe}lcgwfP+P z4=Z_R)s<|0`*22WV%i~l#pRATX8eK!Gd~?vA1S1?eSg@9T$U?}i$}z{pnM*nFA<$X zx5)+RWP;iDdA`$$p(Z(Es_;{>d)opcDFE4gV)pKLu2MFe}qsUZViJ9JOgd>KnxI=pg#rdcHA`sQ#!?bU4mUsk!(+p9Tp3$FW@7 z`4{)CtB-!&+4w)7Qe!{Z`S^>|zSO+mp7NuKI)C;~(WV&`r)d)3{$XH(BuShhh&@dk zW@v)>&=YDa3NW*89&c=LBV`%BchmP62x|8yPpEg4L$M#({-i(Vn`pMjR#2bppOAfH zZ7Y%_vscTlB4x==LeqTD#ooPD;GHrum*x(d6IRANSo{|VOMwKxc!r?r5 zpDXX@os27>@$8p+7VR(BZ9K>0Io`c~i%66O`IkM6FNhfU?HQ(~RD=x{>0fX`N9NXNhv16FP5`Wfl znqtVdUq=+A;-zq(x09+LfHIh@fq~c2ztBicXwY@)9%Us_#wGHv*NZNnV1=S`J1eukkah8#=02g5@ z8mhD!?!pVtP!^gS0L{}XnG+_0%o4(F+M+;2yj+g4qFTIU`GTJQF1FjeoIYDs)GK zm!At$KC_Ex0+7F?r0E+9oqyI2I9P&So9#uZxO;f%2PtX?DY~m{ z;!n5^2!(SU8yG3omP2H#ug|KQWxT#5F2hF$rCRKZC|QI#C5mXFweQte%#2T#1Jj2o z1cAVjzj||)AwHMk_>YAD5h2XTL?XaZ_l~E4Nbk@!=hdZdtdBgxwtpMx;r%Jiq}h5e zWSV!D?u7dhFpd{ElUP!HF;&{mkBCR$PX&Z{M8WwPV#=tZ=jjuGvWoDg6zRU)wcQ~@ zkA4Bcx_fwNkNtr^cYcKPS9bxJp>)h;##tQdskf!&%!>#KPBI{N2POsiTzL&cM%GS$ z>`s2ux!Q_!5`KKd1%ImDP;b-^YNTcylymW7+~-P*k-Q+O8!%j-}20per)R?#5)N?J#MWC46Sn64|Z%Mx9+AqCX-P=EV(=dR4tdKd_MoCj08 z6j!NBW3l(-#2ZRjqm}v^wRLu_!ZUd_`3EHy&~p-i%UM4@8|?VH9$JV=`P=e5pv2`Q z+Q-^8#n@$O#iAlR3k=sZ3khnUj=$#6A#!w_1ey`*vj;8^v?93Gaxe3W^S6)x!9$%G zdP+olh8}30k{R4wHwf|aEcP`%oWnd!AR61iAsvv}9Dl+RafL!RMY)cU^f(bV8{W(Q z4iS&h7p{uPCTphmXZY^8ds0mB$ZE^%B%-#%_y+ZekBtR3aJQS6 z8NU9+JORX~Zs<*zE=0>Wl5`jUA`VzHdTrxb@FEhuiKXzD&cl4e@A7UE-E7a|I#(P*&q~=$`^tsfdYa zlzSD;E@+Ct8Sv4BG`pXL9w;B$-Cq^jCVxiv6j8Vu8VHHwi|xQ|nAcg_J(Q`uQ#V0F z1ZKA?ZcY+iuj6O$9^jwc!&TXHw)!osj(We{4GFrR!uu(9NNc~qKhisV@%gkQgx01o zjATY?V($zifQryM(`I7^v$6Fk@K~G-WkK;)LZF(WWhZqusK;LfggE89&*#aoE`N}0 zX}Gz)_`3xd^*p%hk&$~V)5B&hX+Jp@B;4gUxAS9^ja?MXE?sC}vCKGbnCpLBEWeM6 z_%Dj(UtQ+oV)@-Ae&E0)jKc)Ib6=b$X$Ia(B1P=e*6kQd;M7)4QHc$zaP8LUx~een8$m0W#YX_ z7gKv7CX1aFZ)7-q_wTmahh@7st@xcz7k&$KKWQuCw|bZ&_qw>%!o7a)W80k%Z}oOJ zTh8|fr|$~9J;%oCw^y(|$4-fvogwF+UK6#q28-SP<&R{z-1!@N{aP#~u73(w&~wa} zj988CD#9{ajF@hp%ip-L{1*lD2N#w<70jQaBfexb{!kpT4+DSy1=U4XpGkFHSvS&^6AKY~B57tU#k-?}p)1RLl9Njs_H|7d9xyQz!jCQt<$VYDTyoP5E9fux)qZi|XgjKbM*4mwU zY?4p`&WNB30hU`VQ66e$qkSmUI+v;N*BEavs|3|cZ`J2?cJK2lN0+>V?-RmH6|bAb zMuMbP@)8PW)7xn}RN%d@o0aZDuQ|`>01@l;$zTtynFb^9TJspG?SERmT!)xk?ZzK4 z6euR*kSC8<#jP<6Pop{|Lat^S5<4|Iy-rn0K>}^GAs=?@8be@{o+xpINQd=v+DU*# zx9Uz1D58L~bYm%}kl}nCX9s6s%wUf1>LH*HIGl@RgLLkN#Oe&zU&D&Z^r+>-w z-RkQK4Tu%C`npnCM9v(>tfV@lhEZi4=Za#)=^HyMk9ESktZ(KOu=(DmtQq=YNr3+&e5dKytvQCB0(C zfvFe8(i~FIhkI~TUK@!)u_M~QjFI&t#4fGHmfVNAO3i&bMPhS_7n25T`epYeSi1>r z_=*sfoksKxxPmQdk2Jgro!`;+&AB7(giOyI;UC-=^1OGJe=8pRL4Bx}QXMoY_q2{s zFh#GXYwI`@Pk+$G0e&;N{jD%Tj;1?jbJ>xLZ-={s0oDrXuj=urMxI}%MOK0`NDQG& zHOFuM|JuH!$x*CJcF(Wqy{t{lL+`{q&mdrj0Erm{Vi3RnfiknIGAr||;dPrun8^q( zA}m4&_j~-fzuJfzD7>W&!o#l4nLIhDY4Y;s2Tyh?gnw*Cf#RugNYki0zbEJHI)k)y z!@_z#qA zxaz)NJ=cYb&fd~T|C-$sy&+zGR#-wda6O>)!iC+X2jEq*VngJL+e46y*?aq1h^Ar0PrS4l`gv;eMD=JrbUJ3LTk=ufIDn|aP(MS=R5B%9BvNbex z^;+seE@U!TAo8jzMBAc*b3O&<@|Kgt^LZgPpnsnpYR7Jm7<0l_JiX{COz=vwf|Jzn z?t&QidoUX~ie)5A3}A737lO{0+_k0hu6>w5Fq=wyTW+Rum(+XQO1Y?9sIrZgr^1dS zRF)P7Yvr?ys0wAX<)+{Hk`%1ax!q)G$pB|$BxJi+djpsxDVq1v)eqx#-_q<@_s>3;wZTpiv6n{0eok~oirX~sNP=FM24@%lc84;jYZ z96&JM{#Z}XnpoW8x|Cn7)|sq}H23rj;x|;}d~$H!TFsurXrDb9Q_Rr8z$C&&>gf|wrtZHu(}DhROI6bU*^LvgwiO{tq6*jaPUE1Pr!?Ap7= z&q!774N?2?`6!oSq@{~+eTz8H4t1P$s$j;ef`kHuGUQVU3^1!7INMi+50vgyN{KUF zweT`_4k+QyH7~CyEED0t+JAB*82*~PUJiFJ$N~+;*JXWDJ5w{;q*NaXP&SWpda;|Q zZ)Oxyn^CIb-6WJpA)9lD6*hdUxA0!zx@QXVxu!)~wadjKE0>A*Xx+h6(6w6Cq?%7% zQ%<=q)a%@1&`xIrq3lWc0&<%MBlgwD$MA#>oh_Mzj=S-LMJM?LTz~8+is_uy2-ZQ= zOKfey)ThthuHRGM@$|*(L3w)L>EX=KZd#?mqmNC5r?UBEpW6z^Z)~1zLfW^~yCYgo zWeBbD6GO6A{u0?W4tw(2Xa%$Ba#cOZvxXQWiyLh>qvxvS0X>k#f76P1&TpHeClRQ8 z@;cop)4}-cLcdq7`+q-fMMOdLZ(0$I`^LR{V~b<+fbJ@Nb=UHA;v@(KteKuFE)~nC z;b*Sd-jCUTd z6e+C`0u9FdO^er_9p}~e%8<~-aXgg6aRGSddsLMRH^R*`+N2GH=yuk4 zmqO7|)<5GW$7KOO`jUxiM8#|QEA(yN1P!mX5pe{F{eM#cHA>@vpc%)iJR3{lV6GU; zR&bvbLt$}vod!r3-gg~+h=x81$$ijkFK2I0-2@)pJ<#QRaxfhWMZagyml*O*T(UOC zJXL3LEwrkJ&>o3eVdsr_d+w7CxOS3@kZ-&4n1cl4pW!} zQJTT-Xn)l~Yyqh!MXG2C3DK*bU3v+G`#c8%dY!LgRE_6_napd-{F4+NXDP<63dVgLPKM=-?iCtZXeb#(t?{ck$Dzg+uW7lI@~g4~ff0q>7n5`%E~NRxmO@QBd? z$-_AWC2<5tU~E6~(?nS4<2iho8B@pR6$u??AAg5NE<3iNc0{yeA?Op|K>VH*_5-xr z7a{23EOPkPB}Z2v`Qn;)=;n?WWc-n@e0mWmb_h+5;G5&LUqCzPlWKvY2Q=JYWZ*Fg zd&pJL1HVBZu$N}X7{+0VcQh6bbVrhhjs*Q8mqCswTKKb(2YuOO!;jd`zl4F?d^|N zMypnMNZKce{!K;aTHiL(en=pb81SKeQ`VK2PY~>3fQSBOe7Ai$`0d!Am36wL+1|o^ zbu{Ld)df z3q-QmpA!;4B`GhzhTLnfLHTBqzGmCAN*QyDD7BWJ!-0P|{?M;$?~+fr;u&n)!6j$9 zxf!klT1>XR7r=>+Pj-s#EFCfwy}k<`CzK1O(-XZ|j7G5v6MKIhE(?DEUicY}>|tp;>9Lw~5NQW9+m!Jme^5G+P18LBEVd{(3qT>(&KEhT?D zMRyK`XcaLQ)uu$%=Ze!#iY?OVO!M0XHJUcr61|tsaLg{M!3l%$o(l|6Vht|Bcw$J} z42ep(9!kcM@5g1>QUsma@V&`c=`47ua})w1T)J-b8KMEP^^UP{58#yka#^%Yhm7u0+pF(-q` zLg!8{45u4JqNP`poO`pb7U9{J1kyN#M1ps7ylOL>dhGCufus)RH$OqioTJE=JX}7r z6HML)I!7#2dc@h*4@@m!)_==5vFTPsu_6laj=+iVHMi6=*EzPFZ@=q)6+80_^Icz56|IyrEc8U^+V* z{Dpen+4G$Sw_Xnw zjebW}*Bx81(g1TnjK9-B00@k;h3OnK#IJS5-dWaA<1Mp|{z@25{aP;aSoD@kFS0JM z1aU%UC@tX|Rig`CKQ=&o_9&D^9?d@Ak4K|#`HuWNuOs7YSuYqXok8_*!8@O{W+U>d z$~?YYjYs-G0)1I5ppb8%IpBZ(d>7gKohcWj78&U6@K^J-e=ZdajQTgk`^>x7sJmRb zc%7W#q0fI_qBHl)wKw2v zUZ4@I;+iq#JfP+RKGZ`y-A?N!ogbZfLPtvAGo)_1t$_^q7FsG{zTixCv?`MQfZSob z8N7O=?PN;^3&KsYd6@srtMfbELtl7dh(XmfpH0 zf8u911uh-6Kbw?wV$*-FuGwMY&z>SZYw7V;a+8kKmxq8?v9?-0RJt9XzUBh0Ks`9e z$o#m1z)+z&l84m(-@M5+rcT^=+4jE~PxNERfNj1d71s6-l?dF&;ym3W_;M83vB*M& z;0)lQ4wu}=cBXt5C5Q$+w)rX+)(Uv^M8L&gr0m28|*)h-;pazl$06KQKuuu5m&x0qB+sD4`c5K-l z8HAt7RpK)Qk$r#cDv|qFnMq0=0BJ{DU~)jT4F1);!zZ}w(0+g)mC60sfWz-ccXNNU z{hbi-0DbW7c)!E)13I)H#K%rEA&--(FTGOe&=DU7%h-V|chI?C3Z?#1AEJNsAlt zR|ERP1T6%^+du+PB0iFGF@W_L8wm)M; z!Mz0EyxKU9l0C@gB`%uND^`Vp-2*omT_yL+gX3e2{Ou%Ouah>alixPqeAar?qUQdU z%_W`8r(Ay_2fEyt+2)`&V%ov%L?XqWegU6^z%~lrd>T_ZKdm>A%m#c(s^J`D{H->3 zc=CLz#npLDRnEo5{PGr+#G*%2&MltrTy(G~%qcTbV(>sfbpT$Q8r@EbGUOMBLK+wU zds(nwo{<&;xM5yz=Y=X>*OK(w%U)`%8>oQo4_SY(ItWWqawg4i@VhfFFC4mGg<^o0 zy!_29*y^GQA){GLuauV*)RcS0^EP0yxbHKf&Nny2gBOV}TZ?PgbGXiTbbDNkw0a!^ z9A;e~(Z;iK1|Ao2ikRpFL1BTg5i!-z27jn*#JQg03R8qm9uV#9&fIe|^<>(SKB@=} z8NYv#QHwbog_6I|V*gy54o3O!YTjVhs6|TKM3l#K)&N`8aFdu%jqbo<>R2K@bB)H zejG#*W_9ZXYNz6!UKf6cV?Q-WQh)3~{NsXnIPQkODu^Gw$4?5Pwk4}xe<_H-Q4xQS zg7`V;|MP+f92N0z7sM|lTGZTsVos?%CrX)^?L61|GPh6^zbYeFm2~w(!_LG8DkJmERUco}bIuybO#GnwC&>`Wz`zw4K3u%= z^$?$q6Czn9&(B98Kotf$nTXJvelmZj4L>z*W`O2arlj1aQfCbaUfmVz^;Sg|mw4rV=Xer$GyM7)7&OjG<{OBaq$eFkq1L&5JTG?mcqL z^Y~WH!ugV!YW~EWIjwZ2P5KV9&=6LglP?J{Iz8yi8I#Fd4Yr1=|=8w4$+CYJ%gHON9SNgDaw%LDEa3U3SnoM$v zZKrNfCM?uW{tI`SXJm2uBR5CcdZj8yzj>#1fr{A>p)&n=ru*;BDgT~_KR}Kx%`Q-R zQRmUNFhwM;>$u%?wgbP{fS9VhSfm~<3_)<7;p8W1ylF77S-Ac)o}2#%4TvtJ*jNq% z<{}6nAqvK1CueYyd&hsF$~Xrxzr2e)l}}YIQv5Xt0^wEh6Ry}vr&(LX>1lmFw-qiL zr2t?oG^CW3bA}0%Z`yd?aG6}+$um8X8rub+E3=^TRFZtP6>dxQv8>cyQ!w6~1} zp6}usH2nV2kv4&QTs>n@)KgPC>3TL5E1KE;^FJvV#mzpx6fb`aDNj5}r#W)(A|y3| zixAJ0%CW|PI(LqPu)EU;V5Yv27J&vbxK&t9nq};hg7?UL+<(A?-BNRPhbSz+SHQ!` z2s+HKh{Mzr_IOox-E02orkP4K1oEXGw=jj{+e`P1F=tP_GO!O(NX-!G?zTMw;nflB z=PmHHD-2 z5+>QElnBOdS65l>xx5&x#qjv28cpX z5+)A~E(DSU0Yel8;SdE882U5hbp#B4Vx|v|BJ98{Savi8cESi}hc(g8>UJ_merP7~ zb8AZcV@Z@^pRbSeH=KOLBiS*p020S|>JeRYKcIgSMjm_iJF45SdjA#j+9~fbK>tx| z?EG@ab;s;O`e_i{KaNTtwJ`b-`yNB~;4z;1F}pl;xzHywC;b>F?XWQY5LOC2Of!#p zRP--t=yo8l@;B{=elhY{5a+9LokFK-Lgt6u5}Kuxa{y%c$mokd@z+BECfy|6Itsm zuHbbS3B{CYFC7{xf);J~j$2y1x-mQc2{B|*8STP5z(qaZO9P4qSFWE+9eJu3EzN%; zxfmBdpn6NrH+HyK#p?DtajJtW8<&e+yWC~4H}^aP&gR7yN--Sx##H*5%q0V_I*~XS zQuXYns;}mSP!z?a{l^_<;Jcu_zu}v64J{_Ddm%IZ6p=^;*nxhD=)v^_dVc~0 zKc4)@bH7dA0AEILtes4c7flkxOC^8NYPy4F2SZ;?jJ8Cy+~Ek^PH*FG5|>NR#9J*f z7-`z9*uQ``;b%miRSh+A>4f%5R?H_H9=+KO6e>b;nxWQI>SESnEZ2vxZLn2cC{Z^l zW9Fy^7^;U?NIf$ven}WO9KQH000080EDvyUAGS(2*v{hgtG))m)Rl+I0S^V1YNfkBM9XX z4urD=T{RU$9c2Lk0C57BA0r4FmwHee3zsc42n&D0f!0Wa4F}W2i-?h=0YlMiv$WH) z+U_>fLE-P6mSTuE=d|dt4 zW{&c~=Oxa5uQga3{+}Wv)QqvqK6~9Ceg_4W6y=;NXHl-xxt8C1xEv-Zk>eio($wl2 z)6^eGfzI>Gve%3IzWM=BO9KQH000080EDvyUBL`+@9ru90G*xy0384T0B~t=FJG7V zcMT7JV{dJ6VRSBVdF_4ecH2g>;D0?uUuk>^DimzVGnqs?%#I_=6YV&buO%nht7sSq z07XO~zyP3R#_MzTHTL!PNwz+^8{LftNjoz;_nve0!zPLD>guZM`s(V&o;cV)5OJAg z#qF_JHq(PI@Ryyvojq|=}`J8ML6G89LDPY$0R96dSuTzr=m(MnuIKWB>&n!U{8 zw5ZcWG^J=}sd&9=W@RBR%4zcuRjGJUE{h~;GWZQXq?L$ZyrQYHn`KkNx7#XB=V{R( ztcx@iFV9X+UtOGr&AUcKMIuhlUtPXA`}*zW`I`&;5!&rct8y+Tlj*WqR_SCSviYKa ztf2W#UFORso$&Xa9r=4%t4~#`KGm~jljZu`s@~aYs@3t1fIszPo$sXY;&jo7Gy3JU zs>mnP~Uk3`Z8%&i?oKJZlXGW<)23MxJlnN^bnpciwxMBwt%^lg!&@O(@Sg^ z)nW>tsh2tT$>e64!_|D$}0KxR{o%#_R@x0y-Aa-O5;YuLcvKU zh(cQN9K6CY|XtVye;EOWiWGFVm(FAV;Fw7Kmx%Imc4 zKwp{dpk>qO9@aT%*sK;&g*sJCL46kG!$@R>h$CELjVRJIsX=(^WrcjeF7UK2=c#yz zRub4`3G-c4<*iaz{4+{3NjN?ZW@X(-iQN;sySwd^0seb+yg(62uK$b;;TufkdN33R z2VV)h!C)i?W}6UzNwBwnd3h7%Ek5#R5Rn!|m@nd}FliNecP>>L#^rng`YagyEeQ9A zf5v})HyA1^&Z8!tK||dsq+fWh?cns|v08%kExZMqFM`97I8qJeV?=Hw5W>z*3LM*@ z-?MTX%?@9?Djk0G!MTJcXXoEn(PE+Ht*g*|8oXCy!*U8OtsQKCP}?4r+fm~qQ+>Ll zRjjbg@mOzJI^I-ky(-e*~5^>wSneRj*$lp&}^ zXoDi*QJfB1(8#nZmUcA&dDX3>Wl?*w_-vtOL(;PuNIjvsR_|`oxu)KvR;+@@N&~kZ z9`#3mP4=WUKEXwQ%o241kU=ZQQIj%hxAg-FdYpqGbzlVFMfs9H8lC_u$<~>eBt?xF zOjs`^1#1>YY976tJb=i>QI*u=LjwqC)Nio^s{1I8L4?Uxl;X{c6Y=@s(H~&#p)~*> z{PXbl;nN-669wb=@na%mn5E4$EUVkc$cM+(G{&xbvgZMR_cVqy=Fp(hy1F)9prUL< zQrIq!RGBYY%f{U+>Yo%LX^3DD8jet&PMdj&i@r+dQAQ!enEQZ7J(8aekFQ&Rnn=)4 zbqC9^ECBSr0UEGun|@`}*G7C#oYghB6nlq%{PK$@$0E8fvjjL+WH1#`!m>}RdkBQY z%?i+*E^DNJ|Gbn|s)f?bqC$Yd(Blf!*;AAv41t7rCctvvrz?aB%QCvjvwD^$4wyxm zXYooz^Ad)+xOj=ePry`z}fOu&0Dat_i zcLaC<>ISG(tPHZD@6oms&tF{#l-6VMs2Yezz|&!W%a=W{0N|g97JJ z0(3>FhapmuKxA^5X%!5`c&wL-`pq1Pa*;Zkk-%&X`P_vnr5i<3h^+@;+X*uBV*0;cotPrjH(IUl?rGL+E3)s zO`3~;5?WR)C)fwY_W|g=#eKruE3HU!8gy_)*2x-Va{rdn-Pk8p1SLbg| zPo7H6uS@yA}Z|%hssdHvGG^%BCw^{EgVhhWjFb$tEeL2q@kIVCX0D zxbV}>O7{mda8bs0X%pV24XD`)0R+Q;q2@DEeP0c{54Hx^)6o})Ur?G&Ev|NM%N&IR z*fA}0RHzAhyx;=8Mjpa?spVtT(%^J}d&D@JgS13MLofgZ`7@}@XL<7q8iPCshN?i8-KDFMxJR;3x*&4=HlQc8AUgvF z-mx^Yf#bRoFt4Cn{2*?29;sP>skKbNhAw+mD_75<)|WFzDJzS71*$p)o+PNn$*_Xl zW++4=d3Av_5ukvol>-P#VNuR{j&y^^$^)_}%_Q#5*+c8)jh{4o;)fr8=z8`HIruK)vJPKN z&i^X@mw-<{z$Z@HjWKy@6ZS9JJrFJ;5dy!2`;W*)iNxz?m)~d~RXSg&3K5nQa$#;0 z{H0oLOWqP(1cv?wZBzoV5y~ebf!f403*dyburQ%&xH3N>l%H^(5%8Br=*%w^KOs_f zEB1S9_^M%n4k>Ye6X-sx%7RSDF5E3`9E}R7#v8DS6BkJzvU6jYM9ScNk+Y7xEF1 ztBO#{ti(Ws;mi~{%vPR++#pN{4o!xMmN;J~sU!-vtI)6_ZZC<#%gi=l-g?&CvT+n_o@8@>CG6;F?LeO_mm8A1pn zfA5}*+Fl0Mj1iLR^oL7YkSVe+LWD}WWcv_7@EET6Ne}RV`n$VB57F$=v6h3Cs;O+u zq(jM1(|6#*V(dsPfuYj^YbNS$G6yE|kmb3EFdhXz7AI5BXeTSH;$_yb)#c%K7i5-T z?WemgC|JhO7=0o}Xv9H6gBu~F%4kooe^mJrSo2~Q$%WR55~x|1n=nIMC@L2Tq)a%w z1v6AX4#`zOYD;dzM_<&C-ZaISw+2DALt9)=-nCS~tnlGjt3dn(GtvRACWoJ@xm=q#UYGLtYZxU- zAX^W+yeToen-VE9UKF)IN+JipMUH|B069|KyRapuwz8IcM}BRs{-lO1%;?cl**kmU z-?POF!n&3vTqsZAqeZuz@;(axf5;gkNui_g^Bu0#`ZZhdeA(|*fWRsE8mKM5Qq9yX zs-vc<0^@fGs3sH4%~q4iVA$`QDyU-%=oUNjUF=TZH4cLE7c)L+e+}$LK<0IRJQi(j zGXPM1{C1=Q(>A9n>a6F9k@E(DtUnQN@`AUe*}TyKpB$- zOlWI?R5iwg(Jjo!A)QG#Khge^&QPI(WdRBe490o`IOMY}wBee*J~|kZNW#?#`?U8W zMVu|;#NIUgxEL9y=mitl;oKWWF_KZ6+Fn_4Et9SmwPA!#E_d*H^?EfRt=raAdPRa; zI_@ni(yeu4Q<}G2PoDdxs-z#_CvR35fVIOI(Xx{1J=R{_p?(KNLSXrU=ig4= zUcUH(lSs>j^q$~zmQJeF*}mT8L9dR-*FV<#!PUW!58?G^!(D^N6IiGd3`(M$<`k4w zc8loR=K5@&e}M@G(m2i0_NpmYVQd*_3>GmFi1A!>Q!X3cxXmhvOoUDonb|;un7L=;akC+0TUtlM8l9SsxtY zpjllWeDTE}zHGm#hosnkG(kJ#Yd8lspaTaIVAzAAe=)dgixt6IwmM=LfZ0m&%+0Cm z&zPffyA1UnI~tm$JjuhYI6tGbHf2WYS-H&H+@9P|tag(?b?ZQ*Z1Z=n3LXG`JwDwc9$B`ww|mH(a!{5(pi953GKn6EdX6Fe;c) zfku?TG;HjcrnNohRQV5Ff`l1DfG>|?-M}+EBUR}38XoHc99eDwW9?EQ7U|;P$XS6Pl;&+!xK=wDm7lyQXND0B&+E_7SKpsf2_cvddI~YL|ELlqKXX$ZcRf1#@s+* z@DUuCjO(Q86j@E`*a8i(Ug5zV60XdiIzXeXzZOUKDGuuFmde`HCYniS%+$1aWd?mH zbHXf_yD~^?>87+()G&Q3NLvB*r;G)uWR^KF8yLJnC~uoUuglv9%t}@RF(yO_c<@4( ze>W4*)*Z8Ys0og>8)ikPi0Y)QCUsLT0!I;brig!Xxu_{RakHG-1wcD%aA9`hGXuuW zz(Ho370HAJC~%ZlFFt$Xxl}i`07HqM_-Q6fCsSIREQ>9HKt1IVqt-nQu%~M_^+_65 zbSM#vB`lX`7bjx31El9!O*P3ra&-6SvuP`eayzfPL1Pa#c>jTNoA3Xn`Y;H&R4sr< zBMqsWuVi&#CYA*|cv2#SLkAsjVXzz^0zohF4tDWcx{nXmhg-c)PHBe~@QbpN{=V zVq=WtN5yEkvnO6)B}`e|W7Qep^snMi2VaSQjpmF0CekJjoyZ)|E~zk@^FqF0+z7mH zGpD)l?Edi)9^Mb0j>PY!+${@k4m4^{w?X`>e&14Zu@r~Kx4 z5Qd?fN*@et1%mDl1$=yJ<1VEfEuf==*EVOwU`fwKE%p{Tj1fEIxjKO&+KO_x6E#eL zNm%{OTmQ1CCe^YiWR8^fqdTprMvhPH`Zl=KA64UuEa5gI%F56Ve@f81`HNlXfJBSN z#%*6sGrt8eK+)bd4B;Q%>k74CYSyt91$k!PPM~?P!fYKx12KqP@wiAoayo*c`a6%K zbrNB117OE8MJFPWM|Ch1R2g`a#t}X)(ak6ua6F?081{OZ)*Wz?xeyA@%Ve43B_(5I zAMI{i6rlZW)o9Sgf4zm2l<@{I)M70R&S$O#!cEpfi3%_+eAI9QFstQY1YV!VwPVgm z@HT6-edw%2t5#Pk$-q`{#;P*)$ur$v(5e2qt<%Nb_F4p>Ik@^|&VcG6s2!N3S-OXX zRA74X&Jls36;>+n);(?9-Dl~8k(R*B8(n{s)7GbgOPLrjf7}&kZ#csgV)LlsY!f(+ zi{(w8#gxH`jqHjp*aU2O;6(7jQ#$DbL)80t){V0b1hlPh<>}R!>@~3DP^6Z1r3A-^ zv^=2$N;O|W11u?ySnDeLIlne3QjbaJ271 zOD7{O8A2Yc%t&&v3{R$7!yOkj>pK-HAS#p}sGESMgABiV|KXZ;{_=-t1;c{0q)|%|9-k!}lN}Yjaux;;d;8ce&v0`{fNOAv$3f zVU~A_*(almO-C%H)btxE#NA4G2Lq-q3(o`t>@YmDMH1Do|3vGH6L( z2Qpaid@Y^`(AWLXt`j?SAd^e74)Wmr6Obknn8`%j;}606!w*B9Hsk5~juIKX_TP5Wy+gVCuXE47zp4@a25_ZINS# z2k?p0Pn6oq46jI8z+*e-j;J~m0*2shf4uAJO*${{Q*%o+B0W6^c4w8rD_9ERjfEUK zF>0yN1qk5dIP6@vmt?Ing`zh@zCE>GmB^}k4Yk5SmpG=_cvYh$(mu4px$k^d5pmcK2h@>p^thmBMDl`m_m=hI`nwGf24j1 zcDDeBlT&zum3gEwj^bH5!3Gmqet_p-I9>nMSUe4X|M6w69hi7I#0)*Ro8|39p3O_g zTt!$enrxo^N)fN?)1i}C@%g0oG{_)atn4d^Y`FfN1E4)T~LOO$W=MK;S>>nK;s+U>A>NG3jvRYu;0uQ@@fu^15e~4Pi2srG? zz&0~3AYroUtRLlG^|#)%s|6}q&gZMib^v>@nM60pOw}7SM*UruK8*Mt4f**DOL6hz zMzq=+&zJ!wQ_!(7vo_D$h?)76&heg|3XenLVxp3K>~Ew2_QW&Xo13MWZAjQxq8OSi z1Gs`5$hg-%IXz?$Uz>67e@F#ZBP%@NJO;FKLAH0?uy4-k0mAnn!po7(4qs=t2j@a% zjzNp0HK(wF@@98k85QyKD!pCiQPn0F@W|*+XL5AKP_Dz-Xklg}Eb`=8aaVITDk+yY zIh8u$-5Hddasbo8I~u;zRPdf15eLzBMO{ z0DeICpw@V7084v#TQh+t` z??&R)=?|A9@z>M;c^$6LFtrCXk`0+ADXLP!nDn6#7ujtQ;YHSnpu=6qH|}zpPh0`( z@ZL&YRM%e21B$Vnf5T6SbBCPDWy5%MKF(T9R60}LWKD$_u1`_PTP(;E>E6IWXgf8y z1AX8#YWNb9!xr(8xW!`J1ox#i+${<&q+C~JY3F~oD7tdGkg6^h_r5@Ru!B+s8X2e-aMWU`(O6(cYEMGBKY9 z*+{I0;!or8SE5X=vg`56<~1Jb<@_jzp%f-?Mxd5I(L&=ws#`o>8>t%7L>>2K4dKAW zwa99;>8N5OV=A-U`i24=5hnkbH~cVQ=^Vqolm4wt2zkVlncXKq_?Jhu5({uiy^pUB zuiF+aIAFDke>1Og%7L?dbmBsjZQUq#i||x(UOrKr*A2a#Z^0ks zwjb!ut|WEx@g^0F;N5D2&?lHK8S%#=eaAep7^mW_f1bX~aoxY$2!CZ^(0#T~m@WdH_i6RKYF%LuD1)=4MiN^l3-b~tLYV2RXDVA%pg7kkXiYxsp-E`2d0@a5&%i*w~qP1%N^g9pf9 z{`!UWl}hd9m)D9pX&u{eQfkiG(ci#re~EH;B+}_?jG@wXQtz#8(^^fxq&eEt zeD1G(G?6>(8MxrvO$a$p8`-y4w_9)+!_EZQl+XxAZmD7J`Uci>_GHE^`^N6)3QfD;6KBiIWb%+?6OXt- zFIpDMxap^&h^9iK=@8HxzWGd#YtPr{T7h}v9?2%e~+ z?V+gnWK09O)_qTWi?zA)q~$B(!$Z7hP$iJq1h z^lLVCPYFM>4@zxCZODepM-!>em-D1b0`XoX)?_1W{AzO`3EqCE}tk%05C7^F1LMstATaPg@OCldOSNb6U8zZyaoUrmlb3kWcDu+5E(D?Lf1g41EVB46$3k42 zb>v`=#nn~oD_`5*wPT^dIMZc!WF9K%=*#Lmzl%=ItSxGc&< zE>Pp8xQ%;^P3E-i-g=){SK=!mxXmlUZ1xkn{<5*u`Y(RYf92JMmCI)ZZ7%B`O zv!g42@DiDKu{xx6XbY9WX>&j|wCtbCq7&X=#kQEDgZ0g|8N0qYsbU;&LJfZJ&*mt6%)}a&C~Uxln|pC- z%Ypi^=}qA%e`JhQ+ec7`dU;XORY?jXr${X?Z*Q?ElrLb^w{Jyt%5S$`QWA;OUzW%! zsgJ2#J9~cm>hkQx+36ei^|V*f!LO5R{P)Shmy_%L-^sU38FzLWgIe#Go&&5Fuzd4W zZPgIc?H9fSKE|t$2CwLre!e0^U1^}-ex_dFXTX&-f2G6qJG^SJUgty~&BhnjsCW&R zjXcq(Us#OlB*7P_$0?|_TcYtm}DwXI$v zXAG%cf7Ml`YIF_Icuz{MstM&IDpit1Q}@r%UE7g-RWFNUd#F#$$~;kf=26^$#cZeF zw#VBlYd#R&;utzFJCQu}mErB~?mjb5;G5Q5o(c85y=3!oF>ABK-@#ebMsymQQw$|` z?mL2*81k|z&58D2%_N)BO`3S)01}$^RdIg^f9vmDi>P~XVHM)CKF#5Yf+Z-rXOJHc z>H0aF0V9JUFR3)9{eFq!+WR^!gc$Nc4N0eU4LK5ne|@^sJt_Thhu19L`n&xXbN^#7 zcTFnHVQl~Iq=%^=&(Lky@Qi(BP{SUn6 ze-u~SMU*A)#s`P%7;3fsdoXfa7W@ApLRYKD!T^>;nct@p0k=TJ z6n^}B!>x7DiLV=^1yjrGUUXP3(-(BruL==uEa@7*aP4sN{@Gf-;A;>%yZ@918}qx# z(IP$B;&g_(myZ*Gl3{pTP}1EKe=%S4jf41A6k&mP4TkvQZfx&uw)RFUg_XL!NN&5* z>%zUAwyaoHf7{IVlWrzzHOnhB#5y3I+6 zFI^(}sODWOSf0Z!qW8E1f#-~H-vhriRTQWA1`WC{fl%qrjuKT)t-PUUf8?_9o$m)( zelGWzweC`H+(Hnb#4(H5lyRAduHc&+Cu)WIj$arG?Nd|LOA?bbfBh>LF-$h$)lY^FW|rOFh}&lTs6~RuvMn6fw{JJHL(3VE z_+U)2enk9Z%0S!ogRQ5F`dh1=%|CpzUsg1PBb^ms-AW?l){?rL_3tk7%>{}2Z}14Sf6sDjK)vmqB)R({ zwc5NxolYS5)NSJKf^VT%t?$w~Q+oome`>vq9D}5=H|-BGj)NAxeuR#s@qU02L`FH4 z%G2Egp}3F^8D;tLU5oS{496{0?^s$HnS6Jb0R1&{r$Bc}%JsgQT+_x@tz?PqvGVR~ zw^dI8q>6JLx7T9@e{^fF&ug~Q<*Dm%#Tc~@S0{JYk1J)`{_(hS{M>`An~dnkmXjC1 z9o;St94^p1n%oEj|Fms#LSbnQdNAr&T1jh+5~{`Vw&IwY6ytzD5ByrK^_e7^ZbV9Z z3Jk(m(_gTQ)h#E07Ju$R%SBDO>pGO=C;j$ENq}w|fADzT@(0h=R7x0t6!Z%C zbwMm>3zcR`jjero4FlfQdVrT0=2)~kjp%*V9Zxi7TrsT0U~dq(#o%(O=$8n_9$AYO z6O~&X+b~d9*UmuUz{ghg7JGE0dE>g>v9?bM^7}I7UPW+)m0!}`yi*5oFv3(D|MqVC z-SE06lNSg6X;*RLt%x{_o}-%dasyo`O)dK?rSY$pg2e-mObA|4@#U7IdP0h5y)k{; zWDw`C+KaU1Yf#jtIa^HJ9Vdg?kr1!)*`TePKCNd7{{I6|O9KQH000080EDvyT|$?I z&pZqO09Gsj03es}cMTVRa%FRGb#h~6b1rasty*o5+_n+^zQ2OaA&@rRQ_oHs6pLQl z*s%)(t{ucqf%b4vxGU}+eOgHnwRg7;^4~i%d=W`WdwWR*!>1KzhQr~^@EMY4?EI^9 zwyq0VZI^7{ZO;FKUuI{sGxoA>4z1kYbS!;2XU{L5eSQA?;`z6K?BBBD2ljz~l1&C= zZ{=E4N))WCS$8AY4~OoiuGoiq)7|q{u-EmzDtIU18@z~?apGPLhMA< zp{x%=u(xkszIylJRn|Rpj8_GF`TpIErt!Kj258n{C>9$MSr$@Aj?8b0&99 z-F6^f!catB%1*R@yi~K9X;?`m)}8$&Yx`E#+bxXT{;ZXKYi_suPL}qkI=GhZ&|^E9L6d!&)>rbkmG7EyEkW!ZFQtiVVBzeBw*>=l zUX^0Umgl|wbyw6=Z(*3l$hZ?`{ilF5;19>78B%s}#FXES7*bX+v}!$S2xCBus1uFo ztJ$<-BOkPbfX7_+f@IW8|MzhK{MH-muJ4Dx}%U9#!%IrBb5rJ8-{tPCq6*8Zg-pJt zz!}K7ml(l~sLUuAEV*ALbH)|h412|AlTpuqPVbDm9|WHkH&_8)UtLkXfclPU+E##XD!>Mq{y5~B>b&9WTfP-uN>=;z z4H}3u$VMFWfYb%i3}GY{Nkbp1>UPJ=@?bJRCuwn99oSxh_6Qnaq1$(?g-M4G(7=g* zH%=k1X_P<1vMQn19j`za70h%Nj0F4&!48)d8R<2E5?Y>Lz%T$E6$<*^fv3rp1kiHm zr+}kF!WzD#IXIFV11}>pXuhmZjm8*c2+l0vK$pb$V-iAabtmgR4up63TYC)(tHiw# z*k>K^Vb;mrL?$#I=XnQZKYuh@wSPo^2*`~cr!dFxi^sk#>lH6`5c!$YEnLIIX>n1A zc?R*JGrL`s9#fEFELLt}+#c;CQkDEHP88ac4nduDx3K_=+>sXl$GmY;U+Z=(Jx_R zS#zCz!M?l$7_5n)uRPanc@8}jdJj1xWNnxA5jhl%C?!Eq-7C+FPkYt%aXJ9U#tm#2 z5dI|*>xH!!1WqX{4dzGX7*$Ds?Ln`ZM($oR&)pD&hFy3z zBG1RXeNvh*C_hRPQT;SoIT~S@@cMQV+{^M*nTkJC#c5;rK|3+#U}{KuG{nI5P`4vt zf-9DtW5~*|6xQuwOkf~%xcZL`@Pw*pAn|cQnk6$f3kuW;Z;0^Vv6-~Re^5V>un@JY zB=U_OWsM38>Z*aLk@Cv)zQI+iNTbdi`Z}t(@n>Kl`k>=TV2F$YsZJ~v1!Y;^iz3|& ze2cnFH|!6WxMTy~pjgQh+CfF<-1Ms(xz`Y?*u~uwr~nUNZ8X@zJrEdE3DJpVeK;KS z+FrTqa3^g&;gb`HF?9Sff3pP?7^Z+(XFP(lhsW5T@wH6WN3$f50Ca8eFv+?BM^` zL#x_n4HHxZ&R~U2^I%1aY$w>;J~Y9EuZom-r=>qWT(E4K^9Sq~ho%vck@#E6ybxm@ z!FwJ~xQeD~dF@-02xa9Dbplca72PPLB3U?h)b07RYg{RHe+@T`B~Qa)rAGp>+My%G zO#exHgN|(+ckST_(&~Ayx{B5Uc$* zO=#xWhJ(3_CHu04e~BK`BJfaiYH-BtV{-57_V#IVk3h&%3IcPeay(B2F$>O=z!vTp2GQU7NiZZIT6hHov;Kf1cwiN&7!?w-W^(oM>p#itzkO zqBWmAx)Bhd`~SlN@jK@FDRVbS;ZIH9kEHI2v>hMc};VBqw41u+zw3j1q~Jh zsva14C}ufCgTKyZZiHu`q}@~eI{S73i+Njr7FDkKOSxj_e;aU?CMMROMrUd!?v5BtWL-xUpwG?kD;a+ashJ&qYQQjbd(0|SB6n1rw|BvVlk;BoINOWs1w22 z-1svTKGuk?-JJq?++nva&y5}iVav!-kVDy{cTOfYk^iI19~{n>F6RVO={h}HqM9e6 ze}yIRfuz^&H=SD#>kae4oH}RndmYrg zE=SLhn-I&V_Sx)DAUJU1;blxA;Ju#9I(Ch0F z=J3ef;xIQqX*1kDB7Y4$HT1y$0Z>Z=1QY-O00;ntvjklmH5kiF7XSd0NdN#I0001Rm-KfH z6PF-u2qS+e+uCtrKilcfY?9TZBuHjVksJcFtn9~8cJHx&_au9htttQ{KvIg6X?4GD zi3AFTszQCk2W;?gz~<>9N><0Lkjueye9}4S9I%&Zw#}o}wPgOw9((fm=;`3e<0sG9 zuTc_i*=6`S$^sa68qIkk_=3rl$!pHexAHno*kymZl$$W;>~&fsi%>@J48HK3g|J>C z^JrGc6n?F8&euGV204y z7Ok^1m+Zw>axppU=$~=ATETwI&s3OiIXB;g%!|1!e{IVlGFo#5{!5fCqnMjD{w2yL z$ufWKsF#6x{3;SMj%MS#InQv9`t>|rXQ5;jeAx0bTJ_n2OFoxVo+z645$Vg_*jHkY@UAA*0y5@Nw2z#`6%U*k>yaK=1Gkz?S*8k3_!Z@DuR3Ht+eokl`%m z>US20a+&7qK8up+El+Oy9oGCrVhXPpQ4W82r}?(m=^%Z9k0V3q0Dzs+Gk-cw!Zn{x zVbt{1`1Q%V)2r#5led%C$Di-kF$2^DkZaUE3LbZvA;+lu?&|g6c^CKt ztPkTf;iK-Bi|)@qb$$$m;OkkuWiUSxqi&HW$6|iX*P$4!qj{bR5V-+R{x}rt;P!v0 z%hq8MExC}t+Kc_vVI`=^0*DpKt%DB!)9tVDzO9B@liWN&nDa7RkIP{9dnvCk?25p7X(y z%lS2t7WFQQ8%viAXwE8yz&1GuDI97ciB8GEiiu1BSYZ{X`aEy0Ibto@ zCN!)DXea{#*g(WKPQ!l%gL9UHSmLaM#TH7HZ~4zR+-ZJ5k?&_T&cML;w{!AhjZ zz(o4?v(JExM}`rePVJ#RC4L8Ng1lf)kB-?QO+3l2!+Zgv4cI3voQX6pWVKO6C9uVM zG8(9Sji)a{B7%vCZn?4TaM*#a(oCi)uO^pQr;``c^OKiZ_(Ffb!rUz@U0{TrPw%b;Fqe z+km_#aG=RzrTBlX0|xWwq2PG*z|vatafAFWTxT$7ScrT$i;^J>Vcj7ZU8MsB5FAby zzWGebt!n|m6T?Bo0CLv>aL@TEV480tz-@D##@wc1g+Hk9Fqevwp0kRPJM%$5f>{&= zF56&~L@<&FXI@2Tdol`ol1lrTgG#r)JfUDO?i~t-mfC+~hs>`dSE0bc*d5mq!j->& zVY6!WlqTdHJPBKxI;g4t!g*FIMmpAEj-E5~Q9zP()9Y&;m6LV?O%(GGEG76z=&O7N z2Za@a6pVb(nKnfm8PRwMYtLh_)$Eci&KnTs+)rl+P4BPURl;(@)Id-LbBGL3G0k%x zg19SVC6s^RQ~qoD3`c=FFE=kTK982#83IFNub}2kC10awiul%ywNf9>j^5(-^(hk9 zd=@4vp@!l4o`UNd$?4P=JYM!SZs^N`T>u*jJ}`X;12>IBu$@3$F}lhN(1?qCJI#w^ z^cuaOKEqHT%|^JK#dic7R8Rq*>W4e1S`G$PKiq#o4jwSh`Rxvc{mya!gr56q9;dSz z`RDFSFbq=azGHQI++gd9uQ`Pt>r>EFR-c|2l<+7y-sZ&1TkQ?Mt_fWt)Z+u z5|Do{aMnlUgV~QP5kyXhF&L>NbS;gNNjoS@DTEZ3fT2U69%7K?d#fS>+7MrRgr>E< zV`OobhpTmXjLyqE1&kaOwMama6Ga9L3$-?GQ-}DWq00ndr2L-7NlN1ESg+lf};pUXHCt$D)vLdBY=QBvHY zxPiS9K?9DC+ED5`&6i7h5yW6_f=dE{=kq)V66JF1ds?S>iW{_)x0io4AED%1?0R8w zx&iy#tGrrI(Gz{!HizVZy%SY%2ZMiXlEA;efm5As#4aZ8z)HV${Xh^5*Vn03;H-$^ z#Z;T;7QF{XL4w+Z)0bI$u+Pg2gn;Kx!JyLx(V)s0h=`nLa+`?jL6CAiNKg z8*=7;3G)zW`!>xND+umLKq&T4vO)%x>|<^g&UAWAd4S{LaI@J2Q8Fmvb+F*W|6GTH zayvt`=4l~@dK1Hgm?tZF-6DVeCZTY>3fActh{(er%~wPHeK5yNW*86A)(>Tr4KaLg z6%Q+Jeo#6Y?rBgQG(rgW3Ee!)`v(wkfw-N%Ap{oU z;6XVUgPXhd8?7;D*hBpfzBMzyGL`UJuuWkPmTKMA?72g-M8U))nEB zcTHXdLqAMQ)L^M4=@?*iqoacqvqgOF_ly*gRASE9NEhN*gS7-B&}VrV5s0ofBkVc* z0AIiiKmWuqvq%R7A`^~pyTN`2xZY_q{yJbMiv@d10*M9<%svMTqC!Hxp$wrYmhh;y zkXm7?#17NcHEa5ap4fj%2Od3b7#+(IAuYZL;Z!FrYJ45uO&g2;@#slSlh^mRbL!EP zf7nks-=ATqjJnH7ODLDnuL1mV)Ur&^a*=Vf&JxEpZ7+%#o#z>G;gRhOJ+gH-{F-?m z6K@Ys9avg3+B2*%k6=*O^E_P5W8JT<%esGf+=c)Axw{u0mLPvVTImmM%G8*e*IqMN zWy!``dNjjWMZ4(r=uk)7bzNf|nznQjO|!k|+Q?d_#lWYdgeL|9^nwzspgv5BQ$nQe^uE2wWGZ|J=KCj?$OcsPvf2nTolPDGXLNI z-V5*Y&B!z}d={uV9FJTVOG*h+jI&A{ z%t!Q#1ZBX9aGkTM4d`#VC5Sw&XsrXEWMe8pyq!SHL-vc$|$xOAkk~0^q_7uxVyW9{iHV$ zS4YqwiK+}h&$tuS9oVfASiz?xxC$?u5P{hVl5=JQ*yNFvpdwYRoeI>lb=^w((50~; z(AFw3Y~(Oa7K8w_$e~U|zDpu}|B6OYEctalWAw?{uVGxM8NO%J%s@l54iITo2kU;=-Yrsy-P-ABLC*^06&0$U>b44ZbG0outj-D}uU zQSYUzc7z);BdgRZ^%zzAnW5sZ+%5cxBYZQmb>rpwhcw?JBB~Lt$*9*P+uyHpPMT`e z%b}{LdIS*NLJZDRS>ZIg*{y=sid^=gAX9GvsA(b#0|c!l@{#l6+v7sOUlo5cw=FSY zeJdnx=ArM&dcCYQ1o{e)deKctFhO97O&6^1z9#2LH<|isEX2+L1f94q%NC`fE7%l{ zWa<1`369Qasxsu|*}IFEOZ!{iy1Ff>}b_*e=TejuWIcdDF-g$?VwnxLKA2tTJx&ZjG zTPKthPG?a95f(kO*sx!=m7h=&zkRI(rt8JH4n@0moCh%{26}XKfBA& zv;!9?Y+ZRx_i-)2xj=t64!ok6gY;m7<|>-EXfXUNmKFodjD1|dk4hhstnfRKS>Y=% zJ0t))5r$#^SGyb`1hVQSLoHAVR`qsG{e1)kKbmU`J}ZD{ zC&(R&kyMMX#WNKs-NzBUAD?y#e=BiYN8V4s@snX699GyQrr3X#6r*Ub66|26;@3Uw zyxrK4$gQ`;)>2dd~I?-2`^J9ZR@PI8ufL|AR;)P&Zkw-PnGy{w%&^;3I!6J;3{ zL=hx@p*uCyM6y$R;s^6(QBpMJ_4kogLwnQJb`QlVoQem{@;~V1KGhnhjJq3P-$k;h z3EQ7f&}Ue{KNLvP!6*?^PZ zf5U9A%%WS?+-`=$n+i4X!C$|1fl7?+RqB*%U%l<=(Whp(*r)=*v}BK(nLx zXJ@4_DdpJ&Xe|za|AWq()9j!}YA-kJl+iq2JBz8eu5N_;RNT91wjt{YOWu~F@w zySgmvNn%76g1^naDt9@`%0JTn+`zfQz7aS#LS{=SvHRK;%ElZE>u~Y8pd3Q2Q`iu` z>CD?*()NEPLXEI>Y?>iLdq~j8J2W7fT6ERg?ARQmgAXjZ0 zoIGEMw}iHM6Uqa>0m#F``Mk(6tF+{NF$>krxD9_LZ&ha5O#mx=_oQE!q(FF5(#Xqa z&Frgg{=8SdqvU~Zt==9d1?<)TATLZ^x@clfXx)*XtH`y=} zfmm^?To7Mq*JPI*w8;W~)}f@Fh;t7ypr?L?XxTJ9^FHPuv1giEW14|6O>9x#=GZ!| zBj$hODr*7}tn#$T{3GhbM{`p@TMA;Mx3(n%L_VHqMnov-l;ACOgX_C%=}Rb)Q;5ap>7USSg|WdqNB9> zZQT|VTdyAi5^WAyiS`Gw21+On*m*IFqd7Y{pRi?0ee#v3jM1C%knL-SJ7YGxr5tZU zmUVrrspS|W&9w{gSXJ-cHOhV*-K8Jm>5T9yk<(Mr{?Yx|Hbmy*wQ(COb)bL3uYfYw zFMH}bgi(Dh;H*}k9iX($RO^~XM{^av7u1Pl!d!MjHB>RNeO1NT{|e zd~JbKB7kt>2(P@VKVM+oWtZ0~3N#H4(kh?ZTrCwp$oYq=iloRrYWx)y-{C;5hX|(t zdr^jJl}A#}PMGG3Mir;+@m7Bn6gva4JNM9ttIe%2Gt@m4({&NcD2rQ9$L^GdO~@-H zIfflB1hOgWUc7xZ7>>cZzxN;so5gKcg}+xC`rjfAbeGsUdYGzm@!a8zf;WNAsIH%* z`e{`>&D2xHss9I1O9KQH000080EDvyU4%+`66(AF004dg02=@R0C2ZYga{731cb8$ zU58;o3AbTE3Wq)fgtG))x9ook$jlCevjklV!zU*(m^9B$ALD^|{pQERZB@!4627|%O zU@%yJv>xn~i@PejylR4IXAnH!eD-|weDnEd!SPiZ9NaZmWf2^e7tL)_rNOu5vX~`J zRu(bTJWA8x_1@0z+oRpM`OpM^Nihp{_TL^K?)~-M@&4fvK7?|QviYK{nqXSa(rNwZ zqAKU`JP7T6xKEIo25J=h(tj59Gr50gS z$(X+5Ma(Dz|bUoD#~9|l!&JIRX0vKa`CdmpcIi*sIq^j}PyTjK+&tdR?zuDqj7?z%PFKS?U zV0%Ecc)u(gMxCGx9z8;COe!=F<5uKdgp2nlIr-Re_NjP}) z{qEt>-u~Oky;YunJZ&rO?7unKKHQz`A9M?UIsW?Bui*djFVBZBe*5+0U!R5=N(5y-B$f1B*QdiVCbPO+2I8j11CZ_(h%)Ag-?KMyy){^Vaj{pG(N|Kq>M zzkd1E>B;FCk_o^(dNfNfXrR)`JgKf>K>5#vXTX3b?AqEI{XM{fK@xD8fXk3Htk>IjQ=Fs#oC>6SO9BeDhrJ-~NwF6>%fCGZ0 z49-&)HXz(90M6HOjm&>5x zElWlKDVKQz)#veLRW28GG&tEjbBYKYVnz9WGLO?@77e!AvS|+BkxIId(^QfHe;wTc z2|tk7io#_9qX`Qz5EP&V(mDG-a^B{6)<>DIKcRXr{P+-)scuEG_r!^ zt-)G_@sPKf8Hav%PeZr4g{cA@3~JEHsmzsvbH3tRB60XM;$Z6$tbWf1G2xi)ess zUtebnXsQNzoR_48kSKoCLZRh~8yvQPI?@;CyI1_0Y%MFG&E#Pe`sC=)>Vd4e{*DZP zT)Aa6@d-3Bc$o0AMh9pL>%~=CXZ1Zyct5FE$Y5BneO+~yvyT&JIBm6hcK@7RJzxFu zs$8sG==#axpJP=1<0&BogtEVcok{{78AgP_#MGdr-cm)HBo z(wQ8({K|pEK?DVO_3-r%U3b_%W<2_T#-gt$oq-5}M&bQqAtbj6=hFM5Xl;+p!%4lr zLf8Ahpma|^x`ZyPKSpPH<+W-oyQgYRR>16ikI7S6hHYK5f=@O91C7D<6&pxf)vQsu(?6gjHgF4n+F%r} zhX%h1naJCtc;-Gkq02L#MF-8AK*fY&gEipxy8cnDUurEoH%%!R7`_Q1OK|L=yP)Bh>W@`Dmue|UO!Y+cse{!a&7K0lJh(RbD~<#(wH_B&KD)_KYadd$X{Q)+prDwp|;iRZvbmFK!9Cs;er5RPw9q9(4V zSFoB;kDItio4lM39yC;cdo7M?s5)f6M^5({3li;5b!%AqvYh?#xYPpUPOpMnl7NH~isj<6UIjK45g)8S`}B*C zqShZF$?uu+4lcicpO?XmsF89-^AYUaQh@lhzw!Gdlh_Ta*wAj=mhL{+mFpcd+q4Zw zYhCE+p5K@0Q{V@-0&k$`QRH=J|7|`s{H###Tk3w4ny)_V!8RGBxP~P;m{)4uqfKtv zdmVxrl9#toPQ5Lb^K&s)ZD>h+9#2u@)g6RM%Qo*_Q5JW9b5cS6HI{?vuckbfS;CcCN95tAg|sQ=v|^bkOa?<-xTPd!ac1# zYz1h5s6dxD=i}`UvX=1;w9TH+ny2hTmkxG<<*+q|0izf-RD) z4RfQ32RIpHuuJRVO3#V0<*8T3S2-^7-A*gW+lTF+){}~!=RFB&U%rqSvbO-xx3^vEm z9Nlr3KvxSM0m7GGR|_P6pHV{UfCvg1Ag)@Zv(d(=PJwk~$!M)|;v)Mnq;kk4MEaIYiz= zSd0SgoCZ{Ng`X|2R0Bj;-GGASGn572337_8I4yfPol%gzOB|D_CKZc9-hjW$o0Owi zil(|F#iR*tVbMijhF8v-V-hdQMP%VOB;X&j463fUJ!`4h-zQa{IyNTM`gt_24EDOO zXD3##&PaGmJ5LdR8vLs;HN}w$LPSPZfPPTiL^85K;j5%Z|9n*6(LO_(03714(3;{m zS=B5Pm>)L*0x);jWZMm`ZI7T`C*AAQ&6j=kpy62N#wSE|105u}LT6}{w4^RWGGt=< zA)PK83^r2zT&Gy8$>iYg-|xQte)4+z-P@g4yN8nrEH1@=G$rk9!HQmfhcTCVSzy#( zk)|_rha|OtnO-FYT5lLQ0ubwFc^>4+vY29|$vjPPXCt8y(YVDz`@Tw)_yWm<0E^20 zQShU@;c=nP=1GGdE^>9wlIk`qLXxmi=;=$?*1z`yQyo|m1|+M>LKDdy8l%jKx5cxz zVmsfDK5-KUeV=q`r!2GFHY3*7RsGq$G`H$wM3`^vGwf zizY03RZX+s>)R6KPOg#fsJN>r8`4_NF6aXVTMoZ~x|3c9rPt^7WkE{1`{ zf3U)|N-4qjAyq)dCv<2vUBvobXOz1-o)o`W=(3OaF|l=fav5uc@bMe~MFdbsq$h`rPhqr%JT97zny2!?93tl!r4PmNXR-Y2SnUbP(m zi8?Ew5>1N#H2?*K4d0t2U#0?#`5o#Ff4WPJ#qLH`$JO~jj(-rhuk-sDt#lMQX@1dZ z3}*sjV^0R348ElhT;l?~%q2;&4ksG5TEz1#*4=n|;uNQxM=(8mr0v z5{_4^-@||>GvGVspy=8*YGr7O;3npJt{g-ifQE;yR&6MHg(a6{)4(3X{y5*qfAyBI zn^}8kwc62V_~k{OTzV84#m6U)>oWpeo6aPos%gq=f8B=xyi|2;to$hH;sEXe=P0UQzz2IZ%|V*+@wyr6whNGKcTsu%PncMP zt#(Jp?zvu7mpPMIxEmRR)*JjWNR`x>DIti;-2Q!)*+UXl>=A>R)mKA zO!yBSiqaliiR=i~n~*o5e<*a2d<}GL?6#~(qehnkdVFqDcIDpHFx`#gX6)yy1F}}I z0(y_o2n3sv)$*oWfcd(Tk@6!Y9|a4 zKr~4?CSM11Wf9cVDgzBk1)zv;q+TunCEgu-SAf8;n>%#iA#Su)f6nWn5DT{l*ng6j z(;b=gPu!`7kx zgetb88BZ4POSIMfa(PzWO{!%v{x$*C#1+u;5-ooIX1@k2e|1@AXPn?f^O0nm>I?%0-q!rh>*eALPE{B z#lkVCH`S>w(>90A`L-2l~KnWLP{YnvURVP$9P((h0;KP^ydyrSxEO zfrEwh&r6s_RM)PG!JOC?Tqi&W!q)D}C3>|rEb6anKV}IS98iD~2z}&h7J??0h>=9{ zLX!q!Z_7p&BppbqOfQhwcOZqQczi$IVzC=2mQibie-d6iGh)0Q>i z$7K7$V$ngrLu*rLIx+qAr7wC5UD8MaPrTHH9=WW}v*KE#+Y>KC4WPM~aZJ!0E1}sZ z^}3$5+-O6%L$yH!Hn}IOX%s&S2UJw5^m3Uee-#a6A2JwNTYF;vI_oyVe;R~#P$EbY zn9m}KoxMvj5|IlD{oe62guS^o=yEngi|_S_jBzc<8$ApRs5P-xIteX&7Xc5x3o ze}HZjS@dvSw)pAT#~m~~PSOW66Z`1SGt9PON=Y2*A^BQe-Dm|%kpA&;qv=e3M+ed! z)p~0;%z$)e8Xd-&#uyF4i!PiCaU40$Vn4AQZHg&H`=4Gqwfx{xtV`pzR!o)BNY^!5 z1icZx4XUa0tf7dg`g+Lvg5wdWV?H|vf8uXZ*4c*;^_$3i{LkbjVbd0#PMp#X*)fQ^ zk@P8g(;iU(khhdsNOpS=9GKeqYyg&_0|MB#wJO`Q6!rlMbhcm=2AA?KL;WnIKBnwh zqlg3)5lB5e`SG}31DbtQ>?hV76ZuHLxM0-+pZ-c@7zF=KWAVGNAz4@=PD6U?e-C#d zXCAzcbSWxUTj3lN!8A|i=QCJz0p?bKMzrwGX8H2v%VDrtehwcZg61z{CVMRq3Houh zJbMa?<*QPtSVozA28I8}J0 z9VjkLHMzb8>&e-#l%9DwNBobx8{Os~gq8Q;Zn|d_d;{{wT~-W&84)F(R)$7u!`K73 zNB{TXJH7{0yz^>z=eyl+Ca<=SUUi__L^=sg&?I6*^l^LIwICT7sHtpl)ld)GkSyR~ z@Z^czE~T+yYlK32?o(bIf6iCYj}#8+E&!}r&3fHX_ls}KQq-c*W5JOX*)xx1X3x^| z76moE{p;K9QoQsz# z&8x~oVixrxr1!`eb1PCr(G$;lW{vBOH+^Y4R6^Y6pj}Gb@i1T?;f3<7)8Tz?Mc0+G zekYx*u?9f-5NfE_No=n)<5 zOmwWLb(G7sN=$%u-Z`vH^F@Q9;NUHzzOCPMrclnD#fUv zl=CnojVgsv>1k4_4NWjFvr2f(hCqcedI9%Fqt0LrRRiq$f9=qe-0)bz<3`9iJ_@LV z-#li9P?-v@RO|%QIKW_XO_8wYK;O(g$$~}^Lg>u^hyyq$3(te0emUbTc|4dGNtQF) z6(btam29oTZR;`zE|K5}4%eBLz<;xeA;Hki+Lj>MdJF46r#3E(SP@Dy7CaI2rqR`i(|1I|COXQi9aR0^5E`?O@yu{$(8h;>7c4#3?Yu6^ktee~9l4bcSa0R%lI?E-LB&pg*Cw z0X_WEAZZ}sm|LXhe8tghiaf)h4}+o6VGEn|eH-V4p%Pu0Zx~2aVrY(VXZy^nZFKcJ zT`)4ncFZX7FVxzt;Z<+tw|=iiSiN@!+WzQTGW4pF5T-k>dUc#k(72l5S#BMv#7}_p zci$c#fBt=P=;X%cNfL%(^!34)*r~6glYg9^oShy$=}o?IT0a@X|5#%R>lrF`Uqz?4 zPY2P-)@gi(-=3_f8=am=ZR*iiC#Scgv!~G*>wO&!;T?RFp8yDdpAEk5fd`o5@)Uo+ zd-&J=quqgZkjsqmWsmBVeIz{4xM!iL64$U&f8t?vtDF*uZqhcz8J3FzH@Z(k64fRU z^pJ#S-RBS6R91O}Y>?d?I-buO#?0X6v!_osx6j%`@)(|Gs9++{W+#-D&RA?kV}%!r z1&o)|GUrpSB>ku5vHN{Lzm>lC(RgKt zEYzB4Qbmb+AS!(|C`uXl;YE?tzyDN<2dg%QPV?iX8v(gzx3u#aiowHvAKV-$Myan?S zioM?7*?zr!aPZCc@irm^@KnxzK6-ym=RN0iw+1{Y7%n$o*ULG_Ueeiw7CYV<%h)t( zDA1HF=$0Znzb74%QivVwAMO3fwM&hr4@;z@U`l9VFlQCk!Br`WTHDe{f>!bH>W2k@ z#-dU9uW$gb?!l2jnRwEJWBmY8`RB53oZ&VHqVU6(BgC_mbLZfW(K+a06b64r+qxXO z3&L7&9rE*^eqs*!>8DNrY2VohEvH(%R26fJv}kyPG&G4F+Gm43OJ-|MBXMt%s*b|{ zqBvZzR&k#<^sP{sp9i@opdDCF!w&|S4EcSn=plT>ux^O~f{;A0A%$6_30;v8vXu`y zg^ELlvbRZr6Gkd4qmNP~Qnr6cihGh#0PEX~q_us$a@e=+*0QF}V$(OJ?3a#rO4Jap zVv9#{Gu|c@CO3(~Z#3UXSm60mhtnxn+!nha&GW}L$vZ(AxO*#itvmr!d~$$)l-WMz?n zYr>LKfF!)=7?LT;FH2yFt2q|II8-r&*JDfcW7fx?egbvnr=OINnpJpE0N6^>RA`A4 zjz*(U2^@g44Ry;Yp8$WD(!TyrKXLRr0`t~4tHXnsunxI6X9!fW5sD({J#aU@3B$;{ zE06qP(3n$Jw%%NHg4p`;OlRW&A`AoSBbTKU@@B3MPSc^^s~x4_DHMGgsLrU<aK42F0j@cIL{(FE-2P&l$LW zbYHTFtd>0OlNx_-+Cz6pt@E;XQ>|DTzEMbIAf5P(IV`mpZf#UI*wXN}^k+A)nzUUl z_-Xe*#J!t!j@r6CJUHC{pWU6~$=mHWyOZs=-%KRqobAvEqmyLx^XBMJle4GMNjy5k zpOZ8F@X6rGVAS2I^M6`kG2M^GzJ|TQql3R6zuJH6$Dm#cM~l1YbaonleLDNPy)2A4 zbfV5RMd}OXnm}g_`CxrU#V#kDP=})V05IadO$AX=20A%`_|wEZ=>USS zs8VUtw`ucTM+cWR`9`(ZP7U>F(27ht%9rbG3mboWvNh)@kVp)|?82fmsje9Bp_>f8 z3ON3wEi|CCk-C{VkF(0>9jEm)S)`pBRd{+H4gQyHlLCAWsU; zXDLIc`qvs6wn9(z?wLH&n};ClUS|d)Fx1~)3`}I)sSD~mWzK}PcFHIEXbhB&4H|(Y z^O%2;x|(O#X|Q&D3qL?B*eR=Y?GYiaa!k5j%&K)?o}AtW`U1q&(;NmW5>F%`{?*Ap zPU`_y9Q>Da!5oCk|7XiTRo0l)By~wXn#d*Bk109)){(K9UUU*!+fSTHSJW8a}R1LkcHk(0(_hQlFFy zQh9(>)}SH20@&f1<@?24iFhl?GOcL~Gr0k~jdD6U%VB#eO_!FZ?Qf8cC$ep*;Yfd> z_P0?8)t)CIr*#n+rt&6_)P`~Rq`NWp>$q)6ug_HvM4bC%laIVNXHlqRWhOVl3f9dg zBRBeNS)kQRq^LS~#Gz)T-ivCdk?oCE(&p3>l=o?#RPGHI3S=jqLbs*U(D2jGL zc|FF}zY+V&C&5k1cVr?+#a&#V(QSVRVk4n+EqLgguJlA{f|tD#OHYKSQM=41STI^v zxy#8ClV}<4>*^xiU``i6EAIt{hb-}C<^rTtm9oW1nM94MhJ2RW8B`{<7oYvh#z+vs;re$?HCW^5~SrYoP0`jg8ye zTOh-1G=m9`ionL=Zjo(>GFESdw#7vn8ciG{L4rVhTyMaCQYa=?P>F;vJKb`z;B}@| zkhr2;y;E>!!PdAPXJXr$*tTukwvAt$Ol;e>ZQHhOPx7Dhp6}*-Rr{iARqg7&*t=JC z^;*xfC4=`&D9wwzrrcys{Ho;=%uD{)zJ9f$vlU0j>#41$l${nC_}ih`i3RM`Fu{*UkXoel~=sv*_wQ^-oIG zG)qL2D6Z4b=Pzc2ILdu3IQ3?S-!t7`qK{P^0e&M4R#sDRb1iGM@k49UDLIUO;pl)u zfo{ifG8@&NX5@yu|XPVpIb0->qFdB~Fx-l5*+LcKzL{G|1xEr#Pt2!^N zHPkY*Ortku#CYyPUwJlhC>A+SQ7~l#{$IEtg{xCpsm1G;>1YU@= z{*#lxLy#{UgT&JI8L)ZGfTzYuTgI3qCZxmQ$R+FC+ZqTUsNq0Ekpl7xzJh*C?P0^q zN+_r`O!(^})Lkvm-$u38M--5}0$fp+=ip2z>o}dQG)kNw<$QpYlcj()VHR28=CTmT zrCn^ZdP`ZQ1nNL5kgr;L`s~fhx)F-+v>~!OxfnT8QR%5xB!IB_;&4G^-ulyPq((;f ztt`AHiH`*6p@pNFGb&R^Bx3-rG{w>&)9EG^$6NL{Tj)vM@gczgL<4=rfCQ@Wu;s zWG6J=N;<7VHXrc>K^aw*DVHREa~sCo8y96f3GxLHzd8IV0V%(18!cgKM{27(En$Ws zFU9)(i`^xAXP$2N*1EV9&s$;`-USuQc%WQagKoBCD}tc5q`>*t#e^aPicitb9P6`o zX`js>K3yyoY(=*!kD5}2H`ka~s#L5=U2@-o>nNlgBK?1aXJ1&}#ZqU2)xngNBO63y z2JkjO7G6U?ri-qNc-7QcDAmqf3*E+C{8jxdDknb(H}DQXgD}Ev2}MJf3k~EC`OnxU^=ICCY~CL^Xm-mTpP&kH{Cz^371J63PYk< zTSj+IMGxDPLQ?GnUQ)`ZIq#&GJTTyS1}TU;iV7KNOx)2`%B2`mH|(8BD&F9fYlQBz zv+5*&?qmx4b3qjrA(9(~)uErUfs;1WQ#nC-*`!=EN+(NppNjaeB3((A0q8jNE-)dZg%sEOS`C%=FjAX6DYtImwcH&@!u za1SVN$3OeOM?{>P8<6+N!#AW>uk|Bj@nwE^sXKN zAqbKoph*@`x@2!CP7Ng=Imgq;jHo`>2@;gVEVXYkY>DK>Zw@1cjqs!M4f~Td$lt2Y zIpSbTaMQK%>F3_?d3b-L9aX_IPz7QLTYNZ~d!9n@<@NpO{G9y_BJy25k@cwWUjeF; z8mNptzAK5N9>d;tP;#wsGnR-NriKo%pJsn*>J|%jZ{uAp=YRxitGca`K1gS4*R!ue zHp?%CiSQ-QG?mwV7U^Y<{b<9vW^w9jzLan5WEkv2wb+=_&GO%)IID5w8+Fbt7Q-7l z8psPzV?UunREq6nKw{i z4+9OlhWZ>J;H~*c3kCym3g`-9jX|5gcI#L`O&C~+-5K5^EjW${dSh1M+8myp&et?t;xh`AD%aA-9`pxPu$)%fiQud(K}>eSrM)@lg&i6im|#CR5tuq+TSrAz zYwe2|26Ox?+9WX&{7VYS=a}szS3?&y-wMQ`NbCPR^hau*bG&*>LQCNn$3c?nXT|Tc znIjgFKYhwuenu%(4Qm#919J+mKe=} zsbIFRbaKI*@Io}Eac6vaC3RuFe;w|tpQRqq!76HmKkiT%S`#--H( z)OQQJ5s9R)?i{egrX^a10?G+}bS+c#DYk1)6B?n*|L}`CKbf?5MkupdFurFTBVKfo zd)n0_&@{4aP806U%C2aD5-!F?V`OZU8NQ*#>^56{1D3NdW>H;t?j_ll?cG8c+E$7* zy$|q#1Ol(xa@RXH@Y#rllZ$h#UU`!*jD<&{+k8d;djzt`spqgCHrL!s#5CQ=Qc42h)`I4=C%+Cp*Dpd*11 zLKQP_Fh`99GnN3Fe&Rt0+n7=IuIKbZR zgr}P;&2nm6HDdM*jXR>EFNfo;-(8Dbxa0$syAJF%&)Q)D#mLtFH01AAAe>!u(6Y=7 z0p7xf3s_7v9WoqLvUZ)w{EwPGXr&iE(#P*clsw^g zh8LD2y10F1F?mOQdziyhct)VJt~t+YpwL)ezmhMu1QzJh^aQJJNerFggE3AI)IZE| z454bPB=jckWk%f-fEqhjy+0G4tDU{rsf{U#tf!{t3SFe1LW!Z5m`at@s*@y>kKQ@mpH<@c{B71`%^%2d z%(cXxTYMCInQz1A?=$+})dchGKbtM4B|aMN6|rL4PrFnXEtDk+Z2k|9f)7MKUsjiZ z`^@T5al()DX)@j3GH%YhtC(7+r(1k`{;YbJ@BKR*4|DB(F3ZkenBtU2GV}dLZr|o# zVK>r3*P}@uy2MRzAUOfsd)-x4b=d3L1J|T;1&D8fH#ZOU{-XmHRl_5(?}Og9LfvDN zMeTqYhkH0tcl7kI>gW@Dl-E7PALiIOXsRfa8%BAzYlcw;wu`6_FKP!c6jvU-ixA&0%H&E{t zSzpG7PcCnh>*tA!ph3uQ7b4T+x_1b?4{=I#CHGXd2oAWJ1+j=h3 zAl;Y!V)Og0nw8Fl7{tfN&d#A62OOP);9_qa1$nSHDe`0;@!K=zr|XTD?Lo?`vyADh z&Gs6FDLg5>HJ_o^~IAg7u^FYv5TH<|x$!?{8 zq3G~-&@pE@i7*6SqPWaZebqobw0EvLaNmA^w6JtWt#h!rLGUfCexgJj-te5I`l|J@g7FdG4W-QbXod^j{+bwoOJAO)O-GHy z;Udk)K*ntYOD+M>tY)dg^+EormCQD_4 zCY!9&Iu}`71gU>!vZ1_#9rIk6Ve7pcYYew^z1^gKvnZL%1LZSqc568@hD-Mwboc}9 z3}-RI3CCCXLTRa-<1jAZDB;e7bZOa6_kncfIr^ihmc6>URrII zZDvRm&HLq-y+E?N3q`TcoKK=!8e1EDiS4ylkig)eCZ{-{sm~DLanDkdxJE=IIUp~R3%yQbnsug#$adw zeUA|nXt&{F*3t!744^A0O-VcT&R5scxNJ-(ft zJ;U>d-<|gFf8T%4d|yKG^m=|51l4w&B2IvGd_pF+e(Ef_*XK8(rlRU=8h23J43@bf za5^Yhwo)Bc0;@FzP}+t={a&mT!TY=VY>HZ=+~crzvl0Qg#K($oPzM~cRNpKuH?ECl zZx0$P#C-2|b`NgbtC7$qtNM%J$)XH7+B9#YDg17>V9>KAkd4{7sW!-LX(T72d7Z0E zA$PGXG&9IFl(ADz+f^Gk8c-%0;P-`u*Trr*>E^8EWy`X+2Igu67VTFeE8o@IWJw+rpaGe54kY)z-dea*U>j$IX?kjl5Ua)Lb%XZ2pM(kX zZ`g{yhB`kAf0&E=OiCfQOGic^491QgR*BhJNE)&4gZSz6)%)@_Oe&9H=l6I)uRrX5 zo$p0Acm%#1C4HUoBSaIpo6B5Bj;Cm3=JG&kQSJiFE`S463@!fZI;hhLZ{`Y>{;9U& zfp3G*{*}N0)&mjnLhV@_n-Slsd0@~9F*5Qc#d=|W zdaep0hL$|S%y~lvguR0NYIk%7gN~n|SIP`^4G_;PYBPEWG}6 z!*dAm&J6Xep@f3)QzXYIugq33#KAwO4)%_|*FeVs!jl|1e{(3H+x{DOZ`v;KyR%c> z^v`?LFRBZYk#u{k1P?NIi`CFd=xQE&-ydY+fQk{7I;y9XR&!DF5{@=^p6IuJN#V|o zCEs-B2WMN zMH2f!&XdD-(vu)y$-ay12RgcXW5i+tMHL<96&>4C~d}_hcqV@vkGat^-Mhgu<9Km8sdr=PhgQXpiywmtQf7s7o#DfRow_I ziq*6>{h2phQ*}tEFC@pds!pPl%Mu(CwvvD~z5YnPfQi|4RNWX})ah2XXqlAQc zT;hTE2ViX-z;MaCME&w`UF>YH8Lm0`t$tgqyBvzxp zmFRnCL}s(e%#Fa4PmnLV9S*R#Zm#qX$nXCk(6j#(iO}Rgj3}X6p|2}%^FZ13jl2hg zpX;!lW0`>4qEf} ziU)K7(g+xqUL3Ti`3=VSt=6Ie`Ku}W-9h#Y{zn<-Q!RYV%9^LxAfCJ1It zkj$CK*hb3T@+4a00aVA(bYqZh8tF8ve7Z!+DXJ%`Z-1}AciTwuJQhg^XCMXxMBLBz zbai;>mC$(AXUMp$~C>??Cgj{ zHK&);KQ6wvQaYvC*JQ2$*b_|9mkaAWiL&JP6}CC38_z z3Q?PSK9FjHz-QLU(RMLGd;o2u8D%IqHbMuHmI{zyLi&HV-`#xxsYZXQb0NH|B^a?; z3&HJ&W#M8r$cOpYGoX6mPf}qxN#AV~4r%8o6S+!qt&P=vFm_#$FEl=}rj z3>VmPa8dVqgQYf*=HGdHVJ$HM=`9E^fxkc(HG4&id%yz+2a-l{J)jpXkrMKd)DG4k zGATWkc~*C20msq*uxtwiQR@#P+m9y`XV{NLv(f&VvfIKlxpXdi^$;FI{8~w{=~Lpn z$kVmWSKNxn-0P&ii~E)J-bemeD5F&??uH4Q+5K*-n#(?b0S~%^c&&~a&`$Cp1q}Pr z-V0{C@nG{AL5zx}-%YjlN8%;|!WE6o0vtOXo7~I<8hW?6~Yk_^xN4T`Wilj|9aOSefumca|0JVfL4#nzs@96PmaD0+smJ zS&$@E--x&{&^ix~%NVX<@k+&00t7r)I^7+r#Fj2s;uw4;on-#|P;5S+3%t5md>Rmn zLlJ7hSyvc9a3Ys#OflYZN1d-8 zFPV2pO_&B}!;Y#ioBP9sAPfe)+*U2t^+9?Q{2Q2&W+&CMXA&MWHjI44_;CUKv) z)sI1n-=>qDUvfm6E3`v7QtTYjw_Dzun^|2m>(S4+8d+6#H`W9u*pHj0jGYTBoz=yi z^u+00zRI3@d|PQbJblMW(S}o+;JoCmP82dtjGGS%_zwr>K|5A4bf1u=+T&ABouDoO z5tFcIXavsM=pjYDsaBNY)G~>pjVj|o4bUhWG()|O7dIG$pPXw9S)%S`2r;fJVx$>K zA>ZTdpUr$OY%2y+M*iN6&+zOC%Sha-Zr4!%01qi)mEzi9ww$ z)#^qT!%UYs$J1X@2L;#Q#mNC& zoo)^O36!X|Pq&lHJwq!m(BOH0cX5{3Bc1wT;XwR8MJ5nWld%xn8H(#oTe8S0m$ujx zp9Mhr^kwf4V!(Vq!hebSWSXc2&1yhsH)oMkVeb2>HAjdN<**1XlMju71eI?Ta_P-n zIgtW$(RPg73gISz4=Sq2m7tjAr+R|8!o)Js(6Q;VENlm-@MK2g0P+VYjZNl3?xQ^5*P)RoImOFoSfuwtjD_Chz?=}5Mw_4V(ZRRetfY7+P(7Y{CmSH=7ayQ z015Aw;G36$juGq{k0k~3T%u}pmC!y|ZuxrEz78GzvbB*OTJo-?%1Qo!4Oa6NujO(l zy4bIX0DVqc#tJv&N9fcM^U=Svhw$Wlx7#Gh;C>{j2a!yxL$v5F9eETpgjn3qE5-ONbDF=#N1pKdb| z0z+=Q+*9+XW!Kl-3lSV=o;%)G$d~wp-m3=2$0Gk4FloR8^CdXGG^*SYwgqcMmf~N0 zY9%SsWA(v&gupg!4@UEdPf4pgqRN*kvA&+GvQ+QGZg|>&_GpfvYe(Ev2cyV7+7IGJ z^+oTu@*~RtOhY@MQZe;WwCo6_Aqt#KX;gP}MXRZ%&Y;F5%ext7WxMS4RRp5Cpmkn&k*zf)`9WG07)$p;=&Cs-xw+ojecL=AH~R(Nr*@$j=tCEhyoO~~$-YMka+hkesJCQ|Mk zx$E&%OG4|jwOc&uSjkt$Z_6mT(350&ShtS?kozw8DA*QnC@&wr03m~a=T(yRDr4Bs zzS*si>4lYAMGehz9Rs;#v(TaRHuvfj?o&>-j+7XJmAHISP?x+r{g${P_+t*7<|^%U2s>9L<3!jr0h8?~Q4)O)^V?zJ1=bjcaoa`# zfF3xK8}!69l3UwoM9Jv1+6QS_b3bv1i539-XWnGdG)HBPAc5mg509%lyi}fCp?&Mo zCf&2jM18#C%R}Vs6}e!r^)?xv-_p0apYVpu8 zj_q2G%-`!CH$qq&^)9qrcscGc4t^Y|v<(= zEPLc&g0+eTVo9OQn)Fw|ytzHF1&Sz&mR>|$$4rJ44n#qgUd`|Ne+daDA+51 z8}g}$IJpOjCBFcdwIhnhGjZOz!MIqnFg15$==+>R*@Ok8@dp+LT??NC*6Pw}c z2noyHzVB&Lu5=iR6gRhkz#d?xJ;`TVv+~lO_aLDjOPn3@)u5*Spc#(&l^q74Kn{3n zvegxl!{A0f@__puKtdCh*fcDPd5+)VTk{B>sEC{6QE@*vK#2OA0T$6j+m~O;H8jeh zccT9uzcz4cGb^VtlVnXq!iMq{hjt2qB(;*6DMN#e zRMulCB~2fHE<%w5?5&HKwjw2h6If~@Rcvf zjnpk_`n<4+s6|kgyHAQcQ$;2lMfj>;ng0WCxWg$|amUgJR7Ik-f#%z8h0r86tQ0NW za*>B#XJe^ijHKJb>Z-*+-p+f^lpB1`d{R;M=M{TXW6Qz#-+uB6dbtI2{o7bUuKl$CFH`Cp)LF z)3fQBGGt-tL+Sdm{6_}2^7{RSq?(M&j+JXGQnKTe5&U(bvTjxk z?4bJDLxza1RN?+uKYsE|KqE(9D5A*Y)k8$WI@!eou!e%@E`bId7iGoLGlgv^l`~Kk zabT|`CIp+BE2(XYCv`%XC)Fw;9R6Rx|7T7^0{R6M+z6(doMdP4yXhY&$?QMmBb-+E zWEi%l|I(7`C1G3ZCE@7k{!2?bbcYKX{_oI=7d{K&ze8wQcx~YS8>U*y!Mpr7xGx89 zLi^vKqz^p%um4O`*rIop8twzn0jLYxMxmW&t8X|;P|+bk`(yV@!iaDI>fwc3m>b94 zc}H5v+N`HHy_jzAMw+jw)F9s_J3G*n=2gD7@$(sT-`O3sb+>2NxBai%r#J+l6Di_l zeI4j}N%*vsqccqT$ytMe4>qm@WQy${N%{q5B&a*GGwdv z&fr{YE(DVM`E7Z@FfV0Z9Ol&HZU=F3zr7y`CO8oU>Kjx4OpDGP46r}$<|4syde&ve zm4EIdLzJbP7QK{rnY`{y2DHnqx;rU^TF2wR{%h=VTX6h&q_2byCZFZ5qOUet(P=$R z%kt%qb=FrBbQhQZTf4H5bs5^&bxqQ*(6b12G3e}os)q(jlja#9;JgYr>SHx*8)nxO z3H}(t?!an=cL=~kF@UC+le8z91WcoOt%_5u3G4#>&S^Z0=$8Q zB-~pOYYs;*rMJ_>JhkZW1_=##QUblSpv<@_QSunvx+mZ`4fleYxx}3~^{tNqEMxy7 zPM4wiZQcL{J%3NTN6bE#UkoHw0#h`QdmM+Xr3lM;ZdwV)5`ax&pt~vxtp(#s@0|Y= z*swxbEfxuj1xs!eiinApQt!TQkuH>&j3mX%O>%hw@!C16p+GV^Ph706K2G&pr4lrd zX@W2$x8INCuCqWuSt*NnE=|L3uLAC!LBRPqUe!oK{v)r(ub<_75)+TDR(!j&}hjo##BcG1|wG}#UKC*=RS4O`Cu@T$OY z|M`X13OA7cLCS!XQymGgP*U@J;K2d<`nHCacKZ7NG>4N@v}5#CiWD_8CkIm06SJyw z(lg^KW5MAkzs*h@9ZgFphel7yPD(~bsn<5BD27Lah7J#ttJ+Zgfyz|rn~57mKuIg| z%g4ci{&xlMRuWr&5I{iGh(JL9949~ymJam#Zl-o7_D=NwU4*fX<$pAb>YG+aVu-%E zwPh6a84M&{L^OhiAS0Ll`fkE*sfbbWUPU>M#*)=8tu_dgLVU#fY4no1S{a$Cb#b0` z#Wxoh9a&teQVB;BE!7tnrp8=wB`_PdD#eDzc){Lqp4OLLEUu+eXR6P6EnJz?b{*oM zdDb2*&=U>2fDrG^{s!WHuQa|+FNO~0^wU{=`twrXzfwbo%ecU(5lw>rVQ zE+wic@BdTK0JsRlawk#`rEvnIE`54$d0iu+}a#xxn8H2EFGD zZSZ_;z_RQ97jAu!46bQV`Y8&Or%^AsHw`{FDbhM!T^$gNtc7KXOr9ajf2h;ja(+*! zkB~a209>7@cTyT`R@TU`7d%gbocj6G9dak`IZM5m&RJ;EZ*fqR_;X`xY9SYwz)$hf z5_(Bc8>CzDv_z(AyC{he1)Dx#lqjYD0NgsIV(QarO0Km&C$Pi@tN`JKKBnALh(^d( z(sv;?Gq4kk0j#OFwRUzX$+q5TutI1HlZx2JOCPX) zqiurb`DD78VzwAr%_zLhj!HlVJRqB1EJAV74LFBBeG;$2{($d~Oa?>5?-m$~Hh$G& zE<%B!uQ^Y!bTwMbl&)2ny0)8~sMVtJtn;{pvUj$JjAjM@HJBuI-V;plK=5eCEg~-p zK!i##dBxflET5kT!tVDSwieHLamQ-?PQES}VH0^d?7=uT!%P~asJ9Pfer+QSrXlqqfd{^Poz zH{DpG(}6@aaSF5Qswj7`AnSC^+^i}yz@w>Yjf?x~{2OSp{MQSwQDQNl04s#ZgLBkV zoz%vm=y13e86ROeS~vy)yxY5Pa#nfAs%)=Rjc58GMc?3_vMR$KQD7Eh4q2%kU_SC= z4Myi!^_@1EE#;pgR$(4O8Cndn`V8Pw^uofWCeL{8*OQx}o0oWX4iP`>vW z8A;F&2j|3_vW_izLVX=&dj>8%>wYj{m53NSP#X06<(--?8JTNtGe1tBY&RSH2oD|8 zEg{A01{ls8M4JpOyT5aV;bZ3FX0BkZYpAaNdTvOe&VdRw?a;y@GS+=7XyiUnz2xLG zY+g3HX@S;tGYl`j^gJFv4ZaH#;4ho@oO}#2)zTQl)M}^lx>vd~Nvm{HEz$RW^tZ@) zm>8>996OHy`HP}~U!)fghy5nnZ$trYOusxh9x@Uf!`KHm@Mqt3#(CtukCAP#A?c7q z1`$3NS1fM+qRHGV#*l7OU*(WBf@sk$$r9q~qwtX9>o_du7gCKu)x2>mz$;2u;*f{9 zRoqzp%e^NyVuV_siw5@PV1H3iJc;P%;@bcf>{yVyb(Pku7}n`c1mDMupV2gFRo-cp z{f7DGK(Y$nW+7T{UnFw8%F)u^EecEg3bB%9XBal20X6c7G52I;bL6dWtV=klYk0|N zW3GnMdi9etv)ap0BDbCfz%E7C;85E!$QHIejZhC)%5wI)g2M%R{Umy z&Xr{)fyTZ&ygsI6P1BT8A$})##U9Pgq+8~|rn#peSrsJ`-b}!VSs={0!U@lUT<^#R z7J=ztUtNxW4m%x2Dp{4#kF&2+kV?tTDSPcH`G>@nj7PCPeLM zDy(XGi_!J~t$vV17a|cgZgIb!R&iMWVuF7nTQK&wt?wsXn|Dx11apC6QS&?LA^nLE z3QAMJy8io>RyDNnEA85(1;4nM0$W9}f+RHFuwFN$PWse*V=PSA2~U3LfvaNiEWSi& zkPA1Sy&zKQ<|!8x08)^$Ca;_h(jRSg2P2wAN>JB(@b?K*dS|j3gcCN_5B{8@J#GO@ z7}fCHla-%#K5~8kXaxAcyo&*EJ+i6mj&d~O^y-6yH&zVBU1`cr-@uCKd`#=Xkafk3 zT)*v{mbzD4=4SR5-t#EXd6&hqi$d!jx;?~(_SdIqKdcTaO;I}qF{JX&6SH3h;}0R{ zH_ZPgF!6lZLvC;&pgI&FAgt7QA9z%NvAv0@@xPpPtg&gg#fIp+sxEUJCQ;Y5RwSTE zu?Avy2xJ!kCSml{&!DFB=b>Ra0Yq|m9sRo1oui1(yF43RZocW$Ksqa(8N&%)@{ep~ zQ;3pk7mH%`GQ?F2+5>uUYSyGJM6EV;l8NjQt6A|vmwKDe{l%A%buqJsn-T}0$h5dh z(<%qpdM$z-i;yeEys!9Ycisb4)O0W^;5O@X2$Hcdl_eGk;E3DXJoOM-nDPiVd-7Q8 z+twJaLdR}9$gZOW>NZV8KGc;Kh&E?ss#=cC0NtEMw%pll^`=?OCLJ!HjzY}n^goe8DKJBrzwH)j-w5@R;t0LuB} zPxK?lh|n0?Bb47g8?l-X|7Tw|T6vUalF=u{=VP?q#1HN&i3ats zzd+HF|1bfsR)b>9a99X1sXw8lyxDLv(5k!pv+L$M%~}i3DyEb+_PYu3gWV-|l(8?* zbOU;+NdrzQX$^8yg*vLpMAkSjQH`Y!oHh26bDUpe;C>>M1iEp;9rqOZuAagwpsCHH zlv@vVy^oFqEz7p9PpBnZG!m*?CC_9phYvoIQLBERd9)?Z>Omh+DL0r*u2L&OD2xk{ zP|(JeJ)5h`v3m0^?Q@>1-;<>uHLK4{8<*QwRflV=G&>%DQew>zd=g_@4HO9Vu9_wkvI3VjB!6yJYibQas(rFgI(fy=hGsJ?RcPIUTIl#|HBVO z_QWpxdODQQOy3r1SN>y!o{kL$oK=z_7w{PhpaLuOWnMklBJI#*USA4iL2o!NI#^O?LvugUr%ZOox$@_Au9 zdK)rGgqR-r7IFDWAE~C){ZU%8GBe=9lA=!lA!vLD+IX97-@V1Bu;M#(cQ<)gL2wJg z;R?q8QE3hYN`<@KChW2;ayhpSv=###0>9>G+#(LpAzWXjAj7TO_V<yRp<*u()dj zf5$^Di1a^0ez=lC%Q1(09)j9_MnrHlCO>IyOm^=vk?0~K2dT~6WO#8|(Zj-$6t{Ch zZpup07a=y)=a;x6ElE7ltaSBr{QAkg;gZDw|G(qH>rrqmJqQqxDh`3RJXe; zcKO%M>xD7i3<%;7G#%g2zY>_4&~MEeG=Q6KJks-SwhXcG?v}ZCAlb7Gv96!Hb1i?w z#6(2bWLE@>JHmB{G2ul#yj81X6WtFO4LbrioVNC!9^K_T%&$nbG9|`|@HnxPN>l1x z!Gu0a3t|^S1f@<10bU`ig_t>wTI~30O|;7;$-WauZ%BA~&75kZ+gLF{>ge`=eTfUe z+DwNof6U?pwbB+m<^C>c?`=}sdq(isudIk7#>MmxE9uCoNKFPt#i0b2uriH^9mR?A zmS*LO@pEb28)b^ZNYIPUNjHcs%HwrBkBEYKwJUvr?-{BLlBc|8yf zIaHZ-lQ@ik4g zok;F9M}kT_L(-6M!r~Eo*;t%?`ayA5{gJ;gLG)$ZnRjt>O+MW6x)js%O48+&5VU^HoPyLkxH`jvjS+Iy69i z2aD*1FQBkoNu)e2cOUyw*sV1b2IQ z`~LA!^2NIwsNZ{fcn|0p`f+Eg=|0)m*kHcr!UQ)uGk^FkcS4-3No7HOD*kzJ9LsOp zJVXk=AQK~!WloGAX+u0L^V?R(1%2>a1aq{pc?SsSIf3RXI*4={2oC5mEDFfzu+jad zIk?sRz}Hd>bh#ox1|w?ZM}C{r4|C+zf2slM?uY0H5b$%m_yY(_efz=k{mJ;|@AkMH z=y@D8SW2A#6${u0f$fM=Pm(u9hLv7~Wiqo#BZGNyc%a{pzDwEjRnq%G$x4&)GNhOD zYR1G>8TsvMJUhMC+#HV!gB{fcyFh|7sENVZOS#dD3H*^}Hty;!+c(Q0H&j|h)>I}U z7RAMu)`Vi>lmTGN;DG@II;s{Ju)PQM~nbLk3wAejJ`X_M& zEfUFB!GLGh`?^$VEG)lB7V*Hri=~>DB+Z{x-sG&%e=5NP5oCa2Ruf1x3WISHjT#qw z?-d0Liqr?hD^da??uS1tRv9ZmajltL9gAO1EZm?HkpLpW z?`U^bjLY1UMLY?+yt$7jvAj`egD3~0F@Mw>LI|F8U_E08XgHnwr6va;*Lk@K_**a= z`HYOi!lniHF=wj$5szp=RCKuzvp@Ap?&JYOvkx%uD`Sz3G8)GX0)di=_yRFwOgm9- zl2{}ZUO7(YvIaZD!9RjUu^a&p}~Zz+%NALMo(lRSg}1WJ`tZVh-d%g?f(Uxnqhxm?3ZkAA8^& zjB@izpQ!WlNkN>B1t+~Vh@YHR;W`KuzJtto^A}%a@Rw9C;3$j|==|)l$rId+nVe;~ zQa7(^kY0cz$zhnG&;vS4Z;+7|cmXk(0~NrNOUF1%qQf#HY(fl1q0pDgw8X?_m4DN; z2V@(L{EHYplb*G*ZXwbrhzc0g#Sx;-gOWi32EqIBxP=*91{EHvz%VTc-w{D65llXz zWhrgg5*v1alffZ;x<1gQAzkC;q1|Bm$zWY-E-3fa0MXMPGgf!r5 zLBML?YK1R2_*}xXx;DIVJbIohEQtmHCx^len+C1bHoux^@;@ z&F2k949+4%MtM<3m+hHXuRO#q)t}u%ulX+YAbJ>$OZw15_JRyRgw+OdjM2+Up zl;odJQuV4Q(VGRoS`QmE$B~jjAO|3d6HP;b77s-snWd|_fx%;CdCU=DG3p_2$uzj3 zw+eI4&59$IqOTa3zISVoxw*Ic3ZCTlRL`$PTu4fT}Ho% z3UQ99%Hxv8bL0+qa2ZKJkpU5@j*5JffiHCXaIp=a`Nvx#1pQ~gN&T?V#YJ;)FS`mW zu7gf*u+dU-#e?5xLF-5ntdNg9#&6)Z2?V#`5E3Bx#@*dLuyJ>H*!bi7&b{Z{bML$Fuea7-HPyeW?wXnI zJw2f{U;v6d3A7znsFg8M$fFO5-GEmB^koZRgT3Sy+zF`c7~HaBr4}2QbA7GT z_bU2Yti-2SEW@+PI{G_dP-B4&L;Qgj-r-^%PVkSONM-%+py2eX;?n_hiD03hL@OK? zIE;at%rLku5bCB~a^ZRE4Vvp&-EK-4*jZzZ1R`qrsDS7FmzjSXw%d^1wPkoX49GvD zz7hfgtuvgz#PX0#%(`SS&V7NgK}3jSp7yV2`J@Yql}Hc)cTz?TlS@gz$$Pm|g+s|W?EUD9mHdi! zf*cdE-fk2``L{{oe{%e1`N3(EMJvMO?1isuE~zTlU>S^*3KH8l&s20o1Wqr43zEDT z)<#1!Vnq~kX7WD+_cmRKy&^`PYJ^sR1LF0xV7;?DqU`*80IjVHqN2(aFYkQ zv29iT*jBq(JH$;UU!(YWKU_81e4IAxFVzL!M0?s!aS`LJO`83--D@XssBl@b2WT=7 z{i$o{k7#s)B+ne^uXDJl5-}1n$ZUTnS^sHM&s^kagr}=ZGX6>SQE=%EJZN79amYh& z<0=OQNsZZ(NmXF&+n?+Yi0{(DGOwY$^0`2h_G7OE$@eg5ZBm{#SEYzr7Ofik^}W+; z`|}YL0~!;*CS3_Emc3RdW?B`Kd||6YhF5FdW1o392bM<0=fFTcg;mj_R*J zxBE6+(PqKC51wG^);H{(fL-q<%oiZ;Fkv$K))k5|j-0dRCyC}5q|CqI2T(QL7j$fe zK~ggpCQu%lZt(3*Qb$d8f?;T%vwFBx@ye~&$TNiPS#CkS_lLz%b+uDTfy5Cn8L%;& zo}ALvH;==MbcxF34Vx<~;D5hqHppS&E699I!yQ|~jX0gX8yX5%C^KB4o_z@%Up}+rS-{t1L^#f1c>$ZuAaub^u1Tg+^7(F<F7Dxlct!`XXq4D+x@p!8PfS{Xfc>O7y=vtsux6q# z`R27vT!SW95IUra2Lu=Q$Img1?X6@c*ve@XK(Eldbn!Y`wBt~>L_rw`3o6OX-UG8WRQfNH6W$&8VutmTf z;b&K3Fl#&$kdII> zrJ^6C!&mq%cq5`EA)yL86d7i5)z_oZ)5YcCfZ5Lg#Ph1;CqSw#yv%-17w9~_Ja6vO zs3kl||0c%6wq9HY#@Yqz++e2^F1DQSBIg(LslZ^oJu-V^>=@jD{rKs*{o>QapwTP& zY--)0X=?7yup+gIdMl|!l{ev9Whv)!cv<>PRbget8~blbgQdlC4hU?-65Hd6%zWkE zc72~3S8ZVDLIEnQe7<8(OsCPGeaI^?{eVk2Y%aR&qa?dG;2^2VS7k5)no!a72}-6w z^p$cPtg-ReV_}+&*|$?iqq7B-uc0g6z6z)3ESV}*pZPGh0q!-U#TNfvFX2eWHFxJeRhuTf5pORRDPcmwS4uU4`@vkQ{; zljiQ<56>As-}SB3HI<(Hq)1XzYSu``f#zWb1pmODc}<_(s5t?9(tmYNkrKybo-rtv z^!UI9%ji^kDA@43u~_qJvPuafuwx|cHCc=dy!j;H&~5yCaDdMc;%j@3C8VZ79KuU6 z4N_t)JM>2^Dx*`W2NG4##-!Laoxy{+1Uh7`Wuog$4sZ)OU=l!jMdIv_DNcccqU!#E zUR*=2R$!VbQaMXVL(g+_?5AtAo=#f(h_aBG@JrfsHlcy#Z1%!e`eHrwvU_N|ZU^To z%qoE2B$l?j#{LIJhj`KX&58E$kJv>TJiZq6ZdzJAC+0y}x@`z3fp#%0Xl685O}b^1 z=PLq5Z-?NdCvCfB_gO<5ZwhFZaHCJ?SsVp5BA;}{pD##iDi#i4B9Zd3 zn^&XR!Ut(UP+0d9@@1V#4yiY^P&Y(++_V6uAF@9CY8LR^!j$+cuQ#1lkDgVTM>MdP z|43zoAJi8y=&O;j<6eq9e#oi+_+____KqLrA|Hzop42ft%tywMhWXr~Ynp3ow+!o4 zPON^CZ=!{xSi`QDw&((EO6FF`9v+pOE$A&BfN<-#${MvHM%v)Nf4yCf8e1!HI==;A z7+W}i{lg6c(C;i4H3~Cq=JGVP@=<^Cld*FJ|JInQDp5+|Z`L8>)f6aBSDwymi9cu! zRHH!Bx|39@^YjVJxZ`zWjyVBa=71>?|Gc>T;_@Jl;EGlS=xXRyv3LJya0sq=tU977 zUcq^ld&9lD^g5Hd)Q-e6 zg&Mr$K+aP0f!FGtuD%&js~_{NUs;clGNV1cJW}(oBL>**chy_!fas9s&4fT ze@D%9hbS4@VDOHx5dXoZKz!2ikcEMlmc^<(-?|AI4;yJO6rn3PJ|{0Yc8C{CF=e-c zYT=gRYtY-UUAvqhQ0h$8*<0nVQP7)7x0#oa9vP!f7cA9uu`Yi35nIe5SI-l8(I@hXrWUzb03uj@%C?z3mJteH4Y@LVYChS#z#=b&A5 z`2wXGok>Eg`<;eA2ZeIc&rx()J-#%}RR%gv&~p5k2q>&SSO(ouc8|fOWD}NCGX)^A^D#5QKbT~3 zw!H7}Ainvciw}pca8oPQs2{J1l8H{4Xno5)$yh$61+R@ZCzNJhQ>z0ZmBqHU6b`YV_d5nEs^VgrXa>tz_^>>4*Ty99) z1X5|%Qb`<;!FJz_xa+smd7Xbp(p z+~{Ue$ZyDL0NBq$V_bG+94#~6og!4FDhB!0KT@bTjg+O1P}zqFqm@T9+;EXhn>9b( zR^BafsVp_FcZ|);QJ? zfaxUd&DgaxvObXAOx{*yrH6+_!A%@QOGLkPc`t{R3k(3&-CKTn%X8WWlHhLC{jYes zJTRo2Dw7(w%5e0-huKSUs|h{hsiCfvvOOGob*Kh#iH^(+c}W*hBq>v)P1TA|cq2Ui zufBvSI?Xiipse_8U7K!J2^_u`W*cKVaU}}2?+;B4-{b;~@PgJRMti*Wf|K?%?eHC? zW$(HzfpC`3)!v-TkDu!HiWdh;pb`Oq5vDm7frQRL8PDfjA`(tqTESB=W20|d#@Tkb15g40wtbMh;2XuHaqH7JV* z8=lC-wNad@wNqw9ib18{r?Z%rk!LvOzQ8dCwwS8<=5JO_15asY=hwUyCTGOXelesTOWjLy>Ax7Qa?m_4Vj zK5H;*_tRWANQ0QAhb?}-ie^)qFa@qPuf|wl37j77h18Uz#Jeyk_0T_Zt^3() zQR&Xjb}WwOr`E1wJ6&@~J}4SozSFPU^AP)R2TjYf=K;I@_CRfR{+*?NYvJvlW;#IT z`LNds5endM@l(ECA*{QVgjn#=^P!0ZS*x)htsKI;S{wljH=iV+CmxSzCcCTtRHl7p zG>pRM@4<`(xw$y5R+KH1p_gncZk-KOO$#%PLxIgYKBE3L=)8iN1R$j#y}EU6Qqp-N z+ty*Fk!NzwX}6`G@bo*?f8T4F`Rru4$p`}@Q3eA;{*S#D%y=wxU?g*)lua=H$H^5z zklzNZW$%DALjsathsGUW3U_44ny9Jj4ak5}&sP5Ym+5d*dJR+yOaUClHU9$`Y;VVZufCM4+*XWJh;|UJ@u)sJl|kT`d=JiuLRuRa{~8gO)-*w z(7g?()`$BQ;Pwx2vjW_2v_ZE8SNx&HD=(t17mxeLtLF!3SImmv^U{nla9h^;F!;M| z`N`*gKY}yh;R<`@;W(d)@#xv_Z0q4>y6tIgqYr@Y8wcEutvoe9EOQBx+{vAhmXX=HAIV&3k?ZD24#Pk1S??pJ{jQuO{1{i#sO)AR`nPL_+YYjgExpA4 z^o^PFRCzBfua0T&p*B9+?!d4$awbuZ%3@nWr8n*3_Q+=#>y|>+HSQ9L9$7+D8FUoJ zn=(WC_#L}Ib~#)SzjUy-sa@=j{IF<>fv@A*0bnEfTfyr-&M8Ot9T^6QVzl+^74ALv zq0VaR(G$xxC9G!0S8IzT^aI4NDdEH6F~Dp=8%yK~{2o%y^A1(6i*G~R`}?0J<R_@iumi*U(^IeY5kY=y430shY;o-2Mg3uVCFtIFU>21fXzG9-X+JJvG{#H(p)JLys{zW&k0b2ZzDhpA;6~sF32ul=2LbPG7t7#XNJ-hD4E_5alfYPe0@WfA9wjFf5o*eH6)7P0Ump4SpLHMh_t zHYBl2`e>G~0yj26ptUjJfm5b!qO0LmGUijuJuiFZWUkiYkL^eFwk=}RliNLsG4Hvs z>Jz5Xiu)<4^hBEM1yxoO^a`jM<6q*KN3Aq`t7m}KR7;*>ADa1+$p>2A36(FY14Waw zp@$m2!n&5jvJ<1UqfQ$t*LqK?FKd^yNR6$8Z|e^| zO7BzF?j0kfs?;(cA5yOzBAGw7safK!4AR#+l_AWqYcw3v;dFanXD(6^KI~m`>rYvG z65I+7aH}-%n*Y6K&U;+l#849IHgTLZJgwb5#_tzbeAE<_J1_oaA-jQ{+Jy<8`qz3( zHAW6ga8Fh7>wT2$(jdT^&08;oV~@6Toc?jdHVbTTvjkaj0LK}T!1n%%#K>XF>DV3A z#n!D_7IV8O5#(IVk=I!&n=Xhz71H_BLwsa&_Hm4xNX<}8&j4g-fEP)p+Ir34=%D^C zL@jZ`w4bawP?Si>Vq7Z$oNl~#_w~1iL>sd?d&M^!(j!sd!v{du%hw{Juk?>^(+NJJ zQgke~l1l86NRXJsOUQ5v?H^T0`XASig}Tdg@$GiWDS~H<^5v|&E2w(7bq!wn1vziM z{M<|0M^w)Ur+F03Xj!@vQmLp-isI4a8PA~|GBnSGrDmI4nh=%nhb23|ueOr3 zI}FLiXF;%d)CSPWJP znJR}|`3KN5GC$(3^jRcgPY zlc_**F*T8WRM&>CTB4RO*d%FbhPaPb(okoiMF>A`^^rmM! zZLZhIuz{7sEsW?l~3Oj2}{?O4b&-+sf(|NGZEO(GkU7dO10Vu-q%k&M(ay+g1 zpPM(%k=9=(-w;wxgE|L>%bl#MEtD~uaEC}PK){^VzP_A>2AL>ZYE`3247F5fp=qFXUe@(^rR162KsC@Y`+KT9x@@;iAqm(Yt5+BexbRf%zMw zuf)zbz%(fXGm0&kPHo_(uT=@Q6ugbCxeJEU&DZcI7^TfO&m3 zOS`N(&aJgtOeph`(}8c61OA<^JL60lxMNN~`KNS2Y)@Na;aq#owI{TE#~HfBmj%e}YTHPj>$=+sCryv!8a%iO zR55cRO*9}mzT65zpQ=lX>rri>)cC@!79u4)!_aHrGpe_ttl*wr2^xh&KLa1JUo8zrsV6C| z*!OtEoR#qW=pXz020;YDBF$NcZ{eE|7WhlV8p@4Ialf=~uN^&y1hALYE44T}jz$aK z3K*yzilP0UpxYj%yev7emD$os?Vm_BL1BV?|Q(AjB-FGSUJnNWCtbmDF~Iw-RSF}cp9h% ztJIQJHJV%0;8FI5%lz((M>fBKsZ47O6PyX2SmaB~7^z1t#?CpjEF9D~yrCy^^L zjMg#-mxhCCFrw!u2j#goyA&Kf=%3sLJEao4*FZ0}q)AXi+Qn~2RUd7~Vom#XEm{jo zj5W7K$L=zB>O;ikymuNn_2PSs&GFDMJ~vfw8L$E;!8M>-xVqC(rT)PJE`(zdj{2G) z+f0mlMlt}82jjCr(oQ0)9uTKW3~6?iu^xsncXiDsy0pjovJoX1+;?O^&$+DmR}UQ|$p-?|=;oeIJNtFo0nm5Md#3(pNsIkMl9YPo^G9swS z3O3lC*LrbOClpY)Ko;a&-N`NnRp$2Vg1ku*UUrCyz`{;*O)}T=;o@BQGcfjzE1T!K zqH#Xag^k{$R_$dR=0;9WpG{4NW2S=rgM&fc z#K3CEqknkGFdl(kHBt9EZRq(pF7FKWSm2>ets-v2Sc>R{!_3EoSr)Gx?rh@u2k-3C+ zRJr8WQZiHDEwhE=cIp%NU3(PD@!^v%bgagVxt|#36nQ^_GD;SO06ZbJI2StSY?pVV zmFC)dD6HcFXcfy@X`H*&ebi@FM9f)>u>EnC z;+5cMIh9(-?3U*>@S)4a?}Y9(V!rSab|?J6083W8!_pMRgp!H?D?YDwG%h|bL8SO-m7Krc0`69 zs#REv&i(R1n2N|oZ|6XxhmlpDIL87%c}yxl)~ z3YNP$)4n+qTJ~;I-9Rlo6Ugn&+4L&j^!iTjlt>;EzX4`Gh)Y|h4D@-JZ;nbf+Fl>Y zt56j_c0>?}Xs>-jY$|)@1vR0P__>C7;PK6@C=MOgf?C&t`X`$hJ(glDy~~HM_6~eJ zp$KG1q-eqba2Ss(;=i!8Sbhy*n6nhzTby(}3sIp#Vm*SNl+d-IioH%#wO;&#F&8Q~ zkNhvNL-9Va8e4lFIoH2@QsVVdaLr*C;v2GY94?!#MD|pJ`PGO1>TvCYDC?F9+ZJ4~ zpgveI`|4fs)w}9t>zd_n5Hc`?Y{Cb~nf9U8cdFKRk~xUN3+j@I2GPhdxK`r}|3i7( zv-(1m!FeIDwZA%MU|nUk)>xV@O1qkj9@lfrTJ_TkZ8%$eg26my$G0knmsaQ&CFU_p z<#pAoo2I$lhU`PpD1`MXgz>8Ij*vhy9?{q4G?vZ=Q6ewIb0($30l2}X4F^?+gJdS6 zwI`yP<|kO@pU(aghpkG0lCW)>#Gy_&Y+hNk>9#JtFcz%#E>R_xOAgA z`i>K^)*o>jn=geyOqsQv4d^AZyXAa|NyA{i#p4!v_jkn8ZZ5@R_YcIY_RqzmZ`uxE zRwiocl1$u@bdR>r1JnE>2Ow+LpB-45(v%eAl zs1aY!GuzOU=0K(TK;^|6XesP}Q>JZ#oWa#yK_b}XCOG79)9Lin>5Nue*&LMg`@wm0J`L$^B1_8(Si4+Rs2U(&)rPQ7l+i4?| zIr8Smz}rqQD%3=yT&;5I&Km-ScywTo68(W@SBKVFXNB~0)8vrD+w2c?`G1gim#1y#DIZ{iwN;R zeXMrD$}2kn7iC1@&vU{XYuDn$rR8{%dmfy5EH>U+UEh4JPZ9j~XuP!qE|yj%J&Zp{ z4Cwe^YMSNE+r{bbwGL}q(nyLcW^O*>gX)?Wc`Fs=1$ebxqX-BJlH=wDB+?4?1j!0V zrbQ`w7E7f@K(V9g9IUZGha;0@qcPkdG)o z*#KR7W?EF?+@ZN|_wK^L^$u#UaYb6f$G@H?(pJ4!%RTrCaGVL4_Cv$N2E5rCBuL31z4+o4kN=iMd8qFT;96$G7 z+VJ>usi)VxTj9k8X1~J^7dcvKS!s(~(7a|3%w){;o;*tOWz^BBlPaYB&gTT{)Vd(J zBJ7gH*j+PlBIo)%4H(Dm| zlB1xaNLl@Y&Z5HQ;t^{AG(#_O*8&uBC6t!=*)87Gp}oTkz&^+p=0Xhd_)4Av*HwxM zzw^b^lLc8cvFlh93&awtmAP^ulTz0e7Qfv^?w8!vT1})!KZ`jZvUExMr4H*J%4s+D?2V`QvoAdB;;Y@1MGP`$G%_cdhIbq-w9@DB>;0q!(dP)+ zH?~|Kf=rw2f$K%Puk@;wEnBp(p*@-H4i3lYs4jY*Y3MauT8&r-sBRW_0db{WEHp&n z{T80t0m0jZgPxAPwF9PB$$(VeD#Bz1 z9t2<1Yubiw``=mbB;#Wz1n+PeDca~7(O+hzeqFroD?XegV!q2&*=>9beWGsf`!#ju zA6zc%SjRYmo;1fx)TtcN2?t=yW2o5@HTRdko~!HA#E}_;kXL7A1_t>Y3a{x_pl%_r zwu8+XfJ~q)g+W2_NhAk-BZ;n(0axTQh>y8XT>XF)cdn!ee59ck1*FHG zo_3Ja!Z8kxxIla?RTUbYR)WqRKOhrM$-ik^#j?O@^&!qEOiMC)W+ z%&G{T-}V+!S73Jq8wXLnNQTGW=G9QIVF!D& zrgD=Po@V5BrUSvoJOoiN0b!BE-8}&IZ2)p=%{!b2KThe^vu$t4&P>Ut1-B9TuHl$RTP#HZ8o+3=Wws&xC-FlG-0dYI&?ibj+vg^S}WcWH`-02Gq!+dhW)V+$kUQ_<6UnjHOUSV z$xg^_Y7Unp9vTlyJ|(HEL5DG-q#8u}Fi5R~^Cgl1{AO%5e(*Y{G|ycOS^z353E@Mw zm3rA2CbK0EDfD;Jz-!}lUli- zdUmY5G&r-ndJ;s{WdF@jpkMa2f!Br8sgg?MTclCyyNBJ?8JJYqfT&oK)ft?>n79|_ z^GhzU01BoUKDV`he ztS2cR;4ZygQ7Eo>rgONPpIsF*AL=R&Dm_ezCtv67u{I{m>{)I$&+JKes>O@#ehNM` z6%pXhv^RV43EX?9DwQu(dWc4KOw5elh;a z8m}5hzx=D&klN~Q|Aa-)J*6tb2Rn&j$h)oBg+(RDi(A1*-Ig9V8Ic9)UDP|~ONzzCgPq!ZXY> zj<7&%DBk{zcFBrHi0LG3bD zmmNt)_a;hQDsF>@R#*STgRn0IWkI)sGlC623Y1-5pL*d8YnMLzSz3;@CMElWRg22( z60ZQF)Qn`g3HzqsZEV4hbHigNTNPE^0j(t;M*M%T^C4%wJx@fyA`Y^X0@Nn%l8d_K z4qP>z>p1Y@KHGI^N56j%cR3V0lrZmD@vAl3h4mr0!+LyKzYRbWm*$iz8z)o^I1CU3?&WSikl;TwOtY#(FY5J^7mf{>lB#yj(r(lk_Ro>i@#f2^m|+sEW-n&t$!j zF^}_)>!yAg|H0;q%?!9@o_}N<^B8o?6icq>qGJ4e2oSAo)BdBw{~81TWl;ET1KtBu zk1Q8{`a5j0@zJAg@Rv-N6nE5@zLNRlyq7eWXf3EM(o|B$8(BfC*sB;p3ByMUVO`AXfX5SN zKwK8;ZEAG@>AA)OK=bmbC%R3I%~IH8{Az5)3XRKB#{vDPl1u=@rWM{l#8y%P41d@B z4^Uxiehw{Q(@F$EGLa#^UrTXmQ~(bSXDx?#HZp*)va%uzn>MK@gx&iHZ&~!ST<-01=SH_G=Iv!{M0|w@b~5q zK-uoiYIy+Ci{=3_r`5Re%&GQ2h^ok{L}CDinBl2=Z`F_&=--LD3$5YgSa=bedi$jZ zeX<4d_dh?JUOm+OU~M)b$_Tn>IgePp$zG3KV$1@{&VZr^DSMqHk2SU1o;&W*wn8>q zuTyJw(z#G$h!soKn`95nV5{0%;GsrQVG`T5hOZE%%a=g1I0$%2Ho05osx~0YTEr%q zHOC?@T{mVxF(caWL`k@lsABP|&|TX4Z6PG93I{4|P`K&J=^(P{!_qJRm1uFCVd+QJ zci-5c{x0Dm9a-YSD4u7~cu%7DE$#@2=&lGwqxo`UuTM$xM?mvs)#dEVa{g6vqMeae zf!_U2@*P=;sTbgT`<|p&W@LLhVZMxoTeSy&hO!S#F{r!sskQ)PkkYYt z7E_|q;%7Ra6XpiVKE}CJCGzSN)h{WY^%HEJI^#P4cv!@)Vht{SLI&P7H(=TR)8BviFrL|*EI;9;-ToGs8C z$lp2bfLZ@Oe@t9uyJ4J&Cg*^ygVyI@y$IUKrwd3z$~|3J7MPM73W=u12=I#^q4OD4F1w|!3F&G=$;EI4)Kt>f9UMv$1>%{gpUe>*0oZ8Ww_WEdElp8FAPk$J!@5qP%=sBw0?8iz2@51f^iRdqoj;aojpv3Yo?M5*%&NC zVP!&E=#E>f2#yGj1iw_bf(%%1UW;A0A#Or+?o&lvxPI(+p9PL-t;B|fjvD$c0877K zye#*}C%uP;_96S6UT@f`E|`wuGBECQR5D(9b|oy)KO#+ZTCw6`W!^RHFQ6`JG>a^; zT>2Js(HL&AcQFm>;&a$3ym-EK%-QNVwiK}4P1Di6+%AGbUl!rM2zA+6Famm=wo?9g zU-m{F+5PS~5MF^NBY%-l$Mc3FFnWd##@EqqD4rW!>={}lME&%bTbZ$#b8cGkg}1#)Xj z2M}~>Ty401ukmI15?XYf>f`BNbHEwsYs2?g>v$aOrzWx7buVk~_0aK1Pe>-;_NZ+z z#-r^MzLVx-4)Wyg-=52v8l#50RqN;tLy?S~3Wd~&L0Yfv>6fAHct61Ivag5Kik`AN ztnVVQr2VdeJcQTIS&A@#8O@k>(HTyytF_;Y7c-WblZD1_8rAp3T&9F^E+#XSh8oB? zK`*ZqeI_M*yZgFoTvdb)dqx90_1SrZ^)o`>p+z^Op?E*tP4q`0thDX16ZBIiwRrbq z1mZonu6U42e^6Mh z?y!`N4W9QCx#2PJ7818J8L#xvocN|PvYRtLsZ4p}%P(B8(=Wq1ZN~s?oA)#%NX7F) z7WunYXhx;+Gy13J3C?Ow0Y72vTglgnRcbkmRhf6BpX66ULYPX#l_X_6uDd1fs5_=& zq`2zDF}qASX}`#Yg}3RB=p3CqUF&Q}KJTwA71_3S7$5JdE(koSbKHtDH!E~}9&tQE zX0bwGMFg!6gPL3u{y5@0sU|fYmE&`l8~eC928i`Y2Alz~|L$2Bjpvtw4-W$)iVOoo z@ej{J#Sttl;C;+I7f#ELPNZ(3;uKEDbx5F5(Y3D4G?`RKEG(Na$X3)sXFuj7(&6>n zg%Q~G81IYimQ-bigu~E>U{VC8J+Ty8`J=Nhki_wDq+cgqypriyF)iv&h=v~E4`}Wy z#}ns;Ja-7SGaNZR9oC_t%lL;d2PX5SH}V~?L?f_j03svOk+r8tXTI8z^F?pd$7Y17 zk!B|9(yXq#hQOktaI}ET)G`o};^_K@~Gq$%^SyEKZAC6%+?cv z^GfqPU_ln?66bKJX5CX0DO)*+hVU15KO?sehDy*>)z3HLO$zFAQMRwbw3s`tLV2fI z9}0E@UB@hNPqoOKeR2$XsS7<~BHHniSm%w9yarkWdFhf%Dq*hic4e9TxiAGj1|{6( z9tSWcm4EC^wt^1n%BYczc?K(&HWuNVQVc}D2XKhAZ3wkEI=wZ&Mhh&IYX?PhXsauM zO+>JLhqWr()?7?T3M4xUpQEz4FlJHLr>@&;|F3k~e;39zOZ@@L|Wd z(6`xv(#LM#TIBRIcmGcqhRSbWYU^foo#`8+yN3=6JY)vt>eJ_H$*tyc&)=}fioaUl!1zjzFNKA!I%w_c zZ<64u3Ce%Na3H!fw=2OKV?kY;&Hg^LC&RarpKb+mBKrxi4*eTchTuoP%6gWB0aa|* zfosJ(O$_`81Z#}?FX#jfS{exaMWMrpemd=*{~OVPZzVmQ4lG0R<8Rf3`zOCz3~P)D zwQc_o@d+06IPjkY3c>iV#eWF`%Mkt8SM6Yp(V_b7GjKrbtNgG34QqXMS`&DJ3jOil zh|me5pKALJ!oQIj_*Tl(tH3g3Kk?NASYvFcPy5V9_kV{3;aYJ|qXL}}{AgFnU{_F} z@7n)@Qk8(ekN+w1g>MyH{l5}gest(^;0fkGh-JGVd@IT6NT3svAMa{9>1=tlVD5$*-{@*>dFY#+$3QCe{?2j@Hm-xUL3kt0f9d#Ox+BX)NSJ~*eAxzS_)bltl-$Z06 z-}$FxzfmSZBtLr>m69#5tVW;5gs(9b#*EfO@DgfWom&>1lC7((#-7JSq%jr2jMhs) z_=!dS359F0o4~^?T#8%PAthT#S&br(30q?-lo_p?fY2;Fn@d(Jg~LAL8xtDPMUZG3 zKFRfe)x+bK{V!&F^25Ax)Rom(@(Ktw2qKt^dI+}6!hN`9J5sXEl+}3h3P>~vqL_<% z2`oN^H*zl#Yv4M4k7l;*Bk1`QzRxXto01)%toA0a;I+p0SZ3RP0$KBL7Jx^VBsDu) zSxr7K1fS^a9eZkain7}KJSIkssRU-UK>|XH@OU2C_o>+h%4%A9OspDHiOgt21c??t z<&?C`OdJ}T$;^X81Xur?TKFW7tV62Ewk-lNy?Hn(k1TF#cDSY5hJgOvmxdf}aHvKC3%d`fERznMnW zHLaKjs|c?2!r|E2wNYr=3kF|uJyD_R3mfN(m`q2w_G_T1nXx9}Ke7f;8rmErb-=30YD?3fYFS zm0eSI#*!^$7|UR0{O{1BPxbkJKHuf{e?4!{ea>^9^PGFnJ@?$_oIBU8yvk`R&y=&% zM=x0AfZFbd$y@4vrRD&{bhkSEEmQq;vy{e^!aEUFM|8&Zo|}~f(<$go=)Ev&2v$|l znbgZO>k$sRxN?s-3lW#CYUNZ~{=6QMihT)D1A~Y#-zvkI+ z5g_$6=S87_C)=vD{7o~C&uJyO?bn2ar-z(=uO@Ulew&bn|NXS%Z!ac&6xEyw6S;Xh zOp&T?dw*I>S!n;GZCvO5nbMEbX(eskuSpZ0?y+87Ntd@xx1H+fQ}VVsP5-6zB8V`x_2d0(u2X4JMUUGiGCbzQ9!au8Fn{kX#>0#!qhIF3O@0o?b_ul4I{M`~k zlO!V%O^L8IwfJ}#AObmU*e>d1o_5?xGpRsC(=bd#{4^(zP(a|eDpmi1q~kK0Nzo#j z=M4Wh0RAJtvK5=*2BW$!o3pMY8n>vRlYCIB%;W&>C9Z?BV!z0EHWlzE0N>JJp;LWS ztQCEHM|TaN^Eczq|5pHhbbZCkxP=~_#EMeAf&2?p1!?Pc>)%j)KnmEm$R(?L*~#St zS3bA_K5&UnQ!%+9_Bp3;VBb@*Xo7CH#f>U=9o?6ovkI`E0xo4fzcOLm(vB9?L<<7o z-`@cDhC#ZZ9YAa`9D(|<_poa#rigI|o3>#x`fX(Apkj-Ba`WEZ}bbkHyvgmSv(*7t7~Z9bA@ zq@s2#o(O_o4tk&)1a0KUmAN2;x~FBXszo{ffZI-Lw_665Usv&54a`3sxNsuS`?YFb z$H2?>0YzfvLN$jmrv~y_JW)@{X`zQBze~xf2TDA=&VggfUm{))ci6gJdV=bow-CchFgYru2m(bFpZe=>)^oN?r0Do%TEI%T!pjQMohF3U2VWe12U6O;m->TM({` z8Iez~9Jou&4!Od=%GvJ9>4#ilyGtw!UPHRH+IuetEjtHfERHCLo_0bd8@de+lB#PK zgF*;=kP%r}hu{k*iEix zm=F4Rty=hcwI5)Af=zMom~1k$B*;N9SGd|s!{ngWh1(&q+qJvyvNuw(bftRaO7)mk zb&WMBoPfy>UXuo~2Z(pdiw?DGBtEWRgB(o*6;<^hBG}c6ck&DuAM(2`&sA@NyRxIn zHL&rGuqEOyR9%^hUZ4rOBasS&II-TzACV*0`xVi&VxP!2pm%XQbvNifRuU^s&_%+t z(mQnwVV6RmJ`*{sQ>?9HXs18@AwQ|_GN^GKdGY%tID8s@^meP#S3IDQ_ai(y2!(?FNg z<*PL}-ts&6@*7Jj7?Jt2D8UZl{J8xJy?hFtrxkix6dJu0#Dyn`IvtuRpVKe66w5ZU zSGd|LmWrsq*AZTNAZ!pUT*}l~rCS9Z)w~pUg(Y4tA`S%LY8>~5QKS35)4;>K9I}^V z3ql4egE)lkW93>x6+k!*D1;itkG_d3a2lvo8$2%VmL#s=6jSc6z@Yx|fv{9H$M!-0 z@nxz238KnR@xsqB2?b^D1J{v8u$_RmAzsM|qZAvkl!GdFJbRS3afa*|Ipv2%!gr1= z1O19Pc7`RCT+2d5Xep#|mdt{8d^6>D5e45#SniJnGs!5qv4sjvG^BBf?AQ(#1hW>F z`-{OW3QBHzfpB|;4z9KlkmDdy#g$EK1ovYD_7R1N^5H~idDH7k2-`l2Bj>c=9t1pg zH{rnRv>N#<_vHYq`*KS6rTnI#mLH*hAE9XavQwB|lkI0R}LB;R~r zuI0X5%6++rnIO@Sa`OGx#!?GshL^qsWSxEah(~Vu+k!5;aQ2_$QiU;qA8lFxj#{#wfyR4ZslE&poJTQ6o*(2`&btH*td3mD~)sR zZ*a$~;{BG6u!q|Td}>kDGIu=?Uq&JxLMwG=#o^js$H$>@Ph_x9AlN6}?_N{lJ{^eL zcpz@PN7T3F32>#Q9M@0FPl_|c$1Ogth|sG%?bWH`Wvy~R{c$;2$rZ<9ZNkz!<+ZTb z=Pv!M@(+DlJo~gGz^)!{_8~3ev`jm8Y)E9WZrQdv<(b1-edA%=TU?Cc)Z1md3*v|i z5{L_j5Eldy7xo}7fS#l+JgW!u0Clfd*EtpTOF~&Rlwbrox+=gXmOY z7oY80V_657fV6|Nbt-UiV$E&f!0c2KvNH*_<0%`ieMzY=&9^h@PG?d;XVTryq~OjZ zk&fFd;=@m?kgp=MxCtLjOAYFE0~f6&jSv1kWx$6=l~EY|j0&9h<+_X%O?#~gN z3o36Sy#jV#+z}9v6>~o;CORu7DJv!;E9OO3Okq|GCM%{ki_iV_6Y$?nQwpC@3Po26 zThjR87K1gmr_1cE%j^Tn>`C6|wzmf*qV#Zj_h@?^WLY2)3-;Q+tRGG4EdT!R-fSa}ExIHK6^2cKFBb@Xy?VYu^Zj z_SFELM2(5LleY83>OM>Im5YVWgT{8Gr$NB0>8!()57lTepAUxaC-6~9`=`FM3Exw; zey1ktM zSbJ(%dsgkm_=?*NYfI0h&Is4i<`%iQBO!M}w4zlC< zG0`Ox4WO?yTZy7q+qzzrN)j_a+K3w4EE?3eS(iQ}yo%4FbI%;LIc03KWbnQ%qZBPR z&Wr+>mhD^j$cCd9XjW+8{P?4%scT@s0 zJ40ns3Z!k9NTrgh{w;M3CCKJ{2Bllgx!+mMaKvbvSG?lr+wbfFdHExQ(#>WU26vY) z4EQcGC{;gisLX2shoxfx|18h4A_t!m_NYTn6oHSh&;n;5SA1@&oNscNzzM@g6TAdW zR<4-Rj>!%$JW^{nSqfTI8Vv=NXFka`-eopB9ha=uo*Lbr7Tum6-ToxHJ!2`TJVZdA zY9l|&K3}@;FdAis2a@1yOM{Po!4wCE+>+Py?c|tt$};t2ESivG$ryju3h;6mjqpO5 z_&J#dPJ@@U+wddH_6M=;VI;7Yc=^%o&XU4;l7QLxvKK_DqI9IPdIUUiv-L-tcFxuv zMSo)eC^9SLa$L$qOS-m5O!TPR_JU)X>UWD@O4$lZIZD*ag?zPHn8E#O^BxAWQ!|8t z+)p8TQRGHLzOg{BQn^tdFQXrr*`6)4n4^%Lv*kdJ_MH+HTW-_*EnPnC5|zHYomq0s z?E6D~bKOpK{!pOi*j(Vjjg8;~$DQhu!K4&=x& z5@#v=IT~pe9mh#Q^-p2X4*1z`gTH)0OiZX`w}YTYm+{92{UAr+BrS^j5KfoUdk$rf z#{o!SkPtN9RZtp4I$Vkl#Z^x^kicKN4NncC>u`-XbGK#@g&85(A@LNNbtf1$tpqH(nc2>{^1hfYM zokT#%2q>#Ll;0eB&>X5{4n1cMy=)G(Gl#mHLxaqrapurWbLa>Sv;pL_&`EPB*&NDh z0p+)V9<+ceSwPQOKrdTB?JS_~7SJFIXq*K!(*jyz0c`**pgk7QNed|10?KL$<+p?$ zw1g^ILeE)3FIz(GETQg}&>%}_oFz2V5?W#jZLs{}`m!n1&J-GC0xdBnOc_*m>}lNR zRJ>x#v9Jttij&OjcZFbY3mO*#;+Ha~i=Os zL9DqT)LS`UE1$jI2h&BA789=CoK3ByskLU}}|JYq>6vEvBh$Pt9W z5ro?jMD!6v;SogV5ya9F#0~`na6|!Npnz~wKtwAb3KbBY3Wy~I#12Ko5k-W7BEn4( z5v_x2uF0n5le8S89sSxbaSW`V<<4j7;427I>r=g#T+`u z9BRc9I>r)e#Tq)s8fwKBI>r`i#U47w9%{8EbZkqg)z;9lt)W)iLdUj+T5*JqafDiN zhK_NDT5*MrafLFVh90Hi|0vyFFYC4FJM-k`J={^0ap}s?vdCUy5XWGB!J$%K-D*ln zHPNJdJA1Xn;9FNfviI&FS!S9eJv>tMArTzBaEGS(_Ms{&9U_Sh94jUR@UtYpQ_Lf$ z*hl=~l4Apk(_;HjRh3zhL@o}r>45H8Qh>j)$D4n;3=YJY2w~*SnbPP|K;L~)$ zc-O`6=mSTu{`E8NYA1pp`A-)1U%M~laBmlGWBs@m+A$b2_ zUrCF$PmTQy-Y-_w=;a^)R3_k87}r=@+0>W3)pEk5g!|>xzzeWuvJ7AXzB;TK8D6kO zH4QK9EM38q{Ycg1Z8ysSZjxm=ke(3w1VlurJbER_jKWP=HboiN=fB_47@OY|>$eKs zrdSZds{j@nV;36V@1<0eZp9~55snS@pB?LWs7H@b1{F<*N&3UZT2v@fVSxrfr1nk9 zs^5hUV%n#k-gT%Bnyx5-3fwiWelTrXyuAjQ1FV2&rNp_}hJngr8NjGYEz1eE>)H>X zo;oleShHF+^;`JR?_Ra&UH=<^H_+A?_VEV4x#0>of=S6@c5nU7JHLYi3x%1)LzKYh z8Z|5iP|Ie?JB0p-X|Vm9+=wPhv8TJEE_8!U{mc*2TJfYOPJSx%H<$*m5x5gxn2l0d zo*?&Zil4;P7=ieA0emY-?Jlx%4)^8%PXYXOeKX*}LNJRD>i;)zU;$91Esbf2#x(Ho z)!>gE1^W4ftD~I!2Qdq(F#UG06Dtgs6N3f=aW2!#M}RB>Ic_R&Dh-1gazGBa;ui*Q z24g6L7)t+rrWK``MGU3oU&5hy`vb^x$nq(01|x9D1_ zl#~@}NfM>9lXwl6Jze{Z=bH(iv)iF->y6V*TTK{#Ok6W%g{_QZ-3{XyBV=zQ%Tti-_8HN z1bnUGuZ!#;k1&x(z&TOB&Gyn0=t6KfAR2t`^Fx~xFZuopHbg`DJpnWn9+Cvn7?17) z%2%k%U(7!%7%6{Ebd+y|BIzz^a4%_qcSMPGr0ULTwMU1fx@pox1qn6F|-SIs&h-Qus)+#P37$ zs)C&;fLH3fRkq+UMT4U)Wk9Y|Y=yKf^e@ez3KWRHr@>`6>E48cOXj$>09tmiIuZ>b zzi?3GMFV$*2diA6!EHrc_}`PHvCQz(YUGE=tQ`bxvr_AN-Jr#c|B4d$7fz|IAxkBc z#^8eVI>t`0F+mz~|Ur53a57$(a+9M%+1YUu zLECu(zo8Y65x`^Q3#q}}iR6|QUUuZ1|9CzwvZ-PuzMxl1U^v2Wd9r^A{*mBM=@`W) zu%Som0dPfr@mAb&CShTDfY5^5PU|k!*X2cGA@+N)vk}`o99ZM0%E^b5R%Ch$a3A@W zaSDTXrp}R;E72j#e5E-nWRv=ZYd}e#JJ25?L7Ev_LgVXrPz#tTBVso*{8qoEgJBlw zYkz(q5ud;VBVrhgiG5v_$0&VX5=i{e&71wytq*%CzWi%$AZ}(LznFMXf;8;|FU8mJ zP>5g}|CR@~h!Hj>eq77PH}I4$Vq}ep!_46NNq>w4lHlV|x?cNVR^l2)*jM-|%IwtpsdqD9UiIuXBfc1$lGA-qogqm9h7m9(|Hg?L=+ANT0YPcPD zmv7L|$r2n7sl%b%0A!Tx+53epdzL~*|I9)wYvxpO?6YKLDg%_8*k?&pwxpc(7wpLR zIb7tpzY%0~&T}2JEExxHswJ@IaoXRpXE=8-jA>(VbAR+Riv#AWsQ2~N$`_Rr*k}I+ zBPxfm&w>n#CNzHqpBIl3ta*_33yXGr-8(w%DFFFb4EP&1|4S19GU_z{l)5Ta!{CN| z^iLT{6`lhb{d)fnMo+1R6hDKPjy~XV$54_SNQaE>$_~1gv?qYiGeSyTTX-bK{UqE5 zRUNWd#z6470N~S;**Ad#H(T~dxOtPc{m9zX6`fEXrI{o(qi;DMLBIcz5-1+^=W#DX z6%jBa#zgt$@lweZ0aQH-D5#+1RiIu~P;x6!xfPTb6{r^#l$;7wP6Z{q0+m%k$*e#< ztDt05pfV~b=@qE-3QB4P3jAc;;|kPcf)uW}9wI@g3N&6GXeuS}mpIypJ+eiH;>kaFjx)C4^TK_+)=Tel_8(agJ2y53Ht0 zHZRau1}xP{^^C?b#3&BdQ?puJgH#V1Q%j<#m6N|#p2V>4Yvtra zV78knm_1wdHT!EVUw7+k1z){=ExUS1zfc0yqh6~RHGJIoTf*%+wVE-on$Zt$DM|jQ zxY0K1Ha>1bA{zX^I-uA~71uC2NL3icQ}aev%~r+nRkKxbiYf{ud@-K`Qy`wiTLM4} zVihVY60lj*mc3g$ZG6wpW5_NgpZX{~(xLr?F<5{|v z2IoyKJhyn z;dZl6TxB@!CA-A`kO6IY{kM3bG<_QYQC;KdSr z0_#c#K8_XlX9hfTu_ZQHl$hoA=IBqggQ-@9!{huhbHU&fWeaz++|nHVtakh>R^zT` zYPQ5q-z>W?%z|uEfjdmHtH3#PvA5Au^7Mgr{A<>Pu4iytVh=TU;V{x6my+EN@KN%q zndJ>`7~d7UR*u6+#|j47RR)YR@}Ua){Z?6#zu!2caW}q-b(O)p@fEDA4A+G(VgkXb{5MF83Q{K53=RUY-=D*gIa2a z1UqR4WRyNn2La;nzhc1eZRU%1lHPbhZuX@ypxpj0!$ZMI7M95H1rFN14l*lGk(%5m^&1WH2P8*TJ zi^-M!DqK*V8FLT#8Fqq1UwCpIfoBV|yE{O4QYua#petY=I~R=-&oP9*RX%*4#gIP3 z6G!f_=#y+_WueXTwEAN^&`y7{KfGQ|3i1}Ymk|0FLWA02NCy%@=pS@4CxkZmO=0U6 z#)hztiwNV2O(m!St~#-Yx(BlKjB<)iZ&u9&49#_xqL;^vy4q}(-Rz7xX5H-479)qv zqC>1;$RR^4@+?8J!4GhFVnfxWe$7fjX4i8E~LKa=? zYs_E*KdA17XfNtb4kyQi*ux5e#nCm%7ceL4B87EvZ$IRl&)Mm_%j+1s&v^{ZKe4@P zxa5oSWHLB%hQj1H7n5&J$g}Nz*wFRj=B2|9b;EWY#Rvbsax8}^_6MwRwOaP7lfZ)t z>Fw?h-b<@$5@@Vp%CicKT!$bPNEU)*ea#`ri-Vow6sE`!6PWKJ@9;6Ew#SYQZK{MD zEsq_~Q4?PoQdGwHbcjQwTKKF;fPiXCvWQy%uHR-{6yZ2f{D99VbZKvJu}!L==t#%sI7(j?)D&ahy zEqvtsO|~0D=XKepc74jHFcs6k;T2LxdpI+wDx*EF4dFk?#^%->hIB!kiIwBfg@F0F zY1fiA+-aDRKS6BFJ?^DZ+Jm;&{d087OT}k%&gQ~OG}0zaYsG<7D*u+9vxr3~H0Z9H4o5k{~?Z^V{KWwSbpMqnA!-l6E~YwTdx z^;_*=_5`QE)`-EGgRhXHcED0=eS2wJen`#ybbBwU>z4a18;1sTrbfPyjIB0#WAAv) zEwXDCWGW;%{6;KrAXqQhzAyBWK;+S@EfTNW=j$T&cr4G+vC$y?B>Eyu(@_gv=sn^9 z%5zZo69im+;9>op{w?bcqLoE;9!|)2T;KOz}~DE}`6d<}*FX zq-si}!vp=k>qQvOU6gTu#L7o&dQwW;RH>KPi0K5+mNRo%hnn1J9v}lBz8zpDYDA-x z{abI9xDGy4d%0yqVi(e3@*P<$3r?1SVDFyGDs?E7qLU?y5nKRO$BT}2Q+lYH84CWO z_F?pU)tT$%!1>Y*5i_?hBd2uHULDe}NwahXdE%yzKizo3yY-20LlzGMgEEk;Mb`%K%Qb^%#-^=YjR^gAi^apB z`f8z?`a3ijD`uitpYUP4pT_(>y75b?{nr$V+iG@0+=7igAoEKwcYWcrSuR@u)#qrB z`sr}_Pduq(2W7}6*uZmk-4c#Yz**?wIKI>aYL~>vFhZHc_@s&iE%ae#k02 zR2ElU?^k`+pqEtl_V!g}^hU+yx}x*fgvS#1P6j`QhNfo_z}+RL%&24ryevNd-g=P> zpG8DSKt2v}ym~0Ab>(@3i!Io3-1mnp)M(Q3r;JW_3P=d4C6{{W1w;F%-@2Ffv-8Y8ft7{Muh3;kT$T3eCZ)nRex^Ryt58Eq^WzFW`H=7<&l8*b&}~H zzJuk#$4$5$yY?I@a)v|~xd37ELD39P_+7Z1z}@eH^sG~sImCK^cG?BJH*%l=B?9~Ro2&o|)C{gBee%lzcD_~3Lj{z5DXY0_e} z28bJD?e$B?TG>SntwX})YG>(=(41`Gk=taC4(nktUOPqM%7g^RZDuJ8F4XX1OSf2o zXo0AlK+e#M>010;IQbomwq88JFlxm7_4`6YI>4}M{DNH13(lu?gz*0l00`mWIDh~H znlIrEC`dx8kemd~`o9or%wggLr>KxRq~Y+O6r>cE^+k$UEU5pUsYTFiP?G_>-xr~D zG3$pedT%>mC`CK@PT~(3f>aG@<)4>x!|&yckKBn1u%g>Lru-?nPh4zr@w?uCF%057t*}dcI~*^*o0l zb;tvRocbE6ABDudrGM_$YaRVqAb6|$(%Zjv@@DOV;KgQ@!u9=rt=r=lfr#+arOHzG zSix|})4K0)Gt25Nvri_Q#g-P+R+R3>qdk@M>9STyV1|q4oOnjB>W9Cp^qIR_>ap1T zqq#{`Jb1Qezd8c`mV2L~d*3`+CVe`q73ZG;)x5DUqw)BMDDX=@+W$;|?13LDbEhFB{_41n)cfi9ysG%t22)9Fgv2OK8EA&Zi4XPZ z(c875$<`KH4R2C`<|dnE`zN-1vmaZ5%@RD{jQke-4(FSKUYsS-tCIpo=>R(qk(atdkL~Cnsj(j zJ3ri>9CB%8uyv(i4)`W#$CraA=!+p(MsIIF*fQS?zu6VceG{`+hluY*zE-oY#-GH8 zZ-^U*kF8Y>Is(4#$Tz3!{Qt+|=Ym%X_LISviUwPMBLY8K9IzvR&F5T`+2ds6(%s45 zTDJ)9?XQZnF)t#)tL2MbzO&y4PwM5dTFu|>6WQ_a0I7E}VDwql2cu`==3`F}3tE7VOrOsYE_ z_hqnc*87~;O>Mn3$Lk83NW`p;2VbhB)(A%b%0xGwb5lUl*k$RGz*Lu(S@GyzRcz4} zs&-Y$yiDs((?9-;Y)rK(7&MIjn3HY%4s6SBrGM%2XC?5>iS4CVASN#S-vape>zkwV z&eCpqxj$M*6`OIzfa~1TBr9#{%z)G%l$J?W$EAbrq;74I7K}b|9DC===b|e|IL=un zjh&T_^9NF)E!X!&Hy+1Et<(P3m3nSYtd<}9cbbc3ODHpCVu}th%ry{f?6}R&*zwZ$LWZTA< zSH03Yf&<=V9hB_cmVB_T<3;SjCmr{rC#BJG!wao#I>tuCVk-B#HCa*B}Nr^~aiAbpjx}w8ufc~>T zpo6R9YZKpa>sFS-%G-G(T<@Vj7U*}q0l)t$LZ z5vJ6{89+9V$?~b=Z`m_FbfzBqAp9R7^Dz4NSOG7Zd+kjAnXAmc9r)y%Y2Ut{xig~~ zaZ!%4{a+}d_FHVfPWW+2^Lt>wF8uc^yefusmWA{nuM=7t)P53bw$yE@1E}mrBJy)* zP2xNE7u)gWd_(4@(r@yba8ROja?dn4-Yz*0wm=&C3AA(YHU=`~A0Ggj&#z_N-B+F5Ci}?lBrGA2{_Bjk)NhdYXevSH7pHtIpuG zY;F1ZM**f8eQ$OC)_*KAU{Pc^IWoi)_FLf>b2Q)=I%}By%EV+s2{|_($Xib7*~<>_ zj(5~>GZ^mzm!1pGcSM-Y4@J7mTNnrb$0G2LU7U+>$C~4=cgS9zr-~h}ADK5dt|b_T zK1pO!-gN;OgF&azao6)>t}J7&6T`0Q!>(I~3P{(x&8{1U9C?12zwLf7A0ilhAr$bK zaz$d5EXa~?+38zu){ha0{H1MnKBx&K$`*o}0DqlakQcIvGF|&x+?n3FJx6vdhihzm zgT@~&vC~Ax1Gm97$`_9p=tw5E<;xo>9dkt+vNChX{9PK^ug2PG=8D@{j6KwPd^=a$ zj`WUDB#}v7w=XkOc6_D-{<`npqma|bi8r&~#WJv8MJ33^pgiSn4*RR(=>{4S7WiB! z?w%FGj(WKvOIg2~rct8{v=^}Xub*A4J=XYDF9Lq-PaSI$|E+evq)DO9GcO}_S7tsL zQ)m4Vc!_>v4B8uDT-inuBAp>d;GeFi{A25;{?x&Ky6AOc^!Eupb$ADUoaEyj`f(Vv z56=6SRLuWnl5g>+M;gDp{=o+ytCM_YG&4Fd?V4C-v)Buk3zUz52d*^>saqsyPNLY&_l; zHF|{OvSrfv+3Rus&!H_h_We=`{8IbxDueDk-`XN0Sa{;N1V#eWmN9C;b@^$MjrR4- zfalsRul5(}sQyU_tkaUEApg>So%xTR|HTpGezP~g-`L0B*7?)Xv=o8M9iwhMB{AUS z`gv4~47AYVL|gr6^!CfpWE+d?KP!P>YX9$)z^``de^z~>i{z}hZ*Ns|5w9E1u_@Eh z%K%GB;LqB>t2{oHRKTD9c$-ik&z+gfFtK+7D_JEHSATgitA$Sm{@`l%w>_MrgwQT{8~f^d8XawmPv1J$#N`UdqOd7o_q~> z>>3b1Q*&ohYksO|YJxK2w5@2RgWPt<5?!pgxPs$93{UG-nla|6_aD(uK2qg|S~zto zfpg?CscE*BWAVYr#XDnLhA>YV$l;6fB7)aGrS&%0F+zd6mAIqCq{~NJ9=HTJhlX3J z3dReEuJEhy-&6XiaeR@Ja^>!Y6&M2m%mhYN6?~aXagt3H#s=SJ)_Zq*%9yZPT*CaEV<4XFALqCY1PAlvG(P zYF&9%be75iXYJ=FD+!JLGY|#TKmOR2snBJ>e2-1q-3g^jicF-RvT9sg#F`_FPFPNd-!eQ`v|Nf^tv5B;m_>Bw1s)>I|T|9Fg&cv#EF6$(GO}|6?CV zt^hOBI}gVnQ-4_nS*&hpORleXUEr@<(hIeoir7@bg@ zx}rjn6qOp-`cCj#*|AIs{iUo5Zs21&L860e_~S)bo$Grcskw9GkTWP^ZD^~cpq=!zYR}Mkb2hPa(4gZa(yhZ~&y>xE$B` z3j3Y41+cEAYRyR!Hu+_BZbb9ZDAyOK%|7^Kq<&I}N)Ydb=nI{T<0XshARt;ZtMQxa#*g76}V4-hx z_U-F4Jn9MZF4Ns=y5cJX?co}L#AUgKg_Xf~;p}kA=&TPKjU0La5XW*7$P<<;8yM18 zDAQ^qsA#pDvzdFkthzYx@idLRea~nbxA(czlBzdAc>9jgl3F*6usW7+;9OFNfX`dts#7n)QaBt0P;GPa829VEbSV&@fl3o${bfNhfU$KNje?+*>9a`9?tA}bV)qLbDT5k z-Lw21T>80!Hkt^#fM>@-MY%sdd{>qqMqipDjXzj*yZgd{H>;cIrH7_R8z}(etEcUE zB%+(Wvsv=Ps*AaF5T|Kzg(l%WI`k^}Tr<5ocPs5uLP|1OyzRx@--BXd=B#FZIP6p2 zJGCC2rSz`TkQ4VZ2!48NIqWalnATtzgF2tH+>Qt}AeRfp7+X(X>#dvX9NLBV> z`0l!^2d^g1x}4Et(ZiUQZR4hWn9ZB*y}SC|fh`1{?3dnljDa*~9_@Z;VM{epfs}Z| zOt$aT(~-+&89vzMxl_3hFC|iZ_vl+GX_xKKjC4kep3U;swS^TjiO8qFsyuK_8+cav ztQ~J&lF zSIavfc4&GPk~$1p?A}K$Ssn`eJ~_@Z7{$A9i)8lR){Wk8Rakn|S+Kr(3|mrpa@2Pv zyW5HBVJ>;LKKJk!YwiTNu1qq$bfz;X+L(Fy=;U){8{K9pvGk{0?>{hovR+jO9knsL z@U!+GnI)&HS`B(p5Vr?P-A0W$lTv3{wqVuF_}wk^0)i7a4eS?f$w9OoXIyQ`u!)zAHprixMvELH}4(QoWY*X(;Uw;eJ{=Vr#bB&KEcBWQcr8T3AT3c#XN z&RNgsGIj{CEef#lk7^MdMI0UPM8pqT0K>w|*BfTvPrrY6xAb(%+hkql4uRks@SS@S zs@;gQlOy6GGa?}~qvq}9e@s?*eBd9_R>|}0@AONIh*ImSXdbQD(z?75|BP^j+;Hbb zB>s@Wk|X6&rrO2!9S3c7jL;#)Hz7BdVKW1Mgx0E9_n85AUTtLf{KrUVrPu|fSP9^v zMn^(QSps1%(CDun-ejzVoNRCzO^KiCRfA=RAg@Di_L$gAH6yxWPfXgXM{Iex?cvr= zMt*N~aNuFWoCXIUYs?OefeKGzOm=W~Jbu3kt;rq}4ijj<42Bz{qjKct*?bP?EpD3? z7_&H>w>opoZ<*gVzjgl^(->@RbUQHo@|ryjOWvJV#q>?IOInlJGwNb)8ODHtsNjM6gO#KYX@`Iytq1g!KuEF^AUm%D4aywTz$ z5s}Pu=URFbVc+$RYJDG7N95L>BY@Kbp{k}s0_S6y2WYB_b_tyC28EQML7J*^Fu_d+ zYO6p2$pQ**L7|u`G=Kt$Dzt!tA}c6-pxRJ{4yp|(nA>x)i(Gr^s_Z0phk-N13l!u* zAt3@3-253hv#7!?e}Ey(#}ky0psEQ}NzzVx3@mSBwjDxyVMY*G(Bi^60BU}^{apJI<*MTpwz&1V?v zHcxu9sR$>AXQsX^DBaqBT^utPw7E@IFxE=-nK!hmR{PFo-Q9wQK6f0s1To(dS4ejp zdv;-Ncx?vSSOpDbzox(hgTPRrT8DsUo?6)CR`XlOLGkHyyD;ipQ*v~?nv7D2wVQxB z^YM<2nt94$2e*6`wXA4%o(Ypu<#JNitPOV4(yZmIAwR3e$C~I^emwRBDe{2bd9m*v zyZYjJafp%5k;^lQmteDR`~aiHW?{(EP=yDQm&EqHs!O&*c-><%)%)i%EMz>%up5#>jP z40~;Z&@|u6Y*}xgp;N5}lwOI>qx%j9-&}cc;`wGV`_~u#Xv21%x-V9{=dBFq3ErrO z-eb~*SM&Yn9x8VomG3;D^I21VKIO*5Tv+LaQ_i{V5#$;Rx1=&o$9>Bo4YS8RHxmaT zHaE3XH=>uqfvsIxcXe&yln*HVZV6+*BFC2b!cdG~tB51CpO4-ZyTM_5-(#BI!)3tn z%UXXFFPxz*w(v;c-5`5)w{XFs7Ppmwx;LS*x_oGpT$*Lt0>j%~?Co0e^>#V!@N7fI zr-!k^Y9-~(&PyYD&*9{2dpCAiUbK8Rq(rzDHdWu0lM7@&svm^K9O1z`K%tl{Gnsz z<1>sI1|kq=0JwRKa@%ikrr#Uc?#|Fj_C>anhaTVUd(7hNm;bTiv%X9V4eUa{YRdTLs>iB2G!k`wL7FyhZEJ|;kYbw3gz@mM}=OHm;{u{SCKIQ%F zeW^-^j5>`na?RhvN$bS3Yi8qq28Rv%7Ed`_3jlBLN;8F4+t7_(Fc#aZsP1|j&x(;{ z#0e^%A|5s=AKw@_%XIuUonY0*P=x2nT;;t6yDl8u@|OF~b>`6LSF+mFp=GzcEjKr9 zi{FDxz{`3=wxCWDFJTDYJewu9p^g(J`-L#Lttb`OC_Gbtnm1aQMB5MNpoHQzJm8T< zcmPlqlDXbvdVC=_7EPq{eU0WEjX15C91lK6VJPg0$85w5heh0bkRl52m)SC_< zHE$ZsE|WWl&hqZQyRDI9fV7wzVYXW$uC3&{(Zai%J!eL4{DTDW`$7Z0Do3(zjTL`R zArwY5jvJZRSzw>qb{uH-a-cu>>d}OOty}o=AF?HTqR;CKrFKO+(k5Muy88N4s%}Y> z=4%daZkkVSe6t3-ijcKzeW@B9G~KhJSjxGC7qkUq7l%b-VCD>tSRn0yjl)tHf7!VN zsXgtpq6)O2-trG@+60U37xgu;oPtLTEJz*ZqrO%ujo%w@#jtzLG2e})FgricFFt?l zqVon>_Z_d1wS~=JYOKS2DKd6{br)<%v+Dt@PyB@}MsI`7I%{&6taiB=DF8-8^4*E$ zQ_vSe!dw;>!{OXGwz75y2H(w=j4cwj6-xDathl5D&vs7;R_+ouQDb3LgYR~0@y-p) zj=D!vTeMkn__WrpasU0Z0Kwmy<>fY+bKZ8|ykSDtXErNr?k|pUl(ZDrI)%A>^iEIz zafNCje>OS%J~#aU*ojtM)i-D7O~NHxLTD^acV0%B-WG~Sh^BUXD|xf@#qFv*DJWTU zB*rYa-hTt{t$nv)Du-?FvGT?5`)DH<5#z(#FO+m(OMW+p=0hU=iLAFk0F#Qe@{4WL z`}l+zL;Vof`mRdITgF?LG4y!hrmC|MNv+tS67N0U$jqI&RoCer zq+by}p8Hr#c(c2=$fd$p54R|uapB>R^0yy;1__TAP(GK+pkKecy6luwG;py=TG-V} zBG;?`NIf6No@KNIU1K5haZe5H=Z`UhBO~iJ; zheI*ECoXN?UZDr?-ljDQF^3$z zv~NJrcGrX;Vq438m3xPwP49<0vI!?xOeY>s1@7%Am?^9~1UmV^s-tkl9RJ)F7 zr%YhDMv85?j&1nrTp4wx|JDEDko>BbFno|Z!fAl7dFY=ENwZ}!~-bAZJ4~i z@g-IidrBB09EXg(u7CZVdCRE?RpHCrlZpb?X?|%XvDvrp!9?dzs|fhK(B*h1QK_EA zRTS8K?xY`|0>Aylb>16?Oc;7t`LD}fZ|=D_I9%ZM*_TZ~;Re3_iC2v}zb!Xg{vg}o zK(B^@zMP8;A#q3Ax!Ll3yzKh_04~Xaa)U&>dcK}Q2>D^Ic5TQ0m+0xMIu7#gwskj; z7sQU-o>ZhUY~&~#Qk=I*d*67i+__Msql44=uuq(fo#kAoewuUZlgsDhZdhB!b?V=8 z{(p?U19T@%*Do5|wkEc1+fF97?c^U#Jh5#jlZkEH&cwEz+&s^G?^)+t_k8!Bwf5@% zZPf0nYVE47u6kMIF4aU(4;PsSJ?+w+7dY+dDVY7@6Sxo1xChJK;kFJ3J(kIfw^W}7!K`9 zj1CVaZ}T`pTX`OvkM)3(zuo?tM2v(7P=!qPZk;f^rtc-@_AxvAm_pxn{QT?OBUvtS zA0x>KJCp^^$q1NOy5f8DK-o8hM0#KwM$lR%pgCoHb??l zXUG-#OuNhD2qz9X?56PLKn%>aVJBsjh;X*k?pmgcRyb*R&8=DEO_>3JL&C&QL}G#2 z)A%hPVfMpGi{~e^l2!+@(%BiDIRDditi?{R7?QCBle>Z(rFB_KwZUm!ECPaqFlAvI zLM9T-aVaQ8urMbvS}$}Sl04MVcCZ7UC0+?8_$+5MYxFnxIi&C)Vy?41{6FR%BoHJo zN4seucv?Y*-IXmsm4lpsZ`Ra6kTW80G%2LD;vhB9_%e%vK=Ot}@oWeX-M%lv{!3@&>l?5<3W8=m=t&DTre~Z7A(fs<^cal7sXFAFzq` zL>Hg{fO%`|2LCk%ndTHA`vU3_mH8TsgxiZ*-MwN`Q6E|pQ$$ndh{Od|kIaU|hS-MUjI>>TS!`Ku zSo;uNRHxnDA~FTu_PREYPLI8QU@WB3V`Ulxz<|5D123Y}ZfFt70cU$1l6UT)ze8CC|o$;UQNFDegWSzAGc2)V@)?Q;e0+4UEVb3mF`kQwHs~t+rC7C!K)%^vVf2qx47w73QY{S@K~htjjB5RPZ_NufRu(T8AAp0aR*Ha$k@8K8p-&0wzj78m8Onp zb1Pp@D%Jkr_x<78)pfVFoe|xDVZ)%~rtj}8%{rDSj|Y@CTDNw&Hb0XQ4*C_%X6tK{ zH|}KY_e#&J)g|`l=zsqjZF0-lu!Aw2{d$!*mV=bN#zz;}}5gZy^6n4i@T^?tU>1uCs%ox{9&gW02 zvEq3vw5+S5byPA85%D93A%VtB)E+StB&?!Y^v{FoPaYw zgQWQdj0*;g3*~)L*%>qBkAI0!H?ZPLD|gOe!1t0B&rM*k<^shxoB;(L#c1o5m_!=v zeFSFwI-YLVh$!|57tJ}qB$5U?r+TAIo@D1wbba^=eBB%Jv@+NgfMnW}$>;>Mp zK%p<~xxG7O3+sL6RaQa}&XWNos|1lvH*pG8kMs?G8@;y(>b*Q*lG$7Ea_;1%rGE_jzf=|ju57wS~9PD2%Mkz3KygcKy{@9l7seS8&KzLa80B_ zA1j49Tn}rq1I~O8lI9a2+a+k0D|nErc#u1->XsHH{C}cdX2>B^1V2!oXI;M#o~nq? zbNPxLK-*mT)8<7z^+(f}sQ8-RC_(YsY28w7G0I&V{c|l#ec;or1fX`^wPC6DX8*s* z{)*a}{+kGK^UJF5AJG;5({pNY1mH8@0(&ZoQbz@&B8VMvZ)}t6I7}Dxb@1 zunJu)$JefKv#44H{1+Fo^eG>@v(+`1XS09lLW%IfgC7y}9}(x%UlGAi-~TI`==NX* z-JOApl3-yZ_!;s4rdGw4B%dMU_uxzb9e)tjY_vpyT8Vgt{7>~PoGNZMZC7ecXF7|d z^G+^F55; zxvgRNM0$lCEC`#qS@IJmu2$p~DlA%A=_4oevg##+Ri`*j)fMZ#59szs(SRwVGRMQ# zn~jI8(+J2-5I0D__v7OxMsQ@}hrLVd8B3G)>bUDxv*v1RxQ=Q@7Y@+WXQ*dP7{@C2 zHiOj<9`$Ia^0=k?_4;I@7(F^t2=>M-Cfb2pm-ETy9j-Qu^Z24|_nCw#u}^8ttQTFE z6X%((*?*XKn(ucnUkqSY3{vo44%}r1b6QMa5Yrd<``s7wj~zee`>XEVuYVbRjcl2S zwkIt_Oo7ww9eDF)vl_jvQI~>vz4bbBKd-0$3a!4jV4vj&o##a)1;pj_*e6cg`+zMe}kg5*+U;8*l0% z8J)>?FWRlg*2l%0zcX@*9OtMO~5CiK5n|T*5y80NAx+KPssrr zd%rT?{-gRUY($vs6k>@xr~(^tEA{8S*fs|uKw2KeWVeM@kGVQlxVvQp1YE7;y=vv> zTqj5FN|$!c^IBxtZF6PITWGanT75nJk$(}F%Eaq0$z0!xQ#pP6K2!&PcQ6Upt8NqO zQC@?gPRupfr@N!52pbaz7AtBLLi5Vuk$HfF2!4ONuG_#_0ERBvd)ymRiWg>TT00sE#!Yx{bkZV{# zn))=$L{R03l^nxtL6{*f;d2cc`OKhHcCk#$5#ws`1*?+t&xO!VJMU}>^~2CKu;%Q$ z0>KgT?2lb{8q~8bl#N{T@}R_ul8C73Ir~8I0EyXne(hANMJ+a8aIS1AHlbGhodCV; z2g$^5+a`#$2{$SX0BQ=YF5a)-MH$wu2&$_~w2;l?`i^komkeIxIV4y4S4+)sG7P0f zERtohP8cTbDc16Sk2n(G?EP4WZcQT3s@LU(%WkJ`j{d_u;Otjc5gf$Z%{A0n6pTLb zx-bV{dN0egGckWd=?>n3RpCMZMjg}SG{>_cQ=LE-&iJlj5139}auRV3lrZ#UQ;Y}! z@WAP0SuCMZFu{_LN0$A@b1Xg)`&=BP-d-7oQB^I@I>hJuYWcw~T6kw4azSYN@-`${ z2E~&dKR4nVxpynPlaWh#>xCrR6f01}OGcE0C|6aUK)CUf1@qpd*cel3r)s0KjrBEd zNsaHpeg7d+37F@;sSMBZ=7@2+E$GBvMcFRM+#?G}Q_ntRILHEdGqZP!hWzo(1Pzzy z*Z^}?Y*+F$&C<9^W4dyFhTp?7ReQO~LvcZhKt%3l-Mk#(y14qcO5|)q%aR-v$}mgw zx#}lQV|%l4J_6{eD%TL02A60Hh7nU)d%FxZYW^=b3IHU%Gw-tQ(J< zwLY984{8=Z`3|UC3m15XBuDfezRVE>o@faqGHH#^aBQ?DJ$uQ$h z-ma`}ca>VvJy+DFMEhWh)+CYQkwr@~6E5#j90c$+OP|-=pJjd_=M7xkPiN{HU^7b1Rhym2`MyGM>uzqb7- zb<*{d6sGFN&@=ay!pB_+sxFPp)dGoo3p%azpp*<#Nqg?kfnuz$6kP*pSF$Ol@DM_Z z>H+DC73u79S54tfe`-GEV|DB4w5XlmR;KE1-g#e{#+L>qU$4}J$&pQm*r)uGJ71*! z*k$ceIhJ7}=)IAs1_$MnE_KAaV!@G~#$}nHYG*IbvVOnVa!4JVbZLTvbTm3bKCm`) zdlkNqu+;v6Vf08|5o>XwihdZ@78T+=IiOxlr+V&4!5 zv7siVZVZ{6c7A>=*c|JZ;vXn;HDNhxt}0jO(#yP)dlh2H=0GA=H&NM3PiN>MaOEPd z6uFpLekie=tvfwRui(}1y<#xg-J`0rDD|~6D9QfoBUgX$5`4GdzpmZY1=r=Qja@J`VR3S(YqXu{uXe#Agf4^ z{ay9t`~2be<%TvzaUZaqi>nQk0GV(=SM;!ct-@QCyvgs2Q~&O**$uCNUoh)L4h7wE zARK@yoAFIsWMXY_JJjayTL^5)@@lJOe@@j!?jn$Ut-^qjjTE0X3hu7u?0{4d9VS1gK+ z(3y@H`Ah!sS>L03U%0u80)-AlYcImA2j9eVekTB7k&k~ZACSsr!5*SP1OcVa?4zL^ zVJ;q`Vk0E_n=2?^FKB8zmc%-pD@G>n9Ec(a4HmkOw-v& zikJe1#4JJ|%rN@MH*rxe5yIU8p%-#UkscwUydoso$jDP+VdkR!EF?SF$k$O}9^zuW z1tf2!Py*DwG2!)tqEC*k&J%GgAAiPnA_%}so2Ch=-1;BjCCa0 zTF8F#!D*;B{@p^z=8D52!UXv~1;ID81ehLynjz+V=1eD^1*+{j)V(Qyh^rzJAMBN! z0b`ID*m2%O+huS!C|xSX-KI#+a}xQsusQmj-;o|?Bm|*hL&Q6dVIFw@fe{{=Bm^a4 zLsUDC|G;b~WQ7(eefr*%$SXAxpCrj9=|UZ{oidbbRcOBS)$WI0K89;?vg_YJ(|>yL z&yPZ;_&AIKLb;z68I-Y`57Cz;Y-+W8$uF#X+Cf+d>K2?}+H=5fVa7ei)yJr={4mC8 z7FKTtz zPf^m=q}iXv#Qb5o*(i7n@zJ>1Yw>ErTqp=3g!Ysieiy#Lys;$C!MJfGCQLClCMHC^fhNvDy7@X;$TwQV zIZ1$LWE^g~dT(t_AQhSL>=}lusO~zD!_(V=#Y3fC4r{#Yb!6}4zQVRmA9Dv0F2lOD zE@>H~6GRDFzMZf#LcF*|_e;z5yZ*PRG(X%FIdyzLK=? zcmG!GhR?)gFn+g|8V)e#;wIhAWYXHOoSUOX_l25lr#qmFTm4^fxwP-F@XpT3rwl;CM1h9a- z?LJ3`c8uZm~`2DcsuTAXDZ}^n3?g6NE z;j`jq`LI*Uw4ZT;sDFHH5VW#jV0!c0*Os zoo2wAs(r;*G$)|)hv9D0oBZZ-t|=w-4T2a0Ue2d zimKl;463@^CRrL zyfy902i68aqlaF+VFKWuGWFl$TwmHZKbQvYGxdR)zqXs8%mc1R44}Rt)K741SJHT)b3pn1t?ystHf@-0AG+-@N5hZaln>Vz#$j{6doEUse|TLShrHZA$X^d zu0E|pcy|#<3bYO8GI5Q{mQw%?ST$O+^p;hCHCQ$JGFgpl)^-`Mg~j$Od=*F&^g6vm zXg5Is4|pfNLrV7s$RpG`jYCfNI|w2aFJ;Y^W6=LVfd5JHU-J8pB1|*LC$=qG3|))= ziPY(rN2S40)Bi82!|DPYG5s$A{)4XQ%$(19)7f;sH+1m~KQLhVzrhX?<`k%5_Oyc! z40s~Lt!#%nVhF{E#hL*}j>EPWnPD8$`JckJi^ErLzZ=5VsK6RrlDPOkTGQRUu#Zkt z&;|xY8W1tKd+%*;VDe-!9>JoL0JSEHrNrhtKjYnL0rg-AG5{)pX@mR6jsh^I3F&h= zbVWa#pgOcge_Te@#fpYG%vBr>k&r_~9zkL#wMAOQ=tw?eylm(ck!-4gxNFaj6!3a2 zC@vbk4tUHm3?}$7W#2_!7wSh*yc_WzW9~|IVJW=)Pb!@)?toKpKqs|BQ1?#|4(N5- z8Z7Upf#p7iK>|OfhpWinq54sLZ$@X^n8t`*|54@nF(KSX&U~r(-;D0IF$=y_zE1;3 zeGKA+eoQY{|3}qep5Li-AMDQAug}BNUtM}vFDp$on7~p2V#)xEIJ_oFc}-ILFVZHp z*CM5$i*KTfpTijO#u&kl+^ZCGnk4ZQ&~Tbn7NDTJcmCgYn@=ID?Eemrqm0obGx1j4 z98)s#wg3OQZvU41Kg9-omHUbu&)EK!OvXcxY}R2S|VupnB+TK?hKPXQM3B)F>Eqb4?#8G$bV=Q}m*J38S?o*z!R!WS$Xt+J%%;9uug(FkQIU10e zmwA}W78=vHHypUA`)z&f4QU8b7xR{EB4u7zIS*p=e|u-Sx8hyX}})Fo=yz4 z3qN0yeZNX*AjI-T%=Ps5p@sB)(vrYhfu zg&)9jAd}8u*;)7;|Gxm>A9Va!QwSI&7_>D8e2@A}xjIF0yGW_A?+X6@N_MuwU^wIl z{dzC6T&3Tu_m1Lrl2W7arFtPdn_(~v^@A>nOW8(m)-{qd#{TdhyMoXSVc25k1y0$9 zY=&d#HhluJyLpY+%#!v{5@S${K&vs+7S%MGSadI0y;&atI8#hz@w$H@=fgH-ThEJj zt932*NtLkcTMldL22f*L^QrZ6clRxP_Th-aSQ|#zS z(fZ^Q6|iSc?vKzup}Srg-FkR_)NY*hSU5I?3v9{Jb(`$|=X^3_)l-1S-gF3e2ehBl zwIOdlQW;RYIGV(fXdl!StewEcsarL|VTP_H8As*nVBlFa{^IyEXpCl#Q`fWT48SGr zsUO#2V24+!#GI@ViVJC-SShd{(kG%#dcOu4S}%#Sq~{vx!kD2KRrF!EG*%I8^J^Ct z16=Tj;81`(PzUfG`LvE8MZlkrvW5mdo}c!X?GFUo*u8bZhftdoUmp!1g9%YSlCOBT z!FwZv36cF|J;JZtgL9Dd$lHaVr~uoS(4T@&Q^5ezH`Fb4$6eFwA_AK=qus<%h0iMr z>-`T0c0U2Zqe#INrNJ@DgIO|h??G)tOeF!#>ze_E%I(RIMG_TP%L>wVwO&Mq#FqsH zp)kz&B&aN`p#@6lrY9p%?wE6=!f<}wxOjrIB9~M{CPL}PEl$6PO$f;wX~32!nK~}Y zt!%H>SPi2?ILvxme0so$ESr&4)&ntOxL)U=Oh*FpI!Zz%$Xr5$|5|{NFs-MFG-ILU zI6PxF=P4XpR;5r@k$G&W30ZZy6nrkEBz$P9)Sd7M(oO7HDQTra_*a=-D{1cG+ZLpoe@&Gt>L`-q0Bx*RUu(e`i&@+B?6wy`+lQeQ9!NW#IdNo&T z{D8EtOYY^PO4XbW3PYq36syh_vSn|^bSQq#4XxP4+bqn4PpbTas|Zi%XqAVrOH(bsNXl~SjIuojvEc$u{$Ho!T@pJkv;MjvvodlVgI6V zs$_0ya@p%wova#LXnRcJjPNEM3ZHuH4d=?sG-hIg+*HT$SWT8n{}wx8vr_ALWokYD z#FC%#SDhtQc8z*531%f3BR@(DiEDX8tDtX-`;nK_B33y0*ogF71JN-JF9v=_b*Skk z5mHj@=NIZG>I0Zgi<~A|&fu;3;H_5RqjO}Q`8eqx?C)I}fqogDSsof>poSL)fqs{O z&yz_a`B|6%{B&JQ%JZ`j6+VsjlGJ3`N!7PDKN=|_sj~l$MC(U{f_fcVLwiE4c~nbf zII}}=fKXl`ESOYli`qQPi0FwIR&USaa1(J2kCuQeR5gI~7xV4?*;I(3P`sR-Fix$W zUpA*Fe5Pb$kj#j>U$zHaBhShpBHmn$%}-^SXXKjT1ePiUdD~UNbk;7*=5sTY`r8o% zc4bKOI0E*%P>z6y*0Si`O#bf_N!(g}8^flxp()u9i!my)e_Zo|Wh zHB*8Zk46BIJ^6cVD}ktF}u%A1d|?);~7E|hx3YbPDHq+n!zd*Ovyu(9=D6x zTc@jBIk4q1#u_@Xb4es%?2}vBCyH$U@l?Nnzq10A?GPDK5aT*vpvUf%3zd^fhqX$^ zJ+2_LKKxWcSQOMJ(~Nhzfmr3or1dN}3PTdvRwa{D(}&eSD35^ll(N%@3|;j7zNJx) z;Un47T!XeFI6t6}6EYo0<(EmTwkZT-{d!oeoP)HcXZ$jY(M ze|`ZVpcu}vLRMqkq+@&?`9SfTzk|^1oQSC_S#4qaGll4F#?&KNXJGv0l?Sb9RcEd+~0dEZMpg7KX!jP3!tTGS>8Y3ST+NgFa|*Ts%At~uUk zzQnrY?W1cL2a?RhrX?cWF&N0Cn$JI6ygi%h3NxxF_*Rs3-#p zh6V%#1O*ga2lidFfs#867zoG=1PBN>g~=ZgvnkXMF%1~(A7vA%KO!y&@;{J=Cu^?i z%Z>{S2#EM!yOcCdT=*uJK*Xm1GG+)ytb+KPZYu)O9`|3EqEd|5_*V#Df=Gez7jiZr zK7b(n;}>O>3WxE_?*(wmOdB$4ir_dROp1**E=-egGa~nYdCqG=JOTTgGNuzT7XIIq zzhzcWhre=@0F%;ng@~7;t@D)~^>M`6|FWc=LA-(a*K!Z5G#=rrd}?4RLZYbub&QvP z&DRmJ{^lb_)Y*|75(o$p{jWC}-7jxNe-IfV{~mW`A8{GxFKoC%l!g5Z!QT*Z!2Uf3 zy>hCw+E=w!2&V*0AmKGgAz7mRs{n)CgruSZ0&+_Ew}454NH9$sXh?ii|9VIv;78*A z>r0m(NfY}oY}Ph5HNLCy;8s{e?8wNbAslA?z#C9MQj-B2W>FQ245mPtboe)%1yrYzy+&$bkSk zDey0Z&O`3_TRRlwQsIoyKtMnI{?_2xC^8&CU*FQs(nVjN-of+yyRH2iJJQF6zEHD1 z3X$th&l9=H-0R;&%LbK*qpC5I67@AhRf54D}FMMI@*?Rxu{f@baD?{VZC{$qR#XpAjUM zJhE4kDSmHfCNT6wVl5bd8r`7+a(-a}WR9M$u2AYK&WzCF2lD6#Jv_8s@cWUIPFP?V zkTKct5Fmp*r7?)04zS0R(ZGNhP~w>S~fkhyJ*}~DvkQlNQ;vf$>_f{ZWt=F3ac!Ay& zq|rcewj*b0KQz(P<-C4ACkDl#4e9Vvdtu0u8+Zim+IL}}1UfSBg!Hq-M@KpCXRnbI>S*^2j;3#M?qBlm+vS*=`qSw}z$?HnKyWeE zNC0z)Gm5R<=kf4x?%6Zk7M}Ox1YP5yO<%v=Z^HAreyOy?oI$Vi#kg620R=#{bIV_y zt~qUgNoHp(eN~wmcfysLU=gAWL=cer20yfQcRw=YuJEG+Of+05^2VjSH6M=c=yUx_Gcef4s{Y+pi%;$sqs;Whem0% zo-~#rgSiN;$!8J$p@FB0PBVQJ5(4j6KNAZT$QjGhrr1Sx@4#XPw@8O}Eue|?;i{FO z$AflBl~CXU@@HAF{5k`U0kV}8u+jKW!nD#d4t*I!i^NPgqg+u7CJKa+bp<)uKHj;V zZ9QglP0%624FRLGj#;= zegI+09QV0+%j-Uj6u!AZO8gG)A@>_6tEvRD@PSnSs@g#$$J!6 z@;VW;mJf1l2FQwnxp2SC4aVi8E@iaPk=$K+g6I9@S2tY)aMw})YJFNo)%Ow+ZEdx` z+#Gt{1@MLaZfd@9B0Sw|_3pCYg|WO-YXZu_55=@1In;cH@{`53(1E@n6|x06hz zQyee7lXM?dFTsTw)L}etl&6|}I(m^6vD*pjUV#ar1Psc-&)gk{kaD6O4(XgzKC!ci zEEVezBfKZ5<2qGO(3Y|uzhGhy7-o2l)Tcct$1q_cy^l-Ia-%vm!XFA1jirK9JU9(|&ma zE+TLS*yVCbi;>H}o>6jgWD&NCZzRLwHDUK?vCmbz91*W)Eoes`oolqBz1Jf5C$wlE$6;j@afayuU`o=9k$9=o~gQEt+lIi?E!8j5QX*D+0GPUb z*=SvW9@fcO|FAl$X!K5ND=fI1_TGu~(pY#sF3kO2z4|`)$?S7ib2%+wF}Np5N`v zPzGq=z&Rs?x9s?b0!m!tH1PN0fQ1A3-hP9S;{F2f~IGO67J$jur^U zo?Wi+3#Rk{l7i^78cc!qeH46k*NzksXRHR<1?^UqympcLkaCu#KPj`iN2(28>mF0g zVe&CL6lSuubGr}W9zk}G&1pL3$`xY;R>yYW?=N*B@#)g5YNqgH@0^7T}%L)w2b{)h;0mb-tvw1z+&mkFpBVpw}ewNx6-PJfQ2vf_zpa)%;L z;e#6@MAC!Ipr(=whQwAoEWhRmSLsadJyE?lL%Z;~ym=I}OlLx?SQ~5575?gPwM9M} z06WDQogr*_bHZX==578mug|hiFk2${bYEaXa#Js?yjNpt0Q`wO-$9LqS6W{5!PpG2 zvu~sv)<{cRFYht1SO$wY+qJlpQHgdq7cWFfdV9nGLv73Ka`1UB&Jt{f&XrRXrcaT3 z=#nfJ4Uc7!-HozN(P65Ji!r7qH$_w)z_HlZybyl1Ldon0y+&%c&kt0emfm198rh=( z7}SRXU{p_j12r@;8Xl};Zw8uD{T}amC$=o}Gh&OY4p|qBP>&F8M>=sSLfW>?uZ5U- zXag^2|$IQL1(CjHmX>xaVMN`3)1oYlW zJrhie4jK7ps3c`jS0KTXj=azY?bpQ&*xZ3Z=E+$5_>$-<-q!B77vER}a>YjO>H za|92SkfQlo#YkoJn>Ie+hPn0 zcwAaXD=!d`v5^9lvkk=FVi`}a5U6@Z_*}#P_X`DWx!YIfs}ZzF@K3V?HRW$$?qOp7 z6_jtJZQAdMBMoi7YecQ{`}em@==^Y5aNN?f5D_$3l~yugH^YB0^8S9APG0`fwOI`? z*bVLwbKIzJCmMG*H|NjBzVwotd-J`4cBrR-{wa*-xLgpN+Xf^eNgxHrFK2)sCm}e4 zA-p%Ck!+bBvQ5DXwsfzOO}-3p4G3nEYgQCX{3dVF4_DAEu0`CWC@Z~9BB;BJKtXC) z5M$iO*bc-5(VLZ)*}Q~w7YY#;lgjgo`-I?l<+kkoHIpqC2(Eujpugy&LufnX7TQkl z2@i&G%u|05-GdU21B7>=L7kyxRDvF$Zlo{K*)I&0n*OHbEM;0`)L{o;hb(cCOv)Dm z+J$)O2R4|nKGr*D7zmyf1}|hKWbu2*@&+t(tLiS~(!osTiR#&geKSIFePkHBCFM4) zV+H6sz>psZkI~1W^G?f4Q=TH2X@>`^6SKW4FvK;+bBedt5+T^!SEgBdN3Y9C!_UcG zXwbNYGTN~|N#8D5_$&hOdU@g4+h27)lJ+9*tSLXq9q1g9I|elbiC%#P5qyoiy>VVc zu)6!eKS$MhDUfD6=kR`k)k5RVvZu}q- z@W%&GQAr;o&fl{1Bd(%>#)h}+!}6u<9H&C-ymXXW@`Ss>T4?__wTv*d)1`F_AIl~^C&+^DYP!&p;c?+)vFwAgl|LQ#qcRtn z)WvyvJ%9@PQwxy+6nX5!!qq>WS~}f|S#**uB5AGFc>=Zi3eVhs4VIm`e}bmVNuTTs z+$*=N8LYxKI{ANy8}RE*omrbNFwcK$!_kZ!iuyrl+@t)G<_z}q^&b@^PH%5Zdvmkm zCDciKr|`oRWv+!c#EMZjwh5rEGd+g<>z|!*x1k1cnGrT}K=yj2wK3iOsw(E(?>fCz z+Y;*y8x7Ud%0$ISlgKjmS=}o5yQSm)?53Y}%&X|*5pgJ{smwda@#FogMCPXpF6+!g zK@-)C1W%=H6lRE-gR31ZDW~r8-uIC#`t=)}G!6W6lUGfe1pl+mLwvBv~7@j6C}fc>fqi#_Dt(@iT}T;oHmZf=hcV z9^IN=13PUO{M(RflDq$|qWG^aRZh6+kjv$|fuv>E#;gYkw@t98Z>iE}{Cvhdap6+} zDo^ydj|^F~PY3!X9-mn}W{pGdf+{1rXDDJ?dlc%efTL6650(;cvZph#?Xota@%bQ@ zoTMJ2&krG~&Jopjr`EEI2gV~PnK#~#x=lHbuq;wn5|4e{%*FVIirf6d?T*YO^UfZ2 zT}<$R(})o}=14P78ykcuon6@f{_sJ~c}>@X0s^9^0|NT?)uZHK>A;}xW}3oNkBkbi zGjX!FH2J4*KwHOtjT6J~@fT5M=4?VvJoCP+yNL^RY-6&`!IpH(yn%0hkq4i0oU{%t{`)`rH z5Aw!Au?&(WC$h{OFPrO{Y@)>bqS62iMT4ns$qwU&Nkyejo|pBJ>gY9?Of^<^1}+a- z>|ai8Ue-od7(n-oiL0m(qAkT;2@J7sOgu$VnVvmYk6qrNU;`>aI~wjku=4o&pSrHG zvbuZ)eE583gsu?Z#wnEqQ9<0u6QRHmnU&-vR0wVFqKF3CnD@XfB&vkt*hq(pu>~L??Q2f#P=n>5^WYG1C ziQuF&+>UskM2eZyxSFCMoY&|=9nb6VHT`zfk~;*Y3OIPJkW5D*t|2LV(^*rmi{UCh z@F)H-*NQS(L7HO02u8RNVSxa}0JlYwBhAsiXP^mtNfM$@?E^D;nJO->LY&T~t=h!& zNX9%O$&1q1{GvsesqJ-fK~NpvXDWFQS(KL9E)bOR8q#c-Vn}B1Kv6@&b3)!tzHj#*z~nTM~5 zV1vO=VoEgm1T)Nc*HnsTPXjwPfNyBwI$|Y&^7Og{1ix1mul<7c&y7Dr0s0L@1bO6C zw1EjqidI`Acq?sTomHX8W;Sw2Tc}|>!8)&yK($+dNkRGsi=w{RNNmp&Vj^+@b3Cy^ zbCiBtk`A!eLBAe6gI@)hC`7(ISk76o?^~xJ*^H`_dyCXE*AD9T@RRG{mWSgy-_v;o zA{@!P*+E&*x2;TORx;%x+kY<>d zs@%wpW|q_f^5`w95sRU4iP-%TY6$_mu+^!ot$#w&8XURIypQ&_Un{9>#3GN|;51ML z9{NM zH3RbtT^484*#tjePrSitI|L%Q$*o(_J|pOOq>SfBtE596uFfht5b{#|P}R2ma1(s{w%_!V9jM6d0Cct=u<>fp=Bz!e-`Jl&kx|A$xW}fV zz8hUS-uYUxQ=5ypbUVnM7B6NBq*~=XPOol>EH=meVuPE2KQWm_D@m~eMv-&rIFi7jrG!&A3FNsX+6p#1U{@2?r|GGztr=ta_jQnv9&=%n3Jk|I@UOu8`5e4# zxbJPxUqV!JlGZW_Wad+Gn-hiaGdOB&>F93X_#W#h>b*pAm)O{FyW}^pUPw*j>2;QB z$p9xjnYx1j1&7c%<&S(GyrY+l3kM!^8_;Fxn+!*Fk~0@>fqj+0 zkE>~HSiWNBnQ^m3#xGu+C+oLcD4^nSzX*&e3s;mNydHsjP|3tiPTRXuS>bXFb_eZu%!h6^wuIE;B4~J48+KKv!i}}W#a9f zVhhk80P5-Uz?yhkHZT;@xwr|HJG>v8v7}K?Raph?czl3&qq}hr`_n>Z4(eK(1U2*` z@1s<5g9z1y;Td$`Z}2YLDQp^S1d>h6lzJoJ<_xtz2rT+IQpKN`NqmPKscY{!DZe!! zYs&ior0h#|lPDDDwj*a^vS1i&#^cwepz2rFYq{e6CJR~EPLW1*^Fap{A|6bs3_xjB z@w;l3s2$3bE9$YV$aXbtlO+_1eD6>3t|sqTZoQqTJQAgy5SUKT!6h$zP-Ix=0ICd> zi63un?0Ac%6G~tK^@zaGI4g2oL6RbZicnPmV5AHE3X>HuF)VJw_Lp7@m<4E~C1ta# zGzvL2!R~NBtk^J+S|Zcd);?Bk6>C}_#tChGepER!4$$Fy(hMi;Z(ai)!u&;WqBln& zV6P$f&iF7+arcTxpHrr83TmMwv9Pqn8LWYcQj?IBg*E%G+|$Gk*hIk;6xG$BF`RJ9Hr1eeWjU>ok5*q^H5cab(h z2vWH}yMSXT&;;UC|yiaZ`{y2T2zZG0Q;5`kS*>I_MgV}QVv(~l3*!|Ef7}PR4 zW*8UOC@KTfzN`irC1@2bzMQ+dWb?t~dcc0tijDE|->`ugip;Pd%yAMkBDMg+2hTAYfr$cVY!MZK`{K80+X>1M-kPEN7 zS=_*jr{edM=B6|)sHBb`%{=$7XQwU?jmy&Yg;atM zi{RQqc(?F!YUS~Pk+FJJWRk4hDKW4O%|7X_Cg3%*d&1{oTOKj5Sra+D+YL=PzE~ci zD+`Lw-cc7yxCJufMZAd0hFs5qgCSp8tDHQX1pN0VH0*b;Gt9Ot7$6`D?iBiZWHLa9 zj%~~uJIY6ASKzYGZ&-;oZ;iVF$-p&}Ir3&!XnOBqbRnV{ln%3R;nkD_Ob*SPek>vp z`ES%?!a?kdW3jha+YTX!=el}&{D;~#YmdKz#>U1XBR%t&F+y(3D`AjBWX)vM5~B`V zs6DCFsu}3dSc-nKpm5btka zH#fWB^xM6jcXEDhe!xT@oKwufLI6s?SI(;TgHc`qwbP6B$f&V~*h1Qq;uKbYux!e!&i(Xr=#pO))v0Bz zdUU^OYx7EjRa{vmF1)=zuEi7Z7&0zB00W$th#N`P8EBXsNFvN~lnhP4FihMShy_Z2 zubo0tCsNYf51Wf4jJizU=TOfi#R14!jwBqx__Ta5W#1h9QHd2B)hWl$b)r^dXj4~8kOSUb+w;l1C=yaIKs4dy7oAckNre=1Re&p^^j zAbdhU5{bQ(5he&p!nRoV0t-JMi*hbX%mzV@G1UnltoKeNcVy)iS-5fZFE0J#JQk$6 zrk)*;k=;{di6?YM`48{BlpvNSxj7|6+rBIi2Xy5u%laUgic`L1PS_Zs%3Mo%k3VgeCcI4w%o`l)jK>I2bFykTzliBvtD>GYn-&g&{|@BL@&yT| zcnijY;!AKmOu?`_Z17rw~vkz2OeGkn|p8`EUQ;Pt6nho zdj+0oBb`>=7g)-yiL7WH(U$0#g*}@Zi>cY{(39Nw8~kADf)4}wo`PK+5owe(78?OD z3;Xm-IZc+otDQ!?6G&5-;y}cToe^{tRFedtcSgIKf!?WQICK{;_;LHGC>Sr`4`p9H z0=zQeFjMI08zGjy7%1V<#W+eu4!dezyCgCU``dC-zn>+9Ec?`g`WIhJQkLb^Ll6aC zn4P;e$(<4ZkBfJkEQ`C5%E>fyjY}EO293Oe;f%^q_lj47gh{;w@_-SKOhZvyE5>;Mr4e|% z?Zz9qvvWkGMiYd9&a;1m2Zy;)l1T%zHD&_x{vb=5bd^=pEr^L^ymr&zBl?JlDn<}n zHdO>unPSZ5C3LplD;V?X6psQztc4Bk&tmG`Y|#vBj0cj~+B|S$8!@C-BR32VZ&p<6 zPy1C#HV7Vvu7Ml^`b&; zJSYAhsxtkHX2Pu&Iq19XeTj@B5s(@>UT4du18hdHx>P|Q)yvn}(bWTQ*!6xzz9?1b z)jX^ZA;gr8vlkI4xt(GwCrX+;$bw5bc-;(a?v+vqmNNmFQk)oamaxG^02v2J3 z^ZCRMv-HOD(YgCtnwnC)a)eLS)hLL(o~18Gep>fz&`F#38h9*t3bSe0t0}kt`#US_ zNuXOCf&dANO!x`>jbIJn_PH3rV~tY5=*s&PpgfGod=Rb|zA8+0Qt(#_<2UFAky!W! z6z~io;A*;V;MOLfrNRdB*&A3{h&#$% z$){75s8R217?a^farf5cpl%lh=2}<1yBg6$>|Z^VP-f1!ZG-qJ3zsQBQ-_5JSL)vI zmf!2D5@hy05DX5yt(F$5zpWDq!@eq41Z88u)P0u&T@DZW%P$7%5ibl`;swSXs>X&F>>l!pGG8fw(@ zGy~~PwmdC|v_At8A2IX|G}ZUSv-U}Jpv=5H#%jnzLPTCJ4GRn2HtfzPSVF& zzT~3E8d6`)i@IT2wl@oL@_mh{y`ja?EZr({uVbpy%*t(ulSfhcUq?9ltxtu%9O!U-!CPCr+ zNq(cdRRE$Ut%tJrFSw(FXYQc@+)U*D^yzQ9Yv}3pz3@8i;!W(#rEz{I_Vy1Z8q!?e zV3vPvloXVU@q-zu3tOau(+Y$3s;n4IjYxP2h)P|Tro)KZWfkh zVHozK)?pEYD02m{qK+`E3|MC-s|eVGmRtO)H;=JkcBmrDM~B z8($*#SO!0tgxkY~u`4jetaEQW!UO}>{MX_DogdEkL1RBti*x2BZts!rX6SO`QV#Wj zHygi|dFMeEZP65#>nnfigNTZAw)x*;>*qTOYt%8PQpfxtQtwus31+9fGY`C-3zr+m z%=KmkYL9#n54@v`k}JdRFSN`#R7PxtYWF~V+-_2|Dy+zK2Zw+ z%M$bIH{^dKcsIlX9SA?FClT`h=zypI|AFc_(EbC}HG(t+s#yniO`ZmsNi+6kEol#mnAl$nrZ>wrwhuWQ?*Lw|IntDYVow(9B>~ZOV8Y=v3=dnJ@YxR$rNiP zG0ACqlWSw4bkl__h8(xes<|Zoof{QE9cL^vP!n8|NL}|Ss!owDiL9@F+)|Xs0(iV!h8;ZZ4d;S19n7(p z$`d9~8J6a1M!MpV#fMLtXo7HJ_hV?p`&kco&-t$$@y}Uyhhy1ED94%L|!x?%H6Gkr)`unZJOt`AfKX4*_nq9qD=j1)6NE z4l*_LrNb6RQXj7KOpfo7J+l*+OfY_^yWIH=DMaYMgzw2%wTq!<#unkG+1^y@U=L}M zvHep~5|1W+v417kri8duXtk=yg#~b`{RNTa1G}m-(dHqZL5Q5MpM?KNhC3&se3_R- zPotDzEtSO(kt2?Y0|Dp^H)Hv_tM-~cFg7Btw+}1-U{&gfyvt?Lgf6ep^q;gfVCHm`(SRPdW06V7QuWF;VZ~Tha zh(l~B{Kvb~%-s3zBT4A9k-tukdAGSaa#u74p;G^{l=kuKS`2Wfn2({x;^5r!H+o{D z`eS~;vxnDVM#^=h!(AC+j4^Z&XD-`2Kn{jnZ6S9va56jXX)z_=mLJ?U;+T$DCxb>p z7c%5kQy{W%`!{qh5Dyq8U)^Zos11C-M)S48;~0Y)#{pgCS7XQ3U+I+`1f2I#M^nPm zO@V!-tc(w-QD1;5ma4#Of<-nB0vQYVPF6rP`D~jxH-!osy)nkIB~g^)lad_iow6%~ zW@Wg5DV>-pu{yS$X^l=!wL9gKB= zl6YYik8g;MB?%zJnyRhfE{Iz-xPPD|(Ns-2*gWpN#syH^kR!WMJ^z9O!_%b9x+@f* zsG#aRl`BJrm<=mqZazfnk{e9ZXt{cVbuhPK&0XO``Em3Y{DBVA94ITJ+AA%QTWboH zVyZ)@L+|pOD$iA;4=5|AS7;O4QFZz7NaG!Jt7hj+@l)q%Rt}hMB$tIdz+a$lv_QNp zh}KxD1O{N)hdr)p0>?*dK}?@WHUt9$?yTaj^g{9Ke|I*aw{J(&wC7pZ0q>ocdr+L4seq2|WSS z*0vyyKFrjqTj8&y$^9uaI!4Nt?|zSrn6yo^3MT_=RR+;w&#6s?u3YhWD$L#bsy@^ckr57dx7NB-4t4E8jLL=@7#;3Jz=gVSP*y{$Dk zZNh4{v_In~Q~F3TNjj?Y*iW zASuxX`UmP1Ud9D#YSI-z{4gZRd%Z5vlObL`45)tMC1@cP|7^)Ep0 zzWAQceCVbLIL0Qf7qYar*Kgx{AjJKwGo-L5&tS$P=8*QqHUN;ryXSGXAm9i?Wl&}-8j*n_rV^w z$u`Ez@sr&Ai`jEo?BjpEtvqLfr+R?-jFh^dleOv&Of80^PgprFBHZVusU`3cU5sBB zDj~|R*Eedv=LCTTm=wjV1H|1C9JwTGlfvhKBW=&p^PjDCtcu2*LtCO&;pvFRg*{}y zYTM`PE-GS34$jFy#QKwq?u%}(!W%S|*@$0;OgM#%DSBO(dLcN%m}TWmBc7>kZ57Bk z64yDUB#fcf2*O(L4F%Y3eISb_=6e47<4lj~1IJ4jYD9)l>qGuup)clGHa{XP5D+-q z|3$^S*;{%3a9~rqhAtbe$bgm7-rCGaHd0lEjqqicvBkQa=LSJ^FODT9te=9WpP$H(my%sAeV-K}0nFNKA!= zUg*%Z6)ck?ejn?R)h{s5gwP3&NtR<%l0*Vt>||KS>XROY_<1#F*wod?lJz3A(#TMG zGV7V7SQG2%0E=qWQ>Dy=>1-#r>OG@tbg`b3JZlZ3ZEQLV(Ui`x;@uU^#qsFF?`z7j98`ZeO2!>?~vN&x6_bhm3%yCmtEOQ4%oq+u99MUP79j zN1_+b_bYo-uQ(XAL(L?+bECeiPHVbg2m1GhmEXC4T zv<|^f&}*tVH?jVVv9PAHN2Wcl*P~J@4fui|re05}3{btffrs1|G8mHMsHT97CRoN_ zV(qM zeOGNnuWIsvm5LdFi{!b}=Rm?}=vYW+8CDKY(FL{w))yd&R3<@5Qjlw`*zEwjpn=al z|J&Eg!?H+(*rgC2AeFR`m!HB>I^X~VI8Du7yzu#1%2`Jbb0J9 zUT^AE|DF)%_?Y(u55gP7hu)vV{;48J&j&3?iaB4f5+f=Ke#n3tvmXe1h?MbyO3hK^$Z&fX2d6x(0Q75T8{bf2DgtS8r-Nruo; zhwn_8#xk(=MKue(Cm8y|tL~4-((LnC3!S1b)Pib~GIp2`(254i5HE+FjlQY5=#fLj zkz=67DNxNLB_WNUxXiE*a}#71+Wq}UAnpz`qx`KDfrtL|UTOk(P6Ek;H+bB%k~k&w zfZD9{A2R9^F7y^vBlp(&md57JZdthax^Iobw39eUvMB5F_b?*hf@w)ayXgz_6nk>7 z2-gE*^5Fwi1o~X+SaXxM;iuaQwh^XY@A_`f_eY%-sVzV=e7H2BEvT~A;YSst+G#;u(OsX2AGxurCzDrt^ddJGTbrq+!9l8{e@KWW8aC0HZAh0SCZd{N>K zrg0f)BWV%U0{;90tsk8Lr(c08K^#K;=UQ;QoejY>5S#$Efh1pEp@E|0`lU}Kmad&fC=mvjkd5Y{VJbq zO%*W9)vA0G`Za0r?}OFQ+nbemz3a!ztnJlq&36BAbSn5#tC8;Ip=csiz| zxK7S1E8+%T{&}}_k)@ycz-Y2!Ij4{Pg<-K zwzbdoI#vArJ*(&C+PD8ohacb#s%COqG9g;8`cR9LlEBJE=ikY9nI1mN{wt4VIKb9% z{9jUgB8uARv0i;{yiSowF@au%pIPo5UXAJ-SrBLsnJG`3D6VqTLUc^f76-mZx_y;z z<^mlc-7|3Eu{Uu{E6kv#P6pY_P1l#!LEOK=eR#s7Bbo+0T36!28+jExJUCxgs~W<9 z*LkXQd30Iph<=sEh1OL2AyFVlE+eG`2y_SVlP_4Qdb+XSjPeJ5@?*;ir?;&)e}dhP zcG5#~Y26K6e$n9G6|qekE95%SkG~4X zCNJjsr2&Zxr(q~TAPx*VK^>0rAk|wVuCw(i>o zpsDHGCB6gAH15TeV_8~_HafLWM=4E+;{k;a+@uQo06QVo+OAjK%CPCoNLRk)()kRq(&0Wy z15=ShuJJ9OH);GYk_*Fy9Z_;s=RQ#eB52HPoZ>LtD!0_xfhplOR`N)@cSL++k<>47 z3t>3RDnE+zb$5WO7RNmSlb-Up-uARF4ZnjC>N#i_fhYn44LJLoF4O@@)+86H;%Qbq z*GKUnXsjD2ahRknm2VNPwekS49Gi#B8?&gP*Mv>HHAzYNfFN){V522z&Z|cHp>)Ae zNd26iRp=}F34Bv9>A&A=Muo14UK2;E@esA4=f{1EEi+V{oE*U4i>=KQr+e^PyRP>L zv!#%*TPF3Z2Lq1+#M?u_jG|NpRaU*}{7U-*b5_SwU2=`a`%zf~BcKAXp$oIdF*>si z^?*`~DZ>NoQTfI1l06j-#Hj8%ps&(n-ZLiKTdn3A)Mcq+8T=UokI4wUR(1lmI{W41 zwvx`&Ti>scre*GZg|jV-)vvNJ27&Quqwn4cW?Pht`b!j+yM(5ibMsJG0bPipsg$v< zwlOF*mFUE&3{m!6ehUu3#-Do>!n1O<7<> zuRpPNEJ=1_(?fXngaJ;${+V5Dj1R4(;?^{N zYTL#TQt0c63}KmY*o^D7vKyHFO}4`p>>W9zWy3d%PZ7d1b2kXp_EgKeU&636DfOEM zA2zODE%plMa0^Q^uk&%6!+kT5A|Y5&YsJU(WA(JPi9eU!j7Rv#N&=QY+LSAMUP|&g zEq?;|?+rVr0=)R_M^py$V^bqdXKqBsODFt=3j=Udny?;VLh8KHgd6LlZygT&4e}cL zB?J|HK3ZMY1@n(b>2yTXO^w&RTp?@Z8m>e9N+)_a3SQz0+##=3{c~a}Y}?Ceeij3h zF|*K{bAFYN7{25!0F{iAh7reC*6kKQ<3c#xD08pB+^b8 zcNM^R`_M)v+{VU&c#hW7V`?DvhNtwB;eJ<4d|xgF0`Zi6Ry2@3aUT&&kdiX0hEO)H zkZQWI_R}J(aO6nY`_aT|&bff^DN^&T>kfIXdo@)&^Ye;x<63S|@g1*&MeT6`w;lJncxgcjfj{pP#)cj)y{Qn2y_@99+;l8_M>AZP-M>TABxHORtLw47<h(mH7%fb4bJ{RBAbS)&K!d_JC>t}{tcux1%;7p%`ZS)WGdKq<&?U)}7>Y4y1c-mTW zAbs|ee&Dxn4B>dQCN*EE@)%?4PC}gh`8^R*b8J*MZ2@rsnWWE z7ur*-8T23&>w1h=KpBsF4-(KO+#b-uv=rj-vP^i8GG1Stq@qIY;twJH+4y%R#@NH| z?T4!r8shXmT&{R*F5h(Dpo0KOt^G?89qjqq|7x44NcZqVf+j%nK3W3+=O&{X8@y(9 zz`Y63aUJ5_(8ln6qQ9jh)RC&U)#q%Vp8uA!U@JZl|7D_wgCWZj3iPXg|52{ zA@mXQ2yA%LpZJziQD{W@ho0)r>P>wghlQ@;e()+nrpktecoRoC=;grJobZ2!p7eAt zs&7)E+}n`a$-c+~Ym{<1gMqcY7ESOoQZKsWGa8BaG9qI z8Chd)<+5^fjdp`LaWKpH_)?J@;N%veh0zLSE!?Z*KShK8`&%M5$lE}@Py3iM4iEJ}Wc7UcuUo(saFcw-J%t=VE) zA7MU~*vP!SY32fkYa-V?702-FBeTAnrO0W_C0L^4H8VetxIaOs9_P^nmkrqD`14lg z_|C)vzd60WX}G`JD_eeVHbDy+n#jyYOGn3Jfb#TCnTwuG2kQWV_dclhGY;(oi_Pw+ zQifi3>yzO{ui0gPd{i!fn#i8OhiT&Q5U_sdg_-y5V`T%-oBbHS^U;j(tS!s+r4g7c ztlBaf{Nqx`TwUh~lg7QY>L53qA6}WGoMJGkhM5hEGWgznneiTv&Rnvp^q_Sz|ZXl}?LN zxhb`37LEWP*4`BS*>bBL&pB>iKWp@eL&YfEox0Z_IYEQC;eTsli42m-j58(47azbP z1c?fGd!ARrc&vHeeefTxVM7AMx-?%WWck3>Yd_a(&LUp;rzi7pv-yhUHw{IB>*YG-5F*Yp(zZ{OF^yX|w!Ykrg4zvQe(U^Kkw1 z1N1dbzCy7uEH9OWnd=1qzU4dioyzWRxYNT1i85Djsv8VwvDXCMMBjb9Gq=&lHZ4Q0 z5m#@RoDm{dXwP1nisX(nLDR%TllxkIs-2U2LPjRA3@0&z5UN-64{Z}%iY2rTV1GzY zrCWe*t%5-$Fo$MaluuGkRD7kTYyH%b0G1yp#`{sFk7J;t)+=Z`;VM|f6$sEWqAG9O zF=SWZ2QrN+;NpTEP0;;!#>SoUA3kKJN4FKplhtj!v(FeSKNfGnvpmAA&oW-XAA9Z= zucwJc#N`|x{&46rpRL%d?7tu45d%F>yHS9LxO4~89;y@QiN;+;+6c)@w*?{Z?Wh?n zW*<=Ev*H19J`gP`v&cqX&ZbJ`N`Swi8h_ zZ3R*yqof1xCnk{O;SH@rN2b07JxgARtgDwmAmlGw#6CSRF_LhFQfW7AC>N+wF)IMj zNoh{s!N4Rn4Mch>nOva~%IsbO*iDM$jerbaClKj+Ki7)~k4;G#Rx&nPEN$DrYoSE( zyYUi>?+6yLNl8Us>m5?%{A>b=yitg%{q{Ir5R{`J_0!eS3w?uB^pIng#s`H`&&79 zwFQ>?LGkJ6FvXKREzlVUbYSL6R|CMb~q{+8&G@QreJC$Ws^xG~+)itk;hD z*kUWwiu`nr0J5;?$~{szE&<-z?=#YXK<#@uF5BU0);+0d>!VewAGoZS*u{tAqk2vF zv=>ZvI{K-AL>>w~FAHB;1E(uX$$?E>*tSVmq-Gl<)Laq8CpL|wnHLaz^Db6c1xn%{ z-CvjoGCsi*S_IAwAEBuSGiZI(?QbkBB_CUP{uBHfn9PlP-SJV15D{{KagGM~K>mb> zA@=)dlnSRBX>J}J_C&BjO@jY>|w80M;NazNmWf#EL(!@>JXrVMWpHwH6ex|+{h z!w>DmfcI_N)XbD=ehc_LJ|t$M2pWu*&@v~jP!aivG0|-}i>~x?tNhvz`q;_n$JL=! zmXMziuh|dknXQSTrKqfRX3e6^z`(AeNlm5A3cZbkd)&_kVl-~-72jfHMj_4~s@fTt z;a-R`tGY$yp2De0GHeWXF%|ErOX5pgdz>o*4+vm49uEs_bOMY`KTtlYjW+#jZN|gw zSW!jckusA(%LELdn}b{Nnt!hwc}(>0)rnzWhw4`@CtuAaps>vbJ>7w@Xsb$!YG+#B z)Jz|qQQxwB)I7||yxdUGslS_@mzTq>Z>8}etcLH&H`I z=4tokuE&1mlLI_(gJ@*kgj0%3W^cyol`a+!+v%QkoNJQv>ou2h7vJ*NPF;0Hbnzd+ zl~dIUC|h(yEN0btth<&1k3@r{8W;J?>o+eSkFsVh-3!8+-YaI)Aq!{I+Ue_bZJK>l zR3bb-y{$P?C0e#Cp$kK>=*zhSb^v_@p|zF#OC4SI4oc~v1`e}9 zB@;Pr3{TnJgtSaoS}j97f>dk{?<+Q^2piH z59ye9l?^N+7DNbY(m_NCknqr}(Me2Y?A{`h>fT-cEu>9D{E}khm#RGr%DT>_q--g} zLP+8$@PI3S(92nemGh>wRR{7r)rv>es_o0M!ZN5`>01d!JE$!V%axS(ibc)!3Z7FG zmYoHQJcp8M>_0*TO)Vf=r>=E%u?uStpqt-6RxH_H&NRq;{sKc-`0p5j-5iP}j1s?B zVTu1CR|^M@@-b;;+JzE%pnSz5swSUD-P;^Y2bC_f<00W*0p;UCGLj z&i^@T3AgBCGSWl{_SDzOlSH_8Xw!|xk)IAom9*eKSwmI16eqZQtt2*uMpm^+(p;{r zqUry1ypKRZ!&a1GJ&M0^lPAL;}GD`+Ve*Gu_ z5F@7MxRF%&LNug01fBIYIq7BYVysZSXaT^-N@PdTNEW~si)N7Z_f7c1(~b9M7IW4( zsq7dy8B;uPT@ch%AR;TrxT0Pb%uU|hh;XN#LC2J$A_i&YBK0688UAEg+w8<#W=P)kWz&kzHk}!=UJfa+^ z5kJsGkPH)6!okA6yx>iV_M`-N- znlG@h6WXflzS;&Hx_~*6c!A`E=>S5B*7uGfzdc{TAa~t${yka$@fe0t8N%7n8i|K6 zct4HlSM`uE%=o8eGg7yb)0)asS}JG7k`~HcENy|yi#kDXYnvTi8QH2z!t^KNPrHDd z=OjDxxs^aoZD91J2mR@Vf8LByWoi>LS`>68S|ewJcEw5JJ$wfPT}t5ZGjD(Z;ino= z5TP3jKz$VX>1!SO6uTYX7+Wbr)OO20G2AaNqK;7ngrE7B^Tk4c(qN1llBFK4kyLI7 zGofq`bDuvg0FiP@h|o35?N}W+_lPEANiTi;y`uw9Fl(GTF7}bqu?xbg@@lwe)c!(# z@BiW9V1Zg{mysX;H>Dwr-PT1 z6Em@|fO~dUD*N7#q1M8=UTi*rXn~qG*OHcV1ICbcDNHBvW-!dErnsq6iUzNKL!uaY zZ+f@^SEJJG3jhvbLxYQ-E-pS@8;+zd&RY&6Dlc6^ePv*Y;5mGxi!w~;%sv})v`*HT z&Km-(WZDvdW~V!*r?o~U>P|~XeH>&fT4ioywkBRXeD<5#0Y>4>Js+$+3p4TSIlns{ zJQ)KcCmW;|bm5;Hf9GxYB zW)()87{1(js{f_m-@P!YxyTI?rZ_>W!v1$XsKFrOkZPyk0V|R)30V|KxzAz+u8T^w zL6O2}sZ+C?J6GZh2sTqCPrLq?Z{BQ|);|YmKb>yb)4i3vq*+{TQZ=9=W(}(Qk&gF3 zoLatAO~4J|_2{j|2#v)8YJxEObcEYbcQ#21`UWFyITpE9g(DZ8x^q=1L2IcMP8v`h zziSL(LD!0olzO}Gv%c-N^F&H4N_FU+HE7y+%i1_xWqUs70BKie_t#$iO`0Zj2p~gqqEJD}y+pHz&Oy-ETZ$B=Bq((cyvzTE zxq4@(%j^AX&kjkYX28`M*oMkYHQ9Fdf~XPlFY!bNB6Qa^U3Ldz@|gAG=(^CvU-Yfn z2Y}hzKc24K{hPwEBAR^=E}bjq3pUKOyvs#Gale7{n}w*n5@`7Y=x??sx3`gBEYz)J zwwbTg@*k)dUUU!Q4o1%(Vwr~1CYXNY^|j1J+E;MnpsC94gx@e*p;#w-wJRumeO9dq zVJJCJ&b_}@&BJwyABL^)DM*0pJ+MRfMge_-72FasoVJ>GgSNTFlpmGX5@v99-CT#S zk`f;&E;d$cO&=N;5`@#;`MT~pIIyIlcD=iu*a&HAroxw8_$}cG&Gqtgl3{2ttmXo?@IXI#GtkBUD!6FcpTqk+u)|HA|$r;m>lzZNDV!~4RYnpg#Y+DPJ54_wl~Td^Bo?LThv zXZB#|ok~h$l$3TQygOhaq~lvDi~uk5Bl_QCZSZ<;BS;2fndVRsL0-zve{-i!U+uLNmdY1?1wh zTSv(jQ6m90BW0l{UBg4M1X;j$vt{QA#S@)n!o`_P&y0rj@##DtME>Iu=m#8+|0cg% zlA=`p3NH|lswBKrRyEALC=wCm-H^N>UF?bVFbuY!{D{~Ea2_3)Ir$3vnyxFRv_JSW z$Y+GKGvB4|*Y|ITxxHoS@}$1EG%xnd7P8xT2176By;clCM>Br{9mMSpWF{wcO}_Eu z7~0Mt=P7>uA43e{|3tkdPLO?`{~)Trfd8-O@p~H?8xYbc3h7{nk}sTBwy~yRJB(q( zFH|So_^?5{VHM^`67L~tkY)y=^16!Mi1$k!yVy=${`AHz653xLPRcfFH>Vj6Qc`I} z!1;!bZfdBi`lyl$=Y;gE`UGV8n|`xO<5^Y_3kCBDxmEIoOEyqB6BjDt_V)Af;Y)jb z&1&|?s~G^!mzM4IU9an~Gk*0T9$oo>^k-CssUX&45Xl4y()o<>%FIn{gC{9{FxCpv z4te^Jxf_XEmw8K8Cz5h18rd)=x$rvU1Vjz^}{tV+kp5^q@ePm>Sse`?XgRR+*TBBIa&S8NG=_}XJsh*8ozaz=E zXrAQ?G=?|P7bY$m@FAW-1$(a^ zk1{Ad2-)<*&5_U4k3I@cRtgLiT)GcgNR6fDaMfi0r#?t{1s8k3j!2_4dr)mXtXQ$sh^KV7sUO%frFpW_xF+PX;XkC6bzU zgg^!JPAan(KEI~Jl3U%HL`8*WAU;pyHwoK<|6*A?nw4!9H<+uFo1d2(ewg+^Zeg*J z6()*=_JpDFxX;I}?+qRKarrqwW(H9AtjR|XUiA9`DBY)5g}a6S})Y!@57ib9O#S9V}L^hZoY!vq~bEour((<(FPj=Ewt| zkMv~7hMI;Obk%%GlQm^q5dm$%ojXlkSV?X;d};+`Zi}j!UP$g5^ilu-IRhoo+4GOr z>{;YJmmN}eN^?2RlGO?H6w>eB>R_ftLE=6#wRL-=1YWFdp+Cn$S}iGwFpF?A>Q(y> zX_E_o&@-wUAHl1;gGMWMXAN~BIfdS&Ku9NB{t%RpG$xOf(f=wrUm|+s#zDJJs-6@@ ztW6PTBys+RdnVPQYjmgs1VGYo2H5e_2))Vq$s{7rBA?bL{fRNH!LJ)y8*rmCm#5!d zEGv~*hUanIK$wc%>@F6aoh=i^744xMU%{7hL3ewNlI|K~Em}yW_QsL4mg3Y9h&CyD z5f!vQ(A~#6;oh?{(ml%-`?8%aWFyqepc07wA|5+SmR6JeVjcP5EyuId%~NoVsUePA z^~g4Rqn+aXd)gw`(9PWlSu0rO8eX&T$Pszl7^J6n90 z%uM2cC3xwQWbktmRg{RGNbEpyV|3EPdqxaylxEt-$y`2Bg#Nuo7tq)96>vbab`HCm zYx{}|9MsIv{HeU zI(}Rb%6gLwl-}=Izt~K*O(okE^~}mVC1pZKs&yV#5fs0sB_gtvk_LeyM$^Nu#W1=f zVD({k37iR%vMqp_j4LOGR(!@e6w~pl5u9SD?OgpXe~CM2gXzWKP)u6#ISBqT9SzE` zwE;2dc;SZ9W|c#L8QhFt47h13F555$^<`O8Eb;#S*bocB#tDndFh`gV;g&n!x((pN zLwfud`3jEjhpIG15!{9QMeLT=i)LX3{8l^m-&*LYwP*khF)0Lm>}0`Bc|mfxhIt`I zSY7_g+L~dw)CO@!`f;_cVa5&>d6gqSC8{0Cx66EUx^B>M zCbTOOW}Cj5Ducw*yNjTOzM%(hh{4uk`cKfh0TNHb*|VI@uc6pcPifwdwqZsd@Q?*tW?IC%o(a~pgz|PiX__4f%U1@Z}1){cvPU6`g<#+y4S{tF{7YA zthXHVSoBZ$^YM@X%tME_^~-?74lPUksBMuJ-lrANbM zf*WQbn$k(vI7Vgwl$z2udU)B{PIO_W#r5#L(w%;gdju8#tyJ4=@Ze=9Y0M9Gx}H%~_z5NXb-%S|moDb}U4w$WK0W7ydN!0KaXmjd}PqDQyH_un86 zmvx|io)^?FVeC~GIZnFH;7dqQ7gr4YpQtX(xBkqGP&NJT@)0CiyWrgf3 ze$B2`zegTTl+lZ?a54H+AyUKC*M*15!z{N5UL|P&4*Uy`JEa2RutvaOUA!EjW5Dl? z?uTBMdB^faEd?Uow8DE&^zqBy96HFRITK~2`e>3nFh@i%A>1xdf&xj%QEYRpy|ARc z%dG0wFQHY+GxXb(XBZiGYykZVw~qr~MMbTVxc}r_!CZcA)uJAG#o=5-yQcMdhSIq5 zqG=BR2vh#1V>?(IE?f<0srkiyI8=d8+U+kU>lYLqFqnI16wuG_ymJ z>ln2g850*jbB@_q_3Ugnk-E!*B=o1{!1^}8YrNEafWOc`=35vfbXptnQDIroI`Y$r z&UaS$H{WlhofpG%?Of49=La!wI6#{iO&2GQ5EdIkeK01X2FM@}b!2C}^hnTpZMZ}l zyiF`Dq%Qf*?(iPtC`L)L)j@LS1@Czmsja0TFr@d4(K9eeOMQ?zdniUWgD?&+j65RX zh-EMmcu?ux-e)njbZaM=dvb)ga?8a%N5nd;#m}K~-Sw zOC^06k%XeTF%Z9liM|Y{6J_geE=yA&$}Gjwd_Lua_MsOUbnde-*Zgx39ry&>&a;Ya z&rVIPsNL%QNu8!*$qSR;olB(IKCZ;Tw0JlrYM_@tKTGrdqHr-Pp$+IJT*0V45cSfKHsTvz7pv|vN?DH=?OyXUZ}i?38+K|9{Ilp zVdGO{MG((pSVgvwO+7Q$6gw>pSZ>{#6Q0_spr|wRW|?a97qmZysvWtoVFtc^=EtW< zQ)cn@r5JBgMqH5C=S-r1W#@qaPkO+!Ua9y#y%<+1@GZ3&=E<9=b!Buo+itx)l|&U= zt4m=|@v>lON4 zt#Hn)qFIuVpiu6tY>q!NK#(-+!2NtfS3MxhK=8H5X?SP{WwGXE@XSRgLK)3N^O{K| zyV|35e}&k?A7lIJn^%|s-|n>`mr-TL(Kulx&#G%0FW)C7tPL3bX^lf?r?X|r!PEJ> zzBUosGxdqGz_Fvm>k@d4#VGBZ3Fx>VEi5!tFdbOs(5Oj#*{9$qz}_UwY>chN5o;k; zSZvCY8#=36vze_awIn>6azFrnge)zd4315eD&5YS?Cc3?`hr`mN%kB0ZC>r{I zAg|!u4exaFAJ0}45DSL;g;Fwg!@|}4hP1RDehr{>V+X<&?SV}Q%j;41>H{c#r5ODW#)}ZN-TkfV@0T5ep z%Id3q2EO+>GuKUNqG@{7xT3jlQs8{s?sC(%UF6tOI3aY)MLSxYcFi$opA^*@*|2NQ zzIRv>`;;<7c&e6rmerR%d3ENTF~#z3b

nZ999dq>FYg+&<6tE;IlQ-Hq=tEN-f9 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

E~VrS+^#NK0=)O)T+7O(^ah?e-A? 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≺)L-@_pBh**x%oC&VKSFSJ&}B95Pp{D%NAGQ zn$*xB^d{?=)UadKFF-}!!1*iS0kSWvq zau8VwOaPq%RIVQ8_9_nw0ql?l8^cmw=Y zg#3bl-vsSed*t0PTSx)O+*eHbbPUQNlVTH-$Py2O*iQqTEI!8`4NNIa-#T_+^2K-1 z>{X6vn$*1)(}!fM#h$TsZS;2laVB05e&ubyScu~gGOO+Phq=V5hOq7#!48v!0hT2E zMmsY-m8iaPm$7B;6ta9=wTdMsC4_r5B1s{fbSKNPzaM?Q?+90_{gN&FVYJQc*$Yki=XS z*X*j(emF}D+Nqxr`TTGuN{XK8CAR8TNmP5^?$zPr&qSq8cgGJ)rn1e@NM2=Xx&M>k z8tLJm4iR8Z8Bxme6FM7L0BctT`4xvmsv}un{v7AcR7nUb&;dTP96iv(89GW{l!jVR zE{_;2uI~JpCPV0zUhN4Sc+rJF*N=6;zcFq+(8@vFK6emz88e5wM{iPRkURxtQ)8_E zLI^#Ea#g(jPW-Ut2(>ltKB7k(E*4*Y$|8Rk<2enL;rz7%6l5+74Bt98jkQjXC8VM9 z8TwY~%A*shnJui&t;FT~{1b1FshKS3Gu?>^NB&@NE3WTgPjT`(x$% zN5HG1A!^~86e$naiiz^*VQ3zbXy@4<*_D(1Ne)3OC4!3_N?hAYfGZ*O3&B*U#y_^{EAb{ZY=|H0Xq5*NDu>#~3 zgCsWXo>#Z4h$hJ}%gh#H^8GG%+g^^sc)36~@+4Zj75IL4B_9!U zm_*~}1Zx(WmH1t4EXAm&jAX%iR49f7cYcR%7u_e^Cq3M?3crB{UnrX&5+u$hgmT8l%x}PCa znCZ&4Q;qp;#SS4QZcx(*a^>LaMR;gpx`9)2Ms4n=bR=4OJX%sQDm1(KA$glam9&mX zzLtkB{&k!!AU|Zy-l2pC8~!}dsy*9!9@g?H`wE|8*B~jRQG5EV=Bf${6Oy=xT zc(&<{XI4AoI-OCu&R1Vrey&08%0RvKGaMg)LlE}oD%G|mlRbKD}R zyqI@hu>m>0cGoJBAAa5?j0Kf;RTVGgDj+SUL!gmn0%kvVf2K3+*JD#fwjIAu8l8Z3 zK5A+0vREAFiE`78Z}v~zn9js8)Q>GjE`)2H&uL)k&zNGle9L$^XUj9*#QkmQ_bUqz zANY~r6M32CPx7WpIU+6;l{HQNu$Xlw>t@=lg91Qs3)VH47)aO$R9^?Bjn~(?3n;!L z;*~F@e=IZhl(baU>TFuT4d$s9)8a?rsq%HSh#Qpla8}zQSTs45c?8?B5hm)Hd;G>GjSgj6E>^ zV+fdG&S}3H9=srBu~D(?+WS56+?Vqx5ugd3G3lvyACr7#IoQkpvLQvdHa&afii8s% zp?kYa?J2}(Y(c4gE>o~g;Gjg6&~OlrNq|%)Q+b#3wq1sQ4gXBS{E|o1ecfcq8VTDQ zXF|ZOLMM`qKOzgNMZjE#RM+CjK&m3y00oc)ZkpDeEXZFYU+3I>xDWGDHNh-3TeGhb z^B|(Q|IlOp;)|W!1oFjumLf2jVK8=%_VS=FM6LLy!+QEyqRtu9mYpp+*=(1nw&|7~f<$8?8>V3~zZ&8p-(uJ!rJVk-BcwYuy4!J);Fkeq zO?%S&Ez{&ND&@Yml4IXy;{&&AJxu=^1LH|^AhP$?un1nqQ%&tty0JuViV%hNv?7JA z>3{qviOT3F4=AW--rXVf6$q( zzA<+Q0_mr~G`3&~_|~s8M<^D;?jZ(v?PtwDexf`2X1v5lZ8h8!=vC@)dZ7!}d3}%Y z_L`02;OnS{D-(s-Rx6GoKb5z~KnZk;nC=_epT~GR`}6qar+`NZ#8uDx5>_#A?-Yz@ z99?{C-eTgVrp|)rtoRKp()WO?ret||TD0kIH^{?Q`6IB^Lw%FRx!{vphz9Vd1Uu`@mMzoY79ZEnb46#~$Z%C~d5VXIU@k zJk^{EXdCy&TlfeevG`{8%@#8NsL=9jElK^EC?PI1EttBy>tvO(Rk)>*C$4B8oKb^KYW8%J>P4Hjxf<3Rq>U6{+_}{d48dNl>xKo1 zqE9lb732PsQI0Y6RIm+@!U;v;nWM+Kpq5n!_-#0hK^x+@yf5Z$FbIss&?jwI@qLxo zvDY9p=^Mq4pQX8%Jm89SN|e4UaSZ2|?OIRaC9Ev*EPKAqD;Ln+7EHF;>t)L2M%I8P z^t!~oVE)FJS2CRG=Jc0M0js>kK&V%1ViEk2k!f{@5vr^)L-+bP5gsuJ+dQc{O>eh|PFuR(jrEGK%WgoVgdO-_xvq0k`PsAD}8 zG5WyzTv+gcDKD&EG*8qfzbzZwW*zmtoe~F z4$7YfioI#k(qaWncF34@T(vg8MiZ^R-~2>-0>{>!a$6BYG6xe84I$xQ54T=+?ahD= zdWYCkv+eaD9ZF=gBbj`-c)QyX=sCN1ph2Zo9}Ng+a@ZYOQJDaBP8C7oWwdXZw0!qy zPE+nq!1C}H+$eZ(&bssb@h-mGuqt_wQ5E+a8y(DAMcf`hDzSIpq;PP?M5k=F3$jnp zZ%J%St^M%~M5zcE0O|{G*1G7Z2c^vZh8zk;_(ezyMZa|Q=L;i}*pRxf(rDQKRKQ|A zep~c;@^VVZe*jf#*pY}Zw=nc~ms(QmgL zSdgC|mC@EGOs?|WTle2-SiMN&pm`y>1$G3KcUC|1-+Tk;;%>cj^W1&wCAM2z{C9Lm zgA++U?INU5WN0cS>MZABqjsU+0%qcwmvMG^bv_br2a{M0(|5w-`lV56DaTzQ>P#TA zpz*2S=aR+cZgs#;V(liZRrf&N5I#Is?mom*USaEziL}#vc;t8rK}Wv?!Wjm)#Nz#R z;=2GgufpPS3QXzZHtNqr8wah74)74VKTPB{7I=;x_)+7Ib@?(uSxJK*qbAYj$D4_E zcyNqw1!wDirKXJRuupWTKP~9-2TkiAHkb1oN{iOu)^gO8Au5z3BDLs-j*pW@#lr`1WA{emc~xZKxrpHA z0od0US076pTMoAxIb4LKKe}*l&S|BMdc%BvfhMeeZ7Li+W`4p8%jFmLQFrMKU^N3s z#>z?#%znaDU%A+woAuWdpZwaG<>VBKA81S-wWhv>o^9~lmyfdIflIA#SkYan5uHyS zq=Sqk2pRkuN*IhGh}0m|Q8$kP@dDGrVDWe;1UJ$#{6E;b4tOfx_HWNJvK@P7WGf?E z_D&&WL?WAHA3`*bAAkWI2jMK+a@y|QKR(f{V0`oG8fKA)fX+|T#E?rUGy{haf= zpMdA_uLDz`U14~*O+q5a^;}5pO^&$kK51v=QT4+YZQpt#{nM^A|G_kY!zFnmltYJT zuj;ehBIHx}o+3u4X8B-+(eyRL#k{QT=^_~?>th#V%Hy!TF}F?SgRyk)9cJ04t2uni zxnNARcyiS5#CU*DOtCniejpZ%h-sGe$KDLMJZ zmwv-Ch3ex@Rq5VMx%uZ8GSPVkbM3#k4f671NriU2NgA;>S?$ElXdO|5pF8?EsPW?K zEDbTQ|i-QmfiN3FqZORh@8u1yU?*IL;T{ z`sG#|^Tey)`7mJAVWa2X(xywO%YGrwRZ`%a(Qp5KeFy6L(bw7@O9>cWmRU65+ByRoq#oTErOVueOLI_`!EFybn@om42Flg>uPC_ z9{5zw`hQG~l;_(QUKh4|wQR8Sz9jxhzTZcxgfCxeM!d3K4Kh5=A8@IR!ic*az`FZR zB4%3B&Y(^y1iSIqyVQu{SayxeW}l@NpXr(l^}H@AyHD}lb?jVyulkQr9^cgwiVaNG z`X1&XbZ>H=>mL&BFG{g>CYHhPri$MTZ&|YVP;Sm4q_!S*c|~y}WS^3PU8>fFTKF5vKkBaUE)eckRph7E`Cp}qyv6_ABt>gL*n1^0E%z#T zKf|-cN34}H&8j9^kYBSZQ6-1Q#q~~Xo&|Vh&G%H-KY9=MsGP?{2omH%;X+$(mjjk= z8eemkq%%5~MwzXuXG=-HXnAd;Qj?mX=8pdWtE#Q``+Gk99xKU@7Q;u`ZL6+FBzku! ziAiR+TsCn=BgCFh+24QIUQREMJs&o8_i@Aa$ASn8P(!D^5XT9m$7I!(x0_JV{ z;I1-FxeYre7Fs?kOkoXTHfyrgYYOL^)ZVgIE$VYzHC?-Xry@&Y>EUNc>{{sga2@}5 z^p0NcIy!g*;BAwVZ)JnId%nDtU1p&ex~?PrnRa_Vy^fGT>V@PS2di2~Ae{F82%h%( z^~%<7>|`%v!|4gqXr9*@u2%=@CVn0zWrSC6+V+s@^kuZX^pa;>+i;a?T?l0R#bcPm zxh;mp;v$G!W*aY>c>Mj(-uVZy`HmFuXPJU(Tea_I^8~t|7`6&6UU=d%rzgcDMMw9| zt*f_;AFDk+H+lfK?a)nd!MYYc8zGXnd(m8Z6hxclpGN(6J<9G3&>^Hnxkla)$+*$o0v$9M4p|0yC`eg%zHgV zs!iJ8_u!Ac^_NNQgSw#t)=!3JrW;HwMisqpzoXwHviUBNG>om-#KIW43{L`|Lvp`m z+GL%^b?Uz-v^~VCXsGFbC+E8_#W|`RQ$rKWnDBIZ{hn^s*U_P`<(alfccx0@8n&w| zFVKj1ac*%h+SqBOZdRlcNp;cHe}R3idY^c!WuIWLoA~S4XfL(A1xEB!+V?&Iy`ruq z3r%AEa~TzzdHK{gC|=x88t{PEG#oQDR?8|m>U#bzPsb2={L%+gOQFR%Goa%K(LB9u z)bnR=qv9ieGmkZQE>a)m^jZY(JL173MMmGuIlt0aP*ixhnL5>n&C5lw0+XpOdfQJNZDRUX2S!tU1S6bd){n{j}YT z8+Qu_t3*H5pVxhT&AI4>T$PF-T=`wauWh+1IgT1*G6k&1sVPzF<+q-e(x$95(6Llo z%9{Ni4q4=)yB8H-_ie3quxtt|=>2mF+4GY9b{JR4AM46kM*6Kib(lIzq$hd8kuUM> zcCMZTY0isMf(tjwz67NaL^EU4P2Q+)#M+H{QY};K`g}ac#^y(JHxK?5%J~ZT98vIV zuFt>NN7daqFf^#k{Xgb-;$?kJiGqu2M2uYVXl~Hh@WA>R^-x3NW)|Ac{M%>NR|W`h zF|D4h>)B~kl^h)>i6qyDN)YF69@og+eHhq5ROrB*T64(lDz8~e6-!|v9r-5E()eM= z^9nNc?t~jNho!Lv;W1s`>+)NYir|uZQx=hSLw?`$24RA`3zljYaCH+-Gn@$*3a(NH z&Y*7XXB%rd2A;f9mv4~2qdKqOn&_K5yJ`7pS9>FdQ6ftDW$E1Z8GnCOLQ&h$o9#+HyTo1K;xV*M?01$%prT2MFXG1*pZY#7q@a+3ctyv zlt@@=y7C*Hu+DI2!PmCL1^#2ZHTVD)%odiyaWwX9ukh;BOBE2@boTXJTm7(?7p~ae?iFO+IGY9)b77et=AT zS;hTlzoTs5Rooqgk*5_hb~6$?y(e1rBaG&mTJ+P!h--C8TpF&zADIYz+%cG^T%q*} z%ehrcqZGP~6>K4+d6XFvDs6;&a5q~?K``X~t@VjVJ@h_8yED~1-FGH>Ypvk|cguXL zcX3#Ei3VsX3^NHCs1}|KyMDI#RM6*NA!x~aY%w4?#Tc-V(KQ)Z& zs>dpctizA=1a9_>OjQz5$JBCK8fm)uz4+qm_Hg`3|6cR;5b||Q*XVtVUU>dR$;R{5 zc3g|*R#TKUMwcu(Bk+}ecKGM6!dq`##qrQ=Y0PlM+l31%j$w5PHuw(ft3uSA%}qsG z?}e|#-*AueSJA^9x%TLtl5Qf~H%-54kv-Slon^btTqf1Wx6({~e+V7DE9^eDG&>6N zmC4g3{6yT4v(|jQV=E8Gm3UI&7<=!#gZm0^fYz1UlSi5z1o_KVE(UKozZk+Vc&6SO zuTA_Z9L9X*xMw}0_TBMY=g?+~e&vIq#m|w<>PB})*|=+)q2~_{4}4QdNbYSabkdQJ z6D7N&(RPjo6`8gE7z>bTm%OXiLz;Le^>ed(T-11BULZD8c{0)DLSIO}FN+QXV?mMY z4fvmku|xrtDhon>@Uq4SSDP)FApDRiFMSl@DW*|QeiG8 zsEJ&v?V0@c5&uSn)*g3>nRX4lj=7G^f=NS8HH=8G;AaSYQL9XVE^Jxn*72yIh`o3$ z=66mE=fx`nJRw&Vy5{$tHZ~-TT~k-IRlA4PZ19)jsCTg9^?p%!XnfybPf|)Btm z=*X;8t=7hVOz^ybVH9WET_8Ac_|18igK=B2mZn!$EOQQ+PEQj*(cWF~gy%RO_uKj6 z9F33kb32Erd@QBHwUJx#5N)`b#il&-PRj9B#sj_TntOfCMkDn<+R}cKttQ;rxRfVx zf8jym`0}2ThZy5z!@KPCOdjkfTgf+|;r1QMU`LWA+~MZfjD2J7k`NWO)LARSY8rFtAUGgDdO-rh}8KP;cjS%;YRvO*0GL`mlxsp2FkRJPXUF$ALLIT zwucR61h}3rFfmQc?W8$uo)BNhTm%4zVlgXl?n+MT2eW`kofb&HOM7>6{3q=-^u=Z3jN^{FiI(coWlt zS>C3m)3Kw+_5P+uTDh~>dHm0+vJH~6v;_)>r81^5up%53( z+=SUT%4nRkEj3hI&qr1+!}*=Uxd|$ys>|yAY{YGS1N!I`BE!7TYk$?=SoYgl3z&8@ zr?nFdq1_uuR6^_0yFx{4O2MB^c3B`Z*G`C3RLc16X9tqi8>P&_6diB+?^-PGTkOqHm!!hYe%+_!@L6n{ zVs89B(#Dh`@21h}_M=O;b~8!kwIi{k^X84}L%Ab0@f3pw6~d)+RG*k-niuvgFX=Fy zUwhH?TGGMio~@XE__#xP!4ekUKDvym!yU%5&5KxE0x@9OSISC zui)mJ##>+^vnzWM2^07+OWveK+((4r^zD_T{ zNfYX@3#Q^`W=+cD;Kq6nvtNT-z8c$ISq!4uDy?!B5FK{OzY>*eou+C6AH#kVf)%~W zN>lOmn#ufh5KS6Y?8Yp!7iae3p=RU~H1*yG5)vYP(d_U@f+Op_&F7~Wj^Q|s_WoGf zL)Y}rZ`F7oJw38;@C|V>QL7#;8v1d6lp<%sBP~e79oHC7UwKooMaDFc$HHN?pSz#O z_txbux@aTW+0h)=9!eb6N_ezZs8%=ML%!;8_hOkVMWvsY&vPy{i@Gj|I>6=r@aIgF zd&w}o*jott<4MAm_>yDNsrKB`B1N2^l7RPSlU#wfuwWGi2K< z#U8M?9c=uies{JZe}k$jk4`7*1#SxX^y3$;=^k>5|bm^qJ7k>G@%;Vgh7yMqo$W~W|Ctx1xr3NqEmr{qTf*#)v z8gdb7knS=Pu_~ywY0i}nC}ixP-WnwSs2oM`?R&QkJ2t1swSwgksp&^lERQyGV(hHq z+wbJ}?^Uw!4jT~}mH3A-lV=`3Y&Ie^%#Yl`wD0DEgS20pf{EB8e zG`SbfeEaUYTI{>q?|CD$-l>2ZSm=4t={g9c%l;e02MT zUegQvat1}ixt^|>0_erNu6jmd(fJEz-K_<$@7`-IgoKx8?$}BQy^n>DM781Phz49j2N(A- z=Yr;v9Rk(vOgKxe+*xAkf0-si)r78H@r1^xa`8Fq!~C1HGT6&gQ8SzmcegzH+{m~o zNM?u}M6e|3?YI}K{aSz2!o$Oiv-$0&Fp5hH-uyUk^j&JM%=3V1XJ_qLxcE-)(oHeK zjC`gFi-_1!p_;oXFMV(hqAG-M@z2Q?SFlFL#85TY5Rrs3$}Iydx*p-Gt~X{nUr1?{G+Zv@bhKaZkzQH4+oAuAHZQzN-fR`TgKNhJZfD z;!~-QJMX8nzDk>wpHEUo>l5CbTJDD=7RYu()2oVlCTZ^I6xYA~)Eb(wbaRh5aUv~zckdn7l4@@jU9xA0$oaiq+qR2d)$j@?d;q9_^bL z8}&C_;2}xf_ln;+aLBQvn;|r|s}hpYnlB=(rEg=!LM7snRDj8>TSj zQn#xnrq6Hep{?^hrKU+XG}iiRev`79q|xKs_uha=vD$coENC)MJl-EC7Op5F;lu0LM@qbWVH&A!(H*6ixSw5%X>ZcS*LxWE- zZz{|mY8J~gzR~m8HqqduFXqUJOgu8Xd$iaQ7^xN(po%~7wpj1#R1Vqj@+}Q#zVMIA z?IvbCQ}x%d1!yZi6$Bmf!<(FIj@Rz?xxF7umcNlr>wS**(Yw~F#5T^Fj5~QBH%P+@ zB$Bu}J6`-;U;9kHPir7^Zt~%L<@vGP4>8R|-Yc{0@Hnfuj;{5MZpcs#EHD2J;Zt0+jZ!u`V3>B5^=;*6CSE;wAKg#Ti{{7Ssc9v&3LpH*8YH}>>X+7zv^x%rBtO%6w9T}q-~W< zzj$P%2^qi$yP6szP}BIeBH- zRU{!xLZjSaAFk@-K!(Nz_%Ezq5~;xp`$JRq52mEH1PvC>S*k-kO7N7^{w^8FSn!I}qT0u9CO2b9|BUce5XV@w8qx?9qOy z+e+O-g_y2X7RQMitGAA8yUXYpDX}-zS2-_r=!+KgGdU%jO8mM)e%u3Rf4c8dc(8tW z^)v6)cimAV!}f$fe`D{3rSUkF&rz8FjxgAu8FR#3|Hv_hcMiUPJbA!xseTT3?5SMd z%kk;P!k_JPM+0i9cefr++mGeby!#;`8$#UYwamU7Vi=yQUR~{S3Q(-w)3J36 zTj>>l_x4epnFsGw;2~PUx0hoi<85W-WAEn6f4^|H#MP5Z`$9DI*6ra$K?tvXE;IeB zyy1vAbSELwaTNLD?YAVZ?YOnH^vndu;w@D?!>9P}CIDc0i z+u`w!H}v87EDIfB?@jcpnT`{=TH;nN8l_}H0W2$36eW%MI8tePQ>dmdG+ebJ@%Q<5{Gk8BJqldmaQe?onyUzF0F_Ds8 zXV@Mwp}wJcsqxcIyegKldaS*cj|}C)DI^a=M*G>DOyzpM+D_l&!>gprabP1_rYPJi ztY9DuK&xsp+kmYG8B~lVYe#FN;SS{GbEav&P~l&n_{@uolvMdPhMKP@l`&n>q)+1r(5Q<8AGYfO_1snhPl zTR$%tdR7Y?>=jEAKcW*&hQm!*FqpH4MX4C)BG*--$Q+Z@Bo{8Yc9P}_J3CW zhJx;q`y(cmbO8(In$mA7@Qp@gV-`i z;p;+wEiB4oR?8zch5dVdHTPvMpV2MXnCn5rn7Hec^WKz@XuYh=C^WXAU8gfkGX20Y z-%F=bN=_-yGrOko`l947W8w>>?QH|@Lsh2o59p^DYKK!kKTRnSq3L>6us!Nlm?y1_ zFKr_=mpaL%x?(>j#f_FPu;H6r4xe66cz_MT4Yr${Pj;z2-$~ zvCNxs0;6XG`?Ga8+%wx4(47q36qVk;vXQ|X;k%#27%7vYE%&}SGSv9ndDmRKd6(?n zgfT zQ3x-Z^E1|WEG@qGonhRnGt6XqbET>zXWTRO>tzM{h&3tQb~LKg`OWSDn7URnKChao zQC4K%Rt{fim#>odhwonSCm+z+zwn9o(4~ewqr*&}%5TA?HFppYaZEr$ z>JOd`CU?9fCO^z8mqlEHr;F)0FH3a3n0*#)8O_J8+TdX|Q0sDgyD%_ACNA_FhkVp* zIR4m3i>~J|#7PalX>de1nd0&(?Lk!1FKy9SGIrOikFwih%Nmu?C4?MJa6ZRiOD26V?7mSS={#SO>vl2OW=`?D1RcxcOCib^a~MB5iBl{; z^Nk$!WtDKr555J%GHUbCcU)`CExZ5Y9M+v*tK))15nt19y>Znk@`Gmv%;)z9Ha*eu z3c7*ogelh@pH#50$0L(ac~4Pyhy`4(9H1qsD^t8|w-#-K8pW>cPG4nQDs$@=Ge#-nha09wkeC}#< z4tl`j?1U;bw#S5uBSa5%JnKUjKSmbZ`+zHaGx^qLx}wjGykA1tht{-r+BZZlZQNeS zQgodtdQ!n&|ASs5{L5ql`WP|$)08z08|gV51KB$M-kSPexy|_YY-_mRm4Pzb#l8u< z6}g|>I`{bB)v%AvEHikv+IF*Usge?Y8gD&*AGEOX&HTlsfj!lT?V-{Q^eqfAas9Xb z$9Pq<4(0TLt*gR6f$T+vMmAT4HBt#@18F z`ed@O99cx-RF1SZkG@sWFM1#~*Y*qFmtwsvd98TJhc)fFA_wEP1En{GPUQC`zg)g6 zQ8<&*E=-N`wV059{>Ux3&LR3$2X%x5`U)>om08IYcTRuH;ciR zK*`6>>reAddtKp9!N%D_;nXjmD!R~^3LEa2HP#ZQbU6%^8Fc#fHQEv`3opI28*sEZ zR9cO_0cUixn3Zsxd{HW6WEr`bSoVEs>R!*p%~zNq&uAWrEhZ$Q8h95*rls|o2a({7Fu)n{lBV*VESi@dTNd?_}$``a-?f`aps zIqmZU4Te%?JVCqHOL~-*+u>!4*iH^T44y4qvv_YJNC(T)jn^eHJkDh`1QpXLwy3DM z^##E{%w(pFESC`{X?~qG=vPm3yua=>gEej8Mx4VSVpvsQsWR`H2M>sMB3*E{jLjcg z8m-Cy&|V_5n=2tyDN@|?aMJz9PNwjzRPFf>w1QrdZ*@}CZ=!`7kn}&z zwX-88-CXwW+p&KlOcSc>F3=p`=>D{A(@WC=Z;Gw)F>#xHxM;V`Emxie=YHb+sqFLZ z$~2eijWiO23+)sJM{wYzIOCz?5qb~(#aHHr6n<&{pn{kDuo_Yo+AiF(e0yE)tHk|G z!Tp!FB&}N|DrPpkTW7lHce94J$X08@>}X);5}H`^9^dxXsPK|E@~wVS3;!5>O?kav zpzd9AGSfoidKJc=KSb8*SIXSa+q+;dZA{l%WwTSyaL&2kNRk2_{`+fI5#z9{6J`PU zpPS%+;=5k)v$$F3XHQ zkg?%TV6zhSER?_4Q10c=@X7W{XWzqtsSlrW-P|pn6?Gf-vlo0c{kb8$^pLykcl-z2 zHVM!E+~7N&h8Wpyowzb^`4OLl>EERJcO0&k_x2c^pAchtWK7b=O21|>K+&s5pDg{Y zI-bJE{`ZF2%FNLBz@pMivAJ0kezLE830x`)3)8A8Fnk~TUwSiS+&KFAqLjVy5CJ3Q z3rO4S4RwGsXZ16h5%F7BbVGkxj^dN-_w}vQ?9?Q@FD$tup-dgq5CHG_z|bXS?MnGU zMzeKb?{-W($M$631&_oo^sqpO$9 z=S@}3E*xd3R(G7gEW-#FvU|aJ?HLDKHMV1a6l?pORb|-ikRqC~Tcsa*mx3*CX0>$Hb4r^1GC3uvc88bqe2&9{Gpf57h1=S`(ihIugAlcE5jX;xO^n z@y(-&S+ig8H zT>VeTfem9J*4v~R9n6}Sa5bm3P*>MM20XS7_9lmka*~Y?O zO#GG%c|e*hpmL?A46TVYe$xAOLCVLh)!;C-3K#}uPu~oGUpi5|9sG7_mp@EO_rkdL zPY-9yS4HAZ(5AV&WF~iUP3%vGIsw|q1=9}rH2xn~dFf=+T8F}3v-;e%u0?g#MCCu5 z!Wg!{a;g-#ZZ7kDd9u!|uFGw8Uy3yd&vPK6sGHW|&!sf38mRc!t;KJ=&rQiaKd{SQ zExq%e%PS1LTw2_4$pJP}GTi)QSix$O?xxQOxQ&w<+EO78|jg}VDv3n?D!A&p2F~K;=uSG+N#Dt!}i1@S~-Kyq;(V? z-ed~t*06}XCI@e{9SN6z%ADJ*N=>=aG21>z$`NlH{FyW8tGqAQ{UyFxM^jQ=GwV|H zaow(;l$3!BGf9c#DUXifVmUsQ#j+8XW)~^N&$Ua(j)yf%NR-~gw~8R~$#x+a+jd}c z@DMAlg-w0P8}EG`cU{vC9EGj2m&o0iZC~`9N9AY}9LNtV_>LZOzhG`7Q;|`*f4QD` zvsd%s6uUpHRl9t-K<$$BU0ynR)eIrNQH8YJ9Fnwj&fiYYN^tJ%!Ydn&m{W7whSaZ_ z$-Rvb>GyTnP2O^xq^u58~coh7}`SRLN_iAqMTPsV1 zznV3jFR^X#*wRiXE3qHx1cz%mV1%IQKOY>u*bNQduE3158REu}-ut~P z_E6Rb4I4(3l{t@LYOssDo>Emi_@ssJ6}@(x1o}?s{J3Q@q46rOv>IW)E;vnP_*djq z_D_JBO8b_@{*Fxv6La6mRGv#)?MV9n!sp59HSinoaTXzSi{^+zE ze5M=VF69|74CJ&GtO?0LzyBc^O`^PSe7SO)j@!DU1cz%*h+>={XAaZ*$wO%>3Mq^X z>CcY?vG8n^JiLOYm}SVg?|pZn7mk0y_4E_{NTxDbbI$|q?JKUo`L>0VP2(E4kp`G~hnZ?|Ifi)c08sGj6BnB5^Exn=G>hq>61e5XTS@|FJ_Qu$sys__77TGyA z74>aqv&m|W#g;bYj0XE6=b|cw^zJ?0n%-0L!Y6ZAAeoa9DgDt>Q%f%cQxG%YEX4al zPMsF>>Kd<&9H-Iv%Zm?|fBI%VXx4vQ_Tw_s&;$M%%){6@4U2m>;Iz4)R}Rs5Rfg_I zHV0EYlw-$K&V02TZ`1;Cu_vwMx%2HiUJhBP%Bw@dI|&j2 zLTJ0CMDoSLzXLV+LgyIkYYv1;nzAh?bzn9bUKb;=uExgF;wv?!AH8%93|@|j{#-!U zKNHIS7_vd0HC-E{p zgZL>U&AYhCLeQl#h!iXKbT?Kxx1DZY{MNnq-RR(XLI2gx?S3pTj=nf=2KB13K3wj! zGIJiYr|P}GSKagyjh!Y8_FH@oh)H?Yim}zKVyToyd-trxTt8XBf4Ik~XX%Jb`VDWV z{)}i|bJ6J?TU(c56DixNlPT!<6RLa3Ec)1ijHH%nv&w6y?&5yJyP7(tEm(FA`1e4? zt&V9^@`>3WSkai=`2(|-Km09s>R1k{=9g<(4ifMeYFYMSMkTdu+e6|)wM<9G;uN)P zJFp?$I+mRTiP2g()A~@dejWFQ55rb1%a*ZOO)b-If*I%WOWz5ancV}G#%P!1%{_7~ zn>X){a+!l&<(c1F6RBprs}I+$@?qy(r(-?77J4B)MkG6??$i5rJ+4KaoQdd)M++NG z1|$!*<0Y4`U^4{>z$JfRT(8h8kt^>u%P$cwkx82Gn1LS=lk2FXV~}H#gG2D}&`!=m z&6d5S1I5w6L=k7ScG1AZ5r3Vdg~=fP(x!!hZp3*EpBK^4 zD4`Kr7$c+zgOM|x+}j$Aza~YBhBhgNhQ!unnPCbTFaQdAVE_MwbcD&?;HaGMHfrEzltul&IoeSq3D@!O_0}`0vUH zq;!CmXkj#vt~-qSl;F;rhbmve;kC}-v|Yx3Ks2s0hEMg_|{>v)5Un+;9eN zIfL`Xw}sHq*idGwx}DAx=eKnPowdEawd?MaEaT(a?rd&kC2B4KXvoE}f$A2&!zW;9Tk}XCr8KF{WXf z0;*(0M&ymmZLc`Mh2?*l?BEa}*)E-Bn`B#~(;9&B)Xp-}gMl71@trItXMqSZXM!Q# zXAL z4uVUD8Af$#rkPqsMSc*itiVR7K7Nl40gYrv%6aHxr(2JKhV~iw5-Riu9|Ai37n+fj z%A5~W(hF3LYWyBS1eA*fMsv!5R;rD>10fpPOK^@h`0wA95!i4A0rg>l(VapYaG9%K zfW;Gmtx*|pQ$RqwS&+Qi2Y$V}1{k2DIO|UG8VD#oD^de`4l%i_z)9(V6EXc;{v^G) z=^>yxV(9qLz6jEdcpWb23<5Po0#`y!z-c!Tc>k;Dxc}v=rLw7O;~*aNvCd8_H$qIq zWrLBO${9mHjT;B3&jIF9<@|1pfELQ3<3Yx3NRAel8<>>=Sr;Gyswcd)MBr1|V3en% zovey!$w0oH2s#VpwLw5v{z9KLY?KXxl&gRFEW`d!PoT7r3_I+?DbMs`f~n&fHrU-{o?zB^L&!P3b%m4QN8U&4YbAyV}Qswk$!PK z&mdwE*x3!_aiFn(S4QB4It26{C(>Z(gxnS-fa)Zmdhs9d#D%0A5zq=wB(ItZ4^=z? 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 a=^KF~ezRn-y>5BkGPnd6Zrans$NvEvbKU{~ delta 818779 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

    Jt1#(s-W06g|5Hw4yh!Z{-(=eR_}t?Y7d@Nr%7A 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

    5Tyx(0NJU?zP+*3_q)j=HD-u>L-@oNm+vIvrv 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�FILjv{rF;4;k9ez0S3tM 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⪼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 z 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�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

    m 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

    A=NfA{gXv;*hc?=y)Z)}JYA6%5a#}JC1Kal4 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 gbFWt~CN5tT+Gui6a 0 + self.cursor_pos -= 1 + prev_len = len(self.password) + del self.password[self.cursor_pos] + self.refresh_display(prev_len) + + def handle_kill_line(self): + """Erase entire line (Ctrl+U).""" + prev_len = len(self.password) + self.password.clear() + self.cursor_pos = 0 + self.refresh_display(prev_len) + + def handle_kill_forward(self): + """Kill from cursor to end (Ctrl+K).""" + prev_len = len(self.password) + del self.password[self.cursor_pos:] + self.refresh_display(prev_len) + + def handle_erase_word(self): + """Erase previous word (Ctrl+W).""" + old_cursor = self.cursor_pos + # Calculate the starting position of the previous word, + # ignoring trailing whitespaces. + while self.cursor_pos > 0 and self.password[self.cursor_pos - 1] == ' ': + self.cursor_pos -= 1 + while self.cursor_pos > 0 and self.password[self.cursor_pos - 1] != ' ': + self.cursor_pos -= 1 + # Delete the previous word and refresh the screen. + prev_len = len(self.password) + del self.password[self.cursor_pos:old_cursor] + self.refresh_display(prev_len) + + def handle_literal_next(self): + """State transition to indicate that the next character is literal.""" + assert self.literal_next is False + self.literal_next = True + + def handle_eof(self): + """State transition to indicate that the pressed character was EOF.""" + assert self.eof_pressed is False + self.eof_pressed = True + + def handle_interrupt(self): + """Raise a KeyboardInterrupt after Ctrl+C has been received.""" + raise KeyboardInterrupt + + def handle_nop(self): + """Handler for an ignored character.""" + + def handle(self, char): + """Handle a single character input. Returns True if handled.""" + handler = self.dispatch.get(char) + if handler: + handler() + return True + return False + + def readline(self, input): + """Read a line of password input with echo character support.""" + while True: + assert self.cursor_pos >= 0 + char = input.read(1) + if self.is_eol(char): break + # Handle literal next mode first as Ctrl+V quotes characters. + elif self.literal_next: + self.insert_char(char) + self.literal_next = False + # Handle EOF now as Ctrl+D must be pressed twice + # consecutively to stop reading from the input. + elif self.is_eof(char): + if self.eof_pressed: + break + elif self.handle(char): + # Dispatched to handler. + pass else: - eof_pressed = True - elif char == '\x00': - continue - else: - passwd += char - stream.write(echo_char) - stream.flush() - eof_pressed = False - return passwd + # Insert as normal character. + self.insert_char(char) + + self.eof_pressed = self.is_eof(char) + + return ''.join(self.password) def getuser(): diff --git a/Lib/glob.py b/Lib/glob.py index 173272ba38f..07b9d3eb952 100644 --- a/Lib/glob.py +++ b/Lib/glob.py @@ -149,21 +149,6 @@ def _glob0(dirname, basename, dir_fd, dironly, include_hidden=False): return [basename] return [] -_deprecated_function_message = ( - "{name} is deprecated and will be removed in Python {remove}. Use " - "glob.glob and pass a directory to its root_dir argument instead." -) - -def glob0(dirname, pattern): - import warnings - warnings._deprecated("glob.glob0", _deprecated_function_message, remove=(3, 15)) - return _glob0(dirname, pattern, None, False) - -def glob1(dirname, pattern): - import warnings - warnings._deprecated("glob.glob1", _deprecated_function_message, remove=(3, 15)) - return _glob1(dirname, pattern, None, False) - # This helper function recursively yields relative pathnames inside a literal # directory. @@ -385,6 +370,12 @@ def concat_path(path, text): """ raise NotImplementedError + @staticmethod + def stringify_path(path): + """Converts the path to a string object + """ + raise NotImplementedError + # High-level methods def compile(self, pat, altsep=None): @@ -493,8 +484,9 @@ def recursive_selector(self, part, parts): select_next = self.selector(parts) def select_recursive(path, exists=False): - match_pos = len(str(path)) - if match is None or match(str(path), match_pos): + path_str = self.stringify_path(path) + match_pos = len(path_str) + if match is None or match(path_str, match_pos): yield from select_next(path, exists) stack = [path] while stack: @@ -516,7 +508,7 @@ def select_recursive_step(stack, match_pos): pass if is_dir or not dir_only: - entry_path_str = str(entry_path) + entry_path_str = self.stringify_path(entry_path) if dir_only: entry_path = self.concat_path(entry_path, self.sep) if match is None or match(entry_path_str, match_pos): @@ -556,19 +548,6 @@ def scandir(path): entries = list(scandir_it) return ((entry, entry.name, entry.path) for entry in entries) - -class _PathGlobber(_GlobberBase): - """Provides shell-style pattern matching and globbing for pathlib paths. - """ - - @staticmethod - def lexists(path): - return path.info.exists(follow_symlinks=False) - @staticmethod - def scandir(path): - return ((child.info, child.name, child) for child in path.iterdir()) - - @staticmethod - def concat_path(path, text): - return path.with_segments(str(path) + text) + def stringify_path(path): + return path # Already a string. diff --git a/Lib/http/client.py b/Lib/http/client.py index 6fb7d254ea9..7ef99e7201c 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -111,6 +111,13 @@ _MAXLINE = 65536 _MAXHEADERS = 100 +# maximal number of interim (1xx) responses tolerated before the final +# response. Real servers send at most a few; without a bound, a server +# streaming "100 Continue" responses would hang getresponse() forever. +# A socket timeout cannot detect that as data keeps arriving within every +# timeout window. +_MAXINTERIMRESPONSES = 100 + # Data larger than this will be read in chunks, to prevent extreme # overallocation. _MIN_READ_BUF_SIZE = 1 << 20 @@ -186,11 +193,10 @@ def _strip_ipv6_iface(enc_name: bytes) -> bytes: return enc_name class HTTPMessage(email.message.Message): - # XXX The only usage of this method is in - # http.server.CGIHTTPRequestHandler. Maybe move the code there so - # that it doesn't need to be part of the public API. The API has - # never been defined so this could cause backwards compatibility - # issues. + + # The getallmatchingheaders() method was only used by the CGI handler + # that was removed in Python 3.15. However, since the public API was not + # properly defined, it will be kept for backwards compatibility reasons. def getallmatchingheaders(self, name): """Find all header lines matching a given header name. @@ -215,22 +221,24 @@ def getallmatchingheaders(self, name): lst.append(line) return lst -def _read_headers(fp): +def _read_headers(fp, max_headers): """Reads potential header lines into a list from a file pointer. Length of line is limited by _MAXLINE, and number of - headers is limited by _MAXHEADERS. + headers is limited by max_headers. """ headers = [] + if max_headers is None: + max_headers = _MAXHEADERS while True: line = fp.readline(_MAXLINE + 1) if len(line) > _MAXLINE: raise LineTooLong("header line") - headers.append(line) - if len(headers) > _MAXHEADERS: - raise HTTPException("got more than %d headers" % _MAXHEADERS) if line in (b'\r\n', b'\n', b''): break + headers.append(line) + if len(headers) > max_headers: + raise HTTPException(f"got more than {max_headers} headers") return headers def _parse_header_lines(header_lines, _class=HTTPMessage): @@ -247,10 +255,10 @@ def _parse_header_lines(header_lines, _class=HTTPMessage): hstring = b''.join(header_lines).decode('iso-8859-1') return email.parser.Parser(_class=_class).parsestr(hstring) -def parse_headers(fp, _class=HTTPMessage): +def parse_headers(fp, _class=HTTPMessage, *, _max_headers=None): """Parses only RFC 5322 headers from a file pointer.""" - headers = _read_headers(fp) + headers = _read_headers(fp, _max_headers) return _parse_header_lines(headers, _class) @@ -292,6 +300,7 @@ def __init__(self, sock, debuglevel=0, method=None, url=None): self.chunk_left = _UNKNOWN # bytes left to read in current chunk self.length = _UNKNOWN # number of bytes left in response self.will_close = _UNKNOWN # conn will close at end of response + self._max_headers = None # configured header count limit def _read_status(self): line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1") @@ -326,21 +335,29 @@ def _read_status(self): raise BadStatusLine(line) return version, status, reason - def begin(self): + def begin(self, *, _max_headers=None): if self.headers is not None: # we've already started reading the response return + # Trailers of a chunked response are read by read() long after + # begin() returns, so remember the configured header count limit + # for _read_and_discard_trailer() to enforce. + self._max_headers = _max_headers + # read until we get a non-100 response - while True: + for _ in range(_MAXINTERIMRESPONSES): version, status, reason = self._read_status() if status != CONTINUE: break # skip the header from the 100 response - skipped_headers = _read_headers(self.fp) + skipped_headers = _read_headers(self.fp, _max_headers) if self.debuglevel > 0: print("headers:", skipped_headers) del skipped_headers + else: + raise HTTPException( + f"got more than {_MAXINTERIMRESPONSES} interim responses") self.code = self.status = status self.reason = reason.strip() @@ -352,7 +369,9 @@ def begin(self): else: raise UnknownProtocol(version) - self.headers = self.msg = parse_headers(self.fp) + self.headers = self.msg = parse_headers( + self.fp, _max_headers=_max_headers + ) if self.debuglevel > 0: for hdr, val in self.headers.items(): @@ -558,6 +577,10 @@ def _read_next_chunk_size(self): def _read_and_discard_trailer(self): # read and discard trailer up to the CRLF terminator ### note: we shouldn't have any trailers! + max_trailers = self._max_headers + if max_trailers is None: + max_trailers = _MAXHEADERS + trailers_read = 0 while True: line = self.fp.readline(_MAXLINE + 1) if len(line) > _MAXLINE: @@ -568,6 +591,13 @@ def _read_and_discard_trailer(self): break if line in (b'\r\n', b'\n', b''): break + # Bound the trailer count just as response headers are bounded. + # A server streaming trailer lines forever would otherwise hang + # the client; a socket timeout cannot detect that as data keeps + # arriving within every timeout window. + trailers_read += 1 + if trailers_read > max_trailers: + raise HTTPException(f"got more than {max_trailers} trailers") def _get_chunk_left(self): # return self.chunk_left, reading a new chunk if necessary. @@ -885,7 +915,7 @@ def _get_content_length(body, method): return None def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, - source_address=None, blocksize=8192): + source_address=None, blocksize=8192, *, max_response_headers=None): self.timeout = timeout self.source_address = source_address self.blocksize = blocksize @@ -898,6 +928,7 @@ def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, self._tunnel_port = None self._tunnel_headers = {} self._raw_proxy_headers = None + self.max_response_headers = max_response_headers (self.host, self.port) = self._get_hostport(host, port) @@ -999,7 +1030,7 @@ def _tunnel(self): try: (version, code, message) = response._read_status() - self._raw_proxy_headers = _read_headers(response.fp) + self._raw_proxy_headers = _read_headers(response.fp, self.max_response_headers) if self.debuglevel > 0: for header in self._raw_proxy_headers: @@ -1416,8 +1447,7 @@ def getresponse(self): """Get the response from the server. If the HTTPConnection is in the correct state, returns an - instance of HTTPResponse or of whatever object is returned by - the response_class variable. + instance of HTTPResponse. If a request has not been sent or if a previous response has not be handled, ResponseNotReady is raised. If the HTTP @@ -1456,7 +1486,10 @@ def getresponse(self): try: try: - response.begin() + if self.max_response_headers is None: + response.begin() + else: + response.begin(_max_headers=self.max_response_headers) except ConnectionError: self.close() raise @@ -1487,10 +1520,12 @@ class HTTPSConnection(HTTPConnection): def __init__(self, host, port=None, *, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, - source_address=None, context=None, blocksize=8192): + source_address=None, context=None, blocksize=8192, + max_response_headers=None): super(HTTPSConnection, self).__init__(host, port, timeout, source_address, - blocksize=blocksize) + blocksize=blocksize, + max_response_headers=max_response_headers) if context is None: context = _create_https_context(self._http_vsn) self._context = context diff --git a/Lib/http/cookiejar.py b/Lib/http/cookiejar.py index 68cf16c93cc..13e5b104a81 100644 --- a/Lib/http/cookiejar.py +++ b/Lib/http/cookiejar.py @@ -1032,10 +1032,13 @@ def set_ok_domain(self, cookie, request): if j == 0: # domain like .foo.bar tld = domain[i+1:] sld = domain[j+1:i] - if sld.lower() in ("co", "ac", "com", "edu", "org", "net", - "gov", "mil", "int", "aero", "biz", "cat", "coop", - "info", "jobs", "mobi", "museum", "name", "pro", - "travel", "eu") and len(tld) == 2: + known_slds = ( + "co", "ac", "com", "edu", "org", "net", + "gov", "mil", "int", "aero", "biz", "cat", "coop", + "info", "jobs", "mobi", "museum", "name", "pro", + "travel", "eu", "tv", "or", "nom", "sch", "web", + ) + if sld.lower() in known_slds and len(tld) == 2: # domain like .co.uk _debug(" country-code second level domain %s", domain) return False diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py index abebb4b69fd..9a6f01dfb5e 100644 --- a/Lib/http/cookies.py +++ b/Lib/http/cookies.py @@ -132,6 +132,7 @@ import re import string import types +lazy import warnings __all__ = ["CookieError", "BaseCookie", "SimpleCookie"] @@ -390,7 +391,8 @@ def output(self, attrs=None, header="Set-Cookie:"): def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.OutputString()) - def js_output(self, attrs=None): + def _js_output(self, attrs=None): + """Internal implementation without deprecation warning.""" import urllib.parse # Print javascript output_string = self.OutputString(attrs) @@ -407,6 +409,14 @@ def js_output(self, attrs=None): """ % (output_encoded,) + def js_output(self, attrs=None): + warnings._deprecated( + "http.cookies.Morsel.js_output", + message=warnings._DEPRECATED_MSG + "; use output() instead", + remove=(3, 19), + ) + return self._js_output(attrs) + def OutputString(self, attrs=None): # Build up our result # @@ -541,10 +551,15 @@ def __repr__(self): def js_output(self, attrs=None): """Return a string suitable for JavaScript.""" + warnings._deprecated( + "http.cookies.BaseCookie.js_output", + message=warnings._DEPRECATED_MSG + "; use output() instead", + remove=(3, 19), + ) result = [] items = sorted(self.items()) for key, value in items: - result.append(value.js_output(attrs)) + result.append(value._js_output(attrs)) return _nulljoin(result) def load(self, rawdata): diff --git a/Lib/http/server.py b/Lib/http/server.py index ac1f57c29f0..095b5744bd1 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -1,29 +1,10 @@ """HTTP server classes. Note: BaseHTTPRequestHandler doesn't implement any HTTP request; see -SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST, -and (deprecated) CGIHTTPRequestHandler for CGI scripts. +SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST. It does, however, optionally implement HTTP/1.1 persistent connections. -Notes on CGIHTTPRequestHandler ------------------------------- - -This class is deprecated. It implements GET and POST requests to cgi-bin scripts. - -If the os.fork() function is not present (Windows), subprocess.Popen() is used, -with slightly altered but never documented semantics. Use from a threaded -process is likely to trigger a warning at os.fork() time. - -In all cases, the implementation is intentionally naive -- all -requests are executed synchronously. - -SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL --- it may execute arbitrary Python code or external programs. - -Note that status code 200 is sent prior to execution of a CGI script, so -scripts cannot send other status codes such as 302 (redirect). - XXX To do: - log requests even later (to capture byte count) @@ -80,16 +61,12 @@ # (Actually, the latter is only true if you know the server configuration # at the time the request was made!) -__version__ = "0.6" - __all__ = [ "HTTPServer", "ThreadingHTTPServer", "HTTPSServer", "ThreadingHTTPSServer", "BaseHTTPRequestHandler", "SimpleHTTPRequestHandler", - "CGIHTTPRequestHandler", ] -import copy import datetime import email.utils import html @@ -99,7 +76,6 @@ import mimetypes import os import posixpath -import select import shutil import socket import socketserver @@ -109,6 +85,8 @@ from http import HTTPStatus +lazy import _colorize + # Default error message template DEFAULT_ERROR_MESSAGE = """\ @@ -134,10 +112,6 @@ DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8" -# Data larger than this will be read in chunks, to prevent extreme -# overallocation. -_MIN_READ_BUF_SIZE = 1 << 20 - class HTTPServer(socketserver.TCPServer): allow_reuse_address = True # Seems to make sense in testing environment @@ -306,7 +280,7 @@ class BaseHTTPRequestHandler(socketserver.StreamRequestHandler): # The server software version. You may want to override this. # The format is multiple whitespace-separated strings, # where each string is of the form name[/version]. - server_version = "BaseHTTP/" + __version__ + server_version = "BaseHTTP" error_message_format = DEFAULT_ERROR_MESSAGE error_content_type = DEFAULT_ERROR_CONTENT_TYPE @@ -360,10 +334,13 @@ def parse_request(self): raise ValueError("unreasonable length http version") version_number = int(version_number[0]), int(version_number[1]) except (ValueError, IndexError): + # Send the error response with a status line and headers. + self.request_version = '' self.send_error( HTTPStatus.BAD_REQUEST, "Bad request version (%r)" % version) return False + self.request_version = version if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1": self.close_connection = False if version_number >= (2, 0): @@ -371,9 +348,9 @@ def parse_request(self): HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, "Invalid HTTP version (%s)" % base_version_number) return False - self.request_version = version if not 2 <= len(words) <= 3: + self.request_version = '' self.send_error( HTTPStatus.BAD_REQUEST, "Bad request syntax (%r)" % requestline) @@ -382,6 +359,7 @@ def parse_request(self): if len(words) == 2: self.close_connection = True if command != 'GET': + self.request_version = '' self.send_error( HTTPStatus.BAD_REQUEST, "Bad HTTP/0.9 request type (%r)" % command) @@ -577,13 +555,17 @@ def send_response_only(self, code, message=None): (self.protocol_version, code, message)).encode( 'latin-1', 'strict')) - def send_header(self, keyword, value): + def send_header(self, keyword, value, *, _is_extra=False): """Send a MIME header to the headers buffer.""" if self.request_version != 'HTTP/0.9': if not hasattr(self, '_headers_buffer'): self._headers_buffer = [] self._headers_buffer.append( ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict')) + if not hasattr(self, '_default_response_headers'): + self._default_response_headers = [] + if not _is_extra: + self._default_response_headers.append((keyword, value)) if keyword.lower() == 'connection': if value.lower() == 'close': @@ -601,6 +583,33 @@ def flush_headers(self): if hasattr(self, '_headers_buffer'): self.wfile.write(b"".join(self._headers_buffer)) self._headers_buffer = [] + if hasattr(self, '_default_response_headers'): + self._default_response_headers = [] + + def _colorize_request(self, code, size, t): + try: + code_int = int(code) + except (TypeError, ValueError): + code_color = "" + else: + if code_int >= 500: + code_color = t.status_server_error + elif code_int >= 400: + code_color = t.status_client_error + elif code_int >= 300: + code_color = t.status_redirect + elif code_int >= 200: + code_color = t.status_ok + else: + code_color = t.status_informational + + request_line = self.requestline.translate(self._control_char_table) + parts = request_line.split(None, 2) + if len(parts) == 3: + method, path, version = parts + request_line = f"{method} {t.path}{path}{t.reset} {version}" + + return f'"{request_line}" {code_color}{code} {t.size}{size}{t.reset}' def log_request(self, code='-', size='-'): """Log an accepted request. @@ -610,6 +619,7 @@ def log_request(self, code='-', size='-'): """ if isinstance(code, HTTPStatus): code = code.value + self._log_request_info = (code, size) self.log_message('"%s" %s %s', self.requestline, str(code), str(size)) @@ -624,7 +634,7 @@ def log_error(self, format, *args): XXX This should go to the separate error log. """ - + self._log_is_error = True self.log_message(format, *args) # https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes @@ -651,12 +661,22 @@ def log_message(self, format, *args): before writing the output to stderr. """ - - message = format % args - sys.stderr.write("%s - - [%s] %s\n" % - (self.address_string(), - self.log_date_time_string(), - message.translate(self._control_char_table))) + message = (format % args).translate(self._control_char_table) + t = _colorize.get_theme(tty_file=sys.stderr).http_server + + info = getattr(self, "_log_request_info", None) + if info is not None: + self._log_request_info = None + message = self._colorize_request(*info, t) + elif getattr(self, "_log_is_error", False): + self._log_is_error = False + message = f"{t.error}{message}{t.reset}" + + sys.stderr.write( + f"{t.timestamp}{self.address_string()} - - " + f"[{self.log_date_time_string()}]{t.reset} " + f"{message}\n" + ) def version_string(self): """Return the server software version string.""" @@ -716,7 +736,8 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): """ - server_version = "SimpleHTTP/" + __version__ + server_version = "SimpleHTTP" + default_content_type = "application/octet-stream" index_pages = ("index.html", "index.htm") extensions_map = _encodings_map_default = { '.gz': 'application/gzip', @@ -725,10 +746,11 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): '.xz': 'application/x-xz', } - def __init__(self, *args, directory=None, **kwargs): + def __init__(self, *args, directory=None, extra_response_headers=None, **kwargs): if directory is None: directory = os.getcwd() self.directory = os.fspath(directory) + self.extra_response_headers = extra_response_headers super().__init__(*args, **kwargs) def do_GET(self): @@ -746,6 +768,16 @@ def do_HEAD(self): if f: f.close() + def _send_extra_response_headers(self): + """Send the headers stored in self.extra_response_headers.""" + if self.extra_response_headers is not None: + default_headers = {h.lower() for h, _ in self._default_response_headers} + for header, value in self.extra_response_headers: + # Don't send the header if it's already sent + # as part of the default response headers + if header.lower() not in default_headers: + self.send_header(header, value, _is_extra=True) + def send_head(self): """Common code for GET and HEAD commands. @@ -828,6 +860,7 @@ def send_head(self): self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + self._send_extra_response_headers() self.end_headers() return f except: @@ -892,6 +925,7 @@ def list_directory(self, path): self.send_response(HTTPStatus.OK) self.send_header("Content-type", "text/html; charset=%s" % enc) self.send_header("Content-Length", str(len(encoded))) + self._send_extra_response_headers() self.end_headers() return f @@ -964,57 +998,7 @@ def guess_type(self, path): guess, _ = mimetypes.guess_file_type(path) if guess: return guess - return 'application/octet-stream' - - -# Utilities for CGIHTTPRequestHandler - -def _url_collapse_path(path): - """ - Given a URL path, remove extra '/'s and '.' path elements and collapse - any '..' references and returns a collapsed path. - - Implements something akin to RFC-2396 5.2 step 6 to parse relative paths. - The utility of this function is limited to is_cgi method and helps - preventing some security attacks. - - Returns: The reconstituted URL, which will always start with a '/'. - - Raises: IndexError if too many '..' occur within the path. - - """ - # Query component should not be involved. - path, _, query = path.partition('?') - path = urllib.parse.unquote(path) - - # Similar to os.path.split(os.path.normpath(path)) but specific to URL - # path semantics rather than local operating system semantics. - path_parts = path.split('/') - head_parts = [] - for part in path_parts[:-1]: - if part == '..': - head_parts.pop() # IndexError if more '..' than prior parts - elif part and part != '.': - head_parts.append( part ) - if path_parts: - tail_part = path_parts.pop() - if tail_part: - if tail_part == '..': - head_parts.pop() - tail_part = '' - elif tail_part == '.': - tail_part = '' - else: - tail_part = '' - - if query: - tail_part = '?'.join((tail_part, query)) - - splitpath = ('/' + '/'.join(head_parts), tail_part) - collapsed_path = "/".join(splitpath) - - return collapsed_path - + return self.default_content_type nobody = None @@ -1040,285 +1024,6 @@ def executable(path): return os.access(path, os.X_OK) -class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): - - """Complete HTTP server with GET, HEAD and POST commands. - - GET and HEAD also support running CGI scripts. - - The POST command is *only* implemented for CGI scripts. - - """ - - def __init__(self, *args, **kwargs): - import warnings - warnings._deprecated("http.server.CGIHTTPRequestHandler", - remove=(3, 15)) - super().__init__(*args, **kwargs) - - # Determine platform specifics - have_fork = hasattr(os, 'fork') - - # Make rfile unbuffered -- we need to read one line and then pass - # the rest to a subprocess, so we can't use buffered input. - rbufsize = 0 - - def do_POST(self): - """Serve a POST request. - - This is only implemented for CGI scripts. - - """ - - if self.is_cgi(): - self.run_cgi() - else: - self.send_error( - HTTPStatus.NOT_IMPLEMENTED, - "Can only POST to CGI scripts") - - def send_head(self): - """Version of send_head that support CGI scripts""" - if self.is_cgi(): - return self.run_cgi() - else: - return SimpleHTTPRequestHandler.send_head(self) - - def is_cgi(self): - """Test whether self.path corresponds to a CGI script. - - Returns True and updates the cgi_info attribute to the tuple - (dir, rest) if self.path requires running a CGI script. - Returns False otherwise. - - If any exception is raised, the caller should assume that - self.path was rejected as invalid and act accordingly. - - The default implementation tests whether the normalized url - path begins with one of the strings in self.cgi_directories - (and the next character is a '/' or the end of the string). - - """ - collapsed_path = _url_collapse_path(self.path) - dir_sep = collapsed_path.find('/', 1) - while dir_sep > 0 and not collapsed_path[:dir_sep] in self.cgi_directories: - dir_sep = collapsed_path.find('/', dir_sep+1) - if dir_sep > 0: - head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:] - self.cgi_info = head, tail - return True - return False - - - cgi_directories = ['/cgi-bin', '/htbin'] - - def is_executable(self, path): - """Test whether argument path is an executable file.""" - return executable(path) - - def is_python(self, path): - """Test whether argument path is a Python script.""" - head, tail = os.path.splitext(path) - return tail.lower() in (".py", ".pyw") - - def run_cgi(self): - """Execute a CGI script.""" - dir, rest = self.cgi_info - path = dir + '/' + rest - i = path.find('/', len(dir)+1) - while i >= 0: - nextdir = path[:i] - nextrest = path[i+1:] - - scriptdir = self.translate_path(nextdir) - if os.path.isdir(scriptdir): - dir, rest = nextdir, nextrest - i = path.find('/', len(dir)+1) - else: - break - - # find an explicit query string, if present. - rest, _, query = rest.partition('?') - - # dissect the part after the directory name into a script name & - # a possible additional path, to be stored in PATH_INFO. - i = rest.find('/') - if i >= 0: - script, rest = rest[:i], rest[i:] - else: - script, rest = rest, '' - - scriptname = dir + '/' + script - scriptfile = self.translate_path(scriptname) - if not os.path.exists(scriptfile): - self.send_error( - HTTPStatus.NOT_FOUND, - "No such CGI script (%r)" % scriptname) - return - if not os.path.isfile(scriptfile): - self.send_error( - HTTPStatus.FORBIDDEN, - "CGI script is not a plain file (%r)" % scriptname) - return - ispy = self.is_python(scriptname) - if self.have_fork or not ispy: - if not self.is_executable(scriptfile): - self.send_error( - HTTPStatus.FORBIDDEN, - "CGI script is not executable (%r)" % scriptname) - return - - # Reference: https://www6.uniovi.es/~antonio/ncsa_httpd/cgi/env.html - # XXX Much of the following could be prepared ahead of time! - env = copy.deepcopy(os.environ) - env['SERVER_SOFTWARE'] = self.version_string() - env['SERVER_NAME'] = self.server.server_name - env['GATEWAY_INTERFACE'] = 'CGI/1.1' - env['SERVER_PROTOCOL'] = self.protocol_version - env['SERVER_PORT'] = str(self.server.server_port) - env['REQUEST_METHOD'] = self.command - uqrest = urllib.parse.unquote(rest) - env['PATH_INFO'] = uqrest - env['PATH_TRANSLATED'] = self.translate_path(uqrest) - env['SCRIPT_NAME'] = scriptname - env['QUERY_STRING'] = query - env['REMOTE_ADDR'] = self.client_address[0] - authorization = self.headers.get("authorization") - if authorization: - authorization = authorization.split() - if len(authorization) == 2: - import base64, binascii - env['AUTH_TYPE'] = authorization[0] - if authorization[0].lower() == "basic": - try: - authorization = authorization[1].encode('ascii') - authorization = base64.decodebytes(authorization).\ - decode('ascii') - except (binascii.Error, UnicodeError): - pass - else: - authorization = authorization.split(':') - if len(authorization) == 2: - env['REMOTE_USER'] = authorization[0] - # XXX REMOTE_IDENT - if self.headers.get('content-type') is None: - env['CONTENT_TYPE'] = self.headers.get_content_type() - else: - env['CONTENT_TYPE'] = self.headers['content-type'] - length = self.headers.get('content-length') - if length: - env['CONTENT_LENGTH'] = length - referer = self.headers.get('referer') - if referer: - env['HTTP_REFERER'] = referer - accept = self.headers.get_all('accept', ()) - env['HTTP_ACCEPT'] = ','.join(accept) - ua = self.headers.get('user-agent') - if ua: - env['HTTP_USER_AGENT'] = ua - co = filter(None, self.headers.get_all('cookie', [])) - cookie_str = ', '.join(co) - if cookie_str: - env['HTTP_COOKIE'] = cookie_str - # XXX Other HTTP_* headers - # Since we're setting the env in the parent, provide empty - # values to override previously set values - for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH', - 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'): - env.setdefault(k, "") - - self.send_response(HTTPStatus.OK, "Script output follows") - self.flush_headers() - - decoded_query = query.replace('+', ' ') - - if self.have_fork: - # Unix -- fork as we should - args = [script] - if '=' not in decoded_query: - args.append(decoded_query) - nobody = nobody_uid() - self.wfile.flush() # Always flush before forking - pid = os.fork() - if pid != 0: - # Parent - pid, sts = os.waitpid(pid, 0) - # throw away additional data [see bug #427345] - while select.select([self.rfile], [], [], 0)[0]: - if not self.rfile.read(1): - break - exitcode = os.waitstatus_to_exitcode(sts) - if exitcode: - self.log_error(f"CGI script exit code {exitcode}") - return - # Child - try: - try: - os.setuid(nobody) - except OSError: - pass - os.dup2(self.rfile.fileno(), 0) - os.dup2(self.wfile.fileno(), 1) - os.execve(scriptfile, args, env) - except: - self.server.handle_error(self.request, self.client_address) - os._exit(127) - - else: - # Non-Unix -- use subprocess - import subprocess - cmdline = [scriptfile] - if self.is_python(scriptfile): - interp = sys.executable - if interp.lower().endswith("w.exe"): - # On Windows, use python.exe, not pythonw.exe - interp = interp[:-5] + interp[-4:] - cmdline = [interp, '-u'] + cmdline - if '=' not in query: - cmdline.append(query) - self.log_message("command: %s", subprocess.list2cmdline(cmdline)) - try: - nbytes = int(length) - except (TypeError, ValueError): - nbytes = 0 - p = subprocess.Popen(cmdline, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env = env - ) - if self.command.lower() == "post" and nbytes > 0: - cursize = 0 - data = self.rfile.read(min(nbytes, _MIN_READ_BUF_SIZE)) - while len(data) < nbytes and len(data) != cursize: - cursize = len(data) - # This is a geometric increase in read size (never more - # than doubling out the current length of data per loop - # iteration). - delta = min(cursize, nbytes - cursize) - try: - data += self.rfile.read(delta) - except TimeoutError: - break - else: - data = None - # throw away additional data [see bug #427345] - while select.select([self.rfile._sock], [], [], 0)[0]: - if not self.rfile._sock.recv(1): - break - stdout, stderr = p.communicate(data) - self.wfile.write(stdout) - if stderr: - self.log_error('%s', stderr) - p.stderr.close() - p.stdout.close() - status = p.returncode - if status: - self.log_error("CGI script exit status %#x", status) - else: - self.log_message("CGI script exited OK") - - def _get_best_family(*address): infos = socket.getaddrinfo( *address, @@ -1329,31 +1034,45 @@ def _get_best_family(*address): return family, sockaddr -def test(HandlerClass=BaseHTTPRequestHandler, +def _make_server(HandlerClass=BaseHTTPRequestHandler, + ServerClass=ThreadingHTTPServer, + protocol="HTTP/1.0", port=8000, bind=None, + tls_cert=None, tls_key=None, tls_password=None, + default_content_type=SimpleHTTPRequestHandler.default_content_type): + ServerClass.address_family, addr = _get_best_family(bind, port) + HandlerClass.protocol_version = protocol + HandlerClass.default_content_type = default_content_type + + if tls_cert: + return ServerClass(addr, HandlerClass, certfile=tls_cert, + keyfile=tls_key, password=tls_password) + else: + return ServerClass(addr, HandlerClass) + + +def test(HandlerClass=SimpleHTTPRequestHandler, ServerClass=ThreadingHTTPServer, protocol="HTTP/1.0", port=8000, bind=None, + content_type=SimpleHTTPRequestHandler.default_content_type, tls_cert=None, tls_key=None, tls_password=None): """Test the HTTP request handler class. This runs an HTTP server on port 8000 (or the port argument). - """ - ServerClass.address_family, addr = _get_best_family(bind, port) - HandlerClass.protocol_version = protocol - - if tls_cert: - server = ServerClass(addr, HandlerClass, certfile=tls_cert, - keyfile=tls_key, password=tls_password) - else: - server = ServerClass(addr, HandlerClass) - - with server as httpd: + with _make_server( + HandlerClass=HandlerClass, ServerClass=ServerClass, + protocol=protocol, port=port, bind=bind, + tls_cert=tls_cert, tls_key=tls_key, tls_password=tls_password, + default_content_type=content_type, + ) as httpd: host, port = httpd.socket.getsockname()[:2] url_host = f'[{host}]' if ':' in host else host protocol = 'HTTPS' if tls_cert else 'HTTP' + t = _colorize.get_theme().http_server + url = f"{protocol.lower()}://{url_host}:{port}/" print( - f"Serving {protocol} on {host} port {port} " - f"({protocol.lower()}://{url_host}:{port}/) ..." + f"{t.serving}Serving {protocol} on {host} port {port}{t.reset} " + f"({t.url}{url}{t.reset}) ..." ) try: httpd.serve_forever() @@ -1361,13 +1080,12 @@ def test(HandlerClass=BaseHTTPRequestHandler, print("\nKeyboard interrupt received, exiting.") sys.exit(0) -if __name__ == '__main__': + +def _main(args=None): import argparse import contextlib - parser = argparse.ArgumentParser(color=True) - parser.add_argument('--cgi', action='store_true', - help='run as CGI server') + parser = argparse.ArgumentParser() parser.add_argument('-b', '--bind', metavar='ADDRESS', help='bind to this address ' '(default: all interfaces)') @@ -1378,6 +1096,10 @@ def test(HandlerClass=BaseHTTPRequestHandler, default='HTTP/1.0', help='conform to this HTTP version ' '(default: %(default)s)') + parser.add_argument('--content-type', + default=SimpleHTTPRequestHandler.default_content_type, + help='default content type for unknown extensions ' + '(default: %(default)s)') parser.add_argument('--tls-cert', metavar='PATH', help='path to the TLS certificate chain file') parser.add_argument('--tls-key', metavar='PATH', @@ -1387,7 +1109,11 @@ def test(HandlerClass=BaseHTTPRequestHandler, parser.add_argument('port', default=8000, type=int, nargs='?', help='bind to this port ' '(default: %(default)s)') - args = parser.parse_args() + parser.add_argument('-H', '--header', nargs=2, action='append', + metavar=('HEADER', 'VALUE'), + help='Add a custom response header ' + '(can be specified multiple times)') + args = parser.parse_args(args) if not args.tls_cert and args.tls_key: parser.error("--tls-key requires --tls-cert to be set") @@ -1403,11 +1129,6 @@ def test(HandlerClass=BaseHTTPRequestHandler, except OSError as e: parser.error(f"Failed to read TLS password file: {e}") - if args.cgi: - handler_class = CGIHTTPRequestHandler - else: - handler_class = SimpleHTTPRequestHandler - # ensure dual-stack is not disabled; ref #38907 class DualStackServerMixin: @@ -1420,7 +1141,8 @@ def server_bind(self): def finish_request(self, request, client_address): self.RequestHandlerClass(request, client_address, self, - directory=args.directory) + directory=args.directory, + extra_response_headers=args.header) class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer): pass @@ -1430,12 +1152,26 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer): ServerClass = HTTPSDualStackServer if args.tls_cert else HTTPDualStackServer test( - HandlerClass=handler_class, + HandlerClass=SimpleHTTPRequestHandler, ServerClass=ServerClass, port=args.port, bind=args.bind, protocol=args.protocol, + content_type=args.content_type, tls_cert=args.tls_cert, tls_key=args.tls_key, tls_password=tls_key_password, ) + + +def __getattr__(name): + if name == "__version__": + from warnings import _deprecated + + _deprecated("__version__", remove=(3, 20)) + return "0.6" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +if __name__ == '__main__': + _main() diff --git a/Lib/idlelib/News3.txt b/Lib/idlelib/News3.txt index da3a1458111..bcbd7fa542a 100644 --- a/Lib/idlelib/News3.txt +++ b/Lib/idlelib/News3.txt @@ -1,9 +1,54 @@ -What's New in IDLE 3.14.z +What's New in IDLE 3.15.0 (since 3.14.0) -Released after 2025-10-07 +Released on 2026-10-01 ========================= +gh-152745: When "Run... Customized" with "Restart shell" unchecked +while Shell is running code, including waiting for an input('prompt:') +response, just report that the shell is executing instead of +reporting twice and restarting anyway. Patch by Serhiy Storchaka. + +gh-152743: When an integer entry in IDLE's Settings dialog, such as +"Auto squeeze min lines", is deleted, do not save '', which is an +invalid configuration value. Patch by Serhiy Storchaka. + +gh-152742: MakeChange the default extension when saving a Shell or +Output window '.txt' and list text files before Python files. Their +content is not Python source. Patch by Serhiy Storchaka and Claude Code. + +gh-152740: Fill the "In files:" field of IDLE's Find in Files dialog +with a full directory path, even for an unsaved editor or the Shell. +In the grep output whow which directory was searched. Patch by Serhiy +Storchaka and Claude Code. + +gh-152739: Omit the idlelib directory from the path of the IDLE user +process. User code imports such as `import help' can no longer import +an idlelib submodule (here idlelib.help) instead of the intended +top-level module. Patch by Serhiy Storchaka and Claude Code. + +gh-152738: Fix a rare crash in the IDLE editor when the completion +window is closed: deleting a key binding for a sequence that is not +bound to the virtual event is now ignored instead of raising a +ValueError. Patch by Serhiy Storchaka and Claude Code. + +gh-152737: Fix Replace All in the IDLE editor's Replace dialog when the +search direction is "Up" and "Wrap around" is off: it now replaces all +matches above the current position instead of only the first one. +Patch by Serhiy Storchaka and Claude Code. + +gh-152733: For X11 window managers, set toplevel window classes to +"Idle". Patch by Serhiy Storchaka and Claude Code. + +gh-152728: Move functions run.fix_scaling, editor.fixwordbreaks +(as 'fix_word_breaks') and pyshell.fix_x11_paste to module util. +Patch by Terry J. Reedy. + +gh-85320: IDLE now reads and writes its configuration files and the +breakpoints file using UTF-8 instead of the locale encoding. +Files with non-ASCII characters and non-UTF-8 encoding may need +to be opened in an editor and resaved with UTF-8 encoding. + gh-143774: Better explain the operation of Format / Format Paragraph. Patch by Terry J. Reedy. diff --git a/Lib/idlelib/colorizer.py b/Lib/idlelib/colorizer.py index bffa2ddd3cd..ac12d8ace35 100644 --- a/Lib/idlelib/colorizer.py +++ b/Lib/idlelib/colorizer.py @@ -42,6 +42,11 @@ def make_pat(): ]) + r"))" ) + lazy_softkw = ( # lazy new in 3.15 (+ 2 lines below). + r"^[ \t]*" + # at beginning of line + possible indentation + r"(?Plazy)" + + r"(?=[ \t]+(?:import|from)\b)" # followed by 'import' or 'from' + ) builtinlist = [str(name) for name in dir(builtins) if not name.startswith('_') and name not in keyword.kwlist] @@ -56,7 +61,7 @@ def make_pat(): prog = re.compile("|".join([ builtin, comment, string, kw, match_softkw, case_default, - case_softkw_and_pattern, + case_softkw_and_pattern, lazy_softkw, any("SYNC", [r"\n"]), ]), re.DOTALL | re.MULTILINE) @@ -70,6 +75,7 @@ def make_pat(): "CASE_SOFTKW": "KEYWORD", "CASE_DEFAULT_UNDERSCORE": "KEYWORD", "CASE_SOFTKW2": "KEYWORD", + "LAZY_SOFTKW": "KEYWORD", } diff --git a/Lib/idlelib/config.py b/Lib/idlelib/config.py index d10c88a43f9..82afd6c4926 100644 --- a/Lib/idlelib/config.py +++ b/Lib/idlelib/config.py @@ -73,8 +73,9 @@ def GetOptionList(self, section): def Load(self): "Load the configuration file from disk." - if self.file: - self.read(self.file) + if self.file and os.path.exists(self.file): + with open(self.file, encoding='utf-8', errors='replace') as f: + self.read_file(f) class IdleUserConfParser(IdleConfParser): """ @@ -133,10 +134,10 @@ def Save(self): if fname and fname[0] != '#': if not self.IsEmpty(): try: - cfgFile = open(fname, 'w') + cfgFile = open(fname, 'w', encoding='utf-8') except OSError: os.unlink(fname) - cfgFile = open(fname, 'w') + cfgFile = open(fname, 'w', encoding='utf-8') with cfgFile: self.write(cfgFile) elif os.path.exists(self.file): @@ -476,34 +477,58 @@ def GetExtensionKeys(self, extensionName): Keybindings come from GetCurrentKeySet() active key dict, where previously used bindings are disabled. """ - keysName = extensionName + '_cfgBindings' - activeKeys = self.GetCurrentKeySet() - extKeys = {} - if self.defaultCfg['extensions'].has_section(keysName): - eventNames = self.defaultCfg['extensions'].GetOptionList(keysName) - for eventName in eventNames: - event = '<<' + eventName + '>>' - binding = activeKeys[event] - extKeys[event] = binding - return extKeys - - def __GetRawExtensionKeys(self,extensionName): + bindings_section = f'{extensionName}_cfgBindings' + current_keyset = self.GetCurrentKeySet() + extension_keys = {} + + event_names = set() + if self.userCfg['extensions'].has_section(bindings_section): + event_names |= set( + self.userCfg['extensions'].GetOptionList(bindings_section) + ) + if self.defaultCfg['extensions'].has_section(bindings_section): + event_names |= set( + self.defaultCfg['extensions'].GetOptionList(bindings_section) + ) + + for event_name in event_names: + event = f'<<{event_name}>>' + binding = current_keyset.get(event, None) + if binding is None: + continue + extension_keys[event] = binding + return extension_keys + + def __GetRawExtensionKeys(self, extension_name): """Return dict {configurable extensionName event : keybinding list}. Events come from default config extension_cfgBindings section. Keybindings list come from the splitting of GetOption, which tries user config before default config. """ - keysName = extensionName+'_cfgBindings' - extKeys = {} - if self.defaultCfg['extensions'].has_section(keysName): - eventNames = self.defaultCfg['extensions'].GetOptionList(keysName) - for eventName in eventNames: - binding = self.GetOption( - 'extensions', keysName, eventName, default='').split() - event = '<<' + eventName + '>>' - extKeys[event] = binding - return extKeys + bindings_section = f'{extension_name}_cfgBindings' + extension_keys = {} + + event_names = set() + if self.userCfg['extensions'].has_section(bindings_section): + event_names |= set( + self.userCfg['extensions'].GetOptionList(bindings_section) + ) + if self.defaultCfg['extensions'].has_section(bindings_section): + event_names |= set( + self.defaultCfg['extensions'].GetOptionList(bindings_section) + ) + + for event_name in event_names: + binding = self.GetOption( + 'extensions', + bindings_section, + event_name, + default='', + ).split() + event = f'<<{event_name}>>' + extension_keys[event] = binding + return extension_keys def GetExtensionBindings(self, extensionName): """Return dict {extensionName event : active or defined keybinding}. @@ -512,18 +537,30 @@ def GetExtensionBindings(self, extensionName): configurable events (from default config) to GetOption splits, as in self.__GetRawExtensionKeys. """ - bindsName = extensionName + '_bindings' - extBinds = self.GetExtensionKeys(extensionName) - #add the non-configurable bindings - if self.defaultCfg['extensions'].has_section(bindsName): - eventNames = self.defaultCfg['extensions'].GetOptionList(bindsName) - for eventName in eventNames: - binding = self.GetOption( - 'extensions', bindsName, eventName, default='').split() - event = '<<' + eventName + '>>' - extBinds[event] = binding - - return extBinds + bindings_section = f'{extensionName}_bindings' + extension_keys = self.GetExtensionKeys(extensionName) + + # add the non-configurable bindings + event_names = set() + if self.userCfg['extensions'].has_section(bindings_section): + event_names |= set( + self.userCfg['extensions'].GetOptionList(bindings_section) + ) + if self.defaultCfg['extensions'].has_section(bindings_section): + event_names |= set( + self.defaultCfg['extensions'].GetOptionList(bindings_section) + ) + + for event_name in event_names: + binding = self.GetOption( + 'extensions', + bindings_section, + event_name, + default='' + ).split() + event = f'<<{event_name}>>' + extension_keys[event] = binding + return extension_keys def GetKeyBinding(self, keySetName, eventStr): """Return the keybinding list for keySetName eventStr. diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index e618ef07a90..a5d1aaeb5a9 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -1146,8 +1146,8 @@ def create_page_keys(self): selected keyset. The keybindings are loaded in load_keys_list() and are pairs of (event, [keys]) where keys can be a list of one or more key combinations to bind to the same event. - Mouse button 1 click invokes on_bindingslist_select(), which - allows button_new_keys to be clicked. + Mouse button 1 click or Up or Down key invokes + on_bindingslist_select(), which allows button_new_keys to be clicked. So, an item is selected in listbindings, which activates button_new_keys, and clicking button_new_keys calls function @@ -1221,9 +1221,12 @@ def create_page_keys(self): scroll_target_y = Scrollbar(frame_target) scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL) self.bindingslist = Listbox( - frame_target, takefocus=FALSE, exportselection=FALSE) + frame_target, takefocus=True, exportselection=FALSE) self.bindingslist.bind('', self.on_bindingslist_select) + self.bindingslist.bind('', self.on_bindingslist_select) + self.bindingslist.bind('', + self.on_bindingslist_select) scroll_target_y['command'] = self.bindingslist.yview scroll_target_x['command'] = self.bindingslist.xview self.bindingslist['yscrollcommand'] = scroll_target_y.set @@ -1427,7 +1430,14 @@ def save_as_new_key_set(self): self.create_new_key_set(new_keys_name) def on_bindingslist_select(self, event): - "Activate button to assign new keys to selected action." + """Activate button to assign new keys to selected action. + + Event can result from either mouse click or Up or Down key. + The keys move the selection, but not the anchor used by + get_new_keys and var_changed_keybinding. + """ + if event.type.name == 'KeyRelease': + self.bindingslist.selection_anchor(ACTIVE) self.button_new_keys.state(('!disabled',)) def create_new_key_set(self, new_key_set_name): @@ -1465,9 +1475,8 @@ def load_keys_list(self, keyset_name): An action/key binding can be selected to change the key binding. """ - reselect = False + list_index = 0 if self.bindingslist.curselection(): - reselect = True list_index = self.bindingslist.index(ANCHOR) keyset = idleConf.GetKeySet(keyset_name) # 'set' is dict mapping virtual event to list of key events. @@ -1482,10 +1491,11 @@ def load_keys_list(self, keyset_name): if bind_name in changes['keys'][keyset_name]: key = changes['keys'][keyset_name][bind_name] self.bindingslist.insert(END, bind_name+' - '+key) - if reselect: - self.bindingslist.see(list_index) - self.bindingslist.select_set(list_index) - self.bindingslist.select_anchor(list_index) + self.bindingslist.see(list_index) + self.bindingslist.select_set(list_index) + self.bindingslist.select_anchor(list_index) + self.bindingslist.activate(list_index) + self.button_new_keys.state(('!disabled',)) @staticmethod def save_new_key_set(keyset_name, keyset): @@ -1960,12 +1970,15 @@ def create_page_extensions(self): def load_extensions(self): "Fill self.extensions with data from the default and user configs." self.extensions = {} + for ext_name in idleConf.GetExtensions(active_only=False): # Former built-in extensions are already filtered out. self.extensions[ext_name] = [] for ext_name in self.extensions: - opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name)) + default = set(self.ext_defaultCfg.GetOptionList(ext_name)) + user = set(self.ext_userCfg.GetOptionList(ext_name)) + opt_list = sorted(default | user) # Bring 'enable' options to the beginning of the list. enables = [opt_name for opt_name in opt_list @@ -1975,8 +1988,12 @@ def load_extensions(self): opt_list = enables + opt_list for opt_name in opt_list: - def_str = self.ext_defaultCfg.Get( - ext_name, opt_name, raw=True) + if opt_name in default: + def_str = self.ext_defaultCfg.Get( + ext_name, opt_name, raw=True) + else: + def_str = self.ext_userCfg.Get( + ext_name, opt_name, raw=True) try: def_obj = {'True':True, 'False':False}[def_str] opt_type = 'bool' @@ -2054,10 +2071,11 @@ def set_extension_value(self, section, opt): default = opt['default'] value = opt['var'].get().strip() or default opt['var'].set(value) - # if self.defaultCfg.has_section(section): - # Currently, always true; if not, indent to return. - if (value == default): + + # Only save option in user config if it differs from the default + if self.ext_defaultCfg.has_section(section) and value == default: return self.ext_userCfg.RemoveOption(section, name) + # Set the option. return self.ext_userCfg.SetOption(section, name, value) @@ -2116,6 +2134,8 @@ def create_frame_help(self): scroll_helplist['command'] = self.helplist.yview self.helplist['yscrollcommand'] = scroll_helplist.set self.helplist.bind('', self.help_source_selected) + self.helplist.bind('', self.help_source_selected) + self.helplist.bind('', self.help_source_selected) frame_buttons = Frame(self) self.button_helplist_edit = Button( @@ -2138,7 +2158,14 @@ def create_frame_help(self): self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5) def help_source_selected(self, event): - "Handle event for selecting additional help." + """Handle event for selecting additional help. + + Event can result from either mouse click or Up or Down key. + The keys move the selection, but not the anchor used by + helplist_item_edit and helplist_item_remove. + """ + if event.type.name == 'KeyRelease': + self.helplist.selection_anchor(ACTIVE) self.set_add_delete_state() def set_add_delete_state(self): @@ -2257,7 +2284,11 @@ def make_callback(var, config): "Return default callback function to add values to changes instance." def default_callback(*params): "Add config values to changes instance." - changes.add_option(*config, var.get()) + value = var.get() + # A blanked int entry is an empty string; do not save it as an + # invalid config value (gh-83653). + if value != '': + changes.add_option(*config, value) return default_callback def attach(self): diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 3128934763a..a040d791bde 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -312,6 +312,9 @@ def __init__(self, flist=None, filename=None, key=None, root=None): else: self.update_menu_state('options', '*ine*umbers', 'disabled') + self.mtime = self.last_mtime() + text_frame.bind('', self.focus_in_event) + def handle_winconfig(self, event=None): self.set_width() @@ -857,9 +860,8 @@ def RemoveKeybindings(self): self.text.event_delete(event, *keylist) for extensionName in self.get_standard_extension_names(): xkeydefs = idleConf.GetExtensionBindings(extensionName) - if xkeydefs: - for event, keylist in xkeydefs.items(): - self.text.event_delete(event, *keylist) + for event, keylist in xkeydefs.items(): + self.text.event_delete(event, *keylist) def ApplyKeybindings(self): """Apply the virtual, configurable keybindings. @@ -1027,6 +1029,8 @@ def get_saved(self): def set_saved(self, flag): self.undo.set_saved(flag) + if flag: + self.mtime = self.last_mtime() def reset_undo(self): self.undo.reset_undo() @@ -1112,6 +1116,21 @@ def _close(self): # unless override: unregister from flist, terminate if last window self.close_hook() + def last_mtime(self): + file = self.io.filename + return os.path.getmtime(file) if file else 0 + + def focus_in_event(self, event): + mtime = self.last_mtime() + if self.mtime != mtime: + self.mtime = mtime + if self. askyesno( + 'Reload', '"%s"\n\nThis script has been modified by another program.' + '\nDo you want to reload it?' % self.io.filename, parent=self.text): + self.io.loadfile(self.io.filename) + else: + self.set_saved(False) + def load_extensions(self): self.extensions = {} self.load_standard_extensions() @@ -1685,19 +1704,10 @@ def get_accelerator(keydefs, eventname): return s -def fixwordbreaks(root): - # On Windows, tcl/tk breaks 'words' only on spaces, as in Command Prompt. - # We want Motif style everywhere. See #21474, msg218992 and followup. - tk = root.tk - tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded - tk.call('set', 'tcl_wordchars', r'\w') - tk.call('set', 'tcl_nonwordchars', r'\W') - - -def _editor_window(parent): # htest # - # error if close master window first - timer event, after script - root = parent - fixwordbreaks(root) +def _editor_window(root): # htest # + # Error if close master window first - timer event, after script + from util import fix_word_breaks + fix_word_breaks(root) if sys.argv[1:]: filename = sys.argv[1] else: diff --git a/Lib/idlelib/filelist.py b/Lib/idlelib/filelist.py index e27e5d32a0f..384908f911f 100644 --- a/Lib/idlelib/filelist.py +++ b/Lib/idlelib/filelist.py @@ -112,12 +112,12 @@ def canonize(self, filename): def _test(): # TODO check and convert to htest + # Maybe redundant with test_filelist.FileListTest.test_new_empty. from tkinter import Tk - from idlelib.editor import fixwordbreaks - from idlelib.run import fix_scaling + from idlelib.util import fix_scaling, fix_word_breaks root = Tk() fix_scaling(root) - fixwordbreaks(root) + fix_word_breaks(root) root.withdraw() flist = FileList(root) flist.new() diff --git a/Lib/idlelib/grep.py b/Lib/idlelib/grep.py index 42048ff2395..5d604faec23 100644 --- a/Lib/idlelib/grep.py +++ b/Lib/idlelib/grep.py @@ -40,6 +40,20 @@ def grep(text, io=None, flist=None): dialog.open(text, searchphrase, io) +def default_glob(path): + """Return the initial "In files:" pattern for a file path (gh-80504). + + Always include a full directory so that grep output shows which + directory was searched. + """ + dir, base = os.path.split(path) + dir = os.path.abspath(dir) # An empty dir becomes the current directory. + head, tail = os.path.splitext(base) + if not tail: + tail = ".py" + return os.path.join(dir, "*" + tail) + + def walk_error(msg): "Handle os.walk error." print(msg) @@ -103,11 +117,7 @@ def open(self, text, searchphrase, io=None): path = io.filename or "" else: path = "" - dir, base = os.path.split(path) - head, tail = os.path.splitext(base) - if not tail: - tail = ".py" - self.globvar.set(os.path.join(dir, "*" + tail)) + self.globvar.set(default_glob(path)) def create_entries(self): "Create base entry widgets and add widget for search path." diff --git a/Lib/idlelib/idle_test/README.txt b/Lib/idlelib/idle_test/README.txt index cacd06db873..4af274a81e7 100644 --- a/Lib/idlelib/idle_test/README.txt +++ b/Lib/idlelib/idle_test/README.txt @@ -18,7 +18,7 @@ The idle directory, idlelib, has over 60 xyz.py files. The idle_test subdirectory contains test_xyz.py for each implementation file xyz.py. To add a test for abc.py, open idle_test/template.py and immediately Save As test_abc.py. Insert 'abc' on the first line, and replace -'zzdummy' with 'abc. +'zzdummy' with 'abc'. Remove the imports of requires and tkinter if not needed. Otherwise, add to the tkinter imports as needed. diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py index b63ff9ec287..778e5c3d84e 100644 --- a/Lib/idlelib/idle_test/htest.py +++ b/Lib/idlelib/idle_test/htest.py @@ -216,9 +216,9 @@ 'file': 'browser', 'kwds': {}, 'msg': textwrap.dedent(""" - "Inspect names of module, class(with superclass if applicable), - "methods and functions. Toggle nested items. Double clicking - "on items prints a traceback for an exception that is ignored.""") + Inspect names of module, class(with superclass if applicable), + methods and functions. Toggle nested items. Double clicking + on items prints a traceback for an exception that is ignored.""") } _multistatus_bar_spec = { diff --git a/Lib/idlelib/idle_test/template.py b/Lib/idlelib/idle_test/template.py index 725a55b9c47..6c3e8307693 100644 --- a/Lib/idlelib/idle_test/template.py +++ b/Lib/idlelib/idle_test/template.py @@ -1,6 +1,6 @@ "Test , coverage %." -from idlelib import zzdummy +from idlelib import zzdummy # Replace with the module to test. import unittest from test.support import requires from tkinter import Tk @@ -17,11 +17,12 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): cls.root.update_idletasks() -## for id in cls.root.tk.call('after', 'info'): +## for id in cls.root.after_info(): ## cls.root.after_cancel(id) # Need for EditorWindow. cls.root.destroy() del cls.root + @unittest.skip('Dummy test') def test_init(self): self.assertTrue(True) diff --git a/Lib/idlelib/idle_test/test_autocomplete.py b/Lib/idlelib/idle_test/test_autocomplete.py index a811363c18d..88af3efc35b 100644 --- a/Lib/idlelib/idle_test/test_autocomplete.py +++ b/Lib/idlelib/idle_test/test_autocomplete.py @@ -230,16 +230,14 @@ def test_fetch_completions(self): # For file completion, a large list containing all files in the path, # and a small list containing files that do not start with '.'. acp = self.autocomplete - small, large = acp.fetch_completions( - '', ac.ATTRS) - if hasattr(__main__, '__file__') and __main__.__file__ != ac.__file__: - self.assertNotIn('AutoComplete', small) # See issue 36405. - # Test attributes - s, b = acp.fetch_completions('', ac.ATTRS) - self.assertLess(len(small), len(large)) - self.assertTrue(all(filter(lambda x: x.startswith('_'), s))) - self.assertTrue(any(filter(lambda x: x.startswith('_'), b))) + # Test current module (what='') attributes. + small, large = acp.fetch_completions('', ac.ATTRS) + if hasattr(__main__, '__file__') and __main__.__file__ != ac.__file__: + self.assertNotIn('AutoComplete', small) # See gh-80586. + self.assertLess(len(small), len(large)) # Not equal + self.assertFalse(any(a[:1] == '_' for a in small)) + self.assertTrue(any(a[:1] == '_' for a in large)) # Test smalll should respect to __all__. with patch.dict('__main__.__dict__', {'__all__': ['a', 'b']}): diff --git a/Lib/idlelib/idle_test/test_codecontext.py b/Lib/idlelib/idle_test/test_codecontext.py index 6969ad73b01..3f070da0223 100644 --- a/Lib/idlelib/idle_test/test_codecontext.py +++ b/Lib/idlelib/idle_test/test_codecontext.py @@ -127,7 +127,7 @@ def test_del_with_timer(self): timer = self.cc.t1 = self.text.after(10000, lambda: None) self.cc.__del__() with self.assertRaises(TclError) as cm: - self.root.tk.call('after', 'info', timer) + self.root.after_info(timer) self.assertIn("doesn't exist", str(cm.exception)) def test_reload(self): @@ -151,7 +151,7 @@ def test_toggle_code_context_event(self): eq(cc.context['bg'], self.highlight_cfg['background']) eq(cc.context.get('1.0', 'end-1c'), '') eq(cc.editwin.label, 'Hide Code Context') - eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer') + eq(self.root.after_info(self.cc.t1)[1], 'timer') # Toggle off. toggle() diff --git a/Lib/idlelib/idle_test/test_colorizer.py b/Lib/idlelib/idle_test/test_colorizer.py index 40800df97b0..8a6ab527f93 100644 --- a/Lib/idlelib/idle_test/test_colorizer.py +++ b/Lib/idlelib/idle_test/test_colorizer.py @@ -260,7 +260,7 @@ def test_notify_range(self): # Colorizing already scheduled. save_id = color.after_id - eq(self.root.tk.call('after', 'info', save_id)[1], 'timer') + eq(self.root.after_info(save_id)[1], 'timer') self.assertFalse(color.colorizing) self.assertFalse(color.stop_colorizing) self.assertTrue(color.allow_colorizing) @@ -277,7 +277,7 @@ def test_notify_range(self): color.notify_range('1.0', '1.0+3c') self.assertTrue(color.stop_colorizing) self.assertIsNotNone(color.after_id) - eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer') + eq(self.root.after_info(color.after_id)[1], 'timer') # New event scheduled. self.assertNotEqual(color.after_id, save_id) @@ -297,7 +297,7 @@ def test_toggle_colorize_event(self): self.assertFalse(color.colorizing) self.assertFalse(color.stop_colorizing) self.assertTrue(color.allow_colorizing) - eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer') + eq(self.root.after_info(color.after_id)[1], 'timer') # Toggle colorizing off. color.toggle_colorize_event() @@ -324,7 +324,7 @@ def test_toggle_colorize_event(self): # Toggle on while colorizing not in progress. color.colorizing = False color.toggle_colorize_event() - eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer') + eq(self.root.after_info(color.after_id)[1], 'timer') self.assertFalse(color.colorizing) self.assertTrue(color.stop_colorizing) self.assertTrue(color.allow_colorizing) @@ -363,7 +363,7 @@ def test_recolorize(self, mock_recmain): mock_recmain.assert_called() eq(mock_recmain.call_count, 1) # Rescheduled when TODO tag still exists. - eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer') + eq(self.root.after_info(color.after_id)[1], 'timer') # No changes to text, so no scheduling added. text.tag_remove('TODO', '1.0', 'end') @@ -542,6 +542,24 @@ def test_case_soft_keyword(self): self._assert_highlighting('case _:', {'KEYWORD': [('1.0', '1.4'), ('1.5', '1.6')]}) + def test_lazy_soft_keyword(self): # lazy new in 3.15. + # lazy followed by import + self._assert_highlighting('lazy import foo', + {'KEYWORD': [('1.0', '1.4'), + ('1.5', '1.11')]}) + self._assert_highlighting(' lazy import foo', + {'KEYWORD': [('1.4', '1.8'), + ('1.9', '1.15')]}) + + # lazy followed by from + self._assert_highlighting('lazy from foo import bar', + {'KEYWORD': [('1.0', '1.4'), ('1.5', '1.9'), + ('1.14', '1.20')]}) + + # lazy not followed by import/from (not highlighted) + self._assert_highlighting('lazy = 1', {}) + self._assert_highlighting('lazy foo', {}) + def test_long_multiline_string(self): source = textwrap.dedent('''\ """a diff --git a/Lib/idlelib/idle_test/test_config.py b/Lib/idlelib/idle_test/test_config.py index 6d75cf7aa67..028d9f9dbd6 100644 --- a/Lib/idlelib/idle_test/test_config.py +++ b/Lib/idlelib/idle_test/test_config.py @@ -454,9 +454,6 @@ def test_get_extension_keys(self): self.assertEqual(idleConf.GetExtensionKeys('ZzDummy'), {'<>': ['']}) userextn.remove_section('ZzDummy') -# need option key test -## key = [''] if sys.platform == 'darwin' else [''] -## eq(conf.GetExtensionKeys('ZoomHeight'), {'<>': key}) def test_get_extension_bindings(self): userextn.read_string(''' @@ -491,19 +488,27 @@ def test_get_keybinding(self): def test_get_current_keyset(self): current_platform = sys.platform conf = self.mock_config() - - # Ensure that platform isn't darwin - sys.platform = 'some-linux' - self.assertEqual(conf.GetCurrentKeySet(), conf.GetKeySet(conf.CurrentKeys())) - - # This should not be the same, since replace ') + self.assertEqual(conf.GetKeySet(conf.CurrentKeys())['<>'], + ['']) + sys.platform = 'darwin' + self.assertEqual(conf.GetCurrentKeySet()['<>'], + ['']) + finally: + # Restore platform + sys.platform = current_platform def test_get_keyset(self): conf = self.mock_config() @@ -762,8 +767,29 @@ def test_save_default(self): # Cover 2nd and 3rd false branches. changes = self.changes changes.add_option('main', 'Indent', 'use-spaces', '1') # save_option returns False; cfg_type_changed remains False. + self.assertFalse(changes.save_all()) + self.assertFalse(usermain.has_option('Indent', 'use-spaces')) + self.assertEqual(changes, self.empty) - # TODO: test that save_all calls usercfg Saves. + def test_save_all_saves_files(self): + eq = self.assertEqual + changes = self.changes + for config in testcfg.values(): + config.Save = Func() + try: + # 'main', 'highlight' and 'keys' are saved even if unchanged. + self.assertFalse(changes.save_all()) + eq([testcfg[cfgtype].Save.called + for cfgtype in ('main', 'highlight', 'keys', 'extensions')], + [1, 1, 1, 0]) + # A changed configuration type is saved too. + changes.add_option('extensions', 'Esec', 'eitem', 'eval') + self.assertTrue(changes.save_all()) + eq(testcfg['extensions'].Save.called, 1) + finally: + for config in testcfg.values(): + del config.Save + userextn.remove_section('Esec') def test_delete_section(self): changes = self.load() diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py index 2773ed7ce61..367c63dc011 100644 --- a/Lib/idlelib/idle_test/test_configdialog.py +++ b/Lib/idlelib/idle_test/test_configdialog.py @@ -3,12 +3,15 @@ Half the class creates dialog, half works with user customizations. """ from idlelib import configdialog +from idlelib.editor import EditorWindow from test.support import requires requires('gui') import unittest from unittest import mock from idlelib.idle_test.mock_idle import Func -from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL) +from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL, + EventType) +from types import SimpleNamespace from idlelib import config from idlelib.configdialog import idleConf, changes, tracers @@ -51,12 +54,27 @@ def tearDownModule(): class ConfigDialogTest(unittest.TestCase): + # The methods tested here are mocked out in the tests below. + + def setUp(self): + self.parent = dialog.parent + self.instance = mock.create_autospec(EditorWindow, instance=True) + dialog.parent = mock.Mock(instance_dict={self.instance: []}) + + def tearDown(self): + dialog.parent = self.parent def test_deactivate_current_config(self): - pass + dialog.deactivate_current_config() + self.instance.RemoveKeybindings.assert_called_once_with() - def activate_config_changes(self): - pass + def test_activate_config_changes(self): + dialog.activate_config_changes() + for name in ('ResetColorizer', 'ResetFont', 'set_notabs_indentwidth', + 'ApplyKeybindings', 'reset_help_menu_entries', + 'update_cursor_blink'): + with self.subTest(name=name): + getattr(self.instance, name).assert_called_once_with() class ButtonTest(unittest.TestCase): @@ -149,8 +167,8 @@ def test_fontlist_key(self): font = d.fontlist.get('active') # Test Down key. - fontlist.focus_force() fontlist.update() + fontlist.focus_force() fontlist.event_generate('') fontlist.event_generate('') @@ -159,8 +177,8 @@ def test_fontlist_key(self): self.assertIn(d.font_name.get(), down_font.lower()) # Test Up key. - fontlist.focus_force() fontlist.update() + fontlist.focus_force() fontlist.event_generate('') fontlist.event_generate('') @@ -244,6 +262,7 @@ def test_font_set(self): def test_set_samples(self): d = self.page + self.addCleanup(setattr, d, 'set_samples', Func()) # Re-mask for other tests. del d.set_samples # Unmask method for test orig_samples = d.font_sample, d.highlight_sample d.font_sample, d.highlight_sample = {}, {} @@ -257,7 +276,6 @@ def test_set_samples(self): self.assertTrue(d.font_sample == d.highlight_sample == expected) d.font_sample, d.highlight_sample = orig_samples - d.set_samples = Func() # Re-mask for other tests. class HighPageTest(unittest.TestCase): @@ -307,7 +325,7 @@ def test_load_theme_cfg(self): # builtinlist sets variable builtin_name to the CurrentTheme default. eq(d.builtin_name.get(), 'IDLE Classic') eq(d.custom_name.get(), '- no custom themes -') - eq(d.custom_theme_on.state(), ('disabled',)) + self.assertIn('disabled', d.custom_theme_on.state()) eq(d.set_theme_type.called, 1) eq(d.paint_theme_sample.called, 1) eq(d.set_highlight_target.called, 1) @@ -495,6 +513,7 @@ def test_highlight_sample_b1_motion(self): def test_set_theme_type(self): eq = self.assertEqual d = self.page + self.addCleanup(setattr, d, 'set_theme_type', Func()) # Re-mask method. del d.set_theme_type # Builtin theme selected. @@ -502,16 +521,15 @@ def test_set_theme_type(self): d.set_theme_type() eq(d.builtinlist['state'], NORMAL) eq(d.customlist['state'], DISABLED) - eq(d.button_delete_custom.state(), ('disabled',)) + self.assertIn('disabled', d.button_delete_custom.state()) # Custom theme selected. d.theme_source.set(False) d.set_theme_type() eq(d.builtinlist['state'], DISABLED) - eq(d.custom_theme_on.state(), ('selected',)) + self.assertNotIn('disabled', d.custom_theme_on.state()) eq(d.customlist['state'], NORMAL) - eq(d.button_delete_custom.state(), ()) - d.set_theme_type = Func() + self.assertNotIn('disabled', d.button_delete_custom.state()) def test_get_color(self): eq = self.assertEqual @@ -637,24 +655,23 @@ def test_create_new_and_save_new(self): def test_set_highlight_target(self): eq = self.assertEqual d = self.page + self.addCleanup(setattr, d, 'set_highlight_target', Func()) # Re-mask method. del d.set_highlight_target # Target is cursor. d.highlight_target.set('Cursor') - eq(d.fg_on.state(), ('disabled', 'selected')) - eq(d.bg_on.state(), ('disabled',)) + self.assertIn('disabled', d.fg_on.state()) + self.assertIn('disabled', d.bg_on.state()) self.assertTrue(d.fg_bg_toggle) eq(d.set_color_sample.called, 1) # Target is not cursor. d.highlight_target.set('Comment') - eq(d.fg_on.state(), ('selected',)) - eq(d.bg_on.state(), ()) + self.assertNotIn('disabled', d.fg_on.state()) + self.assertNotIn('disabled', d.bg_on.state()) self.assertTrue(d.fg_bg_toggle) eq(d.set_color_sample.called, 2) - d.set_highlight_target = Func() - def test_set_color_sample_binding(self): d = self.page scs = d.set_color_sample @@ -667,6 +684,7 @@ def test_set_color_sample_binding(self): def test_set_color_sample(self): d = self.page + self.addCleanup(setattr, d, 'set_color_sample', Func()) # Re-mask method. del d.set_color_sample d.highlight_target.set('Selected Text') d.fg_bg_toggle.set(True) @@ -674,7 +692,6 @@ def test_set_color_sample(self): self.assertEqual( d.style.lookup(d.frame_color_set['style'], 'background'), d.highlight_sample.tag_cget('hilite', 'foreground')) - d.set_color_sample = Func() def test_paint_theme_sample(self): eq = self.assertEqual @@ -745,7 +762,7 @@ def test_delete_custom(self): eq(yesno.called, 2) self.assertNotIn(theme_name, highpage) eq(idleConf.GetSectionList('user', 'highlight'), [theme_name2]) - eq(d.custom_theme_on.state(), ()) + self.assertNotIn('disabled', d.custom_theme_on.state()) eq(d.custom_name.get(), theme_name2) eq(dialog.deactivate_current_config.called, 1) eq(dialog.activate_config_changes.called, 1) @@ -758,7 +775,7 @@ def test_delete_custom(self): eq(yesno.called, 3) self.assertNotIn(theme_name, highpage) eq(idleConf.GetSectionList('user', 'highlight'), []) - eq(d.custom_theme_on.state(), ('disabled',)) + self.assertIn('disabled', d.custom_theme_on.state()) eq(d.custom_name.get(), '- no custom themes -') eq(dialog.deactivate_current_config.called, 2) eq(dialog.activate_config_changes.called, 2) @@ -809,7 +826,7 @@ def test_load_key_cfg(self): # builtinlist sets variable builtin_name to the CurrentKeys default. eq(d.builtin_name.get(), 'IDLE Classic OSX') eq(d.custom_name.get(), '- no custom keys -') - eq(d.custom_keyset_on.state(), ('disabled',)) + self.assertIn('disabled', d.custom_keyset_on.state()) eq(d.set_keys_type.called, 1) eq(d.load_keys_list.called, 1) eq(d.load_keys_list.args, ('IDLE Classic OSX', )) @@ -930,6 +947,7 @@ def test_keybinding(self): def test_set_keys_type(self): eq = self.assertEqual d = self.page + self.addCleanup(setattr, d, 'set_keys_type', Func()) # Re-mask method. del d.set_keys_type # Builtin keyset selected. @@ -937,16 +955,15 @@ def test_set_keys_type(self): d.set_keys_type() eq(d.builtinlist['state'], NORMAL) eq(d.customlist['state'], DISABLED) - eq(d.button_delete_custom_keys.state(), ('disabled',)) + self.assertIn('disabled', d.button_delete_custom_keys.state()) # Custom keyset selected. d.keyset_source.set(False) d.set_keys_type() eq(d.builtinlist['state'], DISABLED) - eq(d.custom_keyset_on.state(), ('selected',)) + self.assertNotIn('disabled', d.custom_keyset_on.state()) eq(d.customlist['state'], NORMAL) - eq(d.button_delete_custom_keys.state(), ()) - d.set_keys_type = Func() + self.assertNotIn('disabled', d.button_delete_custom_keys.state()) def test_get_new_keys(self): eq = self.assertEqual @@ -1043,7 +1060,15 @@ def test_on_bindingslist_select(self): b.event_generate('', x=x, y=y) b.event_generate('', x=x, y=y) self.assertEqual(b.get('anchor'), 'find') - self.assertEqual(d.button_new_keys.state(), ()) + self.assertNotIn('disabled', d.button_new_keys.state()) + + # gh-75234: Up and Down keys move the active item, but not the + # anchor; the handler moves the anchor. + d.button_new_keys.state(('disabled',)) + b.activate(0) + d.on_bindingslist_select(SimpleNamespace(type=EventType.KeyRelease)) + self.assertEqual(b.get('anchor'), 'copy') + self.assertNotIn('disabled', d.button_new_keys.state()) def test_create_new_key_set_and_save_new_key_set(self): eq = self.assertEqual @@ -1080,6 +1105,7 @@ def test_load_keys_list(self): eq = self.assertEqual d = self.page gks = idleConf.GetKeySet = Func() + self.addCleanup(setattr, d, 'load_keys_list', Func()) # Re-mask method. del d.load_keys_list b = d.bindingslist @@ -1094,11 +1120,14 @@ def test_load_keys_list(self): 'force-open-completions - ', 'spam - ') - # No current selection. + # No current selection: select the first item. + d.button_new_keys.state(('disabled',)) d.load_keys_list('my keys') eq(b.get(0, 'end'), expected) - eq(b.get('anchor'), '') - eq(b.curselection(), ()) + eq(b.get('anchor'), 'copy - ') + eq(b.curselection(), (0, )) + eq(b.index('active'), 0) + self.assertNotIn('disabled', d.button_new_keys.state()) # Check selection. b.selection_set(1) @@ -1115,7 +1144,6 @@ def test_load_keys_list(self): eq(b.get(0, 'end'), expected) eq(b.get('anchor'), 'spam - ') eq(b.curselection(), (2, )) - d.load_keys_list = Func() del idleConf.GetKeySet @@ -1156,7 +1184,7 @@ def test_delete_custom_keys(self): eq(yesno.called, 2) self.assertNotIn(keyset_name, keyspage) eq(idleConf.GetSectionList('user', 'keys'), [keyset_name2]) - eq(d.custom_keyset_on.state(), ()) + self.assertNotIn('disabled', d.custom_keyset_on.state()) eq(d.custom_name.get(), keyset_name2) eq(dialog.deactivate_current_config.called, 1) eq(dialog.activate_config_changes.called, 1) @@ -1169,7 +1197,7 @@ def test_delete_custom_keys(self): eq(yesno.called, 3) self.assertNotIn(keyset_name, keyspage) eq(idleConf.GetSectionList('user', 'keys'), []) - eq(d.custom_keyset_on.state(), ('disabled',)) + self.assertIn('disabled', d.custom_keyset_on.state()) eq(d.custom_name.get(), '- no custom keys -') eq(dialog.deactivate_current_config.called, 2) eq(dialog.activate_config_changes.called, 2) @@ -1296,13 +1324,123 @@ def test_context(self): self.assertEqual(extpage, {'CodeContext': {'maxlines': '1'}}) -#unittest.skip("Nothing here yet TODO") class ExtPageTest(unittest.TestCase): - """Test that the help source list works correctly.""" + """Test that the extension page works correctly. + + The page loads the options of each extension from the default and + user config files, displays those of the selected extension, and + saves the changed ones to the user config file. ZzDummy is the + only extension shipped with IDLE. + """ @classmethod def setUpClass(cls): - page = dialog.extpage + page = cls.page = dialog.extpage dialog.note.select(page) + page.update() + + def setUp(self): + # Restore the option vars changed by the previous test. + self.page.load_extensions() + + def tearDown(self): + self.page.ext_userCfg.remove_section('ZzDummy') + + def test_load_extensions(self): + eq = self.assertEqual + extensions = self.page.extensions + eq(list(extensions), sorted(idleConf.GetExtensions(active_only=False))) + opts = extensions['ZzDummy'] + # The 'enable' options come first, the others follow, both sorted. + eq([opt['name'] for opt in opts], + ['enable', 'enable_editor', 'enable_shell', 'z-text']) + eq([opt['type'] for opt in opts], ['bool', 'bool', 'bool', None]) + eq([opt['default'] for opt in opts], ['False', 'True', 'False', 'Z']) + eq([opt['value'] for opt in opts], [False, True, False, 'Z']) + for opt in opts: + with self.subTest(name=opt['name']): + eq(opt['var'].get(), str(opt['value'])) + + def test_load_extensions_enable_first(self): + # The 'enable' options come first even if they sort last. + page = self.page + page.ext_userCfg.SetOption('ZzDummy', 'a-text', 'A') + page.load_extensions() + self.assertEqual([opt['name'] for opt in page.extensions['ZzDummy']], + ['enable', 'enable_editor', 'enable_shell', + 'a-text', 'z-text']) + + def test_load_extensions_user_value(self): + # A user option overrides the default value, but not the default. + page = self.page + page.ext_userCfg.SetOption('ZzDummy', 'z-text', 'user') + page.load_extensions() + opt = page.extensions['ZzDummy'][-1] + self.assertEqual(opt['name'], 'z-text') + self.assertEqual(opt['default'], 'Z') + self.assertEqual(opt['value'], 'user') + self.assertEqual(opt['var'].get(), 'user') + + def test_extension_selected(self): + eq = self.assertEqual + page = self.page + frame = page.config_frame['ZzDummy'] + # Deselecting hides the options of the current extension. + page.extension_list.selection_clear(0, 'end') + page.extension_selected(None) + eq(page.current_extension, None) + eq(page.details_frame.cget('text'), '') + eq(frame.winfo_manager(), '') + # Selecting shows the options of the selected extension. + page.extension_list.selection_set(0) + page.extension_selected(None) + eq(page.current_extension, 'ZzDummy') + eq(page.details_frame.cget('text'), 'ZzDummy') + eq(frame.winfo_manager(), 'grid') + + def test_set_extension_value_changed(self): + page = self.page + opt = page.extensions['ZzDummy'][-1] # z-text, default 'Z'. + opt['var'].set('user') + self.assertTrue(page.set_extension_value('ZzDummy', opt)) + self.assertEqual(page.ext_userCfg.Get('ZzDummy', 'z-text'), 'user') + # Saving the same value again is not a change. + self.assertFalse(page.set_extension_value('ZzDummy', opt)) + + def test_set_extension_value_default(self): + page = self.page + opt = page.extensions['ZzDummy'][-1] + # The default value is not saved in the user config file. + opt['var'].set('Z') + self.assertFalse(page.set_extension_value('ZzDummy', opt)) + self.assertFalse(page.ext_userCfg.has_option('ZzDummy', 'z-text')) + # Setting it back to the default removes the user option. + page.ext_userCfg.SetOption('ZzDummy', 'z-text', 'user') + self.assertTrue(page.set_extension_value('ZzDummy', opt)) + self.assertFalse(page.ext_userCfg.has_option('ZzDummy', 'z-text')) + + def test_set_extension_value_empty(self): + # An empty value is replaced with the default. + page = self.page + opt = page.extensions['ZzDummy'][-1] + opt['var'].set(' ') + self.assertFalse(page.set_extension_value('ZzDummy', opt)) + self.assertEqual(opt['var'].get(), 'Z') + + def test_save_all_changed_extensions(self): + page = self.page + page.ext_userCfg.Save = Func() + try: + # Nothing is saved if nothing is changed. + page.save_all_changed_extensions() + self.assertEqual(page.ext_userCfg.Save.called, 0) + page.extensions['ZzDummy'][0]['var'].set('True') + page.extensions['ZzDummy'][-1]['var'].set('user') + page.save_all_changed_extensions() + self.assertEqual(page.ext_userCfg.Save.called, 1) + self.assertEqual(page.ext_userCfg.Get('ZzDummy', 'enable'), 'True') + self.assertEqual(page.ext_userCfg.Get('ZzDummy', 'z-text'), 'user') + finally: + del page.ext_userCfg.Save class HelpSourceTest(unittest.TestCase): @@ -1365,25 +1503,25 @@ def test_set_add_delete_state(self): # Call with 0 items, 1 unselected item, 1 selected item. eq = self.assertEqual fr = self.frame + self.addCleanup(setattr, fr, 'set_add_delete_state', Func()) # Re-mask method. del fr.set_add_delete_state # Unmask method. sad = fr.set_add_delete_state h = fr.helplist h.delete(0, 'end') sad() - eq(fr.button_helplist_edit.state(), ('disabled',)) - eq(fr.button_helplist_remove.state(), ('disabled',)) + self.assertIn('disabled', fr.button_helplist_edit.state()) + self.assertIn('disabled', fr.button_helplist_remove.state()) h.insert(0, 'source') sad() - eq(fr.button_helplist_edit.state(), ('disabled',)) - eq(fr.button_helplist_remove.state(), ('disabled',)) + self.assertIn('disabled', fr.button_helplist_edit.state()) + self.assertIn('disabled', fr.button_helplist_remove.state()) h.selection_set(0) sad() - eq(fr.button_helplist_edit.state(), ()) - eq(fr.button_helplist_remove.state(), ()) - fr.set_add_delete_state = Func() # Mask method. + self.assertNotIn('disabled', fr.button_helplist_edit.state()) + self.assertNotIn('disabled', fr.button_helplist_remove.state()) def test_helplist_item_add(self): # Call without and twice with HelpSource result. @@ -1459,8 +1597,29 @@ def test_helplist_item_remove(self): eq(fr.user_helplist, []) self.assertTrue(fr.upc.called == fr.set.called == 1) + def test_helplist_item_remove_keyboard_selection(self): + # gh-75234: Up and Down keys move the active item, but not the + # anchor; the handler moves the anchor. + eq = self.assertEqual + fr = self.frame + fr.helplist.delete(0, 'end') + fr.helplist.insert('end', 'name1', 'name2') + fr.helplist.selection_anchor(0) + fr.helplist.selection_set(1) + fr.helplist.activate(1) + fr.user_helplist.clear() + fr.user_helplist.extend([('name1', 'file1'), ('name2', 'file2')]) + fr.set.called = fr.upc.called = 0 + + fr.help_source_selected(SimpleNamespace(type=EventType.KeyRelease)) + eq(fr.helplist.get('anchor'), 'name2') + fr.helplist_item_remove() + eq(fr.helplist.get(0, 'end'), ('name1',)) + eq(fr.user_helplist, [('name1', 'file1')]) + def test_update_help_changes(self): fr = self.frame + self.addCleanup(setattr, fr, 'update_help_changes', Func()) # Re-mask method. del fr.update_help_changes fr.user_helplist.clear() fr.user_helplist.append(('name1', 'file1')) @@ -1469,7 +1628,6 @@ def test_update_help_changes(self): fr.update_help_changes() self.assertEqual(mainpage['HelpFiles'], {'1': 'name1;file1', '2': 'name2;file2'}) - fr.update_help_changes = Func() class VarTraceTest(unittest.TestCase): @@ -1544,6 +1702,17 @@ def test_make_callback(self): self.assertEqual(changes['main']['section']['option'], '42') changes.clear() + # gh-83653: a blank int entry is not saved as bad config data. + sv = StringVar(root) + cb = self.tracers.make_callback(sv, ('main', 'section', 'option')) + sv.set('') + cb() + self.assertNotIn('section', changes['main']) + sv.set('5') + cb() + self.assertEqual(changes['main']['section']['option'], '5') + changes.clear() + def test_attach_detach(self): tr = self.tracers iv = tr.add(self.iv, self.var_changed_increment) diff --git a/Lib/idlelib/idle_test/test_editmenu.py b/Lib/idlelib/idle_test/test_editmenu.py index 17478473a3d..0d1eb905491 100644 --- a/Lib/idlelib/idle_test/test_editmenu.py +++ b/Lib/idlelib/idle_test/test_editmenu.py @@ -7,7 +7,7 @@ import tkinter as tk from tkinter import ttk import unittest -from idlelib import pyshell +from idlelib.util import fix_x11_paste class PasteTest(unittest.TestCase): '''Test pasting into widgets that allow pasting. @@ -18,7 +18,7 @@ class PasteTest(unittest.TestCase): def setUpClass(cls): cls.root = root = tk.Tk() cls.root.withdraw() - pyshell.fix_x11_paste(root) + fix_x11_paste(root) cls.text = tk.Text(root) cls.entry = tk.Entry(root) cls.tentry = ttk.Entry(root) diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index 0dfe2f3c58b..873637f67de 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -20,7 +20,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): cls.root.update_idletasks() - for id in cls.root.tk.call('after', 'info'): + for id in cls.root.after_info(): cls.root.after_cancel(id) cls.root.destroy() del cls.root @@ -114,7 +114,7 @@ def tearDownClass(cls): cls.window._close() del cls.window cls.root.update_idletasks() - for id in cls.root.tk.call('after', 'info'): + for id in cls.root.after_info(): cls.root.after_cancel(id) cls.root.destroy() del cls.root @@ -212,6 +212,9 @@ def test_searcher(self): class RMenuTest(unittest.TestCase): + # Test selection-rclick interaction in right_click_event and + # rmenu_check_copy(cut) status settings. These are part of the rmenu + # functions common to all text windows with context windows. @classmethod def setUpClass(cls): @@ -219,13 +222,15 @@ def setUpClass(cls): cls.root = Tk() cls.root.withdraw() cls.window = Editor(root=cls.root) + cls.text = cls.window.text + cls.window.rmenu = cls.DummyRMenu @classmethod def tearDownClass(cls): cls.window._close() - del cls.window + del cls.window, cls.text cls.root.update_idletasks() - for id in cls.root.tk.call('after', 'info'): + for id in cls.root.after_info(): cls.root.after_cancel(id) cls.root.destroy() del cls.root @@ -233,8 +238,54 @@ def tearDownClass(cls): class DummyRMenu: def tk_popup(x, y): pass - def test_rclick(self): - pass + def click(self): + """Simulate a right click at(text pixel 0,0). + """ + Event = namedtuple('Event', ['x', 'y', 'x_root', 'y_root']) + event = Event(0, 0, 0, 0) + self.assertEqual(self.window.right_menu_event(event), 'break') + # This assertion should be moved to a new method that also + # text that dummy rmenu.tk_popup is called. + + def test_rclick_not_in_a_selection(self): + # Like left click, 'insert' moves to click and any selection is deleted. + eq = self.assertEqual + text = self.text + insert(text, 'one two three') + # Selection exists but not clicked. + text.tag_add('sel', '1.4', '1.7') # 'two' selected. + text.mark_set('insert', '1.7') # Outside of selection. + self.click() + eq(text.tag_ranges('sel'), ()) + eq(text.index('insert'), '1.0') + # No selection to click, same result. + text.mark_set('insert', '1.8') + index = self.click() + eq(text.tag_ranges('sel'), ()) + eq(text.index('insert'), '1.0') + + def test_rclick_inside_selection(self): + # Unlike left click, selection is not deleted. + eq = self.assertEqual + text = self.text + insert(text, 'one two three') # 'insert' at 1.13. + # The selection contains the clicked character. + text.tag_add('sel', '1.0', '1.3') # Select 'one'. + text.mark_set('insert', '1.3') # If select rightward, 'insert' at 1.3. + self.click() + eq((text.index('sel.first'), text.index('sel.last')), ('1.0', '1.3')) + eq(text.index('insert'), '1.3') + + def test_rmenu_check_copy(self): + # copy and cut only valid for click inside selection. + eq = self.assertEqual + text = self.text + insert(text, 'one two three') + eq(self.window.rmenu_check_copy(), 'disabled') + eq(self.window.rmenu_check_cut(), 'disabled') + text.tag_add('sel', '1.0', '1.3') # Includes '1.0' click. + eq(self.window.rmenu_check_copy(), 'normal') + eq(self.window.rmenu_check_cut(), 'normal') if __name__ == '__main__': diff --git a/Lib/idlelib/idle_test/test_filelist.py b/Lib/idlelib/idle_test/test_filelist.py index 731f1975e50..e22cc3eced3 100644 --- a/Lib/idlelib/idle_test/test_filelist.py +++ b/Lib/idlelib/idle_test/test_filelist.py @@ -16,7 +16,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): cls.root.update_idletasks() - for id in cls.root.tk.call('after', 'info'): + for id in cls.root.after_info(): cls.root.after_cancel(id) cls.root.destroy() del cls.root diff --git a/Lib/idlelib/idle_test/test_grep.py b/Lib/idlelib/idle_test/test_grep.py index d67dba76911..a8c265b9d91 100644 --- a/Lib/idlelib/idle_test/test_grep.py +++ b/Lib/idlelib/idle_test/test_grep.py @@ -1,16 +1,19 @@ """ !Changing this line will break Test_findfile.test_found! Non-gui unit tests for grep.GrepDialog methods. -dummy_command calls grep_it calls findfiles. +default_command calls grep_it calls findfiles. An exception raised in one method will fail callers. Otherwise, tests are mostly independent. -Currently only test grep_it, coverage 51%. +Currently test default_command and grep_it, coverage 51%. """ from idlelib import grep import unittest +from unittest import mock from test.support import captured_stdout from idlelib.idle_test.mock_tk import Var +import io import os import re +import sys class Dummy_searchengine: @@ -21,22 +24,48 @@ class Dummy_searchengine: def getpat(self): return self._pat + def getprog(self): + return self._prog + searchengine = Dummy_searchengine() class Dummy_grep: - # Methods tested - #default_command = GrepDialog.default_command + # Simplifications: 1. Don't initialize superclass SearchEngineBase + # with searchengine. 2. Use directly set class vars instead of + # instance vars set from the GUI. + globvar = Var('') # File name. + recvar = Var(False) # Recurse down directories? + engine = searchengine # Pattern and flags. + # Methods tested. grep_it = grep.GrepDialog.grep_it - # Other stuff needed - recvar = Var(False) - engine = searchengine + default_command = grep.GrepDialog.default_command # Uses grep_it. + def close(self): # gui method pass _grep = Dummy_grep() +class DefaultGlobTest(unittest.TestCase): + + def test_no_path(self): + # gh-80504: an unsaved editor or the Shell has no path, so the + # pattern uses the current directory, not just '*.py'. + self.assertEqual(grep.default_glob(''), + os.path.join(os.getcwd(), '*.py')) + + def test_full_path(self): + path = os.path.join(os.path.abspath(os.sep), 'ab', 'foo.py') + self.assertEqual(grep.default_glob(path), + os.path.join(os.path.dirname(path), '*.py')) + + def test_other_extension(self): + path = os.path.join(os.path.abspath(os.sep), 'ab', 'foo.txt') + self.assertEqual(grep.default_glob(path), + os.path.join(os.path.dirname(path), '*.txt')) + + class FindfilesTest(unittest.TestCase): @classmethod @@ -147,9 +176,48 @@ def test_found(self): class Default_commandTest(unittest.TestCase): - # To write this, move outwin import to top of GrepDialog - # so it can be replaced by captured_stdout in class setup/teardown. - pass + # default_command searches with grep_it, tested above, writing to an + # OutputWindow, which is replaced here by a text buffer. + + def setUp(self): + self.dialog = Dummy_grep() + self.dialog.top = mock.Mock() # For top.bell(). + self.dialog.flist = 'flist' + self.dialog.globvar = Var(value=__file__) + searchengine._pat = pat = 'xyz*' * 7 # Not in this file. + searchengine._prog = re.compile(pat) + + def default_command(self): + "Return the mock OutputWindow class and what was written to it." + save = sys.stdout + with mock.patch('idlelib.outwin.OutputWindow') as OutputWindow: + OutputWindow.return_value = out = io.StringIO() + self.dialog.default_command() + self.assertIs(sys.stdout, save) # Restored even when not replaced. + return OutputWindow, out.getvalue() + + def test_no_pattern(self): + # An invalid pattern is reported by getprog, not here. + searchengine._prog = None + OutputWindow, output = self.default_command() + OutputWindow.assert_not_called() + self.assertEqual(output, '') + self.dialog.top.bell.assert_not_called() + + def test_no_path(self): + self.dialog.globvar = Var(value='') + OutputWindow, output = self.default_command() + self.dialog.top.bell.assert_called_once() + OutputWindow.assert_not_called() + self.assertEqual(output, '') + + def test_search(self): + OutputWindow, output = self.default_command() + OutputWindow.assert_called_once_with('flist') # The flist argument. + lines = output.split('\n') + self.assertIn(searchengine._pat, lines[0]) + self.assertEqual(lines[1], 'No hits.') + self.dialog.top.bell.assert_not_called() if __name__ == '__main__': diff --git a/Lib/idlelib/idle_test/test_help.py b/Lib/idlelib/idle_test/test_help.py index ebb02b5c0d8..ebbaceb2eb7 100644 --- a/Lib/idlelib/idle_test/test_help.py +++ b/Lib/idlelib/idle_test/test_help.py @@ -4,7 +4,6 @@ import unittest from test.support import requires requires('gui') -from os.path import abspath, dirname, join from tkinter import Tk diff --git a/Lib/idlelib/idle_test/test_iomenu.py b/Lib/idlelib/idle_test/test_iomenu.py index e0642cf0cab..3ef9fcbc5bb 100644 --- a/Lib/idlelib/idle_test/test_iomenu.py +++ b/Lib/idlelib/idle_test/test_iomenu.py @@ -1,11 +1,13 @@ "Test , coverage 17%." from idlelib import iomenu +import os import unittest +from unittest import mock from test.support import requires -from tkinter import Tk +from tkinter import Tk, TclVersion from idlelib.editor import EditorWindow -from idlelib import util +from idlelib import macosx, util from idlelib.idle_test.mock_idle import Func # Fail if either tokenize.open and t.detect_encoding does not exist. @@ -23,15 +25,14 @@ def setUpClass(cls): cls.root = Tk() cls.root.withdraw() cls.editwin = EditorWindow(root=cls.root) - cls.io = iomenu.IOBinding(cls.editwin) + cls.io = cls.editwin.io @classmethod def tearDownClass(cls): - cls.io.close() cls.editwin._close() del cls.editwin cls.root.update_idletasks() - for id in cls.root.tk.call('after', 'info'): + for id in cls.root.after_info(): cls.root.after_cancel(id) # Need for EditorWindow. cls.root.destroy() del cls.root @@ -39,6 +40,17 @@ def tearDownClass(cls): def test_init(self): self.assertIs(self.io.editwin, self.editwin) + def test_defaultfilename_tilde(self): + # gh-59568: escape a leading tilde for Tcl older than 9. + io = self.io + with mock.patch.object(io, 'filename', os.path.join('dir', '~file.py')): + dirname, base = io.defaultfilename() + self.assertEqual(dirname, 'dir') + if TclVersion < 9 and not macosx.isAquaTk(): + self.assertEqual(base, os.path.join(os.curdir, '~file.py')) + else: + self.assertEqual(base, '~file.py') + def test_fixnewlines_end(self): eq = self.assertEqual io = self.io diff --git a/Lib/idlelib/idle_test/test_mainmenu.py b/Lib/idlelib/idle_test/test_mainmenu.py index 51d2accfe48..c884256a99e 100644 --- a/Lib/idlelib/idle_test/test_mainmenu.py +++ b/Lib/idlelib/idle_test/test_mainmenu.py @@ -22,6 +22,7 @@ def test_tcl_indexes(self): # On failure, change pattern here and in function(s). # Patterns here have '.*' for re instead of '*' for tcl. for menu, pattern in ( + ('debug', '.*ebugger'), # PyShell.debug_menu_postcommand ('debug', '.*tack.*iewer'), # PyShell.debug_menu_postcommand ('options', '.*ode.*ontext'), # EW.__init__, CodeContext.toggle... ('options', '.*ine.*umbers'), # EW.__init__, EW.toggle...event. diff --git a/Lib/idlelib/idle_test/test_multicall.py b/Lib/idlelib/idle_test/test_multicall.py index 67f28db6b08..db66fdf14ba 100644 --- a/Lib/idlelib/idle_test/test_multicall.py +++ b/Lib/idlelib/idle_test/test_multicall.py @@ -2,7 +2,7 @@ from idlelib import multicall import unittest -from test.support import requires +from test.support import requires, captured_stderr from tkinter import Tk, Text @@ -19,7 +19,7 @@ def setUpClass(cls): def tearDownClass(cls): del cls.mc cls.root.update_idletasks() -## for id in cls.root.tk.call('after', 'info'): +## for id in cls.root.after_info(): ## cls.root.after_cancel(id) # Need for EditorWindow. cls.root.destroy() del cls.root @@ -43,6 +43,52 @@ def test_yview(self): mctext = self.mc(self.root) self.assertIs(mctext.yview.__func__, Text.yview) + def test_valid_binding(self): + # A valid key binding must bind without a warning (cf. gh-55646). + mctext = self.mc(self.root) + with captured_stderr() as stderr: + mctext.event_add('<>', '') + mctext.bind('<>', lambda e: None) + self.assertEqual(stderr.getvalue(), '') + + def test_invalid_triplet_binding(self): + # gh-55646: '' parses into a triplet on every platform, + # so Tk rejects it in bind(). + mctext = self.mc(self.root) + with captured_stderr() as stderr: + mctext.event_add('<>', '') # Must not raise. + mctext.bind('<>', lambda e: None) # Must not raise. + warning = stderr.getvalue() + self.assertIn('invalid key binding', warning) + self.assertIn('test-bad', warning) # The offending action is named. + + def test_invalid_nontriplet_binding(self): + # gh-55646: '' has no valid modifier, so MultiCall does not + # parse it and falls back to Tk's event_add, which rejects it. + mctext = self.mc(self.root) + with captured_stderr() as stderr: + mctext.event_add('<>', '') # Must not raise. + mctext.bind('<>', lambda e: None) # Must not raise. + warning = stderr.getvalue() + self.assertIn('invalid key binding', warning) + self.assertIn('test-bad', warning) # The offending action is named. + + def test_event_delete_unbound_sequence(self): + # gh-89360: deleting a sequence that was not added to a virtual + # event is ignored instead of raising ValueError. + mctext = self.mc(self.root) + mctext.event_add('<>', '') + info = mctext.event_info('<>') + self.assertEqual(len(info), 1) + + # A different sequence, never added: a no-op, not an error. + mctext.event_delete('<>', '') + self.assertEqual(mctext.event_info('<>'), info) + + # The added sequence can still be deleted normally. + mctext.event_delete('<>', '') + self.assertNotIn(info[0], mctext.event_info('<>')) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_outwin.py b/Lib/idlelib/idle_test/test_outwin.py index 0f13363f84f..667c1667465 100644 --- a/Lib/idlelib/idle_test/test_outwin.py +++ b/Lib/idlelib/idle_test/test_outwin.py @@ -41,6 +41,17 @@ def test_ispythonsource(self): self.assertFalse(w.ispythonsource('test.txt')) self.assertFalse(w.ispythonsource(__file__)) + def test_save_defaults_to_text(self): + # gh-65339: output is saved as text, not Python source. + io = self.window.io + self.assertEqual(io.defaultextension, '.txt') + # Text files are offered before Python files. + self.assertEqual(io.save_filetypes[0][0], 'Text files') + + def test_open_defaults_to_python(self): + # gh-158067: File Open still lists Python files first. + self.assertEqual(self.window.io.filetypes[0][0], 'Python files') + def test_window_title(self): self.assertEqual(self.window.top.title(), 'Output' + ' (%s)' % platform.python_version()) diff --git a/Lib/idlelib/idle_test/test_pyshell.py b/Lib/idlelib/idle_test/test_pyshell.py index 706703965bf..0e292e7be87 100644 --- a/Lib/idlelib/idle_test/test_pyshell.py +++ b/Lib/idlelib/idle_test/test_pyshell.py @@ -2,6 +2,7 @@ # Plus coverage of test_warning. Was 20% with test_openshell. from idlelib import pyshell +import os import unittest from test.support import requires from tkinter import Tk @@ -28,6 +29,14 @@ def test_restart_line_narrow(self): self.assertEqual(pyshell.restart_line(width, ''), expect) self.assertEqual(pyshell.restart_line(taglen+2, ''), expect+' =') + def test_fix_user_path(self): + # gh-134300: the idlelib directory is removed, other entries kept. + eq = self.assertEqual + idlelib_dir = os.path.dirname(os.path.abspath(pyshell.__file__)) + eq(pyshell.fix_user_path(['', '/a', idlelib_dir, '/b']), ['', '/a', '/b']) + eq(pyshell.fix_user_path(['/a', '/b']), ['/a', '/b']) + eq(pyshell.fix_user_path([idlelib_dir]), []) + class PyShellFileListTest(unittest.TestCase): @@ -40,7 +49,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): #cls.root.update_idletasks() -## for id in cls.root.tk.call('after', 'info'): +## for id in cls.root.after_info(): ## cls.root.after_cancel(id) # Need for EditorWindow. cls.root.destroy() del cls.root @@ -60,6 +69,46 @@ def test_init(self): ## self.assertIsInstance(ps, pyshell.PyShell) +class PyShellTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.root.withdraw() + cls.shell = pyshell.PyShell(pyshell.PyShellFileList(cls.root)) + + @classmethod + def tearDownClass(cls): + cls.shell.close() + del cls.shell + cls.root.destroy() + del cls.root + + def setUp(self): + text = self.shell.text + self.shell.per.bottom.delete('1.0', 'end') + text.mark_set('iomark', '1.0') # As after begin(). + text.mark_gravity('iomark', 'left') + self.shell.undo.reset_undo() + + def test_output_at_prompt(self): + # gh-75512: output from a thread while a prompt is shown goes + # before the prompt, which stays with the input in progress. + shell = self.shell + text = shell.text + shell.write('x\n', 'stdout') + shell.resetoutput() + text.tag_add('console', 'iomark-1c') # As showprompt() does. + text.insert('end-1c', 'a = (') + shell.write('hel', 'stdout') + shell.write('lo\n', 'stdout') + self.assertEqual(text.get('iomark-6c', 'end-1c'), 'hello\na = (') + self.assertIn('console', text.tag_names('iomark-1c')) + self.assertNotIn('console', text.tag_names('iomark-7c')) + self.assertEqual(shell.shell_sidebar.line_prompts, {3: '>>>'}) + + class PyShellRemoveLastNewlineAndSurroundingWhitespaceTest(unittest.TestCase): regexp = pyshell.PyShell._last_newline_re diff --git a/Lib/idlelib/idle_test/test_replace.py b/Lib/idlelib/idle_test/test_replace.py index 6c07389b29a..5342a3856ef 100644 --- a/Lib/idlelib/idle_test/test_replace.py +++ b/Lib/idlelib/idle_test/test_replace.py @@ -246,32 +246,108 @@ def test_replace_backwards(self): equal(text.get('1.2', '1.5'), 'was') def test_replace_all(self): + # The default mode, forward with wrap around, replaces every + # match, both below and above the current position. + equal = self.assertEqual text = self.text pv = self.engine.patvar rv = self.dialog.replvar replace_all = self.dialog.replace_all - text.insert('insert', '\n') - text.insert('insert', text.get('1.0', 'end')*100) - pv.set('is') - rv.set('was') + text.delete('1.0', 'end') + text.insert('1.0', 'a\na\na\n') + text.mark_set('insert', '2.1') + pv.set('a') + rv.set('b') replace_all() - self.assertNotIn('is', text.get('1.0', 'end')) + equal(text.get('1.0', '3.end'), 'b\nb\nb') # Wrapped around. + # An empty regular expression is reported as an error. self.engine.revar.set(True) pv.set('') replace_all() self.assertIn('error', showerror.title) self.assertIn('Empty', showerror.message) + # An invalid replacement expression is reported as an error, + # and nothing is replaced. + text.delete('1.0', 'end') + text.insert('1.0', 'asT') pv.set('[s][T]') rv.set('\\') replace_all() + self.assertIn('error', showerror.title) + self.assertIn('Invalid Replace Expression', showerror.message) + equal(text.get('1.0', '1.end'), 'asT') + # A pattern that is not present replaces nothing. self.engine.revar.set(False) + text.delete('1.0', 'end') + text.insert('1.0', 'unchanged') pv.set('text which is not present') rv.set('foobar') replace_all() + equal(text.get('1.0', '1.end'), 'unchanged') + + def test_replace_all_backwards_no_wrap(self): + # gh-71956: 'up' without wrap replaces all matches from the start + # of the text down to the current position, not just one up. + equal = self.assertEqual + text = self.text + pv = self.engine.patvar + rv = self.dialog.replvar + replace_all = self.dialog.replace_all + self.engine.backvar.set(True) + self.engine.wrapvar.set(False) + + text.delete('1.0', 'end') + text.insert('1.0', 'a\na\na\n') + text.mark_set('insert', '2.1') + pv.set('a') + rv.set('b') + replace_all() + equal(text.get('1.0', '1.end'), 'b') # Above the cursor. + equal(text.get('2.0', '2.end'), 'b') # At the cursor. + equal(text.get('3.0', '3.end'), 'a') # Below the cursor, untouched. + + def test_replace_all_forwards_no_wrap(self): + # 'down' without wrap replaces all matches from the current + # position to the end of the text, and none before it. + equal = self.assertEqual + text = self.text + pv = self.engine.patvar + rv = self.dialog.replvar + replace_all = self.dialog.replace_all + self.engine.backvar.set(False) + self.engine.wrapvar.set(False) + + text.delete('1.0', 'end') + text.insert('1.0', 'a\na\na\n') + text.mark_set('insert', '2.1') + pv.set('a') + rv.set('b') + replace_all() + equal(text.get('1.0', '1.end'), 'a') # Before the cursor, untouched. + equal(text.get('2.0', '2.end'), 'a') # Before the cursor, untouched. + equal(text.get('3.0', '3.end'), 'b') # After the cursor. + + def test_replace_all_backwards_wrap(self): + # With wrap around, an 'up' search also replaces every match. + equal = self.assertEqual + text = self.text + pv = self.engine.patvar + rv = self.dialog.replvar + replace_all = self.dialog.replace_all + self.engine.backvar.set(True) + self.engine.wrapvar.set(True) + + text.delete('1.0', 'end') + text.insert('1.0', 'a\na\na\n') + text.mark_set('insert', '2.1') + pv.set('a') + rv.set('b') + replace_all() + equal(text.get('1.0', '3.end'), 'b\nb\nb') def test_default_command(self): text = self.text diff --git a/Lib/idlelib/idle_test/test_rpc.py b/Lib/idlelib/idle_test/test_rpc.py index 81eff398c72..15c3ed14b8f 100644 --- a/Lib/idlelib/idle_test/test_rpc.py +++ b/Lib/idlelib/idle_test/test_rpc.py @@ -1,9 +1,28 @@ "Test rpc, coverage 20%." from idlelib import rpc +import socket +import struct import unittest +class SocketIOTest(unittest.TestCase): + + def test_reconnect_discards_partial_packet(self): + # gh-89544: RPCClient.accept() reinitializes the SocketIO after + # a restart; a partially received packet must not be kept. + old_sock, old_peer = socket.socketpair() + new_sock, new_peer = socket.socketpair() + with old_sock, old_peer, new_sock, new_peer: + sockio = rpc.SocketIO(old_sock) + old_peer.sendall(struct.pack('', tb) + + @force_not_colorized + def test_print_exception_group_max_width(self): + excs = [ValueError(f'v{i}') for i in range(20)] + try: + raise ExceptionGroup('eg', excs) + except ExceptionGroup: + tb = self._capture_exception() + + self.assertIn('+---------------- 15 ----------------', tb) + self.assertIn('+---------------- ... ----------------', tb) + self.assertIn('and 5 more exceptions', tb) + self.assertNotIn('+---------------- 16 ----------------', tb) + + @force_not_colorized + def test_print_exception_group_max_depth(self): + def make_nested(depth): + if depth == 0: + return ValueError('leaf') + return ExceptionGroup(f'level{depth}', + [make_nested(depth - 1)]) + + try: + raise make_nested(15) + except ExceptionGroup: + tb = self._capture_exception() + + self.assertIn('... (max_group_depth is 10)', tb) + self.assertIn('ExceptionGroup: level15', tb) + self.assertNotIn('ValueError: leaf', tb) + # StdioFile tests. class S(str): diff --git a/Lib/idlelib/idle_test/test_runscript.py b/Lib/idlelib/idle_test/test_runscript.py index 5fc60185a66..1e47f402d50 100644 --- a/Lib/idlelib/idle_test/test_runscript.py +++ b/Lib/idlelib/idle_test/test_runscript.py @@ -2,6 +2,7 @@ from idlelib import runscript import unittest +from unittest import mock from test.support import requires from tkinter import Tk from idlelib.editor import EditorWindow @@ -18,7 +19,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): cls.root.update_idletasks() - for id in cls.root.tk.call('after', 'info'): + for id in cls.root.after_info(): cls.root.after_cancel(id) # Need for EditorWindow. cls.root.destroy() del cls.root @@ -28,6 +29,26 @@ def test_init(self): sb = runscript.ScriptBinding(ew) ew._close() + def test_run_module_event_shell_busy_no_restart(self): + # gh-82183: running without restarting the busy shell aborts. + ew = EditorWindow(root=self.root) + sb = runscript.ScriptBinding(ew) + sb.getfilename = mock.Mock(return_value='test.py') + sb.checksyntax = mock.Mock(return_value='code') + sb.tabnanny = mock.Mock(return_value=True) + sb.shell = shell = mock.Mock() + shell.executing = True + interp = shell.interp + with mock.patch.object(runscript, 'CustomRun') as customrun: + # Restart shell unchecked. + customrun.return_value.result = (['arg'], False) + result = sb.run_module_event(None, customize=True) + self.assertEqual(result, 'break') + interp.display_executing_dialog.assert_called_once() + interp.restart_subprocess.assert_not_called() + interp.runcode.assert_not_called() + ew._close() + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 4157a4b4dcd..df84554c4f1 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -11,11 +11,10 @@ from idlelib.idle_test.tkinter_testing_utils import run_in_tk_mainloop from idlelib.delegator import Delegator -from idlelib.editor import fixwordbreaks from idlelib.percolator import Percolator import idlelib.pyshell -from idlelib.pyshell import fix_x11_paste, PyShell, PyShellFileList -from idlelib.run import fix_scaling +from idlelib.pyshell import PyShell, PyShellFileList +from idlelib.util import fix_scaling, fix_word_breaks, fix_x11_paste import idlelib.sidebar from idlelib.sidebar import get_end_linenumber, get_lineno @@ -403,7 +402,7 @@ def setUpClass(cls): root.withdraw() fix_scaling(root) - fixwordbreaks(root) + fix_word_breaks(root) fix_x11_paste(root) cls.flist = flist = PyShellFileList(root) @@ -690,11 +689,15 @@ def test_mousewheel(self): last_lineno = get_end_linenumber(text) self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) - # Delta for , whose meaning is platform-dependent. + # Simulate a mouse wheel notch. Tk 8.7 replaced the X11 + # / wheel events with (whose delta is + # platform-dependent); older Tk on X11 still uses the button events. + x11_buttons = (sidebar.canvas._windowingsystem == 'x11' + and tk.TkVersion < 8.7) delta = 1 if sidebar.canvas._windowingsystem == 'aqua' else 120 # Scroll up. - if sidebar.canvas._windowingsystem == 'x11': + if x11_buttons: sidebar.canvas.event_generate('', x=0, y=0) else: sidebar.canvas.event_generate('', x=0, y=0, delta=delta) @@ -702,7 +705,7 @@ def test_mousewheel(self): self.assertIsNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) # Scroll back down. - if sidebar.canvas._windowingsystem == 'x11': + if x11_buttons: sidebar.canvas.event_generate('', x=0, y=0) else: sidebar.canvas.event_generate('', x=0, y=0, delta=-delta) diff --git a/Lib/idlelib/idle_test/test_stackviewer.py b/Lib/idlelib/idle_test/test_stackviewer.py index 55f510382bf..341f158031d 100644 --- a/Lib/idlelib/idle_test/test_stackviewer.py +++ b/Lib/idlelib/idle_test/test_stackviewer.py @@ -21,7 +21,7 @@ def setUpClass(cls): def tearDownClass(cls): cls.root.update_idletasks() -## for id in cls.root.tk.call('after', 'info'): +## for id in cls.root.after_info(): ## cls.root.after_cancel(id) # Need for EditorWindow. cls.root.destroy() del cls.root @@ -35,6 +35,8 @@ def test_init(self): isi(stackviewer.sc, ScrolledCanvas) isi(stackviewer.item, stackviewer.StackTreeItem) isi(stackviewer.node, TreeNode) + top = stackviewer.sc.frame.winfo_toplevel() + self.assertEqual(top.winfo_class(), 'Idle') if __name__ == '__main__': diff --git a/Lib/idlelib/idle_test/test_window.py b/Lib/idlelib/idle_test/test_window.py index 5a2645b9cc2..d8a77817205 100644 --- a/Lib/idlelib/idle_test/test_window.py +++ b/Lib/idlelib/idle_test/test_window.py @@ -29,7 +29,7 @@ def setUpClass(cls): def tearDownClass(cls): window.registry = window.WindowList() cls.root.update_idletasks() -## for id in cls.root.tk.call('after', 'info'): +## for id in cls.root.after_info(): ## cls.root.after_cancel(id) # Need for EditorWindow. cls.root.destroy() del cls.root @@ -39,6 +39,11 @@ def test_init(self): win = window.ListedToplevel(self.root) self.assertIn(win, window.registry) self.assertEqual(win.focused_widget, win) + self.assertEqual(win.winfo_class(), 'Idle') + + def test_init_class_override(self): + win = window.ListedToplevel(self.root, class_='Other') + self.assertEqual(win.winfo_class(), 'Other') if __name__ == '__main__': diff --git a/Lib/idlelib/idle_test/test_zoomheight.py b/Lib/idlelib/idle_test/test_zoomheight.py index aa5bdfb4fbd..3b97c34d4ab 100644 --- a/Lib/idlelib/idle_test/test_zoomheight.py +++ b/Lib/idlelib/idle_test/test_zoomheight.py @@ -21,7 +21,7 @@ def setUpClass(cls): def tearDownClass(cls): cls.editwin._close() cls.root.update_idletasks() - for id in cls.root.tk.call('after', 'info'): + for id in cls.root.after_info(): cls.root.after_cancel(id) # Need for EditorWindow. cls.root.destroy() del cls.root diff --git a/Lib/idlelib/iomenu.py b/Lib/idlelib/iomenu.py index fc502f7fde1..c2412e98fb9 100644 --- a/Lib/idlelib/iomenu.py +++ b/Lib/idlelib/iomenu.py @@ -5,11 +5,13 @@ import tempfile import tokenize +from tkinter import TclVersion from tkinter import filedialog from tkinter import messagebox from tkinter.simpledialog import askstring # loadfile encoding. from idlelib.config import idleConf +from idlelib import macosx from idlelib.util import py_extensions py_extensions = ' '.join("*"+ext for ext in py_extensions) @@ -384,7 +386,18 @@ def print_window(self, event): ("All files", "*"), ) + save_filetypes = filetypes + + # Output windows (Shell, Output) are not Python source, so they save + # with text files listed first and default to ".txt" (gh-65339). + text_filetypes = ( + ("Text files", "*.txt", "TEXT"), + ("Python files", py_extensions, "TEXT"), + ("All files", "*"), + ) + defaultextension = '.py' if sys.platform == 'darwin' else '' + text_defaultextension = '.txt' def askopenfile(self): dir, base = self.defaultfilename("open") @@ -396,7 +409,12 @@ def askopenfile(self): def defaultfilename(self, mode="open"): if self.filename: - return os.path.split(self.filename) + dirname, base = os.path.split(self.filename) + if base[:1] == '~' and TclVersion < 9 and not macosx.isAquaTk(): + # Tcl before version 9 substitutes a leading tilde in + # a file name (gh-59568). The macOS dialog does not. + base = os.path.join(os.curdir, base) + return dirname, base elif self.dirname: return self.dirname, "" else: @@ -411,7 +429,7 @@ def asksavefile(self): if not self.savedialog: self.savedialog = filedialog.SaveAs( parent=self.text, - filetypes=self.filetypes, + filetypes=self.save_filetypes, defaultextension=self.defaultextension) filename = self.savedialog.show(initialdir=dir, initialfile=base) return filename diff --git a/Lib/idlelib/macosx.py b/Lib/idlelib/macosx.py index 332952f4572..428e49f3eb7 100644 --- a/Lib/idlelib/macosx.py +++ b/Lib/idlelib/macosx.py @@ -39,7 +39,7 @@ def _init_tk_type(): _tk_type = "xquartz" elif 'aqua' not in ws: _tk_type = "other" - elif 'AppKit' in root.tk.call('winfo', 'server', '.'): + elif 'AppKit' in root.winfo_server(): _tk_type = "cocoa" else: _tk_type = "carbon" diff --git a/Lib/idlelib/multicall.py b/Lib/idlelib/multicall.py index 41f81813113..5589884e2af 100644 --- a/Lib/idlelib/multicall.py +++ b/Lib/idlelib/multicall.py @@ -310,6 +310,17 @@ def _triplet_to_sequence(triplet): else: return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>' + +def warn_bad_binding(virtual, sequence, err): + # gh-55646: warn instead of crashing on an invalid key binding. + action = virtual[2:-2] if virtual[:2] == '<<' and virtual[-2:] == '>>' \ + else virtual + print(f'Warning: ignoring invalid key binding {sequence!r} ' + f'for {action!r}: {err}. ' + f'Please reconfigure it in the IDLE Settings dialog.', + file=sys.stderr) + + _multicall_dict = {} def MultiCallCreator(widget): """Return a MultiCall class which inherits its methods from the @@ -343,8 +354,17 @@ def bind(self, sequence=None, func=None, add=None): self.__binders[triplet[1]].unbind(triplet, ei[0]) ei[0] = func if ei[0] is not None: + bad = [] for triplet in ei[1]: - self.__binders[triplet[1]].bind(triplet, func) + try: + self.__binders[triplet[1]].bind(triplet, func) + except tkinter.TclError as err: + warn_bad_binding(sequence, + _triplet_to_sequence(triplet), + err) + bad.append(triplet) + for triplet in bad: # Drop the invalid sequences. + ei[1].remove(triplet) else: self.__eventinfo[sequence] = [func, []] return widget.bind(self, sequence, func, add) @@ -371,10 +391,19 @@ def event_add(self, virtual, *sequences): triplet = _parse_sequence(seq) if triplet is None: #print("Tkinter event_add(%s)" % seq, file=sys.__stderr__) - widget.event_add(self, virtual, seq) + try: + widget.event_add(self, virtual, seq) + except tkinter.TclError as err: + warn_bad_binding(virtual, seq, err) + continue # Drop the invalid sequence. else: if func is not None: - self.__binders[triplet[1]].bind(triplet, func) + try: + self.__binders[triplet[1]].bind(triplet, func) + except tkinter.TclError as err: + warn_bad_binding(virtual, + _triplet_to_sequence(triplet), err) + continue # Drop the invalid sequence. triplets.append(triplet) def event_delete(self, virtual, *sequences): @@ -386,10 +415,11 @@ def event_delete(self, virtual, *sequences): if triplet is None: #print("Tkinter event_delete: %s" % seq, file=sys.__stderr__) widget.event_delete(self, virtual, seq) - else: + elif triplet in triplets: if func is not None: self.__binders[triplet[1]].unbind(triplet, func) triplets.remove(triplet) + # Else the sequence is not bound; ignore it (gh-89360). def event_info(self, virtual=None): if virtual is None or virtual not in self.__eventinfo: diff --git a/Lib/idlelib/outwin.py b/Lib/idlelib/outwin.py index 8baa657550d..b9dfeef1736 100644 --- a/Lib/idlelib/outwin.py +++ b/Lib/idlelib/outwin.py @@ -78,6 +78,10 @@ class OutputWindow(EditorWindow): def __init__(self, *args): EditorWindow.__init__(self, *args) self.text.bind("<>", self.goto_file_line) + # Output is not Python source, so save it as text by default + # (gh-65339). + self.io.save_filetypes = self.io.text_filetypes + self.io.defaultextension = self.io.text_defaultextension # Customize EditorWindow def ispythonsource(self, filename): diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 1b7c2af1a92..dd4fe0d437c 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -36,13 +36,14 @@ from idlelib.delegator import Delegator from idlelib import debugger from idlelib import debugger_r -from idlelib.editor import EditorWindow, fixwordbreaks +from idlelib.editor import EditorWindow from idlelib.filelist import FileList from idlelib.outwin import OutputWindow from idlelib import replace from idlelib import rpc from idlelib.run import idle_formatwarning, StdInputFile, StdOutputFile from idlelib.undo import UndoDelegator +from idlelib.util import fix_word_breaks # Default for testing; defaults to True in main() for running. use_subprocess = False @@ -242,12 +243,13 @@ def store_file_breaks(self): breaks = self.breakpoints filename = self.io.filename try: - with open(self.breakpointPath) as fp: + with open(self.breakpointPath, + encoding='utf-8', errors='replace') as fp: lines = fp.readlines() except OSError: lines = [] try: - with open(self.breakpointPath, "w") as new_file: + with open(self.breakpointPath, "w", encoding='utf-8') as new_file: for line in lines: if not line.startswith(filename + '='): new_file.write(line) @@ -272,7 +274,8 @@ def restore_file_breaks(self): if filename is None: return if os.path.isfile(self.breakpointPath): - with open(self.breakpointPath) as fp: + with open(self.breakpointPath, + encoding='utf-8', errors='replace') as fp: lines = fp.readlines() for line in lines: if line.startswith(filename + '='): @@ -405,6 +408,17 @@ def restart_line(width, filename): # See bpo-38141. return tag[:-2] # Remove ' ='. +def fix_user_path(path): + """Return path without the idlelib directory (gh-134300). + + That directory is on sys.path when idle.py is run as a script. + Otherwise user code could import idlelib submodules as top-level + modules, such as "import help". + """ + idlelib_dir = os.path.dirname(os.path.abspath(__file__)) + return [p for p in path if p != idlelib_dir] + + class ModifiedInterpreter(InteractiveInterpreter): def __init__(self, tkconsole): @@ -498,7 +512,6 @@ def restart_subprocess(self, with_cwd=False, filename=''): self.rpcclt.close() self.terminate_subprocess() console = self.tkconsole - was_executing = console.executing console.executing = False self.spawn_subprocess() try: @@ -566,6 +579,7 @@ def transfer_path(self, with_cwd=False): path.extend(sys.path) else: path = sys.path + path = fix_user_path(path) # gh-134300 self.runcommand("""if 1: import sys as _sys @@ -642,7 +656,7 @@ def remote_stack_viewer(self): return item = debugobj_r.StubObjectTreeItem(self.rpcclt, oid) from idlelib.tree import ScrolledCanvas, TreeNode - top = Toplevel(self.tkconsole.root) + top = Toplevel(self.tkconsole.root, class_='Idle') theme = idleConf.CurrentTheme() background = idleConf.GetHighlight(theme, 'normal')['background'] sc = ScrolledCanvas(top, bg=background, highlightthickness=0) @@ -680,6 +694,8 @@ def execfile(self, filename, source=None): def runsource(self, source): "Extend base class method: Stuff the source in the line cache first" + # Remove the highlighting of a previous syntax error (gh-93966). + self.tkconsole.text.tag_remove("ERROR", "1.0", "end") filename = self.stuffsource(source) # at the moment, InteractiveInterpreter expects str assert isinstance(source, str) @@ -716,7 +732,6 @@ def showsyntaxerror(self, filename=None, **kwargs): """ tkconsole = self.tkconsole text = tkconsole.text - text.tag_remove("ERROR", "1.0", "end") type, value, tb = sys.exc_info() msg = getattr(value, 'msg', '') or value or "" lineno = getattr(value, 'lineno', '') or 1 @@ -880,9 +895,9 @@ def __init__(self, flist=None): if ms[2][0] != "shell": ms.insert(2, ("shell", "She_ll")) self.interp = ModifiedInterpreter(self) - if flist is None: + if flist is None: # TODO possible? root and flist in main. root = Tk() - fixwordbreaks(root) + fix_word_breaks(root) root.withdraw() flist = PyShellFileList(root) @@ -1085,6 +1100,7 @@ def open_debugger(self): def debug_menu_postcommand(self): state = 'disabled' if self.executing else 'normal' + self.update_menu_state('debug', '*ebugger', state) self.update_menu_state('debug', '*tack*iewer', state) def beginexecuting(self): @@ -1417,13 +1433,24 @@ def resetoutput(self): self.ctip.remove_calltip_window() def write(self, s, tags=()): + text = self.text + # Move the prompt (the "console" tag on the preceding newline) + # after output which comes while it is shown (gh-75512). + at_prompt = (s and tags in ("stdout", "stderr") + and not self.executing and not self.reading) + if at_prompt: + text.tag_remove("console", "iomark-1c") + text.tag_remove("stdin", "iomark-1c") try: - self.text.mark_gravity("iomark", "right") + text.mark_gravity("iomark", "right") count = OutputWindow.write(self, s, tags, "iomark") - self.text.mark_gravity("iomark", "left") + text.mark_gravity("iomark", "left") except: raise ###pass # ### 11Aug07 KBK if we are expecting exceptions # let's find out what they are and be specific. + if at_prompt and s.endswith('\n'): + text.tag_add("console", "iomark-1c") + self.shell_sidebar.update_sidebar() if self.canceled: self.canceled = False if not use_subprocess: @@ -1451,17 +1478,6 @@ def on_squeezed_expand(self, index, text, tags): self.shell_sidebar.update_sidebar() -def fix_x11_paste(root): - "Make paste replace selection on x11. See issue #5124." - if root._windowingsystem == 'x11': - for cls in 'Text', 'Entry', 'Spinbox': - root.bind_class( - cls, - '<>', - 'catch {%W delete sel.first sel.last}\n' + - root.bind_class(cls, '<>')) - - usage_msg = """\ USAGE: idle [-deins] [-t title] [file]* @@ -1521,6 +1537,7 @@ def main(): from platform import system from idlelib import testing # bool value from idlelib import macosx + from idlelib.util import fix_scaling, fix_x11_paste global flist, root, use_subprocess @@ -1606,7 +1623,6 @@ def main(): NoDefaultRoot() root = Tk(className="Idle") root.withdraw() - from idlelib.run import fix_scaling fix_scaling(root) # set application icon @@ -1628,7 +1644,7 @@ def main(): root.wm_iconphoto(True, *icons) # start editor and/or shell windows: - fixwordbreaks(root) + fix_word_breaks(root) fix_x11_paste(root) flist = PyShellFileList(root) macosx.setupApp(root, flist) diff --git a/Lib/idlelib/replace.py b/Lib/idlelib/replace.py index 3716d841568..93d4f1c0c6c 100644 --- a/Lib/idlelib/replace.py +++ b/Lib/idlelib/replace.py @@ -122,12 +122,13 @@ def _replace_expand(self, m, repl): def replace_all(self, event=None): """Handle the Replace All button. - Search text for occurrences of the Find value and replace - each of them. The 'wrap around' value controls the start - point for searching. If wrap isn't set, then the searching - starts at the first occurrence after the current selection; - if wrap is set, the replacement starts at the first line. - The replacement is always done top-to-bottom in the text. + Search text for occurrences of the Find value and replace each + of them. The 'wrap around' and direction values control which + occurrences are replaced. With wrap around, every occurrence is + replaced. Without it, a forward search replaces occurrences from + the current position to the end of the text, and a backward search + replaces occurrences from the beginning of the text to the current + position. The replacement is always done top-to-bottom. """ prog = self.engine.getprog() if not prog: @@ -142,9 +143,18 @@ def replace_all(self, event=None): text.tag_remove("hit", "1.0", "end") line = res[0] col = res[1].start() + # For a backward search without wrap, replace top-to-bottom from + # the start of the text down to the first match at or above the + # current position (gh-71956). A mark tracks that stop point. + stop = None if self.engine.iswrap(): line = 1 col = 0 + elif self.engine.isback(): + stop = "replace_all_stop" + text.mark_set(stop, "%d.%d" % (line, res[1].end())) + line = 1 + col = 0 ok = True first = last = None # XXX ought to replace circular instead of top-to-bottom when wrapping @@ -152,12 +162,13 @@ def replace_all(self, event=None): while res := self.engine.search_forward( text, prog, line, col, wrap=False, ok=ok): line, m = res - chars = text.get("%d.0" % line, "%d.0" % (line+1)) + i, j = m.span() + if stop is not None and text.compare("%d.%d" % (line, i), ">=", stop): + break orig = m.group() new = self._replace_expand(m, repl) if new is None: break - i, j = m.span() first = "%d.%d" % (line, i) last = "%d.%d" % (line, j) if new == orig: @@ -170,6 +181,8 @@ def replace_all(self, event=None): text.insert(first, new, self.insert_tags) col = i + len(new) ok = False + if stop is not None: + text.mark_unset(stop) text.undo_block_stop() if first and last: self.show_hit(first, last) diff --git a/Lib/idlelib/rpc.py b/Lib/idlelib/rpc.py index 3f0b2230dd1..9af9863d74e 100644 --- a/Lib/idlelib/rpc.py +++ b/Lib/idlelib/rpc.py @@ -139,6 +139,11 @@ def __init__(self, sock, objtable=None, debugging=None): self.objtable = objtable self.responses = {} self.cvars = {} + # Receive buffer state. A new connection must not inherit a + # partially received packet from the old one (gh-89544). + self.buff = b'' + self.bufneed = 4 + self.bufstate = 0 # meaning: 0 => reading count; 1 => reading data def close(self): sock = self.sock @@ -345,10 +350,6 @@ def putmessage(self, message): raise OSError("socket no longer exists") s = s[n:] - buff = b'' - bufneed = 4 - bufstate = 0 # meaning: 0 => reading count; 1 => reading data - def pollpacket(self, wait): self._stage0() if len(self.buff) < self.bufneed: diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py index a30db99a619..f5564ccf90f 100644 --- a/Lib/idlelib/run.py +++ b/Lib/idlelib/run.py @@ -25,6 +25,7 @@ from idlelib import iomenu # encoding from idlelib import rpc # multiple objects from idlelib import stackviewer # StackTreeItem +from idlelib import util # fix_scaling import __main__ import tkinter # Use tcl and, if startup fails, messagebox. @@ -216,7 +217,7 @@ def show_socket_error(err, address): import tkinter from tkinter.messagebox import showerror root = tkinter.Tk() - fix_scaling(root) + util.fix_scaling(root) root.withdraw() showerror( "Subprocess Connection Error", @@ -249,31 +250,94 @@ def print_exception(): sys.last_type, sys.last_value, sys.last_traceback = excinfo sys.last_exc = val seen = set() + exclude = ("run.py", "rpc.py", "threading.py", "queue.py", + "debugger_r.py", "bdb.py") + max_group_width = 15 + max_group_depth = 10 + group_depth = 0 + + def print_exc_group(typ, exc, tb, prefix=""): + nonlocal group_depth + group_depth += 1 + prefix2 = prefix or " " + if group_depth > max_group_depth: + print(f"{prefix2}| ... (max_group_depth is {max_group_depth})", + file=efile) + group_depth -= 1 + return + if tb: + if not prefix: + print(" + Exception Group Traceback (most recent call last):", file=efile) + else: + print(f"{prefix}| Exception Group Traceback (most recent call last):", file=efile) + tbe = traceback.extract_tb(tb) + cleanup_traceback(tbe, exclude) + for line in traceback.format_list(tbe): + for subline in line.rstrip().splitlines(): + print(f"{prefix2}| {subline}", file=efile) + lines = get_message_lines(typ, exc, tb) + for line in lines: + print(f"{prefix2}| {line}", end="", file=efile) + num_excs = len(exc.exceptions) + if num_excs <= max_group_width: + n = num_excs + else: + n = max_group_width + 1 + for i, sub in enumerate(exc.exceptions[:n], 1): + truncated = (i > max_group_width) + first_line_pre = "+-" if i == 1 else " " + title = str(i) if not truncated else '...' + print(f"{prefix2}{first_line_pre}+---------------- {title} ----------------", file=efile) + if truncated: + remaining = num_excs - max_group_width + plural = 's' if remaining > 1 else '' + print(f"{prefix2} | and {remaining} more exception{plural}", + file=efile) + need_print_underline = True + elif id(sub) not in seen: + if not prefix: + print_exc(type(sub), sub, sub.__traceback__, " ") + else: + print_exc(type(sub), sub, sub.__traceback__, prefix + " ") + need_print_underline = not isinstance(sub, BaseExceptionGroup) + else: + print(f"{prefix2} | ", file=efile) + need_print_underline = True + if need_print_underline and i == n: + print(f"{prefix2} +------------------------------------", file=efile) + group_depth -= 1 - def print_exc(typ, exc, tb): + def print_exc(typ, exc, tb, prefix=""): seen.add(id(exc)) context = exc.__context__ cause = exc.__cause__ + prefix2 = f"{prefix}| " if prefix else "" if cause is not None and id(cause) not in seen: - print_exc(type(cause), cause, cause.__traceback__) - print("\nThe above exception was the direct cause " - "of the following exception:\n", file=efile) + print_exc(type(cause), cause, cause.__traceback__, prefix) + print(f"{prefix2}\n{prefix2}The above exception was the direct cause " + f"of the following exception:\n{prefix2}", file=efile) elif (context is not None and not exc.__suppress_context__ and id(context) not in seen): - print_exc(type(context), context, context.__traceback__) - print("\nDuring handling of the above exception, " - "another exception occurred:\n", file=efile) - if tb: - tbe = traceback.extract_tb(tb) - print('Traceback (most recent call last):', file=efile) - exclude = ("run.py", "rpc.py", "threading.py", "queue.py", - "debugger_r.py", "bdb.py") - cleanup_traceback(tbe, exclude) - traceback.print_list(tbe, file=efile) - lines = get_message_lines(typ, exc, tb) - for line in lines: - print(line, end='', file=efile) + print_exc(type(context), context, context.__traceback__, prefix) + print(f"{prefix2}\n{prefix2}During handling of the above exception, " + f"another exception occurred:\n{prefix2}", file=efile) + if isinstance(exc, BaseExceptionGroup): + print_exc_group(typ, exc, tb, prefix=prefix) + else: + if tb: + print(f"{prefix2}Traceback (most recent call last):", file=efile) + tbe = traceback.extract_tb(tb) + cleanup_traceback(tbe, exclude) + if prefix: + for line in traceback.format_list(tbe): + for subline in line.rstrip().splitlines(): + print(f"{prefix}| {subline}", file=efile) + else: + traceback.print_list(tbe, file=efile) + lines = get_message_lines(typ, exc, tb) + for line in lines: + print(f"{prefix2}{line}", end="", file=efile) print_exc(typ, val, tb) @@ -326,16 +390,6 @@ def exit(): sys.exit(0) -def fix_scaling(root): - """Scale fonts on HiDPI displays.""" - import tkinter.font - scaling = float(root.tk.call('tk', 'scaling')) - if scaling > 1.4: - for name in tkinter.font.names(root): - font = tkinter.font.Font(root=root, name=name, exists=True) - size = int(font['size']) - if size < 0: - font['size'] = round(-0.75*size) def fixdoc(fun, text): diff --git a/Lib/idlelib/runscript.py b/Lib/idlelib/runscript.py index 55712e90460..cd52d206c9c 100644 --- a/Lib/idlelib/runscript.py +++ b/Lib/idlelib/runscript.py @@ -139,6 +139,10 @@ def run_module_event(self, event, *, customize=False): return 'break' self.cli_args, restart = run_args if customize else ([], True) interp = self.shell.interp + if self.shell.executing and not restart: + # Cannot run without restarting the busy shell (gh-82183). + interp.display_executing_dialog() + return 'break' if pyshell.use_subprocess and restart: interp.restart_subprocess( with_cwd=False, filename=filename) diff --git a/Lib/idlelib/stackviewer.py b/Lib/idlelib/stackviewer.py index 95042d4debd..268e2f1daeb 100644 --- a/Lib/idlelib/stackviewer.py +++ b/Lib/idlelib/stackviewer.py @@ -11,7 +11,7 @@ def StackBrowser(root, exc, flist=None, top=None): global sc, item, node # For testing. if top is None: - top = tk.Toplevel(root) + top = tk.Toplevel(root, class_='Idle') sc = ScrolledCanvas(top, bg="white", highlightthickness=0) sc.frame.pack(expand=1, fill="both") item = StackTreeItem(exc, flist) diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py index 23f0f4cb502..b326331efa7 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -92,10 +92,10 @@ def __init__(self, parent, contents, wrap='word'): color_config(text) text.focus_set() - self.button_ok = button_ok = Button( + self.button_ok = Button( self, text='Close', command=self.ok, takefocus=False) self.textframe.pack(side='top', expand=True, fill='both') - button_ok.pack(side='bottom') + self.button_ok.pack(side='bottom') def ok(self, event=None): """Dismiss text viewer dialog.""" @@ -127,10 +127,8 @@ def __init__(self, parent, title, contents, modal=True, wrap=WORD, self.geometry(f'=750x500+{x}+{y}') self.title(title) - self.viewframe = ViewFrame(self, contents, wrap=wrap) self.protocol("WM_DELETE_WINDOW", self.ok) - self.button_ok = button_ok = Button(self, text='Close', - command=self.ok, takefocus=False) + self.viewframe = ViewFrame(self, contents, wrap=wrap) self.viewframe.pack(side='top', expand=True, fill='both') self.is_modal = modal diff --git a/Lib/idlelib/util.py b/Lib/idlelib/util.py index e05604ab485..bf88c905e1d 100644 --- a/Lib/idlelib/util.py +++ b/Lib/idlelib/util.py @@ -19,6 +19,20 @@ py_extensions = ('.py', '.pyw', '.pyi') +# fix_x functions seem only needed once per process. + +def fix_scaling(root): # Called in filelist _test, pyshell, and run. + """Scale fonts on HiDPI displays, once per process.""" + import tkinter.font + scaling = float(root.tk.call('tk', 'scaling')) + if scaling > 1.4: + for name in tkinter.font.names(root): + font = tkinter.font.Font(root=root, name=name, exists=True) + size = int(font['size']) + if size < 0: + font['size'] = round(-0.75*size) + + # Fix for HiDPI screens on Windows. CALL BEFORE ANY TK OPERATIONS! # URL for arguments for the ...Awareness call below. # https://msdn.microsoft.com/en-us/library/windows/desktop/dn280512(v=vs.85).aspx @@ -31,6 +45,25 @@ def fix_win_hidpi(): # Called in pyshell and turtledemo. except (ImportError, AttributeError, OSError): pass +def fix_word_breaks(root): # Called in editor htest, filelist _test, pyshell. + # On Windows, tcl/tk breaks 'words' only on spaces, as in Command Prompt. + # We want Motif style everywhere. See #21474, msg218992 and followup. + tk = root.tk + tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded + tk.call('set', 'tcl_wordchars', r'\w') + tk.call('set', 'tcl_nonwordchars', r'\W') + + +def fix_x11_paste(root): + "Make paste replace selection on x11. See issue #5124." + if root._windowingsystem == 'x11': + for cls in 'Text', 'Entry', 'Spinbox': + root.bind_class( + cls, + '<>', + 'catch {%W delete sel.first sel.last}\n' + + root.bind_class(cls, '<>')) + if __name__ == '__main__': from unittest import main diff --git a/Lib/idlelib/window.py b/Lib/idlelib/window.py index 460d5b67948..a41d69927dc 100644 --- a/Lib/idlelib/window.py +++ b/Lib/idlelib/window.py @@ -61,6 +61,10 @@ def call_callbacks(self): class ListedToplevel(Toplevel): def __init__(self, master, **kw): + # Set the WM_CLASS property so that X11 window managers group and + # label IDLE's windows under 'Idle' instead of the default + # 'Toplevel' (gh-66331). It matches the class name passed to Tk(). + kw.setdefault('class_', 'Idle') Toplevel.__init__(self, master, kw) registry.add(self) self.focused_widget = self diff --git a/Lib/imaplib.py b/Lib/imaplib.py index 4c0aefd082f..d78ad08ce35 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -21,8 +21,6 @@ # GET/SETANNOTATION contributed by Tomas Lindroos June 2005. # IDLE contributed by Forest August 2024. -__version__ = "2.60" - import binascii, errno, random, re, socket, subprocess, sys, time, calendar from datetime import datetime, timezone, timedelta from io import DEFAULT_BUFFER_SIZE @@ -258,7 +256,6 @@ def _connect(self): self._cmd_log_idx = 0 self._cmd_log = {} # Last '_cmd_log_len' interactions if self.debug >= 1: - self._mesg('imaplib version %s' % __version__) self._mesg('new IMAP4 connection, tag=%s' % self.tagpre) self.welcome = self._get_response() @@ -331,25 +328,34 @@ def open(self, host='', port=IMAP4_PORT, timeout=None): self.host = host self.port = port self.sock = self._create_socket(timeout) - self._file = self.sock.makefile('rb') - + # Since IMAP4 implements its own read() and readline() buffering, + # the '_imaplib_file' attribute is unused. Nonetheless it is kept + # and exposed solely for backward compatibility purposes. + self._imaplib_file = self.sock.makefile('rb') @property def file(self): - # The old 'file' attribute is no longer used now that we do our own - # read() and readline() buffering, with which it conflicts. - # As an undocumented interface, it should never have been accessed by - # external code, and therefore does not warrant deprecation. - # Nevertheless, we provide this property for now, to avoid suddenly - # breaking any code in the wild that might have been using it in a - # harmless way. import warnings - warnings.warn( - 'IMAP4.file is unsupported, can cause errors, and may be removed.', - RuntimeWarning, - stacklevel=2) - return self._file + warnings._deprecated("IMAP4.file", remove=(3, 19)) + return self._imaplib_file + @file.setter + def file(self, value): + import warnings + warnings._deprecated("IMAP4.file", remove=(3, 19)) + # Ideally, we would want to close the previous file, + # but since we do not know how subclasses will use + # that setter, it is probably better to leave it to + # the caller. + self._imaplib_file = value + + def _close_imaplib_file(self): + file = self._imaplib_file + if file is not None: + try: + file.close() + except OSError: + pass def read(self, size): """Read 'size' bytes from remote.""" @@ -435,7 +441,7 @@ def send(self, data): def shutdown(self): """Close I/O established in "open".""" - self._file.close() + self._close_imaplib_file() try: self.sock.shutdown(socket.SHUT_RDWR) except OSError as exc: @@ -952,9 +958,10 @@ def starttls(self, ssl_context=None): ssl_context = ssl._create_stdlib_context() typ, dat = self._simple_command(name) if typ == 'OK': + self._close_imaplib_file() self.sock = ssl_context.wrap_socket(self.sock, server_hostname=self.host) - self._file = self.sock.makefile('rb') + self._imaplib_file = self.sock.makefile('rb') self._tls_established = True self._get_capabilities() else: @@ -1392,7 +1399,7 @@ def _get_tagged_response(self, tag, expect_bye=False): try: self._get_response() - except self.abort as val: + except self.abort: if __debug__: if self.debug >= 1: self.print_log() @@ -1800,7 +1807,7 @@ def open(self, host=None, port=None, timeout=None): self.host = None # For compatibility with parent class self.port = None self.sock = None - self._file = None + self._imaplib_file = None self.process = subprocess.Popen(self.command, bufsize=DEFAULT_BUFFER_SIZE, stdin=subprocess.PIPE, stdout=subprocess.PIPE, @@ -1988,7 +1995,7 @@ def Time2Internaldate(date_time): try: optlist, args = getopt.getopt(sys.argv[1:], 'd:s:') - except getopt.error as val: + except getopt.error: optlist, args = (), () stream_command = None @@ -2083,3 +2090,12 @@ def run(cmd, args): ''' % sys.argv[0]) raise + + +def __getattr__(name): + if name == "__version__": + from warnings import _deprecated + + _deprecated("__version__", remove=(3, 20)) + return "2.60" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/keyword.py b/Lib/keyword.py index e22c837835e..98ffe2de28b 100644 --- a/Lib/keyword.py +++ b/Lib/keyword.py @@ -56,6 +56,7 @@ softkwlist = [ '_', 'case', + 'lazy', 'match', 'type' ] diff --git a/Lib/mailbox.py b/Lib/mailbox.py index 364af6bb010..99426220154 100644 --- a/Lib/mailbox.py +++ b/Lib/mailbox.py @@ -39,6 +39,13 @@ def __init__(self, path, factory=None, create=True): self._path = os.path.abspath(os.path.expanduser(path)) self._factory = factory + def __enter__(self): + self.lock() + return self + + def __exit__(self, type, value, traceback): + self.close() + def add(self, message): """Add message and return assigned key.""" raise NotImplementedError('Method must be implemented by subclass') @@ -2090,8 +2097,6 @@ def closed(self): return False return self._file.closed - __class_getitem__ = classmethod(GenericAlias) - class _PartialFile(_ProxyFile): """A read-only wrapper of part of a file.""" diff --git a/Lib/modulefinder.py b/Lib/modulefinder.py index ac478ee7f51..b8dccc2dd33 100644 --- a/Lib/modulefinder.py +++ b/Lib/modulefinder.py @@ -66,7 +66,12 @@ def _find_module(name, path=None): file_path = spec.origin - if spec.loader.is_package(name): + # On namespace packages, spec.loader might be None, but + # spec.submodule_search_locations should always be set — check it instead. + if isinstance(spec.submodule_search_locations, importlib.machinery.NamespacePath): + return None, spec.submodule_search_locations, ("", "", _PKG_DIRECTORY) + + if spec.loader.is_package(name): # non-namespace package return None, os.path.dirname(file_path), ("", "", _PKG_DIRECTORY) if isinstance(spec.loader, importlib.machinery.SourceFileLoader): @@ -334,7 +339,7 @@ def load_module(self, fqname, fp, pathname, file_info): self.msgout(2, "load_module ->", m) return m if type == _PY_SOURCE: - co = compile(fp.read(), pathname, 'exec') + co = compile(fp.read(), pathname, 'exec', module=fqname) elif type == _PY_COMPILED: try: data = fp.read() @@ -400,7 +405,6 @@ def scan_opcodes(self, co): yield "relative_import", (level, fromlist, name) def scan_code(self, co, m): - code = co.co_code scanner = self.scan_opcodes for what, args in scanner(co): if what == "store": @@ -454,6 +458,11 @@ def load_package(self, fqname, pathname): if newname: fqname = newname m = self.add_module(fqname) + + if isinstance(pathname, importlib.machinery.NamespacePath): + m.__path__ = pathname + return m + m.__file__ = pathname m.__path__ = [pathname] diff --git a/Lib/multiprocessing/connection.py b/Lib/multiprocessing/connection.py index f577186e91b..e37ec07d722 100644 --- a/Lib/multiprocessing/connection.py +++ b/Lib/multiprocessing/connection.py @@ -734,8 +734,7 @@ def accept(self): # written data and then disconnected -- see Issue 14725. else: try: - res = _winapi.WaitForMultipleObjects( - [ov.event], False, INFINITE) + _winapi.WaitForMultipleObjects([ov.event], False, INFINITE) except: ov.cancel() _winapi.CloseHandle(handle) @@ -1089,14 +1088,22 @@ def wait(object_list, timeout=None): Returns list of those objects in object_list which are ready/readable. ''' + object_list = list(object_list) + + if not object_list: + if timeout is None: + while True: + time.sleep(1e6) + elif timeout > 0: + time.sleep(timeout) + return [] + if timeout is None: timeout = INFINITE elif timeout < 0: timeout = 0 else: timeout = int(timeout * 1000 + 0.5) - - object_list = list(object_list) waithandle_to_obj = {} ov_list = [] ready_objects = set() diff --git a/Lib/multiprocessing/context.py b/Lib/multiprocessing/context.py index 5fa6d7e4861..e1d25145602 100644 --- a/Lib/multiprocessing/context.py +++ b/Lib/multiprocessing/context.py @@ -183,12 +183,15 @@ def set_executable(self, executable): from .spawn import set_executable set_executable(executable) - def set_forkserver_preload(self, module_names): + def set_forkserver_preload(self, module_names, *, on_error='ignore'): '''Set list of module names to try to load in forkserver process. - This is really just a hint. + + The on_error parameter controls how import failures are handled: + "ignore" (default) silently ignores failures, "warn" emits warnings, + and "fail" raises exceptions breaking the forkserver context. ''' from .forkserver import set_forkserver_preload - set_forkserver_preload(module_names) + set_forkserver_preload(module_names, on_error=on_error) def get_context(self, method=None): if method is None: diff --git a/Lib/multiprocessing/forkserver.py b/Lib/multiprocessing/forkserver.py index e431b3f1298..b204a3ad0f1 100644 --- a/Lib/multiprocessing/forkserver.py +++ b/Lib/multiprocessing/forkserver.py @@ -42,6 +42,7 @@ def __init__(self): self._inherited_fds = None self._lock = threading.Lock() self._preload_modules = ['__main__'] + self._preload_on_error = 'ignore' def _stop(self): # Method used by unit tests to stop the server @@ -64,11 +65,22 @@ def _stop_unlocked(self): self._forkserver_address = None self._forkserver_authkey = None - def set_forkserver_preload(self, modules_names): - '''Set list of module names to try to load in forkserver process.''' + def set_forkserver_preload(self, modules_names, *, on_error='ignore'): + '''Set list of module names to try to load in forkserver process. + + The on_error parameter controls how import failures are handled: + "ignore" (default) silently ignores failures, "warn" emits warnings, + and "fail" raises exceptions breaking the forkserver context. + ''' if not all(type(mod) is str for mod in modules_names): raise TypeError('module_names must be a list of strings') + if on_error not in ('ignore', 'warn', 'fail'): + raise ValueError( + f"on_error must be 'ignore', 'warn', or 'fail', " + f"not {on_error!r}" + ) self._preload_modules = modules_names + self._preload_on_error = on_error def get_inherited_fds(self): '''Return list of fds inherited from parent process. @@ -107,6 +119,14 @@ def connect_to_new_process(self, fds): wrapped_client, self._forkserver_authkey) connection.deliver_challenge( wrapped_client, self._forkserver_authkey) + except (EOFError, ConnectionError, BrokenPipeError) as exc: + if (self._preload_modules and + self._preload_on_error == 'fail'): + exc.add_note( + "Forkserver process may have crashed during module " + "preloading. Check stderr." + ) + raise finally: wrapped_client._detach() del wrapped_client @@ -161,6 +181,8 @@ def ensure_running(self): main_kws['main_path'] = data['init_main_from_path'] if 'sys_argv' in data: sys_argv = data['sys_argv'] + if self._preload_on_error != 'ignore': + main_kws['on_error'] = self._preload_on_error with socket.socket(socket.AF_UNIX) as listener: address = connection.arbitrary_address('AF_UNIX') @@ -207,8 +229,69 @@ def ensure_running(self): # # +def _handle_import_error(on_error, modinfo, exc, *, warn_stacklevel): + """Handle an import error according to the on_error policy.""" + match on_error: + case 'fail': + raise + case 'warn': + warnings.warn( + f"Failed to preload {modinfo}: {exc}", + ImportWarning, + stacklevel=warn_stacklevel + 1 + ) + case 'ignore': + pass + + +def _handle_preload(preload, main_path=None, sys_path=None, sys_argv=None, + on_error='ignore'): + """Handle module preloading with configurable error handling. + + Args: + preload: List of module names to preload. + main_path: Path to __main__ module if '__main__' is in preload. + sys_path: sys.path to use for imports (None means use current). + sys_argv: sys.argv to use (None means use current). + on_error: How to handle import errors ("ignore", "warn", or "fail"). + """ + if not preload: + return + + if sys_argv is not None: + sys.argv[:] = sys_argv + if sys_path is not None: + sys.path[:] = sys_path + + if '__main__' in preload and main_path is not None: + process.current_process()._inheriting = True + try: + spawn.import_main_path(main_path) + except Exception as e: + # Catch broad Exception because import_main_path() uses + # runpy.run_path() which executes the script and can raise + # any exception, not just ImportError + _handle_import_error( + on_error, f"__main__ from {main_path!r}", e, warn_stacklevel=2 + ) + finally: + del process.current_process()._inheriting + + for modname in preload: + try: + __import__(modname) + except ImportError as e: + _handle_import_error( + on_error, f"module {modname!r}", e, warn_stacklevel=2 + ) + + # gh-135335: flush stdout/stderr in case any of the preloaded modules + # wrote to them, otherwise children might inherit buffered data + util._flush_std_streams() + + def main(listener_fd, alive_r, preload, main_path=None, sys_path=None, - *, sys_argv=None, authkey_r=None): + *, sys_argv=None, authkey_r=None, on_error='ignore'): """Run forkserver.""" if authkey_r is not None: try: @@ -219,26 +302,7 @@ def main(listener_fd, alive_r, preload, main_path=None, sys_path=None, else: authkey = b'' - if preload: - if sys_argv is not None: - sys.argv[:] = sys_argv - if sys_path is not None: - sys.path[:] = sys_path - if '__main__' in preload and main_path is not None: - process.current_process()._inheriting = True - try: - spawn.import_main_path(main_path) - finally: - del process.current_process()._inheriting - for modname in preload: - try: - __import__(modname) - except ImportError: - pass - - # gh-135335: flush stdout/stderr in case any of the preloaded modules - # wrote to them, otherwise children might inherit buffered data - util._flush_std_streams() + _handle_preload(preload, main_path, sys_path, sys_argv, on_error) util._close_stdin() diff --git a/Lib/multiprocessing/heap.py b/Lib/multiprocessing/heap.py index 6217dfe1268..5c835648395 100644 --- a/Lib/multiprocessing/heap.py +++ b/Lib/multiprocessing/heap.py @@ -324,10 +324,6 @@ class BufferWrapper(object): _heap = Heap() def __init__(self, size): - if size < 0: - raise ValueError("Size {0:n} out of range".format(size)) - if sys.maxsize <= size: - raise OverflowError("Size {0:n} too large".format(size)) block = BufferWrapper._heap.malloc(size) self._state = (block, size) util.Finalize(self, BufferWrapper._heap.free, args=(block,)) diff --git a/Lib/multiprocessing/managers.py b/Lib/multiprocessing/managers.py index 91bcf243e78..11c6e21de90 100644 --- a/Lib/multiprocessing/managers.py +++ b/Lib/multiprocessing/managers.py @@ -1294,9 +1294,9 @@ class SyncManager(BaseManager): class _SharedMemoryTracker: "Manages one or more shared memory segments." - def __init__(self, name, segment_names=[]): + def __init__(self, name, segment_names=None): self.shared_memory_context_name = name - self.segment_names = segment_names + self.segment_names = [] if segment_names is None else segment_names def register_segment(self, segment_name): "Adds the supplied shared memory block name to tracker." diff --git a/Lib/multiprocessing/resource_tracker.py b/Lib/multiprocessing/resource_tracker.py index dfb5a3dfe8b..d3328a8c617 100644 --- a/Lib/multiprocessing/resource_tracker.py +++ b/Lib/multiprocessing/resource_tracker.py @@ -74,7 +74,7 @@ def __init__(self): # Filenames not supported by the simple format will always be sent # using JSON. # The reader should understand all formats. - self._use_simple_format = True + self._use_simple_format = False # Set to True by _stop_locked() if the waitpid polling loop ran to # its timeout without reaping the tracker. Exposed for tests. diff --git a/Lib/multiprocessing/sharedctypes.py b/Lib/multiprocessing/sharedctypes.py index 6071707027b..eee1172e6e9 100644 --- a/Lib/multiprocessing/sharedctypes.py +++ b/Lib/multiprocessing/sharedctypes.py @@ -37,7 +37,12 @@ # def _new_value(type_): - size = ctypes.sizeof(type_) + try: + size = ctypes.sizeof(type_) + except TypeError as e: + raise TypeError("bad typecode (must be a ctypes type or one of " + "c, b, B, u, h, H, i, I, l, L, q, Q, f or d)") from e + wrapper = heap.BufferWrapper(size) return rebuild_ctype(type_, wrapper, None) diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 0d763d1f0dc..8dd16c6225b 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -12,10 +12,13 @@ import posixpath import sys from errno import * -from glob import _StringGlobber, _no_recurse_symlinks from itertools import chain -from stat import S_ISDIR, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO +from stat import ( + S_IMODE, S_ISDIR, S_ISREG, S_ISLNK, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO, +) from _collections_abc import Sequence +lazy import shutil +lazy from glob import _StringGlobber, _no_recurse_symlinks try: import pwd @@ -27,9 +30,9 @@ grp = None from pathlib._os import ( - PathInfo, DirEntryInfo, + vfsopen, vfspath, ensure_different_files, ensure_distinct_paths, - copyfile2, copyfileobj, magic_open, copy_info, + copyfile2, copyfileobj, ) @@ -187,9 +190,6 @@ def __reduce__(self): def __repr__(self): return "{}({!r})".format(self.__class__.__name__, self.as_posix()) - def __fspath__(self): - return str(self) - def __bytes__(self): """Return the bytes representation of the path. This is only recommended to use under Unix.""" @@ -258,6 +258,9 @@ def __str__(self): self._tail) or '.' return self._str + __fspath__ = __str__ + __vfspath__ = __str__ + @classmethod def _format_parsed_parts(cls, drv, root, tail): if drv or root: @@ -483,16 +486,19 @@ def relative_to(self, other, *, walk_up=False): """ if not hasattr(other, 'with_segments'): other = self.with_segments(other) - for step, path in enumerate(chain([other], other.parents)): + parts = [] + for path in chain([other], other.parents): if path == self or path in self.parents: break elif not walk_up: raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}") elif path.name == '..': raise ValueError(f"'..' segment in {str(other)!r} cannot be walked") + else: + parts.append('..') else: raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors") - parts = ['..'] * step + self._tail[len(path._tail):] + parts.extend(self._tail[len(path._tail):]) return self._from_parsed_parts('', '', parts) def is_relative_to(self, other): @@ -513,18 +519,6 @@ def is_absolute(self): return False return self.parser.isabs(self) - def is_reserved(self): - """Return True if the path contains one of the special names reserved - by the system, if any.""" - import warnings - msg = ("pathlib.PurePath.is_reserved() is deprecated and scheduled " - "for removal in Python 3.15. Use os.path.isreserved() to " - "detect reserved paths on Windows.") - warnings._deprecated("pathlib.PurePath.is_reserved", msg, remove=(3, 15)) - if self.parser is ntpath: - return self.parser.isreserved(self) - return False - def as_uri(self): """Return the path as a URI.""" import warnings @@ -618,6 +612,211 @@ class PureWindowsPath(PurePath): __slots__ = () +_STAT_RESULT_ERROR = [] # falsy sentinel indicating stat() failed. + + +class _Info: + """Implementation of pathlib.types.PathInfo that provides status + information by querying a wrapped os.stat_result object. Don't try to + construct it yourself.""" + __slots__ = ('_path', '_entry', '_stat_result', '_lstat_result') + + def __init__(self, path, entry=None): + self._path = path + self._entry = entry + self._stat_result = None + self._lstat_result = None + + def __repr__(self): + path_type = "WindowsPath" if os.name == "nt" else "PosixPath" + return f"<{path_type}.info>" + + def _stat(self, *, follow_symlinks=True): + """Return the status as an os.stat_result.""" + if self._entry: + return self._entry.stat(follow_symlinks=follow_symlinks) + if follow_symlinks: + if not self._stat_result: + try: + self._stat_result = os.stat(self._path) + except (OSError, ValueError): + self._stat_result = _STAT_RESULT_ERROR + raise + return self._stat_result + else: + if not self._lstat_result: + try: + self._lstat_result = os.lstat(self._path) + except (OSError, ValueError): + self._lstat_result = _STAT_RESULT_ERROR + raise + return self._lstat_result + + def exists(self, *, follow_symlinks=True): + """Whether this path exists.""" + if self._entry: + if not follow_symlinks: + return True + if follow_symlinks: + if self._stat_result is _STAT_RESULT_ERROR: + return False + else: + if self._lstat_result is _STAT_RESULT_ERROR: + return False + try: + self._stat(follow_symlinks=follow_symlinks) + except (OSError, ValueError): + return False + return True + + def is_dir(self, *, follow_symlinks=True): + """Whether this path is a directory.""" + if self._entry: + try: + return self._entry.is_dir(follow_symlinks=follow_symlinks) + except OSError: + return False + if follow_symlinks: + if self._stat_result is _STAT_RESULT_ERROR: + return False + else: + if self._lstat_result is _STAT_RESULT_ERROR: + return False + try: + st = self._stat(follow_symlinks=follow_symlinks) + except (OSError, ValueError): + return False + return S_ISDIR(st.st_mode) + + def is_file(self, *, follow_symlinks=True): + """Whether this path is a regular file.""" + if self._entry: + try: + return self._entry.is_file(follow_symlinks=follow_symlinks) + except OSError: + return False + if follow_symlinks: + if self._stat_result is _STAT_RESULT_ERROR: + return False + else: + if self._lstat_result is _STAT_RESULT_ERROR: + return False + try: + st = self._stat(follow_symlinks=follow_symlinks) + except (OSError, ValueError): + return False + return S_ISREG(st.st_mode) + + def is_symlink(self): + """Whether this path is a symbolic link.""" + if self._entry: + try: + return self._entry.is_symlink() + except OSError: + return False + if self._lstat_result is _STAT_RESULT_ERROR: + return False + try: + st = self._stat(follow_symlinks=False) + except (OSError, ValueError): + return False + return S_ISLNK(st.st_mode) + + def _posix_permissions(self, *, follow_symlinks=True): + """Return the POSIX file permissions.""" + return S_IMODE(self._stat(follow_symlinks=follow_symlinks).st_mode) + + def _file_id(self, *, follow_symlinks=True): + """Returns the identifier of the file.""" + st = self._stat(follow_symlinks=follow_symlinks) + return st.st_dev, st.st_ino + + def _access_time_ns(self, *, follow_symlinks=True): + """Return the access time in nanoseconds.""" + return self._stat(follow_symlinks=follow_symlinks).st_atime_ns + + def _mod_time_ns(self, *, follow_symlinks=True): + """Return the modify time in nanoseconds.""" + return self._stat(follow_symlinks=follow_symlinks).st_mtime_ns + + if hasattr(os.stat_result, 'st_flags'): + def _bsd_flags(self, *, follow_symlinks=True): + """Return the flags.""" + return self._stat(follow_symlinks=follow_symlinks).st_flags + + if hasattr(os, 'listxattr'): + def _xattrs(self, *, follow_symlinks=True): + """Return the xattrs as a list of (attr, value) pairs, or an empty + list if extended attributes aren't supported.""" + try: + return [ + (attr, os.getxattr(self._path, attr, follow_symlinks=follow_symlinks)) + for attr in os.listxattr(self._path, follow_symlinks=follow_symlinks)] + except OSError as err: + if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): + raise + return [] + + +def _copy_info(info, target, follow_symlinks=True): + """Copy metadata from the given PathInfo to the given local path.""" + copy_times_ns = ( + hasattr(info, '_access_time_ns') and + hasattr(info, '_mod_time_ns') and + (follow_symlinks or os.utime in os.supports_follow_symlinks)) + if copy_times_ns: + t0 = info._access_time_ns(follow_symlinks=follow_symlinks) + t1 = info._mod_time_ns(follow_symlinks=follow_symlinks) + os.utime(target, ns=(t0, t1), follow_symlinks=follow_symlinks) + + # We must copy extended attributes before the file is (potentially) + # chmod()'ed read-only, otherwise setxattr() will error with -EACCES. + copy_xattrs = ( + hasattr(info, '_xattrs') and + hasattr(os, 'setxattr') and + (follow_symlinks or os.setxattr in os.supports_follow_symlinks)) + if copy_xattrs: + xattrs = info._xattrs(follow_symlinks=follow_symlinks) + for attr, value in xattrs: + try: + os.setxattr(target, attr, value, follow_symlinks=follow_symlinks) + except OSError as e: + if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): + raise + + copy_posix_permissions = ( + hasattr(info, '_posix_permissions') and + (follow_symlinks or os.chmod in os.supports_follow_symlinks)) + if copy_posix_permissions: + posix_permissions = info._posix_permissions(follow_symlinks=follow_symlinks) + try: + os.chmod(target, posix_permissions, follow_symlinks=follow_symlinks) + except NotImplementedError: + # if we got a NotImplementedError, it's because + # * follow_symlinks=False, + # * lchown() is unavailable, and + # * either + # * fchownat() is unavailable or + # * fchownat() doesn't implement AT_SYMLINK_NOFOLLOW. + # (it returned ENOSUP.) + # therefore we're out of options--we simply cannot chown the + # symlink. give up, suppress the error. + # (which is what shutil always did in this circumstance.) + pass + + copy_bsd_flags = ( + hasattr(info, '_bsd_flags') and + hasattr(os, 'chflags') and + (follow_symlinks or os.chflags in os.supports_follow_symlinks)) + if copy_bsd_flags: + bsd_flags = info._bsd_flags(follow_symlinks=follow_symlinks) + try: + os.chflags(target, bsd_flags, follow_symlinks=follow_symlinks) + except OSError as why: + if why.errno not in (EOPNOTSUPP, ENOTSUP): + raise + + class Path(PurePath): """PurePath subclass that can make system calls. @@ -643,7 +842,7 @@ def info(self): try: return self._info except AttributeError: - self._info = PathInfo(self) + self._info = _Info(str(self)) return self._info def stat(self, *, follow_symlinks=True): @@ -823,7 +1022,7 @@ def _filter_trailing_slash(self, paths): def _from_dir_entry(self, dir_entry, path_str): path = self.with_segments(path_str) path._str = path_str - path._info = DirEntryInfo(dir_entry) + path._info = _Info(dir_entry.path, dir_entry) return path def iterdir(self): @@ -1003,7 +1202,7 @@ def touch(self, mode=0o666, exist_ok=True): fd = os.open(self, flags, mode) os.close(fd) - def mkdir(self, mode=0o777, parents=False, exist_ok=False): + def mkdir(self, mode=0o777, parents=False, exist_ok=False, *, parent_mode=None): """ Create a new directory at this given path. """ @@ -1012,7 +1211,11 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False): except FileNotFoundError: if not parents or self.parent == self: raise - self.parent.mkdir(parents=True, exist_ok=True) + if parent_mode is not None: + self.parent.mkdir(mode=parent_mode, parents=True, exist_ok=True, + parent_mode=parent_mode) + else: + self.parent.mkdir(parents=True, exist_ok=True) self.mkdir(mode, parents=False, exist_ok=exist_ok) except OSError: # Cannot rely on checking for EEXIST, since the operating system @@ -1057,8 +1260,6 @@ def _delete(self): if self.is_symlink() or self.is_junction(): self.unlink() elif self.is_dir(): - # Lazy import to improve module import time - import shutil shutil.rmtree(self) else: self.unlink() @@ -1129,17 +1330,17 @@ def _copy_from(self, source, follow_symlinks=True, preserve_metadata=False): self.joinpath(child.name)._copy_from( child, follow_symlinks, preserve_metadata) if preserve_metadata: - copy_info(source.info, self) + _copy_info(source.info, self) else: self._copy_from_file(source, preserve_metadata) def _copy_from_file(self, source, preserve_metadata=False): ensure_different_files(source, self) - with magic_open(source, 'rb') as source_f: + with vfsopen(source, 'rb') as source_f: with open(self, 'wb') as target_f: copyfileobj(source_f, target_f) if preserve_metadata: - copy_info(source.info, self) + _copy_info(source.info, self) if copyfile2: # Use fast OS routine for local file copying where available. @@ -1159,14 +1360,14 @@ def _copy_from_file(self, source, preserve_metadata=False): # os.symlink() incorrectly creates a file-symlink on Windows. Avoid # this by passing *target_is_dir* to os.symlink() on Windows. def _copy_from_symlink(self, source, preserve_metadata=False): - os.symlink(str(source.readlink()), self, source.info.is_dir()) + os.symlink(vfspath(source.readlink()), self, source.info.is_dir()) if preserve_metadata: - copy_info(source.info, self, follow_symlinks=False) + _copy_info(source.info, self, follow_symlinks=False) else: def _copy_from_symlink(self, source, preserve_metadata=False): - os.symlink(str(source.readlink()), self) + os.symlink(vfspath(source.readlink()), self) if preserve_metadata: - copy_info(source.info, self, follow_symlinks=False) + _copy_info(source.info, self, follow_symlinks=False) def move(self, target): """ diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 039836941dd..79a1969d5f8 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -4,7 +4,6 @@ from errno import * from io import TextIOWrapper, text_encoding -from stat import S_ISDIR, S_ISREG, S_ISLNK, S_IMODE import os import sys try: @@ -166,48 +165,100 @@ def copyfileobj(source_f, target_f): write_target(buf) -def magic_open(path, mode='r', buffering=-1, encoding=None, errors=None, - newline=None): +def _open_reader(obj): + cls = type(obj) + try: + open_reader = cls.__open_reader__ + except AttributeError: + cls_name = cls.__name__ + raise TypeError(f"{cls_name} can't be opened for reading") from None + else: + return open_reader(obj) + + +def _open_writer(obj, mode): + cls = type(obj) + try: + open_writer = cls.__open_writer__ + except AttributeError: + cls_name = cls.__name__ + raise TypeError(f"{cls_name} can't be opened for writing") from None + else: + return open_writer(obj, mode) + + +def _open_updater(obj, mode): + cls = type(obj) + try: + open_updater = cls.__open_updater__ + except AttributeError: + cls_name = cls.__name__ + raise TypeError(f"{cls_name} can't be opened for updating") from None + else: + return open_updater(obj, mode) + + +def vfsopen(obj, mode='r', buffering=-1, encoding=None, errors=None, + newline=None): """ Open the file pointed to by this path and return a file object, as the built-in open() function does. + + Unlike the built-in open() function, this function additionally accepts + 'openable' objects, which are objects with any of these special methods: + + __open_reader__() + __open_writer__(mode) + __open_updater__(mode) + + '__open_reader__' is called for 'r' mode; '__open_writer__' for 'a', 'w' + and 'x' modes; and '__open_updater__' for 'r+' and 'w+' modes. If text + mode is requested, the result is wrapped in an io.TextIOWrapper object. """ + if buffering != -1: + raise ValueError("buffer size can't be customized") text = 'b' not in mode if text: # Call io.text_encoding() here to ensure any warning is raised at an # appropriate stack level. encoding = text_encoding(encoding) try: - return open(path, mode, buffering, encoding, errors, newline) + return open(obj, mode, buffering, encoding, errors, newline) except TypeError: pass - cls = type(path) + if not text: + if encoding is not None: + raise ValueError("binary mode doesn't take an encoding argument") + if errors is not None: + raise ValueError("binary mode doesn't take an errors argument") + if newline is not None: + raise ValueError("binary mode doesn't take a newline argument") mode = ''.join(sorted(c for c in mode if c not in 'bt')) + if mode == 'r': + stream = _open_reader(obj) + elif mode in ('a', 'w', 'x'): + stream = _open_writer(obj, mode) + elif mode in ('+r', '+w'): + stream = _open_updater(obj, mode[1]) + else: + raise ValueError(f'invalid mode: {mode}') if text: - try: - attr = getattr(cls, f'__open_{mode}__') - except AttributeError: - pass - else: - return attr(path, buffering, encoding, errors, newline) - elif encoding is not None: - raise ValueError("binary mode doesn't take an encoding argument") - elif errors is not None: - raise ValueError("binary mode doesn't take an errors argument") - elif newline is not None: - raise ValueError("binary mode doesn't take a newline argument") + stream = TextIOWrapper(stream, encoding, errors, newline) + return stream + +def vfspath(obj): + """ + Return the string representation of a virtual path object. + """ + cls = type(obj) try: - attr = getattr(cls, f'__open_{mode}b__') + vfspath_method = cls.__vfspath__ except AttributeError: - pass + cls_name = cls.__name__ + raise TypeError(f"expected JoinablePath object, not {cls_name}") from None else: - stream = attr(path, buffering) - if text: - stream = TextIOWrapper(stream, encoding, errors, newline) - return stream - - raise TypeError(f"{cls.__name__} can't be opened with mode {mode!r}") + return vfspath_method(obj) def ensure_distinct_paths(source, target): @@ -225,8 +276,8 @@ def ensure_distinct_paths(source, target): err = OSError(EINVAL, "Source path is a parent of target path") else: return - err.filename = str(source) - err.filename2 = str(target) + err.filename = vfspath(source) + err.filename2 = vfspath(target) raise err @@ -247,284 +298,6 @@ def ensure_different_files(source, target): except (OSError, ValueError): return err = OSError(EINVAL, "Source and target are the same file") - err.filename = str(source) - err.filename2 = str(target) + err.filename = vfspath(source) + err.filename2 = vfspath(target) raise err - - -def copy_info(info, target, follow_symlinks=True): - """Copy metadata from the given PathInfo to the given local path.""" - copy_times_ns = ( - hasattr(info, '_access_time_ns') and - hasattr(info, '_mod_time_ns') and - (follow_symlinks or os.utime in os.supports_follow_symlinks)) - if copy_times_ns: - t0 = info._access_time_ns(follow_symlinks=follow_symlinks) - t1 = info._mod_time_ns(follow_symlinks=follow_symlinks) - os.utime(target, ns=(t0, t1), follow_symlinks=follow_symlinks) - - # We must copy extended attributes before the file is (potentially) - # chmod()'ed read-only, otherwise setxattr() will error with -EACCES. - copy_xattrs = ( - hasattr(info, '_xattrs') and - hasattr(os, 'setxattr') and - (follow_symlinks or os.setxattr in os.supports_follow_symlinks)) - if copy_xattrs: - xattrs = info._xattrs(follow_symlinks=follow_symlinks) - for attr, value in xattrs: - try: - os.setxattr(target, attr, value, follow_symlinks=follow_symlinks) - except OSError as e: - if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): - raise - - copy_posix_permissions = ( - hasattr(info, '_posix_permissions') and - (follow_symlinks or os.chmod in os.supports_follow_symlinks)) - if copy_posix_permissions: - posix_permissions = info._posix_permissions(follow_symlinks=follow_symlinks) - try: - os.chmod(target, posix_permissions, follow_symlinks=follow_symlinks) - except NotImplementedError: - # if we got a NotImplementedError, it's because - # * follow_symlinks=False, - # * lchown() is unavailable, and - # * either - # * fchownat() is unavailable or - # * fchownat() doesn't implement AT_SYMLINK_NOFOLLOW. - # (it returned ENOSUP.) - # therefore we're out of options--we simply cannot chown the - # symlink. give up, suppress the error. - # (which is what shutil always did in this circumstance.) - pass - - copy_bsd_flags = ( - hasattr(info, '_bsd_flags') and - hasattr(os, 'chflags') and - (follow_symlinks or os.chflags in os.supports_follow_symlinks)) - if copy_bsd_flags: - bsd_flags = info._bsd_flags(follow_symlinks=follow_symlinks) - try: - os.chflags(target, bsd_flags, follow_symlinks=follow_symlinks) - except OSError as why: - if why.errno not in (EOPNOTSUPP, ENOTSUP): - raise - - -class _PathInfoBase: - __slots__ = ('_path', '_stat_result', '_lstat_result') - - def __init__(self, path): - self._path = str(path) - - def __repr__(self): - path_type = "WindowsPath" if os.name == "nt" else "PosixPath" - return f"<{path_type}.info>" - - def _stat(self, *, follow_symlinks=True, ignore_errors=False): - """Return the status as an os.stat_result, or None if stat() fails and - ignore_errors is true.""" - if follow_symlinks: - try: - result = self._stat_result - except AttributeError: - pass - else: - if ignore_errors or result is not None: - return result - try: - self._stat_result = os.stat(self._path) - except (OSError, ValueError): - self._stat_result = None - if not ignore_errors: - raise - return self._stat_result - else: - try: - result = self._lstat_result - except AttributeError: - pass - else: - if ignore_errors or result is not None: - return result - try: - self._lstat_result = os.lstat(self._path) - except (OSError, ValueError): - self._lstat_result = None - if not ignore_errors: - raise - return self._lstat_result - - def _posix_permissions(self, *, follow_symlinks=True): - """Return the POSIX file permissions.""" - return S_IMODE(self._stat(follow_symlinks=follow_symlinks).st_mode) - - def _file_id(self, *, follow_symlinks=True): - """Returns the identifier of the file.""" - st = self._stat(follow_symlinks=follow_symlinks) - return st.st_dev, st.st_ino - - def _access_time_ns(self, *, follow_symlinks=True): - """Return the access time in nanoseconds.""" - return self._stat(follow_symlinks=follow_symlinks).st_atime_ns - - def _mod_time_ns(self, *, follow_symlinks=True): - """Return the modify time in nanoseconds.""" - return self._stat(follow_symlinks=follow_symlinks).st_mtime_ns - - if hasattr(os.stat_result, 'st_flags'): - def _bsd_flags(self, *, follow_symlinks=True): - """Return the flags.""" - return self._stat(follow_symlinks=follow_symlinks).st_flags - - if hasattr(os, 'listxattr'): - def _xattrs(self, *, follow_symlinks=True): - """Return the xattrs as a list of (attr, value) pairs, or an empty - list if extended attributes aren't supported.""" - try: - return [ - (attr, os.getxattr(self._path, attr, follow_symlinks=follow_symlinks)) - for attr in os.listxattr(self._path, follow_symlinks=follow_symlinks)] - except OSError as err: - if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): - raise - return [] - - -class _WindowsPathInfo(_PathInfoBase): - """Implementation of pathlib.types.PathInfo that provides status - information for Windows paths. Don't try to construct it yourself.""" - __slots__ = ('_exists', '_is_dir', '_is_file', '_is_symlink') - - def exists(self, *, follow_symlinks=True): - """Whether this path exists.""" - if not follow_symlinks and self.is_symlink(): - return True - try: - return self._exists - except AttributeError: - if os.path.exists(self._path): - self._exists = True - return True - else: - self._exists = self._is_dir = self._is_file = False - return False - - def is_dir(self, *, follow_symlinks=True): - """Whether this path is a directory.""" - if not follow_symlinks and self.is_symlink(): - return False - try: - return self._is_dir - except AttributeError: - if os.path.isdir(self._path): - self._is_dir = self._exists = True - return True - else: - self._is_dir = False - return False - - def is_file(self, *, follow_symlinks=True): - """Whether this path is a regular file.""" - if not follow_symlinks and self.is_symlink(): - return False - try: - return self._is_file - except AttributeError: - if os.path.isfile(self._path): - self._is_file = self._exists = True - return True - else: - self._is_file = False - return False - - def is_symlink(self): - """Whether this path is a symbolic link.""" - try: - return self._is_symlink - except AttributeError: - self._is_symlink = os.path.islink(self._path) - return self._is_symlink - - -class _PosixPathInfo(_PathInfoBase): - """Implementation of pathlib.types.PathInfo that provides status - information for POSIX paths. Don't try to construct it yourself.""" - __slots__ = () - - def exists(self, *, follow_symlinks=True): - """Whether this path exists.""" - st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True) - if st is None: - return False - return True - - def is_dir(self, *, follow_symlinks=True): - """Whether this path is a directory.""" - st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True) - if st is None: - return False - return S_ISDIR(st.st_mode) - - def is_file(self, *, follow_symlinks=True): - """Whether this path is a regular file.""" - st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True) - if st is None: - return False - return S_ISREG(st.st_mode) - - def is_symlink(self): - """Whether this path is a symbolic link.""" - st = self._stat(follow_symlinks=False, ignore_errors=True) - if st is None: - return False - return S_ISLNK(st.st_mode) - - -PathInfo = _WindowsPathInfo if os.name == 'nt' else _PosixPathInfo - - -class DirEntryInfo(_PathInfoBase): - """Implementation of pathlib.types.PathInfo that provides status - information by querying a wrapped os.DirEntry object. Don't try to - construct it yourself.""" - __slots__ = ('_entry',) - - def __init__(self, entry): - super().__init__(entry.path) - self._entry = entry - - def _stat(self, *, follow_symlinks=True, ignore_errors=False): - try: - return self._entry.stat(follow_symlinks=follow_symlinks) - except OSError: - if not ignore_errors: - raise - return None - - def exists(self, *, follow_symlinks=True): - """Whether this path exists.""" - if not follow_symlinks: - return True - return self._stat(ignore_errors=True) is not None - - def is_dir(self, *, follow_symlinks=True): - """Whether this path is a directory.""" - try: - return self._entry.is_dir(follow_symlinks=follow_symlinks) - except OSError: - return False - - def is_file(self, *, follow_symlinks=True): - """Whether this path is a regular file.""" - try: - return self._entry.is_file(follow_symlinks=follow_symlinks) - except OSError: - return False - - def is_symlink(self): - """Whether this path is a symbolic link.""" - try: - return self._entry.is_symlink() - except OSError: - return False diff --git a/Lib/pathlib/types.py b/Lib/pathlib/types.py index d8f5c34a1a7..f21ce077454 100644 --- a/Lib/pathlib/types.py +++ b/Lib/pathlib/types.py @@ -11,9 +11,10 @@ from abc import ABC, abstractmethod -from glob import _PathGlobber +from glob import _GlobberBase from io import text_encoding -from pathlib._os import magic_open, ensure_distinct_paths, ensure_different_files, copyfileobj +from pathlib._os import (vfsopen, vfspath, ensure_distinct_paths, + ensure_different_files, copyfileobj) from pathlib import PurePath, Path from typing import Optional, Protocol, runtime_checkable @@ -60,6 +61,25 @@ def is_file(self, *, follow_symlinks: bool = True) -> bool: ... def is_symlink(self) -> bool: ... +class _PathGlobber(_GlobberBase): + """Provides shell-style pattern matching and globbing for ReadablePath. + """ + + @staticmethod + def lexists(path): + return path.info.exists(follow_symlinks=False) + + @staticmethod + def scandir(path): + return ((child.info, child.name, child) for child in path.iterdir()) + + @staticmethod + def concat_path(path, text): + return path.with_segments(vfspath(path) + text) + + stringify_path = staticmethod(vfspath) + + class _JoinablePath(ABC): """Abstract base class for pure path objects. @@ -86,20 +106,19 @@ def with_segments(self, *pathsegments): raise NotImplementedError @abstractmethod - def __str__(self): - """Return the string representation of the path, suitable for - passing to system calls.""" + def __vfspath__(self): + """Return the string representation of the path.""" raise NotImplementedError @property def anchor(self): """The concatenation of the drive and root, or ''.""" - return _explode_path(str(self), self.parser.split)[0] + return _explode_path(vfspath(self), self.parser.split)[0] @property def name(self): """The final path component, if any.""" - return self.parser.split(str(self))[1] + return self.parser.split(vfspath(self))[1] @property def suffix(self): @@ -135,7 +154,7 @@ def with_name(self, name): split = self.parser.split if split(name)[0]: raise ValueError(f"Invalid name {name!r}") - path = str(self) + path = vfspath(self) path = path.removesuffix(split(path)[1]) + name return self.with_segments(path) @@ -168,7 +187,7 @@ def with_suffix(self, suffix): def parts(self): """An object providing sequence-like access to the components in the filesystem path.""" - anchor, parts = _explode_path(str(self), self.parser.split) + anchor, parts = _explode_path(vfspath(self), self.parser.split) if anchor: parts.append(anchor) return tuple(reversed(parts)) @@ -179,24 +198,24 @@ def joinpath(self, *pathsegments): paths) or a totally different path (if one of the arguments is anchored). """ - return self.with_segments(str(self), *pathsegments) + return self.with_segments(vfspath(self), *pathsegments) def __truediv__(self, key): try: - return self.with_segments(str(self), key) + return self.with_segments(vfspath(self), key) except TypeError: return NotImplemented def __rtruediv__(self, key): try: - return self.with_segments(key, str(self)) + return self.with_segments(key, vfspath(self)) except TypeError: return NotImplemented @property def parent(self): """The logical parent of the path.""" - path = str(self) + path = vfspath(self) parent = self.parser.split(path)[0] if path != parent: return self.with_segments(parent) @@ -206,7 +225,7 @@ def parent(self): def parents(self): """A sequence of this path's logical parents.""" split = self.parser.split - path = str(self) + path = vfspath(self) parent = split(path)[0] parents = [] while path != parent: @@ -215,6 +234,33 @@ def parents(self): parent = split(path)[0] return tuple(parents) + def relative_to(self, other, *, walk_up=False): + """Return the relative path to another path identified by the passed + arguments. If the operation is not possible (because this is not + related to the other path), raise ValueError. + + The *walk_up* parameter controls whether `..` may be used to resolve + the path. + """ + parts = [] + for path in (other,) + other.parents: + if self.is_relative_to(path): + break + elif not walk_up: + raise ValueError(f"{self!r} is not in the subpath of {other!r}") + elif path.name == '..': + raise ValueError(f"'..' segment in {other!r} cannot be walked") + else: + parts.append('..') + else: + raise ValueError(f"{self!r} and {other!r} have different anchors") + return self.with_segments(*parts, *self.parts[len(path.parts):]) + + def is_relative_to(self, other): + """Return True if the path is relative to another path or False. + """ + return other == self or other in self.parents + def full_match(self, pattern): """ Return True if this path matches the given glob-style pattern. The @@ -223,7 +269,7 @@ def full_match(self, pattern): case_sensitive = self.parser.normcase('Aa') == 'Aa' globber = _PathGlobber(self.parser.sep, case_sensitive, recursive=True) match = globber.compile(pattern, altsep=self.parser.altsep) - return match(str(self)) is not None + return match(vfspath(self)) is not None class _ReadablePath(_JoinablePath): @@ -245,10 +291,10 @@ def info(self): raise NotImplementedError @abstractmethod - def __open_rb__(self, buffering=-1): + def __open_reader__(self): """ Open the file pointed to by this path for reading in binary mode and - return a file object, like open(mode='rb'). + return a file object. """ raise NotImplementedError @@ -256,7 +302,7 @@ def read_bytes(self): """ Open the file in bytes mode, read it, and close the file. """ - with magic_open(self, mode='rb', buffering=0) as f: + with vfsopen(self, mode='rb') as f: return f.read() def read_text(self, encoding=None, errors=None, newline=None): @@ -266,7 +312,7 @@ def read_text(self, encoding=None, errors=None, newline=None): # Call io.text_encoding() here to ensure any warning is raised at an # appropriate stack level. encoding = text_encoding(encoding) - with magic_open(self, mode='r', encoding=encoding, errors=errors, newline=newline) as f: + with vfsopen(self, mode='r', encoding=encoding, errors=errors, newline=newline) as f: return f.read() @abstractmethod @@ -375,10 +421,10 @@ def mkdir(self): raise NotImplementedError @abstractmethod - def __open_wb__(self, buffering=-1): + def __open_writer__(self, mode): """ Open the file pointed to by this path for writing in binary mode and - return a file object, like open(mode='wb'). + return a file object. """ raise NotImplementedError @@ -388,7 +434,7 @@ def write_bytes(self, data): """ # type-check for the buffer interface before truncating the file view = memoryview(data) - with magic_open(self, mode='wb') as f: + with vfsopen(self, mode='wb') as f: return f.write(view) def write_text(self, data, encoding=None, errors=None, newline=None): @@ -401,7 +447,7 @@ def write_text(self, data, encoding=None, errors=None, newline=None): if not isinstance(data, str): raise TypeError('data must be str, not %s' % data.__class__.__name__) - with magic_open(self, mode='w', encoding=encoding, errors=errors, newline=newline) as f: + with vfsopen(self, mode='w', encoding=encoding, errors=errors, newline=newline) as f: return f.write(data) def _copy_from(self, source, follow_symlinks=True): @@ -412,7 +458,7 @@ def _copy_from(self, source, follow_symlinks=True): while stack: src, dst = stack.pop() if not follow_symlinks and src.info.is_symlink(): - dst.symlink_to(str(src.readlink()), src.info.is_dir()) + dst.symlink_to(vfspath(src.readlink()), src.info.is_dir()) elif src.info.is_dir(): children = src.iterdir() dst.mkdir() @@ -420,8 +466,8 @@ def _copy_from(self, source, follow_symlinks=True): stack.append((child, dst.joinpath(child.name))) else: ensure_different_files(src, dst) - with magic_open(src, 'rb') as source_f: - with magic_open(dst, 'wb') as target_f: + with vfsopen(src, 'rb') as source_f: + with vfsopen(dst, 'wb') as target_f: copyfileobj(source_f, target_f) diff --git a/Lib/pdb.py b/Lib/pdb.py index 7dcd0cb1b37..e0c9b4d2c24 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -97,12 +97,16 @@ import selectors import threading import _colorize -import _pyrepl.utils from contextlib import ExitStack, closing, contextmanager from types import CodeType from warnings import deprecated +try: + import _pyrepl.utils +except ModuleNotFoundError: + _pyrepl = None + class Restart(Exception): """Causes a debugger to be restarted for the debugged python program.""" @@ -130,7 +134,7 @@ def find_first_executable_line(code): return code.co_firstlineno def find_function(funcname, filename): - cre = re.compile(r'def\s+%s(\s*\[.+\])?\s*[(]' % re.escape(funcname)) + cre = re.compile(r'(?:async\s+)?def\s+%s(\s*\[.+\])?\s*[(]' % re.escape(funcname)) try: fp = tokenize.open(filename) except OSError: @@ -314,12 +318,34 @@ def namespace(self): class _PdbInteractiveConsole(code.InteractiveConsole): - def __init__(self, ns, message): + def __init__(self, ns=None, message=None): self._message = message super().__init__(locals=ns, local_exit=True) def write(self, data): - self._message(data, end='') + if self._message is not None: + self._message(data, end='') + else: + super().write(data) + + def more_lines(self, text): + # Generic Python multi-line completeness heuristic. + # Strips pyrepl's trailing auto-indent before compiling. + # This should be functionally identical to simple_interact._more_lines + src = text.rstrip(" \t") + n = len(src) + if n > 0 and text[n-1] == '\n': + text = src + try: + code_obj = self.compile(text, "", "single") + except (OverflowError, SyntaxError, ValueError): + lines = text.splitlines(keepends=True) + if len(lines) == 1: + return False + last = lines[-1] + return ((last.startswith((" ", "\t")) or last.strip() != "") + and not last.endswith("\n")) + return code_obj is None # Interaction prompt line will separate file and call info from code @@ -348,6 +374,117 @@ def get_default_backend(): return _default_backend +def _pyrepl_available(): + """return whether pdb should use _pyrepl for input""" + if os.getenv("PYTHON_BASIC_REPL"): + CAN_USE_PYREPL = False + else: + try: + from _pyrepl.main import CAN_USE_PYREPL + except ModuleNotFoundError: + CAN_USE_PYREPL = False + return CAN_USE_PYREPL + + +class PdbPyReplInput: + def __init__(self, pdb_instance, stdin, stdout, prompt): + import _pyrepl.readline + + self.pdb_instance = pdb_instance + self.prompt = prompt + self.console = _PdbInteractiveConsole() + if not (os.isatty(stdin.fileno())): + raise ValueError("stdin is not a TTY") + self.readline_wrapper = _pyrepl.readline._ReadlineWrapper( + f_in=stdin.fileno(), + f_out=stdout.fileno(), + config=_pyrepl.readline.ReadlineConfig( + completer_delims=frozenset(' \t\n`@#%^&*()=+[{]}\\|;:\'",<>?') + ) + ) + self.readline_wrapper.get_reader().gen_colors = self.gen_colors + + def readline(self): + + def more_lines(text): + if text.strip() == "\x1a": + # Ctrl + Z raises EOFError to quit pdb + # This is similarly handled in simple_interact.py + raise EOFError + cmd, _, line = self.pdb_instance.parseline(text) + if not line or not cmd: + return False + func = getattr(self.pdb_instance, 'do_' + cmd, None) + if func is not None: + return False + return self.console.more_lines(text) + + try: + pyrepl_completer = self.readline_wrapper.get_completer() + self.readline_wrapper.set_completer(self.complete) + multiline = ( + self.readline_wrapper.multiline_input( + more_lines, + self.prompt, + '... ' + ' ' * (len(self.prompt) - 4) + ) + '\n' + ) + return multiline + except EOFError: + return 'EOF' + finally: + self.readline_wrapper.set_completer(pyrepl_completer) + + def complete(self, text, state): + """ + This function is very similar to cmd.Cmd.complete. + However, cmd.Cmd.complete assumes that we use readline module, but + pyrepl does not use it. + """ + if state == 0: + origline = self.readline_wrapper.get_line_buffer() + line = origline.lstrip() + stripped = len(origline) - len(line) + begidx = self.readline_wrapper.get_begidx() - stripped + endidx = self.readline_wrapper.get_endidx() - stripped + if begidx > 0: + cmd, args, foo = self.pdb_instance.parseline(line) + if not cmd: + compfunc = self.pdb_instance.completedefault + else: + try: + compfunc = getattr(self.pdb_instance, 'complete_' + cmd) + except AttributeError: + compfunc = self.pdb_instance.completedefault + else: + compfunc = self.pdb_instance.completenames + self.completion_matches = compfunc(text, line, begidx, endidx) + try: + return self.completion_matches[state] + except IndexError: + return None + + def gen_colors(self, buffer): + from _pyrepl.utils import ColorSpan, Span + + if not buffer.strip(): + return + + leading_spaces = len(buffer) - len(buffer.lstrip()) + leading_text = buffer.split()[0] + if hasattr(self.pdb_instance, 'do_' + leading_text): + yield ColorSpan( + Span(leading_spaces, leading_spaces + len(leading_text) - 1), + "soft_keyword" + ) + # Redact the command text with spaces so there will be no duplicated + # color span generated for it later. + redact_length = leading_spaces + len(leading_text) + buffer = ' ' * redact_length + buffer[redact_length:] + + yield from _pyrepl.utils.gen_colors(buffer) + + class Pdb(bdb.Bdb, cmd.Cmd): _previous_sigint_handler = None @@ -364,8 +501,8 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, bdb.Bdb.__init__(self, skip=skip, backend=backend if backend else get_default_backend()) cmd.Cmd.__init__(self, completekey, stdin, stdout) sys.audit("pdb.Pdb") - if stdout: - self.use_rawinput = 0 + if stdin is not None and stdin is not sys.stdin: + self.use_rawinput = False self.prompt = '(Pdb) ' self.aliases = {} self.displaying = {} @@ -382,6 +519,12 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, except ImportError: pass + self.pyrepl_input = None + if _pyrepl_available(): + try: + self.pyrepl_input = PdbPyReplInput(self, self.stdin, self.stdout, self.prompt) + except Exception: + pass self.allow_kbdint = False self.nosigint = nosigint # Consider these characters as part of the command so when the users type @@ -391,17 +534,22 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, # Read ~/.pdbrc and ./.pdbrc self.rcLines = [] if readrc: + home_rcfile = os.path.expanduser("~/.pdbrc") + local_rcfile = os.path.abspath(".pdbrc") + try: - with open(os.path.expanduser('~/.pdbrc'), encoding='utf-8') as rcFile: - self.rcLines.extend(rcFile) - except OSError: - pass - try: - with open(".pdbrc", encoding='utf-8') as rcFile: - self.rcLines.extend(rcFile) + with open(home_rcfile, encoding='utf-8') as rcfile: + self.rcLines.extend(rcfile) except OSError: pass + if local_rcfile != home_rcfile: + try: + with open(local_rcfile, encoding='utf-8') as rcfile: + self.rcLines.extend(rcfile) + except OSError: + pass + self.commands = {} # associates a command list to breakpoint numbers self.commands_defining = False # True while in the process of defining # a command list @@ -416,6 +564,12 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, self._current_task = None + self.lineno = None + self.stack = [] + self.curindex = 0 + self.curframe = None + self._user_requested_quit = False + def set_trace(self, frame=None, *, commands=None): Pdb._last_pdb_instance = self if frame is None: @@ -492,7 +646,7 @@ def forget(self): self.lineno = None self.stack = [] self.curindex = 0 - if hasattr(self, 'curframe') and self.curframe: + if self.curframe: self.curframe.f_globals.pop('__pdb_convenience_variables', None) self.curframe = None self.tb_lineno.clear() @@ -609,6 +763,31 @@ def user_exception(self, frame, exc_info): self.message('%s%s' % (prefix, self._format_exc(exc_value))) self.interaction(frame, exc_traceback) + @contextmanager + def _replace_attribute(self, attrs): + original_attrs = {} + for attr, value in attrs.items(): + original_attrs[attr] = getattr(self, attr) + setattr(self, attr, value) + try: + yield + finally: + for attr, value in original_attrs.items(): + setattr(self, attr, value) + + @contextmanager + def _maybe_use_pyrepl_as_stdin(self): + if self.pyrepl_input is None: + yield + return + + with self._replace_attribute({ + 'stdin': self.pyrepl_input, + 'use_rawinput': False, + 'prompt': '', + }): + yield + # General interaction function def _cmdloop(self): while True: @@ -616,7 +795,8 @@ def _cmdloop(self): # keyboard interrupts allow for an easy way to cancel # the current command, so allow them during interactive input self.allow_kbdint = True - self.cmdloop() + with self._maybe_use_pyrepl_as_stdin(): + self.cmdloop() self.allow_kbdint = False break except KeyboardInterrupt: @@ -666,7 +846,7 @@ def _show_display(self): def _get_tb_and_exceptions(self, tb_or_exc): """ - Given a tracecack or an exception, return a tuple of chained exceptions + Given a traceback or an exception, return a tuple of chained exceptions and current traceback to inspect. This will deal with selecting the right ``__cause__`` or ``__context__`` @@ -1086,7 +1266,7 @@ def handle_command_def(self, line): return False def _colorize_code(self, code): - if self.colorize: + if self.colorize and _pyrepl: colors = list(_pyrepl.utils.gen_colors(code)) chars, _ = _pyrepl.utils.disp_str(code, colors=colors, force_color=True) code = "".join(chars) @@ -1309,7 +1489,14 @@ def do_commands(self, arg): reached. """ if not arg: - bnum = len(bdb.Breakpoint.bpbynumber) - 1 + for bp in reversed(bdb.Breakpoint.bpbynumber): + if bp is None: + continue + bnum = bp.number + break + else: + self.error('cannot set commands: no existing breakpoint') + return else: try: bnum = int(arg) @@ -1513,7 +1700,7 @@ def checkline(self, filename, lineno, module_globals=None): """ # this method should be callable before starting debugging, so default # to "no globals" if there is no current frame - frame = getattr(self, 'curframe', None) + frame = self.curframe if module_globals is None: module_globals = frame.f_globals if frame else None line = linecache.getline(filename, lineno, module_globals) @@ -2248,9 +2435,13 @@ def _print_lines(self, lines, start, breaks=(), frame=None): s += '->' elif lineno == exc_lineno: s += '>>' + # Strip the trailing newline before colorizing: the colorizer + # renders control characters (like '\n') in caret notation, so a + # later rstrip() could not remove the resulting '^J'. + line = line.rstrip() if self.colorize: line = self._colorize_code(line) - self.message(s + '\t' + line.rstrip()) + self.message(s + '\t' + line) def do_whatis(self, arg): """whatis expression @@ -2342,10 +2533,21 @@ def do_interact(self, arg): contains all the (global and local) names found in the current scope. """ ns = {**self.curframe.f_globals, **self.curframe.f_locals} - with self._enable_rlcompleter(ns): - console = _PdbInteractiveConsole(ns, message=self.message) - console.interact(banner="*pdb interact start*", - exitmsg="*exit from pdb interact command*") + console = _PdbInteractiveConsole(ns, message=self.message) + banner = "*pdb interact start*" + exitmsg = "*exit from pdb interact command*" + if self.pyrepl_input is not None: + from _pyrepl.simple_interact import run_multiline_interactive_console + self.message(banner) + try: + run_multiline_interactive_console(console) + except SystemExit: + pass + self.message(exitmsg) + else: + with self._enable_rlcompleter(ns): + console.interact(banner=banner, + exitmsg=exitmsg) def do_alias(self, arg): """alias [name [command]] @@ -2443,7 +2645,9 @@ def print_stack_trace(self, count=None): except KeyboardInterrupt: pass - def print_stack_entry(self, frame_lineno, prompt_prefix=line_prefix): + def print_stack_entry(self, frame_lineno, prompt_prefix=None): + if prompt_prefix is None: + prompt_prefix = line_prefix frame, lineno = frame_lineno if frame is self.curframe: prefix = '> ' @@ -2958,6 +3162,15 @@ def postloop(self): if self.quitting: self.detach() + @contextmanager + def _maybe_use_pyrepl_as_stdin(self): + # The server reads every command from the client over the socket, never + # from a local pyrepl. The base implementation swaps in pyrepl as stdin + # and blanks `self.prompt` to '' (pyrepl would draw the prompt itself), + # which here would transmit an empty prompt to the client whenever the + # target process happens to be pyrepl-capable. Keep the real prompt. + yield + def detach(self): # Detach the debugger and close the socket without raising BdbQuit self.quitting = False @@ -3428,8 +3641,7 @@ def _connect( f"\nLocal pdb module's protocol version: {attach_ver}" ) else: - remote_pdb.rcLines.extend(commands.splitlines()) - remote_pdb.set_trace(frame=frame) + remote_pdb.set_trace(frame=frame, commands=commands.splitlines()) def attach(pid, commands=()): @@ -3535,18 +3747,18 @@ def help(): pydoc.pager(__doc__) _usage = """\ -Debug the Python program given by pyfile. Alternatively, +Debug the Python program given by `pyfile`. Alternatively, an executable module or package to debug can be specified using -the -m switch. You can also attach to a running Python process -using the -p option with its PID. +the `-m` switch. You can also attach to a running Python process +using the `-p` option with its PID. -Initial commands are read from .pdbrc files in your home directory +Initial commands are read from `.pdbrc` files in your home directory and in the current directory, if they exist. Commands supplied with --c are executed after commands from .pdbrc files. +`-c` are executed after commands from `.pdbrc` files. -To let the script run until an exception occurs, use "-c continue". +To let the script run until an exception occurs, use `-c continue`. To let the script run up to a given line X in the debugged file, use -"-c 'until X'".""" +`-c 'until X'`.""" def exit_with_permission_help_text(): @@ -3579,13 +3791,12 @@ def parse_args(): description=_usage, formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False, - color=True, ) # Get all the commands out first. For backwards compatibility, we allow # -c commands to be after the target. parser.add_argument('-c', '--command', action='append', default=[], metavar='command', dest='commands', - help='pdb commands to execute as if given in a .pdbrc file') + help='pdb commands to execute as if given in a `.pdbrc` file') opts, args = parser.parse_known_args() @@ -3630,7 +3841,13 @@ def main(): if getattr(opts, 'pid', None) is not None: try: attach(opts.pid, opts.commands) - except PermissionError as e: + except RuntimeError: + print( + f"Cannot attach to pid {opts.pid}, please make sure that the process exists " + "and is using the same Python version." + ) + sys.exit(1) + except PermissionError: exit_with_permission_help_text() return elif getattr(opts, 'module', None) is not None: diff --git a/Lib/pickle.py b/Lib/pickle.py index 7b951858604..f92b1fde768 100644 --- a/Lib/pickle.py +++ b/Lib/pickle.py @@ -17,7 +17,6 @@ Misc variables: - __version__ format_version compatible_formats @@ -190,6 +189,11 @@ def __init__(self, value): __all__.extend(x for x in dir() if x.isupper() and not x.startswith('_')) +# Data larger than this will be read in chunks, to prevent extreme +# overallocation. +_MIN_READ_BUF_SIZE = (1 << 20) + + class _Framer: _FRAME_SIZE_MIN = 4 @@ -288,7 +292,7 @@ def read(self, n): "pickle exhausted before end of frame") return data else: - return self.file_read(n) + return self._chunked_file_read(n) def readline(self): if self.current_frame: @@ -303,11 +307,23 @@ def readline(self): else: return self.file_readline() + def _chunked_file_read(self, size): + cursize = min(size, _MIN_READ_BUF_SIZE) + b = self.file_read(cursize) + while cursize < size and len(b) == cursize: + delta = min(cursize, size - cursize) + b += self.file_read(delta) + cursize += delta + return b + def load_frame(self, frame_size): if self.current_frame and self.current_frame.read() != b'': raise UnpicklingError( "beginning of a new frame before end of current frame") - self.current_frame = io.BytesIO(self.file_read(frame_size)) + data = self._chunked_file_read(frame_size) + if len(data) < frame_size: + raise EOFError + self.current_frame = io.BytesIO(data) # Tools used for pickling. @@ -1147,13 +1163,23 @@ def save_frozenset(self, obj): def save_global(self, obj, name=None): write = self.write - memo = self.memo if name is None: name = getattr(obj, '__qualname__', None) if name is None: name = obj.__name__ + if '.__' in name: + # Mangle names of private attributes. + dotted_path = name.split('.') + for i, subpath in enumerate(dotted_path): + if i and subpath.startswith('__') and not subpath.endswith('__'): + prev = prev.lstrip('_') + if prev: + dotted_path[i] = f"_{prev.lstrip('_')}{subpath}" + prev = subpath + name = '.'.join(dotted_path) + module_name = whichmodule(obj, name) if self.proto >= 2: code = _extension_registry.get((module_name, name), _NoValue) @@ -1491,12 +1517,17 @@ def load_binbytes8(self): dispatch[BINBYTES8[0]] = load_binbytes8 def load_bytearray8(self): - len, = unpack(' maxsize: + size, = unpack(' maxsize: raise UnpicklingError("BYTEARRAY8 exceeds system's maximum size " "of %d bytes" % maxsize) - b = bytearray(len) - self.readinto(b) + cursize = min(size, _MIN_READ_BUF_SIZE) + b = bytearray(cursize) + if self.readinto(b) == cursize: + while cursize < size and len(b) == cursize: + delta = min(cursize, size - cursize) + b += self.read(delta) + cursize += delta self.append(b) dispatch[BYTEARRAY8[0]] = load_bytearray8 @@ -1729,7 +1760,7 @@ def load_binget(self): i = self.read(1)[0] try: self.append(self.memo[i]) - except KeyError as exc: + except KeyError: msg = f'Memo value not found at index {i}' raise UnpicklingError(msg) from None dispatch[BINGET[0]] = load_binget @@ -1738,7 +1769,7 @@ def load_long_binget(self): i, = unpack(' 50: annocol = annotate - line += ' ' + opcode.doc.split('\n', 1)[0] + doc = opcode.doc.split('\n', 1)[0] + line += f" {t.annotation}{doc}{t.reset}" print(line, file=out) if errormsg: @@ -2541,7 +2583,11 @@ def dis(pickle, out=None, memo=None, indentlevel=4, annotate=0): stack.extend(after) - print("highest protocol among opcodes =", maxproto, file=out) + print( + "highest protocol among opcodes =", + f"{t.proto}{maxproto}{t.reset}", + file=out, + ) if stack: raise ValueError("stack not empty after STOP: %r" % stack) @@ -2839,12 +2885,9 @@ def __init__(self, value): } -if __name__ == "__main__": +def _main(args=None): import argparse - parser = argparse.ArgumentParser( - description='disassemble one or more pickle files', - color=True, - ) + parser = argparse.ArgumentParser(description='disassemble one or more pickle files') parser.add_argument( 'pickle_file', nargs='+', help='the pickle file') @@ -2856,7 +2899,7 @@ def __init__(self, value): help='preserve memo between disassemblies') parser.add_argument( '-l', '--indentlevel', default=4, type=int, - help='the number of blanks by which to indent a new MARK level') + help='the number of blanks by which to indent a new `MARK` level') parser.add_argument( '-a', '--annotate', action='store_true', help='annotate each line with a short opcode description') @@ -2864,7 +2907,7 @@ def __init__(self, value): '-p', '--preamble', default="==> {name} <==", help='if more than one pickle file is specified, print this before' ' each disassembly') - args = parser.parse_args() + args = parser.parse_args(args) annotate = 30 if args.annotate else 0 memo = {} if args.memo else None if args.output is None: @@ -2885,3 +2928,7 @@ def __init__(self, value): finally: if output is not sys.stdout: output.close() + + +if __name__ == "__main__": + _main() diff --git a/Lib/pkgutil.py b/Lib/pkgutil.py index 8772a66791a..11c2a4b0ef4 100644 --- a/Lib/pkgutil.py +++ b/Lib/pkgutil.py @@ -9,6 +9,9 @@ import os.path import sys +lazy import re + + __all__ = [ 'get_importer', 'iter_importers', 'walk_packages', 'iter_modules', 'get_data', @@ -398,9 +401,10 @@ def get_data(package, resource): return loader.get_data(resource_name) -_NAME_PATTERN = None +_LENIENT_PATTERN = None +_STRICT_PATTERN = None -def resolve_name(name): +def resolve_name(name, *, strict=False): """ Resolve a name to an object. @@ -410,6 +414,7 @@ def resolve_name(name): W(.W)* W(.W)*:(W(.W)*)? + W(.W)*:(W(.W)*) The first form is intended for backward compatibility only. It assumes that some part of the dotted name is a package, and the rest is an object @@ -424,6 +429,11 @@ def resolve_name(name): hierarchy within that package. Only one import is needed in this form. If it ends with the colon, then a module object is returned. + The first two forms are accepted when `strict=False` (the default). + + The third form requires both the module name and callable, separated by + a colon. Only this form is accepted when `strict=True`. + The function will return an object (which might be a module), or raise one of the following exceptions: @@ -432,18 +442,26 @@ def resolve_name(name): AttributeError - if a failure occurred when traversing the object hierarchy within the imported package to get to the desired object. """ - global _NAME_PATTERN - if _NAME_PATTERN is None: - # Lazy import to speedup Python startup time - import re - dotted_words = r'(?!\d)(\w+)(\.(?!\d)(\w+))*' - _NAME_PATTERN = re.compile(f'^(?P{dotted_words})' - f'(?P:(?P{dotted_words})?)?$', - re.UNICODE) - - m = _NAME_PATTERN.match(name) - if not m: + global _LENIENT_PATTERN, _STRICT_PATTERN + dotted_words = r'(?!\d)(\w+)(\.(?!\d)(\w+))*' + if strict: + if _STRICT_PATTERN is None: + _STRICT_PATTERN = re.compile( + f'^(?P{dotted_words})' + f'(?P:(?P{dotted_words}))$', + re.UNICODE) + pattern = _STRICT_PATTERN + else: + if _LENIENT_PATTERN is None: + _LENIENT_PATTERN = re.compile( + f'^(?P{dotted_words})' + f'(?P:(?P{dotted_words})?)?$', + re.UNICODE) + pattern = _LENIENT_PATTERN + + if (m := pattern.match(name)) is None: raise ValueError(f'invalid format: {name!r}') + gd = m.groupdict() if gd.get('cln'): # there is a colon - a one-step import is all that's needed diff --git a/Lib/profile.py b/Lib/profile.py index a5afb12c9d1..304284da421 100644 --- a/Lib/profile.py +++ b/Lib/profile.py @@ -28,9 +28,18 @@ import sys import time import marshal +import warnings __all__ = ["run", "runctx", "Profile"] +# Emit deprecation warning as per PEP 799 +warnings.warn( + "The profile module is deprecated and will be removed in Python 3.17. " + "Use profiling.tracing (or cProfile) for tracing profilers instead.", + DeprecationWarning, + stacklevel=2 +) + # Sample timer for use with #i_count = 0 #def integer_timer(): @@ -598,7 +607,6 @@ def main(): '__file__': spec.origin, '__name__': spec.name, '__package__': None, - '__cached__': None, } try: runctx(code, globs, None, options.outfile, options.sort) diff --git a/Lib/profiling/__init__.py b/Lib/profiling/__init__.py new file mode 100644 index 00000000000..c94e1e16e35 --- /dev/null +++ b/Lib/profiling/__init__.py @@ -0,0 +1,13 @@ +"""Python profiling tools. + +This package provides two types of profilers: + +- profiling.tracing: Deterministic tracing profiler that instruments every + function call and return. Higher overhead but provides exact call counts + and timing. + +- profiling.sampling: Statistical sampling profiler that periodically samples + the call stack. Low overhead and suitable for production use. +""" + +__all__ = ("tracing", "sampling") diff --git a/Lib/profiling/sampling/__init__.py b/Lib/profiling/sampling/__init__.py new file mode 100644 index 00000000000..71579a39032 --- /dev/null +++ b/Lib/profiling/sampling/__init__.py @@ -0,0 +1,23 @@ +"""Statistical sampling profiler for Python. + +This module provides low-overhead profiling by periodically sampling the +call stack rather than tracing every function call. +""" + +from .collector import Collector +from .pstats_collector import PstatsCollector +from .stack_collector import CollapsedStackCollector +from .heatmap_collector import HeatmapCollector +from .gecko_collector import GeckoCollector +from .jsonl_collector import JsonlCollector +from .string_table import StringTable + +__all__ = ( + "Collector", + "PstatsCollector", + "CollapsedStackCollector", + "HeatmapCollector", + "GeckoCollector", + "JsonlCollector", + "StringTable", +) diff --git a/Lib/profiling/sampling/__main__.py b/Lib/profiling/sampling/__main__.py new file mode 100644 index 00000000000..a45b645eae0 --- /dev/null +++ b/Lib/profiling/sampling/__main__.py @@ -0,0 +1,73 @@ +"""Run the sampling profiler from the command line.""" + +import sys + +MACOS_PERMISSION_ERROR = """\ +🔒 Permission Error: Unable to access process memory on macOS + +Tachyon needs elevated permissions to profile processes. Try one of these solutions: + +1. Try running again with elevated permissions by running 'sudo -E !!'. + +2. If targeting system Python processes: + Note: Apple's System Integrity Protection (SIP) may block access to system + Python binaries. Consider using a user-installed Python instead. +""" + +LINUX_PERMISSION_ERROR = """ +🔒 Tachyon was unable to access process memory. This could be because tachyon +has insufficient privileges (the required capability is CAP_SYS_PTRACE). +Unprivileged processes cannot trace processes that they cannot send signals +to or those running set-user-ID/set-group-ID programs, for security reasons. + +If your uid matches the uid of the target process you want to analyze, you +can do one of the following to get 'ptrace' scope permissions: + +* If you are running inside a Docker container, you need to make sure you + start the container using the '--cap-add=SYS_PTRACE' or '--privileged' + command line arguments. Notice that this may not be enough if you are not + running as 'root' inside the Docker container as you may need to disable + hardening (see next points). + +* Try running again with elevated permissions by running 'sudo -E !!'. + +* You can disable kernel hardening for the current session temporarily (until + a reboot happens) by running 'echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope'. +""" + +WINDOWS_PERMISSION_ERROR = """ +🔒 Tachyon requires administrator rights to access process memory on Windows. +Please run your command prompt as Administrator and try again. +""" + +GENERIC_PERMISSION_ERROR = """ +🔒 Tachyon was unable to access the target process due to operating +system restrictions or missing privileges. +""" + +from .cli import main +from .errors import SamplingUnknownProcessError, SamplingModuleNotFoundError, SamplingScriptNotFoundError + +def handle_permission_error(): + """Handle PermissionError by displaying appropriate error message.""" + if sys.platform == "darwin": + print(MACOS_PERMISSION_ERROR, file=sys.stderr) + elif sys.platform.startswith("linux"): + print(LINUX_PERMISSION_ERROR, file=sys.stderr) + elif sys.platform.startswith("win"): + print(WINDOWS_PERMISSION_ERROR, file=sys.stderr) + else: + print(GENERIC_PERMISSION_ERROR, file=sys.stderr) + sys.exit(1) + +if __name__ == '__main__': + try: + main() + except PermissionError: + handle_permission_error() + except SamplingUnknownProcessError as err: + print(f"Tachyon cannot find the process: {err}", file=sys.stderr) + sys.exit(1) + except (SamplingModuleNotFoundError, SamplingScriptNotFoundError) as err: + print(f"Tachyon cannot find the target: {err}", file=sys.stderr) + sys.exit(1) diff --git a/Lib/profiling/sampling/_assets/python-logo-only.png b/Lib/profiling/sampling/_assets/python-logo-only.png new file mode 100644 index 0000000000000000000000000000000000000000..e590b5c6480d949d8c298a1bf7fa3b6533c6ece6 GIT binary patch literal 12720 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 literal 0 HcmV?d00001 diff --git a/Lib/profiling/sampling/_assets/tachyon-logo.png b/Lib/profiling/sampling/_assets/tachyon-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..03e67823d5afc94c156953519418d5a8dc18283a GIT binary patch literal 94008 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�qKygth!Q`wM^1ue!Wqh)HoTF%Pn;@u*tOIfTtNhS{kydv%ozS@lnj`m`1 zYK>_n1%?3b3*rkElWT;F!9EInr4s!aAHG;6@w-Y75=mVTCICr{oI?b0ci>)|Q7*LU(}L@eH@(4iVW?x_7DWc&x9d zV0X*iZ;0~=LQ+$f)w(?)j3%PLW(Tf>Go1;Y)>rsAjG=i--AqPkthyFSo#nfzm*`ID zP!#dC(4KFi$uTU&zor@oMuP#7Ma3Hxc93HDC%}AX0KtD>-NzHUV+1u$jCoL^sbk%c z$8wRJro!5h7ro6UJox$ZDpCk4NLW1p1QoPSn?oa?WgTb1%~A>8n(`1zl&sl}@Skta z7iqjcQ!l&w5fR>O#huG^4lVI@$%F2w(B5y!F8fmPque2>njSl-9{80w3>D@xrlxqx zJ|$2r!oRuc7D=7x`*L}fJsiwvpJg;&6$seD51M60$a$$dG7h`4`IA&`sSCfG1=gj> z0?c`|QD9w_u~cELlK4&3{Z3LpOG4x{IYJ$TU%ZVbyohetM?A$puY^*t?6CV?>$E#W z{4MJ^HO>Is@z#=uaNyH{C5?uun~V6GrS|1j#{fIkQnk7|9t&#BZ?8`)ryu3UTz`^! zb&5N@1v^`U4ZDBi(-iTb8;9LJ(KPm(TO@Uq9@TSE@YTWR-Zm_raA`F0MKaQzn@x1W z>BmA}VL~L8@FYffEXcec)x!25mBDnZgZ=MklDZ@*L{M?PUU`Ir-UbsHfQ{$If{(cg z>b(^Ol}7bq9vH1tVjWlYKC(Q78^@Mq#l0x+37hnMKJ?>&$I4TFfHbK~kgxO$c->s+ z4sXHkSzzoF%=L;p%WMkV1#c*eKT=8i`(NB5se`DkK{;4j8D!pHfp6!16eGmE_vo-pY3Vk z63NZl@3H`{Qn-}CHu@`UvZV2|7nn=|i1N+DZjsd9zWUFHnMhdN&jtCJ`qoSYF|>~& zLXE8Ni=l`+wH*fgHy(~(T8b^!YtzZ`dQ!}NOMO0}agA06AkyIxg)|KFzMO6?{sgC4JK zyL!T9i$3OwmXC zNungFI7;esdK&G(&P)5q+xD#Pkkk_m{AH*((+m!g=DC#fzsyiNC`)&o^V9D~l6okD z$>|#itGvx5^3k<<4=$1lzq^m-0jzf4vsuTFAdEox&s!@V?w3@*rUnEU*0(Ix@kYep z=->CLxm5)0%#D0XLC?9vSx&bPT28my}4O!&E4R34fEi9}Wur0GT)(*<~Pmb`b@#Ru4_;F5s$|~&tBf?Wbu!WIS<_I$0 zNyhQ|L?AhR8DXC1u#l?mzwWo~SrFQ+awC`m7<{q1*dk<`Yt?oqPHoVg2ba!~8i zgbRaQ5XJ3dPf~9!5mXJx&fMvS{`Luwtulye?Z97mhX%2d)B%Bx4~$dbWl^JT8#k_ zdyZ|J^kyQce=RMn`YLl`q9e&76*PI9S+!2_T7Da0qsgnhe&2}De{Iva zxAI2GkxB9yXk0HwncX)!$=c*LMQcFx9R^ZwGUs5+OrIPii#0oR6DT+ zyD}Z~;+jVs&4k#Rrh57Jwj3k{W-kwxAcp;Zt8Ni_p2ItIVj`(q#@3=5cWSTJO5BP9>TkAvT1zGDVZ6>;2;Hn&r+6(tgD}fl zqLQb~aCPnW3G;mef^>C$F{N{}o=NT*7~7@2a(6{Rbq#C{{~BOMUc3hSx9P$y@iwzh zOC@e)0jYCvbg7RJK_3czir+_2udYk4MAh!hbi9I=}Dl3F@wRMXltb+(7J8je8GTlmWH74JSOT-@x3 zn+t47s+~wKTZHx#a=b1a_ag_H`Um}5SfIui)fy#nbs-j#x`!e){~05xED&V8PK@K# z2@^R@fnB0(YxN-7GdCt$xx2ieIr_Hb1Lc!W9UcgB3gl+Vz-E4P0N6F5x>7E}2>UFw ziL#K?A?*Wia3w1ZQLoebyYsV4tC7_(Xq^s2$YisY(<2N-_}eZ5esFT-u-D(dTY7|Q zHw)*ne^Qks6@5JRMTJON1Ke4VgOh!h+TV_<$6hyDd9bq2#?ej2901Yuk-J(!qlsEW zHQ;h=hJb?ojv0|wjf0jOQfE$$D56fB-H^=$xGm_Noyz3YJHWep9Er77c=|$GD zX;0<1IVA(m_1=k9mC}_5-|0vbNv0v{L0E9jg_}bjd1(Apf-9k!cz_(T1^oy+5I+W@lk#xyIqrBA-^xM zK7g2#=K*Q|h;HaX&-Uigb(F+a`66KraKpL=z_g-ll|j@W7H&QV*-MO6?rP*NfCMCv zrnsFi>ihKRm5(VUYVc(R#t$xz_xgv#^h|+65K2X|qO-iv!+m8_nO#KD< z5nXvFxRv6+o8T5n%@*{cZY`6dzh!&z0aOA=HlJ{=uk;&#Z>z5^<=&R`@<_P(%i_Ag zeSxn3g{B8!dsZc3S6zw?*hBz(1R;5;5&mN!l)HnBmy)pwO9@tv5af8}5jG&aOG!?j z#OYAm7g(!Cy_J2RS7oYc9Mie|eML}##GIZH#T1deG`B=VG$5UwRfhVUFQShSyA2(p zH9iszs}MI!Znwx?}xO8lv4l~8Jbw{}u)YkP5j~f0Qn>p8z2E$4g7b{6P&5H#InqJG{ktMTO>6#_L^50gYC^YA0t<$ ziF&M9L?F|-R_CF0;>YdCYF=c&b`uND8{)PwugL}PPc5=eHF3>btdZ2(K}cRcMnd^{ zknuiZ9PgIM{Gi|0$KxboOV#prq!+!MQXzCi_}%Ws1vPT8Y4W-i{rq_qrAqaINtT#q zN1IvCmfh9pFz*~_#1EP(qN)rIOF9{%I<)5so45Acq7{a!O;Ok97vs8ah|0zeZ>Tod zNw^lMC!LRw-GQ9uLij4s7*%Tw&{OMnSU0>uNbX6Engh@WPg%$F_-17Nh-pHCH887h zOKxjjbqx%Ywg!X|XbnJg80xqENMqD+WlGQ1D&Y*pbMS7tfkq_tL?hGQ4?#21o$~Ju zDV3NWm4(&|{8le7I8s^A2EBtlNxeK?E>}^5gj%$@6+KB6iKG&00s*aK&6)8wuGQwD z!Eg9(T1AK2&~iq7f&>RBCV7KBZ{g0rcjS{)EJ7Q%<7spA6lDRobNxsga=g3}?$4tL zYwm4$B#|+S3iu!h$!p6P zP}dr&2r$bzPVC-RY1yccde82yC@AH+|5;W@Re>mu=9Tk0mLj|!APX=BM_3)l_^ex7 zswm*sBitj1Fz)1@ZES0q?V@_{krd9%0W<+=jd8Npz{mV+Kp3#LgRKF$UMOpVzpsVf zSznK6p!~b6P{`+C`|MZr^gB2~sy3V39yjgDyXU{4l&jlOcwUNB?M*~(yUWon1rD(_ zvosk?f2Ysk)Xh*P%XxvCRG*VpUE}%=6m`gUPa*G988=DlUSBtPK8KovrwE&@`XME$ z@kguaDS6q=QxKiiY|Ko}Q#+m&TU3|Fk>ib)b)OPfzq5dYq?Qgs^5!y<%A5hl%7XAW z<9G)INoqgmvGaxnK+txKtEseT*e4>1X^BchlyP0#E4x3dCT(L51laf>Y6;P5H=}0L zd$(dbuUibMOjZCngNRa9dbuueG<(Z|oIZrm+ev+btfBnbrnw~$poUlWJ%RJsKdCa) zdBBH4&JYn*AjI%-0w1ZeK2RmC0V#9OvId}BNo%MI*BsPs8m;`bA(d@Z^8D(|O8fS8 zrL9rY4pqFY?-heHX#cMA6v<6fCmS0&ve1_jus-w|g3yi;l-KHM;cBrqS@r?s-(+`_ zq%LqO$^}7KQz%=o&ViMr!tsnnHO)1fv3pxGSQ@%k3Dn)L*2@$Sj>))3h;(Jca#E}s zslg(jCm0Cj9T5HxFxE&8JJde~=VfN3kW)Kco?Aj~Jg6FGZPA8xW0ZOQTBtHoff_;F z2GqFi9A8737t=y5FOV5+va5TH1pI>L{EMEgouaGj>?}+%IZcjG58-^E>xPDv3EkQ$ ze=hle>-@GLg7{(ip~CYodLGc+10PUssZ@aU(#FwsmERYC81gl6D)1VRQb#K?_u-y{ zDh=8xTW7thocl7fa%pWwWyNRjDy`c#P;!-$TRNdRO@T9{ScCLRKUE<0|L%HKe#ZCb z!qj7vS>Zs9@H{oQcH|9kIcdlGF2GoCA^gL6j!y{8%kfAhsCF-D zsCk3*B5W#d5mJza8ql_jD!dOJ)qgB564CPl5lx7^fo3U0U4Yb2?$JiAnFH~3+Uve9 zxnx0lgneaxEJ*Xz(k7usc}P4DPXv`}@zb!m3KcyBzBhh~?q| z%@Fw$KMzPzFCExa$aDbYmOvH>Qr=KX@axPny0&K|t$}9f9A7IjQaEx75#c|UMD{c8 z<{Yq;pa2P`X+UC>nl#*?5TTUb;Jy6`T!t7GoOci+{K7#!i-D7!i2#&{;{SfmRlP`b zfErHX`WlNjbWIn9mZU=S6t4Ghs+r|pXxC>Ni|lZe`G%ZoAsD4?M~?TlAVF=)N&W!8 zLC3wrxFs0J8tO34qX=gN&an~aygX`jIsD$vwxUTIA1PCCYvcuSY z8ONz$Ge!!CunL~v86Le;^qaCtk)Q?wq~c+N{%vkawW&)}41kySuQQ|6RAo5R^`WZi z1yO#M4G5f+k{CgCEKz~$hv+D{13@GZy3WBZ=!k&&1P`Dy+**zSQA(&~4&`~An^hW- z;{NJfK=}-dG*nW42CQo!e-v5+UsSyBuRYKlh>+BQy($pFwgrMs*B=;M;pajIkv!d2 zZXoS^e8E%u*%yvSwq!pJgek&t#@PP-eN1ho_v&sPMo-eLSTjN@dr8Do@$^8>VEUbia>uDM@d z(C|{uZ%KKA9_#&1P@yZB$GQ_-v64DDwQkUmLMNk{KveCVkKBmzL$uZ@k_AL{kDyc; zElDBgK)wE{6&@&(w#aS`Jm7O6tdA;MHwrx`CM)%t20ytlL~h^A;qpd97Da$iT3?OE zVfN-VmqD7YFIFuP#s96h>lTrk6=(eM+JwY{IR$=idlva5HGnOujvI501rbCijQu9O z>sz?!X86^mUK}pAz>IngKxRgQ`aVaeyb~ZL{+`V-t}u*o-tyaI2>m6UpH`Oe5bgX7 z>(Hrr!hJ~CfTY>!7ixgUgZp(ug0yklCVYbEtli|1N=^Z{7k#93a`3i?G&iKQ-8$E+ z^NLg1TtW)o&8VK!9Jqb`k%Dlpfut}8tbhirKfX(?lS;8(mZ4^n`b)Gl#Q?ujM_gkX zxBnxKEOo2|na+>Q*Gg*3-k3-Jw_2efsmDz&47ZjdmYmj{0^j+PB&Vm0%#l&OFnRBL za=%nC5r?Q}{K=-~>pfUpY%yOLNRAi77(u-)kpKcNA-q5w;|0dwFC%`N5n;9D^8<{b zaEy8!VF76Rx&4y7v*<&$i6a!zD*&YPt5K)`Ky>MT+>}5Ti?<=-uZZkwW|u~T8qZ*9 zO-R9C<`i2G1dvT*X;^~4Ju@>(`gsJK1Gla}QUDTC;aUSpX%1M>pt(^)n(*65Inhnr zY!3Y^zW+bDaQ4-#K{}bahE}HOl2s$$C)b%tZK$$o9?KUBk~+vBjn&{(N^i5SVEi6-E5dHHM#^060zquEJbA(#i3Vh_Fz9;q+_a5;-lS^bSxD328QvNpd4%TfXWb zrkC!tUaef)Bw!-~%|)vm5kz@Gi&S=9EZEVCY)T_Lc9bfQ1B5NwV#EOF2a-mCH8%N8 zCC<+-%@oCi50$HuK2)GVDtAf#`3D`?aD1p;bxCpzKE}X-s)fxO_*`8B?kH$*t${Gi zLGb4wbaOzP>c92PL%wwN!IFa7p`ZE5GHD&|P%bF76mfSh-?+QqaIGmlI8oNK%l8nD zs{g(#o04z`$hvOwwC;bQ`bR$rGeD&Ekc{-V;cZ`%D8wC1{{^GTJP0l{Gx?ohN(01W zt5mWmT9y_o!U!93nhIeYLVO6wp9Ux;ILGk!3Ako7RObgocqx<>pqEU3gfrR;2PBDP zzaYVHeo(v4)R823Nz0Pm0r{0^4reML%(5&D$qiyi`_4h7Uz*hKE3F0;BI2R~?%+c& zV^fA^HBC*y35C$SgWtTFsEawC@qDDHA1b}YKU9F^h^AG+`ZgRNYSVkSl1|KM*T8ze zH4ysi_vV1o_O^Xp#a6vY&?;0N@9Jc|!(G%)-7Gz|SdE}M>63_e5o0E0Y{~Zxk z;(xCbN|IUtlTz{oh9aDnsZ&bUezHh5#2+jr$1Q>c)ZXekWSx|RbA2q$VoyjYettq5 z?|Kl;&w2V&wC4Vn1EPQ+*$Gk6JS#`M}@dJ9Rtm$58YJlbjkz<1px#%Wz zN+6O4n=McND;J74~g3G!bb|PeJ*^ch?Kp6B_?>bi_Slk#fX%` z@P4Qj61;xv8mLfh5Ob*mM1N&`sPe7>Zw`WE)AStJ?))iX-k|ly80r1BdWU=biAM5* zXTNqe5n*bsA)BP0?M;sPs!bsC4nn&Bh4LSCz=|N5gph#H7a4GdjBbe4c3>EfL4F!2%yC z0ElMsO8aybT>r8pz#w|hdt4w^qE1^3YrxiROSEZEbPafO;EeYx^;Q1cgd>v2>q%>5 z^2t_~H+8s!*S)ktj8nL;C#v;~1f*P6^j+)vrfUH+mLYQ*&-%wg#Ae*@t(2l)eO zsYXC19mZ|WI9_vyaUi@vDqsm=Qf(8KnK^YrC_vr3iO;tdUMUM^97oKasxv;3}@vtTUovZk4mNeTOADNV4l6cMV#;3Mm~U{F9ZyRyheRlZIbjBb>Ig6X zFB1NNt}Q#Ee<9pnWnB2S#~F1^-;*QAYt!7MIa--4&#=sdu&_Taf=2ikJ^~nsY8Hea z8PfXG5HbfDb2=lTd@IO!H4*-!9P0vDK-!SgGzjDDUNr5S_EPmj<=8%cs8pqpd2+jH zM+_SUFD5Wb84mS2JTyBy+h$LG2i*rhy z<|*}nD75JH_vLWfg8R=<-av(xjrwqvyFh)j$Z>g94k*_EH4bpBe_3lFQs!WP80WxA zn(o-LCvh+=x44fzA)Toj8zCM8oJgi+yswXC1eZAg8R>;)E;#|k86+1f`@cy12c6gp z`h=bDCojd$8I}eyf0^F2K2l2PoZVh&jeJT859u)x|xgR?~ zXEy8cXEIuj(qPem=FBx{R`mcLKtzzW4zqa0fKl{22>n1&A_zH6fzS-$3Qr^rEN^AZ zd$R_8nUDSTi2Bm+KQz3AV-9#C>97Vqt~f;M^^fhsz6L;-R>rZLbF2Y3=78rpkTnN( zs9c|LsuT>c!TxW`uI?*{qpOv0u}L*()MPkHYDWNvBBY@l zCmmz0>lTJ{eBHst>I_02ht0kc!k;WJA9%S!NAa%=LgLVkFS7=Z1Z(30q{d_&o7HFl znjCUT)*$qeRJNK#5F(dx+)Ipt=97A~aT*KO;9!KH&T!~&N+KL28^~BU=hlsTOSyiC zLa`pZM$G}z?|)R4!OZ8u>n=tDG<;c)3o=r&ufH?L8fYVF4K#_6Ip`F|Ik3Z=(eD#c z&lQ5QneUkEQBSwW8PYc~Y0WDyvy1K2<9;s0*!xo7Ps>!i^k6M{-EAY4q<$nzGgOvT zf+qPOE2N!xYZ+m2*4o}_K|)PS+<~H`tXCQiqM9Dzq9wiGhH;D!8B_hE8OLv6`Ta?R zXKglm=-wY7`XcJtvOATR^=OI2(275n6k+P3{Xno;h{QF{DRGeb>ME9*Dr`kaU$sSwK6YejFblJoVl;~A{MTFNiF zDor!>!Vq%20X+8u5x!akL2beD`)-URwK_+{cL)CMQG};#Hv0?+tE6h-HRn2PapOc5HaTLwzCbv7rken!RMt0Yqk@0fS{8j!da2eB5p=dN3Vx_0UG- z@Bc*@i|`}`Ieil03xou_^MKCeaHZQPG1lZRAJoK^=KyM~S7OyTN-;u;ytS&fhL$B&jcO9ODD5d-{aUhMx`Lh#M~^AWN|MSzm@7n)Y(-WmuC@ z1}Z?hnHiu^z;f*EF4ceWboTE4teWs~StZGEiv0${5Rf^eAg2!@^bS^z!YZgPBKKnj z==`!AJK7~J${Bsw=AcgiXE&znM}&*zxDfG;gzN7u>H57nu*Y}PUdLLdPl{=yf+Qu` zFhwW}C7kVHaG2|}B)M_Ejmryjuyd;KQyb?a*ywF0zb|X!&Wu!&TGJq@3nh}&>F zTEb5D@WP2dTvn37Koj5je4k1$R*paLk$*JxL_@)^vKr)gIXQ{u+X4jlJd2bS=P{Dm zDFUFEM`_%cqPU5vy;`gGmTt?N@JR18tQ!-y8nAI)+e^QGNL%xWdH*n0TmoqiBDxGw z#G8Tl*rQ~uqQLW)$S&I`!0zsnPFMz@;pe?weEin$QL3ZWB~eyAvFty0z%x>Ty;uM6#g4DyKw}FuQLw6ET#b z32DW!_MNzt7G#t?FuELVq+F}-B5Ywf53rMEarcrr|15KGXi6p5o`Z1B0gFYl%u*XG zSU%KbuK{lkETtEH${Tz6wdn#oK3rrGHhLEII zU?i!1B@k4wI9(hna!LZam|t_Dlbr1%m-kae0BVFv4m6hs7BOeSR2V)Dp7xmAxEt~me;&rn=1Z5|+#8f!RJ9<3%-uid8|9~(g=Rkwnj6{#e(q8p7HY2s)9W zCcls_t6RIDGQMj&CQHy&k{cFoF*GY3`J$rKV^^*|kST&=hv6MMx!9!Px0Wk8tkJYs zwEs_@^XS-=t>gmMOt>0Vpftse=Ye$&z#%Cd%}US(@5c4BmfZ-ffxi%TA*?`{0NJUp zN7kRfD1Ubt{VqNU&p}|7i8c)g8K!PxhC|yPbyWBvr7)$~p{gZ}kNEhDywtR4KY5zmWhq3rv-Dz{IyOn+)tUCNab z33AyggH)YUQTH#A=BY0on?)Sg^HVk@C{;z*=&Zgi!&Z~Lim_p)ltgzS(;V~(&m4eS z1IuwsR2t&S^|O}UP}-2*MfeHfbA(1%{w#&fJAE``;NdAW&a=3A@Vc}$!1x@lU^I*`}j zKLi97nwjn}yv$Ar5Y&7!Uw|~56r>PkIjF#94uo>;u!hR%8D+VA>FogNSy|UUpeWS7{9=$=LveeB>L%d} zk(UwwnmIp^sB@h}kk>{;)HiFQA^hJ;;6YeT%2EB}b~F{epmH*PanI!>s8LuT-Bp4L zLhja7`@Izfl})4TFhy#hP7@-sAPu!;Y%NvjS{4Db3Rj1#VG97!PZ<(336cmqS)0n} zJai{l$&sTISLOSMVJ)v9^iHSAY~W+jhm~2A44XVlJur|6x1L3&IhZPE4xrYY<@#mW z&`Mqdw-DxGC5en2@AMSdwwmcVun8#qn^d0z5OA(qowq*1 zL|dU804!0vAb`jA;sMQ+&132)N72jjABLJz5$utm3(DKD1rA3tYJWj3LFSymcpyau zmXZ|ba0;O<0=TZd#j=*~t|-_C{jNL0A2RO&MBKwAq7La&mSaOY){W?E+ za!INL@!5n+W2E}k!dIAY@l~{RxV=J%`e(dqqyX9qN?bgmjr+UHBy}R=3oRd`#8lr< zH7@ff=ND5(bnK|SQIbk|VIx9$u+b&f za&x*h>^ol1=4rJEmYuGeBhZKoOvJ7w9h$4ieRBs|^HBTz%(50+)Jj-1=;8xR= zkEA}Ojs0n)lGG2SNLa z0e1(Gv>+B(4Hrr3ZibiHe+a2532F+2Nulhn|9(vEBt=kVy9|M{0L!uG#zaeYm}<`z zRfY15DLq@$kW*+HnTe3wVJ%OmC8!{a1M)a-tV59V30F-rNcgab=ihI05KaRH5+Liv zD24Wd#CQ61hIe`bLizwfhSToPf&Aw{mEo$_fslx*FAzmtqZ6`2OP*F1eVQK!rP7=F zPavHxD{u#WK8CbqomG$Yo}AHU3OBz2b#c`wX4ej$$qoTheLgf88a5ng-!6!wYQ}B|%Mturibcby}}hNsgc< zqD``pfF`IFP_n>8kel~#CCv$f+$_;k3NwML2>qdUG{|~B7>t~10D*D0)*^`GhpQ%; zW$xXa0}zJ~#~iSj2f*LDb^V8fmfeUu`0+glemE55UA{OeJ_mqxlixH-{#e~BO=_1bKx6GkXh_r1jMip4 zg!o|O)D7``|B{QsVW~+5J4u%3VB=_a<^UvrnC4(&)g)~mKwoy_u7e+b4$|Z3DD69a zMv(XU=%n@>s5Q|wyBdM6qtyjvA$_%%$ChPPx_7DWnB|SzliN1i%B78po2TRU<(DMq znMFQH#YzVV!y}cX<~K;{ILQ+bEA0zFCy;7q-x{&PY4k{lav1nEYGwfrFsIh8FOZ$<6e{liw1+`efqrelBB%^e-#TD8J6 z2jMag2wryMzJs6ha}X5KLEh;dliG8jmYa;vV?;G?ERYgK?a-l)a&WPau*U6t(Of4A zib7b1GX8iyv4GfxK}5}KOxQEh&LmZD79SF+BsGsrEmF6_)TTO-^8*CyQVZY(`jY?o z;`&#N!3aJ{sx6Nw2{-vu4Y0U~af?YNsq-0MXxRh2%pO8mA4-Bcs#8bhFUg(dp~U1K zZKTb`JEcc3>9&H3f!p`TsU~fD?^bqy56({G5nkn7%iU-RDxh(_7+0=&Oy>?vYoMFN znq-gyV>z~4b1*tgbHHjI(8!(Zzq@o$({CxssfH2~-|5{L-|24$A*nS&@f^sk&Hu1! zFXi7&*md%7d4UGz6?((;@#Rk{wVJeY)LTusK*&^H8Y9?~^8pU5W#(=i_v`JsI7~iA z+nPxme^8{7)OTgwl%OQ_)^eeuXKkT@bCgh>2;nwi{W^>LwI_MbHxX9oP7fjv%8faO zQNZS8vTK)4F}%!ff$+b>i|k>9Z$m*$K?f2xHAoMIn`FkVD4?8}R$f^-xQWuIRX3G1 zv;4lfMC;Zzje9d4`*WAwxB$6IjP$ce{bpo+>))M4=!}p_a4j#Vz4p7y3pzz{h4J-> z`eCa{{*ggEKa%F4aJgY2zkU~l;}dXG-wS9hP7alWpZ#+{eQeNDO&NrwHsG0$Z$^gi zIhZs0eO%9(Qc1i%TcF&ohQ6nRA;9X--^Mkr8T^z2benW&rV`26-dtovWRF≺;RD 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 Vw~RBgAp0jnML|>kr<{4v{|E4fh8O?< literal 0 HcmV?d00001 diff --git a/Lib/profiling/sampling/_child_monitor.py b/Lib/profiling/sampling/_child_monitor.py new file mode 100644 index 00000000000..ec56f75719f --- /dev/null +++ b/Lib/profiling/sampling/_child_monitor.py @@ -0,0 +1,279 @@ +""" +Child process monitoring for the sampling profiler. + +This module monitors a target process for child process creation and spawns +separate profiler instances for each discovered child. +""" + +import subprocess +import sys +import threading +import time + +import _remote_debugging + +# Polling interval for child process discovery +_CHILD_POLL_INTERVAL_SEC = 0.1 + +# Default timeout for waiting on child profilers +_DEFAULT_WAIT_TIMEOUT_SEC = 30.0 + +# Maximum number of child profilers to spawn (prevents resource exhaustion) +_MAX_CHILD_PROFILERS = 100 + +# Interval for cleaning up completed profilers (in polling cycles) +_CLEANUP_INTERVAL_CYCLES = 10 + + +def get_child_pids(pid, recursive=True): + """ + Get all child process IDs of the given process. + + Args: + pid: Process ID of the parent process + recursive: If True, return all descendants (children, grandchildren, etc.) + + Returns: + List of child PIDs + """ + return _remote_debugging.get_child_pids(pid, recursive=recursive) + + +def is_python_process(pid): + """ + Check if a process is a Python process. + + Args: + pid: Process ID to check + + Returns: + bool: True if the process appears to be a Python process, False otherwise + """ + return _remote_debugging.is_python_process(pid) + + +class ChildProcessMonitor: + """ + Monitors a target process for child processes and spawns profilers for them. + + Use as a context manager: + with ChildProcessMonitor(pid, cli_args, output_pattern) as monitor: + # monitoring runs here + monitor.wait_for_profilers() # optional: wait before cleanup + # cleanup happens automatically + """ + + def __init__(self, pid, cli_args, output_pattern): + """ + Initialize the child process monitor. + + Args: + pid: Parent process ID to monitor + cli_args: CLI arguments to pass to child profilers + output_pattern: Pattern for output files (format string with {pid}) + """ + self.parent_pid = pid + self.cli_args = cli_args + self.output_pattern = output_pattern + + self._known_children = set() + self._spawned_profilers = [] + self._lock = threading.Lock() + self._stop_event = threading.Event() + self._monitor_thread = None + self._poll_count = 0 + + def __enter__(self): + self._monitor_thread = threading.Thread( + target=self._monitor_loop, + daemon=True, + name=f"child-monitor-{self.parent_pid}", + ) + self._monitor_thread.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._stop_event.set() + if self._monitor_thread is not None: + self._monitor_thread.join(timeout=2.0) + if self._monitor_thread.is_alive(): + print( + "Warning: Monitor thread did not stop cleanly", + file=sys.stderr, + ) + + # Wait for child profilers to complete naturally + self.wait_for_profilers() + + # Terminate any remaining profilers + with self._lock: + profilers_to_cleanup = list(self._spawned_profilers) + self._spawned_profilers.clear() + + for proc in profilers_to_cleanup: + self._cleanup_process(proc) + return False + + def _cleanup_process(self, proc, terminate_timeout=2.0, kill_timeout=1.0): + if proc.poll() is not None: + return # Already terminated + + proc.terminate() + try: + proc.wait(timeout=terminate_timeout) + except subprocess.TimeoutExpired: + proc.kill() + try: + proc.wait(timeout=kill_timeout) + except subprocess.TimeoutExpired: + # Last resort: wait indefinitely to avoid zombie + # SIGKILL should always work, but we must reap the process + try: + proc.wait() + except Exception: + pass + + @property + def spawned_profilers(self): + with self._lock: + return list(self._spawned_profilers) + + def wait_for_profilers(self, timeout=_DEFAULT_WAIT_TIMEOUT_SEC): + """ + Wait for all spawned child profilers to complete. + + Call this before exiting the context if you want profilers to finish + their work naturally rather than being terminated. + + Args: + timeout: Maximum time to wait in seconds + """ + profilers = self.spawned_profilers + if not profilers: + return + + print( + f"Waiting for {len(profilers)} child profiler(s) to complete...", + file=sys.stderr, + ) + + deadline = time.monotonic() + timeout + for proc in profilers: + remaining = deadline - time.monotonic() + if remaining <= 0: + break + try: + proc.wait(timeout=max(0.1, remaining)) + except subprocess.TimeoutExpired: + pass + + def _monitor_loop(self): + # Note: There is an inherent TOCTOU race between discovering a child + # process and checking if it's Python. This is expected for process monitoring. + while not self._stop_event.is_set(): + try: + self._poll_count += 1 + + # Periodically clean up completed profilers to avoid memory buildup + if self._poll_count % _CLEANUP_INTERVAL_CYCLES == 0: + self._cleanup_completed_profilers() + + children = set(get_child_pids(self.parent_pid, recursive=True)) + + with self._lock: + new_children = children - self._known_children + self._known_children.update(new_children) + + for child_pid in new_children: + # Only spawn profiler if this is actually a Python process + if is_python_process(child_pid): + self._spawn_profiler_for_child(child_pid) + + except ProcessLookupError: + # Parent process exited, stop monitoring + break + except Exception as e: + # Log error but continue monitoring + print( + f"Warning: Error in child monitor loop: {e}", + file=sys.stderr, + ) + + self._stop_event.wait(timeout=_CHILD_POLL_INTERVAL_SEC) + + def _cleanup_completed_profilers(self): + with self._lock: + # Keep only profilers that are still running + self._spawned_profilers = [ + p for p in self._spawned_profilers if p.poll() is None + ] + + def _spawn_profiler_for_child(self, child_pid): + if self._stop_event.is_set(): + return + + # Check if we've reached the maximum number of child profilers + with self._lock: + if len(self._spawned_profilers) >= _MAX_CHILD_PROFILERS: + print( + f"Warning: Max child profilers ({_MAX_CHILD_PROFILERS}) reached, " + f"skipping PID {child_pid}", + file=sys.stderr, + ) + return + + cmd = [ + sys.executable, + "-m", + "profiling.sampling", + "attach", + str(child_pid), + ] + cmd.extend(self._build_child_cli_args(child_pid)) + + proc = None + try: + proc = subprocess.Popen( + cmd, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + with self._lock: + if self._stop_event.is_set(): + self._cleanup_process( + proc, terminate_timeout=1.0, kill_timeout=1.0 + ) + return + self._spawned_profilers.append(proc) + + print( + f"Started profiler for child process {child_pid}", + file=sys.stderr, + ) + except Exception as e: + if proc is not None: + self._cleanup_process( + proc, terminate_timeout=1.0, kill_timeout=1.0 + ) + print( + f"Warning: Failed to start profiler for child {child_pid}: {e}", + file=sys.stderr, + ) + + def _build_child_cli_args(self, child_pid): + args = list(self.cli_args) + + if self.output_pattern: + # Use replace() instead of format() to handle user filenames with braces + output_file = self.output_pattern.replace("{pid}", str(child_pid)) + found_output = False + for i, arg in enumerate(args): + if arg in ("-o", "--output") and i + 1 < len(args): + args[i + 1] = output_file + found_output = True + break + if not found_output: + args.extend(["-o", output_file]) + + return args diff --git a/Lib/profiling/sampling/_css_utils.py b/Lib/profiling/sampling/_css_utils.py new file mode 100644 index 00000000000..40912e9b352 --- /dev/null +++ b/Lib/profiling/sampling/_css_utils.py @@ -0,0 +1,22 @@ +import importlib.resources + + +def get_combined_css(component: str) -> str: + template_dir = importlib.resources.files(__package__) + + base_css = (template_dir / "_shared_assets" / "base.css").read_text(encoding="utf-8") + + if component == "flamegraph": + component_css = ( + template_dir / "_flamegraph_assets" / "flamegraph.css" + ).read_text(encoding="utf-8") + elif component == "heatmap": + component_css = (template_dir / "_heatmap_assets" / "heatmap.css").read_text( + encoding="utf-8" + ) + else: + raise ValueError( + f"Unknown component: {component}. Expected 'flamegraph' or 'heatmap'." + ) + + return f"{base_css}\n\n{component_css}" diff --git a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.css b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.css new file mode 100644 index 00000000000..c93ee1e9dd4 --- /dev/null +++ b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.css @@ -0,0 +1,1085 @@ +/* ========================================================================== + Flamegraph Viewer - Component-Specific CSS + + DEPENDENCY: Requires _shared_assets/base.css to be loaded first + This file extends the shared foundation with flamegraph-specific styles. + ========================================================================== */ + +/* -------------------------------------------------------------------------- + Differential Flamegraph + -------------------------------------------------------------------------- */ + +:root { + /* Regression colors */ + --diff-regression-deep: #d32f2f; + --diff-regression-medium: #e57373; + --diff-regression-light: #ef9a9a; + --diff-regression-verylight: #ffcdd2; + + /* Improvement colors */ + --diff-improvement-deep: #1976d2; + --diff-improvement-medium: #42a5f5; + --diff-improvement-light: #64b5f6; + --diff-improvement-verylight: #90caf9; + + /* Other differential colors */ + --diff-neutral: #bdbdbd; + --diff-new: #9575cd; + --diff-elided: #b39ddb; +} + +/* Dark mode differential colors - adjusted for contrast against dark backgrounds */ +[data-theme="dark"] { + --diff-regression-deep: #ef5350; + --diff-regression-medium: #e57373; + --diff-regression-light: #ef9a9a; + --diff-regression-verylight: #ffcdd2; + + --diff-improvement-deep: #42a5f5; + --diff-improvement-medium: #64b5f6; + --diff-improvement-light: #90caf9; + --diff-improvement-verylight: #bbdefb; + + --diff-neutral: #757575; + --diff-new: #b39ddb; + --diff-elided: #ce93d8; +} + +/* -------------------------------------------------------------------------- + Layout Overrides (Flamegraph-specific) + -------------------------------------------------------------------------- */ + +html, body { + height: 100%; + overflow: hidden; +} + +.app-layout { + height: 100vh; +} + +.main-content { + display: flex; + flex: 1; + min-height: 0; +} + +/* -------------------------------------------------------------------------- + Search Input (Flamegraph-specific) + -------------------------------------------------------------------------- */ + +.search-wrapper { + flex: 1; + max-width: 360px; + position: relative; +} + +.search-input { + width: 100%; + padding: 8px 36px 8px 14px; + font-family: var(--font-sans); + font-size: 13px; + color: #2e3338; + background: rgba(255, 255, 255, 0.95); + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 20px; + outline: none; + transition: all var(--transition-fast); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.search-input::placeholder { + color: #6c757d; +} + +.search-input:focus { + border-color: rgba(255, 255, 255, 0.8); + background: white; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); +} + +/* Dark theme search input */ +[data-theme="dark"] .search-input { + color: #e6edf3; + background: rgba(33, 38, 45, 0.95); + border: 2px solid rgba(88, 166, 255, 0.3); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +[data-theme="dark"] .search-input::placeholder { + color: #8b949e; +} + +[data-theme="dark"] .search-input:focus { + border-color: rgba(88, 166, 255, 0.6); + background: rgba(33, 38, 45, 1); + box-shadow: 0 4px 16px rgba(88, 166, 255, 0.2); +} + +.search-input.has-matches { + border-color: rgba(40, 167, 69, 0.8); + box-shadow: 0 4px 16px rgba(40, 167, 69, 0.2); +} + +.search-input.no-matches { + border-color: rgba(220, 53, 69, 0.8); + box-shadow: 0 4px 16px rgba(220, 53, 69, 0.2); +} + +.search-clear { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + width: 20px; + height: 20px; + padding: 0; + display: none; + align-items: center; + justify-content: center; + font-size: 14px; + line-height: 1; + color: #6c757d; + background: transparent; + border: none; + border-radius: 50%; + cursor: pointer; + transition: color var(--transition-fast); +} + +.search-clear:hover { + color: #2e3338; +} + +[data-theme="dark"] .search-clear { + color: #8b949e; +} + +[data-theme="dark"] .search-clear:hover { + color: #e6edf3; +} + +.search-wrapper.has-value .search-clear { + display: flex; +} + +/* -------------------------------------------------------------------------- + Sidebar + -------------------------------------------------------------------------- */ + +.sidebar { + width: var(--sidebar-width); + background: var(--bg-secondary); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + flex-shrink: 0; + overflow: hidden; + position: relative; +} + +.sidebar.collapsed { + width: var(--sidebar-collapsed) !important; + transition: width var(--transition-normal); +} + +.sidebar-toggle { + position: absolute; + top: 12px; + right: 10px; + width: 26px; + height: 26px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 6px; + cursor: pointer; + transition: all var(--transition-fast); + z-index: 10; +} + +.sidebar-toggle svg { + transition: transform var(--transition-fast); +} + +.sidebar-toggle:hover { + color: var(--accent); + border-color: var(--accent); + background: var(--accent-glow); +} + +.sidebar.collapsed .sidebar-toggle { + right: 9px; +} + +.sidebar.collapsed .sidebar-toggle svg { + transform: rotate(180deg); +} + +.sidebar-content { + flex: 1; + overflow-y: auto; + padding: 44px 14px 14px; +} + +.sidebar.collapsed .sidebar-content { + display: none; +} + +.sidebar-resize-handle { + position: absolute; + top: 0; + right: 0; + width: 6px; + height: 100%; + cursor: col-resize; + background: transparent; + transition: background var(--transition-fast); + z-index: 11; +} + +.sidebar-resize-handle:hover, +.sidebar-resize-handle.resizing { + background: var(--python-gold); +} + +.sidebar-resize-handle::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 40px; + background: var(--border); + border-radius: 1px; + opacity: 0; + transition: opacity var(--transition-fast); +} + +.sidebar-resize-handle:hover::before { + opacity: 1; +} + +.sidebar.collapsed .sidebar-resize-handle { + display: none; +} + +body.resizing-sidebar { + cursor: col-resize; + user-select: none; +} + +/* Sidebar Logo */ +.sidebar-logo { + display: flex; + justify-content: center; + margin-bottom: 16px; +} + +.sidebar-logo-img { + width: 220px; + height: 180px; + display: flex; + align-items: center; + justify-content: center; +} + +.sidebar-logo-img svg, +.sidebar-logo-img img { + width: 100%; + height: 100%; + object-fit: contain; +} + +/* Sidebar sections */ +.sidebar-section { + margin-bottom: 20px; +} + +.sidebar-section:last-child { + margin-bottom: 0; +} + +.section-title { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--accent); + margin: 0; + flex: 1; +} + +/* View Mode Section */ +.view-mode-section { + display: flex; + flex-direction: column; + gap: 8px; +} + +.view-mode-section .section-content { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; +} + +/* Collapsible sections */ +.collapsible .section-header { + display: flex; + align-items: center; + width: 100%; + padding: 0 0 8px 0; + margin-bottom: 10px; + background: none; + border: none; + border-bottom: 2px solid var(--python-gold); + cursor: pointer; + transition: all var(--transition-fast); +} + +.collapsible .section-header:hover { + opacity: 0.8; +} + +.section-chevron { + color: var(--text-muted); + transition: transform var(--transition-fast); +} + +.collapsible.collapsed .section-chevron { + transform: rotate(-90deg); +} + +.section-content { + transition: max-height var(--transition-slow) ease-out, opacity var(--transition-normal) ease-out, padding var(--transition-normal) ease-out; + max-height: 1000px; + opacity: 1; +} + +.collapsible.collapsed .section-content { + max-height: 0; + opacity: 0; + padding-top: 0; + pointer-events: none; + transition: max-height var(--transition-slow) ease-in, opacity var(--transition-normal) ease-in, padding var(--transition-normal) ease-in; +} + +/* -------------------------------------------------------------------------- + Profile Summary Cards + -------------------------------------------------------------------------- */ + +.summary-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px; +} + +.summary-card { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + background: var(--bg-primary); + border: 2px solid var(--border); + border-radius: 8px; + transition: all var(--transition-fast); + animation: slideUp 0.4s ease-out backwards; + animation-delay: calc(var(--i, 0) * 0.08s); + overflow: hidden; + position: relative; +} + +.summary-card:nth-child(1) { --i: 0; --card-color: var(--card-blue); } +.summary-card:nth-child(2) { --i: 1; --card-color: var(--card-green); } +.summary-card:nth-child(3) { --i: 2; --card-color: var(--card-yellow); } +.summary-card:nth-child(4) { --i: 3; --card-color: var(--card-purple); } + +.summary-card:hover { + border-color: rgba(var(--card-color), 0.6); + background: linear-gradient(135deg, rgba(var(--card-color), 0.08) 0%, var(--bg-primary) 100%); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(var(--card-color), 0.15); +} + +.summary-icon { + font-size: 14px; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(var(--card-color), 0.15) 0%, rgba(var(--card-color), 0.05) 100%); + border: 1px solid rgba(var(--card-color), 0.2); + border-radius: 6px; + flex-shrink: 0; + transition: all var(--transition-fast); +} + +.summary-card:hover .summary-icon { + transform: scale(1.05); + background: linear-gradient(135deg, rgba(var(--card-color), 0.25) 0%, rgba(var(--card-color), 0.1) 100%); +} + +.summary-data { + min-width: 0; + flex: 1; + overflow: hidden; +} + +.summary-value { + font-family: var(--font-mono); + font-size: 13px; + font-weight: 800; + color: rgb(var(--card-color)); + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.summary-label { + font-size: 8px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* -------------------------------------------------------------------------- + Progress Bars + -------------------------------------------------------------------------- */ + +.bar-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 5px; +} + +.bar-label { + font-size: 9px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.2px; +} + +.bar-value { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 700; + color: var(--accent); +} + +.bar { + height: 6px; + background: var(--bg-tertiary); + border-radius: 3px; + overflow: hidden; +} + +.bar-fill { + height: 100%; + background: linear-gradient(90deg, #28a745 0%, #20c997 50%, #17a2b8 100%); + border-radius: 3px; + transition: width 0.6s ease-out; + position: relative; + overflow: hidden; +} + +.bar-fill::after { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.4) 50%, + transparent 100% + ); + animation: shimmer 2s ease-in-out infinite; +} + +/* -------------------------------------------------------------------------- + Efficiency Section Container + -------------------------------------------------------------------------- */ + +.efficiency-section { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid var(--border); +} + +/* -------------------------------------------------------------------------- + Thread Stats Progress Bars (in Sidebar) + -------------------------------------------------------------------------- */ + +.thread-stats-section { + display: block; +} + +.stats-container { + display: flex; + flex-direction: column; + gap: 10px; +} + +.stat-item { + animation: fadeIn 0.4s ease-out backwards; + animation-delay: calc(var(--i, 0) * 0.05s); +} + +.stat-item:nth-child(1) { --i: 0; } +.stat-item:nth-child(2) { --i: 1; } +.stat-item:nth-child(3) { --i: 2; } +.stat-item:nth-child(4) { --i: 3; } +.stat-item:nth-child(5) { --i: 4; } + +/* Color variants for bar-fill */ +.bar-fill--green { + background: linear-gradient(90deg, #28a745 0%, #20c997 100%); +} + +.bar-fill--yellow { + background: linear-gradient(90deg, #ffc107 0%, #ffdb4d 100%); +} + +.bar-fill--purple { + background: linear-gradient(90deg, #6f42c1 0%, #9b6dd6 100%); +} + +.bar-fill--red { + background: linear-gradient(90deg, #dc3545 0%, #ff6b7a 100%); +} + +/* -------------------------------------------------------------------------- + Hotspot Cards + -------------------------------------------------------------------------- */ + +.hotspot { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px; + margin-bottom: 8px; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + cursor: pointer; + transition: all var(--transition-fast); + opacity: 0; + transform: translateY(8px); + box-shadow: var(--shadow-sm); +} + +.hotspot.visible { + opacity: 1; + transform: translateY(0); +} + +.hotspot:hover { + border-color: var(--accent); + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.hotspot.active { + border-color: var(--python-gold); + background: var(--accent-glow); + box-shadow: 0 0 0 3px var(--accent-glow); +} + +.hotspot:last-child { + margin-bottom: 0; +} + +.hotspot-rank { + width: 26px; + height: 26px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 12px; + flex-shrink: 0; + background: linear-gradient(135deg, var(--python-blue) 0%, var(--python-blue-light) 100%); + color: white; + box-shadow: 0 2px 4px rgba(55, 118, 171, 0.3); +} + +.hotspot-rank--1 { background: linear-gradient(135deg, #d4af37, #f4d03f); color: #5a4a00; } +.hotspot-rank--2 { background: linear-gradient(135deg, #a8a8a8, #c0c0c0); color: #4a4a4a; } +.hotspot-rank--3 { background: linear-gradient(135deg, #cd7f32, #e6a55a); color: #5a3d00; } + +.hotspot-info { + flex: 1; + min-width: 0; +} + +.hotspot-func { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 600; + color: var(--text-primary); + line-height: 1.3; + word-break: break-word; + margin-bottom: 2px; +} + +.hotspot-file { + font-family: var(--font-mono); + font-size: 10px; + color: var(--text-muted); + margin-bottom: 3px; + word-break: break-all; +} + +.hotspot-stats { + font-family: var(--font-mono); + font-size: 10px; + color: var(--text-secondary); +} + +.hotspot-percent { + color: var(--accent); + font-weight: 600; +} + +/* -------------------------------------------------------------------------- + Legend + -------------------------------------------------------------------------- */ + + +.legend { + display: flex; + flex-direction: column; + gap: 4px; +} + +.legend-item { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 8px; + background: var(--bg-primary); + border-radius: 4px; + border: 1px solid var(--border-subtle); + font-size: 11px; +} + +.legend-color { + width: 20px; + height: 10px; + border-radius: 2px; + flex-shrink: 0; + border: 1px solid rgba(0, 0, 0, 0.08); +} + +.legend-label { + color: var(--text-primary); + font-weight: 500; + flex: 1; +} + +.legend-range { + font-family: var(--font-mono); + font-size: 9px; + color: var(--text-muted); +} + +/* -------------------------------------------------------------------------- + Thread Filter + -------------------------------------------------------------------------- */ + +.filter-section { + padding-top: 12px; + border-top: 1px solid var(--border); +} + +.filter-label { + display: block; + font-size: 10px; + font-weight: 600; + color: var(--text-muted); + margin-bottom: 6px; +} + +.filter-select { + width: 100%; + padding: 7px 28px 7px 10px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-primary); + background: var(--bg-primary); + border: 2px solid var(--accent); + border-radius: 6px; + cursor: pointer; + outline: none; + appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%233776ab' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 6px center; + background-size: 14px; + transition: all var(--transition-fast); +} + +.filter-select:hover { + border-color: var(--accent-hover); + box-shadow: 0 2px 6px var(--accent-glow); +} + +.filter-select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); +} + +/* -------------------------------------------------------------------------- + Chart Area + -------------------------------------------------------------------------- */ + +.chart-area { + flex: 1; + min-width: 0; + overflow: hidden; + background: var(--bg-primary); + position: relative; +} + +#chart { + width: 100%; + height: 100%; + padding: 16px; + overflow: auto; +} + +/* D3 Flamegraph overrides */ +.d3-flame-graph rect { + stroke: rgba(55, 118, 171, 0.3); + stroke-width: 1px; + cursor: pointer; + transition: filter 0.1s ease; +} + +.d3-flame-graph rect:hover { + stroke: var(--python-blue); + stroke-width: 2px; + filter: brightness(1.08); +} + +.d3-flame-graph text { + font-family: var(--font-sans); + font-size: 12px; + font-weight: 500; + fill: var(--text-primary); + pointer-events: none; +} + +/* Search highlight */ +.d3-flame-graph rect.search-match { + stroke: #ff6b35 !important; + stroke-width: 2px !important; + stroke-dasharray: 4 2; +} + +.d3-flame-graph rect.search-dim { + opacity: 0.25; +} + +/* -------------------------------------------------------------------------- + Tooltip + -------------------------------------------------------------------------- */ + +.python-tooltip { + position: absolute; + z-index: 1000; + pointer-events: none; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + padding: 14px; + max-width: 480px; + box-shadow: var(--shadow-lg); + font-family: var(--font-sans); + font-size: 13px; + color: var(--text-primary); + word-wrap: break-word; + overflow-wrap: break-word; + line-height: 1.5; +} + +.tooltip-header { + margin-bottom: 10px; +} + +.tooltip-title { + font-size: 14px; + font-weight: 600; + color: var(--accent); + line-height: 1.3; + word-break: break-word; + margin-bottom: 4px; +} + +.tooltip-location { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-secondary); + background: var(--bg-tertiary); + padding: 4px 8px; + border-radius: 4px; + word-break: break-all; +} + +.tooltip-stats { + display: grid; + grid-template-columns: auto 1fr; + gap: 4px 14px; + font-size: 12px; +} + +.tooltip-stat-label { + color: var(--text-secondary); + font-weight: 500; +} + +.tooltip-stat-value { + color: var(--text-primary); + font-weight: 600; +} + +.tooltip-stat-value.accent { + color: var(--accent); +} + +.tooltip-diff { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--border); +} + +.tooltip-diff-title { + font-size: 11px; + font-weight: 600; + color: var(--accent); + margin-bottom: 8px; +} + +.tooltip-diff-row { + display: grid; + grid-template-columns: auto 1fr; + gap: 4px 14px; + font-size: 12px; + margin-bottom: 4px; +} + +.tooltip-diff-row.regression .tooltip-stat-value { + color: var(--diff-regression-deep); + font-weight: 700; +} + +.tooltip-diff-row.improvement .tooltip-stat-value { + color: var(--diff-improvement-deep); + font-weight: 700; +} + +.tooltip-diff-row.neutral .tooltip-stat-value { + color: var(--text-secondary); +} + +.tooltip-source { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid var(--border); +} + +.tooltip-source-title { + font-size: 11px; + font-weight: 600; + color: var(--accent); + margin-bottom: 6px; +} + +.tooltip-source-code { + font-family: var(--font-mono); + font-size: 10px; + line-height: 1.5; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 6px; + padding: 8px; + max-height: 140px; + overflow-y: auto; + white-space: pre-wrap; + word-break: break-all; +} + +.tooltip-source-line { + color: var(--text-secondary); + padding: 1px 0; +} + +.tooltip-source-line.current { + color: var(--accent); + font-weight: 600; +} + +.tooltip-hint { + margin-top: 10px; + padding-top: 8px; + border-top: 1px solid var(--border); + font-size: 11px; + color: var(--text-muted); + text-align: center; +} + +/* -------------------------------------------------------------------------- + Tooltip Bytecode/Opcode Section + -------------------------------------------------------------------------- */ + +.tooltip-opcodes { + margin-top: 16px; + padding-top: 12px; + border-top: 1px solid var(--border); +} + +.tooltip-opcodes-title { + color: var(--accent); + font-size: 13px; + margin-bottom: 8px; + font-weight: 600; +} + +.tooltip-opcodes-list { + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 6px; + padding: 10px; +} + +.tooltip-opcode-row { + display: grid; + grid-template-columns: 1fr 60px 60px; + gap: 8px; + align-items: center; + padding: 3px 0; +} + +.tooltip-opcode-name { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tooltip-opcode-name.specialized { + color: var(--spec-high-text); +} + +.tooltip-opcode-base-hint { + color: var(--text-muted); + font-size: 11px; + margin-left: 4px; +} + +.tooltip-opcode-badge { + background: var(--spec-high); + color: white; + font-size: 9px; + padding: 1px 4px; + border-radius: 3px; + margin-left: 4px; +} + +.tooltip-opcode-count { + text-align: right; + font-size: 11px; + color: var(--text-secondary); +} + +.tooltip-opcode-bar { + background: var(--bg-secondary); + border-radius: 2px; + height: 8px; + overflow: hidden; +} + +.tooltip-opcode-bar-fill { + background: linear-gradient(90deg, var(--python-blue), var(--python-blue-light)); + height: 100%; +} + +/* -------------------------------------------------------------------------- + Responsive (Flamegraph-specific) + -------------------------------------------------------------------------- */ + +@media (max-width: 900px) { + .sidebar { + position: fixed; + left: 0; + top: var(--topbar-height); + bottom: var(--statusbar-height); + z-index: 100; + box-shadow: var(--shadow-lg); + } + + .sidebar.collapsed { + width: var(--sidebar-collapsed); + } + + .search-wrapper { + max-width: 220px; + } +} + +@media (max-width: 600px) { + .search-wrapper { + max-width: 160px; + } + + .brand-info { + display: none; + } +} + +/* -------------------------------------------------------------------------- + Flamegraph Root Node Styling + -------------------------------------------------------------------------- */ + +/* Style the root node - no border, themed text */ +.d3-flame-graph g:first-of-type rect { + stroke: none; +} + +.d3-flame-graph g:first-of-type .d3-flame-graph-label { + color: var(--text-muted); +} + +/* -------------------------------------------------------------------------- + Flamegraph-Specific Toggle Override + -------------------------------------------------------------------------- */ + +#toggle-invert .toggle-track.on, +#toggle-elided .toggle-track.on, +#toggle-path-display .toggle-track.on { + background: #8e44ad; + border-color: #8e44ad; + box-shadow: 0 0 8px rgba(142, 68, 173, 0.3); +} + +.toggle-switch:focus-visible { + border-radius: 4px; +} diff --git a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js new file mode 100644 index 00000000000..d66da0402e5 --- /dev/null +++ b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js @@ -0,0 +1,1528 @@ +const EMBEDDED_DATA = {{FLAMEGRAPH_DATA}}; + +// Global string table for resolving string indices +let stringTable = []; +let normalData = null; +let invertedData = null; +let currentThreadFilter = 'all'; +let isInverted = false; +let useModuleNames = true; +let zoomedNodeValue = null; + +// Heat colors are now defined in CSS variables (--heat-1 through --heat-8) +// and automatically switch with theme changes - no JS color arrays needed! + +// Opcode mappings - loaded from embedded data (generated by Python) +let OPCODE_NAMES = {}; +let DEOPT_MAP = {}; + +// Initialize opcode mappings from embedded data +function initOpcodeMapping(data) { + if (data && data.opcode_mapping) { + OPCODE_NAMES = data.opcode_mapping.names || {}; + DEOPT_MAP = data.opcode_mapping.deopt || {}; + } +} + +// Get opcode info from opcode number +function getOpcodeInfo(opcode) { + const opname = OPCODE_NAMES[opcode] || `<${opcode}>`; + const baseOpcode = DEOPT_MAP[opcode]; + const isSpecialized = baseOpcode !== undefined; + const baseOpname = isSpecialized ? (OPCODE_NAMES[baseOpcode] || `<${baseOpcode}>`) : opname; + + return { + opname: opname, + baseOpname: baseOpname, + isSpecialized: isSpecialized + }; +} + +// ============================================================================ +// String Resolution +// ============================================================================ + +function resolveString(index, table = stringTable) { + if (index === null || index === undefined) { + return null; + } + if (typeof index === 'number' && index >= 0 && index < table.length) { + return table[index]; + } + return String(index); +} + +function resolveStringIndices(node, table) { + if (!node) return node; + + const resolved = { ...node }; + + if (typeof resolved.name === 'number') { + resolved.name = resolveString(resolved.name, table); + } + if (typeof resolved.filename === 'number') { + resolved.filename = resolveString(resolved.filename, table); + } + if (typeof resolved.funcname === 'number') { + resolved.funcname = resolveString(resolved.funcname, table); + } + if (typeof resolved.module === 'number') { + resolved.module = resolveString(resolved.module, table); + } + if (typeof resolved.label === 'number') { + resolved.label = resolveString(resolved.label, table); + } + + if (Array.isArray(resolved.source)) { + resolved.source = resolved.source.map(index => + typeof index === 'number' ? resolveString(index, table) : index + ); + } + + if (Array.isArray(resolved.children)) { + resolved.children = resolved.children.map(child => resolveStringIndices(child, table)); + } + + return resolved; +} + +// Escape HTML special characters +function escapeHtml(str) { + return str.replace(/&/g, "&").replace(//g, ">"); +} + +// Get display path based on user preference (module or full path) +function getDisplayName(moduleName, filename) { + if (useModuleNames) { + return moduleName || filename; + } + return filename; +} + +function samplesToMilliseconds(samples, data) { + return (samples * data.stats.sample_interval_usec / 1000).toFixed(2); +} + +function selectFlamegraphData(selectedThreadId = null) { + let baseData = isShowingElided ? elidedFlamegraphData : normalData; + + if (selectedThreadId !== null) { + baseData = filterDataByThread(baseData, selectedThreadId); + } + + if (!isInverted) { + return baseData; + } + + // Thread-filtered trees have different values, so invert them after filtering + // instead of using the cached all-thread tree. + if (selectedThreadId !== null) { + return generateInvertedFlamegraph(baseData); + } + + if (isShowingElided) { + if (!invertedElidedData) { + invertedElidedData = generateInvertedFlamegraph(baseData); + } + return invertedElidedData; + } + + if (!invertedData) { + invertedData = generateInvertedFlamegraph(baseData); + } + return invertedData; +} + +function updateFlamegraphView() { + const selectedThreadId = currentThreadFilter !== 'all' ? parseInt(currentThreadFilter, 10) : null; + const selectedData = selectFlamegraphData(selectedThreadId); + const tooltip = createPythonTooltip(selectedData); + const chart = createFlamegraph(tooltip, selectedData.value, selectedData); + renderFlamegraph(chart, selectedData); + populateThreadStats(selectedData, selectedThreadId); +} + +// ============================================================================ +// Theme & UI Controls +// ============================================================================ + +function toggleTheme() { + toggleAndSaveTheme(); + + // Re-render flamegraph with new theme colors + if (window.flamegraphData && normalData) { + updateFlamegraphView(); + } +} + +function toggleSidebar() { + const sidebar = document.getElementById('sidebar'); + if (sidebar) { + const isCollapsing = !sidebar.classList.contains('collapsed'); + + if (isCollapsing) { + // Save current width before collapsing + const currentWidth = sidebar.offsetWidth; + sidebar.dataset.expandedWidth = currentWidth; + localStorage.setItem('flamegraph-sidebar-width', currentWidth); + } else { + // Restore width when expanding + const savedWidth = sidebar.dataset.expandedWidth || localStorage.getItem('flamegraph-sidebar-width'); + if (savedWidth) { + sidebar.style.width = savedWidth + 'px'; + } + } + + sidebar.classList.toggle('collapsed'); + localStorage.setItem('flamegraph-sidebar', sidebar.classList.contains('collapsed') ? 'collapsed' : 'expanded'); + + // Resize chart after sidebar animation + setTimeout(() => { + resizeChart(); + }, 300); + } +} + +function resizeChart() { + if (window.flamegraphChart && window.flamegraphData) { + const chartArea = document.querySelector('.chart-area'); + if (chartArea) { + window.flamegraphChart.width(chartArea.clientWidth - 32); + d3.select("#chart").datum(window.flamegraphData).call(window.flamegraphChart); + } + } +} + +function toggleSection(sectionId) { + const section = document.getElementById(sectionId); + if (section) { + section.classList.toggle('collapsed'); + // Save state + const collapsedSections = JSON.parse(localStorage.getItem('flamegraph-collapsed-sections') || '{}'); + collapsedSections[sectionId] = section.classList.contains('collapsed'); + localStorage.setItem('flamegraph-collapsed-sections', JSON.stringify(collapsedSections)); + } +} + +// Restore theme from localStorage, or use browser preference +function restoreUIState() { + applyTheme(getPreferredTheme()); + + // Restore sidebar state + const savedSidebar = localStorage.getItem('flamegraph-sidebar'); + if (savedSidebar === 'collapsed') { + const sidebar = document.getElementById('sidebar'); + if (sidebar) sidebar.classList.add('collapsed'); + } + + // Restore sidebar width + const savedWidth = localStorage.getItem('flamegraph-sidebar-width'); + if (savedWidth) { + const sidebar = document.getElementById('sidebar'); + if (sidebar) { + sidebar.style.width = savedWidth + 'px'; + } + } + + // Restore collapsed sections + const collapsedSections = JSON.parse(localStorage.getItem('flamegraph-collapsed-sections') || '{}'); + for (const [sectionId, isCollapsed] of Object.entries(collapsedSections)) { + if (isCollapsed) { + const section = document.getElementById(sectionId); + if (section) section.classList.add('collapsed'); + } + } +} + +// ============================================================================ +// Logo/Favicon Setup +// ============================================================================ + +function setupLogos() { + const logo = document.querySelector('.sidebar-logo-img img'); + if (!logo) return; + + const navbarLogoContainer = document.getElementById('navbar-logo'); + if (navbarLogoContainer) { + const navbarLogo = logo.cloneNode(true); + navbarLogoContainer.appendChild(navbarLogo); + } + + const favicon = document.createElement('link'); + favicon.rel = 'icon'; + favicon.type = 'image/png'; + favicon.href = logo.src; + document.head.appendChild(favicon); +} + +// ============================================================================ +// Status Bar +// ============================================================================ + +function updateStatusBar(nodeData, rootValue, data) { + const funcname = resolveString(nodeData.funcname) || resolveString(nodeData.name) || "--"; + const filename = resolveString(nodeData.filename) || ""; + const moduleName = resolveString(nodeData.module) || ""; + const lineno = nodeData.lineno; + const timeMs = samplesToMilliseconds(nodeData.value, data); + const percent = rootValue > 0 ? ((nodeData.value / rootValue) * 100).toFixed(1) : "0.0"; + + const brandEl = document.getElementById('status-brand'); + const taglineEl = document.getElementById('status-tagline'); + if (brandEl) brandEl.style.display = 'none'; + if (taglineEl) taglineEl.style.display = 'none'; + + const locationEl = document.getElementById('status-location'); + const funcItem = document.getElementById('status-func-item'); + const timeItem = document.getElementById('status-time-item'); + const percentItem = document.getElementById('status-percent-item'); + + if (locationEl) locationEl.style.display = filename && filename !== "~" ? 'flex' : 'none'; + if (funcItem) funcItem.style.display = 'flex'; + if (timeItem) timeItem.style.display = 'flex'; + if (percentItem) percentItem.style.display = 'flex'; + + const fileEl = document.getElementById('status-file'); + if (fileEl && filename && filename !== "~") { + const displayName = getDisplayName(moduleName, filename); + fileEl.textContent = lineno ? `${displayName}:${lineno}` : displayName; + } + + const funcEl = document.getElementById('status-func'); + if (funcEl) funcEl.textContent = funcname.length > 40 ? funcname.substring(0, 37) + '...' : funcname; + + const timeEl = document.getElementById('status-time'); + if (timeEl) timeEl.textContent = `${timeMs} ms`; + + const percentEl = document.getElementById('status-percent'); + if (percentEl) percentEl.textContent = `${percent}%`; +} + +function clearStatusBar() { + const ids = ['status-location', 'status-func-item', 'status-time-item', 'status-percent-item']; + ids.forEach(id => { + const el = document.getElementById(id); + if (el) el.style.display = 'none'; + }); + + const brandEl = document.getElementById('status-brand'); + const taglineEl = document.getElementById('status-tagline'); + if (brandEl) brandEl.style.display = 'flex'; + if (taglineEl) taglineEl.style.display = 'flex'; +} + +// ============================================================================ +// Tooltip +// ============================================================================ + +function createPythonTooltip(data) { + const pythonTooltip = flamegraph.tooltip.defaultFlamegraphTooltip(); + + pythonTooltip.show = function (d, element) { + if (!this._tooltip) { + this._tooltip = d3.select("body") + .append("div") + .attr("class", "python-tooltip") + .style("opacity", 0); + } + + const timeMs = samplesToMilliseconds(d.data.value, data); + const selfSamples = d.data.self || 0; + const selfMs = samplesToMilliseconds(selfSamples, data); + const percentage = ((d.data.value / data.value) * 100).toFixed(2); + const relativePercentage = Math.min(100, ((d.data.value / (zoomedNodeValue ?? data.value)) * 100)).toFixed(2); + const calls = d.data.calls || 0; + const childCount = d.children ? d.children.length : 0; + const source = d.data.source; + + const funcname = resolveString(d.data.funcname) || resolveString(d.data.name); + const filename = resolveString(d.data.filename) || ""; + const moduleName = resolveString(d.data.module) || ""; + const displayName = escapeHtml(useModuleNames ? (moduleName || filename) : filename); + const isSpecialFrame = filename === "~"; + + // Build source section + let sourceSection = ""; + if (source && Array.isArray(source) && source.length > 0) { + const sourceLines = source + .map((line) => { + const isCurrent = line.startsWith("→"); + const escaped = escapeHtml(line); + return `
    ${escaped}
    `; + }) + .join(""); + + sourceSection = ` +
    +
    Source Code:
    +
    ${sourceLines}
    +
    `; + } + + // Create bytecode/opcode section if available + let opcodeSection = ""; + const opcodes = d.data.opcodes; + if (opcodes && typeof opcodes === 'object' && Object.keys(opcodes).length > 0) { + // Sort opcodes by sample count (descending) + const sortedOpcodes = Object.entries(opcodes) + .sort((a, b) => b[1] - a[1]) + .slice(0, 8); // Limit to top 8 + + const totalOpcodeSamples = sortedOpcodes.reduce((sum, [, count]) => sum + count, 0); + const maxCount = sortedOpcodes[0][1] || 1; + + const opcodeLines = sortedOpcodes.map(([opcode, count]) => { + const opcodeInfo = getOpcodeInfo(parseInt(opcode, 10)); + const pct = ((count / totalOpcodeSamples) * 100).toFixed(1); + const barWidth = (count / maxCount) * 100; + const specializedBadge = opcodeInfo.isSpecialized + ? 'SPECIALIZED' + : ''; + const baseOpHint = opcodeInfo.isSpecialized + ? `(${opcodeInfo.baseOpname})` + : ''; + const nameClass = opcodeInfo.isSpecialized + ? 'tooltip-opcode-name specialized' + : 'tooltip-opcode-name'; + + return ` +
    +
    + ${opcodeInfo.opname}${baseOpHint}${specializedBadge} +
    +
    ${count.toLocaleString()} (${pct}%)
    +
    +
    +
    +
    `; + }).join(''); + + opcodeSection = ` +
    +
    Bytecode Instructions:
    +
    + ${opcodeLines} +
    +
    `; + } + + const fileLocationHTML = isSpecialFrame ? "" : ` +
    ${displayName}${d.data.lineno ? ":" + d.data.lineno : ""}
    `; + + // Differential stats section + let diffSection = ""; + if (d.data.diff !== undefined && d.data.baseline !== undefined) { + const baselineSelf = samplesToMilliseconds(d.data.baseline, data); + const currentSelf = samplesToMilliseconds(d.data.self_time || 0, data); + const diffMs = samplesToMilliseconds(d.data.diff, data); + const diffPct = d.data.diff_pct; + const sign = d.data.diff >= 0 ? "+" : ""; + const diffClass = d.data.diff > 0 ? "regression" : (d.data.diff < 0 ? "improvement" : "neutral"); + + diffSection = ` +
    +
    Self-Time Comparison:
    +
    + Baseline Self: + ${baselineSelf} ms +
    +
    + Current Self: + ${currentSelf} ms +
    +
    + Difference: + ${sign}${diffMs} ms (${sign}${diffPct.toFixed(1)}%) +
    +
    `; + } + + const tooltipHTML = ` +
    +
    ${funcname}
    + ${fileLocationHTML} +
    +
    + Total Time: + ${timeMs} ms + + ${selfSamples > 0 ? ` + Self Time: + ${selfMs} ms + ` : ''} + + Percentage: + ${percentage}% + + ${relativePercentage != percentage && relativePercentage != "100.00" ? ` + Relative Percentage: + ${relativePercentage}% + ` : ''} + + ${calls > 0 ? ` + Function Calls: + ${calls.toLocaleString()} + ` : ''} + + ${childCount > 0 ? ` + Child Functions: + ${childCount} + ` : ''} +
    + ${diffSection} + ${sourceSection} + ${opcodeSection} +
    + ${childCount > 0 ? "Click to zoom into this function" : "Leaf function - no children"} +
    + `; + + // Position tooltip + const event = d3.event || window.event; + const mouseX = event.pageX || event.clientX; + const mouseY = event.pageY || event.clientY; + const padding = 12; + + this._tooltip.html(tooltipHTML); + + // Measure tooltip + const node = this._tooltip.style("display", "block").style("opacity", 0).node(); + const tooltipWidth = node.offsetWidth || 320; + const tooltipHeight = node.offsetHeight || 200; + + // Calculate position + let left = mouseX + padding; + let top = mouseY + padding; + + if (left + tooltipWidth > window.innerWidth) { + left = mouseX - tooltipWidth - padding; + if (left < 0) left = padding; + } + + if (top + tooltipHeight > window.innerHeight) { + top = mouseY - tooltipHeight - padding; + if (top < 0) top = padding; + } + + this._tooltip + .style("left", left + "px") + .style("top", top + "px") + .transition() + .duration(150) + .style("opacity", 1); + + // Update status bar + updateStatusBar(d.data, data.value, data); + }; + + pythonTooltip.hide = function () { + if (this._tooltip) { + this._tooltip.transition().duration(150).style("opacity", 0); + } + clearStatusBar(); + }; + + return pythonTooltip; +} + +// ============================================================================ +// Flamegraph Creation +// ============================================================================ + +function ensureLibraryLoaded() { + if (typeof flamegraph === "undefined") { + console.error("d3-flame-graph library not loaded"); + document.getElementById("chart").innerHTML = + '
    Error: d3-flame-graph library failed to load
    '; + throw new Error("d3-flame-graph library failed to load"); + } +} + +const HEAT_THRESHOLDS = [ + [0.6, 8], + [0.35, 7], + [0.18, 6], + [0.12, 5], + [0.06, 4], + [0.03, 3], + [0.01, 2], +]; + +function getHeatLevel(percentage) { + for (const [threshold, level] of HEAT_THRESHOLDS) { + if (percentage >= threshold) return level; + } + return 1; +} + +function getHeatColors() { + const style = getComputedStyle(document.documentElement); + const colors = {}; + for (let i = 1; i <= 8; i++) { + colors[i] = style.getPropertyValue(`--heat-${i}`).trim(); + } + return colors; +} + +function getDiffColors() { + const style = getComputedStyle(document.documentElement); + return { + elided: style.getPropertyValue('--diff-elided').trim(), + new: style.getPropertyValue('--diff-new').trim(), + neutral: style.getPropertyValue('--diff-neutral').trim(), + regressionDeep: style.getPropertyValue('--diff-regression-deep').trim(), + regressionMedium: style.getPropertyValue('--diff-regression-medium').trim(), + regressionLight: style.getPropertyValue('--diff-regression-light').trim(), + regressionVerylight: style.getPropertyValue('--diff-regression-verylight').trim(), + improvementDeep: style.getPropertyValue('--diff-improvement-deep').trim(), + improvementMedium: style.getPropertyValue('--diff-improvement-medium').trim(), + improvementLight: style.getPropertyValue('--diff-improvement-light').trim(), + improvementVerylight: style.getPropertyValue('--diff-improvement-verylight').trim(), + }; +} + +function getDiffColorForNode(node, diffColors) { + if (isShowingElided) { + return diffColors.elided; + } + + const diff_pct = node.data.diff_pct || 0; + const diff_samples = node.data.diff || 0; + const self_time = node.data.self_time || 0; + + if (diff_pct === 100 && self_time > 0 && Math.abs(diff_samples - self_time) < 0.1) { + return diffColors.new; + } + + // Neutral zone: small percentage change + if (Math.abs(diff_pct) < 15) { + return diffColors.neutral; + } + + // Regression (red scale) + if (diff_pct > 0) { + if (diff_pct >= 100) return diffColors.regressionDeep; + if (diff_pct > 50) return diffColors.regressionMedium; + if (diff_pct > 30) return diffColors.regressionLight; + return diffColors.regressionVerylight; + } + + // Improvement (blue scale) + if (diff_pct <= -100) return diffColors.improvementDeep; + if (diff_pct < -50) return diffColors.improvementMedium; + if (diff_pct < -30) return diffColors.improvementLight; + return diffColors.improvementVerylight; +} + +function createFlamegraph(tooltip, rootValue, data) { + const chartArea = document.querySelector('.chart-area'); + const width = chartArea ? chartArea.clientWidth - 32 : window.innerWidth - 320; + const heatColors = getHeatColors(); + + const isDifferential = data && data.stats && data.stats.is_differential; + const diffColors = isDifferential ? getDiffColors() : null; + + let chart = flamegraph() + .width(width) + .cellHeight(20) + .transitionDuration(300) + .minFrameSize(1) + .tooltip(tooltip) + .inverted(true) + .getName(d => resolveString(useModuleNames ? d.data.label : d.data.name) || resolveString(d.data.name) || '') + .setColorMapper(function (d) { + if (d.depth === 0) return 'transparent'; + + if (isDifferential) { + return getDiffColorForNode(d, diffColors); + } + + const percentage = d.data.value / rootValue; + const level = getHeatLevel(percentage); + return heatColors[level]; + }) + .onClick(function (d) { + zoomedNodeValue = d.data.value; + }); + + return chart; +} + +function renderFlamegraph(chart, data) { + d3.select("#chart").datum(data).call(chart); + window.flamegraphChart = chart; + window.flamegraphData = data; + zoomedNodeValue = null; + populateStats(data); +} + +// ============================================================================ +// Search +// ============================================================================ + +function updateSearchHighlight(searchTerm, searchInput) { + d3.selectAll("#chart rect") + .classed("search-match", false) + .classed("search-dim", false); + + // Clear active state from all hotspots + document.querySelectorAll('.hotspot').forEach(h => h.classList.remove('active')); + + if (searchTerm && searchTerm.length > 0) { + let matchCount = 0; + + d3.selectAll("#chart rect").each(function (d) { + if (d && d.data) { + const name = resolveString(d.data.name) || ""; + const funcname = resolveString(d.data.funcname) || ""; + const filename = resolveString(d.data.filename) || ""; + const moduleName = resolveString(d.data.module) || ""; + const displayName = getDisplayName(moduleName, filename); + const lineno = d.data.lineno; + const term = searchTerm.toLowerCase(); + + // Check if search term looks like path:line pattern + const fileLineMatch = term.match(/^(.+):(\d+)$/); + let matches = false; + + if (fileLineMatch) { + const searchFile = fileLineMatch[1]; + const searchLine = parseInt(fileLineMatch[2], 10); + matches = displayName.toLowerCase().includes(searchFile) && lineno === searchLine; + } else { + // Regular substring search + matches = + name.toLowerCase().includes(term) || + funcname.toLowerCase().includes(term) || + displayName.toLowerCase().includes(term); + } + + if (matches) { + matchCount++; + d3.select(this).classed("search-match", true); + } else { + d3.select(this).classed("search-dim", true); + } + } + }); + + if (searchInput) { + searchInput.classList.remove("has-matches", "no-matches"); + searchInput.classList.add(matchCount > 0 ? "has-matches" : "no-matches"); + } + + // Mark matching hotspot as active + document.querySelectorAll('.hotspot').forEach(h => { + if (h.dataset.searchterm && h.dataset.searchterm.toLowerCase() === searchTerm.toLowerCase()) { + h.classList.add('active'); + } + }); + } else if (searchInput) { + searchInput.classList.remove("has-matches", "no-matches"); + } +} + +function searchForHotspot(funcname) { + const searchInput = document.getElementById('search-input'); + const searchWrapper = document.querySelector('.search-wrapper'); + if (searchInput) { + // Toggle: if already searching for this term, clear it + if (searchInput.value.trim() === funcname) { + clearSearch(); + } else { + searchInput.value = funcname; + if (searchWrapper) { + searchWrapper.classList.add('has-value'); + } + performSearch(); + } + } +} + +function initSearchHandlers() { + const searchInput = document.getElementById("search-input"); + const searchWrapper = document.querySelector(".search-wrapper"); + if (!searchInput) return; + + let searchTimeout; + function performSearch() { + const term = searchInput.value.trim(); + updateSearchHighlight(term, searchInput); + // Toggle has-value class for clear button visibility + if (searchWrapper) { + searchWrapper.classList.toggle("has-value", term.length > 0); + } + } + + searchInput.addEventListener("input", function () { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(performSearch, 150); + }); + + window.performSearch = performSearch; +} + +function clearSearch() { + const searchInput = document.getElementById("search-input"); + const searchWrapper = document.querySelector(".search-wrapper"); + if (searchInput) { + searchInput.value = ""; + searchInput.classList.remove("has-matches", "no-matches"); + if (searchWrapper) { + searchWrapper.classList.remove("has-value"); + } + // Clear highlights + d3.selectAll("#chart rect") + .classed("search-match", false) + .classed("search-dim", false); + // Clear active hotspot + document.querySelectorAll('.hotspot').forEach(h => h.classList.remove('active')); + } +} + +// ============================================================================ +// Resize Handler +// ============================================================================ + +function handleResize() { + let resizeTimeout; + window.addEventListener("resize", function () { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(resizeChart, 100); + }); +} + +function initSidebarResize() { + const sidebar = document.getElementById('sidebar'); + const resizeHandle = document.getElementById('sidebar-resize-handle'); + if (!sidebar || !resizeHandle) return; + + let isResizing = false; + let startX = 0; + let startWidth = 0; + const minWidth = 200; + const maxWidth = 600; + + resizeHandle.addEventListener('mousedown', function(e) { + isResizing = true; + startX = e.clientX; + startWidth = sidebar.offsetWidth; + resizeHandle.classList.add('resizing'); + document.body.classList.add('resizing-sidebar'); + e.preventDefault(); + }); + + document.addEventListener('mousemove', function(e) { + if (!isResizing) return; + + const deltaX = e.clientX - startX; + const newWidth = Math.min(Math.max(startWidth + deltaX, minWidth), maxWidth); + sidebar.style.width = newWidth + 'px'; + e.preventDefault(); + }); + + document.addEventListener('mouseup', function() { + if (isResizing) { + isResizing = false; + resizeHandle.classList.remove('resizing'); + document.body.classList.remove('resizing-sidebar'); + + // Save the new width + const width = sidebar.offsetWidth; + localStorage.setItem('flamegraph-sidebar-width', width); + + // Resize chart after sidebar resize + setTimeout(() => { + resizeChart(); + }, 10); + } + }); +} + +// ============================================================================ +// Thread Stats +// ============================================================================ + +// Mode constants (must match constants.py) +const PROFILING_MODE_WALL = 0; +const PROFILING_MODE_CPU = 1; +const PROFILING_MODE_GIL = 2; +const PROFILING_MODE_ALL = 3; + +function populateThreadStats(data, selectedThreadId = null) { + const stats = data?.stats; + if (!stats || !stats.thread_stats) { + return; + } + + const mode = stats.mode !== undefined ? stats.mode : PROFILING_MODE_WALL; + let threadStats; + + if (selectedThreadId !== null && stats.per_thread_stats && stats.per_thread_stats[selectedThreadId]) { + threadStats = stats.per_thread_stats[selectedThreadId]; + } else { + threadStats = stats.thread_stats; + } + + if (!threadStats || typeof threadStats.total !== 'number' || threadStats.total <= 0) { + return; + } + + const section = document.getElementById('thread-stats-bar'); + if (!section) { + return; + } + + section.style.display = 'block'; + + const gilHeldStat = document.getElementById('gil-held-stat'); + const gilReleasedStat = document.getElementById('gil-released-stat'); + const gilWaitingStat = document.getElementById('gil-waiting-stat'); + + if (mode === PROFILING_MODE_GIL) { + // In GIL mode, hide GIL-related stats + if (gilHeldStat) gilHeldStat.style.display = 'none'; + if (gilReleasedStat) gilReleasedStat.style.display = 'none'; + if (gilWaitingStat) gilWaitingStat.style.display = 'none'; + } else { + // Show all stats + if (gilHeldStat) gilHeldStat.style.display = 'block'; + if (gilReleasedStat) gilReleasedStat.style.display = 'block'; + if (gilWaitingStat) gilWaitingStat.style.display = 'block'; + + const gilHeldPct = threadStats.has_gil_pct || 0; + const gilHeldPctElem = document.getElementById('gil-held-pct'); + if (gilHeldPctElem) gilHeldPctElem.textContent = `${gilHeldPct.toFixed(1)}%`; + const gilHeldFill = document.getElementById('gil-held-fill'); + if (gilHeldFill) gilHeldFill.style.width = `${gilHeldPct}%`; + + // GIL Released = not holding GIL and not waiting for it + const gilReleasedPct = Math.max(0, 100 - (threadStats.has_gil_pct || 0) - (threadStats.gil_requested_pct || 0)); + const gilReleasedPctElem = document.getElementById('gil-released-pct'); + if (gilReleasedPctElem) gilReleasedPctElem.textContent = `${gilReleasedPct.toFixed(1)}%`; + const gilReleasedFill = document.getElementById('gil-released-fill'); + if (gilReleasedFill) gilReleasedFill.style.width = `${gilReleasedPct}%`; + + const gilWaitingPct = threadStats.gil_requested_pct || 0; + const gilWaitingPctElem = document.getElementById('gil-waiting-pct'); + if (gilWaitingPctElem) gilWaitingPctElem.textContent = `${gilWaitingPct.toFixed(1)}%`; + const gilWaitingFill = document.getElementById('gil-waiting-fill'); + if (gilWaitingFill) gilWaitingFill.style.width = `${gilWaitingPct}%`; + } + + const gcPct = threadStats.gc_pct || 0; + const gcPctElem = document.getElementById('gc-pct'); + if (gcPctElem) gcPctElem.textContent = `${gcPct.toFixed(1)}%`; + const gcFill = document.getElementById('gc-fill'); + if (gcFill) gcFill.style.width = `${gcPct}%`; + + // Exception stats + const excPct = threadStats.has_exception_pct || 0; + const excPctElem = document.getElementById('exc-pct'); + if (excPctElem) excPctElem.textContent = `${excPct.toFixed(1)}%`; + const excFill = document.getElementById('exc-fill'); + if (excFill) excFill.style.width = `${excPct}%`; +} + +// ============================================================================ +// Profile Summary Stats +// ============================================================================ + +function formatNumber(num) { + if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; + if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; + return num.toLocaleString(); +} + +function formatDuration(seconds) { + if (seconds >= 3600) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + return `${h}h ${m}m`; + } + if (seconds >= 60) { + const m = Math.floor(seconds / 60); + const s = Math.floor(seconds % 60); + return `${m}m ${s}s`; + } + return seconds.toFixed(2) + 's'; +} + +function populateProfileSummary(data) { + const stats = data.stats || {}; + const totalSamples = currentThreadFilter !== 'all' + ? (data.value ?? 0) + : (stats.total_samples ?? data.value ?? 0); + const duration = stats.duration_sec || 0; + const sampleRate = stats.sample_rate || (duration > 0 ? totalSamples / duration : 0); + const errorRate = stats.error_rate; + const missedSamples = stats.missed_samples; + + const samplesEl = document.getElementById('stat-total-samples'); + if (samplesEl) samplesEl.textContent = formatNumber(totalSamples); + + const durationEl = document.getElementById('stat-duration'); + if (durationEl) durationEl.textContent = duration > 0 ? formatDuration(duration) : '--'; + + const rateEl = document.getElementById('stat-sample-rate'); + if (rateEl) rateEl.textContent = sampleRate > 0 ? formatNumber(Math.round(sampleRate)) : '--'; + + // Count unique functions + // Use normal (non-inverted) tree structure, but respect thread filtering + const uniqueFunctions = new Set(); + function collectUniqueFunctions(node) { + if (!node) return; + const filename = resolveString(node.filename) || 'unknown'; + const funcname = resolveString(node.funcname) || resolveString(node.name) || 'unknown'; + const lineno = node.lineno || 0; + const key = `${filename}|${lineno}|${funcname}`; + uniqueFunctions.add(key); + if (node.children) node.children.forEach(collectUniqueFunctions); + } + // In inverted mode, use normalData (with thread filter if active) + // In normal mode, use the passed data (already has thread filter applied if any) + let functionCountSource; + if (!normalData) { + functionCountSource = data; + } else if (isInverted) { + if (currentThreadFilter !== 'all') { + functionCountSource = filterDataByThread(normalData, parseInt(currentThreadFilter)); + } else { + functionCountSource = normalData; + } + } else { + functionCountSource = data; + } + collectUniqueFunctions(functionCountSource); + + const functionsEl = document.getElementById('stat-functions'); + if (functionsEl) functionsEl.textContent = formatNumber(uniqueFunctions.size); + + // Efficiency bar + if (errorRate !== undefined && errorRate !== null) { + const efficiency = Math.max(0, Math.min(100, (100 - errorRate))); + + const efficiencySection = document.getElementById('efficiency-section'); + if (efficiencySection) efficiencySection.style.display = 'block'; + + const efficiencyValue = document.getElementById('stat-efficiency'); + if (efficiencyValue) efficiencyValue.textContent = efficiency.toFixed(1) + '%'; + + const efficiencyFill = document.getElementById('efficiency-fill'); + if (efficiencyFill) efficiencyFill.style.width = efficiency + '%'; + } + // MissedSamples bar + if (missedSamples !== undefined && missedSamples !== null) { + const sampleEfficiency = Math.max(0, missedSamples); + + const efficiencySection = document.getElementById('efficiency-section'); + if (efficiencySection) efficiencySection.style.display = 'block'; + + const sampleEfficiencyValue = document.getElementById('stat-missed-samples'); + if (sampleEfficiencyValue) sampleEfficiencyValue.textContent = sampleEfficiency.toFixed(1) + '%'; + + const sampleEfficiencyFill = document.getElementById('missed-samples-fill'); + if (sampleEfficiencyFill) sampleEfficiencyFill.style.width = sampleEfficiency + '%'; + } +} + +// ============================================================================ +// Elided Stacks (Differential) +// ============================================================================ + +let elidedFlamegraphData = null; +let invertedElidedData = null; +let isShowingElided = false; + +function setupElidedToggle(data) { + const stats = data.stats || {}; + const elidedCount = stats.elided_count || 0; + const elidedFlamegraph = stats.elided_flamegraph; + + if (!elidedCount || !elidedFlamegraph) { + return; + } + + elidedFlamegraphData = resolveStringIndices(elidedFlamegraph, elidedFlamegraph.strings); + + const toggleElided = document.getElementById('toggle-elided'); + if (toggleElided) { + toggleElided.style.display = 'flex'; + + toggleElided.onclick = function() { + isShowingElided = !isShowingElided; + updateToggleUI('toggle-elided', isShowingElided); + updateFlamegraphView(); + }; + } +} + +// ============================================================================ +// Hotspot Stats +// ============================================================================ + +function populateStats(data) { + // Populate profile summary + populateProfileSummary(data); + + // Populate thread statistics if available + populateThreadStats(data); + + // Setup elided stacks toggle if this is a differential flamegraph + setupElidedToggle(data); + + // For hotspots: use normal (non-inverted) tree structure, but respect thread filtering. + // In inverted view, the tree structure changes but the hottest functions remain the same. + // However, if a thread filter is active, we need to show that thread's hotspots. + let hotspotSource; + if (!normalData) { + hotspotSource = data; + } else if (isInverted) { + // In inverted mode, use normalData (with thread filter if active) + if (currentThreadFilter !== 'all') { + hotspotSource = filterDataByThread(normalData, parseInt(currentThreadFilter)); + } else { + hotspotSource = normalData; + } + } else { + // In normal mode, use the passed data (already has thread filter applied if any) + hotspotSource = data; + } + const totalSamples = hotspotSource.value || 0; + + const functionMap = new Map(); + + function collectFunctions(node) { + if (!node) return; + + let filename = resolveString(node.filename); + let funcname = resolveString(node.funcname); + let moduleName = resolveString(node.module); + + if (!filename || !funcname) { + const nameStr = resolveString(node.name); + if (nameStr?.includes('(')) { + const match = nameStr.match(/^(.+?)\s*\((.+?):(\d+)\)$/); + if (match) { + funcname = funcname || match[1]; + filename = filename || match[2]; + } + } + } + + filename = filename || 'unknown'; + funcname = funcname || 'unknown'; + moduleName = moduleName || 'unknown'; + + if (filename !== 'unknown' && funcname !== 'unknown' && node.value > 0) { + const directSamples = node.self || 0; + + const funcKey = `${filename}:${node.lineno || '?'}:${funcname}`; + + if (functionMap.has(funcKey)) { + const existing = functionMap.get(funcKey); + existing.directSamples += directSamples; + existing.directPercent = (existing.directSamples / totalSamples) * 100; + if (directSamples > existing.maxSingleSamples) { + existing.filename = filename; + existing.module = moduleName; + existing.lineno = node.lineno || '?'; + existing.maxSingleSamples = directSamples; + } + } else { + functionMap.set(funcKey, { + filename: filename, + module: moduleName, + lineno: node.lineno || '?', + funcname: funcname, + directSamples, + directPercent: (directSamples / totalSamples) * 100, + maxSingleSamples: directSamples + }); + } + } + + if (node.children) { + node.children.forEach(child => collectFunctions(child)); + } + } + + collectFunctions(hotspotSource); + + const hotSpots = Array.from(functionMap.values()) + .filter(f => f.directPercent > 0.5) + .sort((a, b) => b.directPercent - a.directPercent) + .slice(0, 3); + + // Populate and animate hotspot cards + for (let i = 0; i < 3; i++) { + const num = i + 1; + const card = document.getElementById(`hotspot-${num}`); + const funcEl = document.getElementById(`hotspot-func-${num}`); + const fileEl = document.getElementById(`hotspot-file-${num}`); + const percentEl = document.getElementById(`hotspot-percent-${num}`); + const samplesEl = document.getElementById(`hotspot-samples-${num}`); + + if (i < hotSpots.length && hotSpots[i]) { + const h = hotSpots[i]; + const filename = h.filename || 'unknown'; + const lineno = h.lineno ?? '?'; + const moduleName = h.module || 'unknown'; + const isSpecialFrame = filename === '~' && (lineno === 0 || lineno === '?'); + + let funcDisplay = h.funcname || 'unknown'; + if (funcDisplay.length > 28) funcDisplay = funcDisplay.substring(0, 25) + '...'; + + if (funcEl) funcEl.textContent = funcDisplay; + if (fileEl) { + if (isSpecialFrame) { + fileEl.textContent = '--'; + } else { + const displayName = getDisplayName(moduleName, filename); + fileEl.textContent = `${displayName}:${lineno}`; + } + } + if (percentEl) percentEl.textContent = `${h.directPercent.toFixed(1)}%`; + if (samplesEl) samplesEl.textContent = ` (${h.directSamples.toLocaleString()})`; + } else { + if (funcEl) funcEl.textContent = '--'; + if (fileEl) fileEl.textContent = '--'; + if (percentEl) percentEl.textContent = '--'; + if (samplesEl) samplesEl.textContent = ''; + } + + // Add click handler and animate entrance + if (card) { + if (i < hotSpots.length && hotSpots[i]) { + const h = hotSpots[i]; + const moduleName = h.module || 'unknown'; + const filename = h.filename || 'unknown'; + const displayName = getDisplayName(moduleName, filename); + const hasValidLocation = displayName !== 'unknown' && h.lineno !== '?'; + const searchTerm = hasValidLocation ? `${displayName}:${h.lineno}` : h.funcname; + card.dataset.searchterm = searchTerm; + card.onclick = () => searchForHotspot(searchTerm); + card.style.cursor = 'pointer'; + } else { + card.onclick = null; + delete card.dataset.searchterm; + card.style.cursor = 'default'; + } + + setTimeout(() => { + card.classList.add('visible'); + }, 100 + i * 80); + } + } +} + +// ============================================================================ +// Thread Filter +// ============================================================================ + +function initThreadFilter(data) { + const threadFilter = document.getElementById('thread-filter'); + const threadSection = document.getElementById('thread-section'); + + if (!threadFilter || !data.threads || data.stats?.is_differential) return; + + threadFilter.innerHTML = ''; + + const threads = data.threads || []; + threads.forEach(threadId => { + const option = document.createElement('option'); + option.value = threadId; + option.textContent = `Thread ${threadId}`; + threadFilter.appendChild(option); + }); + + if (threads.length > 1 && threadSection) { + threadSection.style.display = 'block'; + } +} + +function filterByThread() { + const threadFilter = document.getElementById('thread-filter'); + if (!threadFilter || !normalData) return; + + const selectedThread = threadFilter.value; + currentThreadFilter = selectedThread; + + updateFlamegraphView(); +} + +function filterDataByThread(data, threadId) { + function filterNode(node) { + const threadValues = node.thread_values?.[threadId]; + if (!threadValues) { + return null; + } + + const { + thread_values: _threadValues, + thread_opcodes: threadOpcodes, + ...sharedNode + } = node; + const filteredNode = { + ...sharedNode, + value: threadValues[0], + self: threadValues[1], + opcodes: threadOpcodes?.[threadId] ?? {}, + children: [] + }; + + if (node.children && Array.isArray(node.children)) { + filteredNode.children = node.children + .map(child => filterNode(child)) + .filter(child => child !== null); + } + + return filteredNode; + } + + return filterNode(data); +} + +// ============================================================================ +// Control Functions +// ============================================================================ + +function resetZoom() { + if (window.flamegraphChart) { + zoomedNodeValue = null; + window.flamegraphChart.resetZoom(); + } +} + +function exportSVG() { + const svgElement = document.querySelector("#chart svg"); + if (!svgElement) { + console.warn("Cannot export: No flamegraph SVG found"); + return; + } + const serializer = new XMLSerializer(); + const svgString = serializer.serializeToString(svgElement); + const blob = new Blob([svgString], { type: "image/svg+xml" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "python-performance-flamegraph.svg"; + a.click(); + URL.revokeObjectURL(url); +} + +// ============================================================================ +// Inverted Flamegraph +// ============================================================================ + +// Example: "file.py|10|foo" or "~|0|" for special frames +function getInvertNodeKey(node) { + return `${node.filename || '~'}|${node.lineno || 0}|${node.funcname || node.name}`; +} + +function accumulateInvertedNode(parent, stackFrame, leaf, isDifferential) { + const key = getInvertNodeKey(stackFrame); + + if (!parent.children[key]) { + const newNode = { + name: stackFrame.name, + label: stackFrame.label, + value: 0, + self: 0, + children: {}, + filename: stackFrame.filename, + module: stackFrame.module, + lineno: stackFrame.lineno, + funcname: stackFrame.funcname, + source: stackFrame.source, + opcodes: null, + threads: new Set() + }; + + if (isDifferential) { + newNode.baseline = 0; + newNode.baseline_total = 0; + newNode.self_time = 0; + newNode.diff = 0; + newNode.diff_pct = 0; + } + + parent.children[key] = newNode; + } + + const node = parent.children[key]; + node.value += leaf.value; + node.self += stackFrame.self || 0; + if (leaf.threads) { + leaf.threads.forEach(t => node.threads.add(t)); + } + if (stackFrame.opcodes) { + if (!node.opcodes) { + node.opcodes = { ...stackFrame.opcodes }; + } else { + for (const [op, count] of Object.entries(stackFrame.opcodes)) { + node.opcodes[op] = (node.opcodes[op] || 0) + count; + } + } + } + + if (isDifferential) { + node.baseline += stackFrame.baseline || 0; + node.baseline_total += stackFrame.baseline_total || 0; + node.self_time += stackFrame.self_time || 0; + node.diff += stackFrame.diff || 0; + + if (node.baseline > 0) { + node.diff_pct = (node.diff / node.baseline) * 100.0; + } else if (node.self_time > 0) { + node.diff_pct = 100.0; + } + } + + return node; +} + +function processLeaf(invertedRoot, path, leafNode, isDifferential) { + if (!path || path.length === 0) { + return; + } + + let invertedParent = accumulateInvertedNode(invertedRoot, leafNode, leafNode, isDifferential); + + // Walk backwards through the call stack + for (let i = path.length - 2; i >= 0; i--) { + invertedParent = accumulateInvertedNode(invertedParent, path[i], leafNode, isDifferential); + } +} + +function traverseInvert(path, currentNode, invertedRoot, isDifferential) { + const selfValue = currentNode.self || 0; + + if (selfValue > 0) { + processLeaf(invertedRoot, path, { ...currentNode, value: selfValue }, isDifferential); + } + + const children = currentNode.children || []; + children.forEach(child => traverseInvert(path.concat([child]), child, invertedRoot, isDifferential)); +} + +function convertInvertDictToArray(node) { + if (node.threads instanceof Set) { + node.threads = Array.from(node.threads).sort((a, b) => a - b); + } + + const children = node.children; + if (children && typeof children === 'object' && !Array.isArray(children)) { + node.children = Object.values(children); + node.children.sort((a, b) => b.value - a.value || a.name.localeCompare(b.name)); + node.children.forEach(convertInvertDictToArray); + } + return node; +} + +function generateInvertedFlamegraph(data) { + const isDifferential = data && data.stats && data.stats.is_differential; + + const invertedRoot = { + name: data.name, + label: data.label, + value: data.value, + children: {}, + stats: data.stats, + threads: data.threads + }; + + const children = data.children || []; + if (children.length === 0) { + // Single-frame tree: the root is its own leaf + processLeaf(invertedRoot, [data], data, isDifferential); + } else { + children.forEach(child => traverseInvert([child], child, invertedRoot, isDifferential)); + } + + convertInvertDictToArray(invertedRoot); + return invertedRoot; +} + +function toggleInvert() { + isInverted = !isInverted; + updateToggleUI('toggle-invert', isInverted); + updateFlamegraphView(); +} + +function togglePathDisplay() { + useModuleNames = !useModuleNames; + updateToggleUI('toggle-path-display', useModuleNames); + updateFlamegraphView(); +} + +// ============================================================================ +// Initialization +// ============================================================================ + +function initFlamegraph() { + ensureLibraryLoaded(); + restoreUIState(); + setupLogos(); + + if (EMBEDDED_DATA.strings) { + stringTable = EMBEDDED_DATA.strings; + normalData = resolveStringIndices(EMBEDDED_DATA, EMBEDDED_DATA.strings); + } else { + normalData = EMBEDDED_DATA; + } + + // Initialize opcode mapping from embedded data + initOpcodeMapping(EMBEDDED_DATA); + + // Inverted data will be built on first toggle + invertedData = null; + + initThreadFilter(normalData); + + // Toggle legend based on differential mode + const isDifferential = normalData && normalData.stats && normalData.stats.is_differential; + const heatmapLegend = document.getElementById('heatmap-legend-section'); + const diffLegend = document.getElementById('diff-legend-section'); + if (isDifferential) { + if (heatmapLegend) heatmapLegend.style.display = 'none'; + if (diffLegend) diffLegend.style.display = 'block'; + } else { + if (heatmapLegend) heatmapLegend.style.display = 'block'; + if (diffLegend) diffLegend.style.display = 'none'; + } + + const tooltip = createPythonTooltip(normalData); + const chart = createFlamegraph(tooltip, normalData.value, normalData); + renderFlamegraph(chart, normalData); + initSearchHandlers(); + initSidebarResize(); + handleResize(); + + const toggleInvertBtn = document.getElementById('toggle-invert'); + if (toggleInvertBtn) { + toggleInvertBtn.addEventListener('click', toggleInvert); + } + + const togglePathDisplayBtn = document.getElementById('toggle-path-display'); + if (togglePathDisplayBtn) { + togglePathDisplayBtn.addEventListener('click', togglePathDisplay); + } +} + +// Keyboard shortcut: Enter/Space activates toggle switches +document.addEventListener('keydown', function(e) { + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + return; + } + if ((e.key === 'Enter' || e.key === ' ') && e.target.classList.contains('toggle-switch')) { + e.preventDefault(); + e.target.click(); + } +}); + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initFlamegraph); +} else { + initFlamegraph(); +} diff --git a/Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html b/Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html new file mode 100644 index 00000000000..f1c5bb03006 --- /dev/null +++ b/Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html @@ -0,0 +1,464 @@ + + + + + + {{TITLE}} + + + + + + + +
    + + + + diff --git a/Lib/profiling/sampling/_format_utils.py b/Lib/profiling/sampling/_format_utils.py new file mode 100644 index 00000000000..237a4f4186b --- /dev/null +++ b/Lib/profiling/sampling/_format_utils.py @@ -0,0 +1,5 @@ +import locale + + +def fmt(value: int | float, decimals: int = 1) -> str: + return locale.format_string(f'%.{decimals}f', value, grouping=True) diff --git a/Lib/profiling/sampling/_heatmap_assets/heatmap.css b/Lib/profiling/sampling/_heatmap_assets/heatmap.css new file mode 100644 index 00000000000..8f7f034ba7e --- /dev/null +++ b/Lib/profiling/sampling/_heatmap_assets/heatmap.css @@ -0,0 +1,1555 @@ +/* ========================================================================== + Heatmap Viewer - Component-Specific CSS + + DEPENDENCY: Requires _shared_assets/base.css to be loaded first + This file extends the shared foundation with heatmap-specific styles. + ========================================================================== */ + +/* Heatmap heat colors - using base.css colors with 60% opacity */ +[data-theme="dark"] { + --heat-1: rgba(90, 123, 167, 0.60); + --heat-2: rgba(106, 148, 168, 0.60); + --heat-3: rgba(122, 172, 172, 0.60); + --heat-4: rgba(142, 196, 152, 0.60); + --heat-5: rgba(168, 216, 136, 0.60); + --heat-6: rgba(200, 222, 122, 0.60); + --heat-7: rgba(244, 212, 93, 0.60); + --heat-8: rgba(255, 122, 69, 0.60); +} + +/* -------------------------------------------------------------------------- + Layout Overrides (Heatmap-specific) + -------------------------------------------------------------------------- */ + +.app-layout { + min-height: 100vh; +} + +/* Sticky top bar for heatmap views */ +.top-bar { + position: sticky; + top: 0; + z-index: 100; +} + +/* -------------------------------------------------------------------------- + Main Content Area + -------------------------------------------------------------------------- */ + +.main-content { + flex: 1; + padding: 24px 3%; + width: 100%; + max-width: 100%; +} + +/* -------------------------------------------------------------------------- + Stats Summary Cards - Enhanced with Icons & Animations + -------------------------------------------------------------------------- */ + +.stats-summary { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin-bottom: 24px; +} + +.stat-card { + display: flex; + align-items: center; + gap: 12px; + background: var(--bg-primary); + border: 2px solid var(--border); + border-radius: 10px; + padding: 14px 16px; + transition: all var(--transition-fast); + animation: slideUp 0.5s ease-out backwards; + animation-delay: calc(var(--i, 0) * 0.08s); + position: relative; + overflow: hidden; +} + +.stat-card:nth-child(1) { --i: 0; --card-color: var(--card-blue); } +.stat-card:nth-child(2) { --i: 1; --card-color: var(--card-green); } +.stat-card:nth-child(3) { --i: 2; --card-color: var(--card-yellow); } +.stat-card:nth-child(4) { --i: 3; --card-color: var(--card-purple); } + +.stat-card:hover { + border-color: rgba(var(--card-color), 0.6); + background: linear-gradient(135deg, rgba(var(--card-color), 0.08) 0%, var(--bg-primary) 100%); + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(var(--card-color), 0.15); +} + +.stat-icon { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + background: linear-gradient(135deg, rgba(var(--card-color), 0.15) 0%, rgba(var(--card-color), 0.05) 100%); + border: 1px solid rgba(var(--card-color), 0.2); + border-radius: 10px; + flex-shrink: 0; + transition: all var(--transition-fast); +} + +.stat-card:hover .stat-icon { + transform: scale(1.05) rotate(-2deg); + background: linear-gradient(135deg, rgba(var(--card-color), 0.25) 0%, rgba(var(--card-color), 0.1) 100%); +} + +.stat-data { + flex: 1; + min-width: 0; +} + +.stat-value { + font-family: var(--font-mono); + font-size: 1.35em; + font-weight: 800; + color: rgb(var(--card-color)); + display: block; + line-height: 1.1; + letter-spacing: -0.3px; +} + +.stat-label { + font-size: 10px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.3px; + margin-top: 2px; +} + +/* Sparkline decoration for stats */ +.stat-sparkline { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 30px; + opacity: 0.1; + background: linear-gradient(180deg, + transparent 0%, + rgba(var(--card-color), 0.3) 100% + ); + pointer-events: none; +} + +/* -------------------------------------------------------------------------- + Rate Cards (Error Rate, Missed Samples) with Progress Bars + -------------------------------------------------------------------------- */ + +.rate-card { + display: flex; + flex-direction: column; + gap: 12px; + background: var(--bg-primary); + border: 2px solid var(--border); + border-radius: 12px; + padding: 18px 20px; + transition: all var(--transition-fast); + animation: slideUp 0.5s ease-out backwards; + position: relative; + overflow: hidden; +} + +.rate-card:nth-child(5) { animation-delay: 0.32s; --rate-color: var(--card-red); } +.rate-card:nth-child(6) { animation-delay: 0.40s; --rate-color: var(--card-orange); } + +.rate-card:hover { + border-color: rgba(var(--rate-color), 0.5); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(var(--rate-color), 0.15); +} + +.rate-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.rate-info { + display: flex; + align-items: center; + gap: 10px; +} + +.rate-icon { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + background: linear-gradient(135deg, rgba(var(--rate-color), 0.15) 0%, rgba(var(--rate-color), 0.05) 100%); + border: 1px solid rgba(var(--rate-color), 0.2); + border-radius: 10px; + flex-shrink: 0; +} + +.rate-label { + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.rate-value { + font-family: var(--font-mono); + font-size: 1.4em; + font-weight: 800; + color: rgb(var(--rate-color)); +} + +.rate-bar { + height: 8px; + background: var(--bg-tertiary); + border-radius: 4px; + overflow: hidden; + position: relative; +} + +.rate-fill { + height: 100%; + border-radius: 4px; + transition: width 0.8s ease-out; + position: relative; + overflow: hidden; +} + +.rate-fill.error { + background: linear-gradient(90deg, #dc3545 0%, #ff6b6b 100%); +} + +.rate-fill.warning { + background: linear-gradient(90deg, #ff9800 0%, #ffc107 100%); +} + +.rate-fill.good { + background: linear-gradient(90deg, #28a745 0%, #20c997 100%); +} + +/* Shimmer animation on rate bars */ +.rate-fill::after { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.4) 50%, + transparent 100% + ); + animation: shimmer 2.5s ease-in-out infinite; +} + +/* -------------------------------------------------------------------------- + Section Headers + -------------------------------------------------------------------------- */ + +.section-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 2px solid var(--python-gold); +} + +.section-title { + font-size: 18px; + font-weight: 700; + color: var(--text-primary); + margin: 0; + flex: 1; +} + +/* -------------------------------------------------------------------------- + Filter Controls + -------------------------------------------------------------------------- */ + +.filter-controls { + display: flex; + gap: 8px; + flex-wrap: wrap; + align-items: center; + margin-bottom: 16px; +} + +.control-btn { + padding: 8px 16px; + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border); + border-radius: 6px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all var(--transition-fast); +} + +.control-btn:hover { + background: var(--accent); + color: white; + border-color: var(--accent); +} + +/* -------------------------------------------------------------------------- + Type Sections (stdlib, project, etc) + -------------------------------------------------------------------------- */ + +.type-section { + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; + margin-bottom: 12px; +} + +.type-header { + padding: 12px 16px; + background: var(--header-gradient); + color: white; + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + user-select: none; + transition: all var(--transition-fast); + font-weight: 600; +} + +.type-header:hover { + opacity: 0.95; +} + +.type-icon { + font-size: 12px; + transition: transform var(--transition-fast); + min-width: 12px; +} + +.type-title { + font-size: 14px; + flex: 1; +} + +.type-stats { + font-size: 12px; + opacity: 0.9; + background: rgba(255, 255, 255, 0.15); + padding: 4px 10px; + border-radius: 4px; + font-family: var(--font-mono); +} + +.type-content { + padding: 12px; +} + +/* -------------------------------------------------------------------------- + Folder Nodes (hierarchical structure) + -------------------------------------------------------------------------- */ + +.folder-node { + margin-bottom: 6px; +} + +.folder-header { + padding: 8px 12px; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + user-select: none; + transition: all var(--transition-fast); +} + +.folder-header:hover { + background: var(--accent-glow); + border-color: var(--accent); +} + +.folder-icon { + font-size: 10px; + color: var(--accent); + transition: transform var(--transition-fast); + min-width: 12px; +} + +.folder-name { + flex: 1; + font-weight: 500; + color: var(--text-primary); + font-size: 13px; +} + +.folder-stats { + font-size: 11px; + color: var(--text-secondary); + background: var(--bg-tertiary); + padding: 2px 8px; + border-radius: 4px; + font-family: var(--font-mono); +} + +.folder-content { + padding-left: 20px; + margin-top: 6px; +} + +/* -------------------------------------------------------------------------- + File Items + -------------------------------------------------------------------------- */ + +.files-list { + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 8px; +} + +.file-item { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + background: var(--bg-primary); + border: 1px solid var(--border-subtle); + border-radius: 6px; + transition: all var(--transition-fast); +} + +.file-item:hover { + background: var(--bg-secondary); + border-color: var(--border); +} + +.file-item .file-link { + flex: 1; + min-width: 0; + font-size: 13px; +} + +.file-samples { + font-size: 12px; + color: var(--text-secondary); + font-weight: 600; + white-space: nowrap; + width: 130px; + flex-shrink: 0; + text-align: right; + font-family: var(--font-mono); +} + +.heatmap-bar-container { + width: 120px; + flex-shrink: 0; + display: flex; + align-items: center; +} + +.heatmap-bar { + flex-shrink: 0; + border-radius: 2px; +} + +/* Links */ +.file-link { + color: var(--accent); + text-decoration: none; + font-weight: 500; + transition: color var(--transition-fast); +} + +.file-link:hover { + color: var(--accent-hover); + text-decoration: underline; +} + +/* -------------------------------------------------------------------------- + Module Badges + -------------------------------------------------------------------------- */ + +.module-badge { + display: inline-block; + padding: 3px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; +} + +.badge-stdlib { + background: rgba(40, 167, 69, 0.15); + color: #28a745; +} + +.badge-site-packages { + background: rgba(0, 123, 255, 0.15); + color: #007bff; +} + +.badge-project { + background: rgba(255, 193, 7, 0.2); + color: #d39e00; +} + +.badge-other { + background: var(--bg-tertiary); + color: var(--text-secondary); +} + +[data-theme="dark"] .badge-stdlib { + background: rgba(40, 167, 69, 0.25); + color: #5dd879; +} + +[data-theme="dark"] .badge-site-packages { + background: rgba(88, 166, 255, 0.25); + color: #79b8ff; +} + +[data-theme="dark"] .badge-project { + background: rgba(255, 212, 59, 0.25); + color: #ffd43b; +} + +/* ========================================================================== + FILE VIEW STYLES (Code Display) + ========================================================================== */ + +.code-view { + font-family: var(--font-mono); + min-height: 100vh; +} + +/* Code Header (Top Bar for file view) */ +.code-header { + height: var(--topbar-height); + background: var(--header-gradient); + display: flex; + align-items: center; + padding: 0 16px; + gap: 16px; + box-shadow: 0 2px 10px rgba(55, 118, 171, 0.25); + border-bottom: 2px solid var(--python-gold); + position: sticky; + top: 0; + z-index: 100; +} + +.code-header-content { + display: flex; + align-items: center; + justify-content: space-between; + width: 94%; + max-width: 100%; + margin: 0 auto; +} + +.code-header h1 { + font-size: 14px; + font-weight: 600; + color: white; + margin: 0; + font-family: var(--font-mono); + display: flex; + align-items: center; + gap: 8px; +} + +/* File Stats Bar */ +.file-stats { + background: var(--bg-secondary); + padding: 16px 24px; + border-bottom: 1px solid var(--border); +} + +.file-stats .stats-grid { + width: 94%; + max-width: 100%; + margin: 0 auto; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 12px; +} + +.stat-item { + background: var(--bg-primary); + padding: 12px; + border-radius: 8px; + box-shadow: var(--shadow-sm); + text-align: center; + border: 1px solid var(--border); + transition: all var(--transition-fast); +} + +.stat-item:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); + border-color: var(--accent); +} + +.stat-item .stat-value { + font-size: 1.4em; + font-weight: 700; + color: var(--accent); +} + +.stat-item .stat-label { + color: var(--text-muted); + font-size: 10px; + margin-top: 2px; +} + +/* Legend */ +.legend { + background: var(--bg-secondary); + padding: 12px 24px; + border-bottom: 1px solid var(--border); +} + +.legend-content { + width: 100%; + display: flex; + align-items: center; + gap: 20px; + flex-wrap: nowrap; +} + +.legend-separator { + width: 1px; + height: 24px; + background: var(--border); + flex-shrink: 0; +} + +.legend-title { + font-weight: 600; + color: var(--text-primary); + font-size: 13px; + font-family: var(--font-sans); + flex-shrink: 0; +} + +.legend-gradient { + width: 150px; + flex-shrink: 0; + height: 20px; + background: linear-gradient(90deg, + var(--bg-tertiary) 0%, + var(--heat-2) 25%, + var(--heat-4) 50%, + var(--heat-6) 75%, + var(--heat-8) 100% + ); + border-radius: 4px; + border: 1px solid var(--border); +} + +.legend-labels { + display: flex; + gap: 12px; + font-size: 11px; + color: var(--text-muted); + font-family: var(--font-sans); + flex-shrink: 0; +} + +/* Legend Controls Group - wraps toggles and bytecode button together */ +.legend-controls { + display: flex; + align-items: center; + gap: 20px; + flex-shrink: 0; + margin-left: auto; +} + +/* Heatmap-Specific Toggle Overrides */ +#toggle-color-mode .toggle-track.on { + background: #8e44ad; + border-color: #8e44ad; + box-shadow: 0 0 8px rgba(142, 68, 173, 0.3); +} + +#toggle-cold .toggle-track.on { + background: #e67e22; + border-color: #e67e22; + box-shadow: 0 0 8px rgba(230, 126, 34, 0.3); +} + +/* Code Container */ +.code-container { + width: 94%; + max-width: 100%; + margin: 16px auto; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px 8px 8px 8px; + box-shadow: var(--shadow-sm); + /* Allow horizontal scroll for long lines, but don't clip sticky header */ +} + +/* Code Header Row */ +.code-header-row { + position: sticky; + top: var(--topbar-height); + z-index: 50; + display: flex; + background: var(--bg-secondary); + border-bottom: 2px solid var(--border); + font-weight: 700; + font-size: 11px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + border-radius: 8px 8px 0 0; +} + +.header-line-number { + flex-shrink: 0; + width: 60px; + padding: 8px 10px; + text-align: right; + border-right: 1px solid var(--border); +} + +.header-samples-self, +.header-samples-cumulative { + flex-shrink: 0; + width: 90px; + padding: 8px 10px; + text-align: right; + border-right: 1px solid var(--border); +} + +.header-samples-self { + color: var(--heat-8); +} + +.header-samples-cumulative { + color: var(--accent); +} + +.header-content { + flex: 1; + padding: 8px 15px; +} + +/* Code Lines */ +.code-line { + position: relative; + display: flex; + min-height: 20px; + line-height: 20px; + font-size: 13px; + transition: background var(--transition-fast); + scroll-margin-top: calc(var(--topbar-height) + 50px); +} + +.code-line:hover { + filter: brightness(0.97); +} + +[data-theme="dark"] .code-line:hover { + filter: brightness(1.1); +} + +.line-number { + flex-shrink: 0; + width: 60px; + padding: 0 10px; + text-align: right; + color: var(--text-muted); + background: var(--bg-secondary); + border-right: 1px solid var(--border); + user-select: none; + transition: all var(--transition-fast); +} + +.line-number:hover { + background: var(--accent); + color: white; + cursor: pointer; +} + +.line-samples-self, +.line-samples-cumulative { + flex-shrink: 0; + width: 90px; + padding: 0 10px; + text-align: right; + background: var(--bg-secondary); + border-right: 1px solid var(--border); + font-weight: 600; + user-select: none; + font-size: 12px; +} + +.line-samples-self { + color: var(--heat-8); +} + +.line-samples-cumulative { + color: var(--accent); +} + +.line-content { + flex: 1; + padding: 0 15px; + white-space: pre; + overflow-x: auto; +} + +/* Scrollbar Styling */ +.line-content::-webkit-scrollbar { + height: 6px; +} + +.line-content::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 3px; +} + +.line-content::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* Navigation Buttons */ +.line-nav-buttons { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + display: flex; + gap: 4px; + align-items: center; +} + +.nav-btn { + padding: 2px 6px; + font-size: 12px; + font-weight: 500; + border: 1px solid var(--accent); + border-radius: 4px; + background: var(--bg-primary); + color: var(--accent); + cursor: pointer; + transition: all var(--transition-fast); + user-select: none; + line-height: 1; +} + +.nav-btn:hover:not(:disabled) { + background: var(--accent); + color: white; + transform: translateY(-1px); + box-shadow: var(--shadow-sm); +} + +.nav-btn:active:not(:disabled) { + transform: translateY(0); +} + +.nav-btn:disabled { + opacity: 0.3; + cursor: not-allowed; + color: var(--text-muted); + background: var(--bg-secondary); + border-color: var(--border); +} + +.nav-btn.caller { + color: var(--nav-caller); + border-color: var(--nav-caller); +} + +.nav-btn.callee { + color: var(--nav-callee); + border-color: var(--nav-callee); +} + +.nav-btn.caller:hover:not(:disabled) { + background: var(--nav-caller-hover); + color: white; +} + +.nav-btn.callee:hover:not(:disabled) { + background: var(--nav-callee-hover); + color: white; +} + +/* Highlighted target line */ +.code-line:target { + animation: highlight-line 2s ease-out; +} + +@keyframes highlight-line { + 0% { + background: rgba(255, 212, 59, 0.6) !important; + outline: 3px solid var(--python-gold); + outline-offset: -3px; + } + 50% { + background: rgba(255, 212, 59, 0.5) !important; + outline: 3px solid var(--python-gold); + outline-offset: -3px; + } + 100% { + outline: 3px solid transparent; + outline-offset: -3px; + } +} + +/* Popup menu for multiple callees */ +.callee-menu { + position: absolute; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: var(--shadow-lg); + padding: 8px; + z-index: 1000; + min-width: 250px; + max-width: 400px; + max-height: 300px; + overflow-y: auto; +} + +.callee-menu-header { + font-weight: 600; + color: var(--text-primary); + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border); + font-size: 13px; + font-family: var(--font-sans); +} + +.callee-menu-item { + padding: 8px; + margin: 4px 0; + border-radius: 6px; + cursor: pointer; + transition: background var(--transition-fast); + display: flex; + flex-direction: column; + gap: 4px; +} + +.callee-menu-item:hover { + background: var(--bg-secondary); +} + +.callee-menu-func { + font-weight: 500; + color: var(--accent); + font-size: 12px; +} + +.callee-menu-file { + font-size: 11px; + color: var(--text-muted); +} + +.count-badge { + display: inline-block; + background: var(--accent); + color: white; + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + font-weight: 600; + margin-left: 6px; +} + +/* Callee menu scrollbar */ +.callee-menu::-webkit-scrollbar { + width: 6px; +} + +.callee-menu::-webkit-scrollbar-track { + background: var(--bg-secondary); + border-radius: 3px; +} + +.callee-menu::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 3px; +} + +/* -------------------------------------------------------------------------- + Scroll Minimap Marker + -------------------------------------------------------------------------- */ + +#scroll_marker { + position: fixed; + z-index: 1000; + right: 0; + top: 0; + width: 12px; + height: 100%; + background: var(--bg-secondary); + border-left: 1px solid var(--border); + pointer-events: none; +} + +#scroll_marker .marker { + position: absolute; + min-height: 3px; + width: 100%; + pointer-events: none; +} + +#scroll_marker .marker.cold { + background: var(--heat-1); +} + +#scroll_marker .marker.cool { + background: var(--heat-2); +} + +#scroll_marker .marker.mild { + background: var(--heat-3); +} + +#scroll_marker .marker.warm { + background: var(--heat-4); +} + +#scroll_marker .marker.hot { + background: var(--heat-5); +} + +#scroll_marker .marker.very-hot { + background: var(--heat-6); +} + +#scroll_marker .marker.intense { + background: var(--heat-7); +} + +#scroll_marker .marker.extreme { + background: var(--heat-8); +} + +/* -------------------------------------------------------------------------- + Responsive (Heatmap-specific) + -------------------------------------------------------------------------- */ + +@media (max-width: 1100px) { + .stats-summary { + grid-template-columns: repeat(2, 1fr); + } + + .legend-content { + flex-wrap: wrap; + justify-content: center; + } + + .legend-controls { + margin-left: 0; + } +} + +@media (max-width: 900px) { + .main-content { + padding: 16px; + } +} + +@media (max-width: 600px) { + .stats-summary { + grid-template-columns: 1fr; + } + + .file-stats .stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .legend-content { + flex-direction: column; + align-items: center; + gap: 12px; + } + + .legend-gradient { + width: 100%; + max-width: none; + } + + .legend-separator { + width: 80%; + height: 1px; + } + + .legend-controls { + flex-direction: column; + gap: 12px; + } + + .legend-controls .toggle-switch { + justify-content: center; + } + + .legend-controls .toggle-switch .toggle-label:first-child { + width: 70px; + text-align: right; + } + + .legend-controls .toggle-switch .toggle-label:last-child { + width: 90px; + text-align: left; + } + + /* Compact code columns on small screens */ + .header-line-number, + .line-number { + width: 40px; + } + + .header-samples-self, + .header-samples-cumulative, + .line-samples-self, + .line-samples-cumulative { + width: 55px; + font-size: 10px; + } + + /* Adjust padding - headers need vertical, data rows don't */ + .header-line-number, + .header-samples-self, + .header-samples-cumulative { + padding: 8px 4px; + } + + .line-number, + .line-samples-self, + .line-samples-cumulative { + padding: 0 4px; + } + + .bytecode-panel { + margin: 8px 10px 8px 160px; + } +} + +.bytecode-toggle { + flex-shrink: 0; + width: 20px; + height: 20px; + padding: 0; + margin: 0 4px; + border: none; + background: transparent; + color: var(--code-accent); + cursor: pointer; + font-size: 10px; + transition: transform var(--transition-fast), color var(--transition-fast); + display: inline-flex; + align-items: center; + justify-content: center; +} + +.bytecode-toggle:hover { + color: var(--accent); +} + +.bytecode-spacer { + flex-shrink: 0; + width: 20px; + height: 20px; + margin: 0 4px; +} + +.bytecode-panel { + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: var(--shadow-md); + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-primary); + line-height: 1.5; + word-wrap: break-word; + overflow-wrap: break-word; + padding: 0; + margin: 8px 10px 8px 250px; + position: relative; + z-index: 1; + overflow-y: auto; + max-height: 500px; + flex: 1; + transition: padding 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.bytecode-panel.expanded { + padding: 14px; +} + +.bytecode-wrapper { + position: relative; + display: flex; + overflow: visible; + max-height: 0; + opacity: 0; + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease-in-out; +} + +.bytecode-wrapper.expanded { + max-height: 600px; + opacity: 1; + transition: max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s ease-in-out; +} + +/* Column backdrop matching table header columns (line/self/total) */ +.bytecode-columns { + display: none; + position: absolute; + left: 0; + overflow: hidden; + pointer-events: none; + z-index: 0; +} + +.bytecode-wrapper.expanded .bytecode-columns { + display: flex; + top: 0; + bottom: 0; +} + +.bytecode-panel::-webkit-scrollbar { + width: 8px; +} + +.bytecode-panel::-webkit-scrollbar-track { + background: var(--bg-secondary); + border-radius: 4px; +} + +.bytecode-panel::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} + +.bytecode-panel::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* Specialization summary bar */ +.bytecode-spec-summary { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + margin-bottom: 10px; + border-radius: var(--radius-sm); + background: rgba(100, 100, 100, 0.1); +} + +.bytecode-spec-summary .spec-pct { + font-size: 1.4em; + font-weight: 700; +} + +.bytecode-spec-summary .spec-label { + font-weight: 500; + text-transform: uppercase; + font-size: 0.85em; + letter-spacing: 0.5px; +} + +.bytecode-spec-summary .spec-detail { + color: var(--text-secondary); + font-size: 0.9em; + margin-left: auto; +} + +.bytecode-spec-summary.high { + background: var(--spec-high-bg); + border-left: 3px solid var(--spec-high); +} +.bytecode-spec-summary.high .spec-pct, +.bytecode-spec-summary.high .spec-label { + color: var(--spec-high-text); +} + +.bytecode-spec-summary.medium { + background: var(--spec-medium-bg); + border-left: 3px solid var(--spec-medium); +} +.bytecode-spec-summary.medium .spec-pct, +.bytecode-spec-summary.medium .spec-label { + color: var(--spec-medium-text); +} + +.bytecode-spec-summary.low { + background: var(--spec-low-bg); + border-left: 3px solid var(--spec-low); +} +.bytecode-spec-summary.low .spec-pct, +.bytecode-spec-summary.low .spec-label { + color: var(--spec-low-text); +} + +.bytecode-header { + display: grid; + grid-template-columns: 1fr 80px 80px; + gap: 12px; + padding: 4px 8px; + font-weight: 600; + color: var(--text-secondary); + border-bottom: 1px solid var(--code-border); + margin-bottom: 4px; +} + +.bytecode-expand-all { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: var(--bg-secondary); + border: 1px solid var(--code-border); + border-radius: var(--radius-sm); + color: var(--text-secondary); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all var(--transition-fast); + flex-shrink: 0; +} + +.bytecode-expand-all:hover, +.bytecode-expand-all.expanded { + background: var(--accent); + color: white; + border-color: var(--accent); +} + +.bytecode-expand-all .expand-icon { + font-size: 10px; + transition: transform var(--transition-fast); +} + +.bytecode-expand-all.expanded .expand-icon { + transform: rotate(90deg); +} + +/* ======================================== + INSTRUCTION SPAN HIGHLIGHTING + (triggered only from bytecode panel hover) + ======================================== */ + +/* Highlight from bytecode panel hover */ +.instr-span.highlight-from-bytecode { + outline: 3px solid #ff6b6b !important; + background-color: rgba(255, 107, 107, 0.4) !important; + border-radius: 2px; +} + +/* Bytecode instruction row */ +.bytecode-instruction { + display: grid; + grid-template-columns: 1fr 80px 80px; + gap: 12px; + align-items: center; + padding: 4px 8px; + margin: 2px 0; + border-radius: var(--radius-sm); + cursor: pointer; + transition: background-color var(--transition-fast); +} + +.bytecode-instruction:hover, +.bytecode-instruction.highlight { + background-color: rgba(55, 118, 171, 0.15); +} + +.bytecode-instruction[data-locations] { + cursor: pointer; +} + +.bytecode-instruction[data-locations]:hover { + background-color: rgba(255, 107, 107, 0.2); +} + +.bytecode-opname { + font-weight: 600; + font-family: var(--font-mono); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bytecode-opname.specialized { + color: #2e7d32; +} + +[data-theme="dark"] .bytecode-opname.specialized { + color: #81c784; +} + +.bytecode-opname .base-op { + color: var(--code-text-muted); + font-weight: normal; + font-size: 0.9em; + margin-left: 4px; +} + +.bytecode-samples { + text-align: right; + font-weight: 600; + color: var(--accent); + font-family: var(--font-mono); +} + +.bytecode-samples.hot { + color: #ff6b6b; +} + +.bytecode-heatbar { + width: 60px; + height: 12px; + background: var(--bg-secondary); + border-radius: 2px; + overflow: hidden; + border: 1px solid var(--code-border); +} + +.bytecode-heatbar-fill { + height: 100%; + background: linear-gradient(90deg, #00d4ff 0%, #ff6b00 100%); +} + +.specialization-badge { + display: inline-block; + padding: 1px 6px; + font-size: 0.75em; + background: #e8f5e9; + color: #2e7d32; + border-radius: 3px; + margin-left: 6px; + font-weight: 600; +} + +[data-theme="dark"] .specialization-badge { + background: rgba(129, 199, 132, 0.2); + color: #81c784; +} + +.bytecode-empty { + color: var(--code-text-muted); + font-style: italic; + padding: 8px; +} + +.bytecode-error { + color: #d32f2f; + font-style: italic; + padding: 8px; +} + +/* ======================================== + SPAN TOOLTIPS + ======================================== */ + +.span-tooltip { + position: absolute; + z-index: 10000; + background: var(--bg-primary); + color: var(--text-primary); + padding: 10px 14px; + border-radius: var(--radius-md); + border: 1px solid var(--border); + font-family: var(--font-sans); + font-size: 12px; + box-shadow: var(--shadow-lg); + pointer-events: none; + min-width: 160px; + max-width: 300px; +} + +.span-tooltip::after { + content: ''; + position: absolute; + bottom: -7px; + left: 50%; + transform: translateX(-50%); + border-width: 7px 7px 0; + border-style: solid; + border-color: var(--bg-primary) transparent transparent; + filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.1)); +} + +.span-tooltip-header { + font-weight: 600; + margin-bottom: 8px; + padding-bottom: 6px; + border-bottom: 1px solid var(--border); + color: var(--text-primary); +} + +.span-tooltip-header.hot { + color: #e65100; +} + +.span-tooltip-header.warm { + color: #f59e0b; +} + +.span-tooltip-header.cold { + color: var(--text-muted); +} + +.span-tooltip-row { + display: flex; + justify-content: space-between; + margin: 4px 0; + gap: 16px; +} + +.span-tooltip-label { + color: var(--text-secondary); +} + +.span-tooltip-value { + font-weight: 600; + text-align: right; + color: var(--text-primary); +} + +.span-tooltip-value.highlight { + color: var(--accent); +} + +.span-tooltip-section { + font-weight: 600; + color: var(--text-secondary); + font-size: 11px; + margin-top: 8px; + margin-bottom: 4px; + padding-top: 6px; + border-top: 1px solid var(--border); +} + +.span-tooltip-opcode { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-primary); + background: var(--bg-secondary); + padding: 3px 8px; + margin: 2px 0; + border-radius: var(--radius-sm); + border-left: 2px solid var(--accent); +} diff --git a/Lib/profiling/sampling/_heatmap_assets/heatmap.js b/Lib/profiling/sampling/_heatmap_assets/heatmap.js new file mode 100644 index 00000000000..1f698779f3a --- /dev/null +++ b/Lib/profiling/sampling/_heatmap_assets/heatmap.js @@ -0,0 +1,738 @@ +// Tachyon Profiler - Heatmap JavaScript +// Interactive features for the heatmap visualization +// Aligned with Flamegraph viewer design patterns + +// ============================================================================ +// State Management +// ============================================================================ + +let currentMenu = null; +let colorMode = 'self'; // 'self' or 'cumulative' - default to self +let coldCodeHidden = false; + +// ============================================================================ +// Theme Support +// ============================================================================ + +function toggleTheme() { + toggleAndSaveTheme(); + applyLineColors(); + + // Rebuild scroll marker with new theme colors + buildScrollMarker(); +} + +// ============================================================================ +// Utility Functions +// ============================================================================ + +function createElement(tag, className, textContent = '') { + const el = document.createElement(tag); + if (className) el.className = className; + if (textContent) el.textContent = textContent; + return el; +} + +function calculateMenuPosition(buttonRect, menuWidth, menuHeight) { + const viewport = { width: window.innerWidth, height: window.innerHeight }; + const scroll = { + x: window.pageXOffset || document.documentElement.scrollLeft, + y: window.pageYOffset || document.documentElement.scrollTop + }; + + const left = buttonRect.right + menuWidth + 10 < viewport.width + ? buttonRect.right + scroll.x + 10 + : Math.max(scroll.x + 10, buttonRect.left + scroll.x - menuWidth - 10); + + const top = buttonRect.bottom + menuHeight + 10 < viewport.height + ? buttonRect.bottom + scroll.y + 5 + : Math.max(scroll.y + 10, buttonRect.top + scroll.y - menuHeight - 10); + + return { left, top }; +} + +// ============================================================================ +// Menu Management +// ============================================================================ + +function closeMenu() { + if (currentMenu) { + currentMenu.remove(); + currentMenu = null; + } +} + +function showNavigationMenu(button, items, title) { + closeMenu(); + + const menu = createElement('div', 'callee-menu'); + menu.appendChild(createElement('div', 'callee-menu-header', title)); + + items.forEach(linkData => { + const item = createElement('div', 'callee-menu-item'); + + const funcDiv = createElement('div', 'callee-menu-func'); + funcDiv.textContent = linkData.func; + + if (linkData.count !== undefined && linkData.count > 0) { + const countBadge = createElement('span', 'count-badge'); + countBadge.textContent = linkData.count.toLocaleString(); + countBadge.title = `${linkData.count.toLocaleString()} samples`; + funcDiv.appendChild(document.createTextNode(' ')); + funcDiv.appendChild(countBadge); + } + + item.appendChild(funcDiv); + item.appendChild(createElement('div', 'callee-menu-file', linkData.file)); + item.addEventListener('click', () => navigateToLine(linkData.link)); + menu.appendChild(item); + }); + + const pos = calculateMenuPosition(button.getBoundingClientRect(), 350, 300); + menu.style.left = `${pos.left}px`; + menu.style.top = `${pos.top}px`; + + document.body.appendChild(menu); + currentMenu = menu; +} + +// ============================================================================ +// Navigation +// ============================================================================ + +function handleNavigationClick(button, e) { + e.stopPropagation(); + + const navData = button.getAttribute('data-nav'); + if (navData) { + navigateToLine(JSON.parse(navData).link); + return; + } + + const navMulti = button.getAttribute('data-nav-multi'); + if (navMulti) { + const items = JSON.parse(navMulti); + const title = button.classList.contains('caller') ? 'Choose a caller:' : 'Choose a callee:'; + showNavigationMenu(button, items, title); + } +} + +function restartLineHighlight(target) { + target.style.animation = 'none'; + // Force style recalculation so restoring the animation restarts it. + void target.offsetWidth; + target.style.animation = ''; +} + +function navigateToLine(link) { + const url = new URL(link, window.location.href); + + if (url.href === window.location.href) { + scrollToTargetLine(); + } else { + window.location.href = link; + } +} + +function scrollToTargetLine() { + if (!window.location.hash) return; + const target = document.querySelector(window.location.hash); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + restartLineHighlight(target); + } +} + +// ============================================================================ +// Sample Count & Intensity +// ============================================================================ + +function getSampleCount(line) { + let text; + if (colorMode === 'self') { + text = line.querySelector('.line-samples-self')?.textContent.trim().replace(/,/g, ''); + } else { + text = line.querySelector('.line-samples-cumulative')?.textContent.trim().replace(/,/g, ''); + } + return parseInt(text) || 0; +} + +// ============================================================================ +// Scroll Minimap +// ============================================================================ + +function buildScrollMarker() { + const existing = document.getElementById('scroll_marker'); + if (existing) existing.remove(); + + if (document.body.scrollHeight <= window.innerHeight) return; + + const allLines = document.querySelectorAll('.code-line'); + const lines = Array.from(allLines).filter(line => line.style.display !== 'none'); + const markerScale = window.innerHeight / document.body.scrollHeight; + const lineHeight = Math.min(Math.max(3, window.innerHeight / lines.length), 10); + const maxSamples = Math.max(...Array.from(lines, getSampleCount)); + + const scrollMarker = createElement('div', ''); + scrollMarker.id = 'scroll_marker'; + + let prevLine = -99, lastMark, lastTop; + + lines.forEach((line, index) => { + const samples = getSampleCount(line); + if (samples === 0) return; + + const lineTop = Math.floor(line.offsetTop * markerScale); + const lineNumber = index + 1; + const intensityClass = maxSamples > 0 ? (intensityToClass(samples / maxSamples) || 'cold') : 'cold'; + + if (lineNumber === prevLine + 1 && lastMark?.classList.contains(intensityClass)) { + lastMark.style.height = `${lineTop + lineHeight - lastTop}px`; + } else { + lastMark = createElement('div', `marker ${intensityClass}`); + lastMark.style.height = `${lineHeight}px`; + lastMark.style.top = `${lineTop}px`; + scrollMarker.appendChild(lastMark); + lastTop = lineTop; + } + + prevLine = lineNumber; + }); + + document.body.appendChild(scrollMarker); +} + +function applyLineColors() { + const lines = document.querySelectorAll('.code-line'); + lines.forEach(line => { + let intensity; + if (colorMode === 'self') { + intensity = parseFloat(line.getAttribute('data-self-intensity')) || 0; + } else { + intensity = parseFloat(line.getAttribute('data-cumulative-intensity')) || 0; + } + + const color = intensityToColor(intensity); + line.style.background = color; + }); +} + +// ============================================================================ +// Toggle Controls +// ============================================================================ + +function toggleColdCode() { + coldCodeHidden = !coldCodeHidden; + applyHotFilter(); + updateToggleUI('toggle-cold', coldCodeHidden); + buildScrollMarker(); +} + +function applyHotFilter() { + const lines = document.querySelectorAll('.code-line'); + + lines.forEach(line => { + const selfSamples = line.querySelector('.line-samples-self')?.textContent.trim(); + const cumulativeSamples = line.querySelector('.line-samples-cumulative')?.textContent.trim(); + + let isCold; + if (colorMode === 'self') { + isCold = !selfSamples || selfSamples === ''; + } else { + isCold = !cumulativeSamples || cumulativeSamples === ''; + } + + if (isCold) { + line.style.display = coldCodeHidden ? 'none' : 'flex'; + } else { + line.style.display = 'flex'; + } + }); +} + +function toggleColorMode() { + colorMode = colorMode === 'self' ? 'cumulative' : 'self'; + applyLineColors(); + + updateToggleUI('toggle-color-mode', colorMode === 'cumulative'); + + if (coldCodeHidden) { + applyHotFilter(); + } + + buildScrollMarker(); +} + +// ============================================================================ +// Initialization +// ============================================================================ + +document.addEventListener('DOMContentLoaded', function() { + restoreUIState(); + applyLineColors(); + + // Initialize navigation buttons + document.querySelectorAll('.nav-btn').forEach(button => { + button.addEventListener('click', e => handleNavigationClick(button, e)); + }); + + // Initialize line number permalink handlers + document.querySelectorAll('.line-number').forEach(lineNum => { + lineNum.style.cursor = 'pointer'; + lineNum.addEventListener('click', e => { + window.location.hash = `line-${e.target.textContent.trim()}`; + }); + }); + + // Initialize toggle buttons + const toggleColdBtn = document.getElementById('toggle-cold'); + if (toggleColdBtn) toggleColdBtn.addEventListener('click', toggleColdCode); + + const colorModeBtn = document.getElementById('toggle-color-mode'); + if (colorModeBtn) colorModeBtn.addEventListener('click', toggleColorMode); + + // Initialize specialization view toggle (hide if no bytecode data) + const hasBytecode = document.querySelectorAll('.bytecode-toggle').length > 0; + + const specViewBtn = document.getElementById('toggle-spec-view'); + if (specViewBtn) { + if (hasBytecode) { + specViewBtn.addEventListener('click', toggleSpecView); + } else { + specViewBtn.style.display = 'none'; + } + } + + // Initialize expand-all bytecode button + const expandAllBtn = document.getElementById('toggle-all-bytecode'); + if (expandAllBtn) { + if (hasBytecode) { + expandAllBtn.addEventListener('click', toggleAllBytecode); + } else { + expandAllBtn.style.display = 'none'; + } + } + + // Initialize span tooltips + initSpanTooltips(); + + // Build scroll marker and scroll to target + setTimeout(buildScrollMarker, 200); + setTimeout(scrollToTargetLine, 100); +}); + +// Close menu when clicking outside +document.addEventListener('click', e => { + if (currentMenu && !currentMenu.contains(e.target) && !e.target.classList.contains('nav-btn')) { + closeMenu(); + } +}); + +// ======================================== +// SPECIALIZATION VIEW TOGGLE +// ======================================== + +let specViewEnabled = false; + +/** + * Calculate heat color for given intensity (0-1) + * Hot spans (>30%) get warm orange, cold spans get dimmed gray + * @param {number} intensity - Value between 0 and 1 + * @returns {string} rgba color string + */ +function calculateHeatColor(intensity) { + // Hot threshold: only spans with >30% of max samples get color + if (intensity > 0.3) { + // Normalize intensity above threshold to 0-1 + const normalizedIntensity = (intensity - 0.3) / 0.7; + // Warm orange-red with increasing opacity for hotter spans + const alpha = 0.25 + normalizedIntensity * 0.35; // 0.25 to 0.6 + const hotColor = getComputedStyle(document.documentElement).getPropertyValue('--span-hot-base').trim(); + return `rgba(${hotColor}, ${alpha})`; + } else if (intensity > 0) { + // Cold spans: very subtle gray, almost invisible + const coldColor = getComputedStyle(document.documentElement).getPropertyValue('--span-cold-base').trim(); + return `rgba(${coldColor}, 0.1)`; + } + return 'transparent'; +} + +/** + * Apply intensity-based heat colors to source spans + * Hot spans get orange highlight, cold spans get dimmed + * @param {boolean} enable - Whether to enable or disable span coloring + */ +function applySpanHeatColors(enable) { + document.querySelectorAll('.instr-span').forEach(span => { + const samples = enable ? (parseInt(span.dataset.samples) || 0) : 0; + if (samples > 0) { + const intensity = samples / (parseInt(span.dataset.maxSamples) || 1); + span.style.backgroundColor = calculateHeatColor(intensity); + span.style.borderRadius = '2px'; + span.style.padding = '0 1px'; + span.style.cursor = 'pointer'; + } else { + span.style.cssText = ''; + } + }); +} + +// ======================================== +// SPAN TOOLTIPS +// ======================================== + +let activeTooltip = null; + +/** + * Create and show tooltip for a span + */ +function showSpanTooltip(span) { + hideSpanTooltip(); + + const samples = parseInt(span.dataset.samples) || 0; + const maxSamples = parseInt(span.dataset.maxSamples) || 1; + const pct = span.dataset.pct || '0'; + const opcodes = span.dataset.opcodes || ''; + + if (samples === 0) return; + + const intensity = samples / maxSamples; + const isHot = intensity > 0.7; + const isWarm = intensity > 0.3; + const hotnessText = isHot ? 'Hot' : isWarm ? 'Warm' : 'Cold'; + const hotnessClass = isHot ? 'hot' : isWarm ? 'warm' : 'cold'; + + // Build opcodes rows - each opcode on its own row + let opcodesHtml = ''; + if (opcodes) { + const opcodeList = opcodes.split(',').map(op => op.trim()).filter(op => op); + if (opcodeList.length > 0) { + opcodesHtml = ` +
    Opcodes:
    + ${opcodeList.map(op => `
    ${op}
    `).join('')} + `; + } + } + + const tooltip = document.createElement('div'); + tooltip.className = 'span-tooltip'; + tooltip.innerHTML = ` +
    ${hotnessText}
    +
    + Samples: + ${samples.toLocaleString()} +
    +
    + % of line: + ${pct}% +
    + ${opcodesHtml} + `; + + document.body.appendChild(tooltip); + activeTooltip = tooltip; + + // Position tooltip above the span + const rect = span.getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + + let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2); + let top = rect.top - tooltipRect.height - 8; + + // Keep tooltip in viewport + if (left < 5) left = 5; + if (left + tooltipRect.width > window.innerWidth - 5) { + left = window.innerWidth - tooltipRect.width - 5; + } + if (top < 5) { + top = rect.bottom + 8; // Show below if no room above + } + + tooltip.style.left = `${left + window.scrollX}px`; + tooltip.style.top = `${top + window.scrollY}px`; +} + +/** + * Hide active tooltip + */ +function hideSpanTooltip() { + if (activeTooltip) { + activeTooltip.remove(); + activeTooltip = null; + } +} + +/** + * Initialize span tooltip handlers + */ +function initSpanTooltips() { + document.addEventListener('mouseover', (e) => { + const span = e.target.closest('.instr-span'); + if (span && specViewEnabled) { + showSpanTooltip(span); + } + }); + + document.addEventListener('mouseout', (e) => { + const span = e.target.closest('.instr-span'); + if (span) { + hideSpanTooltip(); + } + }); +} + +function toggleSpecView() { + specViewEnabled = !specViewEnabled; + const lines = document.querySelectorAll('.code-line'); + + if (specViewEnabled) { + lines.forEach(line => { + const specColor = line.getAttribute('data-spec-color'); + line.style.background = specColor || 'transparent'; + }); + } else { + applyLineColors(); + } + + applySpanHeatColors(specViewEnabled); + updateToggleUI('toggle-spec-view', specViewEnabled); + + // Disable/enable color mode toggle based on spec view state + const colorModeToggle = document.getElementById('toggle-color-mode'); + if (colorModeToggle) { + colorModeToggle.classList.toggle('disabled', specViewEnabled); + } + + buildScrollMarker(); +} + +// ======================================== +// BYTECODE PANEL TOGGLE +// ======================================== + +/** + * Toggle bytecode panel visibility for a source line + * @param {HTMLElement} button - The toggle button that was clicked + */ +function toggleBytecode(button) { + const lineDiv = button.closest('.code-line'); + const lineId = lineDiv.id; + const lineNum = lineId.replace('line-', ''); + const panel = document.getElementById(`bytecode-${lineNum}`); + const wrapper = document.getElementById(`bytecode-wrapper-${lineNum}`); + + if (!panel || !wrapper) return; + + const isExpanded = panel.classList.contains('expanded'); + + if (isExpanded) { + panel.classList.remove('expanded'); + wrapper.classList.remove('expanded'); + button.classList.remove('expanded'); + button.innerHTML = '▶'; // Right arrow + } else { + if (!panel.dataset.populated) { + populateBytecodePanel(panel, button); + } + panel.classList.add('expanded'); + wrapper.classList.add('expanded'); + button.classList.add('expanded'); + button.innerHTML = '▼'; // Down arrow + } +} + +/** + * Populate bytecode panel with instruction data + * @param {HTMLElement} panel - The panel element to populate + * @param {HTMLElement} button - The button containing the bytecode data + */ +function populateBytecodePanel(panel, button) { + const bytecodeJson = button.getAttribute('data-bytecode'); + if (!bytecodeJson) return; + + // Get line number from parent + const lineDiv = button.closest('.code-line'); + const lineNum = lineDiv ? lineDiv.id.replace('line-', '') : null; + + try { + const instructions = JSON.parse(bytecodeJson); + if (!instructions.length) { + panel.innerHTML = '
    No bytecode data
    '; + panel.dataset.populated = 'true'; + return; + } + + const maxSamples = Math.max(...instructions.map(i => i.samples), 1); + + // Calculate specialization stats + const totalSamples = instructions.reduce((sum, i) => sum + i.samples, 0); + const specializedSamples = instructions + .filter(i => i.is_specialized) + .reduce((sum, i) => sum + i.samples, 0); + const specPct = totalSamples > 0 ? Math.round(100 * specializedSamples / totalSamples) : 0; + const specializedCount = instructions.filter(i => i.is_specialized).length; + + // Determine specialization level class + let specClass = 'low'; + if (specPct >= 67) specClass = 'high'; + else if (specPct >= 33) specClass = 'medium'; + + // Build specialization summary + const instruction_word = instructions.length === 1 ? 'instruction' : 'instructions'; + const sample_word = totalSamples === 1 ? 'sample' : 'samples'; + let html = `
    + ${specPct}% + specialized + (${specializedCount}/${instructions.length} ${instruction_word}, ${specializedSamples.toLocaleString()}/${totalSamples.toLocaleString()} ${sample_word}) +
    `; + + html += '
    ' + + 'Instruction' + + 'Samples' + + 'Heat
    '; + + for (const instr of instructions) { + const heatPct = (instr.samples / maxSamples) * 100; + const isHot = heatPct > 50; + const specializedClass = instr.is_specialized ? ' specialized' : ''; + const baseOpHtml = instr.is_specialized + ? `(${escapeHtml(instr.base_opname)})` : ''; + const badge = instr.is_specialized + ? 'SPECIALIZED' : ''; + + // Build location data attributes for cross-referencing with source spans + const hasLocations = instr.locations && instr.locations.length > 0; + const locationData = hasLocations + ? `data-locations='${JSON.stringify(instr.locations)}' data-line="${lineNum}" data-opcode="${instr.opcode}"` + : ''; + + html += `
    + ${escapeHtml(instr.opname)}${baseOpHtml}${badge} + ${instr.samples.toLocaleString()} +
    +
    `; + } + + panel.innerHTML = html; + panel.dataset.populated = 'true'; + + // Add hover handlers for bytecode instructions to highlight source spans + panel.querySelectorAll('.bytecode-instruction[data-locations]').forEach(instrEl => { + instrEl.addEventListener('mouseenter', highlightSourceFromBytecode); + instrEl.addEventListener('mouseleave', unhighlightSourceFromBytecode); + }); + } catch (e) { + panel.innerHTML = '
    Error loading bytecode
    '; + console.error('Error parsing bytecode data:', e); + } +} + +/** + * Highlight source spans when hovering over bytecode instruction + */ +function highlightSourceFromBytecode(e) { + const instrEl = e.currentTarget; + const lineNum = instrEl.dataset.line; + const locationsStr = instrEl.dataset.locations; + + if (!lineNum) return; + + const lineDiv = document.getElementById(`line-${lineNum}`); + if (!lineDiv) return; + + // Parse locations and highlight matching spans by column range + try { + const locations = JSON.parse(locationsStr || '[]'); + const spans = lineDiv.querySelectorAll('.instr-span'); + spans.forEach(span => { + const spanStart = parseInt(span.dataset.colStart); + const spanEnd = parseInt(span.dataset.colEnd); + for (const loc of locations) { + // Match if span's range matches instruction's location + if (spanStart === loc.col_offset && spanEnd === loc.end_col_offset) { + span.classList.add('highlight-from-bytecode'); + break; + } + } + }); + } catch (err) { + console.error('Error parsing locations:', err); + } + + // Also highlight the instruction row itself + instrEl.classList.add('highlight'); +} + +/** + * Remove highlighting from source spans + */ +function unhighlightSourceFromBytecode(e) { + const instrEl = e.currentTarget; + const lineNum = instrEl.dataset.line; + + if (!lineNum) return; + + const lineDiv = document.getElementById(`line-${lineNum}`); + if (!lineDiv) return; + + const spans = lineDiv.querySelectorAll('.instr-span.highlight-from-bytecode'); + spans.forEach(span => { + span.classList.remove('highlight-from-bytecode'); + }); + + instrEl.classList.remove('highlight'); +} + +/** + * Escape HTML special characters + * @param {string} text - Text to escape + * @returns {string} Escaped HTML + */ +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +/** + * Toggle all bytecode panels at once + */ +function toggleAllBytecode() { + const buttons = document.querySelectorAll('.bytecode-toggle'); + if (buttons.length === 0) return; + + const someExpanded = Array.from(buttons).some(b => b.classList.contains('expanded')); + const expandAllBtn = document.getElementById('toggle-all-bytecode'); + + buttons.forEach(button => { + const isExpanded = button.classList.contains('expanded'); + if (someExpanded ? isExpanded : !isExpanded) { + toggleBytecode(button); + } + }); + + // Update the expand-all button state + if (expandAllBtn) { + expandAllBtn.classList.toggle('expanded', !someExpanded); + } +} + +// Keyboard shortcut: 'b' toggles all bytecode panels, Enter/Space activates toggle switches +document.addEventListener('keydown', function(e) { + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + return; + } + if (e.key === 'b' && !e.ctrlKey && !e.altKey && !e.metaKey) { + toggleAllBytecode(); + } + if ((e.key === 'Enter' || e.key === ' ') && e.target.classList.contains('toggle-switch')) { + e.preventDefault(); + e.target.click(); + } +}); + +// Handle hash changes +window.addEventListener('hashchange', () => setTimeout(scrollToTargetLine, 50)); + +// Rebuild scroll marker on resize +window.addEventListener('resize', buildScrollMarker); diff --git a/Lib/profiling/sampling/_heatmap_assets/heatmap_index.js b/Lib/profiling/sampling/_heatmap_assets/heatmap_index.js new file mode 100644 index 00000000000..db4b5485056 --- /dev/null +++ b/Lib/profiling/sampling/_heatmap_assets/heatmap_index.js @@ -0,0 +1,104 @@ +// Tachyon Profiler - Heatmap Index JavaScript +// Index page specific functionality + +// ============================================================================ +// Heatmap Bar Coloring +// ============================================================================ + +function applyHeatmapBarColors() { + const bars = document.querySelectorAll('.heatmap-bar[data-intensity]'); + bars.forEach(bar => { + const intensity = parseFloat(bar.getAttribute('data-intensity')) || 0; + const color = intensityToColor(intensity); + bar.style.backgroundColor = color; + }); +} + +// ============================================================================ +// Theme Support +// ============================================================================ + +function toggleTheme() { + toggleAndSaveTheme(); + applyHeatmapBarColors(); +} + +// ============================================================================ +// Type Section Toggle (stdlib, project, etc) +// ============================================================================ + +function toggleTypeSection(header) { + const section = header.parentElement; + const content = section.querySelector('.type-content'); + const icon = header.querySelector('.type-icon'); + + if (content.style.display === 'none') { + content.style.display = 'block'; + icon.textContent = '\u25BC'; + } else { + content.style.display = 'none'; + icon.textContent = '\u25B6'; + } +} + +// ============================================================================ +// Folder Toggle +// ============================================================================ + +function toggleFolder(header) { + const folder = header.parentElement; + const content = folder.querySelector('.folder-content'); + const icon = header.querySelector('.folder-icon'); + + if (content.style.display === 'none') { + content.style.display = 'block'; + icon.textContent = '\u25BC'; + folder.classList.remove('collapsed'); + } else { + content.style.display = 'none'; + icon.textContent = '\u25B6'; + folder.classList.add('collapsed'); + } +} + +// ============================================================================ +// Expand/Collapse All +// ============================================================================ + +function expandAll() { + // Expand all type sections + document.querySelectorAll('.type-section').forEach(section => { + const content = section.querySelector('.type-content'); + const icon = section.querySelector('.type-icon'); + content.style.display = 'block'; + icon.textContent = '\u25BC'; + }); + + // Expand all folders + document.querySelectorAll('.folder-node').forEach(folder => { + const content = folder.querySelector('.folder-content'); + const icon = folder.querySelector('.folder-icon'); + content.style.display = 'block'; + icon.textContent = '\u25BC'; + folder.classList.remove('collapsed'); + }); +} + +function collapseAll() { + document.querySelectorAll('.folder-node').forEach(folder => { + const content = folder.querySelector('.folder-content'); + const icon = folder.querySelector('.folder-icon'); + content.style.display = 'none'; + icon.textContent = '\u25B6'; + folder.classList.add('collapsed'); + }); +} + +// ============================================================================ +// Initialization +// ============================================================================ + +document.addEventListener('DOMContentLoaded', function() { + restoreUIState(); + applyHeatmapBarColors(); +}); diff --git a/Lib/profiling/sampling/_heatmap_assets/heatmap_index_template.html b/Lib/profiling/sampling/_heatmap_assets/heatmap_index_template.html new file mode 100644 index 00000000000..8d04149abe3 --- /dev/null +++ b/Lib/profiling/sampling/_heatmap_assets/heatmap_index_template.html @@ -0,0 +1,136 @@ + + + + + + Tachyon Profiler - Heatmap Report + + + +
    + +
    +
    + + Tachyon + + Heatmap Report +
    +
    + + + + + + +
    +
    + + +
    + +
    +
    +
    📄
    +
    + + Files Profiled +
    +
    +
    +
    +
    📊
    +
    + + Total Snapshots +
    +
    +
    +
    +
    ⏱
    +
    + + Duration +
    +
    +
    +
    +
    ⚡
    +
    + + Samples/sec +
    +
    +
    +
    +
    +
    +
    ⚠
    + Error Rate +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    💥
    + Missed Samples +
    + +
    +
    +
    +
    +
    +
    + + +
    +

    Profiled Files

    +
    + +
    + + +
    + +
    + +
    +
    + + +
    + + Tachyon Profiler + + + Python Sampling Profiler + +
    +
    + + + + diff --git a/Lib/profiling/sampling/_heatmap_assets/heatmap_pyfile_template.html b/Lib/profiling/sampling/_heatmap_assets/heatmap_pyfile_template.html new file mode 100644 index 00000000000..2a9c07647e6 --- /dev/null +++ b/Lib/profiling/sampling/_heatmap_assets/heatmap_pyfile_template.html @@ -0,0 +1,131 @@ + + + + + + <!-- FILENAME --> - Heatmap + + + +
    + +
    +
    + + Tachyon + + +
    +
    + + + + + + + + + + + +
    +
    + + +
    +
    +
    +
    +
    Self Samples
    +
    +
    +
    +
    Cumulative
    +
    +
    +
    +
    Lines Hit
    +
    +
    +
    %
    +
    % of Total
    +
    +
    +
    +
    Max Self
    +
    +
    +
    +
    Max Total
    +
    +
    +
    + + +
    +
    + Intensity: +
    +
    + Cold + + Hot +
    + +
    +
    + Self Time +
    + Total Time +
    +
    + Show All +
    + Hot Only +
    +
    + Heat +
    + Specialization +
    + + +
    +
    +
    + + +
    +
    +
    Line
    +
    Self
    +
    Total
    +
    Code
    +
    + +
    +
    + + + + diff --git a/Lib/profiling/sampling/_heatmap_assets/heatmap_shared.js b/Lib/profiling/sampling/_heatmap_assets/heatmap_shared.js new file mode 100644 index 00000000000..fb761335876 --- /dev/null +++ b/Lib/profiling/sampling/_heatmap_assets/heatmap_shared.js @@ -0,0 +1,64 @@ +// Tachyon Profiler - Shared Heatmap JavaScript +// Common utilities shared between index and file views + +// ============================================================================ +// Heat Level Mapping (Single source of truth for intensity thresholds) +// ============================================================================ + +// Maps intensity (0-1) to heat level (0-8). Level 0 = no heat, 1-8 = heat levels. +function intensityToHeatLevel(intensity) { + if (intensity <= 0) return 0; + if (intensity <= 0.125) return 1; + if (intensity <= 0.25) return 2; + if (intensity <= 0.375) return 3; + if (intensity <= 0.5) return 4; + if (intensity <= 0.625) return 5; + if (intensity <= 0.75) return 6; + if (intensity <= 0.875) return 7; + return 8; +} + +// Class names corresponding to heat levels 1-8 (used by scroll marker) +const HEAT_CLASS_NAMES = ['cold', 'cool', 'mild', 'warm', 'hot', 'very-hot', 'intense', 'extreme']; + +function intensityToClass(intensity) { + const level = intensityToHeatLevel(intensity); + return level === 0 ? null : HEAT_CLASS_NAMES[level - 1]; +} + +// ============================================================================ +// Color Mapping (Intensity to Heat Color) +// ============================================================================ + +function intensityToColor(intensity) { + const level = intensityToHeatLevel(intensity); + if (level === 0) { + return 'transparent'; + } + const rootStyle = getComputedStyle(document.documentElement); + return rootStyle.getPropertyValue(`--heat-${level}`).trim(); +} + +// ============================================================================ +// Theme Support +// ============================================================================ + +// Restore theme from localStorage, or use browser preference +function restoreUIState() { + applyTheme(getPreferredTheme()); +} + +// ============================================================================ +// Favicon (Reuse logo image as favicon) +// ============================================================================ + +(function() { + const logo = document.querySelector('.brand-logo img'); + if (logo) { + const favicon = document.createElement('link'); + favicon.rel = 'icon'; + favicon.type = 'image/png'; + favicon.href = logo.src; + document.head.appendChild(favicon); + } +})(); diff --git a/Lib/profiling/sampling/_shared_assets/base.css b/Lib/profiling/sampling/_shared_assets/base.css new file mode 100644 index 00000000000..2164f3f3aa1 --- /dev/null +++ b/Lib/profiling/sampling/_shared_assets/base.css @@ -0,0 +1,514 @@ +/* ========================================================================== + Python Profiler - Shared CSS Foundation + Design system shared between Flamegraph and Heatmap viewers + ========================================================================== */ + +/* -------------------------------------------------------------------------- + CSS Variables & Theme System + -------------------------------------------------------------------------- */ + +:root { + /* Typography */ + --font-sans: "Source Sans Pro", "Lucida Grande", "Lucida Sans Unicode", + "Geneva", "Verdana", sans-serif; + --font-mono: 'SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', monospace; + + /* Python brand colors (theme-independent) */ + --python-blue: #3776ab; + --python-blue-light: #4584bb; + --python-blue-lighter: #5592cc; + --python-gold: #ffd43b; + --python-gold-dark: #ffcd02; + --python-gold-light: #ffdc5c; + + /* Heat palette - defined per theme below */ + + /* Layout */ + --sidebar-width: 280px; + --sidebar-collapsed: 44px; + --topbar-height: 56px; + --statusbar-height: 32px; + + /* Border radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + + /* Transitions */ + --transition-fast: 0.15s ease; + --transition-normal: 0.25s ease; + --transition-slow: 0.3s ease; +} + +/* Light theme (default) */ +:root, [data-theme="light"] { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --bg-tertiary: #e9ecef; + --border: #e9ecef; + --border-subtle: #f0f2f5; + + --text-primary: #2e3338; + --text-secondary: #5a6c7d; + --text-muted: #6f767e; + + --accent: #3776ab; + --accent-hover: #2d5aa0; + --accent-glow: rgba(55, 118, 171, 0.15); + + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.15); + + --header-gradient: linear-gradient(135deg, #3776ab 0%, #4584bb 100%); + + /* Light mode heat palette - blue to yellow to orange to red (cold to hot) */ + --heat-1: #7ba3d1; + --heat-2: #a8d0ef; + --heat-3: #d6e9f8; + --heat-4: #ffe6a8; + --heat-5: #ffd43b; + --heat-6: #ffb84d; + --heat-7: #ff9966; + --heat-8: #ff6347; + + /* Code view specific */ + --code-bg: #ffffff; + --code-bg-line: #f8f9fa; + --code-border: #e9ecef; + --code-text: #2e3338; + --code-text-muted: #8b949e; + --code-accent: #3776ab; + + /* Navigation colors */ + --nav-caller: #2563eb; + --nav-caller-hover: #1d4ed8; + --nav-callee: #dc2626; + --nav-callee-hover: #b91c1c; + + /* Specialization status colors */ + --spec-high: #4caf50; + --spec-high-text: #2e7d32; + --spec-high-bg: rgba(76, 175, 80, 0.15); + --spec-medium: #ff9800; + --spec-medium-text: #e65100; + --spec-medium-bg: rgba(255, 152, 0, 0.15); + --spec-low: #9e9e9e; + --spec-low-text: #616161; + --spec-low-bg: rgba(158, 158, 158, 0.15); + + /* Heatmap span highlighting colors */ + --span-hot-base: 255, 100, 50; + --span-cold-base: 150, 150, 150; + + /* Summary card colors - optimized for 4.5:1 contrast on light bg */ + --card-blue: 44, 102, 149; + --card-green: 26, 116, 49; + --card-yellow: 134, 100, 4; + --card-purple: 102, 57, 166; + --card-red: 180, 40, 50; + --card-orange: 166, 90, 0; +} + +/* Dark theme */ +[data-theme="dark"] { + --bg-primary: #0d1117; + --bg-secondary: #161b22; + --bg-tertiary: #21262d; + --border: #30363d; + --border-subtle: #21262d; + + --text-primary: #e6edf3; + --text-secondary: #8b949e; + --text-muted: #757e8a; + + --accent: #58a6ff; + --accent-hover: #79b8ff; + --accent-glow: rgba(88, 166, 255, 0.15); + + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5); + + --header-gradient: linear-gradient(135deg, #21262d 0%, #30363d 100%); + + /* Dark mode heat palette - cool to warm gradient for visualization */ + --heat-1: rgba(90, 123, 167, 1); + --heat-2: rgba(106, 148, 168, 1); + --heat-3: rgba(122, 172, 172, 1); + --heat-4: rgba(142, 196, 152, 1); + --heat-5: rgba(168, 216, 136, 1); + --heat-6: rgba(200, 222, 122, 1); + --heat-7: rgba(244, 212, 93, 1); + --heat-8: rgba(255, 122, 69, 1); + + /* Code view specific - dark mode */ + --code-bg: #0d1117; + --code-bg-line: #161b22; + --code-border: #30363d; + --code-text: #e6edf3; + --code-text-muted: #6e7681; + --code-accent: #58a6ff; + + /* Navigation colors - dark theme friendly */ + --nav-caller: #58a6ff; + --nav-caller-hover: #4184e4; + --nav-callee: #f87171; + --nav-callee-hover: #e53e3e; + + /* Specialization status colors - dark theme */ + --spec-high: #81c784; + --spec-high-text: #81c784; + --spec-high-bg: rgba(129, 199, 132, 0.2); + --spec-medium: #ffb74d; + --spec-medium-text: #ffb74d; + --spec-medium-bg: rgba(255, 183, 77, 0.2); + --spec-low: #bdbdbd; + --spec-low-text: #9e9e9e; + --spec-low-bg: rgba(189, 189, 189, 0.15); + + /* Heatmap span highlighting colors - dark theme */ + --span-hot-base: 255, 107, 53; + --span-cold-base: 189, 189, 189; + + /* Summary card colors - optimized for 4.5:1 contrast on dark bg */ + --card-blue: 88, 166, 255; + --card-green: 63, 185, 80; + --card-yellow: 210, 153, 34; + --card-purple: 163, 113, 247; + --card-red: 248, 113, 113; + --card-orange: 251, 146, 60; +} + +/* -------------------------------------------------------------------------- + Base Styles + -------------------------------------------------------------------------- */ + +*, *::before, *::after { + box-sizing: border-box; +} + +html, body { + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-primary); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + transition: background var(--transition-normal), color var(--transition-normal); +} + +/* -------------------------------------------------------------------------- + Layout Structure + -------------------------------------------------------------------------- */ + +.app-layout { + display: flex; + flex-direction: column; +} + +/* -------------------------------------------------------------------------- + Top Bar + -------------------------------------------------------------------------- */ + +.top-bar { + height: var(--topbar-height); + background: var(--header-gradient); + display: flex; + align-items: center; + padding: 0 16px; + gap: 16px; + flex-shrink: 0; + box-shadow: 0 2px 10px rgba(55, 118, 171, 0.25); + border-bottom: 2px solid var(--python-gold); +} + +/* Brand / Logo */ +.brand { + display: flex; + align-items: center; + gap: 12px; + color: white; + text-decoration: none; + flex-shrink: 0; +} + +.brand-logo { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 40px; + flex-shrink: 0; +} + +/* Style the inlined SVG/img inside brand-logo */ +.brand-logo svg, +.brand-logo img { + width: 48px; + height: 40px; + display: block; + object-fit: contain; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2)); +} + +.brand-info { + display: flex; + flex-direction: column; + line-height: 1.15; +} + +.brand-text { + font-family: var(--font-sans); + font-weight: 700; + font-size: 16px; + letter-spacing: -0.3px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); + color: inherit; + text-decoration: none; +} + +.brand-subtitle { + font-weight: 500; + font-size: 10px; + opacity: 0.9; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.brand-divider { + width: 1px; + height: 16px; + background: rgba(255, 255, 255, 0.3); +} + +/* Toolbar */ +.toolbar { + display: flex; + align-items: center; + gap: 6px; + margin-left: auto; +} + +.toolbar-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + font-size: 15px; + color: white; + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 6px; + cursor: pointer; + text-decoration: none; + transition: all var(--transition-fast); +} + +.toolbar-btn:hover { + background: rgba(255, 255, 255, 0.22); + border-color: rgba(255, 255, 255, 0.35); +} + +.toolbar-btn:active { + transform: scale(0.95); +} + +/* -------------------------------------------------------------------------- + Status Bar + -------------------------------------------------------------------------- */ + +.status-bar { + height: var(--statusbar-height); + background: var(--bg-secondary); + border-top: 1px solid var(--border); + display: flex; + align-items: center; + padding: 0 16px; + gap: 16px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-secondary); + flex-shrink: 0; +} + +.status-item { + display: flex; + align-items: center; + gap: 5px; +} + +.status-item::before { + content: ''; + width: 4px; + height: 4px; + background: var(--python-gold); + border-radius: 50%; +} + +.status-item:first-child::before { + display: none; +} + +.status-label { + color: var(--text-muted); +} + +.status-value { + color: var(--text-primary); + font-weight: 500; +} + +.status-value.accent { + color: var(--accent); + font-weight: 600; +} + +/* -------------------------------------------------------------------------- + Animations + -------------------------------------------------------------------------- */ + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes shimmer { + 0% { left: -100%; } + 100% { left: 100%; } +} + +/* -------------------------------------------------------------------------- + Focus States (Accessibility) + -------------------------------------------------------------------------- */ + +button:focus-visible, +select:focus-visible, +input:focus-visible, +.toggle-switch:focus-visible, +a.toolbar-btn:focus-visible { + outline: 2px solid var(--python-gold); + outline-offset: 2px; +} + +/* -------------------------------------------------------------------------- + Shared Responsive + -------------------------------------------------------------------------- */ + +@media (max-width: 900px) { + .brand-subtitle { + display: none; + } +} + +@media (max-width: 600px) { + .toolbar-btn:not(.theme-toggle) { + display: none; + } +} + +/* -------------------------------------------------------------------------- + Toggle Switch + -------------------------------------------------------------------------- */ + +.toggle-switch { + display: inline-flex; + align-items: center; + gap: 8px; + cursor: pointer; + user-select: none; + font-family: var(--font-sans); + transition: opacity var(--transition-fast); + flex-shrink: 0; +} + +.toggle-switch:hover { + opacity: 0.85; +} + +.toggle-switch .toggle-label { + font-size: 11px; + font-weight: 500; + color: var(--text-muted); + transition: color var(--transition-fast); + white-space: nowrap; + display: inline-flex; + flex-direction: column; +} + +.toggle-switch .toggle-label.active { + color: var(--text-primary); + font-weight: 600; +} + +/* Reserve space for bold text to prevent layout shift on toggle */ +.toggle-switch .toggle-label::after { + content: attr(data-text); + font-weight: 600; + height: 0; + visibility: hidden; +} + +.toggle-switch.disabled { + opacity: 0.4; + pointer-events: none; + cursor: not-allowed; +} + +.toggle-track { + position: relative; + width: 36px; + height: 20px; + background: var(--bg-tertiary); + border: 2px solid var(--border); + border-radius: 12px; + transition: all var(--transition-fast); + box-shadow: inset var(--shadow-sm); +} + +.toggle-track:hover { + border-color: var(--text-muted); +} + +.toggle-track.on { + background: var(--accent); + border-color: var(--accent); + box-shadow: 0 0 8px var(--accent-glow); +} + +.toggle-track::after { + content: ''; + position: absolute; + top: 1px; + left: 1px; + width: 14px; + height: 14px; + background: white; + border-radius: 50%; + box-shadow: var(--shadow-sm); + transition: all var(--transition-fast); +} + +.toggle-track.on::after { + transform: translateX(16px); + box-shadow: var(--shadow-md); +} diff --git a/Lib/profiling/sampling/_shared_assets/base.js b/Lib/profiling/sampling/_shared_assets/base.js new file mode 100644 index 00000000000..da8b5851c85 --- /dev/null +++ b/Lib/profiling/sampling/_shared_assets/base.js @@ -0,0 +1,58 @@ +// Tachyon Profiler - Shared JavaScript +// Common utilities shared between flamegraph and heatmap views + +// ============================================================================ +// Theme Support +// ============================================================================ + +// Storage key for theme preference +const THEME_STORAGE_KEY = 'tachyon-theme'; + +// Get the preferred theme from localStorage or system preference +function getPreferredTheme() { + const saved = localStorage.getItem(THEME_STORAGE_KEY); + if (saved) return saved; + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +// Apply theme and update UI +function applyTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + const btn = document.getElementById('theme-btn'); + if (btn) { + const moonIcon = btn.querySelector('.icon-moon'); + const sunIcon = btn.querySelector('.icon-sun'); + if (moonIcon) moonIcon.style.display = theme === 'dark' ? 'none' : ''; + if (sunIcon) sunIcon.style.display = theme === 'dark' ? '' : 'none'; + } +} + +// Toggle theme and save preference. Returns the new theme. +function toggleAndSaveTheme() { + const current = document.documentElement.getAttribute('data-theme') || 'light'; + const next = current === 'light' ? 'dark' : 'light'; + applyTheme(next); + localStorage.setItem(THEME_STORAGE_KEY, next); + return next; +} + +// ============================================================================ +// Toggle Switch UI +// ============================================================================ + +function updateToggleUI(toggleId, isOn) { + const toggle = document.getElementById(toggleId); + if (toggle) { + const track = toggle.querySelector('.toggle-track'); + const labels = toggle.querySelectorAll('.toggle-label'); + if (isOn) { + track.classList.add('on'); + labels[0].classList.remove('active'); + labels[1].classList.add('active'); + } else { + track.classList.remove('on'); + labels[0].classList.add('active'); + labels[1].classList.remove('active'); + } + } +} diff --git a/Lib/profiling/sampling/_sync_coordinator.py b/Lib/profiling/sampling/_sync_coordinator.py new file mode 100644 index 00000000000..a1cce314b33 --- /dev/null +++ b/Lib/profiling/sampling/_sync_coordinator.py @@ -0,0 +1,256 @@ +""" +Internal synchronization coordinator for the sample profiler. + +This module is used internally by the sample profiler to coordinate +the startup of target processes. It should not be called directly by users. +""" + +import importlib.util +import os +import sys +import socket +import runpy +import time +import types +from typing import List, NoReturn + + +class CoordinatorError(Exception): + """Base exception for coordinator errors.""" + pass + + +class ArgumentError(CoordinatorError): + """Raised when invalid arguments are provided.""" + pass + + +class SyncError(CoordinatorError): + """Raised when synchronization with profiler fails.""" + pass + + +class TargetError(CoordinatorError): + """Raised when target execution fails.""" + pass + + +def _validate_arguments(args: List[str]) -> tuple[int, str, List[str]]: + """ + Validate and parse command line arguments. + + Args: + args: Command line arguments including script name + + Returns: + Tuple of (sync_port, working_directory, target_args) + + Raises: + ArgumentError: If arguments are invalid + """ + if len(args) < 4: + raise ArgumentError( + "Insufficient arguments. Expected: [args...]" + ) + + try: + sync_port = int(args[1]) + if not (1 <= sync_port <= 65535): + raise ValueError("Port out of range") + except ValueError as e: + raise ArgumentError(f"Invalid sync port '{args[1]}': {e}") from e + + cwd = args[2] + if not os.path.isdir(cwd): + raise ArgumentError(f"Working directory does not exist: {cwd}") + + target_args = args[3:] + if not target_args: + raise ArgumentError("No target specified") + + return sync_port, cwd, target_args + + +# Constants for socket communication +_MAX_RETRIES = 3 +_INITIAL_RETRY_DELAY_SEC = 0.1 +_SOCKET_TIMEOUT_SEC = 2.0 +_READY_MESSAGE = b"ready" + + +def _signal_readiness(sync_port: int) -> None: + """ + Signal readiness to the profiler via TCP socket. + + Args: + sync_port: Port number where profiler is listening + + Raises: + SyncError: If unable to signal readiness + """ + last_error = None + + for attempt in range(_MAX_RETRIES): + try: + # Use context manager for automatic cleanup + with socket.create_connection(("127.0.0.1", sync_port), timeout=_SOCKET_TIMEOUT_SEC) as sock: + sock.send(_READY_MESSAGE) + return + except (socket.error, OSError) as e: + last_error = e + if attempt < _MAX_RETRIES - 1: + # Exponential backoff before retry + time.sleep(_INITIAL_RETRY_DELAY_SEC * (2 ** attempt)) + + # If we get here, all retries failed + raise SyncError(f"Failed to signal readiness after {_MAX_RETRIES} attempts: {last_error}") from last_error + + +def _setup_environment(cwd: str) -> None: + """ + Set up the execution environment. + + Args: + cwd: Working directory to change to + + Raises: + TargetError: If unable to set up environment + """ + try: + os.chdir(cwd) + except OSError as e: + raise TargetError(f"Failed to change to directory {cwd}: {e}") from e + + # Add current directory to sys.path if not present (for module imports) + if cwd not in sys.path: + sys.path.insert(0, cwd) + + +def _execute_module(module_name: str, module_args: List[str]) -> None: + """ + Execute a Python module. + + Args: + module_name: Name of the module to execute + module_args: Arguments to pass to the module + + Raises: + TargetError: If module cannot be found + """ + # Replace sys.argv to match how Python normally runs modules + # When running 'python -m module args', sys.argv is ["__main__.py", "args"] + sys.argv = ["__main__.py"] + module_args + + try: + runpy.run_module(module_name, run_name="__main__", alter_sys=True) + except ImportError as e: + raise TargetError(f"Module '{module_name}' not found: {e}") from e + # Let other exceptions (including SystemExit) propagate naturally + # so Python prints the full traceback to stderr + + +def _execute_script(script_path: str, script_args: List[str], cwd: str) -> None: + """ + Execute a Python script. + + Args: + script_path: Path to the script to execute + script_args: Arguments to pass to the script + cwd: Current working directory for path resolution + + Raises: + TargetError: If script execution fails + """ + # Make script path absolute if it isn't already + if not os.path.isabs(script_path): + script_path = os.path.join(cwd, script_path) + + if not os.path.isfile(script_path): + raise TargetError(f"Script not found: {script_path}") + + # Replace sys.argv to match original script call + sys.argv = [script_path] + script_args + + try: + with open(script_path, 'rb') as f: + source_code = f.read() + + except FileNotFoundError as e: + raise TargetError(f"Script file not found: {script_path}") from e + except PermissionError as e: + raise TargetError(f"Permission denied reading script: {script_path}") from e + + main_module = types.ModuleType("__main__") + main_module.__file__ = script_path + main_module.__builtins__ = __builtins__ + # gh-140729: Create a __mp_main__ module to allow pickling + sys.modules['__main__'] = sys.modules['__mp_main__'] = main_module + + try: + code = compile(source_code, script_path, 'exec', module='__main__') + except SyntaxError as e: + raise TargetError(f"Syntax error in script {script_path}: {e}") from e + + # Execute the script - let exceptions propagate naturally so Python + # prints the full traceback to stderr + exec(code, main_module.__dict__) + + +def main() -> NoReturn: + """ + Main coordinator function. + + This function coordinates the startup of a target Python process + with the sample profiler by signaling when the process is ready + to be profiled. + """ + # Phase 1: Parse arguments and set up environment + # Errors here are coordinator errors, not script errors + try: + # Parse and validate arguments + sync_port, cwd, target_args = _validate_arguments(sys.argv) + + # Set up execution environment + _setup_environment(cwd) + + # Determine execution type and validate target exists + is_module = target_args[0] == "-m" + if is_module: + if len(target_args) < 2: + raise ArgumentError("Module name required after -m") + module_name = target_args[1] + module_args = target_args[2:] + + if importlib.util.find_spec(module_name) is None: + raise TargetError(f"Module not found: {module_name}") + else: + script_path = target_args[0] + script_args = target_args[1:] + # Match the path resolution logic in _execute_script + check_path = script_path if os.path.isabs(script_path) else os.path.join(cwd, script_path) + if not os.path.isfile(check_path): + raise TargetError(f"Script not found: {script_path}") + + # Signal readiness to profiler + _signal_readiness(sync_port) + + except CoordinatorError as e: + print(f"Profiler coordinator error: {e}", file=sys.stderr) + sys.exit(1) + except KeyboardInterrupt: + print("Interrupted", file=sys.stderr) + sys.exit(1) + + # Phase 2: Execute the target script/module + # Let exceptions propagate naturally so Python prints full tracebacks + if is_module: + _execute_module(module_name, module_args) + else: + _execute_script(script_path, script_args, cwd) + + # Normal exit + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/Lib/profiling/sampling/_vendor/d3-flame-graph/4.1.3/d3-flamegraph-tooltip.min.js b/Lib/profiling/sampling/_vendor/d3-flame-graph/4.1.3/d3-flamegraph-tooltip.min.js new file mode 100644 index 00000000000..7468437093b --- /dev/null +++ b/Lib/profiling/sampling/_vendor/d3-flame-graph/4.1.3/d3-flamegraph-tooltip.min.js @@ -0,0 +1 @@ +!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.flamegraph=n():(t.flamegraph=t.flamegraph||{},t.flamegraph.tooltip=n())}(self,(function(){return(()=>{"use strict";var t={d:(n,e)=>{for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},o:(t,n)=>Object.prototype.hasOwnProperty.call(t,n),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},n={};function e(){}function r(t){return null==t?e:function(){return this.querySelector(t)}}function i(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function o(){return[]}function u(t){return null==t?o:function(){return this.querySelectorAll(t)}}function a(t){return function(){return this.matches(t)}}function s(t){return function(n){return n.matches(t)}}t.r(n),t.d(n,{defaultFlamegraphTooltip:()=>Ae});var l=Array.prototype.find;function c(){return this.firstElementChild}var f=Array.prototype.filter;function h(){return Array.from(this.children)}function p(t){return new Array(t.length)}function d(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function y(t){return function(){return t}}function v(t,n,e,r,i,o){for(var u,a=0,s=n.length,l=o.length;an?1:t>=n?0:NaN}d.prototype={constructor:d,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,n){return this._parent.insertBefore(t,n)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var b="http://www.w3.org/1999/xhtml";const x={svg:"http://www.w3.org/2000/svg",xhtml:b,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function A(t){var n=t+="",e=n.indexOf(":");return e>=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),x.hasOwnProperty(n)?{space:x[n],local:t}:t}function N(t){return function(){this.removeAttribute(t)}}function k(t){return function(){this.removeAttributeNS(t.space,t.local)}}function M(t,n){return function(){this.setAttribute(t,n)}}function E(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function S(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function C(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function P(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function O(t){return function(){this.style.removeProperty(t)}}function q(t,n,e){return function(){this.style.setProperty(t,n,e)}}function j(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function T(t,n){return t.style.getPropertyValue(n)||P(t).getComputedStyle(t,null).getPropertyValue(n)}function X(t){return function(){delete this[t]}}function R(t,n){return function(){this[t]=n}}function I(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function L(t){return t.trim().split(/^|\s+/)}function D(t){return t.classList||new H(t)}function H(t){this._node=t,this._names=L(t.getAttribute("class")||"")}function Y(t,n){for(var e=D(t),r=-1,i=n.length;++r=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}}))}function st(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var pt=[null];function dt(t,n){this._groups=t,this._parents=n}function yt(){return new dt([[document.documentElement]],pt)}dt.prototype=yt.prototype={constructor:dt,select:function(t){"function"!=typeof t&&(t=r(t));for(var n=this._groups,e=n.length,i=new Array(e),o=0;o=M&&(M=k+1);!(N=b[M])&&++M=0;)(r=i[o])&&(u&&4^r.compareDocumentPosition(u)&&u.parentNode.insertBefore(r,u),u=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=w);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?O:"function"==typeof n?j:q)(t,n,null==e?"":e)):T(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?X:"function"==typeof n?I:R)(t,n)):this.node()[t]},classed:function(t,n){var e=L(t+"");if(arguments.length<2){for(var r=D(this.node()),i=-1,o=e.length;++i{}};function _t(){for(var t,n=0,e=arguments.length,r={};n=0&&(e=t.slice(r+1),t=t.slice(0,r)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}}))}function bt(t,n){for(var e,r=0,i=t.length;r0)for(var e,r,i=new Array(e),o=0;o=0&&n._call.call(void 0,t),n=n._next;--Mt}()}finally{Mt=0,function(){var t,n,e=Nt,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:Nt=n);kt=t,Ht(r)}(),Pt=0}}function Dt(){var t=qt.now(),n=t-Ct;n>1e3&&(Ot-=n,Ct=t)}function Ht(t){Mt||(Et&&(Et=clearTimeout(Et)),t-Pt>24?(t<1/0&&(Et=setTimeout(Lt,t-qt.now()-Ot)),St&&(St=clearInterval(St))):(St||(Ct=qt.now(),St=setInterval(Dt,1e3)),Mt=1,jt(Lt)))}function Yt(t,n,e){var r=new Rt;return n=null==n?0:+n,r.restart((e=>{r.stop(),t(e+n)}),n,e),r}Rt.prototype=It.prototype={constructor:Rt,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?Tt():+e)+(null==n?0:+n),this._next||kt===this||(kt?kt._next=this:Nt=this,kt=this),this._call=t,this._time=e,Ht()},stop:function(){this._call&&(this._call=null,this._time=1/0,Ht())}};var Bt=At("start","end","cancel","interrupt"),$t=[];function Vt(t,n,e,r,i,o){var u=t.__transition;if(u){if(e in u)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(t){e.state=1,e.timer.restart(u,e.delay,e.time),e.delay<=t&&u(t-e.delay)}function u(o){var l,c,f,h;if(1!==e.state)return s();for(l in i)if((h=i[l]).name===e.name){if(3===h.state)return Yt(u);4===h.state?(h.state=6,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete i[l]):+l0)throw new Error("too late; already scheduled");return e}function Ut(t,n){var e=Ft(t,n);if(e.state>3)throw new Error("too late; already running");return e}function Ft(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function Kt(t,n){return t=+t,n=+n,function(e){return t*(1-e)+n*e}}var Wt,Gt=180/Math.PI,Jt={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1};function Qt(t,n,e,r,i,o){var u,a,s;return(u=Math.sqrt(t*t+n*n))&&(t/=u,n/=u),(s=t*e+n*r)&&(e-=t*s,r-=n*s),(a=Math.sqrt(e*e+r*r))&&(e/=a,r/=a,s/=a),t*r180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:Kt(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,u.rotate,a,s),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:Kt(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,u.skewX,a,s),function(t,n,e,r,o,u){if(t!==e||n!==r){var a=o.push(i(o)+"scale(",null,",",null,")");u.push({i:a-4,x:Kt(t,e)},{i:a-2,x:Kt(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,u.scaleX,u.scaleY,a,s),o=u=null,function(t){for(var n,e=-1,r=s.length;++e>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):8===e?Mn(n>>24&255,n>>16&255,n>>8&255,(255&n)/255):4===e?Mn(n>>12&15|n>>8&240,n>>8&15|n>>4&240,n>>4&15|240&n,((15&n)<<4|15&n)/255):null):(n=yn.exec(t))?new Cn(n[1],n[2],n[3],1):(n=vn.exec(t))?new Cn(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=gn.exec(t))?Mn(n[1],n[2],n[3],n[4]):(n=_n.exec(t))?Mn(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=mn.exec(t))?jn(n[1],n[2]/100,n[3]/100,1):(n=wn.exec(t))?jn(n[1],n[2]/100,n[3]/100,n[4]):bn.hasOwnProperty(t)?kn(bn[t]):"transparent"===t?new Cn(NaN,NaN,NaN,0):null}function kn(t){return new Cn(t>>16&255,t>>8&255,255&t,1)}function Mn(t,n,e,r){return r<=0&&(t=n=e=NaN),new Cn(t,n,e,r)}function En(t){return t instanceof sn||(t=Nn(t)),t?new Cn((t=t.rgb()).r,t.g,t.b,t.opacity):new Cn}function Sn(t,n,e,r){return 1===arguments.length?En(t):new Cn(t,n,e,null==r?1:r)}function Cn(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function Pn(){return"#"+qn(this.r)+qn(this.g)+qn(this.b)}function On(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function qn(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function jn(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Xn(t,n,e,r)}function Tn(t){if(t instanceof Xn)return new Xn(t.h,t.s,t.l,t.opacity);if(t instanceof sn||(t=Nn(t)),!t)return new Xn;if(t instanceof Xn)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),u=NaN,a=o-i,s=(o+i)/2;return a?(u=n===o?(e-r)/a+6*(e0&&s<1?0:u,new Xn(u,a,s,t.opacity)}function Xn(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Rn(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function In(t,n,e,r,i){var o=t*t,u=o*t;return((1-3*t+3*o-u)*n+(4-6*o+3*u)*e+(1+3*t+3*o-3*u)*r+u*i)/6}un(sn,Nn,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:xn,formatHex:xn,formatHsl:function(){return Tn(this).formatHsl()},formatRgb:An,toString:An}),un(Cn,Sn,an(sn,{brighter:function(t){return t=null==t?cn:Math.pow(cn,t),new Cn(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?ln:Math.pow(ln,t),new Cn(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:Pn,formatHex:Pn,formatRgb:On,toString:On})),un(Xn,(function(t,n,e,r){return 1===arguments.length?Tn(t):new Xn(t,n,e,null==r?1:r)}),an(sn,{brighter:function(t){return t=null==t?cn:Math.pow(cn,t),new Xn(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?ln:Math.pow(ln,t),new Xn(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new Cn(Rn(t>=240?t-240:t+120,i,r),Rn(t,i,r),Rn(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));const Ln=t=>()=>t;function Dn(t,n){return function(e){return t+e*n}}function Hn(t){return 1==(t=+t)?Yn:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):Ln(isNaN(n)?e:n)}}function Yn(t,n){var e=n-t;return e?Dn(t,e):Ln(isNaN(t)?n:t)}const Bn=function t(n){var e=Hn(n);function r(t,n){var r=e((t=Sn(t)).r,(n=Sn(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),u=Yn(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=u(n),t+""}}return r.gamma=t,r}(1);function $n(t){return function(n){var e,r,i=n.length,o=new Array(i),u=new Array(i),a=new Array(i);for(e=0;e=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],u=r>0?t[r-1]:2*i-o,a=ro&&(i=n.slice(o,i),a[u]?a[u]+=i:a[++u]=i),(e=e[0])===(r=r[0])?a[u]?a[u]+=r:a[++u]=r:(a[++u]=null,s.push({i:u,x:Kt(e,r)})),o=zn.lastIndex;return o=0&&(t=t.slice(0,n)),!t||"start"===t}))}(n)?zt:Ut;return function(){var u=o(this,t),a=u.on;a!==r&&(i=(r=a).copy()).on(n,e),u.on=i}}var ce=vt.prototype.constructor;function fe(t){return function(){this.style.removeProperty(t)}}function he(t,n,e){return function(r){this.style.setProperty(t,n.call(this,r),e)}}function pe(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&he(t,o,e)),r}return o._value=n,o}function de(t){return function(n){this.textContent=t.call(this,n)}}function ye(t){var n,e;function r(){var r=t.apply(this,arguments);return r!==e&&(n=(e=r)&&de(r)),n}return r._value=t,r}var ve=0;function ge(t,n,e,r){this._groups=t,this._parents=n,this._name=e,this._id=r}function _e(){return++ve}var me=vt.prototype;ge.prototype=function(t){return vt().transition(t)}.prototype={constructor:ge,select:function(t){var n=this._name,e=this._id;"function"!=typeof t&&(t=r(t));for(var i=this._groups,o=i.length,u=new Array(o),a=0;a2&&e.state<5,e.state=6,e.timer.stop(),e.on.call(r?"interrupt":"cancel",t,t.__data__,e.index,e.group),delete o[i]):u=!1;u&&delete t.__transition}}(this,t)}))},vt.prototype.transition=function(t){var n,e;t instanceof ge?(n=t._id,t=t._name):(n=_e(),(e=we).time=Tt(),t=null==t?null:t+"");for(var r=this._groups,i=r.length,o=0;o{"use strict";var t={d:(n,e)=>{for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},o:(t,n)=>Object.prototype.hasOwnProperty.call(t,n)},n={};function e(){}function r(t){return null==t?e:function(){return this.querySelector(t)}}function i(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function o(){return[]}function u(t){return null==t?o:function(){return this.querySelectorAll(t)}}function a(t){return function(){return this.matches(t)}}function l(t){return function(n){return n.matches(t)}}t.d(n,{default:()=>xr});var s=Array.prototype.find;function c(){return this.firstElementChild}var h=Array.prototype.filter;function f(){return Array.from(this.children)}function p(t){return new Array(t.length)}function d(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function g(t){return function(){return t}}function v(t,n,e,r,i,o){for(var u,a=0,l=n.length,s=o.length;an?1:t>=n?0:NaN}d.prototype={constructor:d,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,n){return this._parent.insertBefore(t,n)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var b="http://www.w3.org/1999/xhtml";const x={svg:"http://www.w3.org/2000/svg",xhtml:b,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function M(t){var n=t+="",e=n.indexOf(":");return e>=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),x.hasOwnProperty(n)?{space:x[n],local:t}:t}function A(t){return function(){this.removeAttribute(t)}}function N(t){return function(){this.removeAttributeNS(t.space,t.local)}}function k(t,n){return function(){this.setAttribute(t,n)}}function E(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function S(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function C(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function j(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function q(t){return function(){this.style.removeProperty(t)}}function O(t,n,e){return function(){this.style.setProperty(t,n,e)}}function P(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function H(t,n){return t.style.getPropertyValue(n)||j(t).getComputedStyle(t,null).getPropertyValue(n)}function L(t){return function(){delete this[t]}}function T(t,n){return function(){this[t]=n}}function D(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function B(t){return t.trim().split(/^|\s+/)}function X(t){return t.classList||new z(t)}function z(t){this._node=t,this._names=B(t.getAttribute("class")||"")}function R(t,n){for(var e=X(t),r=-1,i=n.length;++r=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}}))}function lt(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var pt=[null];function dt(t,n){this._groups=t,this._parents=n}function gt(){return new dt([[document.documentElement]],pt)}dt.prototype=gt.prototype={constructor:dt,select:function(t){"function"!=typeof t&&(t=r(t));for(var n=this._groups,e=n.length,i=new Array(e),o=0;o=k&&(k=N+1);!(A=b[k])&&++k=0;)(r=i[o])&&(u&&4^r.compareDocumentPosition(u)&&u.parentNode.insertBefore(r,u),u=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=_);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?q:"function"==typeof n?P:O)(t,n,null==e?"":e)):H(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?L:"function"==typeof n?D:T)(t,n)):this.node()[t]},classed:function(t,n){var e=B(t+"");if(arguments.length<2){for(var r=X(this.node()),i=-1,o=e.length;++i1?r[0]+r.slice(2):r,+t.slice(e+1)]}function wt(t){return(t=mt(Math.abs(t)))?t[1]:NaN}var _t,bt=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function xt(t){if(!(n=bt.exec(t)))throw new Error("invalid format: "+t);var n;return new Mt({fill:n[1],align:n[2],sign:n[3],symbol:n[4],zero:n[5],width:n[6],comma:n[7],precision:n[8]&&n[8].slice(1),trim:n[9],type:n[10]})}function Mt(t){this.fill=void 0===t.fill?" ":t.fill+"",this.align=void 0===t.align?">":t.align+"",this.sign=void 0===t.sign?"-":t.sign+"",this.symbol=void 0===t.symbol?"":t.symbol+"",this.zero=!!t.zero,this.width=void 0===t.width?void 0:+t.width,this.comma=!!t.comma,this.precision=void 0===t.precision?void 0:+t.precision,this.trim=!!t.trim,this.type=void 0===t.type?"":t.type+""}function At(t,n){var e=mt(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}xt.prototype=Mt.prototype,Mt.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};const Nt={"%":(t,n)=>(100*t).toFixed(n),b:t=>Math.round(t).toString(2),c:t=>t+"",d:function(t){return Math.abs(t=Math.round(t))>=1e21?t.toLocaleString("en").replace(/,/g,""):t.toString(10)},e:(t,n)=>t.toExponential(n),f:(t,n)=>t.toFixed(n),g:(t,n)=>t.toPrecision(n),o:t=>Math.round(t).toString(8),p:(t,n)=>At(100*t,n),r:At,s:function(t,n){var e=mt(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(_t=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,u=r.length;return o===u?r:o>u?r+new Array(o-u+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+mt(t,Math.max(0,n+o-1))[0]},X:t=>Math.round(t).toString(16).toUpperCase(),x:t=>Math.round(t).toString(16)};function kt(t){return t}var Et,St,Ct,jt=Array.prototype.map,qt=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function Ot(t){var n,e,r=void 0===t.grouping||void 0===t.thousands?kt:(n=jt.call(t.grouping,Number),e=t.thousands+"",function(t,r){for(var i=t.length,o=[],u=0,a=n[0],l=0;i>0&&a>0&&(l+a+1>r&&(a=Math.max(1,r-l)),o.push(t.substring(i-=a,i+a)),!((l+=a+1)>r));)a=n[u=(u+1)%n.length];return o.reverse().join(e)}),i=void 0===t.currency?"":t.currency[0]+"",o=void 0===t.currency?"":t.currency[1]+"",u=void 0===t.decimal?".":t.decimal+"",a=void 0===t.numerals?kt:function(t){return function(n){return n.replace(/[0-9]/g,(function(n){return t[+n]}))}}(jt.call(t.numerals,String)),l=void 0===t.percent?"%":t.percent+"",s=void 0===t.minus?"−":t.minus+"",c=void 0===t.nan?"NaN":t.nan+"";function h(t){var n=(t=xt(t)).fill,e=t.align,h=t.sign,f=t.symbol,p=t.zero,d=t.width,g=t.comma,v=t.precision,y=t.trim,m=t.type;"n"===m?(g=!0,m="g"):Nt[m]||(void 0===v&&(v=12),y=!0,m="g"),(p||"0"===n&&"="===e)&&(p=!0,n="0",e="=");var w="$"===f?i:"#"===f&&/[boxX]/.test(m)?"0"+m.toLowerCase():"",_="$"===f?o:/[%p]/.test(m)?l:"",b=Nt[m],x=/[defgprs%]/.test(m);function M(t){var i,o,l,f=w,M=_;if("c"===m)M=b(t)+M,t="";else{var A=(t=+t)<0||1/t<0;if(t=isNaN(t)?c:b(Math.abs(t),v),y&&(t=function(t){t:for(var n,e=t.length,r=1,i=-1;r0&&(i=0)}return i>0?t.slice(0,i)+t.slice(n+1):t}(t)),A&&0==+t&&"+"!==h&&(A=!1),f=(A?"("===h?h:s:"-"===h||"("===h?"":h)+f,M=("s"===m?qt[8+_t/3]:"")+M+(A&&"("===h?")":""),x)for(i=-1,o=t.length;++i(l=t.charCodeAt(i))||l>57){M=(46===l?u+t.slice(i+1):t.slice(i))+M,t=t.slice(0,i);break}}g&&!p&&(t=r(t,1/0));var N=f.length+t.length+M.length,k=N>1)+f+t+M+k.slice(N);break;default:t=k+f+t+M}return a(t)}return v=void 0===v?6:/[gprs]/.test(m)?Math.max(1,Math.min(21,v)):Math.max(0,Math.min(20,v)),M.toString=function(){return t+""},M}return{format:h,formatPrefix:function(t,n){var e=h(((t=xt(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(wt(n)/3))),i=Math.pow(10,-r),o=qt[8+r/3];return function(t){return e(i*t)+o}}}}function Pt(t,n){return null==t||null==n?NaN:tn?1:t>=n?0:NaN}function Ht(t){t.x0=Math.round(t.x0),t.y0=Math.round(t.y0),t.x1=Math.round(t.x1),t.y1=Math.round(t.y1)}function Lt(){var t=1,n=1,e=0,r=!1;function i(i){var o=i.height+1;return i.x0=i.y0=e,i.x1=t,i.y1=n/o,i.eachBefore(function(t,n){return function(r){r.children&&function(t,n,e,r,i){for(var o,u=t.children,a=-1,l=u.length,s=t.value&&(r-n)/t.value;++a=0;)n+=e[r].value;else n=1;t.value=n}function Dt(t,n){t instanceof Map?(t=[void 0,t],void 0===n&&(n=Xt)):void 0===n&&(n=Bt);for(var e,r,i,o,u,a=new It(t),l=[a];e=l.pop();)if((i=n(e.data))&&(u=(i=Array.from(i)).length))for(e.children=i,o=u-1;o>=0;--o)l.push(r=i[o]=new It(i[o])),r.parent=e,r.depth=e.depth+1;return a.eachBefore(Rt)}function Bt(t){return t.children}function Xt(t){return Array.isArray(t)?t[1]:null}function zt(t){void 0!==t.data.value&&(t.value=t.data.value),t.data=t.data.data}function Rt(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function It(t){this.data=t,this.depth=this.height=0,this.parent=null}Et=Ot({thousands:",",grouping:[3],currency:["$",""]}),St=Et.format,Ct=Et.formatPrefix,It.prototype=Dt.prototype={constructor:It,count:function(){return this.eachAfter(Tt)},each:function(t,n){let e=-1;for(const r of this)t.call(n,r,++e,this);return this},eachAfter:function(t,n){for(var e,r,i,o=this,u=[o],a=[],l=-1;o=u.pop();)if(a.push(o),e=o.children)for(r=0,i=e.length;r=0;--r)o.push(e[r]);return this},find:function(t,n){let e=-1;for(const r of this)if(t.call(n,r,++e,this))return r},sum:function(t){return this.eachAfter((function(n){for(var e=+t(n.data)||0,r=n.children,i=r&&r.length;--i>=0;)e+=r[i].value;n.value=e}))},sort:function(t){return this.eachBefore((function(n){n.children&&n.children.sort(t)}))},path:function(t){for(var n=this,e=function(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;t=e.pop(),n=r.pop();for(;t===n;)i=t,t=e.pop(),n=r.pop();return i}(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},descendants:function(){return Array.from(this)},leaves:function(){var t=[];return this.eachBefore((function(n){n.children||t.push(n)})),t},links:function(){var t=this,n=[];return t.each((function(e){e!==t&&n.push({source:e.parent,target:e})})),n},copy:function(){return Dt(this).eachBefore(zt)},[Symbol.iterator]:function*(){var t,n,e,r,i=this,o=[i];do{for(t=o.reverse(),o=[];i=t.pop();)if(yield i,n=i.children)for(e=0,r=n.length;e=0?(o>=$t?10:o>=Vt?5:o>=Yt?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(o>=$t?10:o>=Vt?5:o>=Yt?2:1)}function Ut(t){let n=t,e=t,r=t;function i(t,n,i=0,o=t.length){if(i>>1;r(t[e],n)<0?i=e+1:o=e}while(it(n)-e,e=Pt,r=(n,e)=>Pt(t(n),e)),{left:i,center:function(t,e,r=0,o=t.length){const u=i(t,e,r,o-1);return u>r&&n(t[u-1],e)>-n(t[u],e)?u-1:u},right:function(t,n,i=0,o=t.length){if(i>>1;r(t[e],n)<=0?i=e+1:o=e}while(i>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):8===e?mn(n>>24&255,n>>16&255,n>>8&255,(255&n)/255):4===e?mn(n>>12&15|n>>8&240,n>>8&15|n>>4&240,n>>4&15|240&n,((15&n)<<4|15&n)/255):null):(n=an.exec(t))?new bn(n[1],n[2],n[3],1):(n=ln.exec(t))?new bn(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=sn.exec(t))?mn(n[1],n[2],n[3],n[4]):(n=cn.exec(t))?mn(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=hn.exec(t))?Nn(n[1],n[2]/100,n[3]/100,1):(n=fn.exec(t))?Nn(n[1],n[2]/100,n[3]/100,n[4]):pn.hasOwnProperty(t)?yn(pn[t]):"transparent"===t?new bn(NaN,NaN,NaN,0):null}function yn(t){return new bn(t>>16&255,t>>8&255,255&t,1)}function mn(t,n,e,r){return r<=0&&(t=n=e=NaN),new bn(t,n,e,r)}function wn(t){return t instanceof Qt||(t=vn(t)),t?new bn((t=t.rgb()).r,t.g,t.b,t.opacity):new bn}function _n(t,n,e,r){return 1===arguments.length?wn(t):new bn(t,n,e,null==r?1:r)}function bn(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function xn(){return"#"+An(this.r)+An(this.g)+An(this.b)}function Mn(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function An(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function Nn(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new En(t,n,e,r)}function kn(t){if(t instanceof En)return new En(t.h,t.s,t.l,t.opacity);if(t instanceof Qt||(t=vn(t)),!t)return new En;if(t instanceof En)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),u=NaN,a=o-i,l=(o+i)/2;return a?(u=n===o?(e-r)/a+6*(e0&&l<1?0:u,new En(u,a,l,t.opacity)}function En(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Sn(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function Cn(t,n,e,r,i){var o=t*t,u=o*t;return((1-3*t+3*o-u)*n+(4-6*o+3*u)*e+(1+3*t+3*o-3*u)*r+u*i)/6}Wt(Qt,vn,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:dn,formatHex:dn,formatHsl:function(){return kn(this).formatHsl()},formatRgb:gn,toString:gn}),Wt(bn,_n,Jt(Qt,{brighter:function(t){return t=null==t?nn:Math.pow(nn,t),new bn(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?tn:Math.pow(tn,t),new bn(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:xn,formatHex:xn,formatRgb:Mn,toString:Mn})),Wt(En,(function(t,n,e,r){return 1===arguments.length?kn(t):new En(t,n,e,null==r?1:r)}),Jt(Qt,{brighter:function(t){return t=null==t?nn:Math.pow(nn,t),new En(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?tn:Math.pow(tn,t),new En(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new bn(Sn(t>=240?t-240:t+120,i,r),Sn(t,i,r),Sn(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));const jn=t=>()=>t;function qn(t,n){return function(e){return t+e*n}}function On(t){return 1==(t=+t)?Pn:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):jn(isNaN(n)?e:n)}}function Pn(t,n){var e=n-t;return e?qn(t,e):jn(isNaN(t)?n:t)}const Hn=function t(n){var e=On(n);function r(t,n){var r=e((t=_n(t)).r,(n=_n(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),u=Pn(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=u(n),t+""}}return r.gamma=t,r}(1);function Ln(t){return function(n){var e,r,i=n.length,o=new Array(i),u=new Array(i),a=new Array(i);for(e=0;e=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],u=r>0?t[r-1]:2*i-o,a=ro&&(i=n.slice(o,i),a[u]?a[u]+=i:a[++u]=i),(e=e[0])===(r=r[0])?a[u]?a[u]+=r:a[++u]=r:(a[++u]=null,l.push({i:u,x:Bn(e,r)})),o=Rn.lastIndex;return on&&(e=t,t=n,n=e),s=function(e){return Math.max(t,Math.min(n,e))}),r=l>2?Wn:Kn,i=o=null,h}function h(n){return null==n||isNaN(n=+n)?e:(i||(i=r(u.map(t),a,l)))(t(s(n)))}return h.invert=function(e){return s(n((o||(o=r(a,u.map(t),Bn)))(e)))},h.domain=function(t){return arguments.length?(u=Array.from(t,Fn),c()):u.slice()},h.range=function(t){return arguments.length?(a=Array.from(t),c()):a.slice()},h.rangeRound=function(t){return a=Array.from(t),l=Yn,c()},h.clamp=function(t){return arguments.length?(s=!!t||Zn,c()):s!==Zn},h.interpolate=function(t){return arguments.length?(l=t,c()):l},h.unknown=function(t){return arguments.length?(e=t,h):e},function(e,r){return t=e,n=r,c()}}function te(){return Qn()(Zn,Zn)}function ne(t,n){switch(arguments.length){case 0:break;case 1:this.range(t);break;default:this.range(n).domain(t)}return this}function ee(t,n,e,r){var i,o=function(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=$t?i*=10:o>=Vt?i*=5:o>=Yt&&(i*=2),n0)return[t];if((r=n0){let e=Math.round(t/u),r=Math.round(n/u);for(e*un&&--r,o=new Array(i=r-e+1);++an&&--r,o=new Array(i=r-e+1);++a0;){if((i=Ft(l,s,e))===r)return o[u]=l,o[a]=s,n(o);if(i>0)l=Math.floor(l/i)*i,s=Math.ceil(s/i)*i;else{if(!(i<0))break;l=Math.ceil(l*i)/i,s=Math.floor(s*i)/i}r=i}return t},t}function ie(){var t=te();return t.copy=function(){return Jn(t,ie())},ne.apply(t,arguments),re(t)}function oe(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}var ue={value:()=>{}};function ae(){for(var t,n=0,e=arguments.length,r={};n=0&&(e=t.slice(r+1),t=t.slice(0,r)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}}))}function ce(t,n){for(var e,r=0,i=t.length;r0)for(var e,r,i=new Array(e),o=0;o=0&&n._call.call(void 0,t),n=n._next;--ge}()}finally{ge=0,function(){var t,n,e=pe,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:pe=n);de=t,Ce(r)}(),we=0}}function Se(){var t=be.now(),n=t-me;n>1e3&&(_e-=n,me=t)}function Ce(t){ge||(ve&&(ve=clearTimeout(ve)),t-we>24?(t<1/0&&(ve=setTimeout(Ee,t-be.now()-_e)),ye&&(ye=clearInterval(ye))):(ye||(me=be.now(),ye=setInterval(Se,1e3)),ge=1,xe(Ee)))}function je(t,n,e){var r=new Ne;return n=null==n?0:+n,r.restart((e=>{r.stop(),t(e+n)}),n,e),r}Ne.prototype=ke.prototype={constructor:Ne,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?Me():+e)+(null==n?0:+n),this._next||de===this||(de?de._next=this:pe=this,de=this),this._call=t,this._time=e,Ce()},stop:function(){this._call&&(this._call=null,this._time=1/0,Ce())}};var qe=fe("start","end","cancel","interrupt"),Oe=[];function Pe(t,n,e,r,i,o){var u=t.__transition;if(u){if(e in u)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(t){e.state=1,e.timer.restart(u,e.delay,e.time),e.delay<=t&&u(t-e.delay)}function u(o){var s,c,h,f;if(1!==e.state)return l();for(s in i)if((f=i[s]).name===e.name){if(3===f.state)return je(u);4===f.state?(f.state=6,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[s]):+s0)throw new Error("too late; already scheduled");return e}function Le(t,n){var e=Te(t,n);if(e.state>3)throw new Error("too late; already running");return e}function Te(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}var De,Be=180/Math.PI,Xe={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1};function ze(t,n,e,r,i,o){var u,a,l;return(u=Math.sqrt(t*t+n*n))&&(t/=u,n/=u),(l=t*e+n*r)&&(e-=t*l,r-=n*l),(a=Math.sqrt(e*e+r*r))&&(e/=a,r/=a,l/=a),t*r180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:Bn(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,u.rotate,a,l),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:Bn(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,u.skewX,a,l),function(t,n,e,r,o,u){if(t!==e||n!==r){var a=o.push(i(o)+"scale(",null,",",null,")");u.push({i:a-4,x:Bn(t,e)},{i:a-2,x:Bn(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,u.scaleX,u.scaleY,a,l),o=u=null,function(t){for(var n,e=-1,r=l.length;++e=0&&(t=t.slice(0,n)),!t||"start"===t}))}(n)?He:Le;return function(){var u=o(this,t),a=u.on;a!==r&&(i=(r=a).copy()).on(n,e),u.on=i}}var cr=vt.prototype.constructor;function hr(t){return function(){this.style.removeProperty(t)}}function fr(t,n,e){return function(r){this.style.setProperty(t,n.call(this,r),e)}}function pr(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&fr(t,o,e)),r}return o._value=n,o}function dr(t){return function(n){this.textContent=t.call(this,n)}}function gr(t){var n,e;function r(){var r=t.apply(this,arguments);return r!==e&&(n=(e=r)&&dr(r)),n}return r._value=t,r}var vr=0;function yr(t,n,e,r){this._groups=t,this._parents=n,this._name=e,this._id=r}function mr(){return++vr}var wr=vt.prototype;yr.prototype=function(t){return vt().transition(t)}.prototype={constructor:yr,select:function(t){var n=this._name,e=this._id;"function"!=typeof t&&(t=r(t));for(var i=this._groups,o=i.length,u=new Array(o),a=0;a{p&&(p.textContent="search: "+n+" of "+e+" total samples ( "+St(".3f")(n/e*100,3)+"%)")},d()};const E=k;let S=(t,n,e=!1)=>{if(!n)return!1;let r=b(t);e&&(n=n.toLowerCase(),r=r.toLowerCase());const i=new RegExp(n);return void 0!==r&&r&&r.match(i)};const C=S;let j=function(t){p&&(t?p.textContent=t:"function"==typeof d?d():p.textContent="")};const q=j;let O=function(t){return b(t)+" ("+St(".3f")(100*(t.x1-t.x0),3)+"%, "+x(t)+" samples)"},P=function(t){return t.highlight?"#E600E6":function(t,n){let e=_||"warm";_||void 0===n||""===n||(e="red",void 0!==t&&t&&t.match(/::/)&&(e="yellow"),"kernel"===n?e="orange":"jit"===n?e="green":"inlined"===n&&(e="aqua"));const r=function(t){let n=0;if(t){const e=t.split("`");e.length>1&&(t=e[e.length-1]),n=function(t){let n=0,e=0,r=1;if(t){for(let i=0;i6);i++)n+=r*(t.charCodeAt(i)%10),e+=9*r,r*=.7;e>0&&(n/=e)}return n}(t=t.split("(")[0])}return n}(t);return function(t,n){let e,r,i;return"red"===t?(e=200+Math.round(55*n),r=50+Math.round(80*n),i=r):"orange"===t?(e=190+Math.round(65*n),r=90+Math.round(65*n),i=0):"yellow"===t?(e=175+Math.round(55*n),r=e,i=50+Math.round(20*n)):"green"===t?(e=50+Math.round(60*n),r=200+Math.round(55*n),i=e):"pastelgreen"===t?(e=163+Math.round(75*n),r=195+Math.round(49*n),i=72+Math.round(149*n)):"blue"===t?(e=91+Math.round(126*n),r=156+Math.round(76*n),i=221+Math.round(26*n)):"aqua"===t?(e=50+Math.round(60*n),r=165+Math.round(55*n),i=r):"cold"===t?(e=0+Math.round(55*(1-n)),r=0+Math.round(230*(1-n)),i=200+Math.round(55*n)):(e=200+Math.round(55*n),r=0+Math.round(230*(1-n)),i=0+Math.round(55*(1-n))),"rgb("+e+","+r+","+i+")"}(e,r)}(b(t),A(t))};const H=P;function L(t){t.data.fade=!1,t.data.hide=!1,t.children&&t.children.forEach(L)}function T(t){t.parent&&(t.parent.data.fade=!0,T(t.parent))}function D(t){if(i&&i.hide(),function(t){let n,e,r,i=t,o=i.parent;for(;o;){for(n=o.children,e=n.length;e--;)r=n[e],r!==i&&(r.data.hide=!0);i=o,o=i.parent}}(t),L(t),T(t),I(),y){const n=yt(this).select("svg")._groups[0][0].parentNode.offsetTop,r=(window.innerHeight-n)/e,i=(t.height-r+10)*e;window.scrollTo({top:n+i,left:0,behavior:"smooth"})}"function"==typeof c&&c(t)}function B(t,n){if(t.id===n)return t;{const e=M(t);if(e)for(let t=0;t0){const r=t/(n.x1-n.x0);e=e.filter((function(t){return(t.x1-t.x0)*r>f}))}return e}(r),y=yt(this).select("svg");y.attr("width",t);let w=y.selectAll("g").data(g,(function(t){return t.id}));if(!n||v){const t=Math.max.apply(null,g.map((function(t){return t.depth})));n=(t+3)*e,n{D(n)})),w.exit().remove(),w.on("mouseover",(function(t,n){i&&i.show(n,this),j(O(n)),"function"==typeof h&&h(n)})).on("mouseout",(function(){i&&i.hide(),j(null)}))}))}function $(t,n){n.forEach((function(n){const e=t.find((function(t){return t.name===n.name}));e?(e.value+=n.value,n.children&&(e.children||(e.children=[]),$(e.children,n.children))):t.push(n)}))}function V(t){let n,e,r,i,o,u,a,l;const s=[],c=[],h=[],f=!g;let p=t.data;for(p.hide?(t.value=0,e=t.children,e&&h.push(e)):(t.value=p.fade?0:x(p),s.push(t));n=s.pop();)if(e=n.children,e&&(o=e.length)){for(i=0;o--;)a=e[o],p=a.data,p.hide?(a.value=0,r=a.children,r&&h.push(r)):(p.fade?a.value=0:(l=x(p),a.value=l,i+=l),s.push(a));f&&n.value&&(n.value-=i),c.push(e)}for(o=c.length;o--;){for(e=c[o],i=0,u=e.length;u--;)i+=e[u].value;e[0].parent.value+=i}for(;h.length;)for(e=h.pop(),u=e.length;u--;)a=e[u],a.value=0,r=a.children,r&&h.push(r)}function Y(){r.datum((t=>{if("Node"!==t.constructor.name){const n=Dt(t,M);return function(t){let n=0;!function(t,n){n(t);let e=t.children;if(e){const t=[e];let r,i,o;for(;t.length;)for(e=t.pop(),r=e.length;r--;)i=e[r],n(i),o=i.children,o&&t.push(o)}}(t,(function(t){t.id=n++}))}(n),V(n),n.originalValue=n.value,w&&n.eachAfter((t=>{let n=N(t);const e=t.children;let r=e&&e.length;for(;--r>=0;)n+=e[r].delta;t.delta=n})),n}}))}function F(e){if(!arguments.length)return F;r=e,Y(),r.each((function(e){if(0===yt(this).select("svg").size()){const e=yt(this).append("svg:svg").attr("width",t).attr("class","partition d3-flame-graph");n&&(n($([n.data],[t]),n.data))),Y(),I(),F):F},F.update=function(t){return r?(t&&(r.datum(t),Y()),I(),F):F},F.destroy=function(){return r?(i&&(i.hide(),"function"==typeof i.destroy&&i.destroy()),r.selectAll("svg").remove(),F):F},F.setColorMapper=function(t){return arguments.length?(P=n=>{const e=H(n);return t(n,e)},F):(P=H,F)},F.color=F.setColorMapper,F.setColorHue=function(t){return arguments.length?(_=t,F):(_=null,F)},F.minFrameSize=function(t){return arguments.length?(f=t,F):f},F.setDetailsElement=function(t){return arguments.length?(p=t,F):p},F.details=F.setDetailsElement,F.selfValue=function(t){return arguments.length?(g=t,F):g},F.resetHeightOnZoom=function(t){return arguments.length?(v=t,F):v},F.scrollOnZoom=function(t){return arguments.length?(y=t,F):y},F.getName=function(t){return arguments.length?(b=t,F):b},F.getValue=function(t){return arguments.length?(x=t,F):x},F.getChildren=function(t){return arguments.length?(M=t,F):M},F.getLibtype=function(t){return arguments.length?(A=t,F):A},F.getDelta=function(t){return arguments.length?(N=t,F):N},F.setSearchHandler=function(t){return arguments.length?(k=t,F):(k=E,F)},F.setDetailsHandler=function(t){return arguments.length?(j=t,F):(j=q,F)},F.setSearchMatch=function(t){return arguments.length?(S=t,F):(S=C,F)},F}return vt.prototype.interrupt=function(t){return this.each((function(){!function(t,n){var e,r,i,o=t.__transition,u=!0;if(o){for(i in n=null==n?null:n+"",o)(e=o[i]).name===n?(r=e.state>2&&e.state<5,e.state=6,e.timer.stop(),e.on.call(r?"interrupt":"cancel",t,t.__data__,e.index,e.group),delete o[i]):u=!1;u&&delete t.__transition}}(this,t)}))},vt.prototype.transition=function(t){var n,e;t instanceof yr?(n=t._id,t=t._name):(n=mr(),(e=_r).time=Me(),t=null==t?null:t+"");for(var r=this._groups,i=r.length,o=0;on?1:t>=n?0:NaN}function e(t,n){return null==t||null==n?NaN:nt?1:n>=t?0:NaN}function r(t){let r,o,a;function u(t,n,e=0,i=t.length){if(e>>1;o(t[r],n)<0?e=r+1:i=r}while(en(t(e),r),a=(n,e)=>t(n)-e):(r=t===n||t===e?t:i,o=t,a=t),{left:u,center:function(t,n,e=0,r=t.length){const i=u(t,n,e,r-1);return i>e&&a(t[i-1],n)>-a(t[i],n)?i-1:i},right:function(t,n,e=0,i=t.length){if(e>>1;o(t[r],n)<=0?e=r+1:i=r}while(e{n(t,e,(r<<=2)+0,(i<<=2)+0,o<<=2),n(t,e,r+1,i+1,o),n(t,e,r+2,i+2,o),n(t,e,r+3,i+3,o)}}));function d(t){return function(n,e,r=e){if(!((e=+e)>=0))throw new RangeError("invalid rx");if(!((r=+r)>=0))throw new RangeError("invalid ry");let{data:i,width:o,height:a}=n;if(!((o=Math.floor(o))>=0))throw new RangeError("invalid width");if(!((a=Math.floor(void 0!==a?a:i.length/o))>=0))throw new RangeError("invalid height");if(!o||!a||!e&&!r)return n;const u=e&&t(e),c=r&&t(r),f=i.slice();return u&&c?(p(u,f,i,o,a),p(u,i,f,o,a),p(u,f,i,o,a),g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)):u?(p(u,i,f,o,a),p(u,f,i,o,a),p(u,i,f,o,a)):c&&(g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)),n}}function p(t,n,e,r,i){for(let o=0,a=r*i;o{if(!((o-=a)>=i))return;let u=t*r[i];const c=a*t;for(let t=i,n=i+c;t{if(!((a-=u)>=o))return;let c=n*i[o];const f=u*n,s=f+u;for(let t=o,n=o+f;t=n&&++e;else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(i=+i)>=i&&++e}return e}function _(t){return 0|t.length}function b(t){return!(t>0)}function m(t){return"object"!=typeof t||"length"in t?t:Array.from(t)}function x(t,n){let e,r=0,i=0,o=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(e=n-i,i+=e/++r,o+=e*(n-i));else{let a=-1;for(let u of t)null!=(u=n(u,++a,t))&&(u=+u)>=u&&(e=u-i,i+=e/++r,o+=e*(u-i))}if(r>1)return o/(r-1)}function w(t,n){const e=x(t,n);return e?Math.sqrt(e):e}function M(t,n){let e,r;if(void 0===n)for(const n of t)null!=n&&(void 0===e?n>=n&&(e=r=n):(e>n&&(e=n),r=o&&(e=r=o):(e>o&&(e=o),r0){for(o=t[--i];i>0&&(n=o,e=t[--i],o=n+e,r=e-(o-n),!r););i>0&&(r<0&&t[i-1]<0||r>0&&t[i-1]>0)&&(e=2*r,n=o+e,e==n-o&&(o=n))}return o}}class InternMap extends Map{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const[n,e]of t)this.set(n,e)}get(t){return super.get(A(this,t))}has(t){return super.has(A(this,t))}set(t,n){return super.set(S(this,t),n)}delete(t){return super.delete(E(this,t))}}class InternSet extends Set{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const n of t)this.add(n)}has(t){return super.has(A(this,t))}add(t){return super.add(S(this,t))}delete(t){return super.delete(E(this,t))}}function A({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):e}function S({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):(t.set(r,e),e)}function E({_intern:t,_key:n},e){const r=n(e);return t.has(r)&&(e=t.get(r),t.delete(r)),e}function N(t){return null!==t&&"object"==typeof t?t.valueOf():t}function k(t){return t}function C(t,...n){return F(t,k,k,n)}function P(t,...n){return F(t,Array.from,k,n)}function z(t,n){for(let e=1,r=n.length;et.pop().map((([n,e])=>[...t,n,e]))));return t}function $(t,n,...e){return F(t,k,n,e)}function D(t,n,...e){return F(t,Array.from,n,e)}function R(t){if(1!==t.length)throw new Error("duplicate key");return t[0]}function F(t,n,e,r){return function t(i,o){if(o>=r.length)return e(i);const a=new InternMap,u=r[o++];let c=-1;for(const t of i){const n=u(t,++c,i),e=a.get(n);e?e.push(t):a.set(n,[t])}for(const[n,e]of a)a.set(n,t(e,o));return n(a)}(t,0)}function q(t,n){return Array.from(n,(n=>t[n]))}function U(t,...n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");t=Array.from(t);let[e]=n;if(e&&2!==e.length||n.length>1){const r=Uint32Array.from(t,((t,n)=>n));return n.length>1?(n=n.map((n=>t.map(n))),r.sort(((t,e)=>{for(const r of n){const n=O(r[t],r[e]);if(n)return n}}))):(e=t.map(e),r.sort(((t,n)=>O(e[t],e[n])))),q(t,r)}return t.sort(I(e))}function I(t=n){if(t===n)return O;if("function"!=typeof t)throw new TypeError("compare is not a function");return(n,e)=>{const r=t(n,e);return r||0===r?r:(0===t(e,e))-(0===t(n,n))}}function O(t,n){return(null==t||!(t>=t))-(null==n||!(n>=n))||(tn?1:0)}var B=Array.prototype.slice;function Y(t){return()=>t}const L=Math.sqrt(50),j=Math.sqrt(10),H=Math.sqrt(2);function X(t,n,e){const r=(n-t)/Math.max(0,e),i=Math.floor(Math.log10(r)),o=r/Math.pow(10,i),a=o>=L?10:o>=j?5:o>=H?2:1;let u,c,f;return i<0?(f=Math.pow(10,-i)/a,u=Math.round(t*f),c=Math.round(n*f),u/fn&&--c,f=-f):(f=Math.pow(10,i)*a,u=Math.round(t/f),c=Math.round(n/f),u*fn&&--c),c0))return[];if((t=+t)===(n=+n))return[t];const r=n=i))return[];const u=o-i+1,c=new Array(u);if(r)if(a<0)for(let t=0;t0?(t=Math.floor(t/i)*i,n=Math.ceil(n/i)*i):i<0&&(t=Math.ceil(t*i)/i,n=Math.floor(n*i)/i),r=i}}function K(t){return Math.max(1,Math.ceil(Math.log(v(t))/Math.LN2)+1)}function Q(){var t=k,n=M,e=K;function r(r){Array.isArray(r)||(r=Array.from(r));var i,o,a,u=r.length,c=new Array(u);for(i=0;i=h)if(t>=h&&n===M){const t=V(l,h,e);isFinite(t)&&(t>0?h=(Math.floor(h/t)+1)*t:t<0&&(h=(Math.ceil(h*-t)+1)/-t))}else d.pop()}for(var p=d.length,g=0,y=p;d[g]<=l;)++g;for(;d[y-1]>h;)--y;(g||y0?d[i-1]:l,v.x1=i0)for(i=0;i=n)&&(e=n);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(e=i)&&(e=i)}return e}function tt(t,n){let e,r=-1,i=-1;if(void 0===n)for(const n of t)++i,null!=n&&(e=n)&&(e=n,r=i);else for(let o of t)null!=(o=n(o,++i,t))&&(e=o)&&(e=o,r=i);return r}function nt(t,n){let e;if(void 0===n)for(const n of t)null!=n&&(e>n||void 0===e&&n>=n)&&(e=n);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(e>i||void 0===e&&i>=i)&&(e=i)}return e}function et(t,n){let e,r=-1,i=-1;if(void 0===n)for(const n of t)++i,null!=n&&(e>n||void 0===e&&n>=n)&&(e=n,r=i);else for(let o of t)null!=(o=n(o,++i,t))&&(e>o||void 0===e&&o>=o)&&(e=o,r=i);return r}function rt(t,n,e=0,r=1/0,i){if(n=Math.floor(n),e=Math.floor(Math.max(0,e)),r=Math.floor(Math.min(t.length-1,r)),!(e<=n&&n<=r))return t;for(i=void 0===i?O:I(i);r>e;){if(r-e>600){const o=r-e+1,a=n-e+1,u=Math.log(o),c=.5*Math.exp(2*u/3),f=.5*Math.sqrt(u*c*(o-c)/o)*(a-o/2<0?-1:1);rt(t,n,Math.max(e,Math.floor(n-a*c/o+f)),Math.min(r,Math.floor(n+(o-a)*c/o+f)),i)}const o=t[n];let a=e,u=r;for(it(t,e,n),i(t[r],o)>0&&it(t,e,r);a0;)--u}0===i(t[e],o)?it(t,e,u):(++u,it(t,u,r)),u<=n&&(e=u+1),n<=u&&(r=u-1)}return t}function it(t,n,e){const r=t[n];t[n]=t[e],t[e]=r}function ot(t,e=n){let r,i=!1;if(1===e.length){let o;for(const a of t){const t=e(a);(i?n(t,o)>0:0===n(t,t))&&(r=a,o=t,i=!0)}}else for(const n of t)(i?e(n,r)>0:0===e(n,n))&&(r=n,i=!0);return r}function at(t,n,e){if(t=Float64Array.from(function*(t,n){if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(yield n);else{let e=-1;for(let r of t)null!=(r=n(r,++e,t))&&(r=+r)>=r&&(yield r)}}(t,e)),(r=t.length)&&!isNaN(n=+n)){if(n<=0||r<2)return nt(t);if(n>=1)return J(t);var r,i=(r-1)*n,o=Math.floor(i),a=J(rt(t,o).subarray(0,o+1));return a+(nt(t.subarray(o+1))-a)*(i-o)}}function ut(t,n,e=o){if((r=t.length)&&!isNaN(n=+n)){if(n<=0||r<2)return+e(t[0],0,t);if(n>=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,a=Math.floor(i),u=+e(t[a],a,t);return u+(+e(t[a+1],a+1,t)-u)*(i-a)}}function ct(t,n,e=o){if(!isNaN(n=+n)){if(r=Float64Array.from(t,((n,r)=>o(e(t[r],r,t)))),n<=0)return et(r);if(n>=1)return tt(r);var r,i=Uint32Array.from(t,((t,n)=>n)),a=r.length-1,u=Math.floor(a*n);return rt(i,u,0,a,((t,n)=>O(r[t],r[n]))),(u=ot(i.subarray(0,u+1),(t=>r[t])))>=0?u:-1}}function ft(t){return Array.from(function*(t){for(const n of t)yield*n}(t))}function st(t,n){return[t,n]}function lt(t,n,e){t=+t,n=+n,e=(i=arguments.length)<2?(n=t,t=0,1):i<3?1:+e;for(var r=-1,i=0|Math.max(0,Math.ceil((n-t)/e)),o=new Array(i);++r+t(n)}function kt(t,n){return n=Math.max(0,t.bandwidth()-2*n)/2,t.round()&&(n=Math.round(n)),e=>+t(e)+n}function Ct(){return!this.__axis}function Pt(t,n){var e=[],r=null,i=null,o=6,a=6,u=3,c="undefined"!=typeof window&&window.devicePixelRatio>1?0:.5,f=t===xt||t===Tt?-1:1,s=t===Tt||t===wt?"x":"y",l=t===xt||t===Mt?St:Et;function h(h){var d=null==r?n.ticks?n.ticks.apply(n,e):n.domain():r,p=null==i?n.tickFormat?n.tickFormat.apply(n,e):mt:i,g=Math.max(o,0)+u,y=n.range(),v=+y[0]+c,_=+y[y.length-1]+c,b=(n.bandwidth?kt:Nt)(n.copy(),c),m=h.selection?h.selection():h,x=m.selectAll(".domain").data([null]),w=m.selectAll(".tick").data(d,n).order(),M=w.exit(),T=w.enter().append("g").attr("class","tick"),A=w.select("line"),S=w.select("text");x=x.merge(x.enter().insert("path",".tick").attr("class","domain").attr("stroke","currentColor")),w=w.merge(T),A=A.merge(T.append("line").attr("stroke","currentColor").attr(s+"2",f*o)),S=S.merge(T.append("text").attr("fill","currentColor").attr(s,f*g).attr("dy",t===xt?"0em":t===Mt?"0.71em":"0.32em")),h!==m&&(x=x.transition(h),w=w.transition(h),A=A.transition(h),S=S.transition(h),M=M.transition(h).attr("opacity",At).attr("transform",(function(t){return isFinite(t=b(t))?l(t+c):this.getAttribute("transform")})),T.attr("opacity",At).attr("transform",(function(t){var n=this.parentNode.__axis;return l((n&&isFinite(n=n(t))?n:b(t))+c)}))),M.remove(),x.attr("d",t===Tt||t===wt?a?"M"+f*a+","+v+"H"+c+"V"+_+"H"+f*a:"M"+c+","+v+"V"+_:a?"M"+v+","+f*a+"V"+c+"H"+_+"V"+f*a:"M"+v+","+c+"H"+_),w.attr("opacity",1).attr("transform",(function(t){return l(b(t)+c)})),A.attr(s+"2",f*o),S.attr(s,f*g).text(p),m.filter(Ct).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",t===wt?"start":t===Tt?"end":"middle"),m.each((function(){this.__axis=b}))}return h.scale=function(t){return arguments.length?(n=t,h):n},h.ticks=function(){return e=Array.from(arguments),h},h.tickArguments=function(t){return arguments.length?(e=null==t?[]:Array.from(t),h):e.slice()},h.tickValues=function(t){return arguments.length?(r=null==t?null:Array.from(t),h):r&&r.slice()},h.tickFormat=function(t){return arguments.length?(i=t,h):i},h.tickSize=function(t){return arguments.length?(o=a=+t,h):o},h.tickSizeInner=function(t){return arguments.length?(o=+t,h):o},h.tickSizeOuter=function(t){return arguments.length?(a=+t,h):a},h.tickPadding=function(t){return arguments.length?(u=+t,h):u},h.offset=function(t){return arguments.length?(c=+t,h):c},h}var zt={value:()=>{}};function $t(){for(var t,n=0,e=arguments.length,r={};n=0&&(n=t.slice(e+1),t=t.slice(0,e)),t&&!r.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))),a=-1,u=o.length;if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++a0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Ut.hasOwnProperty(n)?{space:Ut[n],local:t}:t}function Ot(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===qt&&n.documentElement.namespaceURI===qt?n.createElement(t):n.createElementNS(e,t)}}function Bt(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function Yt(t){var n=It(t);return(n.local?Bt:Ot)(n)}function Lt(){}function jt(t){return null==t?Lt:function(){return this.querySelector(t)}}function Ht(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function Xt(){return[]}function Gt(t){return null==t?Xt:function(){return this.querySelectorAll(t)}}function Vt(t){return function(){return this.matches(t)}}function Wt(t){return function(n){return n.matches(t)}}var Zt=Array.prototype.find;function Kt(){return this.firstElementChild}var Qt=Array.prototype.filter;function Jt(){return Array.from(this.children)}function tn(t){return new Array(t.length)}function nn(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function en(t,n,e,r,i,o){for(var a,u=0,c=n.length,f=o.length;un?1:t>=n?0:NaN}function cn(t){return function(){this.removeAttribute(t)}}function fn(t){return function(){this.removeAttributeNS(t.space,t.local)}}function sn(t,n){return function(){this.setAttribute(t,n)}}function ln(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function hn(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function dn(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function pn(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function gn(t){return function(){this.style.removeProperty(t)}}function yn(t,n,e){return function(){this.style.setProperty(t,n,e)}}function vn(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function _n(t,n){return t.style.getPropertyValue(n)||pn(t).getComputedStyle(t,null).getPropertyValue(n)}function bn(t){return function(){delete this[t]}}function mn(t,n){return function(){this[t]=n}}function xn(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function wn(t){return t.trim().split(/^|\s+/)}function Mn(t){return t.classList||new Tn(t)}function Tn(t){this._node=t,this._names=wn(t.getAttribute("class")||"")}function An(t,n){for(var e=Mn(t),r=-1,i=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var Gn=[null];function Vn(t,n){this._groups=t,this._parents=n}function Wn(){return new Vn([[document.documentElement]],Gn)}function Zn(t){return"string"==typeof t?new Vn([[document.querySelector(t)]],[document.documentElement]):new Vn([[t]],Gn)}Vn.prototype=Wn.prototype={constructor:Vn,select:function(t){"function"!=typeof t&&(t=jt(t));for(var n=this._groups,e=n.length,r=new Array(e),i=0;i=m&&(m=b+1);!(_=y[m])&&++m=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=un);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?gn:"function"==typeof n?vn:yn)(t,n,null==e?"":e)):_n(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?bn:"function"==typeof n?xn:mn)(t,n)):this.node()[t]},classed:function(t,n){var e=wn(t+"");if(arguments.length<2){for(var r=Mn(this.node()),i=-1,o=e.length;++i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}}))}(t+""),a=o.length;if(!(arguments.length<2)){for(u=n?Ln:Yn,r=0;r()=>t;function fe(t,{sourceEvent:n,subject:e,target:r,identifier:i,active:o,x:a,y:u,dx:c,dy:f,dispatch:s}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},subject:{value:e,enumerable:!0,configurable:!0},target:{value:r,enumerable:!0,configurable:!0},identifier:{value:i,enumerable:!0,configurable:!0},active:{value:o,enumerable:!0,configurable:!0},x:{value:a,enumerable:!0,configurable:!0},y:{value:u,enumerable:!0,configurable:!0},dx:{value:c,enumerable:!0,configurable:!0},dy:{value:f,enumerable:!0,configurable:!0},_:{value:s}})}function se(t){return!t.ctrlKey&&!t.button}function le(){return this.parentNode}function he(t,n){return null==n?{x:t.x,y:t.y}:n}function de(){return navigator.maxTouchPoints||"ontouchstart"in this}function pe(t,n,e){t.prototype=n.prototype=e,e.constructor=t}function ge(t,n){var e=Object.create(t.prototype);for(var r in n)e[r]=n[r];return e}function ye(){}fe.prototype.on=function(){var t=this._.on.apply(this._,arguments);return t===this._?this:t};var ve=.7,_e=1/ve,be="\\s*([+-]?\\d+)\\s*",me="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",xe="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",we=/^#([0-9a-f]{3,8})$/,Me=new RegExp(`^rgb\\(${be},${be},${be}\\)$`),Te=new RegExp(`^rgb\\(${xe},${xe},${xe}\\)$`),Ae=new RegExp(`^rgba\\(${be},${be},${be},${me}\\)$`),Se=new RegExp(`^rgba\\(${xe},${xe},${xe},${me}\\)$`),Ee=new RegExp(`^hsl\\(${me},${xe},${xe}\\)$`),Ne=new RegExp(`^hsla\\(${me},${xe},${xe},${me}\\)$`),ke={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};function Ce(){return this.rgb().formatHex()}function Pe(){return this.rgb().formatRgb()}function ze(t){var n,e;return t=(t+"").trim().toLowerCase(),(n=we.exec(t))?(e=n[1].length,n=parseInt(n[1],16),6===e?$e(n):3===e?new qe(n>>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):8===e?De(n>>24&255,n>>16&255,n>>8&255,(255&n)/255):4===e?De(n>>12&15|n>>8&240,n>>8&15|n>>4&240,n>>4&15|240&n,((15&n)<<4|15&n)/255):null):(n=Me.exec(t))?new qe(n[1],n[2],n[3],1):(n=Te.exec(t))?new qe(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Ae.exec(t))?De(n[1],n[2],n[3],n[4]):(n=Se.exec(t))?De(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Ee.exec(t))?Le(n[1],n[2]/100,n[3]/100,1):(n=Ne.exec(t))?Le(n[1],n[2]/100,n[3]/100,n[4]):ke.hasOwnProperty(t)?$e(ke[t]):"transparent"===t?new qe(NaN,NaN,NaN,0):null}function $e(t){return new qe(t>>16&255,t>>8&255,255&t,1)}function De(t,n,e,r){return r<=0&&(t=n=e=NaN),new qe(t,n,e,r)}function Re(t){return t instanceof ye||(t=ze(t)),t?new qe((t=t.rgb()).r,t.g,t.b,t.opacity):new qe}function Fe(t,n,e,r){return 1===arguments.length?Re(t):new qe(t,n,e,null==r?1:r)}function qe(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function Ue(){return`#${Ye(this.r)}${Ye(this.g)}${Ye(this.b)}`}function Ie(){const t=Oe(this.opacity);return`${1===t?"rgb(":"rgba("}${Be(this.r)}, ${Be(this.g)}, ${Be(this.b)}${1===t?")":`, ${t})`}`}function Oe(t){return isNaN(t)?1:Math.max(0,Math.min(1,t))}function Be(t){return Math.max(0,Math.min(255,Math.round(t)||0))}function Ye(t){return((t=Be(t))<16?"0":"")+t.toString(16)}function Le(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Xe(t,n,e,r)}function je(t){if(t instanceof Xe)return new Xe(t.h,t.s,t.l,t.opacity);if(t instanceof ye||(t=ze(t)),!t)return new Xe;if(t instanceof Xe)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,c=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&c<1?0:a,new Xe(a,u,c,t.opacity)}function He(t,n,e,r){return 1===arguments.length?je(t):new Xe(t,n,e,null==r?1:r)}function Xe(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Ge(t){return(t=(t||0)%360)<0?t+360:t}function Ve(t){return Math.max(0,Math.min(1,t||0))}function We(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}pe(ye,ze,{copy(t){return Object.assign(new this.constructor,this,t)},displayable(){return this.rgb().displayable()},hex:Ce,formatHex:Ce,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return je(this).formatHsl()},formatRgb:Pe,toString:Pe}),pe(qe,Fe,ge(ye,{brighter(t){return t=null==t?_e:Math.pow(_e,t),new qe(this.r*t,this.g*t,this.b*t,this.opacity)},darker(t){return t=null==t?ve:Math.pow(ve,t),new qe(this.r*t,this.g*t,this.b*t,this.opacity)},rgb(){return this},clamp(){return new qe(Be(this.r),Be(this.g),Be(this.b),Oe(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:Ue,formatHex:Ue,formatHex8:function(){return`#${Ye(this.r)}${Ye(this.g)}${Ye(this.b)}${Ye(255*(isNaN(this.opacity)?1:this.opacity))}`},formatRgb:Ie,toString:Ie})),pe(Xe,He,ge(ye,{brighter(t){return t=null==t?_e:Math.pow(_e,t),new Xe(this.h,this.s,this.l*t,this.opacity)},darker(t){return t=null==t?ve:Math.pow(ve,t),new Xe(this.h,this.s,this.l*t,this.opacity)},rgb(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new qe(We(t>=240?t-240:t+120,i,r),We(t,i,r),We(t<120?t+240:t-120,i,r),this.opacity)},clamp(){return new Xe(Ge(this.h),Ve(this.s),Ve(this.l),Oe(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const t=Oe(this.opacity);return`${1===t?"hsl(":"hsla("}${Ge(this.h)}, ${100*Ve(this.s)}%, ${100*Ve(this.l)}%${1===t?")":`, ${t})`}`}}));const Ze=Math.PI/180,Ke=180/Math.PI,Qe=.96422,Je=1,tr=.82521,nr=4/29,er=6/29,rr=3*er*er,ir=er*er*er;function or(t){if(t instanceof ur)return new ur(t.l,t.a,t.b,t.opacity);if(t instanceof pr)return gr(t);t instanceof qe||(t=Re(t));var n,e,r=lr(t.r),i=lr(t.g),o=lr(t.b),a=cr((.2225045*r+.7168786*i+.0606169*o)/Je);return r===i&&i===o?n=e=a:(n=cr((.4360747*r+.3850649*i+.1430804*o)/Qe),e=cr((.0139322*r+.0971045*i+.7141733*o)/tr)),new ur(116*a-16,500*(n-a),200*(a-e),t.opacity)}function ar(t,n,e,r){return 1===arguments.length?or(t):new ur(t,n,e,null==r?1:r)}function ur(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function cr(t){return t>ir?Math.pow(t,1/3):t/rr+nr}function fr(t){return t>er?t*t*t:rr*(t-nr)}function sr(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function lr(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function hr(t){if(t instanceof pr)return new pr(t.h,t.c,t.l,t.opacity);if(t instanceof ur||(t=or(t)),0===t.a&&0===t.b)return new pr(NaN,0=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],a=r>0?t[r-1]:2*i-o,u=r()=>t;function Cr(t,n){return function(e){return t+e*n}}function Pr(t,n){var e=n-t;return e?Cr(t,e>180||e<-180?e-360*Math.round(e/360):e):kr(isNaN(t)?n:t)}function zr(t){return 1==(t=+t)?$r:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):kr(isNaN(n)?e:n)}}function $r(t,n){var e=n-t;return e?Cr(t,e):kr(isNaN(t)?n:t)}var Dr=function t(n){var e=zr(n);function r(t,n){var r=e((t=Fe(t)).r,(n=Fe(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),a=$r(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}return r.gamma=t,r}(1);function Rr(t){return function(n){var e,r,i=n.length,o=new Array(i),a=new Array(i),u=new Array(i);for(e=0;eo&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,c.push({i:a,x:Yr(e,r)})),o=Hr.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:Yr(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,a.rotate,u,c),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:Yr(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,a.skewX,u,c),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:Yr(t,e)},{i:u-2,x:Yr(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,c),o=a=null,function(t){for(var n,e=-1,r=c.length;++e=0&&n._call.call(void 0,t),n=n._next;--yi}function Ci(){xi=(mi=Mi.now())+wi,yi=vi=0;try{ki()}finally{yi=0,function(){var t,n,e=pi,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:pi=n);gi=t,zi(r)}(),xi=0}}function Pi(){var t=Mi.now(),n=t-mi;n>bi&&(wi-=n,mi=t)}function zi(t){yi||(vi&&(vi=clearTimeout(vi)),t-xi>24?(t<1/0&&(vi=setTimeout(Ci,t-Mi.now()-wi)),_i&&(_i=clearInterval(_i))):(_i||(mi=Mi.now(),_i=setInterval(Pi,bi)),yi=1,Ti(Ci)))}function $i(t,n,e){var r=new Ei;return n=null==n?0:+n,r.restart((e=>{r.stop(),t(e+n)}),n,e),r}Ei.prototype=Ni.prototype={constructor:Ei,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?Ai():+e)+(null==n?0:+n),this._next||gi===this||(gi?gi._next=this:pi=this,gi=this),this._call=t,this._time=e,zi()},stop:function(){this._call&&(this._call=null,this._time=1/0,zi())}};var Di=$t("start","end","cancel","interrupt"),Ri=[],Fi=0,qi=1,Ui=2,Ii=3,Oi=4,Bi=5,Yi=6;function Li(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(t){e.state=qi,e.timer.restart(a,e.delay,e.time),e.delay<=t&&a(t-e.delay)}function a(o){var f,s,l,h;if(e.state!==qi)return c();for(f in i)if((h=i[f]).name===e.name){if(h.state===Ii)return $i(a);h.state===Oi?(h.state=Yi,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete i[f]):+fFi)throw new Error("too late; already scheduled");return e}function Hi(t,n){var e=Xi(t,n);if(e.state>Ii)throw new Error("too late; already running");return e}function Xi(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function Gi(t,n){var e,r,i,o=t.__transition,a=!0;if(o){for(i in n=null==n?null:n+"",o)(e=o[i]).name===n?(r=e.state>Ui&&e.state=0&&(t=t.slice(0,n)),!t||"start"===t}))}(n)?ji:Hi;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i}}(e,t,n))},attr:function(t,n){var e=It(t),r="transform"===e?ni:Ki;return this.attrTween(t,"function"==typeof n?(e.local?ro:eo)(e,r,Zi(this,"attr."+t,n)):null==n?(e.local?Ji:Qi)(e):(e.local?no:to)(e,r,n))},attrTween:function(t,n){var e="attr."+t;if(arguments.length<2)return(e=this.tween(e))&&e._value;if(null==n)return this.tween(e,null);if("function"!=typeof n)throw new Error;var r=It(t);return this.tween(e,(r.local?io:oo)(r,n))},style:function(t,n,e){var r="transform"==(t+="")?ti:Ki;return null==n?this.styleTween(t,function(t,n){var e,r,i;return function(){var o=_n(this,t),a=(this.style.removeProperty(t),_n(this,t));return o===a?null:o===e&&a===r?i:i=n(e=o,r=a)}}(t,r)).on("end.style."+t,lo(t)):"function"==typeof n?this.styleTween(t,function(t,n,e){var r,i,o;return function(){var a=_n(this,t),u=e(this),c=u+"";return null==u&&(this.style.removeProperty(t),c=u=_n(this,t)),a===c?null:a===r&&c===i?o:(i=c,o=n(r=a,u))}}(t,r,Zi(this,"style."+t,n))).each(function(t,n){var e,r,i,o,a="style."+n,u="end."+a;return function(){var c=Hi(this,t),f=c.on,s=null==c.value[a]?o||(o=lo(n)):void 0;f===e&&i===s||(r=(e=f).copy()).on(u,i=s),c.on=r}}(this._id,t)):this.styleTween(t,function(t,n,e){var r,i,o=e+"";return function(){var a=_n(this,t);return a===o?null:a===r?i:i=n(r=a,e)}}(t,r,n),e).on("end.style."+t,null)},styleTween:function(t,n,e){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==n)return this.tween(r,null);if("function"!=typeof n)throw new Error;return this.tween(r,function(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&function(t,n,e){return function(r){this.style.setProperty(t,n.call(this,r),e)}}(t,o,e)),r}return o._value=n,o}(t,n,null==e?"":e))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var n=t(this);this.textContent=null==n?"":n}}(Zi(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},textTween:function(t){var n="text";if(arguments.length<1)return(n=this.tween(n))&&n._value;if(null==t)return this.tween(n,null);if("function"!=typeof t)throw new Error;return this.tween(n,function(t){var n,e;function r(){var r=t.apply(this,arguments);return r!==e&&(n=(e=r)&&function(t){return function(n){this.textContent=t.call(this,n)}}(r)),n}return r._value=t,r}(t))},remove:function(){return this.on("end.remove",function(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}(this._id))},tween:function(t,n){var e=this._id;if(t+="",arguments.length<2){for(var r,i=Xi(this.node(),e).tween,o=0,a=i.length;o()=>t;function Qo(t,{sourceEvent:n,target:e,selection:r,mode:i,dispatch:o}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},target:{value:e,enumerable:!0,configurable:!0},selection:{value:r,enumerable:!0,configurable:!0},mode:{value:i,enumerable:!0,configurable:!0},_:{value:o}})}function Jo(t){t.preventDefault(),t.stopImmediatePropagation()}var ta={name:"drag"},na={name:"space"},ea={name:"handle"},ra={name:"center"};const{abs:ia,max:oa,min:aa}=Math;function ua(t){return[+t[0],+t[1]]}function ca(t){return[ua(t[0]),ua(t[1])]}var fa={name:"x",handles:["w","e"].map(va),input:function(t,n){return null==t?null:[[+t[0],n[0][1]],[+t[1],n[1][1]]]},output:function(t){return t&&[t[0][0],t[1][0]]}},sa={name:"y",handles:["n","s"].map(va),input:function(t,n){return null==t?null:[[n[0][0],+t[0]],[n[1][0],+t[1]]]},output:function(t){return t&&[t[0][1],t[1][1]]}},la={name:"xy",handles:["n","w","e","s","nw","ne","sw","se"].map(va),input:function(t){return null==t?null:ca(t)},output:function(t){return t}},ha={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},da={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"},pa={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"},ga={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1},ya={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1};function va(t){return{type:t}}function _a(t){return!t.ctrlKey&&!t.button}function ba(){var t=this.ownerSVGElement||this;return t.hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]}function ma(){return navigator.maxTouchPoints||"ontouchstart"in this}function xa(t){for(;!t.__brush;)if(!(t=t.parentNode))return;return t.__brush}function wa(t){var n,e=ba,r=_a,i=ma,o=!0,a=$t("start","brush","end"),u=6;function c(n){var e=n.property("__brush",g).selectAll(".overlay").data([va("overlay")]);e.enter().append("rect").attr("class","overlay").attr("pointer-events","all").attr("cursor",ha.overlay).merge(e).each((function(){var t=xa(this).extent;Zn(this).attr("x",t[0][0]).attr("y",t[0][1]).attr("width",t[1][0]-t[0][0]).attr("height",t[1][1]-t[0][1])})),n.selectAll(".selection").data([va("selection")]).enter().append("rect").attr("class","selection").attr("cursor",ha.selection).attr("fill","#777").attr("fill-opacity",.3).attr("stroke","#fff").attr("shape-rendering","crispEdges");var r=n.selectAll(".handle").data(t.handles,(function(t){return t.type}));r.exit().remove(),r.enter().append("rect").attr("class",(function(t){return"handle handle--"+t.type})).attr("cursor",(function(t){return ha[t.type]})),n.each(f).attr("fill","none").attr("pointer-events","all").on("mousedown.brush",h).filter(i).on("touchstart.brush",h).on("touchmove.brush",d).on("touchend.brush touchcancel.brush",p).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function f(){var t=Zn(this),n=xa(this).selection;n?(t.selectAll(".selection").style("display",null).attr("x",n[0][0]).attr("y",n[0][1]).attr("width",n[1][0]-n[0][0]).attr("height",n[1][1]-n[0][1]),t.selectAll(".handle").style("display",null).attr("x",(function(t){return"e"===t.type[t.type.length-1]?n[1][0]-u/2:n[0][0]-u/2})).attr("y",(function(t){return"s"===t.type[0]?n[1][1]-u/2:n[0][1]-u/2})).attr("width",(function(t){return"n"===t.type||"s"===t.type?n[1][0]-n[0][0]+u:u})).attr("height",(function(t){return"e"===t.type||"w"===t.type?n[1][1]-n[0][1]+u:u}))):t.selectAll(".selection,.handle").style("display","none").attr("x",null).attr("y",null).attr("width",null).attr("height",null)}function s(t,n,e){var r=t.__brush.emitter;return!r||e&&r.clean?new l(t,n,e):r}function l(t,n,e){this.that=t,this.args=n,this.state=t.__brush,this.active=0,this.clean=e}function h(e){if((!n||e.touches)&&r.apply(this,arguments)){var i,a,u,c,l,h,d,p,g,y,v,_=this,b=e.target.__data__.type,m="selection"===(o&&e.metaKey?b="overlay":b)?ta:o&&e.altKey?ra:ea,x=t===sa?null:ga[b],w=t===fa?null:ya[b],M=xa(_),T=M.extent,A=M.selection,S=T[0][0],E=T[0][1],N=T[1][0],k=T[1][1],C=0,P=0,z=x&&w&&o&&e.shiftKey,$=Array.from(e.touches||[e],(t=>{const n=t.identifier;return(t=ne(t,_)).point0=t.slice(),t.identifier=n,t}));Gi(_);var D=s(_,arguments,!0).beforestart();if("overlay"===b){A&&(g=!0);const n=[$[0],$[1]||$[0]];M.selection=A=[[i=t===sa?S:aa(n[0][0],n[1][0]),u=t===fa?E:aa(n[0][1],n[1][1])],[l=t===sa?N:oa(n[0][0],n[1][0]),d=t===fa?k:oa(n[0][1],n[1][1])]],$.length>1&&I(e)}else i=A[0][0],u=A[0][1],l=A[1][0],d=A[1][1];a=i,c=u,h=l,p=d;var R=Zn(_).attr("pointer-events","none"),F=R.selectAll(".overlay").attr("cursor",ha[b]);if(e.touches)D.moved=U,D.ended=O;else{var q=Zn(e.view).on("mousemove.brush",U,!0).on("mouseup.brush",O,!0);o&&q.on("keydown.brush",(function(t){switch(t.keyCode){case 16:z=x&&w;break;case 18:m===ea&&(x&&(l=h-C*x,i=a+C*x),w&&(d=p-P*w,u=c+P*w),m=ra,I(t));break;case 32:m!==ea&&m!==ra||(x<0?l=h-C:x>0&&(i=a-C),w<0?d=p-P:w>0&&(u=c-P),m=na,F.attr("cursor",ha.selection),I(t));break;default:return}Jo(t)}),!0).on("keyup.brush",(function(t){switch(t.keyCode){case 16:z&&(y=v=z=!1,I(t));break;case 18:m===ra&&(x<0?l=h:x>0&&(i=a),w<0?d=p:w>0&&(u=c),m=ea,I(t));break;case 32:m===na&&(t.altKey?(x&&(l=h-C*x,i=a+C*x),w&&(d=p-P*w,u=c+P*w),m=ra):(x<0?l=h:x>0&&(i=a),w<0?d=p:w>0&&(u=c),m=ea),F.attr("cursor",ha[b]),I(t));break;default:return}Jo(t)}),!0),ae(e.view)}f.call(_),D.start(e,m.name)}function U(t){for(const n of t.changedTouches||[t])for(const t of $)t.identifier===n.identifier&&(t.cur=ne(n,_));if(z&&!y&&!v&&1===$.length){const t=$[0];ia(t.cur[0]-t[0])>ia(t.cur[1]-t[1])?v=!0:y=!0}for(const t of $)t.cur&&(t[0]=t.cur[0],t[1]=t.cur[1]);g=!0,Jo(t),I(t)}function I(t){const n=$[0],e=n.point0;var r;switch(C=n[0]-e[0],P=n[1]-e[1],m){case na:case ta:x&&(C=oa(S-i,aa(N-l,C)),a=i+C,h=l+C),w&&(P=oa(E-u,aa(k-d,P)),c=u+P,p=d+P);break;case ea:$[1]?(x&&(a=oa(S,aa(N,$[0][0])),h=oa(S,aa(N,$[1][0])),x=1),w&&(c=oa(E,aa(k,$[0][1])),p=oa(E,aa(k,$[1][1])),w=1)):(x<0?(C=oa(S-i,aa(N-i,C)),a=i+C,h=l):x>0&&(C=oa(S-l,aa(N-l,C)),a=i,h=l+C),w<0?(P=oa(E-u,aa(k-u,P)),c=u+P,p=d):w>0&&(P=oa(E-d,aa(k-d,P)),c=u,p=d+P));break;case ra:x&&(a=oa(S,aa(N,i-C*x)),h=oa(S,aa(N,l+C*x))),w&&(c=oa(E,aa(k,u-P*w)),p=oa(E,aa(k,d+P*w)))}ht+e))}function za(t,n){var e=0,r=null,i=null,o=null;function a(a){var u,c=a.length,f=new Array(c),s=Pa(0,c),l=new Array(c*c),h=new Array(c),d=0;a=Float64Array.from({length:c*c},n?(t,n)=>a[n%c][n/c|0]:(t,n)=>a[n/c|0][n%c]);for(let n=0;nr(f[t],f[n])));for(const e of s){const r=n;if(t){const t=Pa(1+~c,c).filter((t=>t<0?a[~t*c+e]:a[e*c+t]));i&&t.sort(((t,n)=>i(t<0?-a[~t*c+e]:a[e*c+t],n<0?-a[~n*c+e]:a[e*c+n])));for(const r of t)if(r<0){(l[~r*c+e]||(l[~r*c+e]={source:null,target:null})).target={index:e,startAngle:n,endAngle:n+=a[~r*c+e]*d,value:a[~r*c+e]}}else{(l[e*c+r]||(l[e*c+r]={source:null,target:null})).source={index:e,startAngle:n,endAngle:n+=a[e*c+r]*d,value:a[e*c+r]}}h[e]={index:e,startAngle:r,endAngle:n,value:f[e]}}else{const t=Pa(0,c).filter((t=>a[e*c+t]||a[t*c+e]));i&&t.sort(((t,n)=>i(a[e*c+t],a[e*c+n])));for(const r of t){let t;if(e=0))throw new Error(`invalid digits: ${t}`);if(n>15)return qa;const e=10**n;return function(t){this._+=t[0];for(let n=1,r=t.length;nRa)if(Math.abs(s*u-c*f)>Ra&&i){let h=e-o,d=r-a,p=u*u+c*c,g=h*h+d*d,y=Math.sqrt(p),v=Math.sqrt(l),_=i*Math.tan(($a-Math.acos((p+l-g)/(2*y*v)))/2),b=_/v,m=_/y;Math.abs(b-1)>Ra&&this._append`L${t+b*f},${n+b*s}`,this._append`A${i},${i},0,0,${+(s*h>f*d)},${this._x1=t+m*u},${this._y1=n+m*c}`}else this._append`L${this._x1=t},${this._y1=n}`;else;}arc(t,n,e,r,i,o){if(t=+t,n=+n,o=!!o,(e=+e)<0)throw new Error(`negative radius: ${e}`);let a=e*Math.cos(r),u=e*Math.sin(r),c=t+a,f=n+u,s=1^o,l=o?r-i:i-r;null===this._x1?this._append`M${c},${f}`:(Math.abs(this._x1-c)>Ra||Math.abs(this._y1-f)>Ra)&&this._append`L${c},${f}`,e&&(l<0&&(l=l%Da+Da),l>Fa?this._append`A${e},${e},0,1,${s},${t-a},${n-u}A${e},${e},0,1,${s},${this._x1=c},${this._y1=f}`:l>Ra&&this._append`A${e},${e},0,${+(l>=$a)},${s},${this._x1=t+e*Math.cos(i)},${this._y1=n+e*Math.sin(i)}`)}rect(t,n,e,r){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}h${e=+e}v${+r}h${-e}Z`}toString(){return this._}};function Ia(){return new Ua}Ia.prototype=Ua.prototype;var Oa=Array.prototype.slice;function Ba(t){return function(){return t}}function Ya(t){return t.source}function La(t){return t.target}function ja(t){return t.radius}function Ha(t){return t.startAngle}function Xa(t){return t.endAngle}function Ga(){return 0}function Va(){return 10}function Wa(t){var n=Ya,e=La,r=ja,i=ja,o=Ha,a=Xa,u=Ga,c=null;function f(){var f,s=n.apply(this,arguments),l=e.apply(this,arguments),h=u.apply(this,arguments)/2,d=Oa.call(arguments),p=+r.apply(this,(d[0]=s,d)),g=o.apply(this,d)-Ea,y=a.apply(this,d)-Ea,v=+i.apply(this,(d[0]=l,d)),_=o.apply(this,d)-Ea,b=a.apply(this,d)-Ea;if(c||(c=f=Ia()),h>Ca&&(Ma(y-g)>2*h+Ca?y>g?(g+=h,y-=h):(g-=h,y+=h):g=y=(g+y)/2,Ma(b-_)>2*h+Ca?b>_?(_+=h,b-=h):(_-=h,b+=h):_=b=(_+b)/2),c.moveTo(p*Ta(g),p*Aa(g)),c.arc(0,0,p,g,y),g!==_||y!==b)if(t){var m=v-+t.apply(this,arguments),x=(_+b)/2;c.quadraticCurveTo(0,0,m*Ta(_),m*Aa(_)),c.lineTo(v*Ta(x),v*Aa(x)),c.lineTo(m*Ta(b),m*Aa(b))}else c.quadraticCurveTo(0,0,v*Ta(_),v*Aa(_)),c.arc(0,0,v,_,b);if(c.quadraticCurveTo(0,0,p*Ta(g),p*Aa(g)),c.closePath(),f)return c=null,f+""||null}return t&&(f.headRadius=function(n){return arguments.length?(t="function"==typeof n?n:Ba(+n),f):t}),f.radius=function(t){return arguments.length?(r=i="function"==typeof t?t:Ba(+t),f):r},f.sourceRadius=function(t){return arguments.length?(r="function"==typeof t?t:Ba(+t),f):r},f.targetRadius=function(t){return arguments.length?(i="function"==typeof t?t:Ba(+t),f):i},f.startAngle=function(t){return arguments.length?(o="function"==typeof t?t:Ba(+t),f):o},f.endAngle=function(t){return arguments.length?(a="function"==typeof t?t:Ba(+t),f):a},f.padAngle=function(t){return arguments.length?(u="function"==typeof t?t:Ba(+t),f):u},f.source=function(t){return arguments.length?(n=t,f):n},f.target=function(t){return arguments.length?(e=t,f):e},f.context=function(t){return arguments.length?(c=null==t?null:t,f):c},f}var Za=Array.prototype.slice;function Ka(t,n){return t-n}var Qa=t=>()=>t;function Ja(t,n){for(var e,r=-1,i=n.length;++rr!=d>r&&e<(h-f)*(r-s)/(d-s)+f&&(i=-i)}return i}function nu(t,n,e){var r,i,o,a;return function(t,n,e){return(n[0]-t[0])*(e[1]-t[1])==(e[0]-t[0])*(n[1]-t[1])}(t,n,e)&&(i=t[r=+(t[0]===n[0])],o=e[r],a=n[r],i<=o&&o<=a||a<=o&&o<=i)}function eu(){}var ru=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]];function iu(){var t=1,n=1,e=K,r=u;function i(t){var n=e(t);if(Array.isArray(n))n=n.slice().sort(Ka);else{const e=M(t,ou);for(n=G(...Z(e[0],e[1],n),n);n[n.length-1]>=e[1];)n.pop();for(;n[1]o(t,n)))}function o(e,i){const o=null==i?NaN:+i;if(isNaN(o))throw new Error(`invalid value: ${i}`);var u=[],c=[];return function(e,r,i){var o,u,c,f,s,l,h=new Array,d=new Array;o=u=-1,f=au(e[0],r),ru[f<<1].forEach(p);for(;++o=r,ru[s<<2].forEach(p);for(;++o0?u.push([t]):c.push(t)})),c.forEach((function(t){for(var n,e=0,r=u.length;e0&&o0&&a=0&&o>=0))throw new Error("invalid size");return t=r,n=o,i},i.thresholds=function(t){return arguments.length?(e="function"==typeof t?t:Array.isArray(t)?Qa(Za.call(t)):Qa(t),i):e},i.smooth=function(t){return arguments.length?(r=t?u:eu,i):r===u},i}function ou(t){return isFinite(t)?t:NaN}function au(t,n){return null!=t&&+t>=n}function uu(t){return null==t||isNaN(t=+t)?-1/0:t}function cu(t,n,e,r){const i=r-n,o=e-n,a=isFinite(i)||isFinite(o)?i/o:Math.sign(i)/Math.sign(o);return isNaN(a)?t:t+a-.5}function fu(t){return t[0]}function su(t){return t[1]}function lu(){return 1}const hu=134217729,du=33306690738754706e-32;function pu(t,n,e,r,i){let o,a,u,c,f=n[0],s=r[0],l=0,h=0;s>f==s>-f?(o=f,f=n[++l]):(o=s,s=r[++h]);let d=0;if(lf==s>-f?(a=f+o,u=o-(a-f),f=n[++l]):(a=s+o,u=o-(a-s),s=r[++h]),o=a,0!==u&&(i[d++]=u);lf==s>-f?(a=o+f,c=a-o,u=o-(a-c)+(f-c),f=n[++l]):(a=o+s,c=a-o,u=o-(a-c)+(s-c),s=r[++h]),o=a,0!==u&&(i[d++]=u);for(;l=33306690738754716e-32*f?c:-function(t,n,e,r,i,o,a){let u,c,f,s,l,h,d,p,g,y,v,_,b,m,x,w,M,T;const A=t-i,S=e-i,E=n-o,N=r-o;m=A*N,h=hu*A,d=h-(h-A),p=A-d,h=hu*N,g=h-(h-N),y=N-g,x=p*y-(m-d*g-p*g-d*y),w=E*S,h=hu*E,d=h-(h-E),p=E-d,h=hu*S,g=h-(h-S),y=S-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,_u[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,_u[1]=b-(v+l)+(l-w),T=_+v,l=T-_,_u[2]=_-(T-l)+(v-l),_u[3]=T;let k=function(t,n){let e=n[0];for(let r=1;r=C||-k>=C)return k;if(l=t-A,u=t-(A+l)+(l-i),l=e-S,f=e-(S+l)+(l-i),l=n-E,c=n-(E+l)+(l-o),l=r-N,s=r-(N+l)+(l-o),0===u&&0===c&&0===f&&0===s)return k;if(C=vu*a+du*Math.abs(k),k+=A*s+N*u-(E*f+S*c),k>=C||-k>=C)return k;m=u*N,h=hu*u,d=h-(h-u),p=u-d,h=hu*N,g=h-(h-N),y=N-g,x=p*y-(m-d*g-p*g-d*y),w=c*S,h=hu*c,d=h-(h-c),p=c-d,h=hu*S,g=h-(h-S),y=S-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const P=pu(4,_u,4,wu,bu);m=A*s,h=hu*A,d=h-(h-A),p=A-d,h=hu*s,g=h-(h-s),y=s-g,x=p*y-(m-d*g-p*g-d*y),w=E*f,h=hu*E,d=h-(h-E),p=E-d,h=hu*f,g=h-(h-f),y=f-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const z=pu(P,bu,4,wu,mu);m=u*s,h=hu*u,d=h-(h-u),p=u-d,h=hu*s,g=h-(h-s),y=s-g,x=p*y-(m-d*g-p*g-d*y),w=c*f,h=hu*c,d=h-(h-c),p=c-d,h=hu*f,g=h-(h-f),y=f-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const $=pu(z,mu,4,wu,xu);return xu[$-1]}(t,n,e,r,i,o,f)}const Tu=Math.pow(2,-52),Au=new Uint32Array(512);class Su{static from(t,n=zu,e=$u){const r=t.length,i=new Float64Array(2*r);for(let o=0;o>1;if(n>0&&"number"!=typeof t[0])throw new Error("Expected coords to contain numbers.");this.coords=t;const e=Math.max(2*n-5,0);this._triangles=new Uint32Array(3*e),this._halfedges=new Int32Array(3*e),this._hashSize=Math.ceil(Math.sqrt(n)),this._hullPrev=new Uint32Array(n),this._hullNext=new Uint32Array(n),this._hullTri=new Uint32Array(n),this._hullHash=new Int32Array(this._hashSize).fill(-1),this._ids=new Uint32Array(n),this._dists=new Float64Array(n),this.update()}update(){const{coords:t,_hullPrev:n,_hullNext:e,_hullTri:r,_hullHash:i}=this,o=t.length>>1;let a=1/0,u=1/0,c=-1/0,f=-1/0;for(let n=0;nc&&(c=e),r>f&&(f=r),this._ids[n]=n}const s=(a+c)/2,l=(u+f)/2;let h,d,p,g=1/0;for(let n=0;n0&&(d=n,g=e)}let _=t[2*d],b=t[2*d+1],m=1/0;for(let n=0;nr&&(n[e++]=i,r=this._dists[i])}return this.hull=n.subarray(0,e),this.triangles=new Uint32Array(0),void(this.halfedges=new Uint32Array(0))}if(Mu(y,v,_,b,x,w)<0){const t=d,n=_,e=b;d=p,_=x,b=w,p=t,x=n,w=e}const M=function(t,n,e,r,i,o){const a=e-t,u=r-n,c=i-t,f=o-n,s=a*a+u*u,l=c*c+f*f,h=.5/(a*f-u*c),d=t+(f*s-u*l)*h,p=n+(a*l-c*s)*h;return{x:d,y:p}}(y,v,_,b,x,w);this._cx=M.x,this._cy=M.y;for(let n=0;n0&&Math.abs(f-o)<=Tu&&Math.abs(s-a)<=Tu)continue;if(o=f,a=s,c===h||c===d||c===p)continue;let l=0;for(let t=0,n=this._hashKey(f,s);t=0;)if(y=g,y===l){y=-1;break}if(-1===y)continue;let v=this._addTriangle(y,c,e[y],-1,-1,r[y]);r[c]=this._legalize(v+2),r[y]=v,T++;let _=e[y];for(;g=e[_],Mu(f,s,t[2*_],t[2*_+1],t[2*g],t[2*g+1])<0;)v=this._addTriangle(_,c,g,r[c],-1,r[_]),r[c]=this._legalize(v+2),e[_]=_,T--,_=g;if(y===l)for(;g=n[y],Mu(f,s,t[2*g],t[2*g+1],t[2*y],t[2*y+1])<0;)v=this._addTriangle(g,c,y,-1,r[y],r[g]),this._legalize(v+2),r[g]=v,e[y]=y,T--,y=g;this._hullStart=n[c]=y,e[y]=n[_]=c,e[c]=_,i[this._hashKey(f,s)]=c,i[this._hashKey(t[2*y],t[2*y+1])]=y}this.hull=new Uint32Array(T);for(let t=0,n=this._hullStart;t0?3-e:1+e)/4}(t-this._cx,n-this._cy)*this._hashSize)%this._hashSize}_legalize(t){const{_triangles:n,_halfedges:e,coords:r}=this;let i=0,o=0;for(;;){const a=e[t],u=t-t%3;if(o=u+(t+2)%3,-1===a){if(0===i)break;t=Au[--i];continue}const c=a-a%3,f=u+(t+1)%3,s=c+(a+2)%3,l=n[o],h=n[t],d=n[f],p=n[s];if(Nu(r[2*l],r[2*l+1],r[2*h],r[2*h+1],r[2*d],r[2*d+1],r[2*p],r[2*p+1])){n[t]=p,n[a]=l;const r=e[s];if(-1===r){let n=this._hullStart;do{if(this._hullTri[n]===s){this._hullTri[n]=t;break}n=this._hullPrev[n]}while(n!==this._hullStart)}this._link(t,r),this._link(a,e[o]),this._link(o,s);const u=c+(a+1)%3;i=e&&n[t[a]]>o;)t[a+1]=t[a--];t[a+1]=r}else{let i=e+1,o=r;Pu(t,e+r>>1,i),n[t[e]]>n[t[r]]&&Pu(t,e,r),n[t[i]]>n[t[r]]&&Pu(t,i,r),n[t[e]]>n[t[i]]&&Pu(t,e,i);const a=t[i],u=n[a];for(;;){do{i++}while(n[t[i]]u);if(o=o-e?(Cu(t,n,i,r),Cu(t,n,e,o-1)):(Cu(t,n,e,o-1),Cu(t,n,i,r))}}function Pu(t,n,e){const r=t[n];t[n]=t[e],t[e]=r}function zu(t){return t[0]}function $u(t){return t[1]}const Du=1e-6;class Ru{constructor(){this._x0=this._y0=this._x1=this._y1=null,this._=""}moveTo(t,n){this._+=`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}`}closePath(){null!==this._x1&&(this._x1=this._x0,this._y1=this._y0,this._+="Z")}lineTo(t,n){this._+=`L${this._x1=+t},${this._y1=+n}`}arc(t,n,e){const r=(t=+t)+(e=+e),i=n=+n;if(e<0)throw new Error("negative radius");null===this._x1?this._+=`M${r},${i}`:(Math.abs(this._x1-r)>Du||Math.abs(this._y1-i)>Du)&&(this._+="L"+r+","+i),e&&(this._+=`A${e},${e},0,1,1,${t-e},${n}A${e},${e},0,1,1,${this._x1=r},${this._y1=i}`)}rect(t,n,e,r){this._+=`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}h${+e}v${+r}h${-e}Z`}value(){return this._||null}}class Fu{constructor(){this._=[]}moveTo(t,n){this._.push([t,n])}closePath(){this._.push(this._[0].slice())}lineTo(t,n){this._.push([t,n])}value(){return this._.length?this._:null}}class qu{constructor(t,[n,e,r,i]=[0,0,960,500]){if(!((r=+r)>=(n=+n)&&(i=+i)>=(e=+e)))throw new Error("invalid bounds");this.delaunay=t,this._circumcenters=new Float64Array(2*t.points.length),this.vectors=new Float64Array(2*t.points.length),this.xmax=r,this.xmin=n,this.ymax=i,this.ymin=e,this._init()}update(){return this.delaunay.update(),this._init(),this}_init(){const{delaunay:{points:t,hull:n,triangles:e},vectors:r}=this;let i,o;const a=this.circumcenters=this._circumcenters.subarray(0,e.length/3*2);for(let r,u,c=0,f=0,s=e.length;c1;)i-=2;for(let t=2;t0){if(n>=this.ymax)return null;(i=(this.ymax-n)/r)0){if(t>=this.xmax)return null;(i=(this.xmax-t)/e)this.xmax?2:0)|(nthis.ymax?8:0)}_simplify(t){if(t&&t.length>4){for(let n=0;n2&&function(t){const{triangles:n,coords:e}=t;for(let t=0;t1e-10)return!1}return!0}(t)){this.collinear=Int32Array.from({length:n.length/2},((t,n)=>n)).sort(((t,e)=>n[2*t]-n[2*e]||n[2*t+1]-n[2*e+1]));const t=this.collinear[0],e=this.collinear[this.collinear.length-1],r=[n[2*t],n[2*t+1],n[2*e],n[2*e+1]],i=1e-8*Math.hypot(r[3]-r[1],r[2]-r[0]);for(let t=0,e=n.length/2;t0&&(this.triangles=new Int32Array(3).fill(-1),this.halfedges=new Int32Array(3).fill(-1),this.triangles[0]=r[0],o[r[0]]=1,2===r.length&&(o[r[1]]=0,this.triangles[1]=r[1],this.triangles[2]=r[1]))}voronoi(t){return new qu(this,t)}*neighbors(t){const{inedges:n,hull:e,_hullIndex:r,halfedges:i,triangles:o,collinear:a}=this;if(a){const n=a.indexOf(t);return n>0&&(yield a[n-1]),void(n=0&&i!==e&&i!==r;)e=i;return i}_step(t,n,e){const{inedges:r,hull:i,_hullIndex:o,halfedges:a,triangles:u,points:c}=this;if(-1===r[t]||!c.length)return(t+1)%(c.length>>1);let f=t,s=Iu(n-c[2*t],2)+Iu(e-c[2*t+1],2);const l=r[t];let h=l;do{let r=u[h];const l=Iu(n-c[2*r],2)+Iu(e-c[2*r+1],2);if(l9999?"+"+Ku(n,6):Ku(n,4))+"-"+Ku(t.getUTCMonth()+1,2)+"-"+Ku(t.getUTCDate(),2)+(o?"T"+Ku(e,2)+":"+Ku(r,2)+":"+Ku(i,2)+"."+Ku(o,3)+"Z":i?"T"+Ku(e,2)+":"+Ku(r,2)+":"+Ku(i,2)+"Z":r||e?"T"+Ku(e,2)+":"+Ku(r,2)+"Z":"")}function Ju(t){var n=new RegExp('["'+t+"\n\r]"),e=t.charCodeAt(0);function r(t,n){var r,i=[],o=t.length,a=0,u=0,c=o<=0,f=!1;function s(){if(c)return Hu;if(f)return f=!1,ju;var n,r,i=a;if(t.charCodeAt(i)===Xu){for(;a++=o?c=!0:(r=t.charCodeAt(a++))===Gu?f=!0:r===Vu&&(f=!0,t.charCodeAt(a)===Gu&&++a),t.slice(i+1,n-1).replace(/""/g,'"')}for(;amc(n,e).then((n=>(new DOMParser).parseFromString(n,t)))}var Sc=Ac("application/xml"),Ec=Ac("text/html"),Nc=Ac("image/svg+xml");function kc(t,n,e,r){if(isNaN(n)||isNaN(e))return t;var i,o,a,u,c,f,s,l,h,d=t._root,p={data:r},g=t._x0,y=t._y0,v=t._x1,_=t._y1;if(!d)return t._root=p,t;for(;d.length;)if((f=n>=(o=(g+v)/2))?g=o:v=o,(s=e>=(a=(y+_)/2))?y=a:_=a,i=d,!(d=d[l=s<<1|f]))return i[l]=p,t;if(u=+t._x.call(null,d.data),c=+t._y.call(null,d.data),n===u&&e===c)return p.next=d,i?i[l]=p:t._root=p,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(f=n>=(o=(g+v)/2))?g=o:v=o,(s=e>=(a=(y+_)/2))?y=a:_=a}while((l=s<<1|f)==(h=(c>=a)<<1|u>=o));return i[h]=d,i[l]=p,t}function Cc(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i}function Pc(t){return t[0]}function zc(t){return t[1]}function $c(t,n,e){var r=new Dc(null==n?Pc:n,null==e?zc:e,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function Dc(t,n,e,r,i,o){this._x=t,this._y=n,this._x0=e,this._y0=r,this._x1=i,this._y1=o,this._root=void 0}function Rc(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}var Fc=$c.prototype=Dc.prototype;function qc(t){return function(){return t}}function Uc(t){return 1e-6*(t()-.5)}function Ic(t){return t.x+t.vx}function Oc(t){return t.y+t.vy}function Bc(t){return t.index}function Yc(t,n){var e=t.get(n);if(!e)throw new Error("node not found: "+n);return e}Fc.copy=function(){var t,n,e=new Dc(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=Rc(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=Rc(n));return e},Fc.add=function(t){const n=+this._x.call(null,t),e=+this._y.call(null,t);return kc(this.cover(n,e),n,e,t)},Fc.addAll=function(t){var n,e,r,i,o=t.length,a=new Array(o),u=new Array(o),c=1/0,f=1/0,s=-1/0,l=-1/0;for(e=0;es&&(s=r),il&&(l=i));if(c>s||f>l)return this;for(this.cover(c,f).cover(s,l),e=0;et||t>=i||r>n||n>=o;)switch(u=(nh||(o=c.y0)>d||(a=c.x1)=v)<<1|t>=y)&&(c=p[p.length-1],p[p.length-1]=p[p.length-1-f],p[p.length-1-f]=c)}else{var _=t-+this._x.call(null,g.data),b=n-+this._y.call(null,g.data),m=_*_+b*b;if(m=(u=(p+y)/2))?p=u:y=u,(s=a>=(c=(g+v)/2))?g=c:v=c,n=d,!(d=d[l=s<<1|f]))return this;if(!d.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(d=n[0]||n[1]||n[2]||n[3])&&d===(n[3]||n[2]||n[1]||n[0])&&!d.length&&(e?e[h]=d:this._root=d),this):(this._root=i,this)},Fc.removeAll=function(t){for(var n=0,e=t.length;n1?r[0]+r.slice(2):r,+t.slice(e+1)]}function Zc(t){return(t=Wc(Math.abs(t)))?t[1]:NaN}var Kc,Qc=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function Jc(t){if(!(n=Qc.exec(t)))throw new Error("invalid format: "+t);var n;return new tf({fill:n[1],align:n[2],sign:n[3],symbol:n[4],zero:n[5],width:n[6],comma:n[7],precision:n[8]&&n[8].slice(1),trim:n[9],type:n[10]})}function tf(t){this.fill=void 0===t.fill?" ":t.fill+"",this.align=void 0===t.align?">":t.align+"",this.sign=void 0===t.sign?"-":t.sign+"",this.symbol=void 0===t.symbol?"":t.symbol+"",this.zero=!!t.zero,this.width=void 0===t.width?void 0:+t.width,this.comma=!!t.comma,this.precision=void 0===t.precision?void 0:+t.precision,this.trim=!!t.trim,this.type=void 0===t.type?"":t.type+""}function nf(t,n){var e=Wc(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}Jc.prototype=tf.prototype,tf.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};var ef={"%":(t,n)=>(100*t).toFixed(n),b:t=>Math.round(t).toString(2),c:t=>t+"",d:function(t){return Math.abs(t=Math.round(t))>=1e21?t.toLocaleString("en").replace(/,/g,""):t.toString(10)},e:(t,n)=>t.toExponential(n),f:(t,n)=>t.toFixed(n),g:(t,n)=>t.toPrecision(n),o:t=>Math.round(t).toString(8),p:(t,n)=>nf(100*t,n),r:nf,s:function(t,n){var e=Wc(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(Kc=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,a=r.length;return o===a?r:o>a?r+new Array(o-a+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+Wc(t,Math.max(0,n+o-1))[0]},X:t=>Math.round(t).toString(16).toUpperCase(),x:t=>Math.round(t).toString(16)};function rf(t){return t}var of,af=Array.prototype.map,uf=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function cf(t){var n,e,r=void 0===t.grouping||void 0===t.thousands?rf:(n=af.call(t.grouping,Number),e=t.thousands+"",function(t,r){for(var i=t.length,o=[],a=0,u=n[0],c=0;i>0&&u>0&&(c+u+1>r&&(u=Math.max(1,r-c)),o.push(t.substring(i-=u,i+u)),!((c+=u+1)>r));)u=n[a=(a+1)%n.length];return o.reverse().join(e)}),i=void 0===t.currency?"":t.currency[0]+"",o=void 0===t.currency?"":t.currency[1]+"",a=void 0===t.decimal?".":t.decimal+"",u=void 0===t.numerals?rf:function(t){return function(n){return n.replace(/[0-9]/g,(function(n){return t[+n]}))}}(af.call(t.numerals,String)),c=void 0===t.percent?"%":t.percent+"",f=void 0===t.minus?"−":t.minus+"",s=void 0===t.nan?"NaN":t.nan+"";function l(t){var n=(t=Jc(t)).fill,e=t.align,l=t.sign,h=t.symbol,d=t.zero,p=t.width,g=t.comma,y=t.precision,v=t.trim,_=t.type;"n"===_?(g=!0,_="g"):ef[_]||(void 0===y&&(y=12),v=!0,_="g"),(d||"0"===n&&"="===e)&&(d=!0,n="0",e="=");var b="$"===h?i:"#"===h&&/[boxX]/.test(_)?"0"+_.toLowerCase():"",m="$"===h?o:/[%p]/.test(_)?c:"",x=ef[_],w=/[defgprs%]/.test(_);function M(t){var i,o,c,h=b,M=m;if("c"===_)M=x(t)+M,t="";else{var T=(t=+t)<0||1/t<0;if(t=isNaN(t)?s:x(Math.abs(t),y),v&&(t=function(t){t:for(var n,e=t.length,r=1,i=-1;r0&&(i=0)}return i>0?t.slice(0,i)+t.slice(n+1):t}(t)),T&&0==+t&&"+"!==l&&(T=!1),h=(T?"("===l?l:f:"-"===l||"("===l?"":l)+h,M=("s"===_?uf[8+Kc/3]:"")+M+(T&&"("===l?")":""),w)for(i=-1,o=t.length;++i(c=t.charCodeAt(i))||c>57){M=(46===c?a+t.slice(i+1):t.slice(i))+M,t=t.slice(0,i);break}}g&&!d&&(t=r(t,1/0));var A=h.length+t.length+M.length,S=A>1)+h+t+M+S.slice(A);break;default:t=S+h+t+M}return u(t)}return y=void 0===y?6:/[gprs]/.test(_)?Math.max(1,Math.min(21,y)):Math.max(0,Math.min(20,y)),M.toString=function(){return t+""},M}return{format:l,formatPrefix:function(t,n){var e=l(((t=Jc(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(Zc(n)/3))),i=Math.pow(10,-r),o=uf[8+r/3];return function(t){return e(i*t)+o}}}}function ff(n){return of=cf(n),t.format=of.format,t.formatPrefix=of.formatPrefix,of}function sf(t){return Math.max(0,-Zc(Math.abs(t)))}function lf(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Zc(n)/3)))-Zc(Math.abs(t)))}function hf(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Zc(n)-Zc(t))+1}t.format=void 0,t.formatPrefix=void 0,ff({thousands:",",grouping:[3],currency:["$",""]});var df=1e-6,pf=1e-12,gf=Math.PI,yf=gf/2,vf=gf/4,_f=2*gf,bf=180/gf,mf=gf/180,xf=Math.abs,wf=Math.atan,Mf=Math.atan2,Tf=Math.cos,Af=Math.ceil,Sf=Math.exp,Ef=Math.hypot,Nf=Math.log,kf=Math.pow,Cf=Math.sin,Pf=Math.sign||function(t){return t>0?1:t<0?-1:0},zf=Math.sqrt,$f=Math.tan;function Df(t){return t>1?0:t<-1?gf:Math.acos(t)}function Rf(t){return t>1?yf:t<-1?-yf:Math.asin(t)}function Ff(t){return(t=Cf(t/2))*t}function qf(){}function Uf(t,n){t&&Of.hasOwnProperty(t.type)&&Of[t.type](t,n)}var If={Feature:function(t,n){Uf(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r=0?1:-1,i=r*e,o=Tf(n=(n*=mf)/2+vf),a=Cf(n),u=Vf*a,c=Gf*o+u*Tf(i),f=u*r*Cf(i);as.add(Mf(f,c)),Xf=t,Gf=o,Vf=a}function ds(t){return[Mf(t[1],t[0]),Rf(t[2])]}function ps(t){var n=t[0],e=t[1],r=Tf(e);return[r*Tf(n),r*Cf(n),Cf(e)]}function gs(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function ys(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function vs(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function _s(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function bs(t){var n=zf(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}var ms,xs,ws,Ms,Ts,As,Ss,Es,Ns,ks,Cs,Ps,zs,$s,Ds,Rs,Fs={point:qs,lineStart:Is,lineEnd:Os,polygonStart:function(){Fs.point=Bs,Fs.lineStart=Ys,Fs.lineEnd=Ls,rs=new T,cs.polygonStart()},polygonEnd:function(){cs.polygonEnd(),Fs.point=qs,Fs.lineStart=Is,Fs.lineEnd=Os,as<0?(Wf=-(Kf=180),Zf=-(Qf=90)):rs>df?Qf=90:rs<-df&&(Zf=-90),os[0]=Wf,os[1]=Kf},sphere:function(){Wf=-(Kf=180),Zf=-(Qf=90)}};function qs(t,n){is.push(os=[Wf=t,Kf=t]),nQf&&(Qf=n)}function Us(t,n){var e=ps([t*mf,n*mf]);if(es){var r=ys(es,e),i=ys([r[1],-r[0],0],r);bs(i),i=ds(i);var o,a=t-Jf,u=a>0?1:-1,c=i[0]*bf*u,f=xf(a)>180;f^(u*JfQf&&(Qf=o):f^(u*Jf<(c=(c+360)%360-180)&&cQf&&(Qf=n)),f?tjs(Wf,Kf)&&(Kf=t):js(t,Kf)>js(Wf,Kf)&&(Wf=t):Kf>=Wf?(tKf&&(Kf=t)):t>Jf?js(Wf,t)>js(Wf,Kf)&&(Kf=t):js(t,Kf)>js(Wf,Kf)&&(Wf=t)}else is.push(os=[Wf=t,Kf=t]);nQf&&(Qf=n),es=e,Jf=t}function Is(){Fs.point=Us}function Os(){os[0]=Wf,os[1]=Kf,Fs.point=qs,es=null}function Bs(t,n){if(es){var e=t-Jf;rs.add(xf(e)>180?e+(e>0?360:-360):e)}else ts=t,ns=n;cs.point(t,n),Us(t,n)}function Ys(){cs.lineStart()}function Ls(){Bs(ts,ns),cs.lineEnd(),xf(rs)>df&&(Wf=-(Kf=180)),os[0]=Wf,os[1]=Kf,es=null}function js(t,n){return(n-=t)<0?n+360:n}function Hs(t,n){return t[0]-n[0]}function Xs(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:ngf&&(t-=Math.round(t/_f)*_f),[t,n]}function ul(t,n,e){return(t%=_f)?n||e?ol(fl(t),sl(n,e)):fl(t):n||e?sl(n,e):al}function cl(t){return function(n,e){return xf(n+=t)>gf&&(n-=Math.round(n/_f)*_f),[n,e]}}function fl(t){var n=cl(t);return n.invert=cl(-t),n}function sl(t,n){var e=Tf(t),r=Cf(t),i=Tf(n),o=Cf(n);function a(t,n){var a=Tf(n),u=Tf(t)*a,c=Cf(t)*a,f=Cf(n),s=f*e+u*r;return[Mf(c*i-s*o,u*e-f*r),Rf(s*i+c*o)]}return a.invert=function(t,n){var a=Tf(n),u=Tf(t)*a,c=Cf(t)*a,f=Cf(n),s=f*i-c*o;return[Mf(c*i+f*o,u*e+s*r),Rf(s*e-u*r)]},a}function ll(t){function n(n){return(n=t(n[0]*mf,n[1]*mf))[0]*=bf,n[1]*=bf,n}return t=ul(t[0]*mf,t[1]*mf,t.length>2?t[2]*mf:0),n.invert=function(n){return(n=t.invert(n[0]*mf,n[1]*mf))[0]*=bf,n[1]*=bf,n},n}function hl(t,n,e,r,i,o){if(e){var a=Tf(n),u=Cf(n),c=r*e;null==i?(i=n+r*_f,o=n-c/2):(i=dl(a,i),o=dl(a,o),(r>0?io)&&(i+=r*_f));for(var f,s=i;r>0?s>o:s1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}}function gl(t,n){return xf(t[0]-n[0])=0;--o)i.point((s=f[o])[0],s[1]);else r(h.x,h.p.x,-1,i);h=h.p}f=(h=h.o).z,d=!d}while(!h.v);i.lineEnd()}}}function _l(t){if(n=t.length){for(var n,e,r=0,i=t[0];++r=0?1:-1,E=S*A,N=E>gf,k=y*w;if(c.add(Mf(k*S*Cf(E),v*M+k*Tf(E))),a+=N?A+S*_f:A,N^p>=e^m>=e){var C=ys(ps(d),ps(b));bs(C);var P=ys(o,C);bs(P);var z=(N^A>=0?-1:1)*Rf(P[2]);(r>z||r===z&&(C[0]||C[1]))&&(u+=N^A>=0?1:-1)}}return(a<-df||a0){for(l||(i.polygonStart(),l=!0),i.lineStart(),t=0;t1&&2&c&&h.push(h.pop().concat(h.shift())),a.push(h.filter(wl))}return h}}function wl(t){return t.length>1}function Ml(t,n){return((t=t.x)[0]<0?t[1]-yf-df:yf-t[1])-((n=n.x)[0]<0?n[1]-yf-df:yf-n[1])}al.invert=al;var Tl=xl((function(){return!0}),(function(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,a){var u=o>0?gf:-gf,c=xf(o-e);xf(c-gf)0?yf:-yf),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),t.point(o,r),n=0):i!==u&&c>=gf&&(xf(e-i)df?wf((Cf(n)*(o=Tf(r))*Cf(e)-Cf(r)*(i=Tf(n))*Cf(t))/(i*o*a)):(n+r)/2}(e,r,o,a),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),n=0),t.point(e=o,r=a),i=u},lineEnd:function(){t.lineEnd(),e=r=NaN},clean:function(){return 2-n}}}),(function(t,n,e,r){var i;if(null==t)i=e*yf,r.point(-gf,i),r.point(0,i),r.point(gf,i),r.point(gf,0),r.point(gf,-i),r.point(0,-i),r.point(-gf,-i),r.point(-gf,0),r.point(-gf,i);else if(xf(t[0]-n[0])>df){var o=t[0]0,i=xf(n)>df;function o(t,e){return Tf(t)*Tf(e)>n}function a(t,e,r){var i=[1,0,0],o=ys(ps(t),ps(e)),a=gs(o,o),u=o[0],c=a-u*u;if(!c)return!r&&t;var f=n*a/c,s=-n*u/c,l=ys(i,o),h=_s(i,f);vs(h,_s(o,s));var d=l,p=gs(h,d),g=gs(d,d),y=p*p-g*(gs(h,h)-1);if(!(y<0)){var v=zf(y),_=_s(d,(-p-v)/g);if(vs(_,h),_=ds(_),!r)return _;var b,m=t[0],x=e[0],w=t[1],M=e[1];x0^_[1]<(xf(_[0]-m)gf^(m<=_[0]&&_[0]<=x)){var S=_s(d,(-p+v)/g);return vs(S,h),[_,ds(S)]}}}function u(n,e){var i=r?t:gf-t,o=0;return n<-i?o|=1:n>i&&(o|=2),e<-i?o|=4:e>i&&(o|=8),o}return xl(o,(function(t){var n,e,c,f,s;return{lineStart:function(){f=c=!1,s=1},point:function(l,h){var d,p=[l,h],g=o(l,h),y=r?g?0:u(l,h):g?u(l+(l<0?gf:-gf),h):0;if(!n&&(f=c=g)&&t.lineStart(),g!==c&&(!(d=a(n,p))||gl(n,d)||gl(p,d))&&(p[2]=1),g!==c)s=0,g?(t.lineStart(),d=a(p,n),t.point(d[0],d[1])):(d=a(n,p),t.point(d[0],d[1],2),t.lineEnd()),n=d;else if(i&&n&&r^g){var v;y&e||!(v=a(p,n,!0))||(s=0,r?(t.lineStart(),t.point(v[0][0],v[0][1]),t.point(v[1][0],v[1][1]),t.lineEnd()):(t.point(v[1][0],v[1][1]),t.lineEnd(),t.lineStart(),t.point(v[0][0],v[0][1],3)))}!g||n&&gl(n,p)||t.point(p[0],p[1]),n=p,c=g,e=y},lineEnd:function(){c&&t.lineEnd(),n=null},clean:function(){return s|(f&&c)<<1}}}),(function(n,r,i,o){hl(o,t,e,i,n,r)}),r?[0,-t]:[-gf,t-gf])}var Sl,El,Nl,kl,Cl=1e9,Pl=-Cl;function zl(t,n,e,r){function i(i,o){return t<=i&&i<=e&&n<=o&&o<=r}function o(i,o,u,f){var s=0,l=0;if(null==i||(s=a(i,u))!==(l=a(o,u))||c(i,o)<0^u>0)do{f.point(0===s||3===s?t:e,s>1?r:n)}while((s=(s+u+4)%4)!==l);else f.point(o[0],o[1])}function a(r,i){return xf(r[0]-t)0?0:3:xf(r[0]-e)0?2:1:xf(r[1]-n)0?1:0:i>0?3:2}function u(t,n){return c(t.x,n.x)}function c(t,n){var e=a(t,1),r=a(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(a){var c,f,s,l,h,d,p,g,y,v,_,b=a,m=pl(),x={point:w,lineStart:function(){x.point=M,f&&f.push(s=[]);v=!0,y=!1,p=g=NaN},lineEnd:function(){c&&(M(l,h),d&&y&&m.rejoin(),c.push(m.result()));x.point=w,y&&b.lineEnd()},polygonStart:function(){b=m,c=[],f=[],_=!0},polygonEnd:function(){var n=function(){for(var n=0,e=0,i=f.length;er&&(h-o)*(r-a)>(d-a)*(t-o)&&++n:d<=r&&(h-o)*(r-a)<(d-a)*(t-o)&&--n;return n}(),e=_&&n,i=(c=ft(c)).length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),o(null,null,1,a),a.lineEnd()),i&&vl(c,u,n,o,a),a.polygonEnd());b=a,c=f=s=null}};function w(t,n){i(t,n)&&b.point(t,n)}function M(o,a){var u=i(o,a);if(f&&s.push([o,a]),v)l=o,h=a,d=u,v=!1,u&&(b.lineStart(),b.point(o,a));else if(u&&y)b.point(o,a);else{var c=[p=Math.max(Pl,Math.min(Cl,p)),g=Math.max(Pl,Math.min(Cl,g))],m=[o=Math.max(Pl,Math.min(Cl,o)),a=Math.max(Pl,Math.min(Cl,a))];!function(t,n,e,r,i,o){var a,u=t[0],c=t[1],f=0,s=1,l=n[0]-u,h=n[1]-c;if(a=e-u,l||!(a>0)){if(a/=l,l<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=i-u,l||!(a<0)){if(a/=l,l<0){if(a>s)return;a>f&&(f=a)}else if(l>0){if(a0)){if(a/=h,h<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=o-c,h||!(a<0)){if(a/=h,h<0){if(a>s)return;a>f&&(f=a)}else if(h>0){if(a0&&(t[0]=u+f*l,t[1]=c+f*h),s<1&&(n[0]=u+s*l,n[1]=c+s*h),!0}}}}}(c,m,t,n,e,r)?u&&(b.lineStart(),b.point(o,a),_=!1):(y||(b.lineStart(),b.point(c[0],c[1])),b.point(m[0],m[1]),u||b.lineEnd(),_=!1)}p=o,g=a,y=u}return x}}var $l={sphere:qf,point:qf,lineStart:function(){$l.point=Rl,$l.lineEnd=Dl},lineEnd:qf,polygonStart:qf,polygonEnd:qf};function Dl(){$l.point=$l.lineEnd=qf}function Rl(t,n){El=t*=mf,Nl=Cf(n*=mf),kl=Tf(n),$l.point=Fl}function Fl(t,n){t*=mf;var e=Cf(n*=mf),r=Tf(n),i=xf(t-El),o=Tf(i),a=r*Cf(i),u=kl*e-Nl*r*o,c=Nl*e+kl*r*o;Sl.add(Mf(zf(a*a+u*u),c)),El=t,Nl=e,kl=r}function ql(t){return Sl=new T,Lf(t,$l),+Sl}var Ul=[null,null],Il={type:"LineString",coordinates:Ul};function Ol(t,n){return Ul[0]=t,Ul[1]=n,ql(Il)}var Bl={Feature:function(t,n){return Ll(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r0&&(i=Ol(t[o],t[o-1]))>0&&e<=i&&r<=i&&(e+r-i)*(1-Math.pow((e-r)/i,2))df})).map(c)).concat(lt(Af(o/d)*d,i,d).filter((function(t){return xf(t%g)>df})).map(f))}return v.lines=function(){return _().map((function(t){return{type:"LineString",coordinates:t}}))},v.outline=function(){return{type:"Polygon",coordinates:[s(r).concat(l(a).slice(1),s(e).reverse().slice(1),l(u).reverse().slice(1))]}},v.extent=function(t){return arguments.length?v.extentMajor(t).extentMinor(t):v.extentMinor()},v.extentMajor=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],u=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),u>a&&(t=u,u=a,a=t),v.precision(y)):[[r,u],[e,a]]},v.extentMinor=function(e){return arguments.length?(n=+e[0][0],t=+e[1][0],o=+e[0][1],i=+e[1][1],n>t&&(e=n,n=t,t=e),o>i&&(e=o,o=i,i=e),v.precision(y)):[[n,o],[t,i]]},v.step=function(t){return arguments.length?v.stepMajor(t).stepMinor(t):v.stepMinor()},v.stepMajor=function(t){return arguments.length?(p=+t[0],g=+t[1],v):[p,g]},v.stepMinor=function(t){return arguments.length?(h=+t[0],d=+t[1],v):[h,d]},v.precision=function(h){return arguments.length?(y=+h,c=Wl(o,i,90),f=Zl(n,t,y),s=Wl(u,a,90),l=Zl(r,e,y),v):y},v.extentMajor([[-180,-90+df],[180,90-df]]).extentMinor([[-180,-80-df],[180,80+df]])}var Ql,Jl,th,nh,eh=t=>t,rh=new T,ih=new T,oh={point:qf,lineStart:qf,lineEnd:qf,polygonStart:function(){oh.lineStart=ah,oh.lineEnd=fh},polygonEnd:function(){oh.lineStart=oh.lineEnd=oh.point=qf,rh.add(xf(ih)),ih=new T},result:function(){var t=rh/2;return rh=new T,t}};function ah(){oh.point=uh}function uh(t,n){oh.point=ch,Ql=th=t,Jl=nh=n}function ch(t,n){ih.add(nh*t-th*n),th=t,nh=n}function fh(){ch(Ql,Jl)}var sh=oh,lh=1/0,hh=lh,dh=-lh,ph=dh,gh={point:function(t,n){tdh&&(dh=t);nph&&(ph=n)},lineStart:qf,lineEnd:qf,polygonStart:qf,polygonEnd:qf,result:function(){var t=[[lh,hh],[dh,ph]];return dh=ph=-(hh=lh=1/0),t}};var yh,vh,_h,bh,mh=gh,xh=0,wh=0,Mh=0,Th=0,Ah=0,Sh=0,Eh=0,Nh=0,kh=0,Ch={point:Ph,lineStart:zh,lineEnd:Rh,polygonStart:function(){Ch.lineStart=Fh,Ch.lineEnd=qh},polygonEnd:function(){Ch.point=Ph,Ch.lineStart=zh,Ch.lineEnd=Rh},result:function(){var t=kh?[Eh/kh,Nh/kh]:Sh?[Th/Sh,Ah/Sh]:Mh?[xh/Mh,wh/Mh]:[NaN,NaN];return xh=wh=Mh=Th=Ah=Sh=Eh=Nh=kh=0,t}};function Ph(t,n){xh+=t,wh+=n,++Mh}function zh(){Ch.point=$h}function $h(t,n){Ch.point=Dh,Ph(_h=t,bh=n)}function Dh(t,n){var e=t-_h,r=n-bh,i=zf(e*e+r*r);Th+=i*(_h+t)/2,Ah+=i*(bh+n)/2,Sh+=i,Ph(_h=t,bh=n)}function Rh(){Ch.point=Ph}function Fh(){Ch.point=Uh}function qh(){Ih(yh,vh)}function Uh(t,n){Ch.point=Ih,Ph(yh=_h=t,vh=bh=n)}function Ih(t,n){var e=t-_h,r=n-bh,i=zf(e*e+r*r);Th+=i*(_h+t)/2,Ah+=i*(bh+n)/2,Sh+=i,Eh+=(i=bh*t-_h*n)*(_h+t),Nh+=i*(bh+n),kh+=3*i,Ph(_h=t,bh=n)}var Oh=Ch;function Bh(t){this._context=t}Bh.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,_f)}},result:qf};var Yh,Lh,jh,Hh,Xh,Gh=new T,Vh={point:qf,lineStart:function(){Vh.point=Wh},lineEnd:function(){Yh&&Zh(Lh,jh),Vh.point=qf},polygonStart:function(){Yh=!0},polygonEnd:function(){Yh=null},result:function(){var t=+Gh;return Gh=new T,t}};function Wh(t,n){Vh.point=Zh,Lh=Hh=t,jh=Xh=n}function Zh(t,n){Hh-=t,Xh-=n,Gh.add(zf(Hh*Hh+Xh*Xh)),Hh=t,Xh=n}var Kh=Vh;let Qh,Jh,td,nd;class ed{constructor(t){this._append=null==t?rd:function(t){const n=Math.floor(t);if(!(n>=0))throw new RangeError(`invalid digits: ${t}`);if(n>15)return rd;if(n!==Qh){const t=10**n;Qh=n,Jh=function(n){let e=1;this._+=n[0];for(const r=n.length;e4*n&&g--){var m=a+h,x=u+d,w=c+p,M=zf(m*m+x*x+w*w),T=Rf(w/=M),A=xf(xf(w)-1)n||xf((v*k+_*C)/b-.5)>.3||a*h+u*d+c*p2?t[2]%360*mf:0,k()):[y*bf,v*bf,_*bf]},E.angle=function(t){return arguments.length?(b=t%360*mf,k()):b*bf},E.reflectX=function(t){return arguments.length?(m=t?-1:1,k()):m<0},E.reflectY=function(t){return arguments.length?(x=t?-1:1,k()):x<0},E.precision=function(t){return arguments.length?(a=dd(u,S=t*t),C()):zf(S)},E.fitExtent=function(t,n){return ud(E,t,n)},E.fitSize=function(t,n){return cd(E,t,n)},E.fitWidth=function(t,n){return fd(E,t,n)},E.fitHeight=function(t,n){return sd(E,t,n)},function(){return n=t.apply(this,arguments),E.invert=n.invert&&N,k()}}function _d(t){var n=0,e=gf/3,r=vd(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*mf,e=t[1]*mf):[n*bf,e*bf]},i}function bd(t,n){var e=Cf(t),r=(e+Cf(n))/2;if(xf(r)0?n<-yf+df&&(n=-yf+df):n>yf-df&&(n=yf-df);var e=i/kf(Nd(n),r);return[e*Cf(r*t),i-e*Tf(r*t)]}return o.invert=function(t,n){var e=i-n,o=Pf(r)*zf(t*t+e*e),a=Mf(t,xf(e))*Pf(e);return e*r<0&&(a-=gf*Pf(t)*Pf(e)),[a/r,2*wf(kf(i/o,1/r))-yf]},o}function Cd(t,n){return[t,n]}function Pd(t,n){var e=Tf(t),r=t===n?Cf(t):(e-Tf(n))/(n-t),i=e/r+t;if(xf(r)=0;)n+=e[r].value;else n=1;t.value=n}function Gd(t,n){t instanceof Map?(t=[void 0,t],void 0===n&&(n=Wd)):void 0===n&&(n=Vd);for(var e,r,i,o,a,u=new Qd(t),c=[u];e=c.pop();)if((i=n(e.data))&&(a=(i=Array.from(i)).length))for(e.children=i,o=a-1;o>=0;--o)c.push(r=i[o]=new Qd(i[o])),r.parent=e,r.depth=e.depth+1;return u.eachBefore(Kd)}function Vd(t){return t.children}function Wd(t){return Array.isArray(t)?t[1]:null}function Zd(t){void 0!==t.data.value&&(t.value=t.data.value),t.data=t.data.data}function Kd(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function Qd(t){this.data=t,this.depth=this.height=0,this.parent=null}function Jd(t){return null==t?null:tp(t)}function tp(t){if("function"!=typeof t)throw new Error;return t}function np(){return 0}function ep(t){return function(){return t}}qd.invert=function(t,n){for(var e,r=n,i=r*r,o=i*i*i,a=0;a<12&&(o=(i=(r-=e=(r*(zd+$d*i+o*(Dd+Rd*i))-n)/(zd+3*$d*i+o*(7*Dd+9*Rd*i)))*r)*i*i,!(xf(e)df&&--i>0);return[t/(.8707+(o=r*r)*(o*(o*o*o*(.003971-.001529*o)-.013791)-.131979)),r]},Od.invert=Md(Rf),Bd.invert=Md((function(t){return 2*wf(t)})),Yd.invert=function(t,n){return[-n,2*wf(Sf(t))-yf]},Qd.prototype=Gd.prototype={constructor:Qd,count:function(){return this.eachAfter(Xd)},each:function(t,n){let e=-1;for(const r of this)t.call(n,r,++e,this);return this},eachAfter:function(t,n){for(var e,r,i,o=this,a=[o],u=[],c=-1;o=a.pop();)if(u.push(o),e=o.children)for(r=0,i=e.length;r=0;--r)o.push(e[r]);return this},find:function(t,n){let e=-1;for(const r of this)if(t.call(n,r,++e,this))return r},sum:function(t){return this.eachAfter((function(n){for(var e=+t(n.data)||0,r=n.children,i=r&&r.length;--i>=0;)e+=r[i].value;n.value=e}))},sort:function(t){return this.eachBefore((function(n){n.children&&n.children.sort(t)}))},path:function(t){for(var n=this,e=function(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;t=e.pop(),n=r.pop();for(;t===n;)i=t,t=e.pop(),n=r.pop();return i}(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},descendants:function(){return Array.from(this)},leaves:function(){var t=[];return this.eachBefore((function(n){n.children||t.push(n)})),t},links:function(){var t=this,n=[];return t.each((function(e){e!==t&&n.push({source:e.parent,target:e})})),n},copy:function(){return Gd(this).eachBefore(Zd)},[Symbol.iterator]:function*(){var t,n,e,r,i=this,o=[i];do{for(t=o.reverse(),o=[];i=t.pop();)if(yield i,n=i.children)for(e=0,r=n.length;e(t=(rp*t+ip)%op)/op}function up(t,n){for(var e,r,i=0,o=(t=function(t,n){let e,r,i=t.length;for(;i;)r=n()*i--|0,e=t[i],t[i]=t[r],t[r]=e;return t}(Array.from(t),n)).length,a=[];i0&&e*e>r*r+i*i}function lp(t,n){for(var e=0;e1e-6?(E+Math.sqrt(E*E-4*S*N))/(2*S):N/E);return{x:r+w+M*k,y:i+T+A*k,r:k}}function gp(t,n,e){var r,i,o,a,u=t.x-n.x,c=t.y-n.y,f=u*u+c*c;f?(i=n.r+e.r,i*=i,a=t.r+e.r,i>(a*=a)?(r=(f+a-i)/(2*f),o=Math.sqrt(Math.max(0,a/f-r*r)),e.x=t.x-r*u-o*c,e.y=t.y-r*c+o*u):(r=(f+i-a)/(2*f),o=Math.sqrt(Math.max(0,i/f-r*r)),e.x=n.x+r*u-o*c,e.y=n.y+r*c+o*u)):(e.x=n.x+e.r,e.y=n.y)}function yp(t,n){var e=t.r+n.r-1e-6,r=n.x-t.x,i=n.y-t.y;return e>0&&e*e>r*r+i*i}function vp(t){var n=t._,e=t.next._,r=n.r+e.r,i=(n.x*e.r+e.x*n.r)/r,o=(n.y*e.r+e.y*n.r)/r;return i*i+o*o}function _p(t){this._=t,this.next=null,this.previous=null}function bp(t,n){if(!(o=(t=function(t){return"object"==typeof t&&"length"in t?t:Array.from(t)}(t)).length))return 0;var e,r,i,o,a,u,c,f,s,l,h;if((e=t[0]).x=0,e.y=0,!(o>1))return e.r;if(r=t[1],e.x=-r.r,r.x=e.r,r.y=0,!(o>2))return e.r+r.r;gp(r,e,i=t[2]),e=new _p(e),r=new _p(r),i=new _p(i),e.next=i.previous=r,r.next=e.previous=i,i.next=r.previous=e;t:for(c=3;c1&&!zp(t,n););return t.slice(0,n)}function zp(t,n){if("/"===t[n]){let e=0;for(;n>0&&"\\"===t[--n];)++e;if(0==(1&e))return!0}return!1}function $p(t,n){return t.parent===n.parent?1:2}function Dp(t){var n=t.children;return n?n[0]:t.t}function Rp(t){var n=t.children;return n?n[n.length-1]:t.t}function Fp(t,n,e){var r=e/(n.i-t.i);n.c-=r,n.s+=e,t.c+=r,n.z+=e,n.m+=e}function qp(t,n,e){return t.a.parent===n.parent?t.a:e}function Up(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function Ip(t,n,e,r,i){for(var o,a=t.children,u=-1,c=a.length,f=t.value&&(i-e)/t.value;++uh&&(h=u),y=s*s*g,(d=Math.max(h/y,y/l))>p){s-=u;break}p=d}v.push(a={value:s,dice:c1?n:1)},e}(Op);var Lp=function t(n){function e(t,e,r,i,o){if((a=t._squarify)&&a.ratio===n)for(var a,u,c,f,s,l=-1,h=a.length,d=t.value;++l1?n:1)},e}(Op);function jp(t,n,e){return(n[0]-t[0])*(e[1]-t[1])-(n[1]-t[1])*(e[0]-t[0])}function Hp(t,n){return t[0]-n[0]||t[1]-n[1]}function Xp(t){const n=t.length,e=[0,1];let r,i=2;for(r=2;r1&&jp(t[e[i-2]],t[e[i-1]],t[r])<=0;)--i;e[i++]=r}return e.slice(0,i)}var Gp=Math.random,Vp=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,1===arguments.length?(e=t,t=0):e-=t,function(){return n()*e+t}}return e.source=t,e}(Gp),Wp=function t(n){function e(t,e){return arguments.length<2&&(e=t,t=0),t=Math.floor(t),e=Math.floor(e)-t,function(){return Math.floor(n()*e+t)}}return e.source=t,e}(Gp),Zp=function t(n){function e(t,e){var r,i;return t=null==t?0:+t,e=null==e?1:+e,function(){var o;if(null!=r)o=r,r=null;else do{r=2*n()-1,o=2*n()-1,i=r*r+o*o}while(!i||i>1);return t+e*o*Math.sqrt(-2*Math.log(i)/i)}}return e.source=t,e}(Gp),Kp=function t(n){var e=Zp.source(n);function r(){var t=e.apply(this,arguments);return function(){return Math.exp(t())}}return r.source=t,r}(Gp),Qp=function t(n){function e(t){return(t=+t)<=0?()=>0:function(){for(var e=0,r=t;r>1;--r)e+=n();return e+r*n()}}return e.source=t,e}(Gp),Jp=function t(n){var e=Qp.source(n);function r(t){if(0==(t=+t))return n;var r=e(t);return function(){return r()/t}}return r.source=t,r}(Gp),tg=function t(n){function e(t){return function(){return-Math.log1p(-n())/t}}return e.source=t,e}(Gp),ng=function t(n){function e(t){if((t=+t)<0)throw new RangeError("invalid alpha");return t=1/-t,function(){return Math.pow(1-n(),t)}}return e.source=t,e}(Gp),eg=function t(n){function e(t){if((t=+t)<0||t>1)throw new RangeError("invalid p");return function(){return Math.floor(n()+t)}}return e.source=t,e}(Gp),rg=function t(n){function e(t){if((t=+t)<0||t>1)throw new RangeError("invalid p");return 0===t?()=>1/0:1===t?()=>1:(t=Math.log1p(-t),function(){return 1+Math.floor(Math.log1p(-n())/t)})}return e.source=t,e}(Gp),ig=function t(n){var e=Zp.source(n)();function r(t,r){if((t=+t)<0)throw new RangeError("invalid k");if(0===t)return()=>0;if(r=null==r?1:+r,1===t)return()=>-Math.log1p(-n())*r;var i=(t<1?t+1:t)-1/3,o=1/(3*Math.sqrt(i)),a=t<1?()=>Math.pow(n(),1/t):()=>1;return function(){do{do{var t=e(),u=1+o*t}while(u<=0);u*=u*u;var c=1-n()}while(c>=1-.0331*t*t*t*t&&Math.log(c)>=.5*t*t+i*(1-u+Math.log(u)));return i*u*a()*r}}return r.source=t,r}(Gp),og=function t(n){var e=ig.source(n);function r(t,n){var r=e(t),i=e(n);return function(){var t=r();return 0===t?0:t/(t+i())}}return r.source=t,r}(Gp),ag=function t(n){var e=rg.source(n),r=og.source(n);function i(t,n){return t=+t,(n=+n)>=1?()=>t:n<=0?()=>0:function(){for(var i=0,o=t,a=n;o*a>16&&o*(1-a)>16;){var u=Math.floor((o+1)*a),c=r(u,o-u+1)();c<=a?(i+=u,o-=u,a=(a-c)/(1-c)):(o=u-1,a/=c)}for(var f=a<.5,s=e(f?a:1-a),l=s(),h=0;l<=o;++h)l+=s();return i+(f?h:o-h)}}return i.source=t,i}(Gp),ug=function t(n){function e(t,e,r){var i;return 0==(t=+t)?i=t=>-Math.log(t):(t=1/t,i=n=>Math.pow(n,t)),e=null==e?0:+e,r=null==r?1:+r,function(){return e+r*i(-Math.log1p(-n()))}}return e.source=t,e}(Gp),cg=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,function(){return t+e*Math.tan(Math.PI*n())}}return e.source=t,e}(Gp),fg=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,function(){var r=n();return t+e*Math.log(r/(1-r))}}return e.source=t,e}(Gp),sg=function t(n){var e=ig.source(n),r=ag.source(n);function i(t){return function(){for(var i=0,o=t;o>16;){var a=Math.floor(.875*o),u=e(a)();if(u>o)return i+r(a-1,o/u)();i+=a,o-=u}for(var c=-Math.log1p(-n()),f=0;c<=o;++f)c-=Math.log1p(-n());return i+f}}return i.source=t,i}(Gp);const lg=1/4294967296;function hg(t,n){switch(arguments.length){case 0:break;case 1:this.range(t);break;default:this.range(n).domain(t)}return this}function dg(t,n){switch(arguments.length){case 0:break;case 1:"function"==typeof t?this.interpolator(t):this.range(t);break;default:this.domain(t),"function"==typeof n?this.interpolator(n):this.range(n)}return this}const pg=Symbol("implicit");function gg(){var t=new InternMap,n=[],e=[],r=pg;function i(i){let o=t.get(i);if(void 0===o){if(r!==pg)return r;t.set(i,o=n.push(i)-1)}return e[o%e.length]}return i.domain=function(e){if(!arguments.length)return n.slice();n=[],t=new InternMap;for(const r of e)t.has(r)||t.set(r,n.push(r)-1);return i},i.range=function(t){return arguments.length?(e=Array.from(t),i):e.slice()},i.unknown=function(t){return arguments.length?(r=t,i):r},i.copy=function(){return gg(n,e).unknown(r)},hg.apply(i,arguments),i}function yg(){var t,n,e=gg().unknown(void 0),r=e.domain,i=e.range,o=0,a=1,u=!1,c=0,f=0,s=.5;function l(){var e=r().length,l=an&&(e=t,t=n,n=e),function(e){return Math.max(t,Math.min(n,e))}}(a[0],a[t-1])),r=t>2?Mg:wg,i=o=null,l}function l(n){return null==n||isNaN(n=+n)?e:(i||(i=r(a.map(t),u,c)))(t(f(n)))}return l.invert=function(e){return f(n((o||(o=r(u,a.map(t),Yr)))(e)))},l.domain=function(t){return arguments.length?(a=Array.from(t,_g),s()):a.slice()},l.range=function(t){return arguments.length?(u=Array.from(t),s()):u.slice()},l.rangeRound=function(t){return u=Array.from(t),c=Vr,s()},l.clamp=function(t){return arguments.length?(f=!!t||mg,s()):f!==mg},l.interpolate=function(t){return arguments.length?(c=t,s()):c},l.unknown=function(t){return arguments.length?(e=t,l):e},function(e,r){return t=e,n=r,s()}}function Sg(){return Ag()(mg,mg)}function Eg(n,e,r,i){var o,a=W(n,e,r);switch((i=Jc(null==i?",f":i)).type){case"s":var u=Math.max(Math.abs(n),Math.abs(e));return null!=i.precision||isNaN(o=lf(a,u))||(i.precision=o),t.formatPrefix(i,u);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(o=hf(a,Math.max(Math.abs(n),Math.abs(e))))||(i.precision=o-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(o=sf(a))||(i.precision=o-2*("%"===i.type))}return t.format(i)}function Ng(t){var n=t.domain;return t.ticks=function(t){var e=n();return G(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){var r=n();return Eg(r[0],r[r.length-1],null==t?10:t,e)},t.nice=function(e){null==e&&(e=10);var r,i,o=n(),a=0,u=o.length-1,c=o[a],f=o[u],s=10;for(f0;){if((i=V(c,f,e))===r)return o[a]=c,o[u]=f,n(o);if(i>0)c=Math.floor(c/i)*i,f=Math.ceil(f/i)*i;else{if(!(i<0))break;c=Math.ceil(c*i)/i,f=Math.floor(f*i)/i}r=i}return t},t}function kg(t,n){var e,r=0,i=(t=t.slice()).length-1,o=t[r],a=t[i];return a-t(-n,e)}function Fg(n){const e=n(Cg,Pg),r=e.domain;let i,o,a=10;function u(){return i=function(t){return t===Math.E?Math.log:10===t&&Math.log10||2===t&&Math.log2||(t=Math.log(t),n=>Math.log(n)/t)}(a),o=function(t){return 10===t?Dg:t===Math.E?Math.exp:n=>Math.pow(t,n)}(a),r()[0]<0?(i=Rg(i),o=Rg(o),n(zg,$g)):n(Cg,Pg),e}return e.base=function(t){return arguments.length?(a=+t,u()):a},e.domain=function(t){return arguments.length?(r(t),u()):r()},e.ticks=t=>{const n=r();let e=n[0],u=n[n.length-1];const c=u0){for(;l<=h;++l)for(f=1;fu)break;p.push(s)}}else for(;l<=h;++l)for(f=a-1;f>=1;--f)if(s=l>0?f/o(-l):f*o(l),!(su)break;p.push(s)}2*p.length{if(null==n&&(n=10),null==r&&(r=10===a?"s":","),"function"!=typeof r&&(a%1||null!=(r=Jc(r)).precision||(r.trim=!0),r=t.format(r)),n===1/0)return r;const u=Math.max(1,a*n/e.ticks().length);return t=>{let n=t/o(Math.round(i(t)));return n*ar(kg(r(),{floor:t=>o(Math.floor(i(t))),ceil:t=>o(Math.ceil(i(t)))})),e}function qg(t){return function(n){return Math.sign(n)*Math.log1p(Math.abs(n/t))}}function Ug(t){return function(n){return Math.sign(n)*Math.expm1(Math.abs(n))*t}}function Ig(t){var n=1,e=t(qg(n),Ug(n));return e.constant=function(e){return arguments.length?t(qg(n=+e),Ug(n)):n},Ng(e)}function Og(t){return function(n){return n<0?-Math.pow(-n,t):Math.pow(n,t)}}function Bg(t){return t<0?-Math.sqrt(-t):Math.sqrt(t)}function Yg(t){return t<0?-t*t:t*t}function Lg(t){var n=t(mg,mg),e=1;return n.exponent=function(n){return arguments.length?1===(e=+n)?t(mg,mg):.5===e?t(Bg,Yg):t(Og(e),Og(1/e)):e},Ng(n)}function jg(){var t=Lg(Ag());return t.copy=function(){return Tg(t,jg()).exponent(t.exponent())},hg.apply(t,arguments),t}function Hg(t){return Math.sign(t)*t*t}const Xg=new Date,Gg=new Date;function Vg(t,n,e,r){function i(n){return t(n=0===arguments.length?new Date:new Date(+n)),n}return i.floor=n=>(t(n=new Date(+n)),n),i.ceil=e=>(t(e=new Date(e-1)),n(e,1),t(e),e),i.round=t=>{const n=i(t),e=i.ceil(t);return t-n(n(t=new Date(+t),null==e?1:Math.floor(e)),t),i.range=(e,r,o)=>{const a=[];if(e=i.ceil(e),o=null==o?1:Math.floor(o),!(e0))return a;let u;do{a.push(u=new Date(+e)),n(e,o),t(e)}while(uVg((n=>{if(n>=n)for(;t(n),!e(n);)n.setTime(n-1)}),((t,r)=>{if(t>=t)if(r<0)for(;++r<=0;)for(;n(t,-1),!e(t););else for(;--r>=0;)for(;n(t,1),!e(t););})),e&&(i.count=(n,r)=>(Xg.setTime(+n),Gg.setTime(+r),t(Xg),t(Gg),Math.floor(e(Xg,Gg))),i.every=t=>(t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?n=>r(n)%t==0:n=>i.count(0,n)%t==0):i:null)),i}const Wg=Vg((()=>{}),((t,n)=>{t.setTime(+t+n)}),((t,n)=>n-t));Wg.every=t=>(t=Math.floor(t),isFinite(t)&&t>0?t>1?Vg((n=>{n.setTime(Math.floor(n/t)*t)}),((n,e)=>{n.setTime(+n+e*t)}),((n,e)=>(e-n)/t)):Wg:null);const Zg=Wg.range,Kg=1e3,Qg=6e4,Jg=36e5,ty=864e5,ny=6048e5,ey=2592e6,ry=31536e6,iy=Vg((t=>{t.setTime(t-t.getMilliseconds())}),((t,n)=>{t.setTime(+t+n*Kg)}),((t,n)=>(n-t)/Kg),(t=>t.getUTCSeconds())),oy=iy.range,ay=Vg((t=>{t.setTime(t-t.getMilliseconds()-t.getSeconds()*Kg)}),((t,n)=>{t.setTime(+t+n*Qg)}),((t,n)=>(n-t)/Qg),(t=>t.getMinutes())),uy=ay.range,cy=Vg((t=>{t.setUTCSeconds(0,0)}),((t,n)=>{t.setTime(+t+n*Qg)}),((t,n)=>(n-t)/Qg),(t=>t.getUTCMinutes())),fy=cy.range,sy=Vg((t=>{t.setTime(t-t.getMilliseconds()-t.getSeconds()*Kg-t.getMinutes()*Qg)}),((t,n)=>{t.setTime(+t+n*Jg)}),((t,n)=>(n-t)/Jg),(t=>t.getHours())),ly=sy.range,hy=Vg((t=>{t.setUTCMinutes(0,0,0)}),((t,n)=>{t.setTime(+t+n*Jg)}),((t,n)=>(n-t)/Jg),(t=>t.getUTCHours())),dy=hy.range,py=Vg((t=>t.setHours(0,0,0,0)),((t,n)=>t.setDate(t.getDate()+n)),((t,n)=>(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Qg)/ty),(t=>t.getDate()-1)),gy=py.range,yy=Vg((t=>{t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCDate(t.getUTCDate()+n)}),((t,n)=>(n-t)/ty),(t=>t.getUTCDate()-1)),vy=yy.range,_y=Vg((t=>{t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCDate(t.getUTCDate()+n)}),((t,n)=>(n-t)/ty),(t=>Math.floor(t/ty))),by=_y.range;function my(t){return Vg((n=>{n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)}),((t,n)=>{t.setDate(t.getDate()+7*n)}),((t,n)=>(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Qg)/ny))}const xy=my(0),wy=my(1),My=my(2),Ty=my(3),Ay=my(4),Sy=my(5),Ey=my(6),Ny=xy.range,ky=wy.range,Cy=My.range,Py=Ty.range,zy=Ay.range,$y=Sy.range,Dy=Ey.range;function Ry(t){return Vg((n=>{n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCDate(t.getUTCDate()+7*n)}),((t,n)=>(n-t)/ny))}const Fy=Ry(0),qy=Ry(1),Uy=Ry(2),Iy=Ry(3),Oy=Ry(4),By=Ry(5),Yy=Ry(6),Ly=Fy.range,jy=qy.range,Hy=Uy.range,Xy=Iy.range,Gy=Oy.range,Vy=By.range,Wy=Yy.range,Zy=Vg((t=>{t.setDate(1),t.setHours(0,0,0,0)}),((t,n)=>{t.setMonth(t.getMonth()+n)}),((t,n)=>n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())),(t=>t.getMonth())),Ky=Zy.range,Qy=Vg((t=>{t.setUTCDate(1),t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCMonth(t.getUTCMonth()+n)}),((t,n)=>n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())),(t=>t.getUTCMonth())),Jy=Qy.range,tv=Vg((t=>{t.setMonth(0,1),t.setHours(0,0,0,0)}),((t,n)=>{t.setFullYear(t.getFullYear()+n)}),((t,n)=>n.getFullYear()-t.getFullYear()),(t=>t.getFullYear()));tv.every=t=>isFinite(t=Math.floor(t))&&t>0?Vg((n=>{n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)}),((n,e)=>{n.setFullYear(n.getFullYear()+e*t)})):null;const nv=tv.range,ev=Vg((t=>{t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCFullYear(t.getUTCFullYear()+n)}),((t,n)=>n.getUTCFullYear()-t.getUTCFullYear()),(t=>t.getUTCFullYear()));ev.every=t=>isFinite(t=Math.floor(t))&&t>0?Vg((n=>{n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)}),((n,e)=>{n.setUTCFullYear(n.getUTCFullYear()+e*t)})):null;const rv=ev.range;function iv(t,n,e,i,o,a){const u=[[iy,1,Kg],[iy,5,5e3],[iy,15,15e3],[iy,30,3e4],[a,1,Qg],[a,5,3e5],[a,15,9e5],[a,30,18e5],[o,1,Jg],[o,3,108e5],[o,6,216e5],[o,12,432e5],[i,1,ty],[i,2,1728e5],[e,1,ny],[n,1,ey],[n,3,7776e6],[t,1,ry]];function c(n,e,i){const o=Math.abs(e-n)/i,a=r((([,,t])=>t)).right(u,o);if(a===u.length)return t.every(W(n/ry,e/ry,i));if(0===a)return Wg.every(Math.max(W(n,e,i),1));const[c,f]=u[o/u[a-1][2]=12)]},q:function(t){return 1+~~(t.getMonth()/3)},Q:k_,s:C_,S:Zv,u:Kv,U:Qv,V:t_,w:n_,W:e_,x:null,X:null,y:r_,Y:o_,Z:u_,"%":N_},m={a:function(t){return a[t.getUTCDay()]},A:function(t){return o[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return u[t.getUTCMonth()]},c:null,d:c_,e:c_,f:d_,g:T_,G:S_,H:f_,I:s_,j:l_,L:h_,m:p_,M:g_,p:function(t){return i[+(t.getUTCHours()>=12)]},q:function(t){return 1+~~(t.getUTCMonth()/3)},Q:k_,s:C_,S:y_,u:v_,U:__,V:m_,w:x_,W:w_,x:null,X:null,y:M_,Y:A_,Z:E_,"%":N_},x={a:function(t,n,e){var r=d.exec(n.slice(e));return r?(t.w=p.get(r[0].toLowerCase()),e+r[0].length):-1},A:function(t,n,e){var r=l.exec(n.slice(e));return r?(t.w=h.get(r[0].toLowerCase()),e+r[0].length):-1},b:function(t,n,e){var r=v.exec(n.slice(e));return r?(t.m=_.get(r[0].toLowerCase()),e+r[0].length):-1},B:function(t,n,e){var r=g.exec(n.slice(e));return r?(t.m=y.get(r[0].toLowerCase()),e+r[0].length):-1},c:function(t,e,r){return T(t,n,e,r)},d:zv,e:zv,f:Uv,g:Nv,G:Ev,H:Dv,I:Dv,j:$v,L:qv,m:Pv,M:Rv,p:function(t,n,e){var r=f.exec(n.slice(e));return r?(t.p=s.get(r[0].toLowerCase()),e+r[0].length):-1},q:Cv,Q:Ov,s:Bv,S:Fv,u:Mv,U:Tv,V:Av,w:wv,W:Sv,x:function(t,n,r){return T(t,e,n,r)},X:function(t,n,e){return T(t,r,n,e)},y:Nv,Y:Ev,Z:kv,"%":Iv};function w(t,n){return function(e){var r,i,o,a=[],u=-1,c=0,f=t.length;for(e instanceof Date||(e=new Date(+e));++u53)return null;"w"in o||(o.w=1),"Z"in o?(i=(r=sv(lv(o.y,0,1))).getUTCDay(),r=i>4||0===i?qy.ceil(r):qy(r),r=yy.offset(r,7*(o.V-1)),o.y=r.getUTCFullYear(),o.m=r.getUTCMonth(),o.d=r.getUTCDate()+(o.w+6)%7):(i=(r=fv(lv(o.y,0,1))).getDay(),r=i>4||0===i?wy.ceil(r):wy(r),r=py.offset(r,7*(o.V-1)),o.y=r.getFullYear(),o.m=r.getMonth(),o.d=r.getDate()+(o.w+6)%7)}else("W"in o||"U"in o)&&("w"in o||(o.w="u"in o?o.u%7:"W"in o?1:0),i="Z"in o?sv(lv(o.y,0,1)).getUTCDay():fv(lv(o.y,0,1)).getDay(),o.m=0,o.d="W"in o?(o.w+6)%7+7*o.W-(i+5)%7:o.w+7*o.U-(i+6)%7);return"Z"in o?(o.H+=o.Z/100|0,o.M+=o.Z%100,sv(o)):fv(o)}}function T(t,n,e,r){for(var i,o,a=0,u=n.length,c=e.length;a=c)return-1;if(37===(i=n.charCodeAt(a++))){if(i=n.charAt(a++),!(o=x[i in pv?n.charAt(a++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}return b.x=w(e,b),b.X=w(r,b),b.c=w(n,b),m.x=w(e,m),m.X=w(r,m),m.c=w(n,m),{format:function(t){var n=w(t+="",b);return n.toString=function(){return t},n},parse:function(t){var n=M(t+="",!1);return n.toString=function(){return t},n},utcFormat:function(t){var n=w(t+="",m);return n.toString=function(){return t},n},utcParse:function(t){var n=M(t+="",!0);return n.toString=function(){return t},n}}}var dv,pv={"-":"",_:" ",0:"0"},gv=/^\s*\d+/,yv=/^%/,vv=/[\\^$*+?|[\]().{}]/g;function _v(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o[t.toLowerCase(),n])))}function wv(t,n,e){var r=gv.exec(n.slice(e,e+1));return r?(t.w=+r[0],e+r[0].length):-1}function Mv(t,n,e){var r=gv.exec(n.slice(e,e+1));return r?(t.u=+r[0],e+r[0].length):-1}function Tv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.U=+r[0],e+r[0].length):-1}function Av(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.V=+r[0],e+r[0].length):-1}function Sv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.W=+r[0],e+r[0].length):-1}function Ev(t,n,e){var r=gv.exec(n.slice(e,e+4));return r?(t.y=+r[0],e+r[0].length):-1}function Nv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.y=+r[0]+(+r[0]>68?1900:2e3),e+r[0].length):-1}function kv(t,n,e){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function Cv(t,n,e){var r=gv.exec(n.slice(e,e+1));return r?(t.q=3*r[0]-3,e+r[0].length):-1}function Pv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function zv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function $v(t,n,e){var r=gv.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function Dv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function Rv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function Fv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function qv(t,n,e){var r=gv.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function Uv(t,n,e){var r=gv.exec(n.slice(e,e+6));return r?(t.L=Math.floor(r[0]/1e3),e+r[0].length):-1}function Iv(t,n,e){var r=yv.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function Ov(t,n,e){var r=gv.exec(n.slice(e));return r?(t.Q=+r[0],e+r[0].length):-1}function Bv(t,n,e){var r=gv.exec(n.slice(e));return r?(t.s=+r[0],e+r[0].length):-1}function Yv(t,n){return _v(t.getDate(),n,2)}function Lv(t,n){return _v(t.getHours(),n,2)}function jv(t,n){return _v(t.getHours()%12||12,n,2)}function Hv(t,n){return _v(1+py.count(tv(t),t),n,3)}function Xv(t,n){return _v(t.getMilliseconds(),n,3)}function Gv(t,n){return Xv(t,n)+"000"}function Vv(t,n){return _v(t.getMonth()+1,n,2)}function Wv(t,n){return _v(t.getMinutes(),n,2)}function Zv(t,n){return _v(t.getSeconds(),n,2)}function Kv(t){var n=t.getDay();return 0===n?7:n}function Qv(t,n){return _v(xy.count(tv(t)-1,t),n,2)}function Jv(t){var n=t.getDay();return n>=4||0===n?Ay(t):Ay.ceil(t)}function t_(t,n){return t=Jv(t),_v(Ay.count(tv(t),t)+(4===tv(t).getDay()),n,2)}function n_(t){return t.getDay()}function e_(t,n){return _v(wy.count(tv(t)-1,t),n,2)}function r_(t,n){return _v(t.getFullYear()%100,n,2)}function i_(t,n){return _v((t=Jv(t)).getFullYear()%100,n,2)}function o_(t,n){return _v(t.getFullYear()%1e4,n,4)}function a_(t,n){var e=t.getDay();return _v((t=e>=4||0===e?Ay(t):Ay.ceil(t)).getFullYear()%1e4,n,4)}function u_(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+_v(n/60|0,"0",2)+_v(n%60,"0",2)}function c_(t,n){return _v(t.getUTCDate(),n,2)}function f_(t,n){return _v(t.getUTCHours(),n,2)}function s_(t,n){return _v(t.getUTCHours()%12||12,n,2)}function l_(t,n){return _v(1+yy.count(ev(t),t),n,3)}function h_(t,n){return _v(t.getUTCMilliseconds(),n,3)}function d_(t,n){return h_(t,n)+"000"}function p_(t,n){return _v(t.getUTCMonth()+1,n,2)}function g_(t,n){return _v(t.getUTCMinutes(),n,2)}function y_(t,n){return _v(t.getUTCSeconds(),n,2)}function v_(t){var n=t.getUTCDay();return 0===n?7:n}function __(t,n){return _v(Fy.count(ev(t)-1,t),n,2)}function b_(t){var n=t.getUTCDay();return n>=4||0===n?Oy(t):Oy.ceil(t)}function m_(t,n){return t=b_(t),_v(Oy.count(ev(t),t)+(4===ev(t).getUTCDay()),n,2)}function x_(t){return t.getUTCDay()}function w_(t,n){return _v(qy.count(ev(t)-1,t),n,2)}function M_(t,n){return _v(t.getUTCFullYear()%100,n,2)}function T_(t,n){return _v((t=b_(t)).getUTCFullYear()%100,n,2)}function A_(t,n){return _v(t.getUTCFullYear()%1e4,n,4)}function S_(t,n){var e=t.getUTCDay();return _v((t=e>=4||0===e?Oy(t):Oy.ceil(t)).getUTCFullYear()%1e4,n,4)}function E_(){return"+0000"}function N_(){return"%"}function k_(t){return+t}function C_(t){return Math.floor(+t/1e3)}function P_(n){return dv=hv(n),t.timeFormat=dv.format,t.timeParse=dv.parse,t.utcFormat=dv.utcFormat,t.utcParse=dv.utcParse,dv}t.timeFormat=void 0,t.timeParse=void 0,t.utcFormat=void 0,t.utcParse=void 0,P_({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var z_="%Y-%m-%dT%H:%M:%S.%LZ";var $_=Date.prototype.toISOString?function(t){return t.toISOString()}:t.utcFormat(z_),D_=$_;var R_=+new Date("2000-01-01T00:00:00.000Z")?function(t){var n=new Date(t);return isNaN(n)?null:n}:t.utcParse(z_),F_=R_;function q_(t){return new Date(t)}function U_(t){return t instanceof Date?+t:+new Date(+t)}function I_(t,n,e,r,i,o,a,u,c,f){var s=Sg(),l=s.invert,h=s.domain,d=f(".%L"),p=f(":%S"),g=f("%I:%M"),y=f("%I %p"),v=f("%a %d"),_=f("%b %d"),b=f("%B"),m=f("%Y");function x(t){return(c(t)Fr(t[t.length-1]),rb=new Array(3).concat("d8b365f5f5f55ab4ac","a6611adfc27d80cdc1018571","a6611adfc27df5f5f580cdc1018571","8c510ad8b365f6e8c3c7eae55ab4ac01665e","8c510ad8b365f6e8c3f5f5f5c7eae55ab4ac01665e","8c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e","8c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e","5430058c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e003c30","5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30").map(H_),ib=eb(rb),ob=new Array(3).concat("af8dc3f7f7f77fbf7b","7b3294c2a5cfa6dba0008837","7b3294c2a5cff7f7f7a6dba0008837","762a83af8dc3e7d4e8d9f0d37fbf7b1b7837","762a83af8dc3e7d4e8f7f7f7d9f0d37fbf7b1b7837","762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b7837","762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b7837","40004b762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b783700441b","40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b").map(H_),ab=eb(ob),ub=new Array(3).concat("e9a3c9f7f7f7a1d76a","d01c8bf1b6dab8e1864dac26","d01c8bf1b6daf7f7f7b8e1864dac26","c51b7de9a3c9fde0efe6f5d0a1d76a4d9221","c51b7de9a3c9fde0eff7f7f7e6f5d0a1d76a4d9221","c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221","c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221","8e0152c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221276419","8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419").map(H_),cb=eb(ub),fb=new Array(3).concat("998ec3f7f7f7f1a340","5e3c99b2abd2fdb863e66101","5e3c99b2abd2f7f7f7fdb863e66101","542788998ec3d8daebfee0b6f1a340b35806","542788998ec3d8daebf7f7f7fee0b6f1a340b35806","5427888073acb2abd2d8daebfee0b6fdb863e08214b35806","5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b35806","2d004b5427888073acb2abd2d8daebfee0b6fdb863e08214b358067f3b08","2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08").map(H_),sb=eb(fb),lb=new Array(3).concat("ef8a62f7f7f767a9cf","ca0020f4a58292c5de0571b0","ca0020f4a582f7f7f792c5de0571b0","b2182bef8a62fddbc7d1e5f067a9cf2166ac","b2182bef8a62fddbc7f7f7f7d1e5f067a9cf2166ac","b2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac","b2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac","67001fb2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac053061","67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061").map(H_),hb=eb(lb),db=new Array(3).concat("ef8a62ffffff999999","ca0020f4a582bababa404040","ca0020f4a582ffffffbababa404040","b2182bef8a62fddbc7e0e0e09999994d4d4d","b2182bef8a62fddbc7ffffffe0e0e09999994d4d4d","b2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d","b2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d","67001fb2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d1a1a1a","67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a").map(H_),pb=eb(db),gb=new Array(3).concat("fc8d59ffffbf91bfdb","d7191cfdae61abd9e92c7bb6","d7191cfdae61ffffbfabd9e92c7bb6","d73027fc8d59fee090e0f3f891bfdb4575b4","d73027fc8d59fee090ffffbfe0f3f891bfdb4575b4","d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4","d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4","a50026d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4313695","a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695").map(H_),yb=eb(gb),vb=new Array(3).concat("fc8d59ffffbf91cf60","d7191cfdae61a6d96a1a9641","d7191cfdae61ffffbfa6d96a1a9641","d73027fc8d59fee08bd9ef8b91cf601a9850","d73027fc8d59fee08bffffbfd9ef8b91cf601a9850","d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850","d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850","a50026d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850006837","a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837").map(H_),_b=eb(vb),bb=new Array(3).concat("fc8d59ffffbf99d594","d7191cfdae61abdda42b83ba","d7191cfdae61ffffbfabdda42b83ba","d53e4ffc8d59fee08be6f59899d5943288bd","d53e4ffc8d59fee08bffffbfe6f59899d5943288bd","d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd","d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd","9e0142d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd5e4fa2","9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2").map(H_),mb=eb(bb),xb=new Array(3).concat("e5f5f999d8c92ca25f","edf8fbb2e2e266c2a4238b45","edf8fbb2e2e266c2a42ca25f006d2c","edf8fbccece699d8c966c2a42ca25f006d2c","edf8fbccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45006d2c00441b").map(H_),wb=eb(xb),Mb=new Array(3).concat("e0ecf49ebcda8856a7","edf8fbb3cde38c96c688419d","edf8fbb3cde38c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d810f7c4d004b").map(H_),Tb=eb(Mb),Ab=new Array(3).concat("e0f3dba8ddb543a2ca","f0f9e8bae4bc7bccc42b8cbe","f0f9e8bae4bc7bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe0868ac084081").map(H_),Sb=eb(Ab),Eb=new Array(3).concat("fee8c8fdbb84e34a33","fef0d9fdcc8afc8d59d7301f","fef0d9fdcc8afc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301fb300007f0000").map(H_),Nb=eb(Eb),kb=new Array(3).concat("ece2f0a6bddb1c9099","f6eff7bdc9e167a9cf02818a","f6eff7bdc9e167a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016c59014636").map(H_),Cb=eb(kb),Pb=new Array(3).concat("ece7f2a6bddb2b8cbe","f1eef6bdc9e174a9cf0570b0","f1eef6bdc9e174a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0045a8d023858").map(H_),zb=eb(Pb),$b=new Array(3).concat("e7e1efc994c7dd1c77","f1eef6d7b5d8df65b0ce1256","f1eef6d7b5d8df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125698004367001f").map(H_),Db=eb($b),Rb=new Array(3).concat("fde0ddfa9fb5c51b8a","feebe2fbb4b9f768a1ae017e","feebe2fbb4b9f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a017749006a").map(H_),Fb=eb(Rb),qb=new Array(3).concat("edf8b17fcdbb2c7fb8","ffffcca1dab441b6c4225ea8","ffffcca1dab441b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea8253494081d58").map(H_),Ub=eb(qb),Ib=new Array(3).concat("f7fcb9addd8e31a354","ffffccc2e69978c679238443","ffffccc2e69978c67931a354006837","ffffccd9f0a3addd8e78c67931a354006837","ffffccd9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443006837004529").map(H_),Ob=eb(Ib),Bb=new Array(3).concat("fff7bcfec44fd95f0e","ffffd4fed98efe9929cc4c02","ffffd4fed98efe9929d95f0e993404","ffffd4fee391fec44ffe9929d95f0e993404","ffffd4fee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c02993404662506").map(H_),Yb=eb(Bb),Lb=new Array(3).concat("ffeda0feb24cf03b20","ffffb2fecc5cfd8d3ce31a1c","ffffb2fecc5cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cbd0026800026").map(H_),jb=eb(Lb),Hb=new Array(3).concat("deebf79ecae13182bd","eff3ffbdd7e76baed62171b5","eff3ffbdd7e76baed63182bd08519c","eff3ffc6dbef9ecae16baed63182bd08519c","eff3ffc6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b508519c08306b").map(H_),Xb=eb(Hb),Gb=new Array(3).concat("e5f5e0a1d99b31a354","edf8e9bae4b374c476238b45","edf8e9bae4b374c47631a354006d2c","edf8e9c7e9c0a1d99b74c47631a354006d2c","edf8e9c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45006d2c00441b").map(H_),Vb=eb(Gb),Wb=new Array(3).concat("f0f0f0bdbdbd636363","f7f7f7cccccc969696525252","f7f7f7cccccc969696636363252525","f7f7f7d9d9d9bdbdbd969696636363252525","f7f7f7d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525000000").map(H_),Zb=eb(Wb),Kb=new Array(3).concat("efedf5bcbddc756bb1","f2f0f7cbc9e29e9ac86a51a3","f2f0f7cbc9e29e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a354278f3f007d").map(H_),Qb=eb(Kb),Jb=new Array(3).concat("fee0d2fc9272de2d26","fee5d9fcae91fb6a4acb181d","fee5d9fcae91fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181da50f1567000d").map(H_),tm=eb(Jb),nm=new Array(3).concat("fee6cefdae6be6550d","feeddefdbe85fd8d3cd94701","feeddefdbe85fd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d94801a636037f2704").map(H_),em=eb(nm);var rm=hi(Tr(300,.5,0),Tr(-240,.5,1)),im=hi(Tr(-100,.75,.35),Tr(80,1.5,.8)),om=hi(Tr(260,.75,.35),Tr(80,1.5,.8)),am=Tr();var um=Fe(),cm=Math.PI/3,fm=2*Math.PI/3;function sm(t){var n=t.length;return function(e){return t[Math.max(0,Math.min(n-1,Math.floor(e*n)))]}}var lm=sm(H_("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725")),hm=sm(H_("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),dm=sm(H_("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),pm=sm(H_("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921"));function gm(t){return function(){return t}}const ym=Math.abs,vm=Math.atan2,_m=Math.cos,bm=Math.max,mm=Math.min,xm=Math.sin,wm=Math.sqrt,Mm=1e-12,Tm=Math.PI,Am=Tm/2,Sm=2*Tm;function Em(t){return t>=1?Am:t<=-1?-Am:Math.asin(t)}function Nm(t){let n=3;return t.digits=function(e){if(!arguments.length)return n;if(null==e)n=null;else{const t=Math.floor(e);if(!(t>=0))throw new RangeError(`invalid digits: ${e}`);n=t}return t},()=>new Ua(n)}function km(t){return t.innerRadius}function Cm(t){return t.outerRadius}function Pm(t){return t.startAngle}function zm(t){return t.endAngle}function $m(t){return t&&t.padAngle}function Dm(t,n,e,r,i,o,a){var u=t-e,c=n-r,f=(a?o:-o)/wm(u*u+c*c),s=f*c,l=-f*u,h=t+s,d=n+l,p=e+s,g=r+l,y=(h+p)/2,v=(d+g)/2,_=p-h,b=g-d,m=_*_+b*b,x=i-o,w=h*g-p*d,M=(b<0?-1:1)*wm(bm(0,x*x*m-w*w)),T=(w*b-_*M)/m,A=(-w*_-b*M)/m,S=(w*b+_*M)/m,E=(-w*_+b*M)/m,N=T-y,k=A-v,C=S-y,P=E-v;return N*N+k*k>C*C+P*P&&(T=S,A=E),{cx:T,cy:A,x01:-s,y01:-l,x11:T*(i/x-1),y11:A*(i/x-1)}}var Rm=Array.prototype.slice;function Fm(t){return"object"==typeof t&&"length"in t?t:Array.from(t)}function qm(t){this._context=t}function Um(t){return new qm(t)}function Im(t){return t[0]}function Om(t){return t[1]}function Bm(t,n){var e=gm(!0),r=null,i=Um,o=null,a=Nm(u);function u(u){var c,f,s,l=(u=Fm(u)).length,h=!1;for(null==r&&(o=i(s=a())),c=0;c<=l;++c)!(c=l;--h)u.point(v[h],_[h]);u.lineEnd(),u.areaEnd()}y&&(v[s]=+t(d,s,f),_[s]=+n(d,s,f),u.point(r?+r(d,s,f):v[s],e?+e(d,s,f):_[s]))}if(p)return u=null,p+""||null}function s(){return Bm().defined(i).curve(a).context(o)}return t="function"==typeof t?t:void 0===t?Im:gm(+t),n="function"==typeof n?n:gm(void 0===n?0:+n),e="function"==typeof e?e:void 0===e?Om:gm(+e),f.x=function(n){return arguments.length?(t="function"==typeof n?n:gm(+n),r=null,f):t},f.x0=function(n){return arguments.length?(t="function"==typeof n?n:gm(+n),f):t},f.x1=function(t){return arguments.length?(r=null==t?null:"function"==typeof t?t:gm(+t),f):r},f.y=function(t){return arguments.length?(n="function"==typeof t?t:gm(+t),e=null,f):n},f.y0=function(t){return arguments.length?(n="function"==typeof t?t:gm(+t),f):n},f.y1=function(t){return arguments.length?(e=null==t?null:"function"==typeof t?t:gm(+t),f):e},f.lineX0=f.lineY0=function(){return s().x(t).y(n)},f.lineY1=function(){return s().x(t).y(e)},f.lineX1=function(){return s().x(r).y(n)},f.defined=function(t){return arguments.length?(i="function"==typeof t?t:gm(!!t),f):i},f.curve=function(t){return arguments.length?(a=t,null!=o&&(u=a(o)),f):a},f.context=function(t){return arguments.length?(null==t?o=u=null:u=a(o=t),f):o},f}function Lm(t,n){return nt?1:n>=t?0:NaN}function jm(t){return t}qm.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var Hm=Gm(Um);function Xm(t){this._curve=t}function Gm(t){function n(n){return new Xm(t(n))}return n._curve=t,n}function Vm(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(Gm(t)):n()._curve},t}function Wm(){return Vm(Bm().curve(Hm))}function Zm(){var t=Ym().curve(Hm),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return Vm(e())},delete t.lineX0,t.lineEndAngle=function(){return Vm(r())},delete t.lineX1,t.lineInnerRadius=function(){return Vm(i())},delete t.lineY0,t.lineOuterRadius=function(){return Vm(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(Gm(t)):n()._curve},t}function Km(t,n){return[(n=+n)*Math.cos(t-=Math.PI/2),n*Math.sin(t)]}Xm.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};class Qm{constructor(t,n){this._context=t,this._x=n}areaStart(){this._line=0}areaEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line}point(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._x?this._context.bezierCurveTo(this._x0=(this._x0+t)/2,this._y0,this._x0,n,t,n):this._context.bezierCurveTo(this._x0,this._y0=(this._y0+n)/2,t,this._y0,t,n)}this._x0=t,this._y0=n}}class Jm{constructor(t){this._context=t}lineStart(){this._point=0}lineEnd(){}point(t,n){if(t=+t,n=+n,0===this._point)this._point=1;else{const e=Km(this._x0,this._y0),r=Km(this._x0,this._y0=(this._y0+n)/2),i=Km(t,this._y0),o=Km(t,n);this._context.moveTo(...e),this._context.bezierCurveTo(...r,...i,...o)}this._x0=t,this._y0=n}}function tx(t){return new Qm(t,!0)}function nx(t){return new Qm(t,!1)}function ex(t){return new Jm(t)}function rx(t){return t.source}function ix(t){return t.target}function ox(t){let n=rx,e=ix,r=Im,i=Om,o=null,a=null,u=Nm(c);function c(){let c;const f=Rm.call(arguments),s=n.apply(this,f),l=e.apply(this,f);if(null==o&&(a=t(c=u())),a.lineStart(),f[0]=s,a.point(+r.apply(this,f),+i.apply(this,f)),f[0]=l,a.point(+r.apply(this,f),+i.apply(this,f)),a.lineEnd(),c)return a=null,c+""||null}return c.source=function(t){return arguments.length?(n=t,c):n},c.target=function(t){return arguments.length?(e=t,c):e},c.x=function(t){return arguments.length?(r="function"==typeof t?t:gm(+t),c):r},c.y=function(t){return arguments.length?(i="function"==typeof t?t:gm(+t),c):i},c.context=function(n){return arguments.length?(null==n?o=a=null:a=t(o=n),c):o},c}const ax=wm(3);var ux={draw(t,n){const e=.59436*wm(n+mm(n/28,.75)),r=e/2,i=r*ax;t.moveTo(0,e),t.lineTo(0,-e),t.moveTo(-i,-r),t.lineTo(i,r),t.moveTo(-i,r),t.lineTo(i,-r)}},cx={draw(t,n){const e=wm(n/Tm);t.moveTo(e,0),t.arc(0,0,e,0,Sm)}},fx={draw(t,n){const e=wm(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}};const sx=wm(1/3),lx=2*sx;var hx={draw(t,n){const e=wm(n/lx),r=e*sx;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},dx={draw(t,n){const e=.62625*wm(n);t.moveTo(0,-e),t.lineTo(e,0),t.lineTo(0,e),t.lineTo(-e,0),t.closePath()}},px={draw(t,n){const e=.87559*wm(n-mm(n/7,2));t.moveTo(-e,0),t.lineTo(e,0),t.moveTo(0,e),t.lineTo(0,-e)}},gx={draw(t,n){const e=wm(n),r=-e/2;t.rect(r,r,e,e)}},yx={draw(t,n){const e=.4431*wm(n);t.moveTo(e,e),t.lineTo(e,-e),t.lineTo(-e,-e),t.lineTo(-e,e),t.closePath()}};const vx=xm(Tm/10)/xm(7*Tm/10),_x=xm(Sm/10)*vx,bx=-_m(Sm/10)*vx;var mx={draw(t,n){const e=wm(.8908130915292852*n),r=_x*e,i=bx*e;t.moveTo(0,-e),t.lineTo(r,i);for(let n=1;n<5;++n){const o=Sm*n/5,a=_m(o),u=xm(o);t.lineTo(u*e,-a*e),t.lineTo(a*r-u*i,u*r+a*i)}t.closePath()}};const xx=wm(3);var wx={draw(t,n){const e=-wm(n/(3*xx));t.moveTo(0,2*e),t.lineTo(-xx*e,-e),t.lineTo(xx*e,-e),t.closePath()}};const Mx=wm(3);var Tx={draw(t,n){const e=.6824*wm(n),r=e/2,i=e*Mx/2;t.moveTo(0,-e),t.lineTo(i,r),t.lineTo(-i,r),t.closePath()}};const Ax=-.5,Sx=wm(3)/2,Ex=1/wm(12),Nx=3*(Ex/2+1);var kx={draw(t,n){const e=wm(n/Nx),r=e/2,i=e*Ex,o=r,a=e*Ex+e,u=-o,c=a;t.moveTo(r,i),t.lineTo(o,a),t.lineTo(u,c),t.lineTo(Ax*r-Sx*i,Sx*r+Ax*i),t.lineTo(Ax*o-Sx*a,Sx*o+Ax*a),t.lineTo(Ax*u-Sx*c,Sx*u+Ax*c),t.lineTo(Ax*r+Sx*i,Ax*i-Sx*r),t.lineTo(Ax*o+Sx*a,Ax*a-Sx*o),t.lineTo(Ax*u+Sx*c,Ax*c-Sx*u),t.closePath()}},Cx={draw(t,n){const e=.6189*wm(n-mm(n/6,1.7));t.moveTo(-e,-e),t.lineTo(e,e),t.moveTo(-e,e),t.lineTo(e,-e)}};const Px=[cx,fx,hx,gx,mx,wx,kx],zx=[cx,px,Cx,Tx,ux,yx,dx];function $x(){}function Dx(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function Rx(t){this._context=t}function Fx(t){this._context=t}function qx(t){this._context=t}function Ux(t,n){this._basis=new Rx(t),this._beta=n}Rx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:Dx(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:Dx(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Fx.prototype={areaStart:$x,areaEnd:$x,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:Dx(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},qx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:Dx(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Ux.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],a=t[e]-i,u=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*a),this._beta*n[c]+(1-this._beta)*(o+r*u));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var Ix=function t(n){function e(t){return 1===n?new Rx(t):new Ux(t,n)}return e.beta=function(n){return t(+n)},e}(.85);function Ox(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function Bx(t,n){this._context=t,this._k=(1-n)/6}Bx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:Ox(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:Ox(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Yx=function t(n){function e(t){return new Bx(t,n)}return e.tension=function(n){return t(+n)},e}(0);function Lx(t,n){this._context=t,this._k=(1-n)/6}Lx.prototype={areaStart:$x,areaEnd:$x,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Ox(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var jx=function t(n){function e(t){return new Lx(t,n)}return e.tension=function(n){return t(+n)},e}(0);function Hx(t,n){this._context=t,this._k=(1-n)/6}Hx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Ox(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Xx=function t(n){function e(t){return new Hx(t,n)}return e.tension=function(n){return t(+n)},e}(0);function Gx(t,n,e){var r=t._x1,i=t._y1,o=t._x2,a=t._y2;if(t._l01_a>Mm){var u=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*u-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*u-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>Mm){var f=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,s=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*f+t._x1*t._l23_2a-n*t._l12_2a)/s,a=(a*f+t._y1*t._l23_2a-e*t._l12_2a)/s}t._context.bezierCurveTo(r,i,o,a,t._x2,t._y2)}function Vx(t,n){this._context=t,this._alpha=n}Vx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:Gx(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Wx=function t(n){function e(t){return n?new Vx(t,n):new Bx(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function Zx(t,n){this._context=t,this._alpha=n}Zx.prototype={areaStart:$x,areaEnd:$x,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Gx(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Kx=function t(n){function e(t){return n?new Zx(t,n):new Lx(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function Qx(t,n){this._context=t,this._alpha=n}Qx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Gx(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Jx=function t(n){function e(t){return n?new Qx(t,n):new Hx(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function tw(t){this._context=t}function nw(t){return t<0?-1:1}function ew(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),a=(e-t._y1)/(i||r<0&&-0),u=(o*i+a*r)/(r+i);return(nw(o)+nw(a))*Math.min(Math.abs(o),Math.abs(a),.5*Math.abs(u))||0}function rw(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function iw(t,n,e){var r=t._x0,i=t._y0,o=t._x1,a=t._y1,u=(o-r)/3;t._context.bezierCurveTo(r+u,i+u*n,o-u,a-u*e,o,a)}function ow(t){this._context=t}function aw(t){this._context=new uw(t)}function uw(t){this._context=t}function cw(t){this._context=t}function fw(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),a=new Array(r);for(i[0]=0,o[0]=2,a[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(a[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n1)for(var e,r,i,o=1,a=t[n[0]],u=a.length;o=0;)e[n]=n;return e}function dw(t,n){return t[n]}function pw(t){const n=[];return n.key=t,n}function gw(t){var n=t.map(yw);return hw(t).sort((function(t,e){return n[t]-n[e]}))}function yw(t){for(var n,e=-1,r=0,i=t.length,o=-1/0;++eo&&(o=n,r=e);return r}function vw(t){var n=t.map(_w);return hw(t).sort((function(t,e){return n[t]-n[e]}))}function _w(t){for(var n,e=0,r=-1,i=t.length;++r=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}};var bw=t=>()=>t;function mw(t,{sourceEvent:n,target:e,transform:r,dispatch:i}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},target:{value:e,enumerable:!0,configurable:!0},transform:{value:r,enumerable:!0,configurable:!0},_:{value:i}})}function xw(t,n,e){this.k=t,this.x=n,this.y=e}xw.prototype={constructor:xw,scale:function(t){return 1===t?this:new xw(this.k*t,this.x,this.y)},translate:function(t,n){return 0===t&0===n?this:new xw(this.k,this.x+this.k*t,this.y+this.k*n)},apply:function(t){return[t[0]*this.k+this.x,t[1]*this.k+this.y]},applyX:function(t){return t*this.k+this.x},applyY:function(t){return t*this.k+this.y},invert:function(t){return[(t[0]-this.x)/this.k,(t[1]-this.y)/this.k]},invertX:function(t){return(t-this.x)/this.k},invertY:function(t){return(t-this.y)/this.k},rescaleX:function(t){return t.copy().domain(t.range().map(this.invertX,this).map(t.invert,t))},rescaleY:function(t){return t.copy().domain(t.range().map(this.invertY,this).map(t.invert,t))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var ww=new xw(1,0,0);function Mw(t){for(;!t.__zoom;)if(!(t=t.parentNode))return ww;return t.__zoom}function Tw(t){t.stopImmediatePropagation()}function Aw(t){t.preventDefault(),t.stopImmediatePropagation()}function Sw(t){return!(t.ctrlKey&&"wheel"!==t.type||t.button)}function Ew(){var t=this;return t instanceof SVGElement?(t=t.ownerSVGElement||t).hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]:[[0,0],[t.clientWidth,t.clientHeight]]}function Nw(){return this.__zoom||ww}function kw(t){return-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002)*(t.ctrlKey?10:1)}function Cw(){return navigator.maxTouchPoints||"ontouchstart"in this}function Pw(t,n,e){var r=t.invertX(n[0][0])-e[0][0],i=t.invertX(n[1][0])-e[1][0],o=t.invertY(n[0][1])-e[0][1],a=t.invertY(n[1][1])-e[1][1];return t.translate(i>r?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}Mw.prototype=xw.prototype,t.Adder=T,t.Delaunay=Lu,t.FormatSpecifier=tf,t.InternMap=InternMap,t.InternSet=InternSet,t.Node=Qd,t.Path=Ua,t.Voronoi=qu,t.ZoomTransform=xw,t.active=function(t,n){var e,r,i=t.__transition;if(i)for(r in n=null==n?null:n+"",i)if((e=i[r]).state>qi&&e.name===n)return new po([[t]],Zo,n,+r);return null},t.arc=function(){var t=km,n=Cm,e=gm(0),r=null,i=Pm,o=zm,a=$m,u=null,c=Nm(f);function f(){var f,s,l=+t.apply(this,arguments),h=+n.apply(this,arguments),d=i.apply(this,arguments)-Am,p=o.apply(this,arguments)-Am,g=ym(p-d),y=p>d;if(u||(u=f=c()),hMm)if(g>Sm-Mm)u.moveTo(h*_m(d),h*xm(d)),u.arc(0,0,h,d,p,!y),l>Mm&&(u.moveTo(l*_m(p),l*xm(p)),u.arc(0,0,l,p,d,y));else{var v,_,b=d,m=p,x=d,w=p,M=g,T=g,A=a.apply(this,arguments)/2,S=A>Mm&&(r?+r.apply(this,arguments):wm(l*l+h*h)),E=mm(ym(h-l)/2,+e.apply(this,arguments)),N=E,k=E;if(S>Mm){var C=Em(S/l*xm(A)),P=Em(S/h*xm(A));(M-=2*C)>Mm?(x+=C*=y?1:-1,w-=C):(M=0,x=w=(d+p)/2),(T-=2*P)>Mm?(b+=P*=y?1:-1,m-=P):(T=0,b=m=(d+p)/2)}var z=h*_m(b),$=h*xm(b),D=l*_m(w),R=l*xm(w);if(E>Mm){var F,q=h*_m(m),U=h*xm(m),I=l*_m(x),O=l*xm(x);if(g1?0:t<-1?Tm:Math.acos(t)}((B*L+Y*j)/(wm(B*B+Y*Y)*wm(L*L+j*j)))/2),X=wm(F[0]*F[0]+F[1]*F[1]);N=mm(E,(l-X)/(H-1)),k=mm(E,(h-X)/(H+1))}else N=k=0}T>Mm?k>Mm?(v=Dm(I,O,z,$,h,k,y),_=Dm(q,U,D,R,h,k,y),u.moveTo(v.cx+v.x01,v.cy+v.y01),kMm&&M>Mm?N>Mm?(v=Dm(D,R,q,U,l,-N,y),_=Dm(z,$,I,O,l,-N,y),u.lineTo(v.cx+v.x01,v.cy+v.y01),N=0))throw new RangeError("invalid r");let e=t.length;if(!((e=Math.floor(e))>=0))throw new RangeError("invalid length");if(!e||!n)return t;const r=y(n),i=t.slice();return r(t,i,0,e,1),r(i,t,0,e,1),r(t,i,0,e,1),t},t.blur2=l,t.blurImage=h,t.brush=function(){return wa(la)},t.brushSelection=function(t){var n=t.__brush;return n?n.dim.output(n.selection):null},t.brushX=function(){return wa(fa)},t.brushY=function(){return wa(sa)},t.buffer=function(t,n){return fetch(t,n).then(_c)},t.chord=function(){return za(!1,!1)},t.chordDirected=function(){return za(!0,!1)},t.chordTranspose=function(){return za(!1,!0)},t.cluster=function(){var t=Ld,n=1,e=1,r=!1;function i(i){var o,a=0;i.eachAfter((function(n){var e=n.children;e?(n.x=function(t){return t.reduce(jd,0)/t.length}(e),n.y=function(t){return 1+t.reduce(Hd,0)}(e)):(n.x=o?a+=t(n,o):0,n.y=0,o=n)}));var u=function(t){for(var n;n=t.children;)t=n[0];return t}(i),c=function(t){for(var n;n=t.children;)t=n[n.length-1];return t}(i),f=u.x-t(u,c)/2,s=c.x+t(c,u)/2;return i.eachAfter(r?function(t){t.x=(t.x-i.x)*n,t.y=(i.y-t.y)*e}:function(t){t.x=(t.x-f)/(s-f)*n,t.y=(1-(i.y?t.y/i.y:1))*e})}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.color=ze,t.contourDensity=function(){var t=fu,n=su,e=lu,r=960,i=500,o=20,a=2,u=3*o,c=r+2*u>>a,f=i+2*u>>a,s=Qa(20);function h(r){var i=new Float32Array(c*f),s=Math.pow(2,-a),h=-1;for(const o of r){var d=(t(o,++h,r)+u)*s,p=(n(o,h,r)+u)*s,g=+e(o,h,r);if(g&&d>=0&&d=0&&pt*r)))(n).map(((t,n)=>(t.value=+e[n],p(t))))}function p(t){return t.coordinates.forEach(g),t}function g(t){t.forEach(y)}function y(t){t.forEach(v)}function v(t){t[0]=t[0]*Math.pow(2,a)-u,t[1]=t[1]*Math.pow(2,a)-u}function _(){return c=r+2*(u=3*o)>>a,f=i+2*u>>a,d}return d.contours=function(t){var n=h(t),e=iu().size([c,f]),r=Math.pow(2,2*a),i=t=>{t=+t;var i=p(e.contour(n,t*r));return i.value=t,i};return Object.defineProperty(i,"max",{get:()=>J(n)/r}),i},d.x=function(n){return arguments.length?(t="function"==typeof n?n:Qa(+n),d):t},d.y=function(t){return arguments.length?(n="function"==typeof t?t:Qa(+t),d):n},d.weight=function(t){return arguments.length?(e="function"==typeof t?t:Qa(+t),d):e},d.size=function(t){if(!arguments.length)return[r,i];var n=+t[0],e=+t[1];if(!(n>=0&&e>=0))throw new Error("invalid size");return r=n,i=e,_()},d.cellSize=function(t){if(!arguments.length)return 1<=1))throw new Error("invalid cell size");return a=Math.floor(Math.log(t)/Math.LN2),_()},d.thresholds=function(t){return arguments.length?(s="function"==typeof t?t:Array.isArray(t)?Qa(Za.call(t)):Qa(t),d):s},d.bandwidth=function(t){if(!arguments.length)return Math.sqrt(o*(o+1));if(!((t=+t)>=0))throw new Error("invalid bandwidth");return o=(Math.sqrt(4*t*t+1)-1)/2,_()},d},t.contours=iu,t.count=v,t.create=function(t){return Zn(Yt(t).call(document.documentElement))},t.creator=Yt,t.cross=function(...t){const n="function"==typeof t[t.length-1]&&function(t){return n=>t(...n)}(t.pop()),e=(t=t.map(m)).map(_),r=t.length-1,i=new Array(r+1).fill(0),o=[];if(r<0||e.some(b))return o;for(;;){o.push(i.map(((n,e)=>t[e][n])));let a=r;for(;++i[a]===e[a];){if(0===a)return n?o.map(n):o;i[a--]=0}}},t.csv=wc,t.csvFormat=rc,t.csvFormatBody=ic,t.csvFormatRow=ac,t.csvFormatRows=oc,t.csvFormatValue=uc,t.csvParse=nc,t.csvParseRows=ec,t.cubehelix=Tr,t.cumsum=function(t,n){var e=0,r=0;return Float64Array.from(t,void 0===n?t=>e+=+t||0:i=>e+=+n(i,r++,t)||0)},t.curveBasis=function(t){return new Rx(t)},t.curveBasisClosed=function(t){return new Fx(t)},t.curveBasisOpen=function(t){return new qx(t)},t.curveBumpX=tx,t.curveBumpY=nx,t.curveBundle=Ix,t.curveCardinal=Yx,t.curveCardinalClosed=jx,t.curveCardinalOpen=Xx,t.curveCatmullRom=Wx,t.curveCatmullRomClosed=Kx,t.curveCatmullRomOpen=Jx,t.curveLinear=Um,t.curveLinearClosed=function(t){return new tw(t)},t.curveMonotoneX=function(t){return new ow(t)},t.curveMonotoneY=function(t){return new aw(t)},t.curveNatural=function(t){return new cw(t)},t.curveStep=function(t){return new sw(t,.5)},t.curveStepAfter=function(t){return new sw(t,1)},t.curveStepBefore=function(t){return new sw(t,0)},t.descending=e,t.deviation=w,t.difference=function(t,...n){t=new InternSet(t);for(const e of n)for(const n of e)t.delete(n);return t},t.disjoint=function(t,n){const e=n[Symbol.iterator](),r=new InternSet;for(const n of t){if(r.has(n))return!1;let t,i;for(;({value:t,done:i}=e.next())&&!i;){if(Object.is(n,t))return!1;r.add(t)}}return!0},t.dispatch=$t,t.drag=function(){var t,n,e,r,i=se,o=le,a=he,u=de,c={},f=$t("start","drag","end"),s=0,l=0;function h(t){t.on("mousedown.drag",d).filter(u).on("touchstart.drag",y).on("touchmove.drag",v,ee).on("touchend.drag touchcancel.drag",_).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function d(a,u){if(!r&&i.call(this,a,u)){var c=b(this,o.call(this,a,u),a,u,"mouse");c&&(Zn(a.view).on("mousemove.drag",p,re).on("mouseup.drag",g,re),ae(a.view),ie(a),e=!1,t=a.clientX,n=a.clientY,c("start",a))}}function p(r){if(oe(r),!e){var i=r.clientX-t,o=r.clientY-n;e=i*i+o*o>l}c.mouse("drag",r)}function g(t){Zn(t.view).on("mousemove.drag mouseup.drag",null),ue(t.view,e),oe(t),c.mouse("end",t)}function y(t,n){if(i.call(this,t,n)){var e,r,a=t.changedTouches,u=o.call(this,t,n),c=a.length;for(e=0;e+t,t.easePoly=wo,t.easePolyIn=mo,t.easePolyInOut=wo,t.easePolyOut=xo,t.easeQuad=_o,t.easeQuadIn=function(t){return t*t},t.easeQuadInOut=_o,t.easeQuadOut=function(t){return t*(2-t)},t.easeSin=Ao,t.easeSinIn=function(t){return 1==+t?1:1-Math.cos(t*To)},t.easeSinInOut=Ao,t.easeSinOut=function(t){return Math.sin(t*To)},t.every=function(t,n){if("function"!=typeof n)throw new TypeError("test is not a function");let e=-1;for(const r of t)if(!n(r,++e,t))return!1;return!0},t.extent=M,t.fcumsum=function(t,n){const e=new T;let r=-1;return Float64Array.from(t,void 0===n?t=>e.add(+t||0):i=>e.add(+n(i,++r,t)||0))},t.filter=function(t,n){if("function"!=typeof n)throw new TypeError("test is not a function");const e=[];let r=-1;for(const i of t)n(i,++r,t)&&e.push(i);return e},t.flatGroup=function(t,...n){return z(P(t,...n),n)},t.flatRollup=function(t,n,...e){return z(D(t,n,...e),e)},t.forceCenter=function(t,n){var e,r=1;function i(){var i,o,a=e.length,u=0,c=0;for(i=0;if+p||os+p||ac.index){var g=f-u.x-u.vx,y=s-u.y-u.vy,v=g*g+y*y;vt.r&&(t.r=t[n].r)}function c(){if(n){var r,i,o=n.length;for(e=new Array(o),r=0;r[u(t,n,r),t])));for(a=0,i=new Array(f);a=u)){(t.data!==n||t.next)&&(0===l&&(p+=(l=Uc(e))*l),0===h&&(p+=(h=Uc(e))*h),p(t=(Lc*t+jc)%Hc)/Hc}();function l(){h(),f.call("tick",n),e1?(null==e?u.delete(t):u.set(t,p(e)),n):u.get(t)},find:function(n,e,r){var i,o,a,u,c,f=0,s=t.length;for(null==r?r=1/0:r*=r,f=0;f1?(f.on(t,e),n):f.on(t)}}},t.forceX=function(t){var n,e,r,i=qc(.1);function o(t){for(var i,o=0,a=n.length;o=.12&&i<.234&&r>=-.425&&r<-.214?u:i>=.166&&i<.234&&r>=-.214&&r<-.115?c:a).invert(t)},s.stream=function(e){return t&&n===e?t:(r=[a.stream(n=e),u.stream(e),c.stream(e)],i=r.length,t={point:function(t,n){for(var e=-1;++ejs(r[0],r[1])&&(r[1]=i[1]),js(i[0],r[1])>js(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(a=-1/0,n=0,r=o[e=o.length-1];n<=e;r=i,++n)i=o[n],(u=js(r[1],i[0]))>a&&(a=u,Wf=i[0],Kf=r[1])}return is=os=null,Wf===1/0||Zf===1/0?[[NaN,NaN],[NaN,NaN]]:[[Wf,Zf],[Kf,Qf]]},t.geoCentroid=function(t){ms=xs=ws=Ms=Ts=As=Ss=Es=0,Ns=new T,ks=new T,Cs=new T,Lf(t,Gs);var n=+Ns,e=+ks,r=+Cs,i=Ef(n,e,r);return i=0))throw new RangeError(`invalid digits: ${t}`);i=n}return null===n&&(r=new ed(i)),a},a.projection(t).digits(i).context(n)},t.geoProjection=yd,t.geoProjectionMutator=vd,t.geoRotation=ll,t.geoStereographic=function(){return yd(Bd).scale(250).clipAngle(142)},t.geoStereographicRaw=Bd,t.geoStream=Lf,t.geoTransform=function(t){return{stream:id(t)}},t.geoTransverseMercator=function(){var t=Ed(Yd),n=t.center,e=t.rotate;return t.center=function(t){return arguments.length?n([-t[1],t[0]]):[(t=n())[1],-t[0]]},t.rotate=function(t){return arguments.length?e([t[0],t[1],t.length>2?t[2]+90:90]):[(t=e())[0],t[1],t[2]-90]},e([0,0,90]).scale(159.155)},t.geoTransverseMercatorRaw=Yd,t.gray=function(t,n){return new ur(t,0,0,null==n?1:n)},t.greatest=ot,t.greatestIndex=function(t,e=n){if(1===e.length)return tt(t,e);let r,i=-1,o=-1;for(const n of t)++o,(i<0?0===e(n,n):e(n,r)>0)&&(r=n,i=o);return i},t.group=C,t.groupSort=function(t,e,r){return(2!==e.length?U($(t,e,r),(([t,e],[r,i])=>n(e,i)||n(t,r))):U(C(t,r),(([t,r],[i,o])=>e(r,o)||n(t,i)))).map((([t])=>t))},t.groups=P,t.hcl=dr,t.hierarchy=Gd,t.histogram=Q,t.hsl=He,t.html=Ec,t.image=function(t,n){return new Promise((function(e,r){var i=new Image;for(var o in n)i[o]=n[o];i.onerror=r,i.onload=function(){e(i)},i.src=t}))},t.index=function(t,...n){return F(t,k,R,n)},t.indexes=function(t,...n){return F(t,Array.from,R,n)},t.interpolate=Gr,t.interpolateArray=function(t,n){return(Ir(n)?Ur:Or)(t,n)},t.interpolateBasis=Er,t.interpolateBasisClosed=Nr,t.interpolateBlues=Xb,t.interpolateBrBG=ib,t.interpolateBuGn=wb,t.interpolateBuPu=Tb,t.interpolateCividis=function(t){return t=Math.max(0,Math.min(1,t)),"rgb("+Math.max(0,Math.min(255,Math.round(-4.54-t*(35.34-t*(2381.73-t*(6402.7-t*(7024.72-2710.57*t)))))))+", "+Math.max(0,Math.min(255,Math.round(32.49+t*(170.73+t*(52.82-t*(131.46-t*(176.58-67.37*t)))))))+", "+Math.max(0,Math.min(255,Math.round(81.24+t*(442.36-t*(2482.43-t*(6167.24-t*(6614.94-2475.67*t)))))))+")"},t.interpolateCool=om,t.interpolateCubehelix=li,t.interpolateCubehelixDefault=rm,t.interpolateCubehelixLong=hi,t.interpolateDate=Br,t.interpolateDiscrete=function(t){var n=t.length;return function(e){return t[Math.max(0,Math.min(n-1,Math.floor(e*n)))]}},t.interpolateGnBu=Sb,t.interpolateGreens=Vb,t.interpolateGreys=Zb,t.interpolateHcl=ci,t.interpolateHclLong=fi,t.interpolateHsl=oi,t.interpolateHslLong=ai,t.interpolateHue=function(t,n){var e=Pr(+t,+n);return function(t){var n=e(t);return n-360*Math.floor(n/360)}},t.interpolateInferno=dm,t.interpolateLab=function(t,n){var e=$r((t=ar(t)).l,(n=ar(n)).l),r=$r(t.a,n.a),i=$r(t.b,n.b),o=$r(t.opacity,n.opacity);return function(n){return t.l=e(n),t.a=r(n),t.b=i(n),t.opacity=o(n),t+""}},t.interpolateMagma=hm,t.interpolateNumber=Yr,t.interpolateNumberArray=Ur,t.interpolateObject=Lr,t.interpolateOrRd=Nb,t.interpolateOranges=em,t.interpolatePRGn=ab,t.interpolatePiYG=cb,t.interpolatePlasma=pm,t.interpolatePuBu=zb,t.interpolatePuBuGn=Cb,t.interpolatePuOr=sb,t.interpolatePuRd=Db,t.interpolatePurples=Qb,t.interpolateRainbow=function(t){(t<0||t>1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return am.h=360*t-100,am.s=1.5-1.5*n,am.l=.8-.9*n,am+""},t.interpolateRdBu=hb,t.interpolateRdGy=pb,t.interpolateRdPu=Fb,t.interpolateRdYlBu=yb,t.interpolateRdYlGn=_b,t.interpolateReds=tm,t.interpolateRgb=Dr,t.interpolateRgbBasis=Fr,t.interpolateRgbBasisClosed=qr,t.interpolateRound=Vr,t.interpolateSinebow=function(t){var n;return t=(.5-t)*Math.PI,um.r=255*(n=Math.sin(t))*n,um.g=255*(n=Math.sin(t+cm))*n,um.b=255*(n=Math.sin(t+fm))*n,um+""},t.interpolateSpectral=mb,t.interpolateString=Xr,t.interpolateTransformCss=ti,t.interpolateTransformSvg=ni,t.interpolateTurbo=function(t){return t=Math.max(0,Math.min(1,t)),"rgb("+Math.max(0,Math.min(255,Math.round(34.61+t*(1172.33-t*(10793.56-t*(33300.12-t*(38394.49-14825.05*t)))))))+", "+Math.max(0,Math.min(255,Math.round(23.31+t*(557.33+t*(1225.33-t*(3574.96-t*(1073.77+707.56*t)))))))+", "+Math.max(0,Math.min(255,Math.round(27.2+t*(3211.1-t*(15327.97-t*(27814-t*(22569.18-6838.66*t)))))))+")"},t.interpolateViridis=lm,t.interpolateWarm=im,t.interpolateYlGn=Ob,t.interpolateYlGnBu=Ub,t.interpolateYlOrBr=Yb,t.interpolateYlOrRd=jb,t.interpolateZoom=ri,t.interrupt=Gi,t.intersection=function(t,...n){t=new InternSet(t),n=n.map(vt);t:for(const e of t)for(const r of n)if(!r.has(e)){t.delete(e);continue t}return t},t.interval=function(t,n,e){var r=new Ei,i=n;return null==n?(r.restart(t,n,e),r):(r._restart=r.restart,r.restart=function(t,n,e){n=+n,e=null==e?Ai():+e,r._restart((function o(a){a+=i,r._restart(o,i+=n,e),t(a)}),n,e)},r.restart(t,n,e),r)},t.isoFormat=D_,t.isoParse=F_,t.json=function(t,n){return fetch(t,n).then(Tc)},t.lab=ar,t.lch=function(t,n,e,r){return 1===arguments.length?hr(t):new pr(e,n,t,null==r?1:r)},t.least=function(t,e=n){let r,i=!1;if(1===e.length){let o;for(const a of t){const t=e(a);(i?n(t,o)<0:0===n(t,t))&&(r=a,o=t,i=!0)}}else for(const n of t)(i?e(n,r)<0:0===e(n,n))&&(r=n,i=!0);return r},t.leastIndex=ht,t.line=Bm,t.lineRadial=Wm,t.link=ox,t.linkHorizontal=function(){return ox(tx)},t.linkRadial=function(){const t=ox(ex);return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t},t.linkVertical=function(){return ox(nx)},t.local=Qn,t.map=function(t,n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");if("function"!=typeof n)throw new TypeError("mapper is not a function");return Array.from(t,((e,r)=>n(e,r,t)))},t.matcher=Vt,t.max=J,t.maxIndex=tt,t.mean=function(t,n){let e=0,r=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(++e,r+=n);else{let i=-1;for(let o of t)null!=(o=n(o,++i,t))&&(o=+o)>=o&&(++e,r+=o)}if(e)return r/e},t.median=function(t,n){return at(t,.5,n)},t.medianIndex=function(t,n){return ct(t,.5,n)},t.merge=ft,t.min=nt,t.minIndex=et,t.mode=function(t,n){const e=new InternMap;if(void 0===n)for(let n of t)null!=n&&n>=n&&e.set(n,(e.get(n)||0)+1);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&i>=i&&e.set(i,(e.get(i)||0)+1)}let r,i=0;for(const[t,n]of e)n>i&&(i=n,r=t);return r},t.namespace=It,t.namespaces=Ut,t.nice=Z,t.now=Ai,t.pack=function(){var t=null,n=1,e=1,r=np;function i(i){const o=ap();return i.x=n/2,i.y=e/2,t?i.eachBefore(xp(t)).eachAfter(wp(r,.5,o)).eachBefore(Mp(1)):i.eachBefore(xp(mp)).eachAfter(wp(np,1,o)).eachAfter(wp(r,i.r/Math.min(n,e),o)).eachBefore(Mp(Math.min(n,e)/(2*i.r))),i}return i.radius=function(n){return arguments.length?(t=Jd(n),i):t},i.size=function(t){return arguments.length?(n=+t[0],e=+t[1],i):[n,e]},i.padding=function(t){return arguments.length?(r="function"==typeof t?t:ep(+t),i):r},i},t.packEnclose=function(t){return up(t,ap())},t.packSiblings=function(t){return bp(t,ap()),t},t.pairs=function(t,n=st){const e=[];let r,i=!1;for(const o of t)i&&e.push(n(r,o)),r=o,i=!0;return e},t.partition=function(){var t=1,n=1,e=0,r=!1;function i(i){var o=i.height+1;return i.x0=i.y0=e,i.x1=t,i.y1=n/o,i.eachBefore(function(t,n){return function(r){r.children&&Ap(r,r.x0,t*(r.depth+1)/n,r.x1,t*(r.depth+2)/n);var i=r.x0,o=r.y0,a=r.x1-e,u=r.y1-e;a0&&(d+=l);for(null!=n?p.sort((function(t,e){return n(g[t],g[e])})):null!=e&&p.sort((function(t,n){return e(a[t],a[n])})),u=0,f=d?(v-h*b)/d:0;u0?l*f:0)+b,g[c]={data:a[c],index:u,value:l,startAngle:y,endAngle:s,padAngle:_};return g}return a.value=function(n){return arguments.length?(t="function"==typeof n?n:gm(+n),a):t},a.sortValues=function(t){return arguments.length?(n=t,e=null,a):n},a.sort=function(t){return arguments.length?(e=t,n=null,a):e},a.startAngle=function(t){return arguments.length?(r="function"==typeof t?t:gm(+t),a):r},a.endAngle=function(t){return arguments.length?(i="function"==typeof t?t:gm(+t),a):i},a.padAngle=function(t){return arguments.length?(o="function"==typeof t?t:gm(+t),a):o},a},t.piecewise=di,t.pointRadial=Km,t.pointer=ne,t.pointers=function(t,n){return t.target&&(t=te(t),void 0===n&&(n=t.currentTarget),t=t.touches||[t]),Array.from(t,(t=>ne(t,n)))},t.polygonArea=function(t){for(var n,e=-1,r=t.length,i=t[r-1],o=0;++eu!=f>u&&a<(c-e)*(u-r)/(f-r)+e&&(s=!s),c=e,f=r;return s},t.polygonHull=function(t){if((e=t.length)<3)return null;var n,e,r=new Array(e),i=new Array(e);for(n=0;n=0;--n)f.push(t[r[o[n]][2]]);for(n=+u;n(n=1664525*n+1013904223|0,lg*(n>>>0))},t.randomLogNormal=Kp,t.randomLogistic=fg,t.randomNormal=Zp,t.randomPareto=ng,t.randomPoisson=sg,t.randomUniform=Vp,t.randomWeibull=ug,t.range=lt,t.rank=function(t,e=n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");let r=Array.from(t);const i=new Float64Array(r.length);2!==e.length&&(r=r.map(e),e=n);const o=(t,n)=>e(r[t],r[n]);let a,u;return(t=Uint32Array.from(r,((t,n)=>n))).sort(e===n?(t,n)=>O(r[t],r[n]):I(o)),t.forEach(((t,n)=>{const e=o(t,void 0===a?t:a);e>=0?((void 0===a||e>0)&&(a=t,u=n),i[t]=u):i[t]=NaN})),i},t.reduce=function(t,n,e){if("function"!=typeof n)throw new TypeError("reducer is not a function");const r=t[Symbol.iterator]();let i,o,a=-1;if(arguments.length<3){if(({done:i,value:e}=r.next()),i)return;++a}for(;({done:i,value:o}=r.next()),!i;)e=n(e,o,++a,t);return e},t.reverse=function(t){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");return Array.from(t).reverse()},t.rgb=Fe,t.ribbon=function(){return Wa()},t.ribbonArrow=function(){return Wa(Va)},t.rollup=$,t.rollups=D,t.scaleBand=yg,t.scaleDiverging=function t(){var n=Ng(L_()(mg));return n.copy=function(){return B_(n,t())},dg.apply(n,arguments)},t.scaleDivergingLog=function t(){var n=Fg(L_()).domain([.1,1,10]);return n.copy=function(){return B_(n,t()).base(n.base())},dg.apply(n,arguments)},t.scaleDivergingPow=j_,t.scaleDivergingSqrt=function(){return j_.apply(null,arguments).exponent(.5)},t.scaleDivergingSymlog=function t(){var n=Ig(L_());return n.copy=function(){return B_(n,t()).constant(n.constant())},dg.apply(n,arguments)},t.scaleIdentity=function t(n){var e;function r(t){return null==t||isNaN(t=+t)?e:t}return r.invert=r,r.domain=r.range=function(t){return arguments.length?(n=Array.from(t,_g),r):n.slice()},r.unknown=function(t){return arguments.length?(e=t,r):e},r.copy=function(){return t(n).unknown(e)},n=arguments.length?Array.from(n,_g):[0,1],Ng(r)},t.scaleImplicit=pg,t.scaleLinear=function t(){var n=Sg();return n.copy=function(){return Tg(n,t())},hg.apply(n,arguments),Ng(n)},t.scaleLog=function t(){const n=Fg(Ag()).domain([1,10]);return n.copy=()=>Tg(n,t()).base(n.base()),hg.apply(n,arguments),n},t.scaleOrdinal=gg,t.scalePoint=function(){return vg(yg.apply(null,arguments).paddingInner(1))},t.scalePow=jg,t.scaleQuantile=function t(){var e,r=[],i=[],o=[];function a(){var t=0,n=Math.max(1,i.length);for(o=new Array(n-1);++t0?o[n-1]:r[0],n=i?[o[i-1],r]:[o[n-1],o[n]]},u.unknown=function(t){return arguments.length?(n=t,u):u},u.thresholds=function(){return o.slice()},u.copy=function(){return t().domain([e,r]).range(a).unknown(n)},hg.apply(Ng(u),arguments)},t.scaleRadial=function t(){var n,e=Sg(),r=[0,1],i=!1;function o(t){var r=function(t){return Math.sign(t)*Math.sqrt(Math.abs(t))}(e(t));return isNaN(r)?n:i?Math.round(r):r}return o.invert=function(t){return e.invert(Hg(t))},o.domain=function(t){return arguments.length?(e.domain(t),o):e.domain()},o.range=function(t){return arguments.length?(e.range((r=Array.from(t,_g)).map(Hg)),o):r.slice()},o.rangeRound=function(t){return o.range(t).round(!0)},o.round=function(t){return arguments.length?(i=!!t,o):i},o.clamp=function(t){return arguments.length?(e.clamp(t),o):e.clamp()},o.unknown=function(t){return arguments.length?(n=t,o):n},o.copy=function(){return t(e.domain(),r).round(i).clamp(e.clamp()).unknown(n)},hg.apply(o,arguments),Ng(o)},t.scaleSequential=function t(){var n=Ng(O_()(mg));return n.copy=function(){return B_(n,t())},dg.apply(n,arguments)},t.scaleSequentialLog=function t(){var n=Fg(O_()).domain([1,10]);return n.copy=function(){return B_(n,t()).base(n.base())},dg.apply(n,arguments)},t.scaleSequentialPow=Y_,t.scaleSequentialQuantile=function t(){var e=[],r=mg;function i(t){if(null!=t&&!isNaN(t=+t))return r((s(e,t,1)-1)/(e.length-1))}return i.domain=function(t){if(!arguments.length)return e.slice();e=[];for(let n of t)null==n||isNaN(n=+n)||e.push(n);return e.sort(n),i},i.interpolator=function(t){return arguments.length?(r=t,i):r},i.range=function(){return e.map(((t,n)=>r(n/(e.length-1))))},i.quantiles=function(t){return Array.from({length:t+1},((n,r)=>at(e,r/t)))},i.copy=function(){return t(r).domain(e)},dg.apply(i,arguments)},t.scaleSequentialSqrt=function(){return Y_.apply(null,arguments).exponent(.5)},t.scaleSequentialSymlog=function t(){var n=Ig(O_());return n.copy=function(){return B_(n,t()).constant(n.constant())},dg.apply(n,arguments)},t.scaleSqrt=function(){return jg.apply(null,arguments).exponent(.5)},t.scaleSymlog=function t(){var n=Ig(Ag());return n.copy=function(){return Tg(n,t()).constant(n.constant())},hg.apply(n,arguments)},t.scaleThreshold=function t(){var n,e=[.5],r=[0,1],i=1;function o(t){return null!=t&&t<=t?r[s(e,t,0,i)]:n}return o.domain=function(t){return arguments.length?(e=Array.from(t),i=Math.min(e.length,r.length-1),o):e.slice()},o.range=function(t){return arguments.length?(r=Array.from(t),i=Math.min(e.length,r.length-1),o):r.slice()},o.invertExtent=function(t){var n=r.indexOf(t);return[e[n-1],e[n]]},o.unknown=function(t){return arguments.length?(n=t,o):n},o.copy=function(){return t().domain(e).range(r).unknown(n)},hg.apply(o,arguments)},t.scaleTime=function(){return hg.apply(I_(uv,cv,tv,Zy,xy,py,sy,ay,iy,t.timeFormat).domain([new Date(2e3,0,1),new Date(2e3,0,2)]),arguments)},t.scaleUtc=function(){return hg.apply(I_(ov,av,ev,Qy,Fy,yy,hy,cy,iy,t.utcFormat).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)]),arguments)},t.scan=function(t,n){const e=ht(t,n);return e<0?void 0:e},t.schemeAccent=G_,t.schemeBlues=Hb,t.schemeBrBG=rb,t.schemeBuGn=xb,t.schemeBuPu=Mb,t.schemeCategory10=X_,t.schemeDark2=V_,t.schemeGnBu=Ab,t.schemeGreens=Gb,t.schemeGreys=Wb,t.schemeOrRd=Eb,t.schemeOranges=nm,t.schemePRGn=ob,t.schemePaired=W_,t.schemePastel1=Z_,t.schemePastel2=K_,t.schemePiYG=ub,t.schemePuBu=Pb,t.schemePuBuGn=kb,t.schemePuOr=fb,t.schemePuRd=$b,t.schemePurples=Kb,t.schemeRdBu=lb,t.schemeRdGy=db,t.schemeRdPu=Rb,t.schemeRdYlBu=gb,t.schemeRdYlGn=vb,t.schemeReds=Jb,t.schemeSet1=Q_,t.schemeSet2=J_,t.schemeSet3=tb,t.schemeSpectral=bb,t.schemeTableau10=nb,t.schemeYlGn=Ib,t.schemeYlGnBu=qb,t.schemeYlOrBr=Bb,t.schemeYlOrRd=Lb,t.select=Zn,t.selectAll=function(t){return"string"==typeof t?new Vn([document.querySelectorAll(t)],[document.documentElement]):new Vn([Ht(t)],Gn)},t.selection=Wn,t.selector=jt,t.selectorAll=Gt,t.shuffle=dt,t.shuffler=pt,t.some=function(t,n){if("function"!=typeof n)throw new TypeError("test is not a function");let e=-1;for(const r of t)if(n(r,++e,t))return!0;return!1},t.sort=U,t.stack=function(){var t=gm([]),n=hw,e=lw,r=dw;function i(i){var o,a,u=Array.from(t.apply(this,arguments),pw),c=u.length,f=-1;for(const t of i)for(o=0,++f;o0)for(var e,r,i,o,a,u,c=0,f=t[n[0]].length;c0?(r[0]=o,r[1]=o+=i):i<0?(r[1]=a,r[0]=a+=i):(r[0]=0,r[1]=i)},t.stackOffsetExpand=function(t,n){if((r=t.length)>0){for(var e,r,i,o=0,a=t[0].length;o0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,a=1;afunction(t){t=`${t}`;let n=t.length;zp(t,n-1)&&!zp(t,n-2)&&(t=t.slice(0,-1));return"/"===t[0]?t:`/${t}`}(t(n,e,r)))),e=n.map(Pp),i=new Set(n).add("");for(const t of e)i.has(t)||(i.add(t),n.push(t),e.push(Pp(t)),h.push(Np));d=(t,e)=>n[e],p=(t,n)=>e[n]}for(a=0,i=h.length;a=0&&(f=h[t]).data===Np;--t)f.data=null}if(u.parent=Sp,u.eachBefore((function(t){t.depth=t.parent.depth+1,--i})).eachBefore(Kd),u.parent=null,i>0)throw new Error("cycle");return u}return r.id=function(t){return arguments.length?(n=Jd(t),r):n},r.parentId=function(t){return arguments.length?(e=Jd(t),r):e},r.path=function(n){return arguments.length?(t=Jd(n),r):t},r},t.style=_n,t.subset=function(t,n){return _t(n,t)},t.sum=function(t,n){let e=0;if(void 0===n)for(let n of t)(n=+n)&&(e+=n);else{let r=-1;for(let i of t)(i=+n(i,++r,t))&&(e+=i)}return e},t.superset=_t,t.svg=Nc,t.symbol=function(t,n){let e=null,r=Nm(i);function i(){let i;if(e||(e=i=r()),t.apply(this,arguments).draw(e,+n.apply(this,arguments)),i)return e=null,i+""||null}return t="function"==typeof t?t:gm(t||cx),n="function"==typeof n?n:gm(void 0===n?64:+n),i.type=function(n){return arguments.length?(t="function"==typeof n?n:gm(n),i):t},i.size=function(t){return arguments.length?(n="function"==typeof t?t:gm(+t),i):n},i.context=function(t){return arguments.length?(e=null==t?null:t,i):e},i},t.symbolAsterisk=ux,t.symbolCircle=cx,t.symbolCross=fx,t.symbolDiamond=hx,t.symbolDiamond2=dx,t.symbolPlus=px,t.symbolSquare=gx,t.symbolSquare2=yx,t.symbolStar=mx,t.symbolTimes=Cx,t.symbolTriangle=wx,t.symbolTriangle2=Tx,t.symbolWye=kx,t.symbolX=Cx,t.symbols=Px,t.symbolsFill=Px,t.symbolsStroke=zx,t.text=mc,t.thresholdFreedmanDiaconis=function(t,n,e){const r=v(t),i=at(t,.75)-at(t,.25);return r&&i?Math.ceil((e-n)/(2*i*Math.pow(r,-1/3))):1},t.thresholdScott=function(t,n,e){const r=v(t),i=w(t);return r&&i?Math.ceil((e-n)*Math.cbrt(r)/(3.49*i)):1},t.thresholdSturges=K,t.tickFormat=Eg,t.tickIncrement=V,t.tickStep=W,t.ticks=G,t.timeDay=py,t.timeDays=gy,t.timeFormatDefaultLocale=P_,t.timeFormatLocale=hv,t.timeFriday=Sy,t.timeFridays=$y,t.timeHour=sy,t.timeHours=ly,t.timeInterval=Vg,t.timeMillisecond=Wg,t.timeMilliseconds=Zg,t.timeMinute=ay,t.timeMinutes=uy,t.timeMonday=wy,t.timeMondays=ky,t.timeMonth=Zy,t.timeMonths=Ky,t.timeSaturday=Ey,t.timeSaturdays=Dy,t.timeSecond=iy,t.timeSeconds=oy,t.timeSunday=xy,t.timeSundays=Ny,t.timeThursday=Ay,t.timeThursdays=zy,t.timeTickInterval=cv,t.timeTicks=uv,t.timeTuesday=My,t.timeTuesdays=Cy,t.timeWednesday=Ty,t.timeWednesdays=Py,t.timeWeek=xy,t.timeWeeks=Ny,t.timeYear=tv,t.timeYears=nv,t.timeout=$i,t.timer=Ni,t.timerFlush=ki,t.transition=go,t.transpose=gt,t.tree=function(){var t=$p,n=1,e=1,r=null;function i(i){var c=function(t){for(var n,e,r,i,o,a=new Up(t,0),u=[a];n=u.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)u.push(e=n.children[i]=new Up(r[i],i)),e.parent=n;return(a.parent=new Up(null,0)).children=[a],a}(i);if(c.eachAfter(o),c.parent.m=-c.z,c.eachBefore(a),r)i.eachBefore(u);else{var f=i,s=i,l=i;i.eachBefore((function(t){t.xs.x&&(s=t),t.depth>l.depth&&(l=t)}));var h=f===s?1:t(f,s)/2,d=h-f.x,p=n/(s.x+h+d),g=e/(l.depth||1);i.eachBefore((function(t){t.x=(t.x+d)*p,t.y=t.depth*g}))}return i}function o(n){var e=n.children,r=n.parent.children,i=n.i?r[n.i-1]:null;if(e){!function(t){for(var n,e=0,r=0,i=t.children,o=i.length;--o>=0;)(n=i[o]).z+=e,n.m+=e,e+=n.s+(r+=n.c)}(n);var o=(e[0].z+e[e.length-1].z)/2;i?(n.z=i.z+t(n._,i._),n.m=n.z-o):n.z=o}else i&&(n.z=i.z+t(n._,i._));n.parent.A=function(n,e,r){if(e){for(var i,o=n,a=n,u=e,c=o.parent.children[0],f=o.m,s=a.m,l=u.m,h=c.m;u=Rp(u),o=Dp(o),u&&o;)c=Dp(c),(a=Rp(a)).a=n,(i=u.z+l-o.z-f+t(u._,o._))>0&&(Fp(qp(u,n,r),n,i),f+=i,s+=i),l+=u.m,f+=o.m,h+=c.m,s+=a.m;u&&!Rp(a)&&(a.t=u,a.m+=l-s),o&&!Dp(c)&&(c.t=o,c.m+=f-h,r=n)}return r}(n,i,n.parent.A||r[0])}function a(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function u(t){t.x*=n,t.y=t.depth*e}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.treemap=function(){var t=Yp,n=!1,e=1,r=1,i=[0],o=np,a=np,u=np,c=np,f=np;function s(t){return t.x0=t.y0=0,t.x1=e,t.y1=r,t.eachBefore(l),i=[0],n&&t.eachBefore(Tp),t}function l(n){var e=i[n.depth],r=n.x0+e,s=n.y0+e,l=n.x1-e,h=n.y1-e;l=e-1){var s=u[n];return s.x0=i,s.y0=o,s.x1=a,void(s.y1=c)}var l=f[n],h=r/2+l,d=n+1,p=e-1;for(;d>>1;f[g]c-o){var _=r?(i*v+a*y)/r:a;t(n,d,y,i,o,_,c),t(d,e,v,_,o,a,c)}else{var b=r?(o*v+c*y)/r:c;t(n,d,y,i,o,a,b),t(d,e,v,i,b,a,c)}}(0,c,t.value,n,e,r,i)},t.treemapDice=Ap,t.treemapResquarify=Lp,t.treemapSlice=Ip,t.treemapSliceDice=function(t,n,e,r,i){(1&t.depth?Ip:Ap)(t,n,e,r,i)},t.treemapSquarify=Yp,t.tsv=Mc,t.tsvFormat=lc,t.tsvFormatBody=hc,t.tsvFormatRow=pc,t.tsvFormatRows=dc,t.tsvFormatValue=gc,t.tsvParse=fc,t.tsvParseRows=sc,t.union=function(...t){const n=new InternSet;for(const e of t)for(const t of e)n.add(t);return n},t.unixDay=_y,t.unixDays=by,t.utcDay=yy,t.utcDays=vy,t.utcFriday=By,t.utcFridays=Vy,t.utcHour=hy,t.utcHours=dy,t.utcMillisecond=Wg,t.utcMilliseconds=Zg,t.utcMinute=cy,t.utcMinutes=fy,t.utcMonday=qy,t.utcMondays=jy,t.utcMonth=Qy,t.utcMonths=Jy,t.utcSaturday=Yy,t.utcSaturdays=Wy,t.utcSecond=iy,t.utcSeconds=oy,t.utcSunday=Fy,t.utcSundays=Ly,t.utcThursday=Oy,t.utcThursdays=Gy,t.utcTickInterval=av,t.utcTicks=ov,t.utcTuesday=Uy,t.utcTuesdays=Hy,t.utcWednesday=Iy,t.utcWednesdays=Xy,t.utcWeek=Fy,t.utcWeeks=Ly,t.utcYear=ev,t.utcYears=rv,t.variance=x,t.version="7.8.5",t.window=pn,t.xml=Sc,t.zip=function(){return gt(arguments)},t.zoom=function(){var t,n,e,r=Sw,i=Ew,o=Pw,a=kw,u=Cw,c=[0,1/0],f=[[-1/0,-1/0],[1/0,1/0]],s=250,l=ri,h=$t("start","zoom","end"),d=500,p=150,g=0,y=10;function v(t){t.property("__zoom",Nw).on("wheel.zoom",T,{passive:!1}).on("mousedown.zoom",A).on("dblclick.zoom",S).filter(u).on("touchstart.zoom",E).on("touchmove.zoom",N).on("touchend.zoom touchcancel.zoom",k).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function _(t,n){return(n=Math.max(c[0],Math.min(c[1],n)))===t.k?t:new xw(n,t.x,t.y)}function b(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new xw(t.k,r,i)}function m(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function x(t,n,e,r){t.on("start.zoom",(function(){w(this,arguments).event(r).start()})).on("interrupt.zoom end.zoom",(function(){w(this,arguments).event(r).end()})).tween("zoom",(function(){var t=this,o=arguments,a=w(t,o).event(r),u=i.apply(t,o),c=null==e?m(u):"function"==typeof e?e.apply(t,o):e,f=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),s=t.__zoom,h="function"==typeof n?n.apply(t,o):n,d=l(s.invert(c).concat(f/s.k),h.invert(c).concat(f/h.k));return function(t){if(1===t)t=h;else{var n=d(t),e=f/n[2];t=new xw(e,c[0]-n[0]*e,c[1]-n[1]*e)}a.zoom(null,t)}}))}function w(t,n,e){return!e&&t.__zooming||new M(t,n)}function M(t,n){this.that=t,this.args=n,this.active=0,this.sourceEvent=null,this.extent=i.apply(t,n),this.taps=0}function T(t,...n){if(r.apply(this,arguments)){var e=w(this,n).event(t),i=this.__zoom,u=Math.max(c[0],Math.min(c[1],i.k*Math.pow(2,a.apply(this,arguments)))),s=ne(t);if(e.wheel)e.mouse[0][0]===s[0]&&e.mouse[0][1]===s[1]||(e.mouse[1]=i.invert(e.mouse[0]=s)),clearTimeout(e.wheel);else{if(i.k===u)return;e.mouse=[s,i.invert(s)],Gi(this),e.start()}Aw(t),e.wheel=setTimeout((function(){e.wheel=null,e.end()}),p),e.zoom("mouse",o(b(_(i,u),e.mouse[0],e.mouse[1]),e.extent,f))}}function A(t,...n){if(!e&&r.apply(this,arguments)){var i=t.currentTarget,a=w(this,n,!0).event(t),u=Zn(t.view).on("mousemove.zoom",(function(t){if(Aw(t),!a.moved){var n=t.clientX-s,e=t.clientY-l;a.moved=n*n+e*e>g}a.event(t).zoom("mouse",o(b(a.that.__zoom,a.mouse[0]=ne(t,i),a.mouse[1]),a.extent,f))}),!0).on("mouseup.zoom",(function(t){u.on("mousemove.zoom mouseup.zoom",null),ue(t.view,a.moved),Aw(t),a.event(t).end()}),!0),c=ne(t,i),s=t.clientX,l=t.clientY;ae(t.view),Tw(t),a.mouse=[c,this.__zoom.invert(c)],Gi(this),a.start()}}function S(t,...n){if(r.apply(this,arguments)){var e=this.__zoom,a=ne(t.changedTouches?t.changedTouches[0]:t,this),u=e.invert(a),c=e.k*(t.shiftKey?.5:2),l=o(b(_(e,c),a,u),i.apply(this,n),f);Aw(t),s>0?Zn(this).transition().duration(s).call(x,l,a,t):Zn(this).call(v.transform,l,a,t)}}function E(e,...i){if(r.apply(this,arguments)){var o,a,u,c,f=e.touches,s=f.length,l=w(this,i,e.changedTouches.length===s).event(e);for(Tw(e),a=0;a --help` for command-specific help.""" + + +# Constants for socket synchronization +_SYNC_TIMEOUT_SEC = 5.0 +_PROCESS_KILL_TIMEOUT_SEC = 2.0 +_READY_MESSAGE = b"ready" +_RECV_BUFFER_SIZE = 1024 +_BINARY_PROFILE_HEADER_SIZE = 64 +_BINARY_PROFILE_MAGICS = (b"HCAT", b"TACH") + +# Format configuration +FORMAT_EXTENSIONS = { + "pstats": "pstats", + "collapsed": "txt", + "flamegraph": "html", + "diff_flamegraph": "html", + "gecko": "json", + "heatmap": "html", + "jsonl": "jsonl", + "binary": "bin", +} + +COLLECTOR_MAP = { + "pstats": PstatsCollector, + "collapsed": CollapsedStackCollector, + "flamegraph": FlamegraphCollector, + "diff_flamegraph": DiffFlamegraphCollector, + "gecko": GeckoCollector, + "heatmap": HeatmapCollector, + "jsonl": JsonlCollector, + "binary": BinaryCollector, +} + +BROWSER_COMPATIBLE_FORMATS = ("flamegraph", "diff_flamegraph", "heatmap") + + +def _setup_child_monitor(args, parent_pid): + # Build CLI args for child profilers (excluding --subprocesses to avoid recursion) + child_cli_args = _build_child_profiler_args(args) + + # Build output pattern + output_pattern = _build_output_pattern(args) + + return ChildProcessMonitor( + pid=parent_pid, + cli_args=child_cli_args, + output_pattern=output_pattern, + ) + + +def _get_child_monitor_context(args, pid): + if getattr(args, 'subprocesses', False): + return _setup_child_monitor(args, pid) + return nullcontext() + + +def _build_child_profiler_args(args): + child_args = [] + + # Sampling options + hz = MICROSECONDS_PER_SECOND // args.sample_interval_usec + child_args.extend(["-r", str(hz)]) + if args.duration is not None: + child_args.extend(["-d", str(args.duration)]) + if args.all_threads: + child_args.append("-a") + if args.realtime_stats: + child_args.append("--realtime-stats") + if args.native: + child_args.append("--native") + if not args.gc: + child_args.append("--no-gc") + if args.opcodes: + child_args.append("--opcodes") + if args.async_aware: + child_args.append("--async-aware") + async_mode = getattr(args, 'async_mode', 'running') + if async_mode != "running": + child_args.extend(["--async-mode", async_mode]) + + # Mode options + mode = getattr(args, 'mode', 'wall') + if mode != "wall": + child_args.extend(["--mode", mode]) + + # Format options (skip pstats as it's the default) + if args.format == "diff_flamegraph": + child_args.extend(["--diff-flamegraph", args.diff_baseline]) + elif args.format != "pstats": + child_args.append(f"--{args.format}") + + return child_args + + +def _build_output_pattern(args): + """Build output filename pattern for child profilers. + + The pattern uses {pid} as a placeholder which will be replaced with the + actual child PID using str.replace(), so user filenames with braces are safe. + """ + if args.outfile: + # User specified output - add PID to filename + base, ext = os.path.splitext(args.outfile) + if ext: + return f"{base}_{{pid}}{ext}" + else: + return f"{args.outfile}_{{pid}}" + else: + # Use default pattern based on format (consistent _ separator) + extension = FORMAT_EXTENSIONS.get(args.format, "txt") + if args.format == "heatmap": + return "heatmap_{pid}" + if args.format == "pstats": + # pstats defaults to stdout, but for subprocesses we need files + return "profile_{pid}.pstats" + return f"{args.format}_{{pid}}.{extension}" + + +def _parse_mode(mode_string): + """Convert mode string to mode constant.""" + mode_map = { + "wall": PROFILING_MODE_WALL, + "cpu": PROFILING_MODE_CPU, + "gil": PROFILING_MODE_GIL, + "exception": PROFILING_MODE_EXCEPTION, + } + return mode_map[mode_string] + + +def _check_process_died(process): + """Check if process died and raise an error with stderr if available.""" + if process.poll() is None: + return + + # Process died - try to get stderr for error message + stderr_msg = "" + if process.stderr: + try: + stderr_msg = process.stderr.read().decode().strip() + except (OSError, UnicodeDecodeError): + pass + + if stderr_msg: + raise RuntimeError(stderr_msg) + raise RuntimeError(f"Process exited with code {process.returncode}") + + +def _wait_for_ready_signal(sync_sock, process, timeout): + """Wait for the ready signal from the subprocess, checking for early death.""" + deadline = time.monotonic() + timeout + sel = selectors.DefaultSelector() + sel.register(sync_sock, selectors.EVENT_READ) + + try: + while True: + _check_process_died(process) + + remaining = deadline - time.monotonic() + if remaining <= 0: + raise socket.timeout("timed out") + + if not sel.select(timeout=min(0.1, remaining)): + continue + + conn, _ = sync_sock.accept() + try: + ready_signal = conn.recv(_RECV_BUFFER_SIZE) + finally: + conn.close() + + if ready_signal != _READY_MESSAGE: + raise RuntimeError(f"Invalid ready signal received: {ready_signal!r}") + return + finally: + sel.close() + + +def _run_with_sync(original_cmd, suppress_output=False): + """Run a command with socket-based synchronization and return the process.""" + # Create a TCP socket for synchronization with better socket options + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sync_sock: + # Set SO_REUSEADDR to avoid "Address already in use" errors + sync_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sync_sock.bind(("127.0.0.1", 0)) # Let OS choose a free port + sync_port = sync_sock.getsockname()[1] + sync_sock.listen(1) + sync_sock.settimeout(_SYNC_TIMEOUT_SEC) + + # Get current working directory to preserve it + cwd = os.getcwd() + + # Build command using the sync coordinator + target_args = original_cmd[1:] # Remove python executable + cmd = ( + sys.executable, + "-m", + "profiling.sampling._sync_coordinator", + str(sync_port), + cwd, + ) + tuple(target_args) + + # Start the process with coordinator + # When suppress_output=True (live mode), capture stderr so we can + # report errors if the process dies before signaling ready. + # When suppress_output=False (normal mode), let stderr inherit so + # script errors print to the terminal. + popen_kwargs = {} + if suppress_output: + popen_kwargs["stdin"] = subprocess.DEVNULL + popen_kwargs["stdout"] = subprocess.DEVNULL + popen_kwargs["stderr"] = subprocess.PIPE + + process = subprocess.Popen(cmd, **popen_kwargs) + + try: + _wait_for_ready_signal(sync_sock, process, _SYNC_TIMEOUT_SEC) + except socket.timeout: + # If we timeout, kill the process and raise an error + if process.poll() is None: + process.terminate() + try: + process.wait(timeout=_PROCESS_KILL_TIMEOUT_SEC) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + raise RuntimeError( + "Process failed to signal readiness within timeout" + ) + + return process + + +_RATE_PATTERN = re.compile(r''' + ^ # Start of string + ( # Group 1: The numeric value + \d+ # One or more digits (integer part) + (?:\.\d+)? # Optional: decimal point followed by digits + ) # Examples: "10", "0.5", "100.25" + ( # Group 2: Optional unit suffix + hz # "hz" - hertz + | khz # "khz" - kilohertz + | k # "k" - shorthand for kilohertz + )? # Suffix is optional (bare number = Hz) + $ # End of string + ''', re.VERBOSE | re.IGNORECASE) + + +def _parse_sampling_rate(rate_str: str) -> int: + """Parse sampling rate string to microseconds.""" + rate_str = rate_str.strip().lower() + + match = _RATE_PATTERN.match(rate_str) + if not match: + raise argparse.ArgumentTypeError( + f"Invalid sampling rate format: {rate_str}. " + "Expected: number followed by optional suffix (hz, khz, k) with no spaces (e.g., 10khz)" + ) + + number_part = match.group(1) + suffix = match.group(2) or '' + + # Determine multiplier based on suffix + suffix_map = { + 'hz': 1, + 'khz': 1000, + 'k': 1000, + } + multiplier = suffix_map.get(suffix, 1) + hz = float(number_part) * multiplier + if hz <= 0: + raise argparse.ArgumentTypeError(f"Sampling rate must be positive: {rate_str}") + + interval_usec = int(MICROSECONDS_PER_SECOND / hz) + if interval_usec < 1: + raise argparse.ArgumentTypeError(f"Sampling rate too high: {rate_str}") + + return interval_usec + + +def _add_sampling_options(parser): + """Add sampling configuration options to a parser.""" + sampling_group = parser.add_argument_group("Sampling configuration") + sampling_group.add_argument( + "-r", + "--sampling-rate", + type=_parse_sampling_rate, + default="1khz", + metavar="RATE", + dest="sample_interval_usec", + help="sampling rate (e.g., 10000, 10khz, 10k)", + ) + sampling_group.add_argument( + "-d", + "--duration", + type=int, + default=None, + metavar="SECONDS", + help="Sampling duration (default: run to completion)", + ) + sampling_group.add_argument( + "-a", + "--all-threads", + action="store_true", + help="Sample all threads in the process instead of just the main thread", + ) + sampling_group.add_argument( + "--realtime-stats", + action="store_true", + help="Print real-time sampling statistics (Hz, mean, min, max) during profiling", + ) + sampling_group.add_argument( + "--native", + action="store_true", + help='Include artificial `` frames to denote calls to non-Python code', + ) + sampling_group.add_argument( + "--no-gc", + action="store_false", + dest="gc", + help='Don\'t include artificial `` frames to denote active garbage collection', + ) + sampling_group.add_argument( + "--opcodes", + action="store_true", + help="Gather bytecode opcode information for instruction-level profiling " + "(shows which bytecode instructions are executing, including specializations).", + ) + sampling_group.add_argument( + "--async-aware", + action="store_true", + help="Enable async-aware profiling (uses task-based stack reconstruction)", + ) + sampling_group.add_argument( + "--subprocesses", + action="store_true", + help="Also profile subprocesses. Each subprocess gets its own profiler and output file.", + ) + sampling_group.add_argument( + "--blocking", + action="store_true", + help="Stop all threads in target process before sampling to get consistent snapshots. " + "Uses thread_suspend on macOS and ptrace on Linux. Adds overhead but ensures memory " + "reads are from a frozen state.", + ) + + +def _add_mode_options(parser): + """Add mode options to a parser.""" + mode_group = parser.add_argument_group("Mode options") + mode_group.add_argument( + "--mode", + choices=["wall", "cpu", "gil", "exception"], + default="wall", + help="Sampling mode: wall (all samples), cpu (only samples when thread is on CPU), " + "gil (only samples when thread holds the GIL), " + "exception (only samples when thread has an active exception). " + "Incompatible with `--async-aware`", + ) + mode_group.add_argument( + "--async-mode", + choices=["running", "all"], + default="running", + help='Async profiling mode: "running" (only running task) ' + 'or "all" (all tasks including waiting). Requires `--async-aware`', + ) + + +def _add_format_options(parser, include_compression=True, include_binary=True): + """Add output format options to a parser.""" + output_group = parser.add_argument_group("Output options") + format_group = output_group.add_mutually_exclusive_group() + format_group.add_argument( + "--pstats", + action="store_const", + const="pstats", + dest="format", + help="Generate pstats output (default)", + ) + format_group.add_argument( + "--collapsed", + action="store_const", + const="collapsed", + dest="format", + help="Generate collapsed stack traces for flamegraphs", + ) + format_group.add_argument( + "--flamegraph", + action="store_const", + const="flamegraph", + dest="format", + help="Generate interactive HTML flamegraph visualization", + ) + format_group.add_argument( + "--gecko", + action="store_const", + const="gecko", + dest="format", + help="Generate Gecko format for Firefox Profiler", + ) + format_group.add_argument( + "--heatmap", + action="store_const", + const="heatmap", + dest="format", + help="Generate interactive HTML heatmap visualization with line-level sample counts", + ) + format_group.add_argument( + "--diff-flamegraph", + metavar="BASELINE", + action=DiffFlamegraphAction, + help="Generate differential flamegraph comparing current profile to `BASELINE` binary file", + ) + format_group.add_argument( + "--jsonl", + action="store_const", + const="jsonl", + dest="format", + help="Generate newline-delimited JSON (JSONL) for programmatic consumers", + ) + if include_binary: + format_group.add_argument( + "--binary", + action="store_const", + const="binary", + dest="format", + help="Generate high-performance binary format (use `replay` command to convert)", + ) + parser.set_defaults(format="pstats", diff_baseline=None) + + if include_compression: + output_group.add_argument( + "--compression", + choices=["auto", "zstd", "none"], + default="auto", + help="Compression for binary format: auto (use zstd if available), zstd, none", + ) + + output_group.add_argument( + "-o", + "--output", + dest="outfile", + help="Output path (default: `stdout` for `pstats` text; with `-o`, `pstats` is binary). " + "Auto-generated for other formats. For heatmap: directory name (default: `heatmap_PID`)", + ) + output_group.add_argument( + "--browser", + action="store_true", + help=( + "Automatically open HTML output " + f"({', '.join('--' + f.replace('_', '-') for f in BROWSER_COMPATIBLE_FORMATS)}) " + "in browser. " + "When using `--subprocesses`, only the main process opens the browser" + ), + ) + + +def _add_pstats_options(parser): + """Add pstats-specific display options to a parser.""" + pstats_group = parser.add_argument_group("pstats format options") + pstats_group.add_argument( + "--sort", + choices=[ + "nsamples", + "tottime", + "cumtime", + "sample-pct", + "cumul-pct", + "nsamples-cumul", + "name", + ], + default=None, + help="Sort order for pstats output (default: nsamples)", + ) + pstats_group.add_argument( + "-l", + "--limit", + type=int, + default=None, + help="Limit the number of rows in the output (default: 15)", + ) + pstats_group.add_argument( + "--no-summary", + action="store_true", + help="Disable the summary section in the pstats output", + ) + + +def _add_dump_options(parser): + """Add one-shot stack dump options to a parser.""" + dump_group = parser.add_argument_group("Dump options") + dump_group.add_argument( + "-a", + "--all-threads", + action="store_true", + help="Dump all threads in the process instead of just the main thread", + ) + dump_group.add_argument( + "--native", + action="store_true", + help='Include artificial `` frames to denote calls to non-Python code', + ) + dump_group.add_argument( + "--no-gc", + action="store_false", + dest="gc", + help='Don\'t include artificial `` frames to denote active garbage collection', + ) + dump_group.add_argument( + "--opcodes", + action="store_true", + help="Show bytecode opcode names when available.", + ) + dump_group.add_argument( + "--async-aware", + action="store_true", + help="Enable async-aware stack reconstruction", + ) + dump_group.add_argument( + "--async-mode", + choices=["running", "all"], + default=argparse.SUPPRESS, + help='Async stack mode: "running" (only running task) ' + 'or "all" (all tasks including waiting, default for dump). ' + "Requires `--async-aware`", + ) + dump_group.add_argument( + "--blocking", + action="store_true", + help="Stop all threads in target process before dumping the stack.", + ) + + +def _sort_to_mode(sort_choice): + """Convert sort choice string to SORT_MODE constant.""" + sort_map = { + "nsamples": SORT_MODE_NSAMPLES, + "tottime": SORT_MODE_TOTTIME, + "cumtime": SORT_MODE_CUMTIME, + "sample-pct": SORT_MODE_SAMPLE_PCT, + "cumul-pct": SORT_MODE_CUMUL_PCT, + "nsamples-cumul": SORT_MODE_NSAMPLES_CUMUL, + "name": -1, + } + return sort_map.get(sort_choice, SORT_MODE_NSAMPLES) + + +def _capture_config_from_args(args): + return { + "all_threads": args.all_threads, + "native": args.native, + "gc": args.gc, + "opcodes": args.opcodes, + "blocking": args.blocking, + } + + +def _create_collector(format_type, sample_interval_usec, skip_idle, opcodes=False, + mode=None, output_file=None, compression='auto', + diff_baseline=None, capture_config=None): + """Create the appropriate collector based on format type. + + Args: + format_type: The output format ('pstats', 'collapsed', 'flamegraph', + 'gecko', 'heatmap', 'jsonl', 'binary', 'diff_flamegraph') + sample_interval_usec: Sampling interval in microseconds + skip_idle: Whether to skip idle samples + opcodes: Whether to collect opcode information (only used by gecko format + for creating interval markers in Firefox Profiler) + mode: Profiling mode for collectors that expose it in metadata + output_file: Output file path (required for binary format) + compression: Compression type for binary format ('auto', 'zstd', 'none') + diff_baseline: Path to baseline binary file for differential flamegraph + capture_config: Capture feature mapping for binary profiles and diffs + + Returns: + A collector instance of the appropriate type + """ + collector_class = COLLECTOR_MAP.get(format_type) + if collector_class is None: + raise ValueError(f"Unknown format: {format_type}") + + if format_type == "diff_flamegraph": + if diff_baseline is None: + raise ValueError("Differential flamegraph requires a baseline file") + if not os.path.exists(diff_baseline): + raise ValueError(f"Baseline file not found: {diff_baseline}") + return collector_class( + sample_interval_usec, + baseline_binary_path=diff_baseline, + skip_idle=skip_idle, + mode=mode, + capture_config=capture_config, + ) + + # Binary format requires output file and compression + if format_type == "binary": + if output_file is None: + raise ValueError("Binary format requires an output file") + return collector_class(output_file, sample_interval_usec, skip_idle=skip_idle, + compression=compression, mode=mode, + capture_config=capture_config) + + # Gecko format never skips idle (it needs both GIL and CPU data) + # and is the only format that uses opcodes for interval markers + if format_type == "gecko": + skip_idle = False + return collector_class(sample_interval_usec, skip_idle=skip_idle, opcodes=opcodes) + + if format_type == "jsonl": + return collector_class( + sample_interval_usec, skip_idle=skip_idle, mode=mode + ) + + return collector_class(sample_interval_usec, skip_idle=skip_idle) + + +def _generate_output_filename(format_type, pid): + """Generate output filename based on format and PID. + + Args: + format_type: The output format + pid: Process ID + + Returns: + Generated filename + """ + extension = FORMAT_EXTENSIONS.get(format_type, "txt") + # For heatmap, use cleaner directory name without extension + if format_type == "heatmap": + return f"heatmap_{pid}" + return f"{format_type}_{pid}.{extension}" + + +def _open_in_browser(path): + """Open a file or directory in the default web browser. + + Args: + path: File path or directory path to open + + For directories (heatmap), opens the index.html file inside. + """ + abs_path = os.path.abspath(path) + + # For heatmap directories, open the index.html file + if os.path.isdir(abs_path): + index_path = os.path.join(abs_path, 'index.html') + if os.path.exists(index_path): + abs_path = index_path + else: + print(f"Warning: Could not find index.html in {path}", file=sys.stderr) + return + + file_url = f"file://{abs_path}" + try: + webbrowser.open(file_url) + except Exception as e: + print(f"Warning: Could not open browser: {e}", file=sys.stderr) + + +def _validate_replay_input_file(filename): + """Validate that the replay input looks like a sampling binary profile.""" + try: + with open(filename, "rb") as file: + header = file.read(_BINARY_PROFILE_HEADER_SIZE) + except OSError as exc: + sys.exit(f"Error: Could not read input file {filename}: {exc}") + + if ( + len(header) < _BINARY_PROFILE_HEADER_SIZE + or header[:4] not in _BINARY_PROFILE_MAGICS + ): + sys.exit( + "Error: Input file is not a binary sampling profile. " + "The replay command only accepts files created with --binary" + ) + + +def _replay_with_reader(args, reader): + """Replay samples from an open binary reader.""" + info = reader.get_info() + interval = info['sample_interval_us'] + + print(f"Replaying {info['sample_count']} samples from {args.input_file}") + print(f" Sample interval: {interval} us") + print( + " Compression: " + f"{'zstd' if info.get('compression_type', 0) == 1 else 'none'}" + ) + + collector = _create_collector( + args.format, interval, skip_idle=False, + mode=info.get("mode"), + diff_baseline=args.diff_baseline, + capture_config=info.get("capture_config"), + ) + + def progress_callback(current, total): + if total > 0: + pct = current / total + bar_width = 40 + filled = int(bar_width * pct) + bar = '█' * filled + '░' * (bar_width - filled) + print( + f"\r [{bar}] {pct*100:5.1f}% ({current:,}/{total:,})", + end="", + flush=True, + ) + + count = reader.replay_samples(collector, progress_callback) + if hasattr(collector, "set_replay_stats"): + collector.set_replay_stats(info) + if hasattr(collector, "set_mode"): + collector.set_mode(info.get("mode")) + print() + + if args.format == "pstats": + if args.outfile: + collector.export(args.outfile) + else: + sort_choice = ( + args.sort if args.sort is not None else "nsamples" + ) + limit = args.limit if args.limit is not None else 15 + sort_mode = _sort_to_mode(sort_choice) + collector.print_stats( + sort_mode, limit, not args.no_summary, + info.get("mode") if info.get("mode") is not None + else PROFILING_MODE_WALL + ) + else: + filename = ( + args.outfile + or _generate_output_filename(args.format, os.getpid()) + ) + export_ok = collector.export(filename) + + # Auto-open browser for HTML output if --browser flag is set + if ( + export_ok + and args.format in BROWSER_COMPATIBLE_FORMATS + and getattr(args, 'browser', False) + ): + _open_in_browser(filename) + + print(f"Replayed {count} samples") + + +def _handle_output(collector, args, pid, mode): + """Handle output for the collector based on format and arguments. + + Args: + collector: The collector instance with profiling data + args: Parsed command-line arguments + pid: Process ID (for generating filenames) + mode: Profiling mode used + """ + if args.format == "binary": + # Binary format already wrote to file incrementally, just finalize + collector.export(None) + filename = collector.filename + print(f"Binary profile written to {filename} ({collector.total_samples} samples)") + elif args.format == "pstats": + if args.outfile: + # If outfile is a directory, generate filename inside it + if os.path.isdir(args.outfile): + filename = os.path.join(args.outfile, _generate_output_filename(args.format, pid)) + collector.export(filename) + else: + collector.export(args.outfile) + else: + # Print to stdout with defaults applied + sort_choice = args.sort if args.sort is not None else "nsamples" + limit = args.limit if args.limit is not None else 15 + sort_mode = _sort_to_mode(sort_choice) + collector.print_stats( + sort_mode, limit, not args.no_summary, mode + ) + else: + # Export to file + if args.outfile and os.path.isdir(args.outfile): + # If outfile is a directory, generate filename inside it + filename = os.path.join(args.outfile, _generate_output_filename(args.format, pid)) + else: + filename = args.outfile or _generate_output_filename(args.format, pid) + export_ok = collector.export(filename) + + # Auto-open browser for HTML output if --browser flag is set + if ( + export_ok + and args.format in BROWSER_COMPATIBLE_FORMATS + and getattr(args, 'browser', False) + ): + _open_in_browser(filename) + + +def _validate_args(args, parser): + """Validate format-specific options and live mode requirements. + + Args: + args: Parsed command-line arguments + parser: ArgumentParser instance for error reporting + """ + command = getattr(args, 'command', None) + + if command == "replay": + return + + # Check if live mode is available + if hasattr(args, 'live') and args.live and LiveStatsCollector is None: + parser.error( + "Live mode requires the curses module, which is not available." + ) + + # --subprocesses is incompatible with --live + if hasattr(args, 'subprocesses') and args.subprocesses: + if ChildProcessMonitor is None: + parser.error( + "--subprocesses is not available on this platform " + "(requires _remote_debugging module)." + ) + if hasattr(args, 'live') and args.live: + parser.error("--subprocesses is incompatible with --live mode.") + + # Async-aware mode is incompatible with options that need thread data. + if getattr(args, 'async_aware', False): + issues = [] + if getattr(args, 'native', False): + issues.append("--native") + if not getattr(args, 'gc', True): + issues.append("--no-gc") + if getattr(args, 'format', None) == "binary": + issues.append("--binary") + if hasattr(args, 'mode') and args.mode != "wall": + issues.append(f"--mode={args.mode}") + if hasattr(args, 'all_threads') and args.all_threads: + issues.append("--all-threads") + if issues: + parser.error( + f"Options {', '.join(issues)} are incompatible with --async-aware. " + "Async-aware profiling uses task-based stack reconstruction." + ) + + # --async-mode requires --async-aware when explicitly set + if not getattr(args, 'async_aware', False): + if command == "dump": + # dump uses SUPPRESS default, so attr only exists if user passed it + if hasattr(args, 'async_mode'): + parser.error("--async-mode requires --async-aware to be enabled.") + elif hasattr(args, 'async_mode') and args.async_mode != "running": + parser.error("--async-mode requires --async-aware to be enabled.") + + if command == "dump": + return + + # Warn about blocking mode with aggressive sampling intervals + if args.blocking and args.sample_interval_usec < 100: + print( + f"Warning: --blocking with a {args.sample_interval_usec} µs interval will stop all threads " + f"{1_000_000 // args.sample_interval_usec} times per second. " + "Consider using --sampling-rate 1khz or lower to reduce overhead.", + file=sys.stderr + ) + + # Live mode is incompatible with format options + if hasattr(args, 'live') and args.live: + if args.format != "pstats": + format_flag = f"--{args.format}" + parser.error( + f"--live is incompatible with {format_flag}. Live mode uses a TUI interface." + ) + + # Live mode is also incompatible with pstats-specific options + issues = [] + if args.sort is not None: + issues.append("--sort") + if args.limit is not None: + issues.append("--limit") + if args.no_summary: + issues.append("--no-summary") + + if issues: + parser.error( + f"Options {', '.join(issues)} are incompatible with --live. " + "Live mode uses a TUI interface with its own controls." + ) + return + + # Validate gecko mode doesn't use non-wall mode + if args.format == "gecko" and getattr(args, 'mode', 'wall') != "wall": + parser.error( + "--mode option is incompatible with --gecko. " + "Gecko format automatically includes both GIL-holding and CPU status analysis." + ) + + # Validate --opcodes is only used with compatible formats + opcodes_compatible_formats = ("live", "gecko", "flamegraph", "diff_flamegraph", "heatmap", "binary") + if getattr(args, 'opcodes', False) and args.format not in opcodes_compatible_formats: + parser.error( + f"--opcodes is only compatible with {', '.join('--' + f for f in opcodes_compatible_formats)}." + ) + + # Validate pstats-specific options are only used with pstats format + if args.format != "pstats": + issues = [] + if args.sort is not None: + issues.append("--sort") + if args.limit is not None: + issues.append("--limit") + if args.no_summary: + issues.append("--no-summary") + + if issues: + format_flag = f"--{args.format}" + parser.error( + f"Options {', '.join(issues)} are only valid with --pstats, not {format_flag}" + ) + + +def main(): + """Main entry point for the CLI.""" + # Set locale for number formatting, restore on exit + old_locale = locale.setlocale(locale.LC_ALL, None) + locale.setlocale(locale.LC_ALL, "") + try: + _main() + finally: + locale.setlocale(locale.LC_ALL, old_locale) + + +def _main(): + # Create the main parser + parser = argparse.ArgumentParser( + description=_HELP_DESCRIPTION, + formatter_class=CustomFormatter, + ) + + # Create subparsers for commands + subparsers = parser.add_subparsers( + dest="command", required=True, help="Command to run" + ) + + # === RUN COMMAND === + run_parser = subparsers.add_parser( + "run", + help="Run and profile a script or module", + formatter_class=CustomFormatter, + description="""Run and profile a Python script or module + +Examples: + # Run and profile a module + `python -m profiling.sampling run -m mymodule arg1 arg2` + + # Generate flamegraph from a script + `python -m profiling.sampling run --flamegraph -o output.html script.py` + + # Profile with custom rate and duration + `python -m profiling.sampling run -r 5khz -d 30 script.py` + + # Save collapsed stacks to file + `python -m profiling.sampling run --collapsed -o stacks.txt script.py` + + # Live interactive mode for a script + `python -m profiling.sampling run --live script.py`""", + ) + run_parser.add_argument( + "-m", + "--module", + action="store_true", + help="Run target as a module (like `python -m`)", + ) + run_parser.add_argument( + "target", + help="Script file or module name to profile", + ) + run_parser.add_argument( + "args", + nargs=argparse.REMAINDER, + help="Arguments to pass to the script or module", + ) + run_parser.add_argument( + "--live", + action="store_true", + help="Interactive TUI profiler (top-like interface, press 'q' to quit, 's' to cycle sort)", + ) + _add_sampling_options(run_parser) + _add_mode_options(run_parser) + _add_format_options(run_parser) + _add_pstats_options(run_parser) + + # === ATTACH COMMAND === + attach_parser = subparsers.add_parser( + "attach", + help="Attach to and profile a running process", + formatter_class=CustomFormatter, + description="""Attach to a running process and profile it + +Examples: + # Profile all threads, sort by total time + `python -m profiling.sampling attach -a --sort tottime 1234` + + # Live interactive mode for a running process + `python -m profiling.sampling attach --live 1234`""", + ) + attach_parser.add_argument( + "pid", + type=int, + help="Process ID to attach to", + ) + attach_parser.add_argument( + "--live", + action="store_true", + help="Interactive TUI profiler (top-like interface, press 'q' to quit, 's' to cycle sort)", + ) + _add_sampling_options(attach_parser) + _add_mode_options(attach_parser) + _add_format_options(attach_parser) + _add_pstats_options(attach_parser) + + # === DUMP COMMAND === + dump_parser = subparsers.add_parser( + "dump", + help="Dump a running process's current stack", + formatter_class=CustomFormatter, + description="""Dump a running process's current Python stack + +Examples: + # Dump the main thread stack + `python -m profiling.sampling dump 1234` + + # Dump all thread stacks + `python -m profiling.sampling dump -a 1234`""", + ) + dump_parser.add_argument( + "pid", + type=int, + help="Process ID to dump", + ) + _add_dump_options(dump_parser) + + # === REPLAY COMMAND === + replay_parser = subparsers.add_parser( + "replay", + help="Replay a binary profile and convert to another format", + formatter_class=CustomFormatter, + description="""Replay a binary profile file and convert to another format + +Examples: + # Convert binary to flamegraph + `python -m profiling.sampling replay --flamegraph -o output.html profile.bin` + + # Convert binary to pstats and print to stdout + `python -m profiling.sampling replay profile.bin` + + # Convert binary to gecko format + `python -m profiling.sampling replay --gecko -o profile.json profile.bin`""", + ) + replay_parser.add_argument( + "input_file", + help="Binary profile file to replay", + ) + _add_format_options(replay_parser, include_compression=False, include_binary=False) + _add_pstats_options(replay_parser) + + # Parse arguments + args = parser.parse_args() + + # Validate arguments + _validate_args(args, parser) + + # Command dispatch table + command_handlers = { + "run": _handle_run, + "attach": _handle_attach, + "dump": _handle_dump, + "replay": _handle_replay, + } + + # Execute the appropriate command + handler = command_handlers.get(args.command) + if handler: + handler(args) + else: + parser.error(f"Unknown command: {args.command}") + + +def _handle_attach(args): + """Handle the 'attach' command.""" + if not _is_process_running(args.pid): + raise SamplingUnknownProcessError(args.pid) + # Check if live mode is requested + if args.live: + _handle_live_attach(args, args.pid) + return + + # Use PROFILING_MODE_ALL for gecko format + mode = ( + PROFILING_MODE_ALL + if args.format == "gecko" + else _parse_mode(args.mode) + ) + + # Determine skip_idle based on mode + skip_idle = ( + mode != PROFILING_MODE_WALL if mode != PROFILING_MODE_ALL else False + ) + + output_file = None + if args.format == "binary": + output_file = args.outfile or _generate_output_filename(args.format, args.pid) + + # Create the appropriate collector + collector = _create_collector( + args.format, args.sample_interval_usec, skip_idle, args.opcodes, mode, + output_file=output_file, + compression=getattr(args, 'compression', 'auto'), + diff_baseline=args.diff_baseline, + capture_config=_capture_config_from_args(args), + ) + + with _get_child_monitor_context(args, args.pid): + collector = sample( + args.pid, + collector, + duration_sec=args.duration, + all_threads=args.all_threads, + realtime_stats=args.realtime_stats, + mode=mode, + async_aware=args.async_mode if args.async_aware else None, + native=args.native, + gc=args.gc, + opcodes=args.opcodes, + blocking=args.blocking, + ) + _handle_output(collector, args, args.pid, mode) + + +def _handle_dump(args): + if not _is_process_running(args.pid): + raise SamplingUnknownProcessError(args.pid) + + # Async-aware reconstruction requires wall mode so every thread is sampled, + # not just those holding the GIL or on CPU. + mode = PROFILING_MODE_WALL if args.async_aware else PROFILING_MODE_ALL + async_mode = getattr(args, "async_mode", "all") if args.async_aware else None + try: + stack_frames = dump_stack( + args.pid, + all_threads=args.all_threads, + mode=mode, + async_aware=async_mode, + native=args.native, + gc=args.gc, + opcodes=args.opcodes, + blocking=args.blocking, + ) + except ProcessLookupError: + sys.exit( + f"No stack dump collected - process {args.pid} exited before " + "its stack could be read." + ) + + print_stack_dump(stack_frames, pid=args.pid) + + +def _handle_run(args): + """Handle the 'run' command.""" + # Validate target exists before launching subprocess + if args.module: + # Temporarily add cwd to sys.path so we can find modules in the + # current directory, matching the coordinator's behavior + cwd = os.getcwd() + added_cwd = False + if cwd not in sys.path: + sys.path.insert(0, cwd) + added_cwd = True + try: + if importlib.util.find_spec(args.target) is None: + raise SamplingModuleNotFoundError(args.target) + finally: + if added_cwd: + sys.path.remove(cwd) + else: + if not os.path.exists(args.target): + raise SamplingScriptNotFoundError(args.target) + + # Check if live mode is requested + if args.live: + _handle_live_run(args) + return + + # Build the command to run + if args.module: + cmd = (sys.executable, "-m", args.target, *args.args) + else: + cmd = (sys.executable, args.target, *args.args) + + # Run with synchronization + try: + process = _run_with_sync(cmd, suppress_output=False) + except RuntimeError as e: + sys.exit(f"Error: {e}") + + # Use PROFILING_MODE_ALL for gecko format + mode = ( + PROFILING_MODE_ALL + if args.format == "gecko" + else _parse_mode(args.mode) + ) + + # Determine skip_idle based on mode + skip_idle = ( + mode != PROFILING_MODE_WALL if mode != PROFILING_MODE_ALL else False + ) + + output_file = None + if args.format == "binary": + output_file = args.outfile or _generate_output_filename(args.format, process.pid) + + # Create the appropriate collector + collector = _create_collector( + args.format, args.sample_interval_usec, skip_idle, args.opcodes, mode, + output_file=output_file, + compression=getattr(args, 'compression', 'auto'), + diff_baseline=args.diff_baseline, + capture_config=_capture_config_from_args(args), + ) + + with _get_child_monitor_context(args, process.pid): + try: + collector = sample( + process.pid, + collector, + duration_sec=args.duration, + all_threads=args.all_threads, + realtime_stats=args.realtime_stats, + mode=mode, + async_aware=args.async_mode if args.async_aware else None, + native=args.native, + gc=args.gc, + opcodes=args.opcodes, + blocking=args.blocking, + ) + _handle_output(collector, args, process.pid, mode) + finally: + # Terminate the main subprocess - child profilers finish when their + # target processes exit + if process.poll() is None: + process.terminate() + try: + process.wait(timeout=_PROCESS_KILL_TIMEOUT_SEC) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + + +def _handle_live_attach(args, pid): + """Handle live mode for an existing process.""" + mode = _parse_mode(args.mode) + + # Determine skip_idle based on mode + skip_idle = mode != PROFILING_MODE_WALL + + # Create live collector with default settings + collector = LiveStatsCollector( + args.sample_interval_usec, + skip_idle=skip_idle, + sort_by="tottime", # Default initial sort + limit=20, # Default limit + pid=pid, + mode=mode, + opcodes=args.opcodes, + async_aware=args.async_mode if args.async_aware else None, + ) + + # Sample in live mode + sample_live( + pid, + collector, + duration_sec=args.duration, + all_threads=args.all_threads, + realtime_stats=args.realtime_stats, + mode=mode, + async_aware=args.async_mode if args.async_aware else None, + native=args.native, + gc=args.gc, + opcodes=args.opcodes, + blocking=args.blocking, + ) + + +def _handle_live_run(args): + """Handle live mode for running a script/module.""" + # Build the command to run + if args.module: + cmd = (sys.executable, "-m", args.target, *args.args) + else: + cmd = (sys.executable, args.target, *args.args) + + # Run with synchronization, suppressing output for live mode + try: + process = _run_with_sync(cmd, suppress_output=True) + except RuntimeError as e: + sys.exit(f"Error: {e}") + + mode = _parse_mode(args.mode) + + # Determine skip_idle based on mode + skip_idle = mode != PROFILING_MODE_WALL + + # Create live collector with default settings + collector = LiveStatsCollector( + args.sample_interval_usec, + skip_idle=skip_idle, + sort_by="tottime", # Default initial sort + limit=20, # Default limit + pid=process.pid, + mode=mode, + opcodes=args.opcodes, + async_aware=args.async_mode if args.async_aware else None, + ) + + # Profile the subprocess in live mode + try: + sample_live( + process.pid, + collector, + duration_sec=args.duration, + all_threads=args.all_threads, + realtime_stats=args.realtime_stats, + mode=mode, + async_aware=args.async_mode if args.async_aware else None, + native=args.native, + gc=args.gc, + opcodes=args.opcodes, + blocking=args.blocking, + ) + finally: + # Clean up the subprocess and get any error output + returncode = process.poll() + if returncode is None: + # Process still running - terminate it + process.terminate() + try: + process.wait(timeout=_PROCESS_KILL_TIMEOUT_SEC) + except subprocess.TimeoutExpired: + process.kill() + # Ensure process is fully terminated + process.wait() + # Read any stderr output (tracebacks, errors, etc.) + if process.stderr: + with process.stderr: + try: + stderr = process.stderr.read() + if stderr: + print(stderr.decode(), file=sys.stderr) + except (OSError, ValueError): + # Ignore errors if pipe is already closed + pass + + +def _handle_replay(args): + """Handle the 'replay' command - convert binary profile to another format.""" + if not os.path.exists(args.input_file): + sys.exit(f"Error: Input file not found: {args.input_file}") + + _validate_replay_input_file(args.input_file) + + try: + with BinaryReader(args.input_file) as reader: + _replay_with_reader(args, reader) + except (OSError, ValueError) as exc: + sys.exit(f"Error: {exc}") + + +if __name__ == "__main__": + main() diff --git a/Lib/profiling/sampling/collector.py b/Lib/profiling/sampling/collector.py new file mode 100644 index 00000000000..1dc3656e0eb --- /dev/null +++ b/Lib/profiling/sampling/collector.py @@ -0,0 +1,292 @@ +from abc import ABC, abstractmethod +from .constants import ( + DEFAULT_LOCATION, + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_ON_CPU, + THREAD_STATUS_GIL_REQUESTED, + THREAD_STATUS_UNKNOWN, + THREAD_STATUS_HAS_EXCEPTION, + _INTERNAL_FRAME_SUFFIXES, +) + +try: + from _remote_debugging import FrameInfo +except ImportError: + # Fallback definition if _remote_debugging is not available + FrameInfo = None + + +def normalize_location(location): + """Normalize location to a 4-tuple format. + + Args: + location: tuple (lineno, end_lineno, col_offset, end_col_offset), + an integer line number, or None + + Returns: + tuple: (lineno, end_lineno, col_offset, end_col_offset) + """ + if location is None: + return DEFAULT_LOCATION + if isinstance(location, int): + return (location, location, -1, -1) + return location + + +def extract_lineno(location): + """Extract lineno from location. + + Args: + location: tuple (lineno, end_lineno, col_offset, end_col_offset), + an integer line number, or None + + Returns: + int: The line number (0 for synthetic frames) + """ + if location is None: + return 0 + if isinstance(location, int): + return location + return location[0] + +def _is_internal_frame(frame): + if isinstance(frame, tuple): + filename = frame[0] if frame else "" + else: + filename = getattr(frame, "filename", "") + + if not filename: + return False + + return filename.endswith(_INTERNAL_FRAME_SUFFIXES) + + +def filter_internal_frames(frames): + if not frames: + return frames + + return [f for f in frames if not _is_internal_frame(f)] + + +def _build_task_graph(awaited_info_list): + task_map = {} + child_to_parent = {} # child_id -> (selected_parent_id, parent_count) + all_task_ids = set() + all_parent_ids = set() + + for awaited_info in awaited_info_list: + thread_id = awaited_info.thread_id + for task_info in awaited_info.awaited_by: + task_id = task_info.task_id + task_map[task_id] = (task_info, thread_id) + all_task_ids.add(task_id) + + if task_info.awaited_by: + parent_ids = [p.task_name for p in task_info.awaited_by] + parent_count = len(parent_ids) + all_parent_ids.update(parent_ids) + selected_parent = min(parent_ids) if parent_count > 1 else parent_ids[0] + child_to_parent[task_id] = (selected_parent, parent_count) + + return task_map, child_to_parent, all_task_ids, all_parent_ids + + +def _find_leaf_tasks(all_task_ids, all_parent_ids): + return all_task_ids - all_parent_ids + + +def _build_linear_stacks(leaf_task_ids, task_map, child_to_parent): + for leaf_id in leaf_task_ids: + frames = [] + visited = set() + current_id = leaf_id + thread_id = None + + while current_id is not None: + if current_id in visited: + break + visited.add(current_id) + + if current_id not in task_map: + break + + task_info, tid = task_map[current_id] + + if thread_id is None: + thread_id = tid + + if task_info.coroutine_stack: + for coro_info in task_info.coroutine_stack: + for frame in coro_info.call_stack: + frames.append(frame) + + parent_info = child_to_parent.get(current_id) + task_name = task_info.task_name or "Task-" + str(task_info.task_id) + if parent_info: + selected_parent, parent_count = parent_info + if parent_count > 1: + task_name = f"{task_name} ({parent_count} parents)" + frames.append(FrameInfo(("", None, task_name, None))) + current_id = selected_parent + else: + frames.append(FrameInfo(("", None, task_name, None))) + current_id = None + + if frames and thread_id is not None: + yield frames, thread_id, leaf_id + + +def iter_async_frames(awaited_info_list): + task_map, child_to_parent, all_task_ids, all_parent_ids = _build_task_graph(awaited_info_list) + leaf_task_ids = _find_leaf_tasks(all_task_ids, all_parent_ids) + yield from _build_linear_stacks(leaf_task_ids, task_map, child_to_parent) + + +class Collector(ABC): + aggregating = False + + @abstractmethod + def collect(self, stack_frames, timestamps_us=None): + """Collect profiling data from stack frames. + + Args: + stack_frames: List of InterpreterInfo objects + timestamps_us: Optional list of timestamps in microseconds. If provided + (from binary replay with RLE batching), use these instead of current + time. If None, collectors should use time.monotonic() or similar. + The list may contain multiple timestamps when samples are batched + together (same stack, different times). + """ + + def collect_failed_sample(self): + """Collect data about a failed sample attempt.""" + + @abstractmethod + def export(self, filename): + """Export collected data. + + Returns: + bool: True if output was generated, False if there was no data to export. + """ + + @staticmethod + def _filter_internal_frames(frames): + return filter_internal_frames(frames) + + def _iter_all_frames(self, stack_frames, skip_idle=False): + for interpreter_info in stack_frames: + for thread_info in interpreter_info.threads: + # skip_idle now means: skip if thread is not actively running + # A thread is "active" if it has the GIL OR is on CPU + if skip_idle: + status_flags = thread_info.status + has_gil = bool(status_flags & THREAD_STATUS_HAS_GIL) + on_cpu = bool(status_flags & THREAD_STATUS_ON_CPU) + if not (has_gil or on_cpu): + continue + frames = thread_info.frame_info + if frames: + # Filter out internal profiler frames from the bottom of the stack + frames = self._filter_internal_frames(frames) + if frames: + yield frames, thread_info.thread_id + + def _iter_async_frames(self, awaited_info_list): + yield from iter_async_frames(awaited_info_list) + + def _iter_stacks(self, stack_frames, skip_idle=False): + """Yield (frames, thread_id) for all stacks, handling both sync and async modes.""" + if stack_frames and hasattr(stack_frames[0], "awaited_by"): + for frames, thread_id, _ in iter_async_frames(stack_frames): + if frames: + yield frames, thread_id + else: + for frames, thread_id in self._iter_all_frames(stack_frames, skip_idle=skip_idle): + if frames: + yield frames, thread_id + + def _is_gc_frame(self, frame): + if isinstance(frame, tuple): + funcname = frame[2] if len(frame) >= 3 else "" + else: + funcname = getattr(frame, "funcname", "") + + return "" in funcname or "gc_collect" in funcname + + def _collect_thread_status_stats(self, stack_frames): + """Collect aggregate and per-thread status statistics from a sample. + + Returns: + tuple: (aggregate_status_counts, has_gc_frame, per_thread_stats) + - aggregate_status_counts: dict with has_gil, on_cpu, has_exception, etc. + - has_gc_frame: bool indicating if any thread has GC frames + - per_thread_stats: dict mapping thread_id to per-thread counts + """ + status_counts = { + "has_gil": 0, + "on_cpu": 0, + "gil_requested": 0, + "unknown": 0, + "has_exception": 0, + "total": 0, + } + has_gc_frame = False + per_thread_stats = {} + + for interpreter_info in stack_frames: + threads = getattr(interpreter_info, "threads", []) + for thread_info in threads: + status_counts["total"] += 1 + + # Track thread status using bit flags + status_flags = getattr(thread_info, "status", 0) + + if status_flags & THREAD_STATUS_HAS_GIL: + status_counts["has_gil"] += 1 + if status_flags & THREAD_STATUS_ON_CPU: + status_counts["on_cpu"] += 1 + if status_flags & THREAD_STATUS_GIL_REQUESTED: + status_counts["gil_requested"] += 1 + if status_flags & THREAD_STATUS_UNKNOWN: + status_counts["unknown"] += 1 + if status_flags & THREAD_STATUS_HAS_EXCEPTION: + status_counts["has_exception"] += 1 + + # Track per-thread statistics + thread_id = getattr(thread_info, "thread_id", None) + if thread_id is not None: + if thread_id not in per_thread_stats: + per_thread_stats[thread_id] = { + "has_gil": 0, + "on_cpu": 0, + "gil_requested": 0, + "unknown": 0, + "has_exception": 0, + "total": 0, + "gc_samples": 0, + } + + thread_stats = per_thread_stats[thread_id] + thread_stats["total"] += 1 + + if status_flags & THREAD_STATUS_HAS_GIL: + thread_stats["has_gil"] += 1 + if status_flags & THREAD_STATUS_ON_CPU: + thread_stats["on_cpu"] += 1 + if status_flags & THREAD_STATUS_GIL_REQUESTED: + thread_stats["gil_requested"] += 1 + if status_flags & THREAD_STATUS_UNKNOWN: + thread_stats["unknown"] += 1 + if status_flags & THREAD_STATUS_HAS_EXCEPTION: + thread_stats["has_exception"] += 1 + + # Check for GC frames in this thread + frames = getattr(thread_info, "frame_info", None) + if frames: + for frame in frames: + if self._is_gc_frame(frame): + thread_stats["gc_samples"] += 1 + has_gc_frame = True + break + + return status_counts, has_gc_frame, per_thread_stats diff --git a/Lib/profiling/sampling/constants.py b/Lib/profiling/sampling/constants.py new file mode 100644 index 00000000000..d7c710f943b --- /dev/null +++ b/Lib/profiling/sampling/constants.py @@ -0,0 +1,57 @@ +"""Constants for the sampling profiler.""" + +# Time unit conversion constants +MICROSECONDS_PER_SECOND = 1_000_000 +MILLISECONDS_PER_SECOND = 1_000 + +# Profiling mode constants +PROFILING_MODE_WALL = 0 +PROFILING_MODE_CPU = 1 +PROFILING_MODE_GIL = 2 +PROFILING_MODE_ALL = 3 # Combines GIL + CPU checks +PROFILING_MODE_EXCEPTION = 4 # Only samples when thread has an active exception + +PROFILING_MODE_NAMES = { + PROFILING_MODE_WALL: "wall", + PROFILING_MODE_CPU: "cpu", + PROFILING_MODE_GIL: "gil", + PROFILING_MODE_ALL: "all", + PROFILING_MODE_EXCEPTION: "exception", +} + +# Sort mode constants +SORT_MODE_NSAMPLES = 0 +SORT_MODE_TOTTIME = 1 +SORT_MODE_CUMTIME = 2 +SORT_MODE_SAMPLE_PCT = 3 +SORT_MODE_CUMUL_PCT = 4 +SORT_MODE_NSAMPLES_CUMUL = 5 + +# Default location for synthetic frames (native, GC) that have no source location +# Format: (lineno, end_lineno, col_offset, end_col_offset) +DEFAULT_LOCATION = (0, 0, -1, -1) + +# Internal frame path suffixes to filter from profiling output +# These are internal profiler modules that should not appear in user-facing output +_INTERNAL_FRAME_SUFFIXES = ( + "_sync_coordinator.py", +) + +# Thread status flags +try: + from _remote_debugging import ( + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_ON_CPU, + THREAD_STATUS_UNKNOWN, + THREAD_STATUS_GIL_REQUESTED, + THREAD_STATUS_HAS_EXCEPTION, + THREAD_STATUS_MAIN_THREAD, + ) +except ImportError: + # Fallback for tests or when module is not available + THREAD_STATUS_HAS_GIL = (1 << 0) + THREAD_STATUS_ON_CPU = (1 << 1) + THREAD_STATUS_UNKNOWN = (1 << 2) + THREAD_STATUS_GIL_REQUESTED = (1 << 3) + THREAD_STATUS_HAS_EXCEPTION = (1 << 4) + THREAD_STATUS_MAIN_THREAD = (1 << 5) diff --git a/Lib/profiling/sampling/dump.py b/Lib/profiling/sampling/dump.py new file mode 100644 index 00000000000..7cf43bd29eb --- /dev/null +++ b/Lib/profiling/sampling/dump.py @@ -0,0 +1,295 @@ +"""Pretty printing for one-shot sampling stack dumps.""" + +import contextlib +import linecache +import os +import sys +from traceback import _byte_offset_to_character_offset + +import _colorize + +from .collector import extract_lineno, filter_internal_frames, iter_async_frames +from .constants import ( + THREAD_STATUS_GIL_REQUESTED, + THREAD_STATUS_HAS_EXCEPTION, + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_MAIN_THREAD, + THREAD_STATUS_ON_CPU, + THREAD_STATUS_UNKNOWN, +) +from .opcode_utils import format_opcode + + +_STATUS_LABELS = ( + (THREAD_STATUS_MAIN_THREAD, "main thread"), + (THREAD_STATUS_HAS_GIL, "has GIL"), + (THREAD_STATUS_ON_CPU, "on CPU"), + (THREAD_STATUS_GIL_REQUESTED, "waiting for GIL"), + (THREAD_STATUS_HAS_EXCEPTION, "has exception"), +) + + +def _theme_for(file, colorize): + if colorize is True: + return _colorize.get_theme(force_color=True).profiler_dump + if colorize is False: + return _colorize.get_theme(force_no_color=True).profiler_dump + return _colorize.get_theme(tty_file=file).profiler_dump + + +def _color(text, color, theme): + if not color: + return text + return f"{color}{text}{theme.reset}" + + +def _frame_fields(frame): + if isinstance(frame, tuple): + filename = frame[0] if len(frame) > 0 else "" + location = frame[1] if len(frame) > 1 else None + qualname = frame[2] if len(frame) > 2 else "" + opcode = frame[3] if len(frame) > 3 else None + else: + filename = getattr(frame, "filename", "") + location = getattr(frame, "location", None) + qualname = getattr(frame, "qualname", None) + if qualname is None: + qualname = getattr(frame, "funcname", "") + opcode = getattr(frame, "opcode", None) + return filename, location, qualname, opcode + + +def _location_field(location, index, default=None): + if location is None: + return default + try: + value = location[index] + except (IndexError, TypeError): + return default + return default if value is None else value + + +def _status_text(status): + labels = [label for flag, label in _STATUS_LABELS if status & flag] + has_state = status & ( + THREAD_STATUS_HAS_GIL + | THREAD_STATUS_GIL_REQUESTED + | THREAD_STATUS_HAS_EXCEPTION + ) + if not has_state and not status & (THREAD_STATUS_UNKNOWN | THREAD_STATUS_ON_CPU): + labels.append("idle") + return ", ".join(labels) if labels else None + + +def _is_async_dump(stack_frames): + return bool(stack_frames) and hasattr(stack_frames[0], "awaited_by") + + +def _iter_dump_sections(stack_frames): + if not stack_frames: + return + + if _is_async_dump(stack_frames): + for frames, thread_id, leaf_id in iter_async_frames(stack_frames): + frames = filter_internal_frames(frames) + if frames: + yield None, thread_id, None, frames, f"task {leaf_id}" + return + + for interpreter_info in stack_frames: + interpreter_id = getattr(interpreter_info, "interpreter_id", None) + for thread_info in getattr(interpreter_info, "threads", ()): + frames = getattr(thread_info, "frame_info", None) or [] + frames = filter_internal_frames(frames) + yield ( + interpreter_id, + getattr(thread_info, "thread_id", None), + getattr(thread_info, "status", None), + frames, + None, + ) + + +def _display_filename(filename): + if not filename or filename == "~": + return filename + with contextlib.suppress(ValueError): + relpath = os.path.relpath(filename) + if not relpath.startswith(".." + os.sep) and relpath != "..": + return relpath + return filename + + +def _format_frame(frame, theme): + filename, location, qualname, opcode = _frame_fields(frame) + source_filename = filename + lineno = extract_lineno(location) + qualname_part = _color(qualname, theme.frame, theme) + + if filename == "~" and lineno == 0: + line = f" {qualname_part}" + else: + filename = _display_filename(filename) + if filename: + file_part = _color(f'"{filename}"', theme.filename, theme) + if lineno > 0: + line_part = _color(str(lineno), theme.line_no, theme) + line = f" File {file_part}, line {line_part}, in {qualname_part}" + else: + line = f" File {file_part}, in {qualname_part}" + else: + line = f" {qualname_part}" + + if opcode is not None: + line = f"{line} {_color(f'opcode={format_opcode(opcode)}', theme.opcode, theme)}" + + lines = [line] + source = _source_line(source_filename, location, lineno, theme) + if source: + lines.append(f" {source}") + return lines + + +def _source_offsets(line, location, lineno): + end_lineno = _location_field(location, 1, lineno) + col_offset = _location_field(location, 2, -1) + end_col_offset = _location_field(location, 3, -1) + + if col_offset < 0 or end_col_offset < 0 or end_lineno < lineno: + return None + + start = _byte_offset_to_character_offset(line, col_offset) + if end_lineno == lineno: + end = _byte_offset_to_character_offset(line, end_col_offset) + else: + end = len(line) + if start < 0 or end <= start: + return None + return start, end + + +def _trim_source_line(line, offsets): + stripped = line.lstrip() + leading = len(line) - len(stripped) + if offsets is None: + return stripped, None + + start, end = offsets + start = max(start - leading, 0) + end = max(end - leading, start + 1) + end = min(end, len(stripped)) + return stripped, (start, end) + + +def _highlight_source_line(line, offsets, theme): + if offsets is None or offsets[1] <= offsets[0]: + return _color(line, theme.source, theme) + + start, end = offsets + parts = [] + if line[:start]: + parts.append(_color(line[:start], theme.source, theme)) + parts.append(_color(line[start:end], theme.source_highlight, theme)) + if line[end:]: + parts.append(_color(line[end:], theme.source, theme)) + return "".join(parts) + + +def _source_line(filename, location, lineno, theme): + if not filename or filename == "~" or lineno <= 0: + return None + line = linecache.getline(filename, lineno).removesuffix("\n") + if not line: + return None + + offsets = _source_offsets(line, location, lineno) + line, offsets = _trim_source_line(line, offsets) + if not line: + return None + return _highlight_source_line(line, offsets, theme) + + +def _section_header( + *, + pid, + interpreter_id, + thread_id, + status, + label, + show_pid, + show_interpreter, + theme, +): + subject = "Stack dump" + if show_pid and pid is not None: + subject = f"{subject} for PID {pid}" + if thread_id is not None: + subject = f"{subject}, thread {thread_id}" + + details = [] + if show_interpreter and interpreter_id is not None: + details.append(f"interpreter {interpreter_id}") + if label: + details.append(label) + if status is not None: + status_text = _status_text(status) + if status_text: + details.append(status_text) + + suffix = "most recent call last" + if details: + suffix = f"{'; '.join(details)}; {suffix}" + return _color(f"{subject} ({suffix}):", theme.header, theme) + + +def format_stack_dump(stack_frames, *, pid=None, file=None, colorize=None): + """Return a formatted one-shot stack dump.""" + if file is None: + file = sys.stdout + + theme = _theme_for(file, colorize) + lines = [] + sections = list(_iter_dump_sections(stack_frames)) + if not sections: + if pid is None: + return f"{_color('No Python stacks found', theme.warning, theme)}\n" + return f"{_color(f'No Python stacks found for PID {pid}', theme.warning, theme)}\n" + + interpreter_ids = { + interpreter_id + for interpreter_id, _thread_id, _status, _frames, _label in sections + if interpreter_id is not None + } + show_interpreter = len(interpreter_ids) > 1 + + for section_index, (interpreter_id, thread_id, status, frames, label) in enumerate(sections): + if section_index: + lines.append("") + lines.append( + _section_header( + pid=pid, + interpreter_id=interpreter_id, + thread_id=thread_id, + status=status, + label=label, + show_pid=section_index == 0, + show_interpreter=show_interpreter, + theme=theme, + ) + ) + + if not frames: + lines.append(_color("No Python frames", theme.warning, theme)) + continue + + for frame in reversed(frames): + lines.extend(_format_frame(frame, theme)) + + return "\n".join(lines) + "\n" + + +def print_stack_dump(stack_frames, *, pid=None, file=None, colorize=None): + """Pretty-print a one-shot stack dump.""" + if file is None: + file = sys.stdout + file.write(format_stack_dump(stack_frames, pid=pid, file=file, colorize=colorize)) diff --git a/Lib/profiling/sampling/errors.py b/Lib/profiling/sampling/errors.py new file mode 100644 index 00000000000..0832ad2d438 --- /dev/null +++ b/Lib/profiling/sampling/errors.py @@ -0,0 +1,19 @@ +"""Custom exceptions for the sampling profiler.""" + +class SamplingProfilerError(Exception): + """Base exception for sampling profiler errors.""" + +class SamplingUnknownProcessError(SamplingProfilerError): + def __init__(self, pid): + self.pid = pid + super().__init__(f"Process with PID '{pid}' does not exist.") + +class SamplingScriptNotFoundError(SamplingProfilerError): + def __init__(self, script_path): + self.script_path = script_path + super().__init__(f"Script '{script_path}' not found.") + +class SamplingModuleNotFoundError(SamplingProfilerError): + def __init__(self, module_name): + self.module_name = module_name + super().__init__(f"Module '{module_name}' not found.") diff --git a/Lib/profiling/sampling/gecko_collector.py b/Lib/profiling/sampling/gecko_collector.py new file mode 100644 index 00000000000..2de8cce387e --- /dev/null +++ b/Lib/profiling/sampling/gecko_collector.py @@ -0,0 +1,1006 @@ +import itertools +import io +import json +import os +import platform +import sys +import tempfile +import threading +import time + +from .collector import Collector, filter_internal_frames +from .opcode_utils import get_opcode_info, format_opcode +try: + from _remote_debugging import THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU, THREAD_STATUS_UNKNOWN, THREAD_STATUS_GIL_REQUESTED, THREAD_STATUS_HAS_EXCEPTION, THREAD_STATUS_MAIN_THREAD +except ImportError: + # Fallback if module not available (shouldn't happen in normal use) + THREAD_STATUS_HAS_GIL = (1 << 0) + THREAD_STATUS_ON_CPU = (1 << 1) + THREAD_STATUS_UNKNOWN = (1 << 2) + THREAD_STATUS_GIL_REQUESTED = (1 << 3) + THREAD_STATUS_HAS_EXCEPTION = (1 << 4) + THREAD_STATUS_MAIN_THREAD = (1 << 5) + + +# Categories matching Firefox Profiler expectations +GECKO_CATEGORIES = [ + {"name": "Other", "color": "grey", "subcategories": ["Other"]}, + {"name": "Python", "color": "yellow", "subcategories": ["Other"]}, + {"name": "Native", "color": "blue", "subcategories": ["Other"]}, + {"name": "GC", "color": "orange", "subcategories": ["Other"]}, + {"name": "GIL", "color": "green", "subcategories": ["Other"]}, + {"name": "CPU", "color": "purple", "subcategories": ["Other"]}, + {"name": "Code Type", "color": "red", "subcategories": ["Other"]}, + {"name": "Opcodes", "color": "magenta", "subcategories": ["Other"]}, + {"name": "Exception", "color": "lightblue", "subcategories": ["Other"]}, +] + +# Category indices +CATEGORY_OTHER = 0 +CATEGORY_PYTHON = 1 +CATEGORY_NATIVE = 2 +CATEGORY_GC = 3 +CATEGORY_GIL = 4 +CATEGORY_CPU = 5 +CATEGORY_CODE_TYPE = 6 +CATEGORY_OPCODES = 7 +CATEGORY_EXCEPTION = 8 + +# Subcategory indices +DEFAULT_SUBCATEGORY = 0 + +GECKO_FORMAT_VERSION = 32 +GECKO_PREPROCESSED_VERSION = 57 + +# Resource type constants +RESOURCE_TYPE_LIBRARY = 1 + +# Frame constants +FRAME_ADDRESS_NONE = -1 +FRAME_INLINE_DEPTH_ROOT = 0 + +# Process constants +PROCESS_TYPE_MAIN = 0 +STACKWALK_DISABLED = 0 + +# In-memory buffer before spilling to disk +DEFAULT_SPILL_BUFFER_BYTES = 128 * 1024 +_JSON_SEPARATORS = (",", ":") +_JSON_ENCODER = json.JSONEncoder( + separators=_JSON_SEPARATORS, allow_nan=False +) + + +class SpillColumn: + def __init__(self, directory, basename, *, + buffer_bytes=None): + self.path = os.path.join(directory, basename) + self.buffer = bytearray() + self._buffer_bytes = ( + DEFAULT_SPILL_BUFFER_BYTES if buffer_bytes is None + else buffer_bytes + ) + + def append(self, value): + self.buffer += (_JSON_ENCODER.encode(value) + "\n").encode("utf-8") + if len(self.buffer) >= self._buffer_bytes: + self.flush() + + def flush(self): + with open(self.path, "ab") as file: + file.write(self.buffer) + self.buffer.clear() + + def iter_tokens(self): + with open(self.path, encoding="utf-8") as file: + for line in file: + yield line.rstrip("\n") + + +class GeckoThreadSpill: + _COLUMNS = ( + ("samples_stack", "samples-stack.json"), + ("samples_time", "samples-time.json"), + ("markers_name", "markers-name.json"), + ("markers_start_time", "markers-start-time.json"), + ("markers_end_time", "markers-end-time.json"), + ("markers_phase", "markers-phase.json"), + ("markers_category", "markers-category.json"), + ("markers_data", "markers-data.json"), + ) + + def __init__(self, directory, tid): + prefix = f"thread-{tid}-" + for attr, basename in self._COLUMNS: + setattr(self, attr, SpillColumn(directory, prefix + basename)) + self.sample_count = 0 + self.marker_count = 0 + + def append_sample(self, stack_index, time_ms): + self.samples_stack.append(stack_index) + self.samples_time.append(time_ms) + self.sample_count += 1 + + def append_marker(self, name_idx, start_time, end_time, phase, category, data): + self.markers_name.append(name_idx) + self.markers_start_time.append(start_time) + self.markers_end_time.append(end_time) + self.markers_phase.append(phase) + self.markers_category.append(category) + self.markers_data.append(data) + self.marker_count += 1 + + def prepare_read(self): + for attr, _basename in self._COLUMNS: + getattr(self, attr).flush() + + +class GeckoCollector(Collector): + aggregating = True + + def __init__(self, sample_interval_usec, *, skip_idle=False, opcodes=False): + self.sample_interval_usec = sample_interval_usec + self.skip_idle = skip_idle + self.opcodes_enabled = opcodes + self.start_time = time.monotonic() * 1000 # milliseconds since start + + # Global string table (shared across all threads) + self.global_strings = ["(root)"] # Start with root + self.global_string_map = {"(root)": 0} + + # Per-thread data structures + self.threads = {} # tid -> thread data + self.spill_dir = None + self.exported = False + + # Global tables + self.libs = [] + + # Sampling interval tracking + self.sample_count = 0 + self.last_sample_time = 0 + self.interval = 1.0 # Will be calculated from actual sampling + + # State tracking for interval markers (tid -> start_time) + self.has_gil_start = {} # Thread has the GIL + self.no_gil_start = {} # Thread doesn't have the GIL + self.on_cpu_start = {} # Thread is running on CPU + self.off_cpu_start = {} # Thread is off CPU + self.python_code_start = {} # Thread running Python code (has GIL) + self.native_code_start = {} # Thread running native code (on CPU without GIL) + self.gil_wait_start = {} # Thread waiting for GIL + self.exception_start = {} # Thread has an exception set + self.no_exception_start = {} # Thread has no exception set + + # GC event tracking: track GC start time per thread + self.gc_start_per_thread = {} # tid -> start_time + + # Track which threads have been initialized for state tracking + self.initialized_threads = set() + + # Opcode state tracking per thread: tid -> (opcode, lineno, col_offset, funcname, filename, start_time) + self.opcode_state = {} + + # For binary replay: track base timestamp (first sample's timestamp) + self._replay_base_timestamp_us = None + + def _track_state_transition(self, tid, condition, active_dict, inactive_dict, + active_name, inactive_name, category, current_time): + """Track binary state transitions and emit markers. + + Args: + tid: Thread ID + condition: Whether the active state is true + active_dict: Dict tracking start time of active state + inactive_dict: Dict tracking start time of inactive state + active_name: Name for active state marker + inactive_name: Name for inactive state marker + category: Gecko category for the markers + current_time: Current timestamp + """ + # On first observation of a thread, just record the current state + # without creating a marker (we don't know what the previous state was) + if tid not in self.initialized_threads: + if condition: + active_dict[tid] = current_time + else: + inactive_dict[tid] = current_time + return + + # For already-initialized threads, track transitions + if condition: + active_dict.setdefault(tid, current_time) + if tid in inactive_dict: + self._add_marker(tid, inactive_name, inactive_dict.pop(tid), + current_time, category) + else: + inactive_dict.setdefault(tid, current_time) + if tid in active_dict: + self._add_marker(tid, active_name, active_dict.pop(tid), + current_time, category) + + def collect(self, stack_frames, timestamps_us=None): + """Collect samples from stack frames. + + Args: + stack_frames: List of interpreter/thread frame info + timestamps_us: List of timestamps in microseconds (None for live sampling) + """ + if self.exported: + raise RuntimeError("cannot append to GeckoCollector after export") + + # Handle live sampling (no timestamps provided) + if timestamps_us is None: + current_time = (time.monotonic() * 1000) - self.start_time + times = [current_time] + else: + if not timestamps_us: + return + # Initialize base timestamp if needed + if self._replay_base_timestamp_us is None: + self._replay_base_timestamp_us = timestamps_us[0] + # Convert all timestamps to times (ms relative to first sample) + base = self._replay_base_timestamp_us + times = [(ts - base) / 1000 for ts in timestamps_us] + + first_time = times[0] + + # Update interval calculation + if self.sample_count > 0 and self.last_sample_time > 0: + self.interval = (times[-1] - self.last_sample_time) / self.sample_count + self.last_sample_time = times[-1] + + # Process async tasks + if stack_frames and hasattr(stack_frames[0], "awaited_by"): + for frames, thread_id, _ in self._iter_async_frames(stack_frames): + frames = filter_internal_frames(frames) + if not frames: + continue + + if thread_id not in self.threads: + self.threads[thread_id] = self._create_thread( + thread_id, False + ) + + self._record_stack_sample( + self.threads[thread_id], frames, thread_id, times, first_time + ) + + self.sample_count += len(times) + return + + # Process threads + for interpreter_info in stack_frames: + for thread_info in interpreter_info.threads: + frames = filter_internal_frames(thread_info.frame_info) + tid = thread_info.thread_id + status_flags = thread_info.status + is_main_thread = bool(status_flags & THREAD_STATUS_MAIN_THREAD) + + # Initialize thread if needed + if tid not in self.threads: + self.threads[tid] = self._create_thread(tid, is_main_thread) + + thread_data = self.threads[tid] + + # Decode status flags + has_gil = bool(status_flags & THREAD_STATUS_HAS_GIL) + on_cpu = bool(status_flags & THREAD_STATUS_ON_CPU) + gil_requested = bool(status_flags & THREAD_STATUS_GIL_REQUESTED) + + # Track state transitions using first timestamp + self._track_state_transition( + tid, has_gil, self.has_gil_start, self.no_gil_start, + "Has GIL", "No GIL", CATEGORY_GIL, first_time + ) + self._track_state_transition( + tid, on_cpu, self.on_cpu_start, self.off_cpu_start, + "On CPU", "Off CPU", CATEGORY_CPU, first_time + ) + + # Track code type + if has_gil: + self._track_state_transition( + tid, True, self.python_code_start, self.native_code_start, + "Python Code", "Native Code", CATEGORY_CODE_TYPE, first_time + ) + elif on_cpu: + self._track_state_transition( + tid, True, self.native_code_start, self.python_code_start, + "Native Code", "Python Code", CATEGORY_CODE_TYPE, first_time + ) + else: + if tid in self.initialized_threads: + if tid in self.python_code_start: + self._add_marker(tid, "Python Code", self.python_code_start.pop(tid), + first_time, CATEGORY_CODE_TYPE) + if tid in self.native_code_start: + self._add_marker(tid, "Native Code", self.native_code_start.pop(tid), + first_time, CATEGORY_CODE_TYPE) + + # Track GIL wait + if gil_requested: + self.gil_wait_start.setdefault(tid, first_time) + elif tid in self.gil_wait_start: + self._add_marker(tid, "Waiting for GIL", self.gil_wait_start.pop(tid), + first_time, CATEGORY_GIL) + + # Track exception state + has_exception = bool(status_flags & THREAD_STATUS_HAS_EXCEPTION) + self._track_state_transition( + tid, has_exception, self.exception_start, self.no_exception_start, + "Has Exception", "No Exception", CATEGORY_EXCEPTION, first_time + ) + + # Track GC events + has_gc_frame = any(frame[2] == "" for frame in frames) + if has_gc_frame: + if tid not in self.gc_start_per_thread: + self.gc_start_per_thread[tid] = first_time + elif tid in self.gc_start_per_thread: + self._add_marker(tid, "GC Collecting", self.gc_start_per_thread.pop(tid), + first_time, CATEGORY_GC) + + # Mark thread as initialized + self.initialized_threads.add(tid) + + # Skip idle threads if requested + is_idle = not has_gil and not on_cpu + if self.skip_idle and is_idle: + continue + + if not frames: + continue + + self._record_stack_sample( + thread_data, frames, tid, times, first_time + ) + + self.sample_count += len(times) + + def _record_stack_sample(self, thread_data, frames, tid, times, first_time): + stack_index = self._process_stack(thread_data, frames) + + thread_spill = thread_data["_spill"] + for t in times: + thread_spill.append_sample(stack_index, t) + + if self.opcodes_enabled and frames: + leaf_frame = frames[0] + filename, location, funcname, opcode = leaf_frame + if isinstance(location, tuple): + lineno, _, col_offset, _ = location + else: + lineno = location + col_offset = -1 + + current_state = (opcode, lineno, col_offset, funcname, filename) + + if tid not in self.opcode_state: + self.opcode_state[tid] = (*current_state, first_time) + elif self.opcode_state[tid][:5] != current_state: + ( + prev_opcode, prev_lineno, prev_col, prev_funcname, + prev_filename, prev_start + ) = self.opcode_state[tid] + self._add_opcode_interval_marker( + tid, prev_opcode, prev_lineno, prev_col, prev_funcname, + prev_start, first_time + ) + self.opcode_state[tid] = (*current_state, first_time) + + def _create_thread(self, tid, is_main_thread): + """Create a new thread structure with processed profile format.""" + if self.spill_dir is None: + self.spill_dir = tempfile.TemporaryDirectory() + + thread = { + "name": f"Thread-{tid}", + "isMainThread": is_main_thread, + "processStartupTime": 0, + "processShutdownTime": None, + "registerTime": 0, + "unregisterTime": None, + "pausedRanges": [], + "pid": str(os.getpid()), + "tid": tid, + "processType": "default", + "processName": "Python Process", + # Stack table - processed format + "stackTable": { + "frame": [], + "category": [], + "subcategory": [], + "prefix": [], + "length": 0, # Will be updated on export + }, + # Frame table - processed format + "frameTable": { + "address": [], + "category": [], + "subcategory": [], + "func": [], + "innerWindowID": [], + "implementation": [], + "optimizations": [], + "line": [], + "column": [], + "inlineDepth": [], + "nativeSymbol": [], + "length": 0, # Will be updated on export + }, + # Function table - processed format + "funcTable": { + "name": [], + "isJS": [], + "relevantForJS": [], + "resource": [], + "fileName": [], + "lineNumber": [], + "columnNumber": [], + "length": 0, # Will be updated on export + }, + # Resource table - processed format + "resourceTable": { + "lib": [], + "name": [], + "host": [], + "type": [], + "length": 0, # Will be updated on export + }, + # Native symbols table (empty for Python) + "nativeSymbols": { + "libIndex": [], + "address": [], + "name": [], + "functionSize": [], + "length": 0, + }, + # Caches for deduplication + "_stackCache": {}, + "_frameCache": {}, + "_funcCache": {}, + "_resourceCache": {}, + "_spill": GeckoThreadSpill(self.spill_dir.name, tid), + } + + return thread + + def _is_python(self, filename: str) -> bool: + return not filename.startswith("<") or filename in ["", ""] + + def _get_category(self, filename: str) -> int: + return CATEGORY_PYTHON if self._is_python(filename) else CATEGORY_NATIVE + + def _intern_string(self, s): + """Intern a string in the global string table.""" + if s in self.global_string_map: + return self.global_string_map[s] + idx = len(self.global_strings) + self.global_strings.append(s) + self.global_string_map[s] = idx + return idx + + def _add_marker(self, tid, name, start_time, end_time, category): + """Add an interval marker for a specific thread.""" + if tid not in self.threads: + return + + duration = end_time - start_time + + name_idx = self._intern_string(name) + self.threads[tid]["_spill"].append_marker( + name_idx, start_time, end_time, 1, category, { + "type": name.replace(" ", ""), + "duration": duration, + "tid": tid, + } + ) + + def _add_opcode_interval_marker(self, tid, opcode, lineno, col_offset, + funcname, start_time, end_time): + """Add an interval marker for opcode execution span.""" + if tid not in self.threads or opcode is None: + return + + opcode_info = get_opcode_info(opcode) + # Use formatted opcode name (with base opcode for specialized ones) + formatted_opname = format_opcode(opcode) + + name_idx = self._intern_string(formatted_opname) + + self.threads[tid]["_spill"].append_marker( + name_idx, start_time, end_time, 1, CATEGORY_OPCODES, { + "type": "Opcode", + "opcode": opcode, + "opname": formatted_opname, + "base_opname": opcode_info["base_opname"], + "is_specialized": opcode_info["is_specialized"], + "line": lineno, + "column": col_offset if col_offset >= 0 else None, + "function": funcname, + "duration": end_time - start_time, + } + ) + + def _process_stack(self, thread_data, frames): + """Process a stack and return the stack index.""" + if not frames: + return None + + # Cache references to avoid repeated dictionary lookups + stack_cache = thread_data["_stackCache"] + stack_table = thread_data["stackTable"] + stack_frames = stack_table["frame"] + stack_prefix = stack_table["prefix"] + stack_category = stack_table["category"] + stack_subcategory = stack_table["subcategory"] + + # Build stack bottom-up (from root to leaf) + prefix_stack_idx = None + + for frame_tuple in reversed(frames): + # frame_tuple is (filename, location, funcname, opcode) + # location is (lineno, end_lineno, col_offset, end_col_offset) or just lineno + filename, location, funcname, opcode = frame_tuple + if isinstance(location, tuple): + lineno, end_lineno, col_offset, end_col_offset = location + else: + # Legacy format: location is just lineno + lineno = location + col_offset = -1 + end_col_offset = -1 + + # Get or create function + func_idx = self._get_or_create_func( + thread_data, filename, funcname, lineno + ) + + # Get or create frame (include column for precise source location) + frame_idx = self._get_or_create_frame( + thread_data, func_idx, lineno, col_offset + ) + + # Check stack cache + stack_key = (frame_idx, prefix_stack_idx) + if stack_key in stack_cache: + prefix_stack_idx = stack_cache[stack_key] + else: + # Create new stack entry + stack_idx = len(stack_frames) + stack_frames.append(frame_idx) + stack_prefix.append(prefix_stack_idx) + + # Determine category + category = self._get_category(filename) + stack_category.append(category) + stack_subcategory.append(DEFAULT_SUBCATEGORY) + + stack_cache[stack_key] = stack_idx + prefix_stack_idx = stack_idx + + return prefix_stack_idx + + def _get_or_create_func(self, thread_data, filename, funcname, lineno): + """Get or create a function entry.""" + func_cache = thread_data["_funcCache"] + func_key = (filename, funcname) + + if func_key in func_cache: + return func_cache[func_key] + + # Cache references for func table + func_table = thread_data["funcTable"] + func_names = func_table["name"] + func_is_js = func_table["isJS"] + func_relevant = func_table["relevantForJS"] + func_resources = func_table["resource"] + func_filenames = func_table["fileName"] + func_line_numbers = func_table["lineNumber"] + func_column_numbers = func_table["columnNumber"] + + func_idx = len(func_names) + + # Intern strings in global table + name_idx = self._intern_string(funcname) + + # Determine if Python + is_python = self._is_python(filename) + + # Create resource + resource_idx = self._get_or_create_resource(thread_data, filename) + + # Add function + func_names.append(name_idx) + func_is_js.append(is_python) + func_relevant.append(is_python) + func_resources.append(resource_idx) + + if is_python: + filename_idx = self._intern_string(os.path.basename(filename)) + func_filenames.append(filename_idx) + func_line_numbers.append(lineno) + else: + func_filenames.append(None) + func_line_numbers.append(None) + func_column_numbers.append(None) + + func_cache[func_key] = func_idx + return func_idx + + def _get_or_create_resource(self, thread_data, filename): + """Get or create a resource entry.""" + resource_cache = thread_data["_resourceCache"] + + if filename in resource_cache: + return resource_cache[filename] + + # Cache references for resource table + resource_table = thread_data["resourceTable"] + resource_libs = resource_table["lib"] + resource_names = resource_table["name"] + resource_hosts = resource_table["host"] + resource_types = resource_table["type"] + + resource_idx = len(resource_names) + resource_name = ( + os.path.basename(filename) if "/" in filename else filename + ) + name_idx = self._intern_string(resource_name) + + resource_libs.append(None) + resource_names.append(name_idx) + resource_hosts.append(None) + resource_types.append(RESOURCE_TYPE_LIBRARY) + + resource_cache[filename] = resource_idx + return resource_idx + + def _get_or_create_frame(self, thread_data, func_idx, lineno, col_offset=-1): + """Get or create a frame entry.""" + frame_cache = thread_data["_frameCache"] + # Include column in cache key for precise frame identification + frame_key = (func_idx, lineno, col_offset if col_offset >= 0 else None) + + if frame_key in frame_cache: + return frame_cache[frame_key] + + # Cache references for frame table + frame_table = thread_data["frameTable"] + frame_addresses = frame_table["address"] + frame_inline_depths = frame_table["inlineDepth"] + frame_categories = frame_table["category"] + frame_subcategories = frame_table["subcategory"] + frame_funcs = frame_table["func"] + frame_native_symbols = frame_table["nativeSymbol"] + frame_inner_window_ids = frame_table["innerWindowID"] + frame_implementations = frame_table["implementation"] + frame_lines = frame_table["line"] + frame_columns = frame_table["column"] + frame_optimizations = frame_table["optimizations"] + + frame_idx = len(frame_funcs) + + # Determine category based on function - use cached func table reference + is_python = thread_data["funcTable"]["isJS"][func_idx] + category = CATEGORY_PYTHON if is_python else CATEGORY_NATIVE + + frame_addresses.append(FRAME_ADDRESS_NONE) + frame_inline_depths.append(FRAME_INLINE_DEPTH_ROOT) + frame_categories.append(category) + frame_subcategories.append(DEFAULT_SUBCATEGORY) + frame_funcs.append(func_idx) + frame_native_symbols.append(None) + frame_inner_window_ids.append(None) + frame_implementations.append(None) + frame_lines.append(lineno if lineno else None) + # Store column offset if available (>= 0), otherwise None + frame_columns.append(col_offset if col_offset >= 0 else None) + frame_optimizations.append(None) + + frame_cache[frame_key] = frame_idx + return frame_idx + + def _finalize_markers(self): + """Close any open markers at the end of profiling.""" + end_time = self.last_sample_time + + # Close all open markers for each thread using a generic approach + marker_states = [ + (self.has_gil_start, "Has GIL", CATEGORY_GIL), + (self.no_gil_start, "No GIL", CATEGORY_GIL), + (self.on_cpu_start, "On CPU", CATEGORY_CPU), + (self.off_cpu_start, "Off CPU", CATEGORY_CPU), + (self.python_code_start, "Python Code", CATEGORY_CODE_TYPE), + (self.native_code_start, "Native Code", CATEGORY_CODE_TYPE), + (self.gil_wait_start, "Waiting for GIL", CATEGORY_GIL), + (self.gc_start_per_thread, "GC Collecting", CATEGORY_GC), + (self.exception_start, "Has Exception", CATEGORY_EXCEPTION), + (self.no_exception_start, "No Exception", CATEGORY_EXCEPTION), + ] + + for state_dict, marker_name, category in marker_states: + for tid in list(state_dict.keys()): + self._add_marker(tid, marker_name, state_dict[tid], end_time, category) + del state_dict[tid] + + # Close any open opcode markers + for tid, state in list(self.opcode_state.items()): + opcode, lineno, col_offset, funcname, filename, start_time = state + self._add_opcode_interval_marker(tid, opcode, lineno, col_offset, funcname, start_time, end_time) + self.opcode_state.clear() + + def export(self, filename): + """Export the profile to a Gecko JSON file.""" + if self.sample_count > 0 and self.last_sample_time > 0: + self.interval = self.last_sample_time / self.sample_count + + # Spinner for progress indication + spinner = itertools.cycle(['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']) + stop_spinner = threading.Event() + + def spin(): + message = 'Building Gecko profile...' + while not stop_spinner.is_set(): + sys.stderr.write(f'\r{next(spinner)} {message}') + sys.stderr.flush() + time.sleep(0.1) + # Clear the spinner line + sys.stderr.write('\r' + ' ' * (len(message) + 3) + '\r') + sys.stderr.flush() + + spinner_thread = threading.Thread(target=spin, daemon=True) + spinner_thread.start() + + temp_path = None + replaced = False + try: + self._prepare_for_serialization() + output_dir = os.path.dirname(os.path.abspath(filename)) or "." + with tempfile.NamedTemporaryFile( + "w", dir=output_dir, delete=False + ) as file: + temp_path = file.name + self._stream_profile(file) + os.replace(temp_path, filename) + replaced = True + finally: + self.exported = True + stop_spinner.set() + spinner_thread.join(timeout=1.0) + # Small delay to ensure the clear happens + time.sleep(0.01) + if temp_path is not None and not replaced: + try: + os.unlink(temp_path) + except FileNotFoundError: + pass + self._cleanup_spills() + + print(f"Gecko profile written to {filename}") + print( + f"Open in Firefox Profiler: https://profiler.firefox.com/" + ) + return True + + def _build_marker_schema(self): + """Build marker schema definitions for Firefox Profiler.""" + schema = [] + + # Opcode marker schema (only if opcodes enabled) + if self.opcodes_enabled: + schema.append({ + "name": "Opcode", + "display": ["marker-table", "marker-chart"], + "tooltipLabel": "{marker.data.opname}", + "tableLabel": "{marker.data.opname} at line {marker.data.line}", + "chartLabel": "{marker.data.opname}", + "fields": [ + {"key": "opname", "label": "Opcode", "format": "string", "searchable": True}, + {"key": "base_opname", "label": "Base Opcode", "format": "string"}, + {"key": "is_specialized", "label": "Specialized", "format": "string"}, + {"key": "line", "label": "Line", "format": "integer"}, + {"key": "column", "label": "Column", "format": "integer"}, + {"key": "function", "label": "Function", "format": "string"}, + {"key": "duration", "label": "Duration", "format": "duration"}, + ], + }) + + return schema + + def _build_profile(self): + """Build the complete profile structure in processed format.""" + try: + self._prepare_for_serialization() + file = io.StringIO() + self._stream_profile(file) + return json.loads(file.getvalue()) + finally: + self.exported = True + self._cleanup_spills() + + def _profile_head(self): + return { + "meta": { + "interval": self.interval, + "startTime": self.start_time, + "abi": platform.machine(), + "misc": "Python profiler", + "oscpu": platform.machine(), + "platform": platform.system(), + "processType": PROCESS_TYPE_MAIN, + "categories": GECKO_CATEGORIES, + "stackwalk": STACKWALK_DISABLED, + "toolkit": "", + "version": GECKO_FORMAT_VERSION, + "preprocessedProfileVersion": GECKO_PREPROCESSED_VERSION, + "appBuildID": "", + "physicalCPUs": os.cpu_count() or 0, + "logicalCPUs": os.cpu_count() or 0, + "CPUName": "", + "product": "Python", + "symbolicated": True, + "markerSchema": self._build_marker_schema(), + "importedFrom": "Tachyon Sampling Profiler", + "extensions": { + "id": [], + "name": [], + "baseURL": [], + "length": 0, + }, + }, + "libs": self.libs, + } + + def _profile_tail(self): + return { + "pages": [], + "shared": { + "stringArray": self.global_strings, + "sources": {"length": 0, "uuid": [], "filename": []}, + }, + } + + def _prepare_for_serialization(self): + if self.exported: + raise RuntimeError("GeckoCollector has already been exported") + self._finalize_markers() + for thread_data in self.threads.values(): + thread_data["_spill"].prepare_read() + thread_data["stackTable"]["length"] = len(thread_data["stackTable"]["frame"]) + thread_data["frameTable"]["length"] = len(thread_data["frameTable"]["func"]) + thread_data["funcTable"]["length"] = len(thread_data["funcTable"]["name"]) + thread_data["resourceTable"]["length"] = len(thread_data["resourceTable"]["name"]) + + def _cleanup_spills(self): + if self.spill_dir is not None: + self.spill_dir.cleanup() + self.spill_dir = None + + def _stream_profile(self, file): + file.write("{") + first = True + for key, value in self._profile_head().items(): + first = _write_json_member(file, key, value, first) + + first = _write_member_name(file, "threads", first) + file.write("[") + for index, (tid, thread_data) in enumerate(self.threads.items()): + if index: + file.write(",") + self._stream_thread(file, tid, thread_data) + file.write("]") + + for key, value in self._profile_tail().items(): + first = _write_json_member(file, key, value, first) + file.write("}") + + def _stream_thread(self, file, tid, thread_data): + spill = thread_data["_spill"] + metadata = { + "name": thread_data["name"], + "isMainThread": thread_data["isMainThread"], + "processStartupTime": thread_data["processStartupTime"], + "processShutdownTime": thread_data["processShutdownTime"], + "registerTime": thread_data["registerTime"], + "unregisterTime": thread_data["unregisterTime"], + "pausedRanges": thread_data["pausedRanges"], + "pid": thread_data["pid"], + "tid": thread_data["tid"], + "processType": thread_data["processType"], + "processName": thread_data["processName"], + } + file.write("{") + first = True + for key, value in metadata.items(): + first = _write_json_member(file, key, value, first) + + first = _write_member_name(file, "samples", first) + self._stream_samples(file, spill) + for key in ( + "stackTable", + "frameTable", + "funcTable", + "resourceTable", + "nativeSymbols", + ): + first = _write_json_member(file, key, thread_data[key], first) + first = _write_member_name(file, "markers", first) + self._stream_markers(file, spill) + file.write("}") + + def _stream_samples(self, file, spill): + _stream_column_table( + file, + ( + ("stack", spill.samples_stack.iter_tokens()), + ("time", spill.samples_time.iter_tokens()), + ("eventDelay", ("null" for _ in range(spill.sample_count))), + ), + spill.sample_count, + ( + ("weight", None), + ("weightType", "samples"), + ("length", spill.sample_count), + ), + ) + + def _stream_markers(self, file, spill): + _stream_column_table( + file, + ( + ("data", spill.markers_data.iter_tokens()), + ("name", spill.markers_name.iter_tokens()), + ("startTime", spill.markers_start_time.iter_tokens()), + ("endTime", spill.markers_end_time.iter_tokens()), + ("phase", spill.markers_phase.iter_tokens()), + ("category", spill.markers_category.iter_tokens()), + ), + spill.marker_count, + (("length", spill.marker_count),), + ) + + +def _write_json(file, value): + for chunk in _JSON_ENCODER.iterencode(value): + file.write(chunk) + + +def _write_member_name(file, name, first): + if not first: + file.write(",") + _write_json(file, name) + file.write(":") + return False + + +def _write_json_member(file, name, value, first): + first = _write_member_name(file, name, first) + _write_json(file, value) + return first + + +def _stream_column_table(file, columns, expected_count, trailing_members=()): + file.write("{") + first = True + for name, token_iter in columns: + first = _write_member_name(file, name, first) + _stream_array(file, token_iter, expected_count, name) + for name, value in trailing_members: + first = _write_json_member(file, name, value, first) + file.write("}") + + +def _stream_array(file, token_iter, expected_count, label="array"): + file.write("[") + count = 0 + for token in token_iter: + if count: + file.write(",") + file.write(token) + count += 1 + if count != expected_count: + raise RuntimeError( + f"streamed {count} {label} items, expected {expected_count}" + ) + file.write("]") diff --git a/Lib/profiling/sampling/heatmap_collector.py b/Lib/profiling/sampling/heatmap_collector.py new file mode 100644 index 00000000000..0361fd357b2 --- /dev/null +++ b/Lib/profiling/sampling/heatmap_collector.py @@ -0,0 +1,1145 @@ +"""Heatmap collector for Python profiling with line-level execution heat visualization.""" + +import base64 +import collections +import html +import importlib.resources +import json +import locale +import math +import os +import platform +import site +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, List, Tuple + +from ._css_utils import get_combined_css +from ._format_utils import fmt +from .collector import normalize_location, extract_lineno +from .opcode_utils import get_opcode_info, format_opcode +from .stack_collector import StackTraceCollector +from .module_utils import extract_module_name, get_python_path_info + + +# ============================================================================ +# Data Classes +# ============================================================================ + +@dataclass +class FileStats: + """Statistics for a single profiled file.""" + filename: str + module_name: str + module_type: str + total_samples: int + total_self_samples: int + num_lines: int + max_samples: int + max_self_samples: int + percentage: float = 0.0 + + +@dataclass +class TreeNode: + """Node in the hierarchical file tree structure.""" + files: List[FileStats] = field(default_factory=list) + samples: int = 0 + count: int = 0 + children: Dict[str, 'TreeNode'] = field(default_factory=dict) + + +# ============================================================================ +# Helper Classes +# ============================================================================ + +class _TemplateLoader: + """Loads and caches HTML/CSS/JS templates for heatmap generation.""" + + def __init__(self): + """Load all templates and assets once.""" + self.index_template = None + self.file_template = None + self.index_css = None + self.index_js = None + self.file_css = None + self.file_js = None + self.logo_html = None + + self._load_templates() + + def _load_templates(self): + """Load all template files from _heatmap_assets.""" + try: + template_dir = importlib.resources.files(__package__) + assets_dir = template_dir / "_heatmap_assets" + + # Load HTML templates + self.index_template = (assets_dir / "heatmap_index_template.html").read_text(encoding="utf-8") + self.file_template = (assets_dir / "heatmap_pyfile_template.html").read_text(encoding="utf-8") + + # Load CSS (same file used for both index and file pages) + css_content = get_combined_css("heatmap") + self.index_css = css_content + self.file_css = css_content + + # Load JS + base_js = (template_dir / "_shared_assets" / "base.js").read_text(encoding="utf-8") + heatmap_shared_js = (assets_dir / "heatmap_shared.js").read_text(encoding="utf-8") + shared_js = f"{base_js}\n{heatmap_shared_js}" + self.index_js = f"{shared_js}\n{(assets_dir / 'heatmap_index.js').read_text(encoding='utf-8')}" + self.file_js = f"{shared_js}\n{(assets_dir / 'heatmap.js').read_text(encoding='utf-8')}" + + # Load Tachyon logo + logo_dir = template_dir / "_assets" + try: + png_path = logo_dir / "tachyon-logo.png" + b64_logo = base64.b64encode(png_path.read_bytes()).decode("ascii") + self.logo_html = f'' + except (FileNotFoundError, IOError) as e: + self.logo_html = '
    ' + print(f"Warning: Could not load Tachyon logo: {e}") + + except (FileNotFoundError, IOError) as e: + raise RuntimeError(f"Failed to load heatmap template files: {e}") from e + + +class _TreeBuilder: + """Builds hierarchical tree structure from file statistics.""" + + @staticmethod + def build_file_tree(file_stats: List[FileStats]) -> Dict[str, TreeNode]: + """Build hierarchical tree grouped by module type, then by module structure. + + Args: + file_stats: List of FileStats objects + + Returns: + Dictionary mapping module types to their tree roots + """ + # Group by module type first + type_groups = {'stdlib': [], 'site-packages': [], 'project': [], 'other': []} + for stat in file_stats: + type_groups[stat.module_type].append(stat) + + # Build tree for each type + trees = {} + for module_type, stats in type_groups.items(): + if not stats: + continue + + root_node = TreeNode() + + for stat in stats: + module_name = stat.module_name + parts = module_name.split('.') + + # Navigate/create tree structure + current_node = root_node + for i, part in enumerate(parts): + if i == len(parts) - 1: + # Last part - store the file + current_node.files.append(stat) + else: + # Intermediate part - create or navigate + if part not in current_node.children: + current_node.children[part] = TreeNode() + current_node = current_node.children[part] + + # Calculate aggregate stats for this type's tree + _TreeBuilder._calculate_node_stats(root_node) + trees[module_type] = root_node + + return trees + + @staticmethod + def _calculate_node_stats(node: TreeNode) -> Tuple[int, int]: + """Recursively calculate aggregate statistics for tree nodes. + + Args: + node: TreeNode to calculate stats for + + Returns: + Tuple of (total_samples, file_count) + """ + total_samples = 0 + file_count = 0 + + # Count files at this level + for file_stat in node.files: + total_samples += file_stat.total_samples + file_count += 1 + + # Recursively process children + for child in node.children.values(): + child_samples, child_count = _TreeBuilder._calculate_node_stats(child) + total_samples += child_samples + file_count += child_count + + node.samples = total_samples + node.count = file_count + return total_samples, file_count + + +class _HtmlRenderer: + """Renders hierarchical tree structures as HTML.""" + + def __init__(self, file_index: Dict[str, str]): + """Initialize renderer with file index. + + Args: + file_index: Mapping from filenames to HTML file names + """ + self.file_index = file_index + self.heatmap_bar_height = 16 + + def render_hierarchical_html(self, trees: Dict[str, TreeNode]) -> str: + """Build hierarchical HTML with type sections and collapsible module folders. + + Args: + trees: Dictionary mapping module types to tree roots + + Returns: + Complete HTML string for all sections + """ + type_names = { + 'stdlib': '📚 Standard Library', + 'site-packages': '📦 Site Packages', + 'project': '🏗️ Project Files', + 'other': '📄 Other Files' + } + + sections = [] + for module_type in ['project', 'stdlib', 'site-packages', 'other']: + if module_type not in trees: + continue + + tree = trees[module_type] + + # Project starts expanded, others start collapsed + is_collapsed = module_type in {'stdlib', 'site-packages', 'other'} + icon = '▶' if is_collapsed else '▼' + content_style = ' style="display: none;"' if is_collapsed else '' + + file_word = "file" if tree.count == 1 else "files" + sample_word = "sample" if tree.samples == 1 else "samples" + section_html = f''' +
    +
    + {icon} + {type_names[module_type]} + ({tree.count} {file_word}, {tree.samples:n} {sample_word}) +
    +
    +''' + + # Render root folders + root_folders = sorted(tree.children.items(), + key=lambda x: x[1].samples, reverse=True) + + for folder_name, folder_node in root_folders: + section_html += self._render_folder(folder_node, folder_name, level=1) + + # Render root files (files not in any module) + if tree.files: + sorted_files = sorted(tree.files, key=lambda x: x.total_samples, reverse=True) + section_html += '
    \n' + for stat in sorted_files: + section_html += self._render_file_item(stat, indent=' ') + section_html += '
    \n' + + section_html += '
    \n
    \n' + sections.append(section_html) + + return '\n'.join(sections) + + def _render_folder(self, node: TreeNode, name: str, level: int = 1) -> str: + """Render a single folder node recursively. + + Args: + node: TreeNode to render + name: Display name for the folder + level: Nesting level for indentation + + Returns: + HTML string for this folder and its contents + """ + indent = ' ' * level + parts = [] + + # Render folder header (collapsed by default) + file_word = "file" if node.count == 1 else "files" + sample_word = "sample" if node.samples == 1 else "samples" + parts.append(f'{indent}') + + return '\n'.join(parts) + + def _render_file_item(self, stat: FileStats, indent: str = '') -> str: + """Render a single file item with heatmap bar. + + Args: + stat: FileStats object + indent: Indentation string + + Returns: + HTML string for file item + """ + full_path = html.escape(stat.filename) + module_name = html.escape(stat.module_name) + + intensity = stat.percentage / 100.0 + bar_width = min(stat.percentage, 100) + + html_file = self.file_index[stat.filename] + s = "" if stat.total_samples == 1 else "s" + + return (f'{indent}
    \n' + f'{indent} 📄 {module_name}\n' + f'{indent} {stat.total_samples:n} sample{s}\n' + f'{indent}
    \n' + f'{indent}
    \n') + + +# ============================================================================ +# Main Collector Class +# ============================================================================ + +class HeatmapCollector(StackTraceCollector): + """Collector that generates coverage.py-style heatmap HTML output with line intensity. + + This collector creates detailed HTML reports showing which lines of code + were executed most frequently during profiling, similar to coverage.py + but showing execution "heat" rather than just coverage. + """ + + # File naming and formatting constants + FILE_INDEX_FORMAT = "file_{:04d}.html" + + def __init__(self, *args, **kwargs): + """Initialize the heatmap collector with data structures for analysis.""" + super().__init__(*args, **kwargs) + + # Sample counting data structures + self.line_samples = collections.Counter() + self.file_samples = collections.defaultdict(collections.Counter) + self.line_self_samples = collections.Counter() + self.file_self_samples = collections.defaultdict(collections.Counter) + + # Call graph data structures for navigation (sets for O(1) deduplication) + self.call_graph = collections.defaultdict(set) + self.callers_graph = collections.defaultdict(set) + self.function_definitions = {} + + # Map each sampled line to its function for proper caller lookup + # (filename, lineno) -> funcname + self.line_to_function = {} + + # Edge counting for call path analysis + self.edge_samples = collections.Counter() + + # Bytecode-level tracking data structures + # Track samples per (file, lineno) -> {opcode: {'count': N, 'locations': set()}} + # Locations are deduplicated via set to minimize memory usage + self.line_opcodes = collections.defaultdict(dict) + + # Statistics and metadata + self._total_samples = 0 + self._path_info = get_python_path_info() + self.stats = {} + + # Opcode collection flag + self.opcodes_enabled = False + + # Template loader (loads all templates once) + self._template_loader = _TemplateLoader() + + # File index (populated during export) + self.file_index = {} + + # Reusable set for deduplicating line locations within a single sample. + # This avoids over-counting recursive functions in cumulative stats. + self._seen_lines = set() + + def set_stats(self, sample_interval_usec, duration_sec, sample_rate, error_rate=None, missed_samples=None, **kwargs): + """Set profiling statistics to include in heatmap output. + + Args: + sample_interval_usec: Sampling interval in microseconds + duration_sec: Total profiling duration in seconds + sample_rate: Effective sampling rate + error_rate: Optional error rate during profiling + missed_samples: Optional percentage of missed samples + **kwargs: Additional statistics to include + """ + self.stats = { + "sample_interval_usec": sample_interval_usec, + "duration_sec": duration_sec, + "sample_rate": sample_rate, + "error_rate": error_rate, + "missed_samples": missed_samples, + "python_version": sys.version, + "python_implementation": platform.python_implementation(), + "platform": platform.platform(), + } + self.stats.update(kwargs) + + def process_frames(self, frames, thread_id, weight=1): + """Process stack frames and count samples per line. + + Args: + frames: List of (filename, location, funcname, opcode) tuples in + leaf-to-root order. location is (lineno, end_lineno, col_offset, end_col_offset). + opcode is None if not gathered. + thread_id: Thread ID for this stack trace + weight: Number of samples this stack represents (for batched RLE) + """ + self._total_samples += weight + self._seen_lines.clear() + + for i, (filename, location, funcname, opcode) in enumerate(frames): + # Normalize location to 4-tuple format + lineno, end_lineno, col_offset, end_col_offset = normalize_location(location) + + if not self._is_valid_frame(filename, lineno): + continue + + # frames[0] is the leaf - where execution is actually happening + is_leaf = (i == 0) + line_key = (filename, lineno) + count_cumulative = line_key not in self._seen_lines + if count_cumulative: + self._seen_lines.add(line_key) + + self._record_line_sample(filename, lineno, funcname, is_leaf=is_leaf, + count_cumulative=count_cumulative, weight=weight) + + if opcode is not None: + # Set opcodes_enabled flag when we first encounter opcode data + self.opcodes_enabled = True + self._record_bytecode_sample(filename, lineno, opcode, + end_lineno, col_offset, end_col_offset, + weight=weight) + + # Build call graph for adjacent frames (relationships are deduplicated anyway) + if i + 1 < len(frames): + next_frame = frames[i + 1] + next_lineno = extract_lineno(next_frame[1]) + self._record_call_relationship( + (filename, lineno, funcname), + (next_frame[0], next_lineno, next_frame[2]), + weight=weight, + ) + + def _is_valid_frame(self, filename, lineno): + """Check if a frame should be included in the heatmap.""" + # Skip internal or invalid files + if not filename or filename.startswith('<') or filename.startswith('['): + return False + + # Skip invalid frames with corrupted filename data + if filename == "__init__" and lineno == 0: + return False + + return True + + def _record_line_sample(self, filename, lineno, funcname, is_leaf=False, + count_cumulative=True, weight=1): + """Record a sample for a specific line.""" + # Track cumulative samples (all occurrences in stack) + if count_cumulative: + self.line_samples[(filename, lineno)] += weight + self.file_samples[filename][lineno] += weight + + # Track self/leaf samples (only when at top of stack) + if is_leaf: + self.line_self_samples[(filename, lineno)] += weight + self.file_self_samples[filename][lineno] += weight + + # Record function definition location + if funcname and (filename, funcname) not in self.function_definitions: + self.function_definitions[(filename, funcname)] = lineno + + # Map this line to its function for caller/callee navigation + if funcname: + self.line_to_function[(filename, lineno)] = funcname + + def _record_bytecode_sample(self, filename, lineno, opcode, + end_lineno=None, col_offset=None, end_col_offset=None, + weight=1): + """Record a sample for a specific bytecode instruction. + + Args: + filename: Source filename + lineno: Line number + opcode: Opcode number being executed + end_lineno: End line number (may be -1 if not available) + col_offset: Column offset in UTF-8 bytes (may be -1 if not available) + end_col_offset: End column offset in UTF-8 bytes (may be -1 if not available) + weight: Number of samples this represents (for batched RLE) + """ + key = (filename, lineno) + + # Initialize opcode entry if needed - use set for location deduplication + if opcode not in self.line_opcodes[key]: + self.line_opcodes[key][opcode] = {'count': 0, 'locations': set()} + + self.line_opcodes[key][opcode]['count'] += weight + + # Store unique location info if column offset is available (not -1) + if col_offset is not None and col_offset >= 0: + # Use tuple as set key for deduplication + loc_key = (end_lineno, col_offset, end_col_offset) + self.line_opcodes[key][opcode]['locations'].add(loc_key) + + def _get_bytecode_data_for_line(self, filename, lineno): + """Get bytecode disassembly data for instructions on a specific line. + + Args: + filename: Source filename + lineno: Line number + + Returns: + List of dicts with instruction info, sorted by samples descending + """ + key = (filename, lineno) + opcode_data = self.line_opcodes.get(key, {}) + + result = [] + for opcode, data in opcode_data.items(): + info = get_opcode_info(opcode) + # Handle both old format (int count) and new format (dict with count/locations) + if isinstance(data, dict): + count = data.get('count', 0) + raw_locations = data.get('locations', set()) + # Convert set of tuples to list of dicts for JSON serialization + if isinstance(raw_locations, set): + locations = [ + {'end_lineno': loc[0], 'col_offset': loc[1], 'end_col_offset': loc[2]} + for loc in raw_locations + ] + else: + locations = raw_locations + else: + count = data + locations = [] + + result.append({ + 'opcode': opcode, + 'opname': format_opcode(opcode), + 'base_opname': info['base_opname'], + 'is_specialized': info['is_specialized'], + 'samples': count, + 'locations': locations, + }) + + # Sort by samples descending, then by opcode number + result.sort(key=lambda x: (-x['samples'], x['opcode'])) + return result + + def _record_call_relationship(self, callee_frame, caller_frame, weight=1): + """Record caller/callee relationship between adjacent frames.""" + callee_filename, callee_lineno, callee_funcname = callee_frame + caller_filename, caller_lineno, caller_funcname = caller_frame + + # Skip internal files for call graph + if callee_filename.startswith('<') or callee_filename.startswith('['): + return + + # Get the callee's function definition line + callee_def_line = self.function_definitions.get( + (callee_filename, callee_funcname), callee_lineno + ) + + # Record caller -> callee relationship (set handles deduplication) + caller_key = (caller_filename, caller_lineno) + callee_info = (callee_filename, callee_def_line, callee_funcname) + self.call_graph[caller_key].add(callee_info) + + # Record callee <- caller relationship (set handles deduplication) + callee_key = (callee_filename, callee_def_line) + caller_info = (caller_filename, caller_lineno, caller_funcname) + self.callers_graph[callee_key].add(caller_info) + + # Count this call edge for path analysis + edge_key = (caller_key, callee_key) + self.edge_samples[edge_key] += weight + + def export(self, output_path): + """Export heatmap data as HTML files in a directory. + + Args: + output_path: Path where to create the heatmap output directory + """ + if not self.file_samples: + print("Warning: No heatmap data to export") + return False + + try: + output_dir = self._prepare_output_directory(output_path) + file_stats = self._calculate_file_stats() + self._create_file_index(file_stats) + + # Generate individual file reports + self._generate_file_reports(output_dir, file_stats) + + # Generate index page + self._generate_index_html(output_dir / 'index.html', file_stats) + + self._print_export_summary(output_dir, file_stats) + return True + + except Exception as e: + print(f"Error: Failed to export heatmap: {e}") + raise + + def _prepare_output_directory(self, output_path): + """Create output directory for heatmap files.""" + output_dir = Path(output_path) + if output_dir.suffix == '.html': + output_dir = output_dir.with_suffix('') + + try: + output_dir.mkdir(exist_ok=True, parents=True) + except (IOError, OSError) as e: + raise RuntimeError(f"Failed to create output directory {output_dir}: {e}") from e + + return output_dir + + def _create_file_index(self, file_stats: List[FileStats]): + """Create mapping from filenames to HTML file names.""" + self.file_index = { + stat.filename: self.FILE_INDEX_FORMAT.format(i) + for i, stat in enumerate(file_stats) + } + + def _generate_file_reports(self, output_dir, file_stats: List[FileStats]): + """Generate HTML report for each source file.""" + for stat in file_stats: + file_path = output_dir / self.file_index[stat.filename] + line_counts = self.file_samples[stat.filename] + valid_line_counts = {line: count for line, count in line_counts.items() if line >= 0} + + self_counts = self.file_self_samples.get(stat.filename, {}) + valid_self_counts = {line: count for line, count in self_counts.items() if line >= 0} + + self._generate_file_html( + file_path, + stat.filename, + valid_line_counts, + valid_self_counts, + stat + ) + + def _print_export_summary(self, output_dir, file_stats: List[FileStats]): + """Print summary of exported heatmap.""" + print(f"Heatmap output written to {output_dir}/") + print(f" - Index: {output_dir / 'index.html'}") + s = "" if len(file_stats) == 1 else "s" + print(f" - {len(file_stats)} source file{s} analyzed") + + def _calculate_file_stats(self) -> List[FileStats]: + """Calculate statistics for each file. + + Returns: + List of FileStats objects sorted by total samples + """ + file_stats = [] + for filename, line_counts in self.file_samples.items(): + # Skip special frames + if filename in ('~', '...', '.') or filename.startswith('<') or filename.startswith('['): + continue + + # Filter out lines with -1 (special frames) + valid_line_counts = {line: count for line, count in line_counts.items() if line >= 0} + if not valid_line_counts: + continue + + # Get self samples for this file + self_line_counts = self.file_self_samples.get(filename, {}) + valid_self_counts = {line: count for line, count in self_line_counts.items() if line >= 0} + + total_samples = sum(valid_line_counts.values()) + total_self_samples = sum(valid_self_counts.values()) + num_lines = len(valid_line_counts) + max_samples = max(valid_line_counts.values()) + max_self_samples = max(valid_self_counts.values()) if valid_self_counts else 0 + module_name, module_type = extract_module_name(filename, self._path_info) + + file_stats.append(FileStats( + filename=filename, + module_name=module_name, + module_type=module_type, + total_samples=total_samples, + total_self_samples=total_self_samples, + num_lines=num_lines, + max_samples=max_samples, + max_self_samples=max_self_samples, + percentage=0.0 + )) + + # Sort by total samples and calculate percentages + file_stats.sort(key=lambda x: x.total_samples, reverse=True) + if file_stats: + max_total = file_stats[0].total_samples + for stat in file_stats: + stat.percentage = (stat.total_samples / max_total * 100) if max_total > 0 else 0 + + return file_stats + + def _generate_index_html(self, index_path: Path, file_stats: List[FileStats]): + """Generate index.html with list of all profiled files.""" + # Build hierarchical tree + tree = _TreeBuilder.build_file_tree(file_stats) + + # Render tree as HTML + renderer = _HtmlRenderer(self.file_index) + sections_html = renderer.render_hierarchical_html(tree) + + # Format error rate and missed samples with bar classes + error_rate = self.stats.get('error_rate') + if error_rate is not None: + error_rate_str = f"{fmt(error_rate)}%" + error_rate_width = min(error_rate, 100) + # Determine bar color class based on rate + if error_rate < 5: + error_rate_class = "good" + elif error_rate < 15: + error_rate_class = "warning" + else: + error_rate_class = "error" + else: + error_rate_str = "N/A" + error_rate_width = 0 + error_rate_class = "good" + + missed_samples = self.stats.get('missed_samples') + if missed_samples is not None: + missed_samples_str = f"{fmt(missed_samples)}%" + missed_samples_width = min(missed_samples, 100) + if missed_samples < 5: + missed_samples_class = "good" + elif missed_samples < 15: + missed_samples_class = "warning" + else: + missed_samples_class = "error" + else: + missed_samples_str = "N/A" + missed_samples_width = 0 + missed_samples_class = "good" + + # Populate template + replacements = { + "": f"", + "": f"", + "": self._template_loader.logo_html, + "": f"{sys.version_info.major}.{sys.version_info.minor}", + "": f"{len(file_stats):n}", + "": f"{self._total_samples:n}", + "": fmt(self.stats.get('duration_sec', 0)), + "": fmt(self.stats.get('sample_rate', 0)), + "": error_rate_str, + "": str(error_rate_width), + "": error_rate_class, + "": missed_samples_str, + "": str(missed_samples_width), + "": missed_samples_class, + "": sections_html, + } + + html_content = self._template_loader.index_template + for placeholder, value in replacements.items(): + html_content = html_content.replace(placeholder, value) + + try: + index_path.write_text(html_content, encoding='utf-8') + except (IOError, OSError) as e: + raise RuntimeError(f"Failed to write index file {index_path}: {e}") from e + + def _generate_file_html(self, output_path: Path, filename: str, + line_counts: Dict[int, int], self_counts: Dict[int, int], + file_stat: FileStats): + """Generate HTML for a single source file with heatmap coloring.""" + # Read source file + try: + source_lines = Path(filename).read_text(encoding='utf-8', errors='replace').splitlines() + except (IOError, OSError) as e: + if not (filename.startswith('<') or filename.startswith('[') or + filename in ('~', '...', '.') or len(filename) < 2): + print(f"Warning: Could not read source file {filename}: {e}") + source_lines = [f"# Source file not available: {filename}"] + + # Generate HTML for each line + max_samples = max(line_counts.values()) if line_counts else 1 + max_self_samples = max(self_counts.values()) if self_counts else 1 + code_lines_html = [ + self._build_line_html(line_num, line_content, line_counts, self_counts, + max_samples, max_self_samples, filename) + for line_num, line_content in enumerate(source_lines, start=1) + ] + + # Populate template + replacements = { + "": html.escape(filename), + "": f"{file_stat.total_samples:n}", + "": f"{file_stat.total_self_samples:n}", + "": f"{file_stat.num_lines:n}", + "": fmt(file_stat.percentage, 2), + "": f"{file_stat.max_samples:n}", + "": f"{file_stat.max_self_samples:n}", + "": ''.join(code_lines_html), + "": f"", + "": f"", + "": self._template_loader.logo_html, + "": f"{sys.version_info.major}.{sys.version_info.minor}", + } + + html_content = self._template_loader.file_template + for placeholder, value in replacements.items(): + html_content = html_content.replace(placeholder, value) + + try: + output_path.write_text(html_content, encoding='utf-8') + except (IOError, OSError) as e: + raise RuntimeError(f"Failed to write file {output_path}: {e}") from e + + def _build_line_html(self, line_num: int, line_content: str, + line_counts: Dict[int, int], self_counts: Dict[int, int], + max_samples: int, max_self_samples: int, filename: str) -> str: + """Build HTML for a single line of source code.""" + cumulative_samples = line_counts.get(line_num, 0) + self_samples = self_counts.get(line_num, 0) + + # Calculate colors for both self and cumulative modes + if cumulative_samples > 0: + log_cumulative = math.log(cumulative_samples + 1) + log_max = math.log(max_samples + 1) + cumulative_intensity = log_cumulative / log_max if log_max > 0 else 0 + + if self_samples > 0 and max_self_samples > 0: + log_self = math.log(self_samples + 1) + log_max_self = math.log(max_self_samples + 1) + self_intensity = log_self / log_max_self if log_max_self > 0 else 0 + else: + self_intensity = 0 + + self_display = f"{self_samples:n}" if self_samples > 0 else "" + cumulative_display = f"{cumulative_samples:n}" + tooltip = f"Self: {self_samples:n}, Total: {cumulative_samples:n}" + else: + cumulative_intensity = 0 + self_intensity = 0 + self_display = "" + cumulative_display = "" + tooltip = "" + + # Get bytecode data for this line (if any) + bytecode_data = self._get_bytecode_data_for_line(filename, line_num) + has_bytecode = len(bytecode_data) > 0 and cumulative_samples > 0 + + # Build bytecode toggle button if data is available + bytecode_btn_html = '' + bytecode_panel_html = '' + if has_bytecode: + bytecode_json = html.escape(json.dumps(bytecode_data)) + + # Calculate specialization percentage + total_samples = sum(d['samples'] for d in bytecode_data) + specialized_samples = sum(d['samples'] for d in bytecode_data if d['is_specialized']) + spec_pct = int(100 * specialized_samples / total_samples) if total_samples > 0 else 0 + + bytecode_btn_html = ( + f'' + ) + # Wrapper contains columns + content panel + bytecode_panel_html = ( + f'
    \n' + f'
    ' + f'
    ' + f'
    ' + f'
    ' + f'
    \n' + f'
    \n' + f'
    \n' + ) + elif self.opcodes_enabled: + # Add invisible spacer to maintain consistent indentation when opcodes are enabled + bytecode_btn_html = '
    ' + + # Get navigation buttons + nav_buttons_html = self._build_navigation_buttons(filename, line_num) + + # Build line HTML with instruction highlights if available + line_html = self._render_source_with_highlights(line_content, line_num, + filename, bytecode_data) + title_attr = f' title="{html.escape(tooltip)}"' if tooltip else "" + + # Specialization color for toggle mode (green gradient based on spec %) + spec_color_attr = '' + if has_bytecode: + spec_color = self._format_specialization_color(spec_pct) + spec_color_attr = f'data-spec-color="{spec_color}" ' + + return ( + f'
    \n' + f'
    {line_num}
    \n' + f'
    {self_display}
    \n' + f'
    {cumulative_display}
    \n' + f' {bytecode_btn_html}\n' + f'
    {line_html}
    \n' + f' {nav_buttons_html}\n' + f'
    \n' + f'{bytecode_panel_html}' + ) + + def _render_source_with_highlights(self, line_content: str, line_num: int, + filename: str, bytecode_data: list) -> str: + """Render source line with instruction highlight spans. + + Simple: collect ranges with sample counts, assign each byte position to + smallest covering range, then emit spans for contiguous runs with sample data. + """ + content = line_content.rstrip('\n') + if not content: + return '' + + # Collect all (start, end) -> {samples, opcodes} mapping from instructions + # Multiple instructions may share the same range, so we sum samples and collect opcodes + range_data = {} + for instr in bytecode_data: + samples = instr.get('samples', 0) + opname = instr.get('opname', '') + for loc in instr.get('locations', []): + if loc.get('end_lineno', line_num) == line_num: + start, end = loc.get('col_offset', -1), loc.get('end_col_offset', -1) + if start >= 0 and end >= 0: + key = (start, end) + if key not in range_data: + range_data[key] = {'samples': 0, 'opcodes': []} + range_data[key]['samples'] += samples + if opname and opname not in range_data[key]['opcodes']: + range_data[key]['opcodes'].append(opname) + + if not range_data: + return html.escape(content) + + # For each byte position, find the smallest covering range + byte_to_range = {} + for (start, end) in range_data.keys(): + for pos in range(start, end): + if pos not in byte_to_range: + byte_to_range[pos] = (start, end) + else: + # Keep smaller range + old_start, old_end = byte_to_range[pos] + if (end - start) < (old_end - old_start): + byte_to_range[pos] = (start, end) + + # Calculate totals for percentage and intensity + total_line_samples = sum(d['samples'] for d in range_data.values()) + max_range_samples = max(d['samples'] for d in range_data.values()) if range_data else 1 + + # Render character by character + result = [] + byte_offset = 0 + char_idx = 0 + current_range = None + span_chars = [] + + def flush_span(): + nonlocal span_chars, current_range + if span_chars: + text = html.escape(''.join(span_chars)) + if current_range: + data = range_data.get(current_range, {'samples': 0, 'opcodes': []}) + samples = data['samples'] + opcodes = ', '.join(data['opcodes'][:3]) # Top 3 opcodes + if len(data['opcodes']) > 3: + opcodes += f" +{len(data['opcodes']) - 3} more" + pct = int(100 * samples / total_line_samples) if total_line_samples > 0 else 0 + result.append(f'{text}') + else: + result.append(text) + span_chars = [] + + while char_idx < len(content): + char = content[char_idx] + char_bytes = len(char.encode('utf-8')) + char_range = byte_to_range.get(byte_offset) + + if char_range != current_range: + flush_span() + current_range = char_range + + span_chars.append(char) + byte_offset += char_bytes + char_idx += 1 + + flush_span() + return ''.join(result) + + def _format_specialization_color(self, spec_pct: int) -> str: + """Format specialization color based on percentage. + + Uses a gradient from gray (0%) through orange (50%) to green (100%). + """ + # Normalize to 0-1 + ratio = spec_pct / 100.0 + + if ratio >= 0.5: + # Orange to green (50-100%) + t = (ratio - 0.5) * 2 # 0 to 1 + r = int(255 * (1 - t)) # 255 -> 0 + g = int(180 + 75 * t) # 180 -> 255 + b = int(50 * (1 - t)) # 50 -> 0 + else: + # Gray to orange (0-50%) + t = ratio * 2 # 0 to 1 + r = int(158 + 97 * t) # 158 -> 255 + g = int(158 + 22 * t) # 158 -> 180 + b = int(158 - 108 * t) # 158 -> 50 + + alpha = 0.15 + 0.25 * ratio # 0.15 to 0.4 + return f"rgba({r}, {g}, {b}, {alpha})" + + def _build_navigation_buttons(self, filename: str, line_num: int) -> str: + """Build navigation buttons for callers/callees. + + - Callers: All lines in a function show who calls this function + - Callees: Only actual call site lines show what they call + """ + line_key = (filename, line_num) + + funcname = self.line_to_function.get(line_key) + + # Get callers: look up by function definition line, not current line + # This ensures all lines in a function show who calls this function + if funcname: + func_def_line = self.function_definitions.get((filename, funcname), line_num) + func_def_key = (filename, func_def_line) + caller_list = self._deduplicate_by_function(self.callers_graph.get(func_def_key, set())) + else: + caller_list = self._deduplicate_by_function(self.callers_graph.get(line_key, set())) + + # Get callees: only show for actual call site lines (not every line in function) + callee_list = self._deduplicate_by_function(self.call_graph.get(line_key, set())) + + # Get edge counts for each caller/callee + # For callers, use the function definition key for edge lookup + if funcname: + func_def_line = self.function_definitions.get((filename, funcname), line_num) + caller_edge_key = (filename, func_def_line) + else: + caller_edge_key = line_key + callers_with_counts = self._get_edge_counts(caller_edge_key, caller_list, is_caller=True) + # For callees, use the actual line key since that's where the call happens + callees_with_counts = self._get_edge_counts(line_key, callee_list, is_caller=False) + + # Build navigation buttons with counts + caller_btn = self._create_navigation_button(callers_with_counts, 'caller', '▲') + callee_btn = self._create_navigation_button(callees_with_counts, 'callee', '▼') + + if caller_btn or callee_btn: + return f'
    {caller_btn}{callee_btn}
    ' + return '' + + def _get_edge_counts(self, line_key: Tuple[str, int], + items: List[Tuple[str, int, str]], + is_caller: bool) -> List[Tuple[str, int, str, int]]: + """Get sample counts for each caller/callee edge.""" + result = [] + for file, line, func in items: + edge_line_key = (file, line) + if is_caller: + edge_key = (edge_line_key, line_key) + else: + edge_key = (line_key, edge_line_key) + + count = self.edge_samples.get(edge_key, 0) + result.append((file, line, func, count)) + + result.sort(key=lambda x: x[3], reverse=True) + return result + + def _deduplicate_by_function(self, items) -> List[Tuple[str, int, str]]: + """Remove duplicate entries based on (file, function) key. + + Args: + items: Iterable of (file, line, func) tuples (set or list) + """ + seen = {} + result = [] + for file, line, func in items: + key = (file, func) + if key not in seen: + seen[key] = True + result.append((file, line, func)) + return result + + def _create_navigation_button(self, items_with_counts: List[Tuple[str, int, str, int]], + btn_class: str, arrow: str) -> str: + """Create HTML for a navigation button with sample counts.""" + # Filter valid items + valid_items = [(f, l, fn, cnt) for f, l, fn, cnt in items_with_counts + if f in self.file_index and l > 0] + if not valid_items: + return "" + + if len(valid_items) == 1: + file, line, func, count = valid_items[0] + target_html = self.file_index[file] + nav_data = json.dumps({'link': f"{target_html}#line-{line}", 'func': func}) + title = f"Go to {btn_class}: {html.escape(func)} ({count:n} samples)" + return f'' + + # Multiple items - create menu + total_samples = sum(cnt for _, _, _, cnt in valid_items) + items_data = [ + { + 'file': os.path.basename(file), + 'func': func, + 'count': count, + 'link': f"{self.file_index[file]}#line-{line}" + } + for file, line, func, count in valid_items + ] + items_json = html.escape(json.dumps(items_data)) + title = f"{len(items_data)} {btn_class}s ({total_samples:n} samples)" + return f'' diff --git a/Lib/profiling/sampling/jsonl_collector.py b/Lib/profiling/sampling/jsonl_collector.py new file mode 100644 index 00000000000..41b0456d6f5 --- /dev/null +++ b/Lib/profiling/sampling/jsonl_collector.py @@ -0,0 +1,268 @@ +"""JSON Lines (JSONL) collector for the sampling profiler. + +Emits a normalized newline-delimited JSON record stream suitable for +programmatic consumption by external tools, scripts, and agents. Each line +is one JSON object; consumers can parse the file incrementally line by +line, but the producer writes the whole file at the end of the run (it is +not a live/streaming producer). + +Record schema +============= + +Every record is a JSON object with at least ``"type"``, ``"v"`` (record +schema version), and ``"run_id"`` (UUID4 hex tagging the run; allows +demultiplexing concatenated streams). Records appear in this fixed order: + +1. ``meta`` (exactly one, first line):: + + {"type":"meta","v":0,"run_id":"", + "sample_interval_usec":,"mode":"wall|cpu|gil|all|exception"} + + ``mode`` is omitted when not provided. + +2. ``string_table`` (zero or more):: + + {"type":"string_table","v":0,"run_id":"", + "strings":[{"str_id":,"value":""}, ...]} + + Strings (filenames, function names) are interned to keep repeated values + compact. IDs are zero-based. Each chunk holds up to ``_CHUNK_SIZE`` + entries, and each entry carries its explicit ``str_id`` so consumers do + not need to infer offsets across chunks. + +3. ``frame_table`` (zero or more):: + + {"type":"frame_table","v":0,"run_id":"", + "frames":[{"frame_id":,"path_str_id":,"func_str_id":, + "line":,"end_line":,"col":, + "end_col":}, ...]} + + ``end_line``/``col``/``end_col`` are *omitted* when source location data + is unavailable (a missing key means "not available", not zero or null). + ``line`` is ``0`` for synthetic frames (for example, internal marker + frames whose source location is None). Frame IDs are zero-based. + +4. ``agg`` (zero or more):: + + {"type":"agg","v":0,"run_id":"","kind":"frame","scope":"final", + "samples_total":, + "entries":[{"frame_id":,"self":,"cumulative":}, ...]} + + ``self`` counts samples where the frame was the leaf (currently + executing); ``cumulative`` counts samples where the frame appeared + anywhere in the stack (deduped per sample so recursion does not + double-count). ``samples_total`` is the run-wide total, repeated on + each chunk so a streaming consumer always knows the denominator. + +5. ``end`` (exactly one, last line):: + + {"type":"end","v":0,"run_id":"","samples_total":} + + Presence of ``end`` is the consumer's signal that the file is complete. + +Forward compatibility +===================== + +Consumers MUST ignore unknown record ``"type"`` values and unknown object +fields. New fields will be added by adding optional keys; an incompatible +schema change will bump the per-record ``"v"``. +""" + +from collections import Counter +import json +import uuid +from itertools import batched + +from .constants import PROFILING_MODE_NAMES +from .collector import normalize_location +from .stack_collector import StackTraceCollector + + +_CHUNK_SIZE = 256 +_SCHEMA_VERSION = 0 + + +class JsonlCollector(StackTraceCollector): + """Collector that exports finalized profiling data as JSONL. + + See the module docstring for the full record schema. The collector + accumulates samples in memory and writes the complete file at + ``export()`` time. + """ + + def __init__(self, sample_interval_usec, *, skip_idle=False, mode=None): + super().__init__(sample_interval_usec, skip_idle=skip_idle) + self.run_id = uuid.uuid4().hex + + self._string_to_id = {} + self._strings = [] + + self._frame_to_id = {} + self._frames = [] + + self._frame_self = Counter() + self._frame_cumulative = Counter() + self._samples_total = 0 + self._seen_frame_ids = set() + + self._mode = mode + + def process_frames(self, frames, _thread_id, weight=1): + self._samples_total += weight + self._seen_frame_ids.clear() + + for i, (filename, location, funcname, _opcode) in enumerate(frames): + frame_id = self._get_or_create_frame_id( + filename, location, funcname + ) + is_leaf = i == 0 + count_cumulative = frame_id not in self._seen_frame_ids + + if count_cumulative: + self._seen_frame_ids.add(frame_id) + + if is_leaf: + self._frame_self[frame_id] += weight + + if count_cumulative: + self._frame_cumulative[frame_id] += weight + + def export(self, filename): + with open(filename, "w", encoding="utf-8") as output: + self._write_message(output, self._build_meta_record()) + self._write_chunked_records( + output, + { + "type": "string_table", + "v": _SCHEMA_VERSION, + "run_id": self.run_id, + }, + "strings", + self._strings, + ) + self._write_chunked_records( + output, + { + "type": "frame_table", + "v": _SCHEMA_VERSION, + "run_id": self.run_id, + }, + "frames", + self._frames, + ) + self._write_chunked_records( + output, + { + "type": "agg", + "v": _SCHEMA_VERSION, + "run_id": self.run_id, + "kind": "frame", + "scope": "final", + "samples_total": self._samples_total, + }, + "entries", + self._iter_final_agg_entries(), + ) + self._write_message(output, self._build_end_record()) + print(f"JSONL profile written to {filename}") + return True + + def _build_meta_record(self): + record = { + "type": "meta", + "v": _SCHEMA_VERSION, + "run_id": self.run_id, + "sample_interval_usec": self.sample_interval_usec, + } + + if self._mode is not None: + record["mode"] = PROFILING_MODE_NAMES.get( + self._mode, str(self._mode) + ) + + return record + + def _build_end_record(self): + record = { + "type": "end", + "v": _SCHEMA_VERSION, + "run_id": self.run_id, + "samples_total": self._samples_total, + } + + return record + + def _iter_final_agg_entries(self): + for frame_record in self._frames: + frame_id = frame_record["frame_id"] + yield { + "frame_id": frame_id, + "self": self._frame_self[frame_id], + "cumulative": self._frame_cumulative[frame_id], + } + + def _get_or_create_frame_id(self, filename, location, funcname): + location_fields = self._location_to_export_fields(location) + func_str_id = self._intern_string(funcname) + path_str_id = self._intern_string(filename) + + frame_key = ( + path_str_id, + func_str_id, + location_fields["line"], + location_fields.get("end_line"), + location_fields.get("col"), + location_fields.get("end_col"), + ) + + if (frame_id := self._frame_to_id.get(frame_key)) is not None: + return frame_id + + frame_id = len(self._frames) + frame_record = { + "frame_id": frame_id, + "path_str_id": path_str_id, + "func_str_id": func_str_id, + **location_fields, + } + + self._frame_to_id[frame_key] = frame_id + self._frames.append(frame_record) + return frame_id + + def _intern_string(self, value): + value = str(value) + + if (string_id := self._string_to_id.get(value)) is not None: + return string_id + + string_id = len(self._strings) + self._string_to_id[value] = string_id + self._strings.append({"str_id": string_id, "value": value}) + return string_id + + @staticmethod + def _location_to_export_fields(location): + lineno, end_lineno, col_offset, end_col_offset = normalize_location( + location + ) + + fields = {"line": lineno} + if end_lineno > 0: + fields["end_line"] = end_lineno + if col_offset >= 0: + fields["col"] = col_offset + if end_col_offset >= 0: + fields["end_col"] = end_col_offset + return fields + + def _write_chunked_records( + self, output, base_record, chunk_field, entries + ): + for chunk in batched(entries, _CHUNK_SIZE): + self._write_message(output, {**base_record, chunk_field: chunk}) + + @staticmethod + def _write_message(output, record): + output.write(json.dumps(record, separators=(",", ":"))) + output.write("\n") diff --git a/Lib/profiling/sampling/live_collector/__init__.py b/Lib/profiling/sampling/live_collector/__init__.py new file mode 100644 index 00000000000..59d50955e52 --- /dev/null +++ b/Lib/profiling/sampling/live_collector/__init__.py @@ -0,0 +1,200 @@ +"""Live profiling collector that displays top-like statistics using curses. + + ┌─────────────────────────────┐ + │ Target Python Process │ + │ (being profiled) │ + └──────────────┬──────────────┘ + │ Stack sampling at + │ configured interval + │ (e.g., 10000µs) + ▼ + ┌─────────────────────────────┐ + │ LiveStatsCollector │ + │ ┌───────────────────────┐ │ + │ │ collect() │ │ Aggregates samples + │ │ - Iterates frames │ │ into statistics + │ │ - Updates counters │ │ + │ └───────────┬───────────┘ │ + │ │ │ + │ ▼ │ + │ ┌───────────────────────┐ │ + │ │ Data Storage │ │ + │ │ - result dict │ │ Tracks per-function: + │ │ - direct_calls │ │ • Direct samples + │ │ - cumulative_calls │ │ • Cumulative samples + │ └───────────┬───────────┘ │ • Derived time stats + │ │ │ + │ ▼ │ + │ ┌───────────────────────┐ │ + │ │ Display Update │ │ + │ │ (10Hz by default) │ │ Rate-limited refresh + │ └───────────┬───────────┘ │ + └──────────────┼──────────────┘ + │ + ▼ + ┌─────────────────────────────┐ + │ DisplayInterface │ + │ (Abstract layer) │ + └──────────────┬──────────────┘ + ┌───────┴────────┐ + │ │ + ┌──────────▼────────┐ ┌───▼──────────┐ + │ CursesDisplay │ │ MockDisplay │ + │ - Real terminal │ │ - Testing │ + │ - ncurses backend │ │ - No UI │ + └─────────┬─────────┘ └──────────────┘ + │ + ▼ + ┌─────────────────────────────────────┐ + │ Widget-Based Rendering │ + │ ┌─────────────────────────────────┐ │ + │ │ HeaderWidget │ │ + │ │ • PID, uptime, time, interval │ │ + │ │ • Sample stats & progress bar │ │ + │ │ • Efficiency bar │ │ + │ │ • Thread status & GC stats │ │ + │ │ • Function summary │ │ + │ │ • Top 3 hottest functions │ │ + │ ├─────────────────────────────────┤ │ + │ │ TableWidget │ │ + │ │ • Column headers (sortable) │ │ Interactive display + │ │ • Stats rows (scrolling) │ │ with keyboard controls: + │ │ - nsamples % time │ │ s: sort, p: pause + │ │ - function file:line │ │ r: reset, /: filter + │ ├─────────────────────────────────┤ │ q: quit, h: help + │ │ FooterWidget │ │ + │ │ • Legend and status │ │ + │ │ • Filter input prompt │ │ + │ └─────────────────────────────────┘ │ + └─────────────────────────────────────┘ + +Architecture: + +The live collector is organized into four layers. The data collection layer +(LiveStatsCollector) aggregates stack samples into per-function statistics without +any knowledge of how they will be presented. The display abstraction layer +(DisplayInterface) defines rendering operations without coupling to curses or any +specific UI framework. The widget layer (Widget, HeaderWidget, TableWidget, +FooterWidget, HelpWidget, ProgressBarWidget) encapsulates individual UI components +with their own rendering logic, promoting modularity and reusability. The +presentation layer (CursesDisplay/MockDisplay) implements the actual rendering for +terminal output and testing. + +The system runs two independent update loops. The sampling loop is driven by the +profiler at the configured interval (e.g., 10000µs) and continuously collects +stack frames and updates statistics. The display loop runs at a fixed refresh rate +(default 10Hz) and updates the terminal independently of sampling frequency. This +separation allows high-frequency sampling without overwhelming the terminal with +constant redraws. + +Statistics are computed incrementally as samples arrive. The collector maintains +running counters (direct calls and cumulative calls) in a dictionary keyed by +function location. Derived metrics like time estimates and percentages are computed +on-demand during display updates rather than being stored, which minimizes memory +overhead as the number of tracked functions grows. + +User input is processed asynchronously during display updates using non-blocking I/O. +This allows interactive controls (sorting, filtering, pausing) without interrupting +the data collection pipeline. The collector maintains mode flags (paused, +filter_input_mode) that affect what gets displayed but not what gets collected. + +""" + +# Re-export all public classes and constants for backward compatibility +from .collector import LiveStatsCollector +from .display import DisplayInterface, CursesDisplay, MockDisplay +from .widgets import ( + Widget, + ProgressBarWidget, + HeaderWidget, + TableWidget, + FooterWidget, + HelpWidget, +) +from .constants import ( + MICROSECONDS_PER_SECOND, + DISPLAY_UPDATE_HZ, + DISPLAY_UPDATE_INTERVAL_SEC, + MIN_TERMINAL_WIDTH, + MIN_TERMINAL_HEIGHT, + WIDTH_THRESHOLD_SAMPLE_PCT, + WIDTH_THRESHOLD_TOTTIME, + WIDTH_THRESHOLD_CUMUL_PCT, + WIDTH_THRESHOLD_CUMTIME, + HEADER_LINES, + FOOTER_LINES, + SAFETY_MARGIN, + TOP_FUNCTIONS_DISPLAY_COUNT, + COL_WIDTH_NSAMPLES, + COL_SPACING, + COL_WIDTH_SAMPLE_PCT, + COL_WIDTH_TIME, + MIN_FUNC_NAME_WIDTH, + MAX_FUNC_NAME_WIDTH, + MIN_AVAILABLE_SPACE, + MIN_BAR_WIDTH, + MAX_SAMPLE_RATE_BAR_WIDTH, + MAX_EFFICIENCY_BAR_WIDTH, + MIN_SAMPLE_RATE_FOR_SCALING, + FINISHED_BANNER_EXTRA_LINES, + COLOR_PAIR_HEADER_BG, + COLOR_PAIR_CYAN, + COLOR_PAIR_YELLOW, + COLOR_PAIR_GREEN, + COLOR_PAIR_MAGENTA, + COLOR_PAIR_RED, + COLOR_PAIR_SORTED_HEADER, + DEFAULT_SORT_BY, + DEFAULT_DISPLAY_LIMIT, +) + +__all__ = [ + # Main collector + "LiveStatsCollector", + # Display interfaces + "DisplayInterface", + "CursesDisplay", + "MockDisplay", + # Widgets + "Widget", + "ProgressBarWidget", + "HeaderWidget", + "TableWidget", + "FooterWidget", + "HelpWidget", + # Constants + "MICROSECONDS_PER_SECOND", + "DISPLAY_UPDATE_HZ", + "DISPLAY_UPDATE_INTERVAL_SEC", + "MIN_TERMINAL_WIDTH", + "MIN_TERMINAL_HEIGHT", + "WIDTH_THRESHOLD_SAMPLE_PCT", + "WIDTH_THRESHOLD_TOTTIME", + "WIDTH_THRESHOLD_CUMUL_PCT", + "WIDTH_THRESHOLD_CUMTIME", + "HEADER_LINES", + "FOOTER_LINES", + "SAFETY_MARGIN", + "TOP_FUNCTIONS_DISPLAY_COUNT", + "COL_WIDTH_NSAMPLES", + "COL_SPACING", + "COL_WIDTH_SAMPLE_PCT", + "COL_WIDTH_TIME", + "MIN_FUNC_NAME_WIDTH", + "MAX_FUNC_NAME_WIDTH", + "MIN_AVAILABLE_SPACE", + "MIN_BAR_WIDTH", + "MAX_SAMPLE_RATE_BAR_WIDTH", + "MAX_EFFICIENCY_BAR_WIDTH", + "MIN_SAMPLE_RATE_FOR_SCALING", + "FINISHED_BANNER_EXTRA_LINES", + "COLOR_PAIR_HEADER_BG", + "COLOR_PAIR_CYAN", + "COLOR_PAIR_YELLOW", + "COLOR_PAIR_GREEN", + "COLOR_PAIR_MAGENTA", + "COLOR_PAIR_RED", + "COLOR_PAIR_SORTED_HEADER", + "DEFAULT_SORT_BY", + "DEFAULT_DISPLAY_LIMIT", +] diff --git a/Lib/profiling/sampling/live_collector/collector.py b/Lib/profiling/sampling/live_collector/collector.py new file mode 100644 index 00000000000..c5bd0178f26 --- /dev/null +++ b/Lib/profiling/sampling/live_collector/collector.py @@ -0,0 +1,1112 @@ +"""LiveStatsCollector - Main collector class for live profiling.""" + +import collections +import contextlib +import curses +from dataclasses import dataclass, field +import os +import site +import sys +import sysconfig +import time +lazy import _colorize + +from ..collector import Collector, extract_lineno +from ..constants import ( + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_ON_CPU, + THREAD_STATUS_UNKNOWN, + THREAD_STATUS_GIL_REQUESTED, + THREAD_STATUS_HAS_EXCEPTION, + PROFILING_MODE_CPU, + PROFILING_MODE_GIL, + PROFILING_MODE_WALL, +) +from .constants import ( + MICROSECONDS_PER_SECOND, + DISPLAY_UPDATE_INTERVAL_SEC, + MIN_TERMINAL_WIDTH, + MIN_TERMINAL_HEIGHT, + HEADER_LINES, + FOOTER_LINES, + SAFETY_MARGIN, + FINISHED_BANNER_EXTRA_LINES, + DEFAULT_SORT_BY, + DEFAULT_DISPLAY_LIMIT, + COLOR_PAIR_SAMPLES, + COLOR_PAIR_FILE, + COLOR_PAIR_FUNC, + COLOR_PAIR_HEADER_BG, + COLOR_PAIR_CYAN, + COLOR_PAIR_YELLOW, + COLOR_PAIR_GREEN, + COLOR_PAIR_MAGENTA, + COLOR_PAIR_RED, + COLOR_PAIR_SORTED_HEADER, +) +from .display import CursesDisplay +from .widgets import HeaderWidget, TableWidget, FooterWidget, HelpWidget, OpcodePanel +from .trend_tracker import TrendTracker + + +@dataclass +class ThreadData: + """Encapsulates all profiling data for a single thread.""" + + thread_id: int + + # Function call statistics: {location: {direct_calls: int, cumulative_calls: int}} + result: dict = field(default_factory=lambda: collections.defaultdict( + lambda: dict(direct_calls=0, cumulative_calls=0) + )) + + # Thread status statistics + has_gil: int = 0 + on_cpu: int = 0 + gil_requested: int = 0 + unknown: int = 0 + has_exception: int = 0 + total: int = 0 # Total status samples for this thread + + # Sample counts + sample_count: int = 0 + gc_frame_samples: int = 0 + + # Opcode statistics: {location: {opcode: count}} + opcode_stats: dict = field(default_factory=lambda: collections.defaultdict( + lambda: collections.defaultdict(int) + )) + + def increment_status_flag(self, status_flags): + """Update status counts based on status bit flags.""" + if status_flags & THREAD_STATUS_HAS_GIL: + self.has_gil += 1 + if status_flags & THREAD_STATUS_ON_CPU: + self.on_cpu += 1 + if status_flags & THREAD_STATUS_GIL_REQUESTED: + self.gil_requested += 1 + if status_flags & THREAD_STATUS_UNKNOWN: + self.unknown += 1 + if status_flags & THREAD_STATUS_HAS_EXCEPTION: + self.has_exception += 1 + self.total += 1 + + def as_status_dict(self): + """Return status counts as a dict for compatibility.""" + return { + "has_gil": self.has_gil, + "on_cpu": self.on_cpu, + "gil_requested": self.gil_requested, + "unknown": self.unknown, + "has_exception": self.has_exception, + "total": self.total, + } + + +class LiveStatsCollector(Collector): + """Collector that displays live top-like statistics using ncurses.""" + + def __init__( + self, + sample_interval_usec, + *, + skip_idle=False, + sort_by=DEFAULT_SORT_BY, + limit=DEFAULT_DISPLAY_LIMIT, + pid=None, + display=None, + mode=None, + opcodes=False, + async_aware=None, + ): + """ + Initialize the live stats collector. + + Args: + sample_interval_usec: Sampling interval in microseconds + skip_idle: Whether to skip idle threads + sort_by: Sort key ('tottime', 'nsamples', 'cumtime', 'sample_pct', 'cumul_pct') + limit: Maximum number of functions to display + pid: Process ID being profiled + display: DisplayInterface implementation (None means curses will be used) + mode: Profiling mode ('cpu', 'gil', etc.) - affects what stats are shown + opcodes: Whether to show opcode panel (requires --opcodes flag) + async_aware: Async tracing mode - None (sync only), "all" or "running" + """ + self.result = collections.defaultdict( + lambda: dict(total_rec_calls=0, direct_calls=0, cumulative_calls=0) + ) + self.sample_interval_usec = sample_interval_usec + self.sample_interval_sec = ( + sample_interval_usec / MICROSECONDS_PER_SECOND + ) + self.skip_idle = skip_idle + self.sort_by = sort_by + self.limit = limit + self.total_samples = 0 + self.start_time = None + self.stdscr = None + self.display = display # DisplayInterface implementation + self.running = True + self.pid = pid + self.mode = mode # Profiling mode + self.async_aware = async_aware # Async tracing mode + # Pre-select frame iterator method to avoid per-call dispatch overhead + self._get_frame_iterator = self._get_async_frame_iterator if async_aware else self._get_sync_frame_iterator + self._saved_stdout = None + self._saved_stderr = None + self._devnull = None + self._last_display_update = None + self.max_sample_rate = 0 # Track maximum sample rate seen + self.successful_samples = 0 # Track samples that captured frames + self.failed_samples = 0 # Track samples that failed to capture frames + self.display_update_interval_sec = DISPLAY_UPDATE_INTERVAL_SEC # Instance variable for display refresh rate + + # Thread status statistics (bit flags) + self.thread_status_counts = { + "has_gil": 0, + "on_cpu": 0, + "gil_requested": 0, + "unknown": 0, + "has_exception": 0, + "total": 0, # Total thread count across all samples + } + self.gc_frame_samples = 0 # Track samples with GC frames + + # Opcode statistics: {location: {opcode: count}} + self.opcode_stats = collections.defaultdict(lambda: collections.defaultdict(int)) + self.show_opcodes = opcodes # Show opcode panel when --opcodes flag is passed + self.selected_row = 0 # Currently selected row in table for opcode view + self.scroll_offset = 0 # Scroll offset for table when in opcode mode + + # Interactive controls state + self.paused = False # Pause UI updates (profiling continues) + self.show_help = False # Show help screen + self.filter_pattern = None # Glob pattern to filter functions + self.filter_input_mode = False # Currently entering filter text + self.filter_input_buffer = "" # Buffer for filter input + self.finished = False # Program has finished, showing final state + self.finish_timestamp = None # When profiling finished (for time freezing) + self.finish_wall_time = None # Wall clock time when profiling finished + + # Thread tracking state + self.thread_ids = [] # List of thread IDs seen + self.view_mode = "ALL" # "ALL" or "PER_THREAD" + self.current_thread_index = ( + 0 # Index into thread_ids when in PER_THREAD mode + ) + self.per_thread_data = {} # {thread_id: ThreadData} + + # Calculate common path prefixes to strip + self._path_prefixes = self._get_common_path_prefixes() + + # Widgets (initialized when display is available) + self.header_widget = None + self.table_widget = None + self.footer_widget = None + self.help_widget = None + self.opcode_panel = None + + # Color mode + self._can_colorize = _colorize.can_colorize() + + # Trend tracking (initialized after colors are set up) + self._trend_tracker = None + + self._seen_locations = set() + + @property + def elapsed_time(self): + """Get the elapsed time, frozen when finished.""" + if self.finished and self.finish_timestamp is not None: + # Handle case where process exited before any samples were collected + if self.start_time is None: + return 0 + return self.finish_timestamp - self.start_time + return time.perf_counter() - self.start_time if self.start_time else 0 + + @property + def current_time_display(self): + """Get the current time for display, frozen when finished.""" + if self.finished and self.finish_wall_time is not None: + return time.strftime("%H:%M:%S", time.localtime(self.finish_wall_time)) + return time.strftime("%H:%M:%S") + + def _get_or_create_thread_data(self, thread_id): + """Get or create ThreadData for a thread ID.""" + if thread_id not in self.per_thread_data: + self.per_thread_data[thread_id] = ThreadData(thread_id=thread_id) + return self.per_thread_data[thread_id] + + def _get_current_thread_data(self): + """Get ThreadData for currently selected thread in PER_THREAD mode.""" + if self.view_mode == "PER_THREAD" and self.current_thread_index < len(self.thread_ids): + thread_id = self.thread_ids[self.current_thread_index] + return self.per_thread_data.get(thread_id) + return None + + def _get_current_result_source(self): + """Get result dict for current view mode (aggregated or per-thread).""" + if self.view_mode == "ALL": + return self.result + thread_data = self._get_current_thread_data() + return thread_data.result if thread_data else {} + + def _get_common_path_prefixes(self): + """Get common path prefixes to strip from file paths.""" + prefixes = [] + + # Get the actual stdlib location from the os module + # This works for both installed Python and development builds + os_module_file = os.__file__ + if os_module_file: + # os.__file__ points to os.py, get its directory + stdlib_dir = os.path.dirname(os.path.abspath(os_module_file)) + prefixes.append(stdlib_dir) + + # Get stdlib location from sysconfig (may be different or same) + stdlib_path = sysconfig.get_path("stdlib") + if stdlib_path: + prefixes.append(stdlib_path) + + # Get platstdlib location (platform-specific stdlib) + platstdlib_path = sysconfig.get_path("platstdlib") + if platstdlib_path: + prefixes.append(platstdlib_path) + + # Get site-packages locations + for site_path in site.getsitepackages(): + prefixes.append(site_path) + + # Also check user site-packages + user_site = site.getusersitepackages() + if user_site: + prefixes.append(user_site) + + # Remove duplicates and sort by length (longest first) to match most specific paths first + prefixes = list(set(prefixes)) + prefixes.sort(key=lambda x: len(x), reverse=True) + + return prefixes + + def simplify_path(self, filepath): + """Simplify a file path by removing common prefixes.""" + # Try to match against known prefixes + for prefix_path in self._path_prefixes: + if filepath.startswith(prefix_path): + # Remove the prefix completely + relative = filepath[len(prefix_path) :].lstrip(os.sep) + return relative + + # If no match, return the original path + return filepath + + def process_frames(self, frames, thread_id=None): + """Process a single thread's frame stack. + + Args: + frames: List of frame information + thread_id: Thread ID for per-thread tracking (optional) + """ + if not frames: + return + + # Get per-thread data if tracking per-thread + thread_data = self._get_or_create_thread_data(thread_id) if thread_id is not None else None + self._seen_locations.clear() + + # Process each frame in the stack to track cumulative calls + # frame.location is (lineno, end_lineno, col_offset, end_col_offset), int, or None + for frame in frames: + lineno = extract_lineno(frame.location) + location = (frame.filename, lineno, frame.funcname) + if location not in self._seen_locations: + self._seen_locations.add(location) + self.result[location]["cumulative_calls"] += 1 + if thread_data: + thread_data.result[location]["cumulative_calls"] += 1 + + # The top frame gets counted as an inline call (directly executing) + top_frame = frames[0] + top_lineno = extract_lineno(top_frame.location) + top_location = (top_frame.filename, top_lineno, top_frame.funcname) + self.result[top_location]["direct_calls"] += 1 + if thread_data: + thread_data.result[top_location]["direct_calls"] += 1 + + # Track opcode for top frame (the actively executing instruction) + opcode = getattr(top_frame, 'opcode', None) + if opcode is not None: + self.opcode_stats[top_location][opcode] += 1 + if thread_data: + thread_data.opcode_stats[top_location][opcode] += 1 + + def _get_sync_frame_iterator(self, stack_frames): + """Iterator for sync frames.""" + return self._iter_all_frames(stack_frames, skip_idle=self.skip_idle) + + def _get_async_frame_iterator(self, stack_frames): + """Iterator for async frames, yielding (frames, thread_id) tuples.""" + for frames, thread_id, task_id in self._iter_async_frames(stack_frames): + yield frames, thread_id + + def collect_failed_sample(self): + self.failed_samples += 1 + self.total_samples += 1 + + def collect(self, stack_frames, timestamp_us=None): + """Collect and display profiling data.""" + if self.start_time is None: + self.start_time = time.perf_counter() + self._last_display_update = self.start_time + + has_gc_frame = False + + # Collect thread status stats (only available in sync mode) + if not self.async_aware: + status_counts, sample_has_gc, per_thread_stats = self._collect_thread_status_stats(stack_frames) + for key, count in status_counts.items(): + self.thread_status_counts[key] += count + if sample_has_gc: + has_gc_frame = True + + for thread_id, stats in per_thread_stats.items(): + thread_data = self._get_or_create_thread_data(thread_id) + thread_data.has_gil += stats.get("has_gil", 0) + thread_data.on_cpu += stats.get("on_cpu", 0) + thread_data.gil_requested += stats.get("gil_requested", 0) + thread_data.unknown += stats.get("unknown", 0) + thread_data.has_exception += stats.get("has_exception", 0) + thread_data.total += stats.get("total", 0) + if stats.get("gc_samples", 0): + thread_data.gc_frame_samples += stats["gc_samples"] + + # Process frames using pre-selected iterator + frames_processed = False + for frames, thread_id in self._get_frame_iterator(stack_frames): + if not frames: + continue + + self.process_frames(frames, thread_id=thread_id) + frames_processed = True + + # Track thread IDs + if thread_id is not None and thread_id not in self.thread_ids: + self.thread_ids.append(thread_id) + + if thread_id is not None: + thread_data = self._get_or_create_thread_data(thread_id) + thread_data.sample_count += 1 + + if has_gc_frame: + self.gc_frame_samples += 1 + + # Count as successful - the sample worked even if no frames matched the filter + # (e.g., in --mode exception when no thread has an active exception) + self.successful_samples += 1 + self.total_samples += 1 + + # Handle input on every sample for instant responsiveness + if self.display is not None: + self._handle_input() + + # Update display at configured rate if display is initialized and not paused + if self.display is not None and not self.paused: + current_time = time.perf_counter() + if ( + self._last_display_update is None + or (current_time - self._last_display_update) + >= self.display_update_interval_sec + ): + self._update_display() + self._last_display_update = current_time + + def _prepare_display_data(self, height): + """Prepare data for display rendering.""" + elapsed = self.elapsed_time + stats_list = self.build_stats_list() + + # Calculate available space for stats + # Add extra lines for finished banner when in finished state + extra_header_lines = ( + FINISHED_BANNER_EXTRA_LINES if self.finished else 0 + ) + max_stats_lines = max( + 0, + height + - HEADER_LINES + - extra_header_lines + - FOOTER_LINES + - SAFETY_MARGIN, + ) + stats_list = stats_list[:max_stats_lines] + + return elapsed, stats_list + + def _initialize_widgets(self, colors): + """Initialize widgets with display and colors.""" + if self.header_widget is None: + # Initialize trend tracker with colors + if self._trend_tracker is None: + self._trend_tracker = TrendTracker(colors, enabled=True) + + self.header_widget = HeaderWidget(self.display, colors, self) + self.table_widget = TableWidget(self.display, colors, self) + self.footer_widget = FooterWidget(self.display, colors, self) + self.help_widget = HelpWidget(self.display, colors) + self.opcode_panel = OpcodePanel(self.display, colors, self) + + def _render_display_sections( + self, height, width, elapsed, stats_list, colors + ): + """Render all display sections to the screen.""" + line = 0 + try: + # Initialize widgets if not already done + self._initialize_widgets(colors) + + # Render header + line = self.header_widget.render( + line, width, elapsed=elapsed, stats_list=stats_list + ) + + # Render table + line = self.table_widget.render( + line, width, height=height, stats_list=stats_list + ) + + # Render opcode panel if enabled + if self.show_opcodes: + line = self.opcode_panel.render( + line, width, height=height, stats_list=stats_list + ) + + except curses.error: + pass + + def _update_display(self): + """Update the display with current stats.""" + try: + # Clear screen and get dimensions + self.display.clear() + height, width = self.display.get_dimensions() + + # Check terminal size + if width < MIN_TERMINAL_WIDTH or height < MIN_TERMINAL_HEIGHT: + self._show_terminal_too_small(height, width) + self.display.refresh() + return + + # Setup colors and initialize widgets (needed for both help and normal display) + colors = self._setup_colors() + self._initialize_widgets(colors) + + # Show help screen if requested + if self.show_help: + self.help_widget.render(0, width, height=height) + self.display.refresh() + return + + # Prepare data + elapsed, stats_list = self._prepare_display_data(height) + + # Render all sections + self._render_display_sections( + height, width, elapsed, stats_list, colors + ) + + # Footer + self.footer_widget.render(height - 2, width) + + # Show filter input prompt if in filter input mode + if self.filter_input_mode: + self.footer_widget.render_filter_input_prompt( + height - 1, width + ) + + # Refresh display + self.display.redraw() + self.display.refresh() + + except Exception: + pass + + def _cycle_sort(self, reverse=False): + """Cycle through different sort modes in column order. + + Args: + reverse: If True, cycle backwards (right to left), otherwise forward (left to right) + """ + sort_modes = [ + "nsamples", + "sample_pct", + "tottime", + "cumul_pct", + "cumtime", + ] + try: + current_idx = sort_modes.index(self.sort_by) + if reverse: + self.sort_by = sort_modes[(current_idx - 1) % len(sort_modes)] + else: + self.sort_by = sort_modes[(current_idx + 1) % len(sort_modes)] + except ValueError: + self.sort_by = "nsamples" + + def _setup_colors(self): + """Set up color pairs and return color attributes.""" + A_BOLD = self.display.get_attr("A_BOLD") + A_REVERSE = self.display.get_attr("A_REVERSE") + A_UNDERLINE = self.display.get_attr("A_UNDERLINE") + A_NORMAL = self.display.get_attr("A_NORMAL") + + if self.display.has_colors() and self._can_colorize: + with contextlib.suppress(Exception): + theme = _colorize.get_theme(force_color=True).live_profiler + default_bg = -1 + + self.display.init_color_pair(COLOR_PAIR_SAMPLES, theme.samples_fg, default_bg) + self.display.init_color_pair(COLOR_PAIR_FILE, theme.file_fg, default_bg) + self.display.init_color_pair(COLOR_PAIR_FUNC, theme.func_fg, default_bg) + + # Normal header background color pair + self.display.init_color_pair( + COLOR_PAIR_HEADER_BG, + theme.normal_header_fg, + theme.normal_header_bg, + ) + + self.display.init_color_pair(COLOR_PAIR_CYAN, theme.pid_fg, default_bg) + self.display.init_color_pair(COLOR_PAIR_YELLOW, theme.time_fg, default_bg) + self.display.init_color_pair(COLOR_PAIR_GREEN, theme.uptime_fg, default_bg) + self.display.init_color_pair(COLOR_PAIR_MAGENTA, theme.interval_fg, default_bg) + self.display.init_color_pair(COLOR_PAIR_RED, theme.off_gil_fg, default_bg) + self.display.init_color_pair( + COLOR_PAIR_SORTED_HEADER, + theme.sorted_header_fg, + theme.sorted_header_bg, + ) + + TREND_UP_PAIR = 11 + TREND_DOWN_PAIR = 12 + self.display.init_color_pair(TREND_UP_PAIR, theme.trend_up_fg, default_bg) + self.display.init_color_pair(TREND_DOWN_PAIR, theme.trend_down_fg, default_bg) + + return { + "header": self.display.get_color_pair(COLOR_PAIR_HEADER_BG) | A_BOLD, + "cyan": self.display.get_color_pair(COLOR_PAIR_CYAN) | A_BOLD, + "yellow": self.display.get_color_pair(COLOR_PAIR_YELLOW) | A_BOLD, + "green": self.display.get_color_pair(COLOR_PAIR_GREEN) | A_BOLD, + "magenta": self.display.get_color_pair(COLOR_PAIR_MAGENTA) | A_BOLD, + "red": self.display.get_color_pair(COLOR_PAIR_RED) | A_BOLD, + "sorted_header": self.display.get_color_pair(COLOR_PAIR_SORTED_HEADER) | A_BOLD, + "normal_header": self.display.get_color_pair(COLOR_PAIR_HEADER_BG) | A_BOLD, + "color_samples": self.display.get_color_pair(COLOR_PAIR_SAMPLES), + "color_file": self.display.get_color_pair(COLOR_PAIR_FILE), + "color_func": self.display.get_color_pair(COLOR_PAIR_FUNC), + "trend_up": self.display.get_color_pair(TREND_UP_PAIR) | A_BOLD, + "trend_down": self.display.get_color_pair(TREND_DOWN_PAIR) | A_BOLD, + "trend_stable": A_NORMAL, + } + + # Fallback for no-color mode + return { + "header": A_REVERSE | A_BOLD, + "cyan": A_BOLD, + "yellow": A_BOLD, + "green": A_BOLD, + "magenta": A_BOLD, + "red": A_BOLD, + "sorted_header": A_REVERSE | A_BOLD | A_UNDERLINE, + "normal_header": A_REVERSE | A_BOLD, + "color_samples": A_NORMAL, + "color_file": A_NORMAL, + "color_func": A_NORMAL, + "trend_up": A_BOLD, + "trend_down": A_BOLD, + "trend_stable": A_NORMAL, + } + + def build_stats_list(self): + """Build and sort the statistics list.""" + stats_list = [] + result_source = self._get_current_result_source() + + for func, call_counts in result_source.items(): + # Apply filter if set (using substring matching) + if self.filter_pattern: + filename, lineno, funcname = func + # Simple substring match (case-insensitive) + pattern_lower = self.filter_pattern.lower() + filename_lower = filename.lower() + funcname_lower = funcname.lower() + + # Match if pattern is substring of filename, funcname, or combined + matched = ( + pattern_lower in filename_lower + or pattern_lower in funcname_lower + or pattern_lower in f"{filename_lower}:{funcname_lower}" + ) + if not matched: + continue + + direct_calls = call_counts.get("direct_calls", 0) + cumulative_calls = call_counts.get("cumulative_calls", 0) + total_time = direct_calls * self.sample_interval_sec + cumulative_time = cumulative_calls * self.sample_interval_sec + + # Calculate sample percentages using successful_samples as denominator + # This ensures percentages are relative to samples that actually had data, + # not all sampling attempts (important for filtered modes like --mode exception) + sample_pct = (direct_calls / self.successful_samples * 100) if self.successful_samples > 0 else 0 + cumul_pct = (cumulative_calls / self.successful_samples * 100) if self.successful_samples > 0 else 0 + + # Calculate trends for all columns using TrendTracker + trends = {} + if self._trend_tracker is not None: + trends = self._trend_tracker.update_metrics( + func, + { + 'nsamples': direct_calls, + 'tottime': total_time, + 'cumtime': cumulative_time, + 'sample_pct': sample_pct, + 'cumul_pct': cumul_pct, + } + ) + + stats_list.append( + { + "func": func, + "direct_calls": direct_calls, + "cumulative_calls": cumulative_calls, + "total_time": total_time, + "cumulative_time": cumulative_time, + "sample_pct": sample_pct, + "cumul_pct": cumul_pct, + "trends": trends, + } + ) + + # Sort the stats + if self.sort_by == "nsamples": + stats_list.sort(key=lambda x: x["direct_calls"], reverse=True) + elif self.sort_by == "tottime": + stats_list.sort(key=lambda x: x["total_time"], reverse=True) + elif self.sort_by == "cumtime": + stats_list.sort(key=lambda x: x["cumulative_time"], reverse=True) + elif self.sort_by == "sample_pct": + stats_list.sort(key=lambda x: x["sample_pct"], reverse=True) + elif self.sort_by == "cumul_pct": + stats_list.sort(key=lambda x: x["cumul_pct"], reverse=True) + + return stats_list + + def reset_stats(self): + """Reset all collected statistics.""" + self.result.clear() + self.opcode_stats.clear() + self.per_thread_data.clear() + self.thread_ids.clear() + self.view_mode = "ALL" + self.current_thread_index = 0 + self.total_samples = 0 + self.successful_samples = 0 + self.failed_samples = 0 + self.max_sample_rate = 0 + self.thread_status_counts = { + "has_gil": 0, + "on_cpu": 0, + "gil_requested": 0, + "unknown": 0, + "has_exception": 0, + "total": 0, + } + self.gc_frame_samples = 0 + # Clear trend tracking + if self._trend_tracker is not None: + self._trend_tracker.clear() + # Reset finished state and finish timestamp + self.finished = False + self.finish_timestamp = None + self.finish_wall_time = None + self.start_time = time.perf_counter() + self._last_display_update = self.start_time + + def mark_finished(self): + """Mark the profiling session as finished.""" + self.finished = True + # Capture the finish timestamp to freeze all timing displays + self.finish_timestamp = time.perf_counter() + self.finish_wall_time = time.time() # Wall clock time for display + # Force a final display update to show the finished message + if self.display is not None: + self._update_display() + + def _handle_finished_input_update(self, had_input): + """Update display after input when program is finished.""" + if self.finished and had_input and self.display is not None: + self._update_display() + + def _get_visible_rows_info(self): + """Calculate visible rows and stats list for opcode navigation.""" + stats_list = self.build_stats_list() + if self.display: + height, _ = self.display.get_dimensions() + extra_header = FINISHED_BANNER_EXTRA_LINES if self.finished else 0 + max_stats = max(0, height - HEADER_LINES - extra_header - FOOTER_LINES - SAFETY_MARGIN) + stats_list = stats_list[:max_stats] + visible_rows = max(1, height - 8 - 2 - 12) + else: + visible_rows = self.limit + total_rows = len(stats_list) + return stats_list, visible_rows, total_rows + + def _move_selection_down(self): + """Move selection down in opcode mode with scrolling.""" + if not self.show_opcodes: + return + + stats_list, visible_rows, total_rows = self._get_visible_rows_info() + if total_rows == 0: + return + + # Max scroll is when last item is at bottom + max_scroll = max(0, total_rows - visible_rows) + # Current absolute position + abs_pos = self.scroll_offset + self.selected_row + + # Only move if not at the last item + if abs_pos < total_rows - 1: + # Try to move selection within visible area first + if self.selected_row < visible_rows - 1: + self.selected_row += 1 + elif self.scroll_offset < max_scroll: + # Scroll down + self.scroll_offset += 1 + + # Clamp to valid range + self.scroll_offset = min(self.scroll_offset, max_scroll) + max_selected = min(visible_rows - 1, total_rows - self.scroll_offset - 1) + self.selected_row = min(self.selected_row, max(0, max_selected)) + + def _move_selection_up(self): + """Move selection up in opcode mode with scrolling.""" + if not self.show_opcodes: + return + + if self.selected_row > 0: + self.selected_row -= 1 + elif self.scroll_offset > 0: + self.scroll_offset -= 1 + + # Clamp to valid range based on actual stats_list + stats_list, visible_rows, total_rows = self._get_visible_rows_info() + if total_rows > 0: + max_scroll = max(0, total_rows - visible_rows) + self.scroll_offset = min(self.scroll_offset, max_scroll) + max_selected = min(visible_rows - 1, total_rows - self.scroll_offset - 1) + self.selected_row = min(self.selected_row, max(0, max_selected)) + + def _navigate_to_previous_thread(self): + """Navigate to previous thread in PER_THREAD mode, or switch from ALL to PER_THREAD.""" + if len(self.thread_ids) > 0: + if self.view_mode == "ALL": + self.view_mode = "PER_THREAD" + self.current_thread_index = len(self.thread_ids) - 1 + else: + self.current_thread_index = ( + self.current_thread_index - 1 + ) % len(self.thread_ids) + + def _navigate_to_next_thread(self): + """Navigate to next thread in PER_THREAD mode, or switch from ALL to PER_THREAD.""" + if len(self.thread_ids) > 0: + if self.view_mode == "ALL": + self.view_mode = "PER_THREAD" + self.current_thread_index = 0 + else: + self.current_thread_index = ( + self.current_thread_index + 1 + ) % len(self.thread_ids) + + def _show_terminal_too_small(self, height, width): + """Display a message when terminal is too small.""" + A_BOLD = self.display.get_attr("A_BOLD") + msg1 = "Terminal too small!" + msg2 = f"Need: {MIN_TERMINAL_WIDTH}x{MIN_TERMINAL_HEIGHT}" + msg3 = f"Have: {width}x{height}" + msg4 = "Please resize" + + # Center the messages + if height >= 4: + self.display.add_str( + height // 2 - 2, + max(0, (width - len(msg1)) // 2), + msg1[: width - 1], + A_BOLD, + ) + self.display.add_str( + height // 2 - 1, + max(0, (width - len(msg2)) // 2), + msg2[: width - 1], + ) + self.display.add_str( + height // 2, + max(0, (width - len(msg3)) // 2), + msg3[: width - 1], + ) + self.display.add_str( + height // 2 + 1, + max(0, (width - len(msg4)) // 2), + msg4[: width - 1], + ) + elif height >= 1: + self.display.add_str(0, 0, msg1[: width - 1], A_BOLD) + + def _show_terminal_size_warning_and_wait(self, height, width): + """Show terminal size warning during initialization and wait for user acknowledgment.""" + A_BOLD = self.display.get_attr("A_BOLD") + A_DIM = self.display.get_attr("A_DIM") + + self.display.clear() + msg1 = "WARNING: Terminal too small!" + msg2 = f"Required: {MIN_TERMINAL_WIDTH}x{MIN_TERMINAL_HEIGHT}" + msg3 = f"Current: {width}x{height}" + msg4 = "Please resize your terminal for best experience" + msg5 = "Press any key to continue..." + + # Center the messages + if height >= 5: + self.display.add_str( + height // 2 - 2, + max(0, (width - len(msg1)) // 2), + msg1[: width - 1], + A_BOLD, + ) + self.display.add_str( + height // 2 - 1, + max(0, (width - len(msg2)) // 2), + msg2[: width - 1], + ) + self.display.add_str( + height // 2, + max(0, (width - len(msg3)) // 2), + msg3[: width - 1], + ) + self.display.add_str( + height // 2 + 1, + max(0, (width - len(msg4)) // 2), + msg4[: width - 1], + ) + self.display.add_str( + height // 2 + 3, + max(0, (width - len(msg5)) // 2), + msg5[: width - 1], + A_DIM, + ) + elif height >= 1: + self.display.add_str(0, 0, msg1[: width - 1], A_BOLD) + + self.display.refresh() + # Wait for user acknowledgment (2 seconds timeout) + self.display.set_nodelay(False) + # Note: timeout is curses-specific, skipping for now + self.display.get_input() + self.display.set_nodelay(True) + + def _handle_input(self): + """Handle keyboard input (non-blocking).""" + self.display.set_nodelay(True) + ch = self.display.get_input() + + # Handle filter input mode FIRST - takes precedence over all commands + if self.filter_input_mode: + if ch == 27: # ESC key + self.filter_input_mode = False + self.filter_input_buffer = "" + elif ch == 10 or ch == 13: # Enter key + self.filter_pattern = ( + self.filter_input_buffer + if self.filter_input_buffer + else None + ) + self.filter_input_mode = False + self.filter_input_buffer = "" + elif ch == 127 or ch == 263: # Backspace + if self.filter_input_buffer: + self.filter_input_buffer = self.filter_input_buffer[:-1] + elif ch >= 32 and ch < 127: # Printable characters + self.filter_input_buffer += chr(ch) + + # Update display if input was processed while finished + self._handle_finished_input_update(ch != -1) + return + + # Handle help toggle keys + if ch == ord("h") or ch == ord("H") or ch == ord("?"): + self.show_help = not self.show_help + return + + # If showing help, any other key closes it + if self.show_help and ch != -1: + self.show_help = False + return + + # Handle regular commands + if ch == ord("q") or ch == ord("Q"): + self.running = False + + elif ch == ord("s"): + self._cycle_sort(reverse=False) + + elif ch == ord("S"): + self._cycle_sort(reverse=True) + + elif ch == ord("p") or ch == ord("P"): + self.paused = not self.paused + + elif ch == ord("r") or ch == ord("R"): + # Don't allow reset when profiling is finished + if not self.finished: + self.reset_stats() + + elif ch == ord("+") or ch == ord("="): + # Decrease update interval (faster refresh) + self.display_update_interval_sec = max( + 0.05, self.display_update_interval_sec - 0.05 + ) # Min 20Hz + + elif ch == ord("-") or ch == ord("_"): + # Increase update interval (slower refresh) + self.display_update_interval_sec = min( + 1.0, self.display_update_interval_sec + 0.05 + ) # Max 1Hz + + elif ch == ord("c") or ch == ord("C"): + if self.filter_pattern: + self.filter_pattern = None + + elif ch == ord("/"): + self.filter_input_mode = True + self.filter_input_buffer = self.filter_pattern or "" + + elif ch == ord("t") or ch == ord("T"): + # Toggle between ALL and PER_THREAD modes + if self.view_mode == "ALL": + if len(self.thread_ids) > 0: + self.view_mode = "PER_THREAD" + self.current_thread_index = 0 + else: + self.view_mode = "ALL" + + elif ch == ord("x") or ch == ord("X"): + # Toggle trend colors on/off + if self._trend_tracker is not None: + self._trend_tracker.toggle() + + elif ch == ord("j") or ch == ord("J"): + # Move selection down in opcode mode (with scrolling) + self._move_selection_down() + + elif ch == ord("k") or ch == ord("K"): + # Move selection up in opcode mode (with scrolling) + self._move_selection_up() + + elif ch == curses.KEY_UP: + # Move selection up (same as 'k') when in opcode mode + if self.show_opcodes: + self._move_selection_up() + else: + # Navigate to previous thread (same as KEY_LEFT) + self._navigate_to_previous_thread() + + elif ch == curses.KEY_DOWN: + # Move selection down (same as 'j') when in opcode mode + if self.show_opcodes: + self._move_selection_down() + else: + # Navigate to next thread (same as KEY_RIGHT) + self._navigate_to_next_thread() + + elif ch == curses.KEY_LEFT: + # Navigate to previous thread + self._navigate_to_previous_thread() + + elif ch == curses.KEY_RIGHT: + # Navigate to next thread + self._navigate_to_next_thread() + + # Update display if input was processed while finished + self._handle_finished_input_update(ch != -1) + + def init_curses(self, stdscr): + """Initialize curses display and suppress stdout/stderr.""" + self.stdscr = stdscr + self.display = CursesDisplay(stdscr) + + # Check terminal size upfront and warn if too small + height, width = self.display.get_dimensions() + + if width < MIN_TERMINAL_WIDTH or height < MIN_TERMINAL_HEIGHT: + # Show warning and wait briefly for user to see it + self._show_terminal_size_warning_and_wait(height, width) + + curses.curs_set(0) # Hide cursor + stdscr.nodelay(True) # Non-blocking input + stdscr.scrollok(False) # Disable scrolling + stdscr.idlok(False) # Disable hardware insert/delete + stdscr.leaveok(True) # Don't care about cursor position + + if curses.has_colors(): + curses.start_color() + curses.use_default_colors() + + # Suppress stdout and stderr to prevent interfering with curses display + # Use contextlib.redirect_stdout/stderr for better resource management + self._saved_stdout = sys.stdout + self._saved_stderr = sys.stderr + # Open devnull and ensure it's cleaned up even if an exception occurs + try: + self._devnull = open(os.devnull, "w") + sys.stdout = self._devnull + sys.stderr = self._devnull + except Exception: + # If redirection fails, restore original streams + sys.stdout = self._saved_stdout + sys.stderr = self._saved_stderr + raise + + # Initial clear + self.display.clear() + self.display.refresh() + + def cleanup_curses(self): + """Clean up curses display and restore stdout/stderr.""" + # Restore stdout and stderr in reverse order + # Use try-finally to ensure cleanup even if restoration fails + try: + if self._saved_stdout is not None: + sys.stdout = self._saved_stdout + self._saved_stdout = None + if self._saved_stderr is not None: + sys.stderr = self._saved_stderr + self._saved_stderr = None + finally: + # Always close devnull, even if stdout/stderr restoration fails + if self._devnull is not None: + with contextlib.suppress(Exception): + self._devnull.close() + self._devnull = None + + if self.display is not None and self.stdscr is not None: + with contextlib.suppress(Exception): + curses.curs_set(1) # Show cursor + self.display.set_nodelay(False) + + def export(self, filename): + """Export is not supported in live mode.""" + raise NotImplementedError( + "Export to file is not supported in live mode. " + "Use the live TUI to view statistics in real-time." + ) diff --git a/Lib/profiling/sampling/live_collector/constants.py b/Lib/profiling/sampling/live_collector/constants.py new file mode 100644 index 00000000000..bb45006553a --- /dev/null +++ b/Lib/profiling/sampling/live_collector/constants.py @@ -0,0 +1,65 @@ +"""Constants for the live profiling collector.""" + +# Time conversion constants +MICROSECONDS_PER_SECOND = 1_000_000 + +# Display update constants +DISPLAY_UPDATE_HZ = 10 +DISPLAY_UPDATE_INTERVAL_SEC = 1.0 / DISPLAY_UPDATE_HZ # 0.1 seconds + +# Terminal size constraints +MIN_TERMINAL_WIDTH = 60 +MIN_TERMINAL_HEIGHT = 12 + +# Column width thresholds +WIDTH_THRESHOLD_SAMPLE_PCT = 80 +WIDTH_THRESHOLD_TOTTIME = 100 +WIDTH_THRESHOLD_CUMUL_PCT = 120 +WIDTH_THRESHOLD_CUMTIME = 140 + +# Display layout constants +HEADER_LINES = 10 # Increased to include thread status line +FOOTER_LINES = 2 +SAFETY_MARGIN = 1 +TOP_FUNCTIONS_DISPLAY_COUNT = 3 + +# Column widths for data display +COL_WIDTH_NSAMPLES = 13 +COL_SPACING = 2 +COL_WIDTH_SAMPLE_PCT = 5 +COL_WIDTH_TIME = 10 + +# Function name display +MIN_FUNC_NAME_WIDTH = 10 +MAX_FUNC_NAME_WIDTH = 40 +MIN_AVAILABLE_SPACE = 10 + +# Progress bar display +MIN_BAR_WIDTH = 10 +MAX_SAMPLE_RATE_BAR_WIDTH = 30 +MAX_EFFICIENCY_BAR_WIDTH = 60 + +# Sample rate scaling +MIN_SAMPLE_RATE_FOR_SCALING = 100 + +# Finished banner display +FINISHED_BANNER_EXTRA_LINES = 3 # Blank line + banner + blank line + +# Opcode panel display +OPCODE_PANEL_HEIGHT = 12 # Height reserved for opcode statistics panel + +# Color pair IDs +COLOR_PAIR_SAMPLES = 1 +COLOR_PAIR_FILE = 2 +COLOR_PAIR_FUNC = 3 +COLOR_PAIR_HEADER_BG = 4 +COLOR_PAIR_CYAN = 5 +COLOR_PAIR_YELLOW = 6 +COLOR_PAIR_GREEN = 7 +COLOR_PAIR_MAGENTA = 8 +COLOR_PAIR_RED = 9 +COLOR_PAIR_SORTED_HEADER = 10 + +# Default display settings +DEFAULT_SORT_BY = "nsamples" # Number of samples in leaf (self time) +DEFAULT_DISPLAY_LIMIT = 20 diff --git a/Lib/profiling/sampling/live_collector/display.py b/Lib/profiling/sampling/live_collector/display.py new file mode 100644 index 00000000000..f5324421b10 --- /dev/null +++ b/Lib/profiling/sampling/live_collector/display.py @@ -0,0 +1,240 @@ +"""Display interface abstractions for the live profiling collector.""" + +import contextlib +import curses +from abc import ABC, abstractmethod + + +class DisplayInterface(ABC): + """Abstract interface for display operations to enable testing.""" + + @abstractmethod + def get_dimensions(self): + """Get terminal dimensions as (height, width).""" + pass + + @abstractmethod + def clear(self): + """Clear the screen.""" + pass + + @abstractmethod + def refresh(self): + """Refresh the screen to show changes.""" + pass + + @abstractmethod + def redraw(self): + """Redraw the entire window.""" + pass + + @abstractmethod + def add_str(self, line, col, text, attr=0): + """Add a string at the specified position.""" + pass + + @abstractmethod + def get_input(self): + """Get a character from input (non-blocking). Returns -1 if no input.""" + pass + + @abstractmethod + def set_nodelay(self, flag): + """Set non-blocking mode for input.""" + pass + + @abstractmethod + def has_colors(self): + """Check if terminal supports colors.""" + pass + + @abstractmethod + def init_color_pair(self, pair_id, fg, bg): + """Initialize a color pair.""" + pass + + @abstractmethod + def get_color_pair(self, pair_id): + """Get a color pair attribute.""" + pass + + @abstractmethod + def get_attr(self, name): + """Get a display attribute by name (e.g., 'A_BOLD', 'A_REVERSE').""" + pass + + +class CursesDisplay(DisplayInterface): + """Real curses display implementation.""" + + def __init__(self, stdscr): + self.stdscr = stdscr + + def get_dimensions(self): + return self.stdscr.getmaxyx() + + def clear(self): + # Use erase() instead of clear() to avoid flickering + # clear() forces a complete screen redraw, erase() just clears the buffer + self.stdscr.erase() + + def refresh(self): + self.stdscr.refresh() + + def redraw(self): + # Use noutrefresh + doupdate for smoother updates + self.stdscr.noutrefresh() + curses.doupdate() + + def add_str(self, line, col, text, attr=0): + try: + height, width = self.get_dimensions() + if 0 <= line < height and 0 <= col < width: + max_len = width - col - 1 + if len(text) > max_len: + text = text[:max_len] + self.stdscr.addstr(line, col, text, attr) + except curses.error: + pass + + def get_input(self): + try: + return self.stdscr.getch() + except (KeyError, curses.error): + return -1 + + def set_nodelay(self, flag): + self.stdscr.nodelay(flag) + + def has_colors(self): + return curses.has_colors() + + def init_color_pair(self, pair_id, fg, bg): + try: + curses.init_pair(pair_id, fg, bg) + except curses.error: + pass + + def get_color_pair(self, pair_id): + return curses.color_pair(pair_id) + + def get_attr(self, name): + return getattr(curses, name, 0) + + +class MockDisplay(DisplayInterface): + """Mock display for testing.""" + + def __init__(self, height=40, width=160): + self.height = height + self.width = width + self.buffer = {} + self.cleared = False + self.refreshed = False + self.redrawn = False + self.input_queue = [] + self.nodelay_flag = True + self.colors_supported = True + self.color_pairs = {} + + def get_dimensions(self): + return (self.height, self.width) + + def clear(self): + self.buffer.clear() + self.cleared = True + + def refresh(self): + self.refreshed = True + + def redraw(self): + self.redrawn = True + + def add_str(self, line, col, text, attr=0): + if 0 <= line < self.height and 0 <= col < self.width: + max_len = self.width - col - 1 + if len(text) > max_len: + text = text[:max_len] + self.buffer[(line, col)] = (text, attr) + + def get_input(self): + if self.input_queue: + return self.input_queue.pop(0) + return -1 + + def set_nodelay(self, flag): + self.nodelay_flag = flag + + def has_colors(self): + return self.colors_supported + + def init_color_pair(self, pair_id, fg, bg): + self.color_pairs[pair_id] = (fg, bg) + + def get_color_pair(self, pair_id): + return pair_id << 8 + + def get_attr(self, name): + attrs = { + "A_NORMAL": 0, + "A_BOLD": 1 << 16, + "A_REVERSE": 1 << 17, + "A_UNDERLINE": 1 << 18, + "A_DIM": 1 << 19, + } + return attrs.get(name, 0) + + def simulate_input(self, char): + """Helper method for tests to simulate keyboard input.""" + self.input_queue.append(char) + + def get_text_at(self, line, col): + """Helper method for tests to inspect buffer content.""" + if (line, col) in self.buffer: + return self.buffer[(line, col)][0] + return None + + def get_all_lines(self): + """Get all display content as a list of lines (for testing).""" + if not self.buffer: + return [] + + max_line = max(pos[0] for pos in self.buffer.keys()) + lines = [] + for line_num in range(max_line + 1): + line_parts = [] + for col in range(self.width): + if (line_num, col) in self.buffer: + text, _ = self.buffer[(line_num, col)] + line_parts.append((col, text)) + + # Reconstruct line from parts + if line_parts: + line_parts.sort(key=lambda x: x[0]) + line = "" + last_col = 0 + for col, text in line_parts: + if col > last_col: + line += " " * (col - last_col) + line += text + last_col = col + len(text) + lines.append(line.rstrip()) + else: + lines.append("") + + # Remove trailing empty lines + while lines and not lines[-1]: + lines.pop() + + return lines + + def find_text(self, pattern): + """Find text matching pattern in buffer (for testing). Returns (line, col) or None.""" + for (line, col), (text, _) in self.buffer.items(): + if pattern in text: + return (line, col) + return None + + def contains_text(self, text): + """Check if display contains the given text anywhere (for testing).""" + return self.find_text(text) is not None diff --git a/Lib/profiling/sampling/live_collector/trend_tracker.py b/Lib/profiling/sampling/live_collector/trend_tracker.py new file mode 100644 index 00000000000..c025b83a134 --- /dev/null +++ b/Lib/profiling/sampling/live_collector/trend_tracker.py @@ -0,0 +1,157 @@ +"""TrendTracker - Encapsulated trend tracking for live profiling metrics. + +This module provides trend tracking functionality for profiling metrics, +calculating direction indicators (up/down/stable) and managing associated +visual attributes like colors. +""" + +import curses +from typing import Dict, Literal, Any + +TrendDirection = Literal["up", "down", "stable"] + + +class TrendTracker: + """ + Tracks metric trends over time and provides visual indicators. + + This class encapsulates all logic for: + - Tracking previous values of metrics + - Calculating trend directions (up/down/stable) + - Determining visual attributes (colors) for trends + - Managing enable/disable state + + Example: + tracker = TrendTracker(colors_dict) + tracker.update("func1", "nsamples", 10) + trend = tracker.get_trend("func1", "nsamples") + color = tracker.get_color("func1", "nsamples") + """ + + # Threshold for determining if a value has changed significantly + CHANGE_THRESHOLD = 0.001 + + def __init__(self, colors: Dict[str, int], enabled: bool = True): + """ + Initialize the trend tracker. + + Args: + colors: Dictionary containing color attributes including + 'trend_up', 'trend_down', 'trend_stable' + enabled: Whether trend tracking is initially enabled + """ + self._previous_values: Dict[Any, Dict[str, float]] = {} + self._enabled = enabled + self._colors = colors + + @property + def enabled(self) -> bool: + """Whether trend tracking is enabled.""" + return self._enabled + + def toggle(self) -> bool: + """ + Toggle trend tracking on/off. + + Returns: + New enabled state + """ + self._enabled = not self._enabled + return self._enabled + + def set_enabled(self, enabled: bool) -> None: + """Set trend tracking enabled state.""" + self._enabled = enabled + + def update(self, key: Any, metric: str, value: float) -> TrendDirection: + """ + Update a metric value and calculate its trend. + + Args: + key: Identifier for the entity (e.g., function) + metric: Name of the metric (e.g., 'nsamples', 'tottime') + value: Current value of the metric + + Returns: + Trend direction: 'up', 'down', or 'stable' + """ + # Initialize storage for this key if needed + if key not in self._previous_values: + self._previous_values[key] = {} + + # Get previous value, defaulting to current if not tracked yet + prev_value = self._previous_values[key].get(metric, value) + + # Calculate trend + if value > prev_value + self.CHANGE_THRESHOLD: + trend = "up" + elif value < prev_value - self.CHANGE_THRESHOLD: + trend = "down" + else: + trend = "stable" + + # Update previous value for next iteration + self._previous_values[key][metric] = value + + return trend + + def get_trend(self, key: Any, metric: str) -> TrendDirection: + """ + Get the current trend for a metric without updating. + + Args: + key: Identifier for the entity + metric: Name of the metric + + Returns: + Trend direction, or 'stable' if not tracked + """ + # This would require storing trends separately, which we don't do + # For now, return stable if not found + return "stable" + + def get_color(self, trend: TrendDirection) -> int: + """ + Get the color attribute for a trend direction. + + Args: + trend: The trend direction + + Returns: + Curses color attribute (or A_NORMAL if disabled) + """ + if not self._enabled: + return curses.A_NORMAL + + if trend == "up": + return self._colors.get("trend_up", curses.A_BOLD) + elif trend == "down": + return self._colors.get("trend_down", curses.A_BOLD) + else: # stable + return self._colors.get("trend_stable", curses.A_NORMAL) + + def update_metrics(self, key: Any, metrics: Dict[str, float]) -> Dict[str, TrendDirection]: + """ + Update multiple metrics at once and get their trends. + + Args: + key: Identifier for the entity + metrics: Dictionary of metric_name -> value + + Returns: + Dictionary of metric_name -> trend_direction + """ + trends = {} + for metric, value in metrics.items(): + trends[metric] = self.update(key, metric, value) + return trends + + def clear(self) -> None: + """Clear all tracked values (useful on stats reset).""" + self._previous_values.clear() + + def __repr__(self) -> str: + """String representation for debugging.""" + status = "enabled" if self._enabled else "disabled" + tracked = len(self._previous_values) + return f"TrendTracker({status}, tracking {tracked} entities)" diff --git a/Lib/profiling/sampling/live_collector/widgets.py b/Lib/profiling/sampling/live_collector/widgets.py new file mode 100644 index 00000000000..86d2649f875 --- /dev/null +++ b/Lib/profiling/sampling/live_collector/widgets.py @@ -0,0 +1,1090 @@ +"""Widget classes for the live profiling collector UI.""" + +import curses +import time +from abc import ABC, abstractmethod + +from .constants import ( + TOP_FUNCTIONS_DISPLAY_COUNT, + MIN_FUNC_NAME_WIDTH, + MAX_FUNC_NAME_WIDTH, + WIDTH_THRESHOLD_SAMPLE_PCT, + WIDTH_THRESHOLD_TOTTIME, + WIDTH_THRESHOLD_CUMUL_PCT, + WIDTH_THRESHOLD_CUMTIME, + MICROSECONDS_PER_SECOND, + DISPLAY_UPDATE_INTERVAL_SEC, + MIN_BAR_WIDTH, + MAX_SAMPLE_RATE_BAR_WIDTH, + MAX_EFFICIENCY_BAR_WIDTH, + MIN_SAMPLE_RATE_FOR_SCALING, + FOOTER_LINES, + FINISHED_BANNER_EXTRA_LINES, + OPCODE_PANEL_HEIGHT, +) +from ..constants import ( + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_ON_CPU, + THREAD_STATUS_UNKNOWN, + THREAD_STATUS_GIL_REQUESTED, + PROFILING_MODE_CPU, + PROFILING_MODE_GIL, + PROFILING_MODE_WALL, +) +from ..opcode_utils import get_opcode_info, format_opcode + + +class Widget(ABC): + """Base class for UI widgets.""" + + def __init__(self, display, colors): + """ + Initialize widget. + + Args: + display: DisplayInterface implementation + colors: Dictionary of color attributes + """ + self.display = display + self.colors = colors + + @abstractmethod + def render(self, line, width, **kwargs): + """ + Render the widget starting at the given line. + + Args: + line: Starting line number + width: Available width + **kwargs: Additional rendering parameters + + Returns: + Next available line number after rendering + """ + pass + + def add_str(self, line, col, text, attr=0): + """Add a string to the display at the specified position.""" + self.display.add_str(line, col, text, attr) + + +class ProgressBarWidget(Widget): + """Reusable progress bar widget.""" + + def render(self, line, width, **kwargs): + """Render is not used for progress bars - use render_bar instead.""" + raise NotImplementedError("Use render_bar method instead") + + def render_bar( + self, filled, total, max_width, fill_char="█", empty_char="░" + ): + """ + Render a progress bar and return the bar string and its length. + + Args: + filled: Current filled amount + total: Total amount (max value) + max_width: Maximum width for the bar + fill_char: Character to use for filled portion + empty_char: Character to use for empty portion + + Returns: + Tuple of (bar_string, bar_length) + """ + bar_width = min(max_width, max_width) + normalized = min(filled / max(total, 1), 1.0) + bar_fill = int(normalized * bar_width) + + bar = "[" + for i in range(bar_width): + if i < bar_fill: + bar += fill_char + else: + bar += empty_char + bar += "]" + return bar, len(bar) + + +class HeaderWidget(Widget): + """Widget for rendering the header section (lines 0-8).""" + + def __init__(self, display, colors, collector): + """ + Initialize header widget. + + Args: + display: DisplayInterface implementation + colors: Dictionary of color attributes + collector: Reference to LiveStatsCollector for accessing stats + """ + super().__init__(display, colors) + self.collector = collector + self.progress_bar = ProgressBarWidget(display, colors) + + def render(self, line, width, **kwargs): + """ + Render the complete header section. + + Args: + line: Starting line number + width: Available width + kwargs: Must contain 'elapsed' key + + Returns: + Next available line number + """ + elapsed = kwargs["elapsed"] + + line = self.draw_header_info(line, width, elapsed) + line = self.draw_sample_stats(line, width, elapsed) + line = self.draw_efficiency_bar(line, width) + line = self.draw_thread_status(line, width) + line = self.draw_function_stats( + line, width, kwargs.get("stats_list", []) + ) + line = self.draw_top_functions( + line, width, kwargs.get("stats_list", []) + ) + + # Show prominent finished banner if profiling is complete + if self.collector.finished: + line = self.draw_finished_banner(line, width) + + # Separator + A_DIM = self.display.get_attr("A_DIM") + separator = "─" * (width - 1) + self.add_str(line, 0, separator[: width - 1], A_DIM) + line += 1 + + return line + + def format_uptime(self, elapsed): + """Format elapsed time as uptime string.""" + uptime_sec = int(elapsed) + hours = uptime_sec // 3600 + minutes = (uptime_sec % 3600) // 60 + seconds = uptime_sec % 60 + if hours > 0: + return f"{hours}h{minutes:02d}m{seconds:02d}s" + else: + return f"{minutes}m{seconds:02d}s" + + def draw_header_info(self, line, width, elapsed): + """Draw the header information line with PID, uptime, time, and interval.""" + # Draw title + A_BOLD = self.display.get_attr("A_BOLD") + title = "Tachyon Profiler" + self.add_str(line, 0, title, A_BOLD | self.colors["cyan"]) + line += 1 + + current_time = self.collector.current_time_display + uptime = self.format_uptime(elapsed) + + # Calculate display refresh rate + refresh_hz = ( + 1.0 / self.collector.display_update_interval_sec if self.collector.display_update_interval_sec > 0 else 0 + ) + + # Get current view mode and thread display + if self.collector.view_mode == "ALL": + thread_name = "ALL" + thread_color = self.colors["green"] + else: + # PER_THREAD mode + if self.collector.current_thread_index < len( + self.collector.thread_ids + ): + thread_id = self.collector.thread_ids[ + self.collector.current_thread_index + ] + num_threads = len(self.collector.thread_ids) + thread_name = f"{thread_id} ({self.collector.current_thread_index + 1}/{num_threads})" + thread_color = self.colors["magenta"] + else: + thread_name = "ALL" + thread_color = self.colors["green"] + + header_parts = [ + ("PID: ", curses.A_BOLD), + (f"{self.collector.pid}", self.colors["cyan"]), + (" │ ", curses.A_DIM), + ("Thread: ", curses.A_BOLD), + (thread_name, thread_color), + (" │ ", curses.A_DIM), + ("Uptime: ", curses.A_BOLD), + (uptime, self.colors["green"]), + (" │ ", curses.A_DIM), + ("Time: ", curses.A_BOLD), + (current_time, self.colors["yellow"]), + (" │ ", curses.A_DIM), + ("Interval: ", curses.A_BOLD), + ( + f"{self.collector.sample_interval_usec}µs", + self.colors["magenta"], + ), + (" │ ", curses.A_DIM), + ("Display: ", curses.A_BOLD), + (f"{refresh_hz:.1f}Hz", self.colors["cyan"]), + ] + + col = 0 + for text, attr in header_parts: + if col < width - 1: + self.add_str(line, col, text, attr) + col += len(text) + return line + 1 + + def format_rate_with_units(self, rate_hz): + """Format a rate in Hz with appropriate units (Hz, KHz, MHz).""" + if rate_hz >= MICROSECONDS_PER_SECOND: + return f"{rate_hz / MICROSECONDS_PER_SECOND:.1f}MHz" + elif rate_hz >= 1_000: + return f"{rate_hz / 1_000:.1f}KHz" + else: + return f"{rate_hz:.1f}Hz" + + def draw_sample_stats(self, line, width, elapsed): + """Draw sample statistics with visual progress bar.""" + sample_rate = ( + self.collector.total_samples / elapsed if elapsed > 0 else 0 + ) + + # Update max sample rate + if sample_rate > self.collector.max_sample_rate: + self.collector.max_sample_rate = sample_rate + + col = 0 + self.add_str(line, col, "Samples: ", curses.A_BOLD) + col += 9 + self.add_str( + line, + col, + f"{self.collector.total_samples:>8}", + self.colors["cyan"], + ) + col += 8 + self.add_str( + line, col, f" total ({sample_rate:>7.1f}/s) ", curses.A_NORMAL + ) + col += 23 + + # Draw sample rate bar + target_rate = ( + MICROSECONDS_PER_SECOND / self.collector.sample_interval_usec + ) + + # Show current/target ratio with percentage + if sample_rate > 0 and target_rate > 0: + percentage = min((sample_rate / target_rate) * 100, 100) + current_formatted = self.format_rate_with_units(sample_rate) + target_formatted = self.format_rate_with_units(target_rate) + + if percentage >= 99.5: # Show 100% when very close + rate_label = f" {current_formatted}/{target_formatted} (100%)" + else: + rate_label = f" {current_formatted}/{target_formatted} ({percentage:>4.1f}%)" + else: + target_formatted = self.format_rate_with_units(target_rate) + rate_label = f" target: {target_formatted}" + + available_width = width - col - len(rate_label) - 3 + + if available_width >= MIN_BAR_WIDTH: + bar_width = min(MAX_SAMPLE_RATE_BAR_WIDTH, available_width) + # Use target rate as the reference, with a minimum for scaling + reference_rate = max(target_rate, MIN_SAMPLE_RATE_FOR_SCALING) + normalized_rate = min(sample_rate / reference_rate, 1.0) + bar_fill = int(normalized_rate * bar_width) + + bar = "[" + for i in range(bar_width): + bar += "█" if i < bar_fill else "░" + bar += "]" + self.add_str(line, col, bar, self.colors["green"]) + col += len(bar) + + if col + len(rate_label) < width - 1: + self.add_str(line, col + 1, rate_label, curses.A_DIM) + return line + 1 + + def draw_efficiency_bar(self, line, width): + """Draw sample efficiency bar showing success/failure rates.""" + # total_samples = successful_samples + failed_samples, so percentages add to 100% + total = max(1, self.collector.total_samples) + success_pct = (self.collector.successful_samples / total) * 100 + failed_pct = (self.collector.failed_samples / total) * 100 + + col = 0 + self.add_str(line, col, "Efficiency:", curses.A_BOLD) + col += 11 + + label = f" {success_pct:>5.2f}% good, {failed_pct:>5.2f}% failed" + available_width = width - col - len(label) - 3 + + if available_width >= MIN_BAR_WIDTH: + bar_width = min(MAX_EFFICIENCY_BAR_WIDTH, available_width) + success_fill = int((self.collector.successful_samples / total) * bar_width) + failed_fill = bar_width - success_fill + + self.add_str(line, col, "[", curses.A_NORMAL) + col += 1 + if success_fill > 0: + self.add_str( + line, col, "█" * success_fill, self.colors["green"] + ) + col += success_fill + if failed_fill > 0: + self.add_str(line, col, "█" * failed_fill, self.colors["red"]) + col += failed_fill + self.add_str(line, col, "]", curses.A_NORMAL) + col += 1 + + self.add_str(line, col + 1, label, curses.A_NORMAL) + return line + 1 + + def _add_percentage_stat( + self, line, col, value, label, color, add_separator=False + ): + """Add a percentage stat to the display. + + Args: + line: Line number + col: Starting column + value: Percentage value + label: Label text + color: Color attribute + add_separator: Whether to add separator before the stat + + Returns: + Updated column position + """ + if add_separator: + self.add_str(line, col, " │ ", curses.A_DIM) + col += 3 + + self.add_str(line, col, f"{value:>4.1f}", color) + col += 4 + self.add_str(line, col, f"% {label}", curses.A_NORMAL) + col += len(label) + 2 + + return col + + def draw_thread_status(self, line, width): + """Draw thread status statistics and GC information.""" + # Get status counts for current view mode + thread_data = self.collector._get_current_thread_data() + status_counts = thread_data.as_status_dict() if thread_data else self.collector.thread_status_counts + + # Calculate percentages + total_threads = max(1, status_counts["total"]) + pct_on_gil = (status_counts["has_gil"] / total_threads) * 100 + pct_off_gil = 100.0 - pct_on_gil + pct_gil_requested = (status_counts["gil_requested"] / total_threads) * 100 + pct_exception = (status_counts.get("has_exception", 0) / total_threads) * 100 + + # Get GC percentage based on view mode + if thread_data: + total_samples = max(1, thread_data.sample_count) + pct_gc = (thread_data.gc_frame_samples / total_samples) * 100 + else: + # Use total_samples for GC percentage since gc_frame_samples is tracked + # across ALL samples (via thread status), not just successful ones + total_samples = max(1, self.collector.total_samples) + pct_gc = (self.collector.gc_frame_samples / total_samples) * 100 + + col = 0 + self.add_str(line, col, "Threads: ", curses.A_BOLD) + col += 11 + + # Show GIL stats only if mode is not GIL (GIL mode filters to only GIL holders) + if self.collector.mode != PROFILING_MODE_GIL: + col = self._add_percentage_stat( + line, col, pct_on_gil, "on gil", self.colors["green"] + ) + col = self._add_percentage_stat( + line, + col, + pct_off_gil, + "off gil", + self.colors["red"], + add_separator=True, + ) + + # Show "waiting for gil" only if mode is not GIL + if self.collector.mode != PROFILING_MODE_GIL and col < width - 30: + col = self._add_percentage_stat( + line, + col, + pct_gil_requested, + "waiting for gil", + self.colors["yellow"], + add_separator=True, + ) + + # Show exception stats + if col < width - 15: + col = self._add_percentage_stat( + line, + col, + pct_exception, + "exc", + self.colors["red"], + add_separator=(col > 11), + ) + + # Always show GC stats + if col < width - 15: + col = self._add_percentage_stat( + line, + col, + pct_gc, + "GC", + self.colors["magenta"], + add_separator=(col > 11), + ) + + return line + 1 + + def draw_function_stats(self, line, width, stats_list): + """Draw function statistics summary.""" + result_set = self.collector._get_current_result_source() + total_funcs = len(result_set) + funcs_shown = len(stats_list) + executing_funcs = sum( + 1 for f in result_set.values() if f.get("direct_calls", 0) > 0 + ) + stack_only = total_funcs - executing_funcs + + col = 0 + self.add_str(line, col, "Functions: ", curses.A_BOLD) + col += 11 + self.add_str(line, col, f"{total_funcs:>5}", self.colors["cyan"]) + col += 5 + self.add_str(line, col, " total", curses.A_NORMAL) + col += 6 + + if col < width - 25: + self.add_str(line, col, " │ ", curses.A_DIM) + col += 3 + self.add_str( + line, col, f"{executing_funcs:>5}", self.colors["green"] + ) + col += 5 + self.add_str(line, col, " exec", curses.A_NORMAL) + col += 5 + + if col < width - 25: + self.add_str(line, col, " │ ", curses.A_DIM) + col += 3 + self.add_str(line, col, f"{stack_only:>5}", self.colors["yellow"]) + col += 5 + self.add_str(line, col, " stack", curses.A_NORMAL) + col += 6 + + if col < width - 20: + self.add_str(line, col, " │ ", curses.A_DIM) + col += 3 + self.add_str( + line, col, f"{funcs_shown:>5}", self.colors["magenta"] + ) + col += 5 + self.add_str(line, col, " shown", curses.A_NORMAL) + return line + 1 + + def draw_top_functions(self, line, width, stats_list): + """Draw top N hottest functions.""" + col = 0 + self.add_str( + line, + col, + f"Top {TOP_FUNCTIONS_DISPLAY_COUNT}: ", + curses.A_BOLD, + ) + col += 11 + + top_by_samples = sorted( + stats_list, key=lambda x: x["direct_calls"], reverse=True + ) + emojis = ["🥇", "🥈", "🥉"] + medal_colors = [ + self.colors["red"], + self.colors["yellow"], + self.colors["green"], + ] + + displayed = 0 + for func_data in top_by_samples: + if displayed >= TOP_FUNCTIONS_DISPLAY_COUNT: + break + if col >= width - 20: + break + if func_data["direct_calls"] == 0: + continue + + func_name = func_data["func"][2] + func_pct = func_data["sample_pct"] + + # Medal emoji + if col + 3 < width - 15: + self.add_str( + line, col, emojis[displayed] + " ", medal_colors[displayed] + ) + col += 3 + + # Function name (truncate to fit) + available_for_name = width - col - 15 + max_name_len = min(25, max(5, available_for_name)) + if len(func_name) > max_name_len: + func_name = func_name[: max_name_len - 3] + "..." + + if col + len(func_name) < width - 10: + self.add_str(line, col, func_name, medal_colors[displayed]) + col += len(func_name) + + pct_str = ( + f" ({func_pct:.1f}%)" + if func_pct >= 0.1 + else f" ({func_data['direct_calls']})" + ) + self.add_str(line, col, pct_str, curses.A_DIM) + col += len(pct_str) + + displayed += 1 + + if displayed < 3 and col < width - 30: + self.add_str(line, col, " │ ", curses.A_DIM) + col += 3 + + if displayed == 0 and col < width - 25: + self.add_str(line, col, "(collecting samples...)", curses.A_DIM) + + return line + 1 + + def draw_finished_banner(self, line, width): + """Draw a prominent banner when profiling is finished.""" + A_REVERSE = self.display.get_attr("A_REVERSE") + A_BOLD = self.display.get_attr("A_BOLD") + + # Add blank line for separation + line += 1 + + # Create the banner message + message = " ✓ PROFILING COMPLETE - Final Results Below - Press 'q' to Quit " + + # Center the message and fill the width with reverse video + if len(message) < width - 1: + padding_total = width - len(message) - 1 + padding_left = padding_total // 2 + padding_right = padding_total - padding_left + full_message = " " * padding_left + message + " " * padding_right + else: + full_message = message[: width - 1] + + # Draw the banner with reverse video and bold + self.add_str( + line, 0, full_message, A_REVERSE | A_BOLD | self.colors["green"] + ) + line += 1 + + # Add blank line for separation + line += 1 + + return line + + +class TableWidget(Widget): + """Widget for rendering column headers and data rows.""" + + def __init__(self, display, colors, collector): + """ + Initialize table widget. + + Args: + display: DisplayInterface implementation + colors: Dictionary of color attributes + collector: Reference to LiveStatsCollector for accessing stats + """ + super().__init__(display, colors) + self.collector = collector + + def render(self, line, width, **kwargs): + """ + Render column headers and data rows. + + Args: + line: Starting line number + width: Available width + kwargs: Must contain 'height' and 'stats_list' keys + + Returns: + Next available line number + """ + height = kwargs["height"] + stats_list = kwargs["stats_list"] + + # Draw column headers + line, show_sample_pct, show_tottime, show_cumul_pct, show_cumtime = ( + self.draw_column_headers(line, width) + ) + column_flags = ( + show_sample_pct, + show_tottime, + show_cumul_pct, + show_cumtime, + ) + + # Draw data rows + line = self.draw_stats_rows( + line, height, width, stats_list, column_flags + ) + + return line + + def draw_column_headers(self, line, width): + """Draw column headers with sort indicators.""" + # Determine which columns to show based on width + show_sample_pct = width >= WIDTH_THRESHOLD_SAMPLE_PCT + show_tottime = width >= WIDTH_THRESHOLD_TOTTIME + show_cumul_pct = width >= WIDTH_THRESHOLD_CUMUL_PCT + show_cumtime = width >= WIDTH_THRESHOLD_CUMTIME + + sorted_header = self.colors["sorted_header"] + normal_header = self.colors["normal_header"] + + # Determine which column is sorted + sort_col = { + "nsamples": 0, + "sample_pct": 1, + "tottime": 2, + "cumul_pct": 3, + "cumtime": 4, + }.get(self.collector.sort_by, -1) + + # Build the full header line first, then draw it + # This avoids gaps between columns when using reverse video + header_parts = [] + col = 0 + + # Column 0: nsamples + text = f"{'▼nsamples' if sort_col == 0 else 'nsamples':>13} " + header_parts.append((col, text, sorted_header if sort_col == 0 else normal_header)) + col += 15 + + # Column 1: sample % + if show_sample_pct: + text = f"{'▼%' if sort_col == 1 else '%':>5} " + header_parts.append((col, text, sorted_header if sort_col == 1 else normal_header)) + col += 7 + + # Column 2: tottime + if show_tottime: + text = f"{'▼tottime' if sort_col == 2 else 'tottime':>10} " + header_parts.append((col, text, sorted_header if sort_col == 2 else normal_header)) + col += 12 + + # Column 3: cumul % + if show_cumul_pct: + text = f"{'▼%' if sort_col == 3 else '%':>5} " + header_parts.append((col, text, sorted_header if sort_col == 3 else normal_header)) + col += 7 + + # Column 4: cumtime + if show_cumtime: + text = f"{'▼cumtime' if sort_col == 4 else 'cumtime':>10} " + header_parts.append((col, text, sorted_header if sort_col == 4 else normal_header)) + col += 12 + + # Remaining headers + if col < width - 15: + remaining_space = width - col - 1 + func_width = min( + MAX_FUNC_NAME_WIDTH, + max(MIN_FUNC_NAME_WIDTH, remaining_space // 2), + ) + text = f"{'function':<{func_width}} " + header_parts.append((col, text, normal_header)) + col += func_width + 2 + + if col < width - 10: + file_text = "file:line" + padding = width - col - len(file_text) + text = file_text + " " * max(0, padding) + header_parts.append((col, text, normal_header)) + + # Draw full-width background first + self.add_str(line, 0, " " * (width - 1), normal_header) + + # Draw each header part on top + for col_pos, text, attr in header_parts: + self.add_str(line, col_pos, text.rstrip(), attr) + + return ( + line + 1, + show_sample_pct, + show_tottime, + show_cumul_pct, + show_cumtime, + ) + + def draw_stats_rows(self, line, height, width, stats_list, column_flags): + """Draw the statistics data rows.""" + show_sample_pct, show_tottime, show_cumul_pct, show_cumtime = ( + column_flags + ) + + # Get color attributes + color_file = self.colors.get("color_file", curses.A_NORMAL) + color_func = self.colors.get("color_func", curses.A_NORMAL) + + # Get trend tracker for color decisions + trend_tracker = self.collector._trend_tracker + + # Check if opcode mode is enabled for row selection highlighting + show_opcodes = getattr(self.collector, 'show_opcodes', False) + selected_row = getattr(self.collector, 'selected_row', 0) + scroll_offset = getattr(self.collector, 'scroll_offset', 0) if show_opcodes else 0 + A_REVERSE = self.display.get_attr("A_REVERSE") + A_BOLD = self.display.get_attr("A_BOLD") + + # Reserve space for opcode panel when enabled + opcode_panel_height = OPCODE_PANEL_HEIGHT if show_opcodes else 0 + + # Apply scroll offset when in opcode mode + display_stats = stats_list[scroll_offset:] if show_opcodes else stats_list + + for row_idx, stat in enumerate(display_stats): + if line >= height - FOOTER_LINES - opcode_panel_height: + break + + func = stat["func"] + direct_calls = stat["direct_calls"] + cumulative_calls = stat["cumulative_calls"] + total_time = stat["total_time"] + cumulative_time = stat["cumulative_time"] + sample_pct = stat["sample_pct"] + cum_pct = stat["cumul_pct"] + trends = stat.get("trends", {}) + + # Check if this row is selected + is_selected = show_opcodes and row_idx == selected_row + + # Helper function to get trend color + def get_trend_color(column_name): + if is_selected: + return A_REVERSE | A_BOLD + trend = trends.get(column_name, "stable") + if trend_tracker is not None and trend_tracker.enabled: + return trend_tracker.get_color(trend) + return curses.A_NORMAL + + filename, lineno, funcname = func[0], func[1], func[2] + samples_str = f"{direct_calls}/{cumulative_calls}" + col = 0 + + # Fill entire row with reverse video background for selected row + if is_selected: + self.add_str(line, 0, " " * (width - 1), A_REVERSE | A_BOLD) + + # Show selection indicator when opcode panel is enabled + if show_opcodes: + if is_selected: + self.add_str(line, col, "►", A_REVERSE | A_BOLD) + else: + self.add_str(line, col, " ", curses.A_NORMAL) + col += 2 + + # Samples column - apply trend color based on nsamples trend + nsamples_color = get_trend_color("nsamples") + self.add_str(line, col, f"{samples_str:>13} ", nsamples_color) + col += 15 + + # Sample % column + if show_sample_pct: + sample_pct_color = get_trend_color("sample_pct") + self.add_str(line, col, f"{sample_pct:>5.1f} ", sample_pct_color) + col += 7 + + # Total time column + if show_tottime: + tottime_color = get_trend_color("tottime") + self.add_str(line, col, f"{total_time:>10.3f} ", tottime_color) + col += 12 + + # Cumul % column + if show_cumul_pct: + cumul_pct_color = get_trend_color("cumul_pct") + self.add_str(line, col, f"{cum_pct:>5.1f} ", cumul_pct_color) + col += 7 + + # Cumul time column + if show_cumtime: + cumtime_color = get_trend_color("cumtime") + self.add_str(line, col, f"{cumulative_time:>10.3f} ", cumtime_color) + col += 12 + + # Function name column + if col < width - 15: + remaining_space = width - col - 1 + func_width = min( + MAX_FUNC_NAME_WIDTH, + max(MIN_FUNC_NAME_WIDTH, remaining_space // 2), + ) + + func_display = funcname + if len(funcname) > func_width: + func_display = funcname[: func_width - 3] + "..." + func_display = f"{func_display:<{func_width}}" + func_color = A_REVERSE | A_BOLD if is_selected else color_func + self.add_str(line, col, func_display, func_color) + col += func_width + 2 + + # File:line column + if col < width - 10: + simplified_path = self.collector.simplify_path(filename) + file_line = f"{simplified_path}:{lineno}" + remaining_width = width - col - 1 + file_color = A_REVERSE | A_BOLD if is_selected else color_file + self.add_str( + line, col, file_line[:remaining_width], file_color + ) + + line += 1 + + return line + + +class FooterWidget(Widget): + """Widget for rendering the footer section (legend and controls).""" + + def __init__(self, display, colors, collector): + """ + Initialize footer widget. + + Args: + display: DisplayInterface implementation + colors: Dictionary of color attributes + collector: Reference to LiveStatsCollector for accessing state + """ + super().__init__(display, colors) + self.collector = collector + + def render(self, line, width, **kwargs): + """ + Render the footer at the specified position. + + Args: + line: Starting line number (should be height - 2) + width: Available width + + Returns: + Next available line number + """ + A_DIM = self.display.get_attr("A_DIM") + A_BOLD = self.display.get_attr("A_BOLD") + + # Legend line + legend = "nsamples: direct/cumulative (direct=executing, cumulative=on stack)" + self.add_str(line, 0, legend[: width - 1], A_DIM) + line += 1 + + # Controls line with status + sort_names = { + "tottime": "Total Time", + "nsamples": "Direct Samples", + "cumtime": "Cumulative Time", + "sample_pct": "Sample %", + "cumul_pct": "Cumulative %", + } + sort_display = sort_names.get( + self.collector.sort_by, self.collector.sort_by + ) + + # Build status indicators + status = [] + if self.collector.finished: + status.append("[PROFILING FINISHED - Press 'q' to quit]") + elif self.collector.paused: + status.append("[PAUSED]") + if self.collector.filter_pattern: + status.append( + f"[Filter: {self.collector.filter_pattern} (c to clear)]" + ) + # Show trend colors status if disabled + if self.collector._trend_tracker is not None and not self.collector._trend_tracker.enabled: + status.append("[Trend colors: OFF]") + status_str = " ".join(status) + " " if status else "" + + if self.collector.finished: + footer = f"{status_str}" + else: + footer = f"{status_str}Sort: {sort_display} | 't':mode 'x':trends ←→:thread 'h':help 'q':quit" + self.add_str( + line, + 0, + footer[: width - 1], + A_BOLD + if (self.collector.paused or self.collector.finished) + else A_DIM, + ) + + return line + 1 + + def render_filter_input_prompt(self, line, width): + """Draw the filter input prompt at the bottom of the screen.""" + A_BOLD = self.display.get_attr("A_BOLD") + A_REVERSE = self.display.get_attr("A_REVERSE") + + # Draw prompt on last line + prompt = f"Function filter: {self.collector.filter_input_buffer}_" + self.add_str(line, 0, prompt[: width - 1], A_REVERSE | A_BOLD) + + +class HelpWidget(Widget): + """Widget for rendering the help screen overlay.""" + + def render(self, line, width, **kwargs): + """ + Render the help screen. + + Args: + line: Starting line number (ignored, help is centered) + width: Available width + kwargs: Must contain 'height' key + + Returns: + Next available line number (not used for overlays) + """ + height = kwargs["height"] + A_BOLD = self.display.get_attr("A_BOLD") + A_NORMAL = self.display.get_attr("A_NORMAL") + + help_lines = [ + ("Tachyon Profiler - Interactive Commands", A_BOLD), + ("", A_NORMAL), + ("Navigation & Display:", A_BOLD), + (" s - Cycle through sort modes (forward)", A_NORMAL), + (" S - Cycle through sort modes (backward)", A_NORMAL), + (" t - Toggle view mode (ALL / per-thread)", A_NORMAL), + (" x - Toggle trend colors (on/off)", A_NORMAL), + (" j/k or ↑/↓ - Select next/previous function (--opcodes)", A_NORMAL), + (" ← / → - Cycle through threads", A_NORMAL), + (" + - Faster display refresh rate", A_NORMAL), + (" - - Slower display refresh rate", A_NORMAL), + ("", A_NORMAL), + ("Control:", A_BOLD), + (" p - Freeze display (snapshot)", A_NORMAL), + (" r - Reset all statistics", A_NORMAL), + ("", A_NORMAL), + ("Filtering:", A_BOLD), + (" / - Enter function filter (substring)", A_NORMAL), + (" c - Clear filter", A_NORMAL), + (" ESC - Cancel filter input", A_NORMAL), + ("", A_NORMAL), + ("Other:", A_BOLD), + (" h or ? - Show/hide this help", A_NORMAL), + (" q - Quit profiler", A_NORMAL), + ("", A_NORMAL), + ("Press any key to close this help screen", A_BOLD), + ] + + start_line = (height - len(help_lines)) // 2 + for i, (text, attr) in enumerate(help_lines): + if start_line + i < height - 1: + col = 2 # Left-align with small margin + self.add_str(start_line + i, col, text[: width - 3], attr) + + return line # Not used for overlays + + +class OpcodePanel(Widget): + """Widget for displaying opcode statistics for a selected function.""" + + def __init__(self, display, colors, collector): + super().__init__(display, colors) + self.collector = collector + + def render(self, line, width, **kwargs): + """Render opcode statistics panel. + + Args: + line: Starting line number + width: Available width + kwargs: Must contain 'stats_list', 'height' + + Returns: + Next available line number + """ + stats_list = kwargs.get("stats_list", []) + height = kwargs.get("height", 24) + selected_row = self.collector.selected_row + scroll_offset = getattr(self.collector, 'scroll_offset', 0) + + A_BOLD = self.display.get_attr("A_BOLD") + A_NORMAL = self.display.get_attr("A_NORMAL") + color_cyan = self.colors.get("color_cyan", A_NORMAL) + color_yellow = self.colors.get("color_yellow", A_NORMAL) + color_magenta = self.colors.get("color_magenta", A_NORMAL) + + # Get the selected function from stats_list (accounting for scroll) + actual_index = scroll_offset + selected_row + if not stats_list or actual_index >= len(stats_list): + self.add_str(line, 0, "No function selected (use j/k to select)", A_NORMAL) + return line + 1 + + selected_stat = stats_list[actual_index] + func = selected_stat["func"] + filename, lineno, funcname = func + + # Get opcode stats for this function + opcode_stats = self.collector.opcode_stats.get(func, {}) + + if not opcode_stats: + self.add_str(line, 0, f"No opcode data for {funcname}() (requires --opcodes)", A_NORMAL) + return line + 1 + + # Sort opcodes by count + sorted_opcodes = sorted(opcode_stats.items(), key=lambda x: -x[1]) + total_opcode_samples = sum(opcode_stats.values()) + + # Draw header + header = f"─── Opcodes for {funcname}() " + header += "─" * max(0, width - len(header) - 1) + self.add_str(line, 0, header[:width-1], color_cyan | A_BOLD) + line += 1 + + # Calculate max samples for bar scaling + max_count = sorted_opcodes[0][1] if sorted_opcodes else 1 + + # Draw opcode rows (limit to available space) + max_rows = min(8, height - line - 3) # Leave room for footer + bar_width = 20 + + for i, (opcode_num, count) in enumerate(sorted_opcodes[:max_rows]): + if line >= height - 3: + break + + opcode_info = get_opcode_info(opcode_num) + is_specialized = opcode_info["is_specialized"] + name_display = format_opcode(opcode_num) + + pct = (count / total_opcode_samples * 100) if total_opcode_samples > 0 else 0 + + # Draw bar + bar_fill = int((count / max_count) * bar_width) if max_count > 0 else 0 + bar = "█" * bar_fill + "░" * (bar_width - bar_fill) + + # Format: [████████░░░░] LOAD_ATTR 45.2% (1234) + # Specialized opcodes shown in magenta, base opcodes in yellow + name_color = color_magenta if is_specialized else color_yellow + + row_text = f"[{bar}] {name_display:<35} {pct:>5.1f}% ({count:>6})" + self.add_str(line, 2, row_text[:width-3], name_color) + line += 1 + + # Show "..." if more opcodes exist + if len(sorted_opcodes) > max_rows: + remaining = len(sorted_opcodes) - max_rows + self.add_str(line, 2, f"... and {remaining} more opcodes", A_NORMAL) + line += 1 + + return line diff --git a/Lib/profiling/sampling/module_utils.py b/Lib/profiling/sampling/module_utils.py new file mode 100644 index 00000000000..dfde2b28ab2 --- /dev/null +++ b/Lib/profiling/sampling/module_utils.py @@ -0,0 +1,102 @@ +"""Utilities for extracting module names from file paths.""" + +import os +import site +import sys +from pathlib import Path + + +def get_python_path_info(): + """Get information about Python's search paths. + + Returns: + dict: Dictionary containing stdlib path, site-packages paths, and sys.path entries. + """ + info = { + 'stdlib': None, + 'site_packages': [], + 'sys_path': [] + } + + # Get standard library path from os module location + try: + if hasattr(os, '__file__') and os.__file__: + info['stdlib'] = Path(os.__file__).parent + except (AttributeError, OSError): + pass # Silently continue if we can't determine stdlib path + + # Get site-packages directories + site_packages = [] + try: + site_packages.extend(Path(p) for p in site.getsitepackages()) + except (AttributeError, OSError): + pass # Continue without site packages if unavailable + + # Get user site-packages + try: + user_site = site.getusersitepackages() + if user_site and Path(user_site).exists(): + site_packages.append(Path(user_site)) + except (AttributeError, OSError): + pass # Continue without user site packages + + info['site_packages'] = site_packages + info['sys_path'] = [Path(p) for p in sys.path if p] + + return info + + +def extract_module_name(filename, path_info): + """Extract Python module name and type from file path. + + Args: + filename: Path to the Python file + path_info: Dictionary from get_python_path_info() + + Returns: + tuple: (module_name, module_type) where module_type is one of: + 'stdlib', 'site-packages', 'project', or 'other' + """ + if not filename: + return ('unknown', 'other') + + try: + file_path = Path(filename) + except (ValueError, OSError): + return (str(filename), 'other') + + # Check if it's in stdlib + if path_info['stdlib'] and file_path.is_relative_to(path_info['stdlib']): + return (_path_to_module(file_path.relative_to(path_info['stdlib'])), 'stdlib') + + # Check site-packages + for site_pkg in path_info['site_packages']: + if file_path.is_relative_to(site_pkg): + return (_path_to_module(file_path.relative_to(site_pkg)), 'site-packages') + + # Check other sys.path entries (project files) + if not str(file_path).startswith(('<', '[')): # Skip special files + for path_entry in path_info['sys_path']: + if file_path.is_relative_to(path_entry): + return (_path_to_module(file_path.relative_to(path_entry)), 'project') + + # Fallback: just use the filename + return (_path_to_module(file_path), 'other') + + +def _path_to_module(path): + if isinstance(path, str): + path = Path(path) + + # Remove .py extension + if path.suffix == '.py': + path = path.with_suffix('') + + # Convert path separators to dots, stripping root/drive (e.g. "/" or "C:\") + parts = [p for p in path.parts if p != path.root and p != path.drive] + + # Handle __init__ files - they represent the package itself + if parts and parts[-1] == '__init__': + parts = parts[:-1] + + return '.'.join(parts) if parts else path.stem diff --git a/Lib/profiling/sampling/opcode_utils.py b/Lib/profiling/sampling/opcode_utils.py new file mode 100644 index 00000000000..71b35383da1 --- /dev/null +++ b/Lib/profiling/sampling/opcode_utils.py @@ -0,0 +1,94 @@ +"""Opcode utilities for bytecode-level profiler visualization. + +This module provides utilities to get opcode names and detect specialization +status using the opcode module's metadata. Used by heatmap and flamegraph +collectors to display which bytecode instructions are executing at each +source line, including Python's adaptive specialization optimizations. +""" + +import opcode + +# Build opcode name mapping: opcode number -> opcode name +# This includes both standard opcodes and specialized variants (Python 3.11+) +_OPCODE_NAMES = dict(enumerate(opcode.opname)) +if hasattr(opcode, "_specialized_opmap"): + for name, op in opcode._specialized_opmap.items(): + _OPCODE_NAMES[op] = name + +# Build deopt mapping: specialized opcode number -> base opcode number +# Python 3.11+ uses adaptive specialization where generic opcodes like +# LOAD_ATTR can be replaced at runtime with specialized variants like +# LOAD_ATTR_INSTANCE_VALUE. This mapping lets us show both forms. +_DEOPT_MAP = {} +if hasattr(opcode, "_specializations") and hasattr( + opcode, "_specialized_opmap" +): + for base_name, variant_names in opcode._specializations.items(): + base_opcode = opcode.opmap.get(base_name) + if base_opcode is not None: + for variant_name in variant_names: + variant_opcode = opcode._specialized_opmap.get(variant_name) + if variant_opcode is not None: + _DEOPT_MAP[variant_opcode] = base_opcode + + +def get_opcode_info(opcode_num): + """Get opcode name and specialization info from an opcode number. + + Args: + opcode_num: The opcode number (0-255 or higher for specialized) + + Returns: + A dict with keys: + - 'opname': The opcode name (e.g., 'LOAD_ATTR_INSTANCE_VALUE') + - 'base_opname': The base opcode name (e.g., 'LOAD_ATTR') + - 'is_specialized': True if this is a specialized instruction + """ + opname = _OPCODE_NAMES.get(opcode_num) + if opname is None: + return { + "opname": f"<{opcode_num}>", + "base_opname": f"<{opcode_num}>", + "is_specialized": False, + } + + base_opcode = _DEOPT_MAP.get(opcode_num) + if base_opcode is not None: + base_opname = _OPCODE_NAMES.get(base_opcode, f"<{base_opcode}>") + return { + "opname": opname, + "base_opname": base_opname, + "is_specialized": True, + } + + return { + "opname": opname, + "base_opname": opname, + "is_specialized": False, + } + + +def format_opcode(opcode_num): + """Format an opcode for display, showing base opcode for specialized ones. + + Args: + opcode_num: The opcode number (0-255 or higher for specialized) + + Returns: + A formatted string like 'LOAD_ATTR' or 'LOAD_ATTR_INSTANCE_VALUE (LOAD_ATTR)' + """ + info = get_opcode_info(opcode_num) + if info["is_specialized"]: + return f"{info['opname']} ({info['base_opname']})" + return info["opname"] + + +def get_opcode_mapping(): + """Get opcode name and deopt mappings for JavaScript consumption. + + Returns: + A dict with keys: + - 'names': Dict mapping opcode numbers to opcode names + - 'deopt': Dict mapping specialized opcode numbers to base opcode numbers + """ + return {"names": _OPCODE_NAMES, "deopt": _DEOPT_MAP} diff --git a/Lib/profiling/sampling/pstats_collector.py b/Lib/profiling/sampling/pstats_collector.py new file mode 100644 index 00000000000..0f31d93582b --- /dev/null +++ b/Lib/profiling/sampling/pstats_collector.py @@ -0,0 +1,429 @@ +import collections +import marshal +import pstats +lazy from _colorize import get_colors + +from .collector import Collector, extract_lineno +from .constants import MICROSECONDS_PER_SECOND, PROFILING_MODE_CPU + + +class PstatsCollector(Collector): + aggregating = True + + def __init__(self, sample_interval_usec, *, skip_idle=False): + self.result = collections.defaultdict( + lambda: dict(total_rec_calls=0, direct_calls=0, cumulative_calls=0) + ) + self.stats = {} + self.sample_interval_usec = sample_interval_usec + self.callers = collections.defaultdict( + lambda: collections.defaultdict(int) + ) + self.skip_idle = skip_idle + self._seen_locations = set() + + def _process_frames(self, frames, weight=1): + """Process a single thread's frame stack.""" + if not frames: + return + + self._seen_locations.clear() + + # Process each frame in the stack to track cumulative calls + # frame.location is int, tuple (lineno, end_lineno, col_offset, end_col_offset), or None + for frame in frames: + lineno = extract_lineno(frame.location) + location = (frame.filename, lineno, frame.funcname) + if location not in self._seen_locations: + self._seen_locations.add(location) + self.result[location]["cumulative_calls"] += weight + + # The top frame gets counted as an inline call (directly executing) + top_lineno = extract_lineno(frames[0].location) + top_location = (frames[0].filename, top_lineno, frames[0].funcname) + self.result[top_location]["direct_calls"] += weight + + # Track caller-callee relationships for call graph + for i in range(1, len(frames)): + callee_frame = frames[i - 1] + caller_frame = frames[i] + + callee_lineno = extract_lineno(callee_frame.location) + caller_lineno = extract_lineno(caller_frame.location) + callee = (callee_frame.filename, callee_lineno, callee_frame.funcname) + caller = (caller_frame.filename, caller_lineno, caller_frame.funcname) + + self.callers[callee][caller] += weight + + def collect(self, stack_frames, timestamps_us=None): + weight = len(timestamps_us) if timestamps_us else 1 + for frames, _ in self._iter_stacks(stack_frames, skip_idle=self.skip_idle): + self._process_frames(frames, weight=weight) + + def export(self, filename): + self.create_stats() + self._dump_stats(filename) + return True + + def _dump_stats(self, file): + stats_with_marker = dict(self.stats) + stats_with_marker[("__sampled__",)] = True + with open(file, "wb") as f: + marshal.dump(stats_with_marker, f) + + # Needed for compatibility with pstats.Stats + def create_stats(self): + sample_interval_sec = self.sample_interval_usec / MICROSECONDS_PER_SECOND + callers = {} + for fname, call_counts in self.result.items(): + total = call_counts["direct_calls"] * sample_interval_sec + cumulative_calls = call_counts["cumulative_calls"] + cumulative = cumulative_calls * sample_interval_sec + callers = dict(self.callers.get(fname, {})) + self.stats[fname] = ( + call_counts["direct_calls"], # cc = direct calls for sample percentage + cumulative_calls, # nc = cumulative calls for cumulative percentage + total, + cumulative, + callers, + ) + + def print_stats(self, sort=-1, limit=None, show_summary=True, mode=None): + """Print formatted statistics to stdout.""" + # Create stats object + stats = pstats.SampledStats(self).strip_dirs() + if not stats.stats: + print("No samples were collected.") + if mode == PROFILING_MODE_CPU: + print("This can happen in CPU mode when all threads are idle.") + return + + # Get the stats data + stats_list = [] + for func, ( + direct_calls, + cumulative_calls, + total_time, + cumulative_time, + callers, + ) in stats.stats.items(): + stats_list.append( + ( + func, + direct_calls, + cumulative_calls, + total_time, + cumulative_time, + callers, + ) + ) + + # Calculate total samples for percentage calculations (using direct_calls) + total_samples = sum( + direct_calls for _, direct_calls, _, _, _, _ in stats_list + ) + + # Sort based on the requested field + sort_field = sort + if sort_field == -1: # stdname + stats_list.sort(key=lambda x: str(x[0])) + elif sort_field == 0: # nsamples (direct samples) + stats_list.sort(key=lambda x: x[1], reverse=True) # direct_calls + elif sort_field == 1: # tottime + stats_list.sort(key=lambda x: x[3], reverse=True) # total_time + elif sort_field == 2: # cumtime + stats_list.sort(key=lambda x: x[4], reverse=True) # cumulative_time + elif sort_field == 3: # sample% + stats_list.sort( + key=lambda x: (x[1] / total_samples * 100) + if total_samples > 0 + else 0, + reverse=True, # direct_calls percentage + ) + elif sort_field == 4: # cumul% + stats_list.sort( + key=lambda x: (x[2] / total_samples * 100) + if total_samples > 0 + else 0, + reverse=True, # cumulative_calls percentage + ) + elif sort_field == 5: # nsamples (cumulative samples) + stats_list.sort(key=lambda x: x[2], reverse=True) # cumulative_calls + + # Apply limit if specified + if limit is not None: + stats_list = stats_list[:limit] + + # Determine the best unit for time columns based on maximum values + max_total_time = max( + (total_time for _, _, _, total_time, _, _ in stats_list), default=0 + ) + max_cumulative_time = max( + (cumulative_time for _, _, _, _, cumulative_time, _ in stats_list), + default=0, + ) + + total_time_unit, total_time_scale = self._determine_best_unit(max_total_time) + cumulative_time_unit, cumulative_time_scale = self._determine_best_unit( + max_cumulative_time + ) + + # Define column widths for consistent alignment + col_widths = { + "nsamples": 15, # "nsamples" column (inline/cumulative format) + "sample_pct": 8, # "sample%" column + "tottime": max(12, len(f"tottime ({total_time_unit})")), + "cum_pct": 8, # "cumul%" column + "cumtime": max(12, len(f"cumtime ({cumulative_time_unit})")), + } + + # Print header with colors and proper alignment + ANSIColors = get_colors() + + print(f"{ANSIColors.BOLD_BLUE}Profile Stats:{ANSIColors.RESET}") + + header_nsamples = f"{ANSIColors.BOLD_BLUE}{'nsamples':>{col_widths['nsamples']}}{ANSIColors.RESET}" + header_sample_pct = f"{ANSIColors.BOLD_BLUE}{'sample%':>{col_widths['sample_pct']}}{ANSIColors.RESET}" + header_tottime = f"{ANSIColors.BOLD_BLUE}{f'tottime ({total_time_unit})':>{col_widths['tottime']}}{ANSIColors.RESET}" + header_cum_pct = f"{ANSIColors.BOLD_BLUE}{'cumul%':>{col_widths['cum_pct']}}{ANSIColors.RESET}" + header_cumtime = f"{ANSIColors.BOLD_BLUE}{f'cumtime ({cumulative_time_unit})':>{col_widths['cumtime']}}{ANSIColors.RESET}" + header_filename = ( + f"{ANSIColors.BOLD_BLUE}filename:lineno(function){ANSIColors.RESET}" + ) + + print( + f"{header_nsamples} {header_sample_pct} {header_tottime} {header_cum_pct} {header_cumtime} {header_filename}" + ) + + # Print each line with proper alignment + for ( + func, + direct_calls, + cumulative_calls, + total_time, + cumulative_time, + callers, + ) in stats_list: + # Calculate percentages + sample_pct = ( + (direct_calls / total_samples * 100) if total_samples > 0 else 0 + ) + cum_pct = ( + (cumulative_calls / total_samples * 100) + if total_samples > 0 + else 0 + ) + + # Format values with proper alignment - always use A/B format + nsamples_str = f"{direct_calls}/{cumulative_calls}" + nsamples_str = f"{nsamples_str:>{col_widths['nsamples']}}" + sample_pct_str = f"{sample_pct:{col_widths['sample_pct']}.1f}" + tottime = f"{total_time * total_time_scale:{col_widths['tottime']}.3f}" + cum_pct_str = f"{cum_pct:{col_widths['cum_pct']}.1f}" + cumtime = f"{cumulative_time * cumulative_time_scale:{col_widths['cumtime']}.3f}" + + # Format the function name with colors + func_name = ( + f"{ANSIColors.GREEN}{func[0]}{ANSIColors.RESET}:" + f"{ANSIColors.YELLOW}{func[1]}{ANSIColors.RESET}(" + f"{ANSIColors.CYAN}{func[2]}{ANSIColors.RESET})" + ) + + # Print the formatted line with consistent spacing + print( + f"{nsamples_str} {sample_pct_str} {tottime} {cum_pct_str} {cumtime} {func_name}" + ) + + # Print legend + print(f"\n{ANSIColors.BOLD_BLUE}Legend:{ANSIColors.RESET}") + print( + f" {ANSIColors.YELLOW}nsamples{ANSIColors.RESET}: Direct/Cumulative samples (direct executing / on call stack)" + ) + print( + f" {ANSIColors.YELLOW}sample%{ANSIColors.RESET}: Percentage of total samples this function was directly executing" + ) + print( + f" {ANSIColors.YELLOW}tottime{ANSIColors.RESET}: Estimated total time spent directly in this function" + ) + print( + f" {ANSIColors.YELLOW}cumul%{ANSIColors.RESET}: Percentage of total samples when this function was on the call stack" + ) + print( + f" {ANSIColors.YELLOW}cumtime{ANSIColors.RESET}: Estimated cumulative time (including time in called functions)" + ) + print( + f" {ANSIColors.YELLOW}filename:lineno(function){ANSIColors.RESET}: Function location and name" + ) + + # Print summary of interesting functions if enabled + if show_summary and stats_list: + self._print_summary(stats_list, total_samples) + + @staticmethod + def _determine_best_unit(max_value): + """Determine the best unit (s, ms, μs) and scale factor for a maximum value.""" + if max_value >= 1.0: + return "s", 1.0 + elif max_value >= 0.001: + return "ms", 1000.0 + else: + return "μs", float(MICROSECONDS_PER_SECOND) + + def _print_summary(self, stats_list, total_samples): + """Print summary of interesting functions.""" + ANSIColors = get_colors() + + print( + f"\n{ANSIColors.BOLD_BLUE}Summary of Interesting Functions:{ANSIColors.RESET}" + ) + + # Aggregate stats by fully qualified function name (ignoring line numbers) + func_aggregated = {} + for ( + func, + direct_calls, + cumulative_calls, + total_time, + cumulative_time, + callers, + ) in stats_list: + # Use filename:function_name as the key to get fully qualified name + qualified_name = f"{func[0]}:{func[2]}" + if qualified_name not in func_aggregated: + func_aggregated[qualified_name] = [ + 0, + 0, + 0, + 0, + ] # direct_calls, cumulative_calls, total_time, cumulative_time + func_aggregated[qualified_name][0] += direct_calls + func_aggregated[qualified_name][1] += cumulative_calls + func_aggregated[qualified_name][2] += total_time + func_aggregated[qualified_name][3] += cumulative_time + + # Convert aggregated data back to list format for processing + aggregated_stats = [] + for qualified_name, ( + prim_calls, + total_calls, + total_time, + cumulative_time, + ) in func_aggregated.items(): + # Parse the qualified name back to filename and function name + if ":" in qualified_name: + filename, func_name = qualified_name.rsplit(":", 1) + else: + filename, func_name = "", qualified_name + # Create a dummy func tuple with filename and function name for display + dummy_func = (filename, "", func_name) + aggregated_stats.append( + ( + dummy_func, + prim_calls, + total_calls, + total_time, + cumulative_time, + {}, + ) + ) + + # Determine best units for summary metrics + max_total_time = max( + (total_time for _, _, _, total_time, _, _ in aggregated_stats), + default=0, + ) + max_cumulative_time = max( + ( + cumulative_time + for _, _, _, _, cumulative_time, _ in aggregated_stats + ), + default=0, + ) + + total_unit, total_scale = self._determine_best_unit(max_total_time) + cumulative_unit, cumulative_scale = self._determine_best_unit( + max_cumulative_time + ) + + def _format_func_name(func): + """Format function name with colors.""" + return ( + f"{ANSIColors.GREEN}{func[0]}{ANSIColors.RESET}:" + f"{ANSIColors.YELLOW}{func[1]}{ANSIColors.RESET}(" + f"{ANSIColors.CYAN}{func[2]}{ANSIColors.RESET})" + ) + + def _print_top_functions(stats_list, title, key_func, format_line, n=3): + """Print top N functions sorted by key_func with formatted output.""" + print(f"\n{ANSIColors.BOLD_BLUE}{title}:{ANSIColors.RESET}") + sorted_stats = sorted(stats_list, key=key_func, reverse=True) + for stat in sorted_stats[:n]: + if line := format_line(stat): + print(f" {line}") + + # Functions with highest direct/cumulative ratio (hot spots) + def format_hotspots(stat): + func, direct_calls, cumulative_calls, total_time, _, _ = stat + if direct_calls > 0 and cumulative_calls > 0: + ratio = direct_calls / cumulative_calls + direct_pct = ( + (direct_calls / total_samples * 100) + if total_samples > 0 + else 0 + ) + return ( + f"{ratio:.3f} direct/cumulative ratio, " + f"{direct_pct:.1f}% direct samples: {_format_func_name(func)}" + ) + return None + + _print_top_functions( + aggregated_stats, + "Functions with Highest Direct/Cumulative Ratio (Hot Spots)", + key_func=lambda x: (x[1] / x[2]) if x[2] > 0 else 0, + format_line=format_hotspots, + ) + + # Functions with highest call frequency (cumulative/direct difference) + def format_call_frequency(stat): + func, direct_calls, cumulative_calls, total_time, _, _ = stat + if cumulative_calls > direct_calls: + call_frequency = cumulative_calls - direct_calls + cum_pct = ( + (cumulative_calls / total_samples * 100) + if total_samples > 0 + else 0 + ) + return ( + f"{call_frequency:d} indirect calls, " + f"{cum_pct:.1f}% total stack presence: {_format_func_name(func)}" + ) + return None + + _print_top_functions( + aggregated_stats, + "Functions with Highest Call Frequency (Indirect Calls)", + key_func=lambda x: x[2] - x[1], # Sort by (cumulative - direct) + format_line=format_call_frequency, + ) + + # Functions with highest cumulative-to-direct multiplier (call magnification) + def format_call_magnification(stat): + func, direct_calls, cumulative_calls, total_time, _, _ = stat + if direct_calls > 0 and cumulative_calls > direct_calls: + multiplier = cumulative_calls / direct_calls + indirect_calls = cumulative_calls - direct_calls + return ( + f"{multiplier:.1f}x call magnification, " + f"{indirect_calls:d} indirect calls from {direct_calls:d} direct: {_format_func_name(func)}" + ) + return None + + _print_top_functions( + aggregated_stats, + "Functions with Highest Call Magnification (Cumulative/Direct)", + key_func=lambda x: (x[2] / x[1]) + if x[1] > 0 + else 0, # Sort by cumulative/direct ratio + format_line=format_call_magnification, + ) diff --git a/Lib/profiling/sampling/sample.py b/Lib/profiling/sampling/sample.py new file mode 100644 index 00000000000..702e00c87b9 --- /dev/null +++ b/Lib/profiling/sampling/sample.py @@ -0,0 +1,665 @@ +import _remote_debugging +import contextlib +import os +import statistics +import sys +import sysconfig +import time +from collections import deque +lazy from _colorize import get_colors + +from .pstats_collector import PstatsCollector +from .stack_collector import CollapsedStackCollector, FlamegraphCollector +from .heatmap_collector import HeatmapCollector +from .gecko_collector import GeckoCollector +from .binary_collector import BinaryCollector + + +@contextlib.contextmanager +def _pause_threads(unwinder, blocking): + """Context manager to pause/resume threads around sampling if blocking is True.""" + if blocking: + unwinder.pause_threads() + try: + yield + finally: + unwinder.resume_threads() + else: + yield + + +from .constants import ( + PROFILING_MODE_WALL, + PROFILING_MODE_CPU, + PROFILING_MODE_GIL, + PROFILING_MODE_ALL, + PROFILING_MODE_EXCEPTION, +) +from ._format_utils import fmt +try: + from .live_collector import LiveStatsCollector +except ImportError: + LiveStatsCollector = None + +_FREE_THREADED_BUILD = sysconfig.get_config_var("Py_GIL_DISABLED") is not None + +# Minimum number of samples required before showing the TUI +# If fewer samples are collected, we skip the TUI and just print a message +MIN_SAMPLES_FOR_TUI = 200 + +# Maximum number of consecutive identical samples to keep before flushing. +MAX_PENDING_SAMPLES = 8192 + + +def _resolve_python_pid(pid): + """On Windows, if pid is a venvlauncher process, return the child Python PID. + + The venvlauncher (used as python.exe in venvs) spawns the real Python + interpreter as a child process via CreateProcessW. The RemoteUnwinder + needs the child's PID, not the launcher's. + + Returns the original pid if not on Windows, not a venv launcher, + or no child process is found. + """ + if os.name != "nt" or sys.prefix == sys.base_prefix: + return pid + try: + children = _remote_debugging.get_child_pids(pid, recursive=False) + python_children = [ + child for child in children + if _remote_debugging.is_python_process(child) + ] + if len(python_children) == 1: + return python_children[0] + except (OSError, RuntimeError) as err: + raise SystemExit( + f"Failed to initialize profiler from virtualenv: {err}\n" + f"Try running with the base interpreter: {sys._base_executable}" + ) from err + return pid + + +class SampleProfiler: + def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MODE_WALL, native=False, gc=True, opcodes=False, skip_non_matching_threads=True, collect_stats=False, blocking=False): + self.pid = _resolve_python_pid(pid) + self.sample_interval_usec = sample_interval_usec + self.all_threads = all_threads + self.mode = mode # Store mode for later use + self.collect_stats = collect_stats + self.blocking = blocking + try: + self.unwinder = self._new_unwinder(native, gc, opcodes, skip_non_matching_threads) + except RuntimeError as err: + raise SystemExit(err) from err + # Track sample intervals and total sample count + self.sample_intervals = deque(maxlen=100) + self.total_samples = 0 + self.realtime_stats = False + + def _new_unwinder(self, native, gc, opcodes, skip_non_matching_threads): + kwargs = {} + if _FREE_THREADED_BUILD or self.all_threads: + kwargs['all_threads'] = self.all_threads + else: + kwargs['only_active_thread'] = bool(self.all_threads) + + return _remote_debugging.RemoteUnwinder( + self.pid, + mode=self.mode, + native=native, + gc=gc, + opcodes=opcodes, + skip_non_matching_threads=skip_non_matching_threads, + cache_frames=True, + stats=self.collect_stats, + **kwargs + ) + + def _get_stack_trace(self, async_aware=None): + with _pause_threads(self.unwinder, self.blocking): + if async_aware == "all": + return self.unwinder.get_all_awaited_by() + if async_aware == "running": + return self.unwinder.get_async_stack_trace() + return self.unwinder.get_stack_trace() + + def dump_stack(self, *, async_aware=None): + """Return a single stack snapshot from the target process.""" + return self._get_stack_trace(async_aware=async_aware) + + def sample(self, collector, duration_sec=None, *, async_aware=False): + sample_interval_sec = self.sample_interval_usec / 1_000_000 + num_samples = 0 + errors = 0 + interrupted = False + running_time_sec = 0 + start_time = next_time = time.perf_counter() + last_sample_time = start_time + realtime_update_interval = 1.0 # Update every second + last_realtime_update = start_time + aggregating = getattr(collector, 'aggregating', False) is True + prev_stack = None + pending_count = 0 + pending_timestamps = [] if aggregating else None + + def flush_pending(): + nonlocal pending_count, pending_timestamps + if pending_count == 0: + return + pending_count = 0 + ts = pending_timestamps + pending_timestamps = [] + collector.collect(prev_stack, timestamps_us=ts) + + try: + while duration_sec is None or running_time_sec < duration_sec: + # Check if live collector wants to stop + if hasattr(collector, 'running') and not collector.running: + break + + current_time = time.perf_counter() + current_time_us = int(current_time * 1_000_000) + if next_time > current_time: + sleep_time = (next_time - current_time) * 0.9 + if sleep_time > 0.0001: + time.sleep(sleep_time) + elif next_time < current_time: + try: + stack_frames = self._get_stack_trace( + async_aware=async_aware + ) + if aggregating: + if stack_frames != prev_stack: + flush_pending() + prev_stack = stack_frames + pending_count += 1 + pending_timestamps.append(current_time_us) + if pending_count >= MAX_PENDING_SAMPLES: + flush_pending() + else: + collector.collect(stack_frames) + except ProcessLookupError as e: + running_time_sec = current_time - start_time + break + except (RuntimeError, UnicodeDecodeError, MemoryError, OSError): + flush_pending() + collector.collect_failed_sample() + errors += 1 + prev_stack = None + except Exception as e: + if not _is_process_running(self.pid): + break + raise e from None + + # Track actual sampling intervals for real-time stats + if num_samples > 0: + actual_interval = current_time - last_sample_time + self.sample_intervals.append( + 1.0 / actual_interval + ) # Convert to Hz + self.total_samples += 1 + + # Print real-time statistics if enabled + if ( + self.realtime_stats + and (current_time - last_realtime_update) + >= realtime_update_interval + ): + self._print_realtime_stats() + last_realtime_update = current_time + + last_sample_time = current_time + num_samples += 1 + next_time += sample_interval_sec + + running_time_sec = time.perf_counter() - start_time + except KeyboardInterrupt: + interrupted = True + running_time_sec = time.perf_counter() - start_time + print("Interrupted by user.") + finally: + flush_pending() + + # Clear real-time stats line if it was being displayed + if self.realtime_stats and len(self.sample_intervals) > 0: + print() # Add newline after real-time stats + + sample_rate = num_samples / running_time_sec if running_time_sec > 0 else 0 + error_rate = (errors / num_samples) * 100 if num_samples > 0 else 0 + expected_samples = int(running_time_sec / sample_interval_sec) + missed_samples = (expected_samples - num_samples) / expected_samples * 100 if expected_samples > 0 else 0 + + # Don't print stats for live mode (curses is handling display) + is_live_mode = LiveStatsCollector is not None and isinstance(collector, LiveStatsCollector) + if not is_live_mode: + s = "" if num_samples == 1 else "s" + print(f"Captured {num_samples:n} sample{s} in {fmt(running_time_sec, 2)} seconds") + print(f"Sample rate: {fmt(sample_rate, 2)} samples/sec") + print(f"Error rate: {fmt(error_rate, 2)}") + + # Print unwinder stats if stats collection is enabled + if self.collect_stats: + self._print_unwinder_stats() + + if isinstance(collector, BinaryCollector): + self._print_binary_stats(collector) + + # Pass stats to flamegraph collector if it's the right type + if hasattr(collector, 'set_stats'): + collector.set_stats(self.sample_interval_usec, running_time_sec, sample_rate, error_rate, missed_samples, mode=self.mode) + + if num_samples < expected_samples and not is_live_mode and not interrupted: + print( + f"Warning: missed {expected_samples - num_samples} samples " + f"from the expected total of {expected_samples} " + f"({fmt((expected_samples - num_samples) / expected_samples * 100, 2)}%)" + ) + + def _print_realtime_stats(self): + """Print real-time sampling statistics.""" + if len(self.sample_intervals) < 2: + return + + # Calculate statistics on the Hz values (deque automatically maintains rolling window) + hz_values = list(self.sample_intervals) + mean_hz = statistics.mean(hz_values) + min_hz = min(hz_values) + max_hz = max(hz_values) + + # Calculate microseconds per sample for all metrics (1/Hz * 1,000,000) + mean_us_per_sample = (1.0 / mean_hz) * 1_000_000 if mean_hz > 0 else 0 + min_us_per_sample = ( + (1.0 / max_hz) * 1_000_000 if max_hz > 0 else 0 + ) # Min time = Max Hz + max_us_per_sample = ( + (1.0 / min_hz) * 1_000_000 if min_hz > 0 else 0 + ) # Max time = Min Hz + + # Build cache stats string if stats collection is enabled + ANSIColors = get_colors() + + cache_stats_str = "" + if self.collect_stats: + try: + stats = self.unwinder.get_stats() + hits = stats.get('frame_cache_hits', 0) + partial = stats.get('frame_cache_partial_hits', 0) + misses = stats.get('frame_cache_misses', 0) + total = hits + partial + misses + if total > 0: + hit_pct = (hits + partial) / total * 100 + cache_stats_str = f" {ANSIColors.MAGENTA}Cache: {fmt(hit_pct)}% ({hits}+{partial}/{misses}){ANSIColors.RESET}" + except RuntimeError: + pass + + # Clear line and print stats + print( + f"\r\033[K{ANSIColors.BOLD_BLUE}Stats:{ANSIColors.RESET} " + f"{ANSIColors.YELLOW}{fmt(mean_hz)}Hz ({fmt(mean_us_per_sample)}µs){ANSIColors.RESET} " + f"{ANSIColors.GREEN}Min: {fmt(min_hz)}Hz{ANSIColors.RESET} " + f"{ANSIColors.RED}Max: {fmt(max_hz)}Hz{ANSIColors.RESET} " + f"{ANSIColors.CYAN}N={self.total_samples}{ANSIColors.RESET}" + f"{cache_stats_str}", + end="", + flush=True, + ) + + def _print_unwinder_stats(self): + """Print unwinder statistics including cache performance.""" + try: + stats = self.unwinder.get_stats() + except RuntimeError: + return # Stats not enabled + + ANSIColors = get_colors() + + print(f"\n{ANSIColors.BOLD_BLUE}{'='*50}{ANSIColors.RESET}") + print(f"{ANSIColors.BOLD_BLUE}Unwinder Statistics:{ANSIColors.RESET}") + + # Frame cache stats + total_samples = stats.get('total_samples', 0) + frame_cache_hits = stats.get('frame_cache_hits', 0) + frame_cache_partial_hits = stats.get('frame_cache_partial_hits', 0) + frame_cache_misses = stats.get('frame_cache_misses', 0) + total_lookups = frame_cache_hits + frame_cache_partial_hits + frame_cache_misses + + # Calculate percentages + hits_pct = (frame_cache_hits / total_lookups * 100) if total_lookups > 0 else 0 + partial_pct = (frame_cache_partial_hits / total_lookups * 100) if total_lookups > 0 else 0 + misses_pct = (frame_cache_misses / total_lookups * 100) if total_lookups > 0 else 0 + + print(f" {ANSIColors.CYAN}Frame Cache:{ANSIColors.RESET}") + print(f" Total samples: {total_samples:n}") + print(f" Full hits: {frame_cache_hits:n} ({ANSIColors.GREEN}{fmt(hits_pct)}%{ANSIColors.RESET})") + print(f" Partial hits: {frame_cache_partial_hits:n} ({ANSIColors.YELLOW}{fmt(partial_pct)}%{ANSIColors.RESET})") + print(f" Misses: {frame_cache_misses:n} ({ANSIColors.RED}{fmt(misses_pct)}%{ANSIColors.RESET})") + + # Frame read stats + frames_from_cache = stats.get('frames_read_from_cache', 0) + frames_from_memory = stats.get('frames_read_from_memory', 0) + total_frames = frames_from_cache + frames_from_memory + cache_frame_pct = (frames_from_cache / total_frames * 100) if total_frames > 0 else 0 + memory_frame_pct = (frames_from_memory / total_frames * 100) if total_frames > 0 else 0 + + print(f" {ANSIColors.CYAN}Frame Reads:{ANSIColors.RESET}") + print(f" From cache: {frames_from_cache:n} ({ANSIColors.GREEN}{fmt(cache_frame_pct)}%{ANSIColors.RESET})") + print(f" From memory: {frames_from_memory:n} ({ANSIColors.RED}{fmt(memory_frame_pct)}%{ANSIColors.RESET})") + + # Code object cache stats + code_hits = stats.get('code_object_cache_hits', 0) + code_misses = stats.get('code_object_cache_misses', 0) + total_code = code_hits + code_misses + code_hits_pct = (code_hits / total_code * 100) if total_code > 0 else 0 + code_misses_pct = (code_misses / total_code * 100) if total_code > 0 else 0 + + print(f" {ANSIColors.CYAN}Code Object Cache:{ANSIColors.RESET}") + print(f" Hits: {code_hits:n} ({ANSIColors.GREEN}{fmt(code_hits_pct)}%{ANSIColors.RESET})") + print(f" Misses: {code_misses:n} ({ANSIColors.RED}{fmt(code_misses_pct)}%{ANSIColors.RESET})") + + batched_attempts = stats.get('batched_read_attempts', 0) + batched_successes = stats.get('batched_read_successes', 0) + batched_misses = stats.get('batched_read_misses', 0) + segments_requested = stats.get('batched_read_segments_requested', 0) + segments_completed = stats.get('batched_read_segments_completed', 0) + if batched_attempts > 0: + batched_success_rate = stats.get('batched_read_success_rate', 0.0) + batched_miss_rate = 100.0 - batched_success_rate + segment_completion_rate = stats.get( + 'batched_read_segment_completion_rate', 0.0 + ) + + print(f" {ANSIColors.CYAN}Batched Reads:{ANSIColors.RESET}") + print(f" Attempts: {batched_attempts:n}") + print( + f" Successes: {batched_successes:n} " + f"({ANSIColors.GREEN}{fmt(batched_success_rate)}%{ANSIColors.RESET})" + ) + print( + f" Misses: {batched_misses:n} " + f"({ANSIColors.RED}{fmt(batched_miss_rate)}%{ANSIColors.RESET})" + ) + print( + f" Segments read: {segments_completed:n}/{segments_requested:n} " + f"({ANSIColors.GREEN}{fmt(segment_completion_rate)}%{ANSIColors.RESET})" + ) + + # Memory operations + memory_reads = stats.get('memory_reads', 0) + memory_bytes = stats.get('memory_bytes_read', 0) + if memory_bytes >= 1024 * 1024: + memory_str = f"{fmt(memory_bytes / (1024 * 1024))} MB" + elif memory_bytes >= 1024: + memory_str = f"{fmt(memory_bytes / 1024)} KB" + else: + memory_str = f"{memory_bytes} B" + print(f" {ANSIColors.CYAN}Memory:{ANSIColors.RESET}") + print(f" Read operations: {memory_reads:n} ({memory_str})") + + # Stale invalidations + stale_invalidations = stats.get('stale_cache_invalidations', 0) + if stale_invalidations > 0: + print(f" {ANSIColors.YELLOW}Stale cache invalidations: {stale_invalidations}{ANSIColors.RESET}") + + def _print_binary_stats(self, collector): + """Print binary I/O encoding statistics.""" + try: + stats = collector.get_stats() + except (ValueError, RuntimeError): + return # Collector closed or stats unavailable + + ANSIColors = get_colors() + + print(f" {ANSIColors.CYAN}Binary Encoding:{ANSIColors.RESET}") + + repeat_records = stats.get('repeat_records', 0) + repeat_samples = stats.get('repeat_samples', 0) + full_records = stats.get('full_records', 0) + suffix_records = stats.get('suffix_records', 0) + pop_push_records = stats.get('pop_push_records', 0) + total_records = stats.get('total_records', 0) + + if total_records > 0: + repeat_pct = repeat_records / total_records * 100 + full_pct = full_records / total_records * 100 + suffix_pct = suffix_records / total_records * 100 + pop_push_pct = pop_push_records / total_records * 100 + else: + repeat_pct = full_pct = suffix_pct = pop_push_pct = 0 + + print(f" Records: {total_records:,}") + print(f" RLE repeat: {repeat_records:,} ({ANSIColors.GREEN}{repeat_pct:.1f}%{ANSIColors.RESET}) [{repeat_samples:,} samples]") + print(f" Full stack: {full_records:,} ({full_pct:.1f}%)") + print(f" Suffix match: {suffix_records:,} ({suffix_pct:.1f}%)") + print(f" Pop-push: {pop_push_records:,} ({pop_push_pct:.1f}%)") + + frames_written = stats.get('total_frames_written', 0) + frames_saved = stats.get('frames_saved', 0) + compression_pct = stats.get('frame_compression_pct', 0) + + print(f" {ANSIColors.CYAN}Frame Efficiency:{ANSIColors.RESET}") + print(f" Frames written: {frames_written:,}") + print(f" Frames saved: {frames_saved:,} ({ANSIColors.GREEN}{compression_pct:.1f}%{ANSIColors.RESET})") + + bytes_written = stats.get('bytes_written', 0) + if bytes_written >= 1024 * 1024: + bytes_str = f"{bytes_written / (1024 * 1024):.1f} MB" + elif bytes_written >= 1024: + bytes_str = f"{bytes_written / 1024:.1f} KB" + else: + bytes_str = f"{bytes_written} B" + print(f" Bytes (pre-zstd): {bytes_str}") + + +def _is_process_running(pid): + if pid <= 0: + return False + if os.name == "posix": + try: + os.kill(pid, 0) + return True + except ProcessLookupError: + return False + except PermissionError: + # EPERM means process exists but we can't signal it + return True + elif sys.platform == "win32": + try: + _remote_debugging.RemoteUnwinder(pid) + except Exception: + return False + return True + else: + raise ValueError(f"Unsupported platform: {sys.platform}") + + +def sample( + pid, + collector, + *, + duration_sec=None, + all_threads=False, + realtime_stats=False, + mode=PROFILING_MODE_WALL, + async_aware=None, + native=False, + gc=True, + opcodes=False, + blocking=False, +): + """Sample a process using the provided collector. + + Args: + pid: Process ID to sample + collector: Collector instance to use for gathering samples + duration_sec: How long to sample for (seconds), or None to run until + the process exits or interrupted + all_threads: Whether to sample all threads + realtime_stats: Whether to print real-time sampling statistics + mode: Profiling mode - WALL (all samples), CPU (only when on CPU), + GIL (only when holding GIL), ALL (includes GIL and CPU status), + EXCEPTION (only when thread has an active exception) + native: Whether to include native frames + gc: Whether to include GC frames + opcodes: Whether to include opcode information + blocking: Whether to stop all threads before sampling for consistent snapshots + + Returns: + The collector with collected samples + """ + # Get sample interval from collector + sample_interval_usec = collector.sample_interval_usec + + # PROFILING_MODE_ALL implies no skipping at all + if mode == PROFILING_MODE_ALL: + skip_non_matching_threads = False + else: + # For most modes, skip non-matching threads + # Gecko collector overrides this by setting skip_idle=False + skip_non_matching_threads = True + + profiler = SampleProfiler( + pid, + sample_interval_usec, + all_threads=all_threads, + mode=mode, + native=native, + gc=gc, + opcodes=opcodes, + skip_non_matching_threads=skip_non_matching_threads, + collect_stats=realtime_stats, + blocking=blocking, + ) + profiler.realtime_stats = realtime_stats + + # Run the sampling + profiler.sample(collector, duration_sec, async_aware=async_aware) + + return collector + + +def dump_stack( + pid, + *, + all_threads=False, + mode=PROFILING_MODE_ALL, + async_aware=None, + native=False, + gc=True, + opcodes=False, + blocking=False, +): + """Return a single stack snapshot from a process.""" + if mode == PROFILING_MODE_ALL: + skip_non_matching_threads = False + else: + skip_non_matching_threads = True + + profiler = SampleProfiler( + pid, + sample_interval_usec=1, + all_threads=all_threads, + mode=mode, + native=native, + gc=gc, + opcodes=opcodes, + skip_non_matching_threads=skip_non_matching_threads, + blocking=blocking, + ) + return profiler.dump_stack(async_aware=async_aware) + + +def sample_live( + pid, + collector, + *, + duration_sec=None, + all_threads=False, + realtime_stats=False, + mode=PROFILING_MODE_WALL, + async_aware=None, + native=False, + gc=True, + opcodes=False, + blocking=False, +): + """Sample a process in live/interactive mode with curses TUI. + + Args: + pid: Process ID to sample + collector: LiveStatsCollector instance + duration_sec: How long to sample for (seconds) + all_threads: Whether to sample all threads + realtime_stats: Whether to print real-time sampling statistics + mode: Profiling mode - WALL (all samples), CPU (only when on CPU), + GIL (only when holding GIL), ALL (includes GIL and CPU status), + EXCEPTION (only when thread has an active exception) + native: Whether to include native frames + gc: Whether to include GC frames + opcodes: Whether to include opcode information + blocking: Whether to stop all threads before sampling for consistent snapshots + + Returns: + The collector with collected samples + """ + import curses + + # Check if process is alive before doing any heavy initialization + if not _is_process_running(pid): + print(f"No samples collected - process {pid} exited before profiling could begin.", file=sys.stderr) + return collector + + # Get sample interval from collector + sample_interval_usec = collector.sample_interval_usec + + # PROFILING_MODE_ALL implies no skipping at all + if mode == PROFILING_MODE_ALL: + skip_non_matching_threads = False + else: + skip_non_matching_threads = True + + profiler = SampleProfiler( + pid, + sample_interval_usec, + all_threads=all_threads, + mode=mode, + native=native, + gc=gc, + opcodes=opcodes, + skip_non_matching_threads=skip_non_matching_threads, + collect_stats=realtime_stats, + blocking=blocking, + ) + profiler.realtime_stats = realtime_stats + + def curses_wrapper_func(stdscr): + collector.init_curses(stdscr) + try: + profiler.sample(collector, duration_sec, async_aware=async_aware) + # If too few samples were collected, exit cleanly without showing TUI + if collector.successful_samples < MIN_SAMPLES_FOR_TUI: + # Clear screen before exiting to avoid visual artifacts + stdscr.clear() + stdscr.refresh() + return + # Mark as finished and keep the TUI running until user presses 'q' + collector.mark_finished() + # Keep processing input until user quits + while collector.running: + collector._handle_input() + time.sleep(0.05) # Small sleep to avoid busy waiting + finally: + collector.cleanup_curses() + + try: + curses.wrapper(curses_wrapper_func) + except KeyboardInterrupt: + pass + + # If too few samples were collected, print a message + if collector.successful_samples < MIN_SAMPLES_FOR_TUI: + if collector.successful_samples == 0: + print(f"No samples collected - process {pid} exited before profiling could begin.", file=sys.stderr) + else: + print(f"Only {collector.successful_samples} sample(s) collected (minimum {MIN_SAMPLES_FOR_TUI} required for TUI) - process {pid} exited too quickly.", file=sys.stderr) + + return collector diff --git a/Lib/profiling/sampling/stack_collector.py b/Lib/profiling/sampling/stack_collector.py new file mode 100644 index 00000000000..1610f4a3655 --- /dev/null +++ b/Lib/profiling/sampling/stack_collector.py @@ -0,0 +1,907 @@ +import base64 +import collections +import functools +import importlib.resources +import json +import linecache +import os +import sys +import sysconfig + +from ._css_utils import get_combined_css +from .collector import Collector, extract_lineno +from .opcode_utils import get_opcode_mapping +from .string_table import StringTable +from .module_utils import extract_module_name, get_python_path_info + + +class StackTraceCollector(Collector): + aggregating = True + + def __init__(self, sample_interval_usec, *, skip_idle=False): + self.sample_interval_usec = sample_interval_usec + self.skip_idle = skip_idle + + def collect(self, stack_frames, timestamps_us=None): + weight = len(timestamps_us) if timestamps_us else 1 + for frames, thread_id in self._iter_stacks(stack_frames, skip_idle=self.skip_idle): + self.process_frames(frames, thread_id, weight=weight) + + def process_frames(self, frames, thread_id, weight=1): + pass + + +class CollapsedStackCollector(StackTraceCollector): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.stack_counter = collections.Counter() + + def process_frames(self, frames, thread_id, weight=1): + # Extract only (filename, lineno, funcname) - opcode not needed for collapsed stacks + # frame is (filename, location, funcname, opcode) + call_tree = tuple( + (f[0], extract_lineno(f[1]), f[2]) for f in reversed(frames) + ) + self.stack_counter[(call_tree, thread_id)] += weight + + def export(self, filename): + lines = [] + for (call_tree, thread_id), count in self.stack_counter.items(): + parts = [f"tid:{thread_id}"] + for file, line, func in call_tree: + # This is what pstats does for "special" frames: + if file == "~" and line == 0: + part = func + else: + part = f"{os.path.basename(file)}:{func}:{line}" + parts.append(part) + stack_str = ";".join(parts) + lines.append((stack_str, count)) + + lines.sort(key=lambda x: (-x[1], x[0])) + + with open(filename, "w") as f: + for stack, count in lines: + f.write(f"{stack} {count}\n") + print(f"Collapsed stack output written to {filename}") + return True + + +class FlamegraphCollector(StackTraceCollector): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.stats = {"sample_interval_usec": self.sample_interval_usec} + self._root = { + "samples": 0, + "children": {}, + "threads": set(), + "thread_samples": collections.Counter(), + "thread_self": collections.Counter(), + } + self._total_samples = 0 + self._sample_count = 0 # Track actual number of samples (not thread traces) + self._func_intern = {} + self._string_table = StringTable() + self._module_cache = {} + self._all_threads = set() + + # Thread status statistics (similar to LiveStatsCollector) + self.thread_status_counts = { + "has_gil": 0, + "on_cpu": 0, + "gil_requested": 0, + "unknown": 0, + "has_exception": 0, + "total": 0, + } + self.samples_with_gc_frames = 0 + + # Per-thread statistics + self.per_thread_stats = {} # {thread_id: {has_gil, on_cpu, gil_requested, unknown, has_exception, total, gc_samples}} + + def collect(self, stack_frames, timestamps_us=None): + """Override to track thread status statistics before processing frames.""" + # Weight is number of timestamps (samples with identical stack) + weight = len(timestamps_us) if timestamps_us else 1 + # Increment sample count by weight + self._sample_count += weight + + # Collect both aggregate and per-thread statistics using base method + status_counts, has_gc_frame, per_thread_stats = self._collect_thread_status_stats(stack_frames) + + # Merge aggregate status counts (multiply by weight) + for key in status_counts: + self.thread_status_counts[key] += status_counts[key] * weight + + # Update aggregate GC frame count + if has_gc_frame: + self.samples_with_gc_frames += weight + + # Merge per-thread statistics (multiply by weight) + for thread_id, stats in per_thread_stats.items(): + if thread_id not in self.per_thread_stats: + self.per_thread_stats[thread_id] = { + "has_gil": 0, + "on_cpu": 0, + "gil_requested": 0, + "unknown": 0, + "has_exception": 0, + "total": 0, + "gc_samples": 0, + } + for key, value in stats.items(): + self.per_thread_stats[thread_id][key] += value * weight + + # Call parent collect to process frames + super().collect(stack_frames, timestamps_us) + + def set_stats(self, sample_interval_usec, duration_sec, sample_rate, + error_rate=None, missed_samples=None, mode=None): + """Set profiling statistics to include in flamegraph data.""" + self.stats = { + "sample_interval_usec": sample_interval_usec, + "duration_sec": duration_sec, + "sample_rate": sample_rate, + "error_rate": error_rate, + "missed_samples": missed_samples, + "mode": mode + } + + def set_replay_stats(self, info): + """Restore measured statistics stored in a binary profile.""" + duration_sec = info.get("duration_sec") + sample_rate = info.get("sample_rate") + if duration_sec is None or sample_rate is None: + return + self.set_stats( + self.sample_interval_usec, + duration_sec, + sample_rate, + error_rate=info.get("error_rate"), + missed_samples=info.get("missed_samples"), + mode=self.stats.get("mode"), + ) + def set_mode(self, mode): + self.stats["mode"] = mode + + def export(self, filename): + flamegraph_data = self._convert_to_flamegraph_format() + + # Debug output with string table statistics + num_functions = len(flamegraph_data.get("children", [])) + total_time = flamegraph_data.get("value", 0) + string_count = len(self._string_table) + s1 = "" if num_functions == 1 else "s" + s2 = "" if total_time == 1 else "s" + s3 = "" if string_count == 1 else "s" + print( + f"Flamegraph data: {num_functions} root function{s1}, " + f"{total_time} total sample{s2}, " + f"{string_count} unique string{s3}" + ) + + if num_functions == 0: + print( + "Warning: No functions found in profiling data. Check if sampling captured any data." + ) + return False + + html_content = self._create_flamegraph_html(flamegraph_data) + + with open(filename, "w", encoding="utf-8") as f: + f.write(html_content) + + print(f"Flamegraph saved to: {filename}") + return True + + @staticmethod + @functools.lru_cache(maxsize=None) + def _format_function_name(func): + filename, lineno, funcname = func + + # Special frames like and should not show file:line + if filename == "~" and lineno == 0: + return funcname + + if len(filename) > 50: + parts = filename.split("/") + if len(parts) > 2: + filename = f".../{'/'.join(parts[-2:])}" + + return f"{funcname} ({filename}:{lineno})" + + @staticmethod + @functools.lru_cache(maxsize=None) + def _format_module_name(func, module_name): + filename, lineno, funcname = func + + # Special frames like and should not show file:line + if filename == "~" and lineno == 0: + return funcname + + return f"{funcname} ({module_name}:{lineno})" + + def _get_module_name(self, filename, path_info): + module_name = self._module_cache.get(filename) + if module_name is None: + module_name, _ = extract_module_name(filename, path_info) + self._module_cache[filename] = module_name + return module_name + + def _convert_to_flamegraph_format(self, *, min_samples=None): + if self._total_samples == 0: + return { + "name": self._string_table.intern("No Data"), + "value": 0, + "children": [], + "threads": [], + "strings": self._string_table.get_strings() + } + + def convert_children(children, min_samples, path_info): + out = [] + for func, node in children.items(): + samples = node["samples"] + significant_for_thread = any( + thread_samples >= max( + 1, + int( + self._root["thread_samples"][thread_id] + * 0.001 + ), + ) + for thread_id, thread_samples + in node["thread_samples"].items() + ) + if samples < min_samples and not significant_for_thread: + continue + + # Intern all string components for maximum efficiency + filename_idx = self._string_table.intern(func[0]) + funcname_idx = self._string_table.intern(func[2]) + module_name = self._get_module_name(func[0], path_info) + + module_idx = self._string_table.intern(module_name) + name_idx = self._string_table.intern(self._format_function_name(func)) + label_idx = self._string_table.intern(self._format_module_name(func, module_name)) + + child_entry = { + "name": name_idx, + "label": label_idx, + "value": samples, + "self": node.get("self", 0), + "children": [], + "filename": filename_idx, + "module": module_idx, + "lineno": func[1], + "funcname": funcname_idx, + "threads": sorted(list(node.get("threads", set()))), + "thread_values": { + thread_id: [ + samples, + node["thread_self"].get(thread_id, 0), + ] + for thread_id, samples in sorted( + node["thread_samples"].items() + ) + }, + } + + source = self._get_source_lines(func) + if source: + # Intern source lines for memory efficiency + source_indices = [self._string_table.intern(line) for line in source] + child_entry["source"] = source_indices + + # Include opcode data if available + opcodes = node.get("opcodes", {}) + if opcodes: + child_entry["opcodes"] = dict(opcodes) + thread_opcodes = node.get("thread_opcodes") + if thread_opcodes: + child_entry["thread_opcodes"] = { + thread_id: dict(counts) + for thread_id, counts in sorted( + thread_opcodes.items() + ) + } + + # Recurse + child_entry["children"] = convert_children( + node["children"], min_samples, path_info + ) + out.append(child_entry) + + # Sort by value (descending) then by name index for consistent ordering + out.sort(key=lambda x: (-x["value"], x["name"])) + return out + + # Filter out very small functions (less than 0.1% of total samples) + total_samples = self._total_samples + if min_samples is None: + min_samples = max(1, int(total_samples * 0.001)) + path_info = get_python_path_info() + + root_children = convert_children(self._root["children"], min_samples, path_info) + if not root_children: + return { + "name": self._string_table.intern("No significant data"), + "value": 0, + "children": [], + "strings": self._string_table.get_strings() + } + + # Calculate thread status percentages for display + is_free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + total_threads = max(1, self.thread_status_counts["total"]) + thread_stats = { + "has_gil_pct": (self.thread_status_counts["has_gil"] / total_threads) * 100, + "on_cpu_pct": (self.thread_status_counts["on_cpu"] / total_threads) * 100, + "gil_requested_pct": (self.thread_status_counts["gil_requested"] / total_threads) * 100, + "has_exception_pct": (self.thread_status_counts["has_exception"] / total_threads) * 100, + "gc_pct": (self.samples_with_gc_frames / max(1, self._sample_count)) * 100, + "free_threaded": is_free_threaded, + **self.thread_status_counts + } + + # Calculate per-thread statistics with percentages + per_thread_stats_with_pct = {} + total_samples_denominator = max(1, self._sample_count) + for thread_id, stats in self.per_thread_stats.items(): + total = max(1, stats["total"]) + per_thread_stats_with_pct[thread_id] = { + "has_gil_pct": (stats["has_gil"] / total) * 100, + "on_cpu_pct": (stats["on_cpu"] / total) * 100, + "gil_requested_pct": (stats["gil_requested"] / total) * 100, + "has_exception_pct": (stats["has_exception"] / total) * 100, + "gc_pct": (stats["gc_samples"] / total_samples_denominator) * 100, + **stats + } + + # Build opcode mapping for JS + opcode_mapping = get_opcode_mapping() + + # If we only have one root child, make it the root to avoid redundant level + root_thread_values = { + thread_id: [samples, 0] + for thread_id, samples in sorted( + self._root["thread_samples"].items() + ) + } + sole_root_covers_profile = ( + len(root_children) == 1 + and root_children[0]["value"] == total_samples + and { + thread_id: values[0] + for thread_id, values + in root_children[0]["thread_values"].items() + } == { + thread_id: values[0] + for thread_id, values in root_thread_values.items() + } + ) + if sole_root_covers_profile: + main_child = root_children[0] + # Update name and label to indicate it's the program root + old_name = self._string_table.get_string(main_child["name"]) + main_child["name"] = self._string_table.intern(f"Program Root: {old_name}") + old_label = self._string_table.get_string(main_child["label"]) + main_child["label"] = self._string_table.intern(f"Program Root: {old_label}") + main_child["stats"] = { + **self.stats, + "thread_stats": thread_stats, + "per_thread_stats": per_thread_stats_with_pct + } + main_child["threads"] = sorted(list(self._all_threads)) + main_child["strings"] = self._string_table.get_strings() + main_child["opcode_mapping"] = opcode_mapping + return main_child + + program_root_idx = self._string_table.intern("Program Root") + return { + "name": program_root_idx, + "label": program_root_idx, + "value": total_samples, + "children": root_children, + "stats": { + **self.stats, + "thread_stats": thread_stats, + "per_thread_stats": per_thread_stats_with_pct + }, + "threads": sorted(list(self._all_threads)), + "thread_values": root_thread_values, + "strings": self._string_table.get_strings(), + "opcode_mapping": opcode_mapping + } + + def process_frames(self, frames, thread_id, weight=1): + """Process stack frames into flamegraph tree structure. + + Args: + frames: List of (filename, location, funcname, opcode) tuples in + leaf-to-root order. location is (lineno, end_lineno, col_offset, end_col_offset). + opcode is None if not gathered. + thread_id: Thread ID for this stack trace + weight: Number of samples this stack represents (for batched RLE) + """ + # Reverse to root->leaf order for tree building + self._root["samples"] += weight + self._root["thread_samples"][thread_id] += weight + self._total_samples += weight + self._root["threads"].add(thread_id) + self._all_threads.add(thread_id) + + current = self._root + for filename, location, funcname, opcode in reversed(frames): + lineno = extract_lineno(location) + func = (filename, lineno, funcname) + func = self._func_intern.setdefault(func, func) + + node = current["children"].get(func) + if node is None: + node = { + "samples": 0, + "children": {}, + "threads": set(), + "thread_samples": collections.Counter(), + "thread_self": collections.Counter(), + "opcodes": collections.Counter(), + "self": 0, + } + current["children"][func] = node + node["samples"] += weight + node["thread_samples"][thread_id] += weight + node["threads"].add(thread_id) + + if opcode is not None: + node["opcodes"][opcode] += weight + thread_opcodes = node.setdefault("thread_opcodes", {}) + thread_opcodes.setdefault( + thread_id, collections.Counter() + )[opcode] += weight + + current = node + + if current is not self._root: + current["self"] += weight + current["thread_self"][thread_id] += weight + + def _get_source_lines(self, func): + filename, lineno, _ = func + + try: + lines = [] + start_line = max(1, lineno - 2) + end_line = lineno + 3 + + for line_num in range(start_line, end_line): + line = linecache.getline(filename, line_num) + if line.strip(): + marker = "→ " if line_num == lineno else " " + lines.append(f"{marker}{line_num}: {line.rstrip()}") + + return lines if lines else None + + except Exception: + return None + + def _create_flamegraph_html(self, data): + data_json = json.dumps(data) + + template_dir = importlib.resources.files(__package__) + vendor_dir = template_dir / "_vendor" + assets_dir = template_dir / "_assets" + + d3_path = vendor_dir / "d3" / "7.8.5" / "d3.min.js" + d3_flame_graph_dir = vendor_dir / "d3-flame-graph" / "4.1.3" + fg_css_path = d3_flame_graph_dir / "d3-flamegraph.css" + fg_js_path = d3_flame_graph_dir / "d3-flamegraph.min.js" + fg_tooltip_js_path = d3_flame_graph_dir / "d3-flamegraph-tooltip.min.js" + + html_template = (template_dir / "_flamegraph_assets" / "flamegraph_template.html").read_text(encoding="utf-8") + css_content = get_combined_css("flamegraph") + base_js = (template_dir / "_shared_assets" / "base.js").read_text(encoding="utf-8") + component_js = (template_dir / "_flamegraph_assets" / "flamegraph.js").read_text(encoding="utf-8") + js_content = f"{base_js}\n{component_js}" + + # Set title and subtitle based on whether this is a differential flamegraph + is_differential = data.get("stats", {}).get("is_differential", False) + if is_differential: + title = "Tachyon Profiler - Differential Flamegraph Report" + subtitle = "Differential Flamegraph Report" + else: + title = "Tachyon Profiler - Flamegraph Report" + subtitle = "Flamegraph Report" + + html_template = html_template.replace("{{TITLE}}", title) + html_template = html_template.replace("{{SUBTITLE}}", subtitle) + + # Inline first-party CSS/JS + html_template = html_template.replace( + "", f"" + ) + html_template = html_template.replace( + "", f"" + ) + + png_path = assets_dir / "tachyon-logo.png" + b64_logo = base64.b64encode(png_path.read_bytes()).decode("ascii") + + # Let CSS control size; keep markup simple + logo_html = f'Tachyon logo' + html_template = html_template.replace("", logo_html) + html_template = html_template.replace( + "", f"{sys.version_info.major}.{sys.version_info.minor}" + ) + + d3_js = d3_path.read_text(encoding="utf-8") + fg_css = fg_css_path.read_text(encoding="utf-8") + fg_js = fg_js_path.read_text(encoding="utf-8") + fg_tooltip_js = fg_tooltip_js_path.read_text(encoding="utf-8") + + html_template = html_template.replace( + "", + f"", + ) + html_template = html_template.replace( + "", + f"", + ) + html_template = html_template.replace( + "", + f"", + ) + html_template = html_template.replace( + "", + f"", + ) + + # Replace the placeholder with actual data + html_content = html_template.replace( + "{{FLAMEGRAPH_DATA}}", data_json + ) + + return html_content + + +class DiffFlamegraphCollector(FlamegraphCollector): + """Differential flamegraph collector that compares against a baseline binary profile.""" + + def __init__(self, sample_interval_usec, *, baseline_binary_path, + skip_idle=False, mode=None, capture_config=None): + super().__init__(sample_interval_usec, skip_idle=skip_idle) + if not os.path.exists(baseline_binary_path): + raise ValueError(f"Baseline file not found: {baseline_binary_path}") + self.baseline_binary_path = baseline_binary_path + self._baseline_collector = None + self._elided_paths = set() + self.mode = mode + self.capture_config = capture_config + + def _load_baseline(self): + """Load baseline profile from binary file.""" + from .binary_reader import BinaryReader + + with BinaryReader(self.baseline_binary_path) as reader: + info = reader.get_info() + + baseline_mode = info.get("mode") + if ( + baseline_mode is not None + and self.mode is not None + and baseline_mode != self.mode + ): + raise ValueError( + "Baseline profiling mode does not match current mode" + ) + + baseline_config = info.get("capture_config") + if baseline_config is not None and self.capture_config is not None: + names = baseline_config.keys() | self.capture_config.keys() + mismatches = [ + name for name in names + if baseline_config.get(name, False) + != self.capture_config.get(name, False) + ] + else: + mismatches = [] + if mismatches: + raise ValueError( + "Baseline capture configuration does not match current " + f"configuration: {', '.join(sorted(mismatches))}" + ) + + baseline_collector = FlamegraphCollector( + sample_interval_usec=info['sample_interval_us'], + skip_idle=self.skip_idle + ) + + reader.replay_samples(baseline_collector) + + self._baseline_collector = baseline_collector + + def _aggregate_path_samples(self, root_node, path=None): + """Aggregate samples by stack path, excluding line numbers for cross-profile matching.""" + if path is None: + path = () + + stats = {} + + for func, node in root_node["children"].items(): + filename, _lineno, funcname = func + func_key = (filename, funcname) + path_key = path + (func_key,) + + total_samples = node.get("samples", 0) + self_samples = node.get("self", 0) + + if path_key in stats: + stats[path_key]["total"] += total_samples + stats[path_key]["self"] += self_samples + else: + stats[path_key] = { + "total": total_samples, + "self": self_samples + } + + child_stats = self._aggregate_path_samples(node, path_key) + for key, data in child_stats.items(): + if key in stats: + stats[key]["total"] += data["total"] + stats[key]["self"] += data["self"] + else: + stats[key] = data + + return stats + + def _convert_to_flamegraph_format(self): + """Convert to flamegraph format with differential annotations.""" + if self._baseline_collector is None: + self._load_baseline() + + current_flamegraph = super()._convert_to_flamegraph_format() + + current_stats = self._aggregate_path_samples(self._root) + baseline_stats = self._aggregate_path_samples(self._baseline_collector._root) + + # Scale baseline values to make them comparable, accounting for both + # sample count differences and sample interval differences. + baseline_total = self._baseline_collector._total_samples + if baseline_total > 0 and self._total_samples > 0: + current_time = self._total_samples * self.sample_interval_usec + baseline_time = baseline_total * self._baseline_collector.sample_interval_usec + scale = current_time / baseline_time + elif baseline_total > 0: + # Current profile is empty - use interval-based scale for elided display + scale = self.sample_interval_usec / self._baseline_collector.sample_interval_usec + else: + scale = 1.0 + + self._annotate_nodes_with_diff(current_flamegraph, current_stats, baseline_stats, scale) + self._add_elided_flamegraph(current_flamegraph, current_stats, baseline_stats, scale) + + return current_flamegraph + + def _annotate_nodes_with_diff(self, current_flamegraph, current_stats, baseline_stats, scale): + """Annotate each node in the tree with diff metadata.""" + if "stats" not in current_flamegraph: + current_flamegraph["stats"] = {} + + current_flamegraph["stats"]["baseline_samples"] = self._baseline_collector._total_samples + current_flamegraph["stats"]["current_samples"] = self._total_samples + current_flamegraph["stats"]["baseline_scale"] = scale + current_flamegraph["stats"]["is_differential"] = True + + if self._is_promoted_root(current_flamegraph): + self._add_diff_data_to_node(current_flamegraph, (), current_stats, baseline_stats, scale) + else: + for child in current_flamegraph["children"]: + self._add_diff_data_to_node(child, (), current_stats, baseline_stats, scale) + + def _add_diff_data_to_node(self, node, path, current_stats, baseline_stats, scale): + """Recursively add diff metadata to nodes.""" + func_key = self._extract_func_key(node, self._string_table) + path_key = path + (func_key,) if func_key else path + + current_data = current_stats.get(path_key, {"total": 0, "self": 0}) + baseline_data = baseline_stats.get(path_key, {"total": 0, "self": 0}) + + current_self = node.get("self", 0) + current_total = node.get("value", 0) + + current_nonself = current_total - current_self + aggregate_nonself = current_data["total"] - current_data["self"] + + # Allocate self and descendant samples separately. Line-number + # changes can split one function path into several rendered nodes, + # and using independent weights for self and inclusive totals could + # otherwise assign a node more self samples than total samples. + self_weight = self._sample_weight( + current_self, + current_data["self"], + current_total, + current_data["total"], + ) + nonself_weight = self._sample_weight( + current_nonself, + aggregate_nonself, + current_total, + current_data["total"], + ) + baseline_self = baseline_data["self"] * scale * self_weight + baseline_nonself = ( + baseline_data["total"] - baseline_data["self"] + ) * scale * nonself_weight + baseline_total = baseline_self + baseline_nonself + + diff = current_self - baseline_self + if baseline_self > 0: + diff_pct = (diff / baseline_self) * 100.0 + elif current_self > 0: + diff_pct = 100.0 + else: + diff_pct = 0.0 + + node["baseline"] = baseline_self + node["baseline_total"] = baseline_total + node["self_time"] = current_self + node["diff"] = diff + node["diff_pct"] = diff_pct + + if "children" in node and node["children"]: + for child in node["children"]: + self._add_diff_data_to_node(child, path_key, current_stats, baseline_stats, scale) + + @staticmethod + def _sample_weight(value, aggregate, fallback_value, fallback_aggregate): + if aggregate > 0: + return value / aggregate + if fallback_aggregate > 0: + return fallback_value / fallback_aggregate + return 0 + + def _is_promoted_root(self, data): + """Check if the data represents a promoted root node.""" + return "filename" in data and "funcname" in data + + def _add_elided_flamegraph(self, current_flamegraph, current_stats, baseline_stats, scale): + """Calculate elided paths and add elided flamegraph to stats.""" + self._elided_paths = baseline_stats.keys() - current_stats.keys() + + # A sampled stack can end at an internal path that also has elided + # descendants. Count every disappeared path with self samples, not + # just the leaves of the elided path tree. + elided_stacks = { + path + for path in self._elided_paths + if baseline_stats[path]["self"] > 0 + } + current_flamegraph["stats"]["elided_count"] = len(elided_stacks) + + if self._elided_paths: + elided_flamegraph = self._build_elided_flamegraph(baseline_stats, scale) + if elided_flamegraph: + current_flamegraph["stats"]["elided_flamegraph"] = elided_flamegraph + + def _build_elided_flamegraph(self, baseline_stats, scale): + """Build flamegraph containing only elided paths from baseline. + + This re-runs the base conversion pipeline on the baseline collector + to produce a complete formatted flamegraph, then prunes it to keep + only elided paths. + """ + if not self._baseline_collector or not self._elided_paths: + return None + + # Suppress source line collection for elided nodes - these functions + # no longer exist in the current profile, so source lines from the + # current machine's filesystem would be misleading or unavailable. + orig_get_source = self._baseline_collector._get_source_lines + self._baseline_collector._get_source_lines = lambda func: None + try: + baseline_data = self._baseline_collector._convert_to_flamegraph_format( + min_samples=1 + ) + finally: + self._baseline_collector._get_source_lines = orig_get_source + + # Remove non-elided nodes and recalculate values + if not self._extract_elided_nodes(baseline_data, path=()): + return None + + self._add_elided_metadata(baseline_data, baseline_stats, scale, path=()) + + # Merge only profiling metadata, not thread-level stats + for key in ("sample_interval_usec", "duration_sec", "sample_rate", + "error_rate", "missed_samples", "mode"): + if key in self.stats: + baseline_data["stats"][key] = self.stats[key] + baseline_data["stats"]["is_differential"] = True + baseline_data["stats"]["baseline_samples"] = self._baseline_collector._total_samples + baseline_data["stats"]["current_samples"] = self._total_samples + + return baseline_data + + def _extract_elided_nodes(self, node, path): + """Remove non-elided nodes and recalculate values bottom-up.""" + if not node: + return False + + func_key = self._extract_func_key(node, self._baseline_collector._string_table) + current_path = path + (func_key,) if func_key else path + + is_elided = current_path in self._elided_paths if func_key else False + + if "children" in node: + # Filter children, keeping only those with elided descendants + elided_children = [] + total_value = 0 + for child in node["children"]: + if self._extract_elided_nodes(child, current_path): + elided_children.append(child) + total_value += child.get("value", 0) + node["children"] = elided_children + + # Recalculate value for structural (non-elided) ancestor nodes; + # elided nodes keep their original value to preserve self-samples + if elided_children and not is_elided: + node["value"] = total_value + node["self"] = 0 + node.pop("opcodes", None) + node.pop("thread_opcodes", None) + + # Keep this node if it's elided or has elided descendants + return is_elided or bool(node.get("children")) + + def _add_elided_metadata(self, node, baseline_stats, scale, path): + """Add differential metadata showing this path disappeared.""" + if not node: + return + + func_key = self._extract_func_key(node, self._baseline_collector._string_table) + current_path = path + (func_key,) if func_key else path + + baseline_self = 0 + baseline_total = 0 + if func_key and current_path in baseline_stats: + baseline_total = node.get("value", 0) * scale + + # Matched nodes are retained only as structural ancestors. Their + # own samples are still present in the current profile and must + # not be reported as disappeared. + if current_path in self._elided_paths: + baseline_self = node.get("self", 0) * scale + + node["baseline"] = baseline_self + node["baseline_total"] = baseline_total + node["diff"] = -baseline_self + else: + node["baseline"] = 0 + node["baseline_total"] = 0 + node["diff"] = 0 + + node["self_time"] = 0 + # Elided paths have zero current self-time, so the change is always + # -100% when there was actual baseline self-time to lose. + # For internal nodes with no baseline self-time, use 0% to avoid + # misleading tooltips. + if baseline_self > 0: + node["diff_pct"] = -100.0 + else: + node["diff_pct"] = 0.0 + + if "children" in node and node["children"]: + for child in node["children"]: + self._add_elided_metadata(child, baseline_stats, scale, current_path) + + def _extract_func_key(self, node, string_table): + """Extract (filename, funcname) key from node, excluding line numbers. + + Line numbers are excluded to match functions even if they moved. + Returns None for root nodes that don't have function information. + """ + if "filename" not in node or "funcname" not in node: + return None + filename = string_table.get_string(node["filename"]) + funcname = string_table.get_string(node["funcname"]) + return (filename, funcname) diff --git a/Lib/profiling/sampling/string_table.py b/Lib/profiling/sampling/string_table.py new file mode 100644 index 00000000000..25c347f7ff1 --- /dev/null +++ b/Lib/profiling/sampling/string_table.py @@ -0,0 +1,53 @@ +"""String table implementation for memory-efficient string storage in profiling data.""" + +class StringTable: + """A string table for interning strings and reducing memory usage.""" + + def __init__(self): + self._strings = [] + self._string_to_index = {} + + def intern(self, string): + """Intern a string and return its index. + + Args: + string: The string to intern + + Returns: + int: The index of the string in the table + """ + if not isinstance(string, str): + string = str(string) + + if string in self._string_to_index: + return self._string_to_index[string] + + index = len(self._strings) + self._strings.append(string) + self._string_to_index[string] = index + return index + + def get_string(self, index): + """Get a string by its index. + + Args: + index: The index of the string + + Returns: + str: The string at the given index, or empty string if invalid + """ + if 0 <= index < len(self._strings): + return self._strings[index] + return "" + + def get_strings(self): + """Get the list of all strings in the table. + + Returns: + list: A copy of the strings list + """ + return self._strings.copy() + + def __len__(self): + """Return the number of strings in the table.""" + return len(self._strings) diff --git a/Lib/profiling/tracing/__init__.py b/Lib/profiling/tracing/__init__.py new file mode 100644 index 00000000000..165665304e0 --- /dev/null +++ b/Lib/profiling/tracing/__init__.py @@ -0,0 +1,220 @@ +"""Tracing profiler for Python. + +This module provides deterministic profiling of Python programs by tracing +every function call and return. +""" + +__all__ = ("run", "runctx", "Profile") + +import _lsprof +import importlib.machinery +import importlib.util +import io +from profiling.tracing._utils import _Utils + +# ____________________________________________________________ +# Simple interface + +def run(statement, filename=None, sort=-1): + """Run statement under profiler optionally saving results in filename + + This function takes a single argument that can be passed to the + "exec" statement, and an optional file name. In all cases this + routine attempts to "exec" its first argument and gather profiling + statistics from the execution. If no file name is present, then this + function automatically prints a simple profiling report, sorted by the + standard name string (file/line/function-name) that is presented in + each line. + """ + return _Utils(Profile).run(statement, filename, sort) + +def runctx(statement, globals, locals, filename=None, sort=-1): + """Run statement under profiler, supplying your own globals and locals, + optionally saving results in filename. + + statement and filename have the same semantics as profile.run + """ + return _Utils(Profile).runctx(statement, globals, locals, + filename, sort) + +# ____________________________________________________________ + +class Profile(_lsprof.Profiler): + """Profile(timer=None, timeunit=None, subcalls=True, builtins=True) + + Builds a profiler object using the specified timer function. + The default timer is a fast built-in one based on real time. + For custom timer functions returning integers, timeunit can + be a float specifying a scale (i.e. how long each integer unit + is, in seconds). + """ + + # Most of the functionality is in the base class. + # This subclass only adds convenient and backward-compatible methods. + + def print_stats(self, sort=-1): + import pstats + if not isinstance(sort, tuple): + sort = (sort,) + pstats.Stats(self).strip_dirs().sort_stats(*sort).print_stats() + + def dump_stats(self, file): + import marshal + with open(file, 'wb') as f: + self.create_stats() + marshal.dump(self.stats, f) + + def create_stats(self): + self.disable() + self.snapshot_stats() + + def snapshot_stats(self): + entries = self.getstats() + self.stats = {} + callersdicts = {} + # call information + for entry in entries: + func = label(entry.code) + nc = entry.callcount # ncalls column of pstats (before '/') + cc = nc - entry.reccallcount # ncalls column of pstats (after '/') + tt = entry.inlinetime # tottime column of pstats + ct = entry.totaltime # cumtime column of pstats + callers = {} + callersdicts[id(entry.code)] = callers + self.stats[func] = cc, nc, tt, ct, callers + # subcall information + for entry in entries: + if entry.calls: + func = label(entry.code) + for subentry in entry.calls: + try: + callers = callersdicts[id(subentry.code)] + except KeyError: + continue + nc = subentry.callcount + cc = nc - subentry.reccallcount + tt = subentry.inlinetime + ct = subentry.totaltime + if func in callers: + prev = callers[func] + nc += prev[0] + cc += prev[1] + tt += prev[2] + ct += prev[3] + callers[func] = nc, cc, tt, ct + + # The following two methods can be called by clients to use + # a profiler to profile a statement, given as a string. + + def run(self, cmd): + import __main__ + dict = __main__.__dict__ + return self.runctx(cmd, dict, dict) + + def runctx(self, cmd, globals, locals): + self.enable() + try: + exec(cmd, globals, locals) + finally: + self.disable() + return self + + # This method is more useful to profile a single function call. + def runcall(self, func, /, *args, **kw): + self.enable() + try: + return func(*args, **kw) + finally: + self.disable() + + def __enter__(self): + self.enable() + return self + + def __exit__(self, *exc_info): + self.disable() + +# ____________________________________________________________ + +def label(code): + if isinstance(code, str): + return ('~', 0, code) # built-in functions ('~' sorts at the end) + else: + return (code.co_filename, code.co_firstlineno, code.co_name) + +# ____________________________________________________________ + +def main(): + import os + import sys + import runpy + import pstats + from optparse import OptionParser + usage = "cProfile.py [-o output_file_path] [-s sort] [-m module | scriptfile] [arg] ..." + parser = OptionParser(usage=usage) + parser.allow_interspersed_args = False + parser.add_option('-o', '--outfile', dest="outfile", + help="Save stats to ", default=None) + parser.add_option('-s', '--sort', dest="sort", + help="Sort order when printing to stdout, based on pstats.Stats class", + default=2, + choices=sorted(pstats.Stats.sort_arg_dict_default)) + parser.add_option('-m', dest="module", action="store_true", + help="Profile a library module", default=False) + + if not sys.argv[1:]: + parser.print_usage() + sys.exit(2) + + (options, args) = parser.parse_args() + sys.argv[:] = args + + # The script that we're profiling may chdir, so capture the absolute path + # to the output file at startup. + if options.outfile is not None: + options.outfile = os.path.abspath(options.outfile) + + if len(args) > 0: + if options.module: + code = "run_module(modname, run_name='__main__')" + globs = { + 'run_module': runpy.run_module, + 'modname': args[0] + } + else: + progname = args[0] + sys.path.insert(0, os.path.dirname(progname)) + with io.open_code(progname) as fp: + code = compile(fp.read(), progname, 'exec', module='__main__') + spec = importlib.machinery.ModuleSpec(name='__main__', loader=None, + origin=progname) + module = importlib.util.module_from_spec(spec) + # Set __main__ so that importing __main__ in the profiled code will + # return the same namespace that the code is executing under. + sys.modules['__main__'] = module + # Ensure that we're using the same __dict__ instance as the module + # for the global variables so that updates to globals are reflected + # in the module's namespace. + globs = module.__dict__ + globs.update({ + # Set __spec__ to None so the profiled program behaves like a + # script run directly (gh-140729). + '__spec__': None, + '__file__': spec.origin, + '__name__': spec.name, + '__package__': None, + }) + + try: + runctx(code, globs, None, options.outfile, options.sort) + except BrokenPipeError as exc: + # Prevent "Exception ignored" during interpreter shutdown. + sys.stdout = None + sys.exit(exc.errno) + else: + parser.print_usage() + return parser + +# When invoked as main program, invoke the profiler on a script +if __name__ == '__main__': + main() diff --git a/Lib/profiling/tracing/__main__.py b/Lib/profiling/tracing/__main__.py new file mode 100644 index 00000000000..95041d1368b --- /dev/null +++ b/Lib/profiling/tracing/__main__.py @@ -0,0 +1,6 @@ +"""Run the tracing profiler from the command line.""" + +from profiling.tracing import main + +if __name__ == '__main__': + main() diff --git a/Lib/profiling/tracing/_utils.py b/Lib/profiling/tracing/_utils.py new file mode 100644 index 00000000000..8c4b65889c7 --- /dev/null +++ b/Lib/profiling/tracing/_utils.py @@ -0,0 +1,32 @@ +class _Utils: + """Support class for utility functions which are shared by + profile.py and cProfile.py modules. + Not supposed to be used directly. + """ + + def __init__(self, profiler): + self.profiler = profiler + + def run(self, statement, filename, sort): + prof = self.profiler() + try: + prof.run(statement) + except SystemExit: + pass + finally: + self._show(prof, filename, sort) + + def runctx(self, statement, globals, locals, filename, sort): + prof = self.profiler() + try: + prof.runctx(statement, globals, locals) + except SystemExit: + pass + finally: + self._show(prof, filename, sort) + + def _show(self, prof, filename, sort): + if filename is not None: + prof.dump_stats(filename) + else: + prof.print_stats(sort) diff --git a/Lib/pstats.py b/Lib/pstats.py index becaf35580e..07ecda07796 100644 --- a/Lib/pstats.py +++ b/Lib/pstats.py @@ -139,7 +139,11 @@ def load_stats(self, arg): return elif isinstance(arg, str): with open(arg, 'rb') as f: - self.stats = marshal.load(f) + stats = marshal.load(f) + if (('__sampled__',)) in stats: + stats.pop((('__sampled__',))) + self.__class__ = SampledStats + self.stats = stats try: file_stats = os.stat(arg) arg = time.ctime(file_stats.st_mtime) + " " + arg @@ -150,6 +154,7 @@ def load_stats(self, arg): arg.create_stats() self.stats = arg.stats arg.stats = {} + return if not self.stats: raise TypeError("Cannot create or construct a %r object from %r" % (self.__class__, arg)) @@ -467,7 +472,10 @@ def print_call_heading(self, name_size, column_title): subheader = isinstance(value, tuple) break if subheader: - print(" "*name_size + " ncalls tottime cumtime", file=self.stream) + self.print_call_subheading(name_size) + + def print_call_subheading(self, name_size): + print(" "*name_size + " ncalls tottime cumtime", file=self.stream) def print_call_line(self, name_size, source, call_dict, arrow="->"): print(func_std_string(source).ljust(name_size) + arrow, end=' ', file=self.stream) @@ -516,6 +524,35 @@ def print_line(self, func): # hack: should print percentages print(f8(ct/cc), end=' ', file=self.stream) print(func_std_string(func), file=self.stream) + +class SampledStats(Stats): + def __init__(self, *args, stream=None): + super().__init__(*args, stream=stream) + + self.sort_arg_dict = { + "samples" : (((1,-1), ), "sample count"), + "nsamples" : (((1,-1), ), "sample count"), + "cumtime" : (((3,-1), ), "cumulative time"), + "cumulative": (((3,-1), ), "cumulative time"), + "filename" : (((4, 1), ), "file name"), + "line" : (((5, 1), ), "line number"), + "module" : (((4, 1), ), "file name"), + "name" : (((6, 1), ), "function name"), + "nfl" : (((6, 1),(4, 1),(5, 1),), "name/file/line"), + "psamples" : (((0,-1), ), "primitive call count"), + "stdname" : (((7, 1), ), "standard name"), + "time" : (((2,-1), ), "internal time"), + "tottime" : (((2,-1), ), "internal time"), + } + + def print_call_subheading(self, name_size): + print(" "*name_size + " nsamples tottime cumtime", file=self.stream) + + def print_title(self): + print(' nsamples tottime persample cumtime persample', end=' ', file=self.stream) + print('filename:lineno(function)', file=self.stream) + + class TupleComp: """This class provides a generic function for comparing any two tuples. Each instance records a list of tuple-indices (from most significant @@ -607,6 +644,24 @@ def f8(x): # Statistics browser added by ESR, April 2001 #************************************************************************** +class StatsLoaderShim: + """Compatibility shim implementing 'create_stats' needed by Stats classes + to handle already unmarshalled data.""" + def __init__(self, raw_stats): + self.stats = raw_stats + + def create_stats(self): + pass + +def stats_factory(raw_stats): + """Return a Stats or SampledStats instance based on the marker in raw_stats.""" + if (('__sampled__',)) in raw_stats: + raw_stats = dict(raw_stats) # avoid mutating caller's dict + raw_stats.pop((('__sampled__',))) + return SampledStats(StatsLoaderShim(raw_stats)) + else: + return Stats(StatsLoaderShim(raw_stats)) + if __name__ == '__main__': import cmd try: @@ -693,7 +748,15 @@ def help_quit(self): def do_read(self, line): if line: try: - self.stats = Stats(line) + with open(line, 'rb') as f: + raw_stats = marshal.load(f) + self.stats = stats_factory(raw_stats) + try: + file_stats = os.stat(line) + arg = time.ctime(file_stats.st_mtime) + " " + line + except Exception: + arg = line + self.stats.files = [arg] except OSError as err: print(err.args[1], file=self.stream) return diff --git a/Lib/py_compile.py b/Lib/py_compile.py index 43d8ec90ffb..7ca479141e0 100644 --- a/Lib/py_compile.py +++ b/Lib/py_compile.py @@ -177,7 +177,7 @@ def main(): import argparse description = 'A simple command-line interface for py_compile module.' - parser = argparse.ArgumentParser(description=description, color=True) + parser = argparse.ArgumentParser(description=description) parser.add_argument( '-q', '--quiet', action='store_true', @@ -194,8 +194,10 @@ def main(): else: filenames = args.filenames for filename in filenames: + cfilename = (None if sys.implementation.cache_tag + else f"{filename.rpartition('.')[0]}.pyc") try: - compile(filename, doraise=True) + compile(filename, cfilename, doraise=True) except PyCompileError as error: if args.quiet: parser.exit(1) diff --git a/Lib/pyclbr.py b/Lib/pyclbr.py index 37f86995d6c..35772cc01d0 100644 --- a/Lib/pyclbr.py +++ b/Lib/pyclbr.py @@ -158,7 +158,6 @@ def _readmodule(module, path, inpackage=None): return _readmodule(submodule, parent['__path__'], package) # Search the path for the module. - f = None if inpackage is not None: search_path = path else: diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 091f8af01b2..1076caefd93 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -78,20 +78,41 @@ class or function within a module or module in a package. If the from reprlib import Repr from traceback import format_exception_only -from _pyrepl.pager import (get_pager, pipe_pager, - plain_pager, tempfile_pager, tty_pager) +try: + from _pyrepl.pager import (get_pager, pipe_pager, + plain_pager, tempfile_pager, tty_pager) -# Expose plain() as pydoc.plain() -from _pyrepl.pager import plain # noqa: F401 + # Expose plain() as pydoc.plain() + from _pyrepl.pager import plain # noqa: F401 + # --------------------------------------------------------- old names + getpager = get_pager + pipepager = pipe_pager + plainpager = plain_pager + tempfilepager = tempfile_pager + ttypager = tty_pager -# --------------------------------------------------------- old names +except ModuleNotFoundError: + # Minimal alternatives for cases where _pyrepl is absent. -getpager = get_pager -pipepager = pipe_pager -plainpager = plain_pager -tempfilepager = tempfile_pager -ttypager = tty_pager + def plain(text: str) -> str: + """Remove boldface formatting from text.""" + return re.sub('.\b', '', text) + + def plain_pager(text: str, title: str = '') -> None: + """Simply print unformatted text. This is the ultimate fallback.""" + encoding = getattr(sys.stdout, 'encoding', None) or 'utf-8' + text = text.encode(encoding, 'backslashreplace').decode(encoding) + text = plain(text) + sys.stdout.write(text) + + def get_pager(): + """Unconditionally return the plain pager, since _pyrepl is absent.""" + return plain_pager + + # --------------------------------------------------------- old names + getpager = get_pager + plainpager = plain_pager # --------------------------------------------------------- common routines @@ -108,96 +129,10 @@ def pathdirs(): normdirs.append(normdir) return dirs -def _findclass(func): - cls = sys.modules.get(func.__module__) - if cls is None: - return None - for name in func.__qualname__.split('.')[:-1]: - cls = getattr(cls, name) - if not inspect.isclass(cls): - return None - return cls - -def _finddoc(obj): - if inspect.ismethod(obj): - name = obj.__func__.__name__ - self = obj.__self__ - if (inspect.isclass(self) and - getattr(getattr(self, name, None), '__func__') is obj.__func__): - # classmethod - cls = self - else: - cls = self.__class__ - elif inspect.isfunction(obj): - name = obj.__name__ - cls = _findclass(obj) - if cls is None or getattr(cls, name) is not obj: - return None - elif inspect.isbuiltin(obj): - name = obj.__name__ - self = obj.__self__ - if (inspect.isclass(self) and - self.__qualname__ + '.' + name == obj.__qualname__): - # classmethod - cls = self - else: - cls = self.__class__ - # Should be tested before isdatadescriptor(). - elif isinstance(obj, property): - name = obj.__name__ - cls = _findclass(obj.fget) - if cls is None or getattr(cls, name) is not obj: - return None - elif inspect.ismethoddescriptor(obj) or inspect.isdatadescriptor(obj): - name = obj.__name__ - cls = obj.__objclass__ - if getattr(cls, name) is not obj: - return None - if inspect.ismemberdescriptor(obj): - slots = getattr(cls, '__slots__', None) - if isinstance(slots, dict) and name in slots: - return slots[name] - else: - return None - for base in cls.__mro__: - try: - doc = _getowndoc(getattr(base, name)) - except AttributeError: - continue - if doc is not None: - return doc - return None - -def _getowndoc(obj): - """Get the documentation string for an object if it is not - inherited from its class.""" - try: - doc = object.__getattribute__(obj, '__doc__') - if doc is None: - return None - if obj is not type: - typedoc = type(obj).__doc__ - if isinstance(typedoc, str) and typedoc == doc: - return None - return doc - except AttributeError: - return None - def _getdoc(object): - """Get the documentation string for an object. - - All tabs are expanded to spaces. To clean up docstrings that are - indented to line up with blocks of code, any whitespace than can be - uniformly removed from the second line onwards is removed.""" - doc = _getowndoc(object) - if doc is None: - try: - doc = _finddoc(object) - except (AttributeError, TypeError): - return None - if not isinstance(doc, str): - return None - return inspect.cleandoc(doc) + return inspect.getdoc(object, + fallback_to_class_doc=False, + inherit_class_doc=False) def getdoc(object): """Get the doc string or comments for an object.""" @@ -327,12 +262,12 @@ def visiblename(name, all=None, obj=None): """Decide whether to show documentation on a variable.""" # Certain special names are redundant or internal. # XXX Remove __initializing__? - if name in {'__author__', '__builtins__', '__cached__', '__credits__', - '__date__', '__doc__', '__file__', '__spec__', - '__loader__', '__module__', '__name__', '__package__', - '__path__', '__qualname__', '__slots__', '__version__', - '__static_attributes__', '__firstlineno__', - '__annotate_func__', '__annotations_cache__'}: + if name in {'__author__', '__builtins__', '__credits__', '__date__', + '__doc__', '__file__', '__spec__', '__loader__', '__module__', + '__name__', '__package__', '__path__', '__qualname__', + '__slots__', '__version__', '__static_attributes__', + '__firstlineno__', '__annotate_func__', + '__annotations_cache__'}: return 0 # Private names are hidden, but special names are displayed. if name.startswith('__') and name.endswith('__'): return 1 @@ -536,6 +471,7 @@ class Doc: PYTHONDOCS = os.environ.get("PYTHONDOCS", "https://docs.python.org/%d.%d/library" % sys.version_info[:2]) + STDLIB_DIR = sysconfig.get_path('stdlib') def document(self, object, name=None, *args): """Generate documentation for an object.""" @@ -561,23 +497,12 @@ def fail(self, object, name=None, *args): docmodule = docclass = docroutine = docother = docproperty = docdata = fail - def getdocloc(self, object, basedir=sysconfig.get_path('stdlib')): + def getdocloc(self, object, basedir=None): """Return the location of module docs or None""" - - try: - file = inspect.getabsfile(object) - except TypeError: - file = '(built-in)' - + basedir = self.STDLIB_DIR if basedir is None else basedir docloc = os.environ.get("PYTHONDOCS", self.PYTHONDOCS) - basedir = os.path.normcase(basedir) - if (isinstance(object, type(os)) and - (object.__name__ in ('errno', 'exceptions', 'gc', - 'marshal', 'posix', 'signal', 'sys', - '_thread', 'zipimport') or - (file.startswith(basedir) and - not file.startswith(os.path.join(basedir, 'site-packages')))) and + if (self._is_stdlib_module(object, basedir) and object.__name__ not in ('xml.etree', 'test.test_pydoc.pydoc_mod')): try: @@ -597,6 +522,36 @@ def getdocloc(self, object, basedir=sysconfig.get_path('stdlib')): docloc = None return docloc + def _get_version(self, object): + if self._is_stdlib_module(object): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + version = getattr(object, '__version__', None) + else: + version = getattr(object, '__version__', None) + return '' if version is None else str(version) + + def _is_stdlib_module(self, object, basedir=None): + basedir = self.STDLIB_DIR if basedir is None else basedir + + try: + file = inspect.getabsfile(object) + except TypeError: + file = '(built-in)' + + if sysconfig.is_python_build(): + srcdir = sysconfig.get_config_var('srcdir') + if srcdir: + basedir = os.path.join(srcdir, 'Lib') + + basedir = os.path.normcase(basedir) + return (isinstance(object, type(os)) and + (object.__name__ in ('errno', 'exceptions', 'gc', + 'marshal', 'posix', 'signal', 'sys', + '_thread', 'zipimport') + or (file.startswith(basedir) and + not file.startswith(os.path.join(basedir, 'site-packages'))))) + # -------------------------------------------- HTML documentation generator class HTMLRepr(Repr): @@ -856,8 +811,8 @@ def docmodule(self, object, name=None, mod=None, *ignored): except TypeError: filelink = '(built-in)' info = [] - if hasattr(object, '__version__'): - version = str(object.__version__) + + if version := self._get_version(object): if version[:11] == '$' + 'Revision: ' and version[-1:] == '$': version = version[11:-1].strip() info.append('version %s' % self.escape(version)) @@ -1392,8 +1347,7 @@ def docmodule(self, object, name=None, mod=None, *ignored): contents.append(self.docother(value, key, name, maxlen=70)) result = result + self.section('DATA', '\n'.join(contents)) - if hasattr(object, '__version__'): - version = str(object.__version__) + if version := self._get_version(object): if version[:11] == '$' + 'Revision: ' and version[-1:] == '$': version = version[11:-1].strip() result = result + self.section('VERSION', version) @@ -1824,7 +1778,6 @@ def writedocs(dir, pkgpath='', done=None): def _introdoc(): - import textwrap ver = '%d.%d' % sys.version_info[:2] if os.environ.get('PYTHON_BASIC_REPL'): pyrepl_keys = '' @@ -1893,6 +1846,7 @@ class Helper: 'in': ('in', 'SEQUENCEMETHODS'), 'is': 'COMPARISON', 'lambda': ('lambda', 'FUNCTIONS'), + 'lazy': ('lazy', 'MODULES'), 'match': ('match', 'if'), 'nonlocal': ('nonlocal', 'global NAMESPACES'), 'not': 'BOOLEAN', @@ -2185,7 +2139,6 @@ def showtopic(self, topic, more_xrefs=''): if more_xrefs: xrefs = (xrefs or '') + ' ' + more_xrefs if xrefs: - import textwrap text = 'Related help topics: ' + ', '.join(xrefs.split()) + '\n' wrapped_text = textwrap.wrap(text, 72) doc += '\n%s\n' % '\n'.join(wrapped_text) diff --git a/Lib/pydoc_data/module_docs.py b/Lib/pydoc_data/module_docs.py index 2d3543f2983..89e65350c04 100644 --- a/Lib/pydoc_data/module_docs.py +++ b/Lib/pydoc_data/module_docs.py @@ -1,4 +1,4 @@ -# Autogenerated by Sphinx on Wed Aug 5 13:29:41 2026 +# Autogenerated by Sphinx on Fri Oct 9 13:16:32 2026 # as part of the release process. module_docs = { @@ -23,7 +23,6 @@ 'bisect': 'bisect#module-bisect', 'builtins': 'builtins#module-builtins', 'bz2': 'bz2#module-bz2', - 'cProfile': 'profile#module-cProfile', 'calendar': 'calendar#module-calendar', 'cgi': 'cgi#module-cgi', 'cgitb': 'cgitb#module-cgitb', @@ -149,6 +148,7 @@ 'mailcap': 'mailcap#module-mailcap', 'marshal': 'marshal#module-marshal', 'math': 'math#module-math', + 'math.integer': 'math.integer#module-math.integer', 'mimetypes': 'mimetypes#module-mimetypes', 'mmap': 'mmap#module-mmap', 'modulefinder': 'modulefinder#module-modulefinder', @@ -183,7 +183,10 @@ 'posix': 'posix#module-posix', 'pprint': 'pprint#module-pprint', 'profile': 'profile#module-profile', - 'pstats': 'profile#module-pstats', + 'profiling': 'profiling#module-profiling', + 'profiling.sampling': 'profiling.sampling#module-profiling.sampling', + 'profiling.tracing': 'profiling.tracing#module-profiling.tracing', + 'pstats': 'pstats#module-pstats', 'pty': 'pty#module-pty', 'pwd': 'pwd#module-pwd', 'py_compile': 'py_compile#module-py_compile', diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py index 753d9d43069..39e570dc34d 100644 --- a/Lib/pydoc_data/topics.py +++ b/Lib/pydoc_data/topics.py @@ -1,4 +1,4 @@ -# Autogenerated by Sphinx on Wed Aug 5 13:29:41 2026 +# Autogenerated by Sphinx on Fri Oct 9 13:16:32 2026 # as part of the release process. topics = { @@ -912,9 +912,10 @@ class to a new value, *value*. renders the meaning of the program undefined. In the future, a check may be added to prevent this. -* "TypeError" will be raised if nonempty *__slots__* are defined for a - class derived from a ""variable-length" built-in type" such as - "int", "bytes", and "tuple". +* "TypeError" will be raised if *__slots__* other than *__dict__* and + *__weakref__* are defined for a class derived from a ""variable- + length" built-in type" such as "int", "bytes", and "type", except + "tuple". * Any non-string *iterable* may be assigned to *__slots__*. @@ -934,6 +935,10 @@ class derived from a ""variable-length" built-in type" such as * If an *iterator* is used for *__slots__* then a *descriptor* is created for each of the iterator’s values. However, the *__slots__* attribute will be an empty iterator. + +Changed in version 3.15: Allowed defining the *__dict__* and +*__weakref__* *__slots__* for any class. Allowed defining any +*__slots__* for a class derived from "tuple". ''', 'attribute-references': r'''Attribute references ******************** @@ -1394,7 +1399,7 @@ class and instance attributes applies as for regular assignments. "return" statement, the return value is "None". a built-in function or method: - The result is up to the interpreter; see Built-in Functions for the + The result is up to the interpreter; see Built-in functions for the descriptions of built-in functions and methods. a class object: @@ -1433,6 +1438,9 @@ class Foo: class Foo(object): pass +There may be one or more base classes; see Multiple inheritance below +for more information. + The class’s suite is then executed in a new execution frame (see Naming and binding), using a newly created local namespace and the original global namespace. (Usually, the suite contains mostly @@ -1496,6 +1504,115 @@ class attributes; they are shared by instances. Instance attributes **PEP 3129** - Class Decorators The proposal that added class decorators. Function and method decorators were introduced in **PEP 318**. + + +Multiple inheritance +==================== + +Python classes may have multiple base classes, a technique known as +*multiple inheritance*. The base classes are specified in the class +definition by listing them in parentheses after the class name, +separated by commas. For example, the following class definition: + + >>> class A: pass + >>> class B: pass + >>> class C(A, B): pass + +defines a class "C" that inherits from classes "A" and "B". + +The *method resolution order* (MRO) is the order in which base classes +are searched when looking up an attribute on a class. See The Python +2.3 Method Resolution Order for a description of how Python determines +the MRO for a class. + +Multiple inheritance is not always allowed. Attempting to define a +class with multiple inheritance will raise an error if one of the +bases does not allow subclassing, if a consistent MRO cannot be +created, if no valid metaclass can be determined, or if there is an +instance layout conflict. We’ll discuss each of these in turn. + +First, all base classes must allow subclassing. While most classes +allow subclassing, some built-in classes do not, such as "bool": + + >>> class SubBool(bool): # TypeError + ... pass + Traceback (most recent call last): + ... + TypeError: type 'bool' is not an acceptable base type + +In the resolved MRO of a class, the class’s bases appear in the order +they were specified in the class’s bases list. Additionally, the MRO +always lists a child class before any of its bases. A class definition +will fail if it is impossible to resolve a consistent MRO that +satisfies these rules from the list of bases provided: + + >>> class Base: pass + >>> class Child(Base): pass + >>> class Grandchild(Base, Child): pass # TypeError + Traceback (most recent call last): + ... + TypeError: Cannot create a consistent method resolution order (MRO) for bases Base, Child + +In the MRO of "Grandchild", "Base" must appear before "Child" because +it is first in the base class list, but it must also appear after +"Child" because it is a parent of "Child". This is a contradiction, so +the class cannot be defined. + +If some of the bases have a custom *metaclass*, the metaclass of the +resulting class is chosen among the metaclasses of the bases and the +explicitly specified metaclass of the child class. It must be a +metaclass that is a subclass of all other candidate metaclasses. If no +such metaclass exists among the candidates, the class cannot be +created, as explained in Determining the appropriate metaclass. + +Finally, the instance layouts of the bases must be compatible. This +means that it must be possible to compute a *solid base* for the +class. Exactly which classes are solid bases depends on the Python +implementation. + +**CPython implementation detail:** In CPython, a class is a solid base +if it has a nonempty "__slots__" definition. Many but not all classes +defined in C are also solid bases, including most builtins (such as +"int" or "BaseException") but excluding most concrete "Exception" +classes. Generally, a C class is a solid base if its underlying struct +is different in size from its base class. + +Every class has a solid base. "object", the base class, has itself as +its solid base. If there is a single base, the child class’s solid +base is that class if it is a solid base, or else the base class’s +solid base. If there are multiple bases, we first find the solid base +for each base class to produce a list of candidate solid bases. If +there is a unique solid base that is a subclass of all others, then +that class is the solid base. Otherwise, class creation fails. + +Example: + + >>> class Solid1: + ... __slots__ = ("solid1",) + >>> + >>> class Solid2: + ... __slots__ = ("solid2",) + >>> + >>> class SolidChild(Solid1): + ... __slots__ = ("solid_child",) + >>> + >>> class C1: # solid base is `object` + ... pass + >>> + >>> # OK: solid bases are `Solid1` and `object`, and `Solid1` is a subclass of `object`. + >>> class C2(Solid1, C1): # solid base is `Solid1` + ... pass + >>> + >>> # OK: solid bases are `SolidChild` and `Solid1`, and `SolidChild` is a subclass of `Solid1`. + >>> class C3(SolidChild, Solid1): # solid base is `SolidChild` + ... pass + >>> + >>> # Error: solid bases are `Solid1` and `Solid2`, but neither is a subclass of the other. + >>> class C4(Solid1, Solid2): # error: no single solid base + ... pass + Traceback (most recent call last): + ... + TypeError: multiple bases have instance lay-out conflict ''', 'comparisons': r'''Comparisons *********** @@ -1851,7 +1968,7 @@ class attributes; they are shared by instances. Instance attributes iterable. The first item provided by the iterator is then assigned to the target list using the standard rules for assignments (see Assignment statements), and the suite is executed. This repeats for -each item provided by the iterator. When the iterator is exhausted, +each item provided by the iterator. When the iterator is *exhausted*, the suite in the "else" clause, if present, is executed, and the loop terminates. @@ -2455,7 +2572,7 @@ def foo(): | "None" | "True" | "False" - signed_number: ["-"] NUMBER + signed_number: ["+" | "-"] NUMBER The rule "strings" and the token "NUMBER" are defined in the standard Python grammar. Triple-quoted strings are supported. Raw strings and @@ -2779,6 +2896,8 @@ def foo(): * "float" + * "frozendict" + * "frozenset" * "int" @@ -2832,7 +2951,7 @@ def foo(): | parameter_list_no_posonly parameter_list_no_posonly: defparameter ("," defparameter)* ["," [parameter_list_starargs]] | parameter_list_starargs - parameter_list_starargs: "*" [star_parameter] ("," defparameter)* ["," [parameter_star_kwargs]] + parameter_list_starargs: "*" star_parameter ("," defparameter)* ["," [parameter_star_kwargs]] | "*" ("," defparameter)+ ["," [parameter_star_kwargs]] | parameter_star_kwargs parameter_star_kwargs: "**" parameter [","] @@ -2997,6 +3116,9 @@ class Foo: class Foo(object): pass +There may be one or more base classes; see Multiple inheritance below +for more information. + The class’s suite is then executed in a new execution frame (see Naming and binding), using a newly created local namespace and the original global namespace. (Usually, the suite contains mostly @@ -3062,6 +3184,115 @@ class attributes; they are shared by instances. Instance attributes decorators were introduced in **PEP 318**. +Multiple inheritance +-------------------- + +Python classes may have multiple base classes, a technique known as +*multiple inheritance*. The base classes are specified in the class +definition by listing them in parentheses after the class name, +separated by commas. For example, the following class definition: + + >>> class A: pass + >>> class B: pass + >>> class C(A, B): pass + +defines a class "C" that inherits from classes "A" and "B". + +The *method resolution order* (MRO) is the order in which base classes +are searched when looking up an attribute on a class. See The Python +2.3 Method Resolution Order for a description of how Python determines +the MRO for a class. + +Multiple inheritance is not always allowed. Attempting to define a +class with multiple inheritance will raise an error if one of the +bases does not allow subclassing, if a consistent MRO cannot be +created, if no valid metaclass can be determined, or if there is an +instance layout conflict. We’ll discuss each of these in turn. + +First, all base classes must allow subclassing. While most classes +allow subclassing, some built-in classes do not, such as "bool": + + >>> class SubBool(bool): # TypeError + ... pass + Traceback (most recent call last): + ... + TypeError: type 'bool' is not an acceptable base type + +In the resolved MRO of a class, the class’s bases appear in the order +they were specified in the class’s bases list. Additionally, the MRO +always lists a child class before any of its bases. A class definition +will fail if it is impossible to resolve a consistent MRO that +satisfies these rules from the list of bases provided: + + >>> class Base: pass + >>> class Child(Base): pass + >>> class Grandchild(Base, Child): pass # TypeError + Traceback (most recent call last): + ... + TypeError: Cannot create a consistent method resolution order (MRO) for bases Base, Child + +In the MRO of "Grandchild", "Base" must appear before "Child" because +it is first in the base class list, but it must also appear after +"Child" because it is a parent of "Child". This is a contradiction, so +the class cannot be defined. + +If some of the bases have a custom *metaclass*, the metaclass of the +resulting class is chosen among the metaclasses of the bases and the +explicitly specified metaclass of the child class. It must be a +metaclass that is a subclass of all other candidate metaclasses. If no +such metaclass exists among the candidates, the class cannot be +created, as explained in Determining the appropriate metaclass. + +Finally, the instance layouts of the bases must be compatible. This +means that it must be possible to compute a *solid base* for the +class. Exactly which classes are solid bases depends on the Python +implementation. + +**CPython implementation detail:** In CPython, a class is a solid base +if it has a nonempty "__slots__" definition. Many but not all classes +defined in C are also solid bases, including most builtins (such as +"int" or "BaseException") but excluding most concrete "Exception" +classes. Generally, a C class is a solid base if its underlying struct +is different in size from its base class. + +Every class has a solid base. "object", the base class, has itself as +its solid base. If there is a single base, the child class’s solid +base is that class if it is a solid base, or else the base class’s +solid base. If there are multiple bases, we first find the solid base +for each base class to produce a list of candidate solid bases. If +there is a unique solid base that is a subclass of all others, then +that class is the solid base. Otherwise, class creation fails. + +Example: + + >>> class Solid1: + ... __slots__ = ("solid1",) + >>> + >>> class Solid2: + ... __slots__ = ("solid2",) + >>> + >>> class SolidChild(Solid1): + ... __slots__ = ("solid_child",) + >>> + >>> class C1: # solid base is `object` + ... pass + >>> + >>> # OK: solid bases are `Solid1` and `object`, and `Solid1` is a subclass of `object`. + >>> class C2(Solid1, C1): # solid base is `Solid1` + ... pass + >>> + >>> # OK: solid bases are `SolidChild` and `Solid1`, and `SolidChild` is a subclass of `Solid1`. + >>> class C3(SolidChild, Solid1): # solid base is `SolidChild` + ... pass + >>> + >>> # Error: solid bases are `Solid1` and `Solid2`, but neither is a subclass of the other. + >>> class C4(Solid1, Solid2): # error: no single solid base + ... pass + Traceback (most recent call last): + ... + TypeError: multiple bases have instance lay-out conflict + + Coroutines ========== @@ -3816,11 +4047,11 @@ def f() -> annotation: ... object.__hash__(self) Called by built-in function "hash()" and for operations on members - of hashed collections including "set", "frozenset", and "dict". - The "__hash__()" method should return an integer. The only required - property is that objects which compare equal have the same hash - value; it is advised to mix together the hash values of the - components of the object that also play a part in comparison of + of hashed collections including "set", "frozenset", "dict", and + "frozendict". The "__hash__()" method should return an integer. The + only required property is that objects which compare equal have the + same hash value; it is advised to mix together the hash values of + the components of the object that also play a part in comparison of objects by packing them into a tuple and hashing the tuple. Example: @@ -4381,8 +4612,8 @@ class pdb.Pdb(completekey='tab', stdin=None, stdout=None, skip=None, nosigint=Fa To remove all commands from a breakpoint, type "commands" and follow it immediately with "end"; that is, give no commands. - With no *bpnumber* argument, "commands" refers to the last - breakpoint set. + With no *bpnumber* argument, "commands" refers to the most recently + set breakpoint that still exists. You can use breakpoint commands to start your program up again. Simply use the "continue" command, or "step", or any other command @@ -4686,7 +4917,7 @@ def middle(): def inner(x): 1 / x - out() + out() calling "pdb.pm()" will allow to move between exceptions: @@ -4742,47 +4973,49 @@ def inner(x): 'dict': r'''Dictionary displays ******************* -A dictionary display is a possibly empty series of dict items -(key/value pairs) enclosed in curly braces: - - dict_display: "{" [dict_item_list | dict_comprehension] "}" - dict_item_list: dict_item ("," dict_item)* [","] - dict_item: expression ":" expression | "**" or_expr - dict_comprehension: expression ":" expression comp_for - -A dictionary display yields a new dictionary object. - -If a comma-separated sequence of dict items is given, they are -evaluated from left to right to define the entries of the dictionary: -each key object is used as a key into the dictionary to store the -corresponding value. This means that you can specify the same key -multiple times in the dict item list, and the final dictionary’s value -for that key will be the last one given. - -A double asterisk "**" denotes *dictionary unpacking*. Its operand -must be a *mapping*. Each mapping item is added to the new -dictionary. Later values replace values already set by earlier dict -items and earlier dictionary unpackings. +A *dictionary display* is a possibly empty series of *dict items* +enclosed in curly braces. Each dict item is a colon-separated pair of +expressions: the *key* and its associated *value*. For example: + + >>> {1: 'one', 2: 'two'} + {1: 'one', 2: 'two'} + +At runtime, when a dictionary comprehension is evaluated, the +expressions are evaluated from left to right. Each key object is used +as a key into the dictionary to store the corresponding value. This +means that you can specify the same key multiple times in the +comprehension, and the final dictionary’s value for a given key will +be the last one given. For example: + + >>> { + ... 1: 'this will be overridden', + ... 2: 'two', + ... 1: 'also overridden', + ... 1: 'one', + ... } + {1: 'one', 2: 'two'} + +Instead of a key-value pair, a dict item may be an expression prefixed +by a double asterisk "**". This denotes *dictionary unpacking*. At +runtime, the expression must evaluate to a *mapping*; each item of the +mapping is added to the new dictionary. As with key-value pairs, later +values replace values already set by earlier items and unpackings. +This may be used to override a set of defaults: + + >>> defaults = {'color': 'blue', 'count': 8} + >>> overrides = {'color': 'yellow'} + >>> {**defaults, **overrides} + {'color': 'yellow', 'count': 8} Added in version 3.5: Unpacking into dictionary displays, originally proposed by **PEP 448**. -A dict comprehension, in contrast to list and set comprehensions, -needs two expressions separated with a colon followed by the usual -“for” and “if” clauses. When the comprehension is run, the resulting -key and value elements are inserted in the new dictionary in the order -they are produced. - -Restrictions on the types of the key values are listed earlier in -section The standard type hierarchy. (To summarize, the key type -should be *hashable*, which excludes all mutable objects.) Clashes -between duplicate keys are not detected; the last value (textually -rightmost in the display) stored for a given key value prevails. - -Changed in version 3.8: Prior to Python 3.8, in dict comprehensions, -the evaluation order of key and value was not well-defined. In -CPython, the value was evaluated before the key. Starting with 3.8, -the key is evaluated before the value, as proposed by **PEP 572**. +The formal grammar for dict displays is: + + dict: '{' [double_starred_kvpairs] '}' + double_starred_kvpairs: ','.double_starred_kvpair+ [','] + double_starred_kvpair: '**' or_expr | kvpair + kvpair: expression ':' expression ''', 'dynamic-features': r'''Interaction with dynamic features ********************************* @@ -5412,8 +5645,22 @@ class of the instance or a *non-virtual base class* thereof. The is the number of expressions in the list. The expressions are evaluated from left to right. -An asterisk "*" denotes *iterable unpacking*. Its operand must be an -*iterable*. The iterable is expanded into a sequence of items, which +A trailing comma is required only to create a one-item tuple, such as +"1,"; it is optional in all other cases. A single expression without a +trailing comma doesn’t create a tuple, but rather yields the value of +that expression. (To create an empty tuple, use an empty pair of +parentheses: "()".) + + +Iterable unpacking +================== + +In an expression list or tuple, list or set display, any expression +may be prefixed with an asterisk ("*"). This denotes *iterable +unpacking*. + +At runtime, the asterisk-prefixed expression must evaluate to an +*iterable*. The iterable is expanded into a sequence of items, which are included in the new tuple, list, or set, at the site of the unpacking. @@ -5422,12 +5669,6 @@ class of the instance or a *non-virtual base class* thereof. The Added in version 3.11: Any item in an expression list may be starred. See **PEP 646**. - -A trailing comma is required only to create a one-item tuple, such as -"1,"; it is optional in all other cases. A single expression without a -trailing comma doesn’t create a tuple, but rather yields the value of -that expression. (To create an empty tuple, use an empty pair of -parentheses: "()".) ''', 'floating': r'''Floating-point literals *********************** @@ -5497,7 +5738,7 @@ class of the instance or a *non-virtual base class* thereof. The iterable. The first item provided by the iterator is then assigned to the target list using the standard rules for assignments (see Assignment statements), and the suite is executed. This repeats for -each item provided by the iterator. When the iterator is exhausted, +each item provided by the iterator. When the iterator is *exhausted*, the suite in the "else" clause, if present, is executed, and the loop terminates. @@ -6096,7 +6337,7 @@ class of the instance or a *non-virtual base class* thereof. The | parameter_list_no_posonly parameter_list_no_posonly: defparameter ("," defparameter)* ["," [parameter_list_starargs]] | parameter_list_starargs - parameter_list_starargs: "*" [star_parameter] ("," defparameter)* ["," [parameter_star_kwargs]] + parameter_list_starargs: "*" star_parameter ("," defparameter)* ["," [parameter_star_kwargs]] | "*" ("," defparameter)+ ["," [parameter_star_kwargs]] | parameter_star_kwargs parameter_star_kwargs: "**" parameter [","] @@ -6369,6 +6610,8 @@ def whats_on_the_telly(penguin=None): * "type", when used in the "type" statement. +* "lazy", when used before an "import" statement. + These syntactically act as keywords in their specific contexts, but this distinction is done at the parser level, not when tokenizing. @@ -6378,6 +6621,8 @@ def whats_on_the_telly(penguin=None): Changed in version 3.12: "type" is now a soft keyword. +Changed in version 3.15: "lazy" is now a soft keyword. + Reserved classes of identifiers =============================== @@ -6587,10 +6832,10 @@ def whats_on_the_telly(penguin=None): 'import': r'''The "import" statement ********************** - import_stmt: "import" module ["as" identifier] ("," module ["as" identifier])* - | "from" relative_module "import" identifier ["as" identifier] + import_stmt: ["lazy"] "import" module ["as" identifier] ("," module ["as" identifier])* + | ["lazy"] "from" relative_module "import" identifier ["as" identifier] ("," identifier ["as" identifier])* - | "from" relative_module "import" "(" identifier ["as" identifier] + | ["lazy"] "from" relative_module "import" "(" identifier ["as" identifier] ("," identifier ["as" identifier])* [","] ")" | "from" relative_module "import" "*" module: (identifier ".")* identifier @@ -6703,6 +6948,91 @@ def whats_on_the_telly(penguin=None): "sys.path", "sys.meta_path", "sys.path_hooks". +Lazy imports +============ + +The "lazy" keyword is a soft keyword that only has special meaning +when it appears immediately before an "import" or "from" statement. +When an import statement is preceded by the "lazy" keyword, the import +becomes *lazy*: the module is not loaded immediately at the import +statement. Instead, a lazy proxy object is created and bound to the +name. The actual module is loaded on first use of that name. + +Lazy imports are only permitted at module scope. Using "lazy" inside a +function, class body, or "try"/"except"/"finally" block raises a +"SyntaxError". Star imports cannot be lazy ("lazy from module import +*" is a syntax error), and future statements cannot be lazy. + +When using "lazy from ... import", each imported name is bound to a +lazy proxy object. The first access to any of these names triggers +loading of the entire module and resolves only that specific name to +its actual value. Other names remain as lazy proxies until they are +accessed. + +Example: + + lazy import json + import sys + + print('json' in sys.modules) # False - json module not yet loaded + + # First use triggers loading + result = json.dumps({"hello": "world"}) + + print('json' in sys.modules) # True - now loaded + +If an error occurs during module loading (such as "ImportError" or +"SyntaxError"), it is raised at the point where the lazy import is +first used, not at the import statement itself. + +See **PEP 810** for the full specification of lazy imports. + +Added in version 3.15. + + +Compatibility via "__lazy_modules__" +------------------------------------ + +As an alternative to using the "lazy" keyword, a module can opt into +lazy loading for specific imports by defining a module-level +"__lazy_modules__" variable. When present, it must be a container of +fully qualified module name strings. Any regular (non-"lazy") +"import" statement at module scope whose target appears in +"__lazy_modules__" is treated as a lazy import, exactly as if the +"lazy" keyword had been used. + +This provides a way to enable lazy loading for specific dependencies +without changing individual "import" statements. This is useful when +supporting Python versions older than 3.15 while using lazy imports in +3.15+: + + __lazy_modules__ = ["json", "pathlib"] + + import json # loaded lazily (name is in __lazy_modules__) + import os # loaded eagerly (name not in __lazy_modules__) + + import pathlib # loaded lazily + +Relative imports are resolved to their absolute name before the +lookup, so "__lazy_modules__" must always contain fully qualified +module names. + +For "from"-style imports, the relevant name is the module following +"from", not the names of its members: + + # In mypackage/mymodule.py + __lazy_modules__ = ["mypackage", "mypackage.sub.utils"] + + from . import helper # loaded lazily: . resolves to mypackage + from .sub.utils import func # loaded lazily: .sub.utils resolves to mypackage.sub.utils + import json # loaded eagerly (not in __lazy_modules__) + +Imports inside functions, class bodies, or "try"/"except"/"finally" +blocks are always eager, regardless of "__lazy_modules__". + +Added in version 3.15. + + Future statements ================= @@ -6902,21 +7232,109 @@ def (parameters): See section Function definitions for the syntax of parameter lists. Note that functions created with lambda expressions cannot contain statements or annotations. +''', + 'lazy': r'''Lazy imports +************ + +The "lazy" keyword is a soft keyword that only has special meaning +when it appears immediately before an "import" or "from" statement. +When an import statement is preceded by the "lazy" keyword, the import +becomes *lazy*: the module is not loaded immediately at the import +statement. Instead, a lazy proxy object is created and bound to the +name. The actual module is loaded on first use of that name. + +Lazy imports are only permitted at module scope. Using "lazy" inside a +function, class body, or "try"/"except"/"finally" block raises a +"SyntaxError". Star imports cannot be lazy ("lazy from module import +*" is a syntax error), and future statements cannot be lazy. + +When using "lazy from ... import", each imported name is bound to a +lazy proxy object. The first access to any of these names triggers +loading of the entire module and resolves only that specific name to +its actual value. Other names remain as lazy proxies until they are +accessed. + +Example: + + lazy import json + import sys + + print('json' in sys.modules) # False - json module not yet loaded + + # First use triggers loading + result = json.dumps({"hello": "world"}) + + print('json' in sys.modules) # True - now loaded + +If an error occurs during module loading (such as "ImportError" or +"SyntaxError"), it is raised at the point where the lazy import is +first used, not at the import statement itself. + +See **PEP 810** for the full specification of lazy imports. + +Added in version 3.15. + + +Compatibility via "__lazy_modules__" +==================================== + +As an alternative to using the "lazy" keyword, a module can opt into +lazy loading for specific imports by defining a module-level +"__lazy_modules__" variable. When present, it must be a container of +fully qualified module name strings. Any regular (non-"lazy") +"import" statement at module scope whose target appears in +"__lazy_modules__" is treated as a lazy import, exactly as if the +"lazy" keyword had been used. + +This provides a way to enable lazy loading for specific dependencies +without changing individual "import" statements. This is useful when +supporting Python versions older than 3.15 while using lazy imports in +3.15+: + + __lazy_modules__ = ["json", "pathlib"] + + import json # loaded lazily (name is in __lazy_modules__) + import os # loaded eagerly (name not in __lazy_modules__) + + import pathlib # loaded lazily + +Relative imports are resolved to their absolute name before the +lookup, so "__lazy_modules__" must always contain fully qualified +module names. + +For "from"-style imports, the relevant name is the module following +"from", not the names of its members: + + # In mypackage/mymodule.py + __lazy_modules__ = ["mypackage", "mypackage.sub.utils"] + + from . import helper # loaded lazily: . resolves to mypackage + from .sub.utils import func # loaded lazily: .sub.utils resolves to mypackage.sub.utils + import json # loaded eagerly (not in __lazy_modules__) + +Imports inside functions, class bodies, or "try"/"except"/"finally" +blocks are always eager, regardless of "__lazy_modules__". + +Added in version 3.15. ''', 'lists': r'''List displays ************* -A list display is a possibly empty series of expressions enclosed in -square brackets: +A *list display* is a possibly empty series of expressions enclosed in +square brackets. For example: - list_display: "[" [flexible_expression_list | comprehension] "]" + >>> ["one", "two", "three"] + ['one', 'two', 'three'] + >>> ["one"] # One-element list + ['one'] + >>> [] # empty list + [] -A list display yields a new list object, the contents being specified -by either a list of expressions or a comprehension. When a comma- -separated list of expressions is supplied, its elements are evaluated -from left to right and placed into the list object in that order. -When a comprehension is supplied, the list is constructed from the -elements resulting from the comprehension. +See Container displays for general information on displays. + +The formal grammar for list displays is: + + list: '[' [flexible_expression_list] ']' ''', 'match': r'''The "match" statement ********************* @@ -7154,7 +7572,7 @@ def (parameters): | "None" | "True" | "False" - signed_number: ["-"] NUMBER + signed_number: ["+" | "-"] NUMBER The rule "strings" and the token "NUMBER" are defined in the standard Python grammar. Triple-quoted strings are supported. Raw strings and @@ -7478,6 +7896,8 @@ def (parameters): * "float" + * "frozendict" + * "frozenset" * "int" @@ -9054,11 +9474,11 @@ class C: pass # a class with no methods (yet) object.__hash__(self) Called by built-in function "hash()" and for operations on members - of hashed collections including "set", "frozenset", and "dict". - The "__hash__()" method should return an integer. The only required - property is that objects which compare equal have the same hash - value; it is advised to mix together the hash values of the - components of the object that also play a part in comparison of + of hashed collections including "set", "frozenset", "dict", and + "frozendict". The "__hash__()" method should return an integer. The + only required property is that objects which compare equal have the + same hash value; it is advised to mix together the hash values of + the components of the object that also play a part in comparison of objects by packing them into a tuple and hashing the tuple. Example: @@ -9449,9 +9869,10 @@ class to a new value, *value*. renders the meaning of the program undefined. In the future, a check may be added to prevent this. -* "TypeError" will be raised if nonempty *__slots__* are defined for a - class derived from a ""variable-length" built-in type" such as - "int", "bytes", and "tuple". +* "TypeError" will be raised if *__slots__* other than *__dict__* and + *__weakref__* are defined for a class derived from a ""variable- + length" built-in type" such as "int", "bytes", and "type", except + "tuple". * Any non-string *iterable* may be assigned to *__slots__*. @@ -9472,6 +9893,10 @@ class derived from a ""variable-length" built-in type" such as created for each of the iterator’s values. However, the *__slots__* attribute will be an empty iterator. +Changed in version 3.15: Allowed defining the *__dict__* and +*__weakref__* *__slots__* for any class. Allowed defining any +*__slots__* for a class derived from "tuple". + Customizing class creation ========================== @@ -10530,7 +10955,7 @@ class is used in a class pattern with positional arguments, each >>> 'straße'.casefold() 'strasse' - The casefolding algorithm is described in section 3.13 ‘Default + The casefolding algorithm is described in section 3.13.3 ‘Default Case Folding’ of the Unicode Standard. Added in version 3.3. @@ -10754,7 +11179,7 @@ class is used in a class pattern with positional arguments, each characters are those characters defined in the Unicode character database as “Letter”, i.e., those with general category property being one of “Lm”, “Lt”, “Lu”, “Ll”, or “Lo”. Note that this is - different from the Alphabetic property defined in the section 4.10 + different from the Alphabetic property defined in section 4.10 ‘Letters, Alphabetic, and Ideographic’ of the Unicode Standard. For example: @@ -10985,8 +11410,8 @@ class is used in a class pattern with positional arguments, each >>> 'Lower Method Example'.lower() 'lower method example' - The lowercasing algorithm used is described in section 3.13 - ‘Default Case Folding’ of the Unicode Standard. + The lowercasing algorithm used is described in section 3.13.2 + ‘Default Case Conversion’ of the Unicode Standard. str.lstrip(chars=None, /) @@ -11026,6 +11451,8 @@ class is used in a class pattern with positional arguments, each a third argument, it must be a string, whose characters will be mapped to "None" in the result. + Changed in version 3.15: *dict* can now be a "frozendict". + str.partition(sep, /) Split the string at the first occurrence of *sep*, and return a @@ -11162,9 +11589,9 @@ class is used in a class pattern with positional arguments, each Return a list of the words in the string, using *sep* as the delimiter string. If *maxsplit* is given, at most *maxsplit* splits are done, the *rightmost* ones. If *sep* is not specified or - "None", any whitespace string is a separator. Except for splitting - from the right, "rsplit()" behaves like "split()" which is - described in detail below. + "None", any "whitespace" string is a separator. Except for + splitting from the right, "rsplit()" behaves like "split()" which + is described in detail below. str.rstrip(chars=None, /) @@ -11216,7 +11643,7 @@ class is used in a class pattern with positional arguments, each ['1', '2', '3<4'] If *sep* is not specified or is "None", a different splitting - algorithm is applied: runs of consecutive whitespace are regarded + algorithm is applied: runs of consecutive "whitespace" are regarded as a single separator, and the result will contain no empty strings at the start or end if the string has leading or trailing whitespace. Consequently, splitting an empty string or a string @@ -11422,6 +11849,12 @@ class is used in a class pattern with positional arguments, each You can use "str.maketrans()" to create a translation map from character-to-character mappings in different formats. + The following example uses a mapping to replace "'a'" with "'X'", + "'b'" with "'Y'", and delete "'c'": + + >>> 'abc123'.translate({ord('a'): 'X', ord('b'): 'Y', ord('c'): None}) + 'XY123' + See also the "codecs" module for a more flexible approach to custom character mappings. @@ -11433,8 +11866,8 @@ class is used in a class pattern with positional arguments, each category of the resulting character(s) is not “Lu” (Letter, uppercase), but e.g. “Lt” (Letter, titlecase). - The uppercasing algorithm used is described in section 3.13 - ‘Default Case Folding’ of the Unicode Standard. + The uppercasing algorithm used is described in section 3.13.2 + ‘Default Case Conversion’ of the Unicode Standard. str.zfill(width, /) @@ -12717,7 +13150,7 @@ def foo(): target of assignments or "del" statements. The built-in function "len()" returns the number of items in a mapping. -There is currently a single intrinsic mapping type: +There are two intrinsic mapping types: Dictionaries @@ -12751,6 +13184,18 @@ def foo(): rather than a language guarantee. +Frozen dictionaries +------------------- + +These represent an immutable dictionary. They are created by the +built-in "frozendict()" constructor. A frozendict is *hashable* if +all of its keys and values are hashable, in which case it can be used +as an element of a set, or as a key in another mapping. "frozendict" +is not a subclass of "dict"; it inherits directly from "object". + +Added in version 3.15. + + Callable types ============== @@ -12914,12 +13359,12 @@ def foo(): Generator functions ------------------- -A function or method which uses the "yield" statement (see section The -yield statement) is called a *generator function*. Such a function, +A function or method which contains a "yield" expression (see section +Yield expressions) is called a *generator function*. Such a function, when called, always returns an *iterator* object which can be used to execute the body of the function: calling the iterator’s "iterator.__next__()" method will cause the function to execute until -it provides a value using the "yield" statement. When the function +it provides a value using the "yield" expression. When the function executes a "return" statement or falls off the end, a "StopIteration" exception is raised and the iterator will have reached the end of the set of values to be returned. @@ -12938,11 +13383,11 @@ def foo(): Asynchronous generator functions -------------------------------- -A function or method which is defined using "async def" and which uses -the "yield" statement is called a *asynchronous generator function*. -Such a function, when called, returns an *asynchronous iterator* -object which can be used in an "async for" statement to execute the -body of the function. +A function or method which is defined using "async def" and which +contains a "yield" expression is called a *asynchronous generator +function*. Such a function, when called, returns an *asynchronous +iterator* object which can be used in an "async for" statement to +execute the body of the function. Calling the asynchronous iterator’s "aiterator.__anext__" method will return an *awaitable* which when awaited will execute until it @@ -13108,7 +13553,7 @@ def foo(): "ImportWarning" when falling back to "__package__" during import resolution. - Deprecated since version 3.13, will be removed in version 3.15: + Deprecated since version 3.13, removed in version 3.15: "__package__" will cease to be set or taken into consideration by the import system or standard library. @@ -13152,41 +13597,21 @@ def foo(): module.__file__ -module.__cached__ - - "__file__" and "__cached__" are both optional attributes that may - or may not be set. Both attributes should be a "str" when they are - available. - - "__file__" indicates the pathname of the file from which the module - was loaded (if loaded from a file), or the pathname of the shared - library file for extension modules loaded dynamically from a shared - library. It might be missing for certain types of modules, such as - C modules that are statically linked into the interpreter, and the - import system may opt to leave it unset if it has no semantic - meaning (for example, a module loaded from a database). - - If "__file__" is set then the "__cached__" attribute might also be - set, which is the path to any compiled version of the code (for - example, a byte-compiled file). The file does not need to exist to - set this attribute; the path can simply point to where the compiled - file *would* exist (see **PEP 3147**). - - Note that "__cached__" may be set even if "__file__" is not set. - However, that scenario is quite atypical. Ultimately, the *loader* - is what makes use of the module spec provided by the *finder* (from - which "__file__" and "__cached__" are derived). So if a loader can - load from a cached module but otherwise does not load from a file, - that atypical scenario may be appropriate. + "__file__" is an optional attribute that may or may not be set. + Both attributes should be a "str" when they are available. - It is **strongly** recommended that you use - "module.__spec__.cached" instead of "module.__cached__". + An optional attribute, "__file__" indicates the pathname of the + file from which the module was loaded (if loaded from a file), or + the pathname of the shared library file for extension modules + loaded dynamically from a shared library. It might be missing for + certain types of modules, such as C modules that are statically + linked into the interpreter, and the import system may opt to leave + it unset if it has no semantic meaning (for example, a module + loaded from a database). - Deprecated since version 3.13, will be removed in version 3.15: - Setting "__cached__" on a module while failing to set - "__spec__.cached" is deprecated. In Python 3.15, "__cached__" will - cease to be set or taken into consideration by the import system or - standard library. +Changed in version 3.15: The "__cached__" attribute is no longer set +on modules or taken into consideration by the import system or +standard library. Other writable attributes on module objects @@ -13216,6 +13641,19 @@ def foo(): Added in version 3.14. +module.__lazy_modules__ + + A container (an object implementing "__contains__()") of fully + qualified module name strings. When defined at module scope, any + regular "import" statement in that module whose target module name + appears in this container is treated as a lazy import, as if the + "lazy" keyword had been used. Imports inside functions, class + bodies, or "try"/"except"/"finally" blocks are unaffected. + + See Compatibility via __lazy_modules__ for details and examples. + + Added in version 3.15. + Module dictionaries ------------------- @@ -13512,11 +13950,14 @@ class instance has a namespace implemented as a dictionary which is +----------------------------------------------------+----------------------------------------------------+ | codeobject.co_firstlineno | The line number of the first line of the function | +----------------------------------------------------+----------------------------------------------------+ -| codeobject.co_lnotab | A string encoding the mapping from *bytecode* | -| | offsets to line numbers. For details, see the | -| | source code of the interpreter. Deprecated since | -| | version 3.12: This attribute of code objects is | -| | deprecated, and may be removed in Python 3.15. | +| codeobject.co_linetable | A "bytes" object containing encoded source | +| | location information. The exact format is an | +| | implementation detail and may change between | +| | Python versions. Use "co_lines()" and | +| | "co_positions()" for supported access to line and | +| | position information. To create a modified copy of | +| | a code object, use "replace()". Added in version | +| | 3.10. | +----------------------------------------------------+----------------------------------------------------+ | codeobject.co_stacksize | The required stack size of the code object | +----------------------------------------------------+----------------------------------------------------+ @@ -13785,6 +14226,10 @@ class instance has a namespace implemented as a dictionary which is Slice objects are used to represent slices for "__getitem__()" methods. They are also created by the built-in "slice()" function. +Added in version 3.15: The "slice()" type now supports subscription. +For example, "slice[float]" may be used in type annotations to +indicate a slice containing "float" objects. + Special read-only attributes: "start" is the lower bound; "stop" is the upper bound; "step" is the step value; each is "None" if omitted. These attributes can have any type. @@ -13838,13 +14283,15 @@ class instance has a namespace implemented as a dictionary which is See Function definitions for more information. ''', - 'typesmapping': r'''Mapping Types — "dict" -********************** + 'typesmapping': r'''Mapping types — "dict", "frozendict" +************************************ -A *mapping* object maps *hashable* values to arbitrary objects. -Mappings are mutable objects. There is currently only one standard -mapping type, the *dictionary*. (For other containers see the built- -in "list", "set", and "tuple" classes, and the "collections" module.) +A *mapping* object maps *hashable* values to arbitrary objects. There +are currently two standard mapping types, the *dictionary* and +"frozendict". (For other containers see the built-in "list", "set", +and "tuple" classes, and the "collections" module.) See Time +complexity of operations on built-in types for the costs of the +various dictionary operations. A dictionary’s keys are *almost* arbitrary values. Values that are not *hashable*, that is, values containing lists, dictionaries or @@ -14121,10 +14568,10 @@ class dict(iterable, /, **kwargs) Changed in version 3.8: Dictionaries are now reversible. -See also: + See also: - "types.MappingProxyType" can be used to create a read-only view of a - "dict". + "frozendict" and "types.MappingProxyType" can be used to create a + read-only view of a "dict". See also: @@ -14234,13 +14681,65 @@ class dict(iterable, /, **kwargs) mappingproxy({'bacon': 1, 'spam': 500}) >>> values.mapping['spam'] 500 + + +Frozen dictionaries +=================== + +class frozendict(**kwargs) +class frozendict(mapping, /, **kwargs) +class frozendict(iterable, /, **kwargs) + + Return a new frozen dictionary initialized from an optional + positional argument and a possibly empty set of keyword arguments. + + A "frozendict" has a similar API to the "dict" API, with the + following differences: + + * "dict" has more methods than "frozendict": + + * "__delitem__()" + + * "__setitem__()" + + * "clear()" + + * "pop()" + + * "popitem()" + + * "setdefault()" + + * "update()" + + * A "frozendict" can be hashed with "hash(frozendict)" if all keys + and values can be hashed. + + * "frozendict |= other" does not modify the "frozendict" in-place + but creates a new frozen dictionary. + + "frozendict" is not a "dict" subclass but inherits directly from + "object". + + Like dictionaries, frozendicts are generic over two types, + signifying (respectively) the types of the frozendict’s keys and + values. + + classmethod fromkeys(iterable, value=None, /) + + Similar to "dict.fromkeys()", but call again the type + constructor with an initialized "frozendict" if the type is a + "frozendict" subclass or if the constructor returned a + "frozendict". + + Added in version 3.15. ''', 'typesmethods': r'''Methods ******* Methods are functions that are called using the attribute notation. There are two flavors: built-in methods (such as "append()" on lists) -and class instance method. Built-in methods are described with the +and class instance methods. Built-in methods are described with the types that support them. If you access a method (a function defined in a class namespace) @@ -14324,6 +14823,9 @@ class dict(iterable, /, **kwargs) operations have the same priority as the corresponding numeric operations. [3] +See Time complexity of operations on built-in types for the costs of +the various sequence operations. + +----------------------------+----------------------------------+------------+ | Operation | Result | Notes | |============================|==================================|============| @@ -14589,8 +15091,8 @@ class dict(iterable, /, **kwargs) sequence.pop(index=-1, /) - Retrieve the item at *index* and also removes it from *sequence*. - By default, the last item in *sequence* is removed and returned. + Retrieve the item at *index* and also remove it from *sequence*. By + default, the last item in *sequence* is removed and returned. sequence.remove(value, /) @@ -14948,8 +15450,8 @@ class range(start, stop, step=1, /) sequence.pop(index=-1, /) - Retrieve the item at *index* and also removes it from *sequence*. - By default, the last item in *sequence* is removed and returned. + Retrieve the item at *index* and also remove it from *sequence*. By + default, the last item in *sequence* is removed and returned. sequence.remove(value, /) diff --git a/Lib/re/__init__.py b/Lib/re/__init__.py index af2808a77da..e6d29fd7a40 100644 --- a/Lib/re/__init__.py +++ b/Lib/re/__init__.py @@ -85,17 +85,18 @@ \\ Matches a literal backslash. This module exports the following functions: - match Match a regular expression pattern to the beginning of a string. - fullmatch Match a regular expression pattern to all of a string. - search Search a string for the presence of a pattern. - sub Substitute occurrences of a pattern found in a string. - subn Same as sub, but also return the number of substitutions made. - split Split a string by the occurrences of a pattern. - findall Find all occurrences of a pattern in a string. - finditer Return an iterator yielding a Match object for each match. - compile Compile a pattern into a Pattern object. - purge Clear the regular expression cache. - escape Backslash all non-alphanumerics in a string. + prefixmatch Match a regular expression pattern to the beginning of a str. + match The original name of prefixmatch prior to 3.15. + fullmatch Match a regular expression pattern to all of a string. + search Search a string for the presence of a pattern. + sub Substitute occurrences of a pattern found in a string. + subn Same as sub, but also return the number of substitutions made. + split Split a string by the occurrences of a pattern. + findall Find all occurrences of a pattern in a string. + finditer Return an iterator yielding a Match object for each match. + compile Compile a pattern into a Pattern object. + purge Clear the regular expression cache. + escape Backslash all non-alphanumerics in a string. Each function other than purge and escape can take an optional 'flags' argument consisting of one or more of the following module constants, joined by "|". @@ -130,15 +131,13 @@ # public symbols __all__ = [ - "match", "fullmatch", "search", "sub", "subn", "split", + "prefixmatch", "match", "fullmatch", "search", "sub", "subn", "split", "findall", "finditer", "compile", "purge", "escape", "error", "Pattern", "Match", "A", "I", "L", "M", "S", "X", "U", "ASCII", "IGNORECASE", "LOCALE", "MULTILINE", "DOTALL", "VERBOSE", "UNICODE", "NOFLAG", "RegexFlag", "PatternError" ] -__version__ = "2.2.1" - @enum.global_enum @enum._simple_enum(enum.IntFlag, boundary=enum.KEEP) class RegexFlag: @@ -161,10 +160,13 @@ class RegexFlag: # -------------------------------------------------------------------- # public interface -def match(pattern, string, flags=0): +def prefixmatch(pattern, string, flags=0): """Try to apply the pattern at the start of the string, returning a Match object, or None if no match was found.""" - return _compile(pattern, flags).match(string) + return _compile(pattern, flags).prefixmatch(string) + +# Our original name which was less explicitly clear about the behavior for prefixmatch. +match = prefixmatch def fullmatch(pattern, string, flags=0): """Try to apply the pattern to all of the string, returning @@ -313,7 +315,7 @@ def escape(pattern): return pattern.translate(_special_chars_map).encode('latin1') Pattern = type(_compiler.compile('', 0)) -Match = type(_compiler.compile('', 0).match('')) +Match = type(_compiler.compile('', 0).prefixmatch('')) # -------------------------------------------------------------------- # internals @@ -399,9 +401,12 @@ def __init__(self, lexicon, flags=0): s = _parser.State() s.flags = flags for phrase, action in lexicon: + sub_pattern = _parser.parse(phrase, flags) + if sub_pattern.state.groups != 1: + raise ValueError("Cannot use capturing groups in re.Scanner") gid = s.opengroup() p.append(_parser.SubPattern(s, [ - (SUBPATTERN, (gid, 0, 0, _parser.parse(phrase, flags))), + (SUBPATTERN, (gid, 0, 0, sub_pattern)), ])) s.closegroup(gid, p[-1]) p = _parser.SubPattern(s, [(BRANCH, (None, p))]) @@ -409,10 +414,10 @@ def __init__(self, lexicon, flags=0): def scan(self, string): result = [] append = result.append - match = self.scanner.scanner(string).match + _match = self.scanner.scanner(string).prefixmatch i = 0 while True: - m = match() + m = _match() if not m: break j = m.end() @@ -426,3 +431,12 @@ def scan(self, string): append(action) i = j return result, string[i:] + + +def __getattr__(name): + if name == "__version__": + from warnings import _deprecated + + _deprecated("__version__", remove=(3, 20)) + return "2.2.1" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/re/_compiler.py b/Lib/re/_compiler.py index 20dd561d1c1..c2ca8e25abe 100644 --- a/Lib/re/_compiler.py +++ b/Lib/re/_compiler.py @@ -375,7 +375,7 @@ def _optimize_charset(charset, iscased=None, fixup=None, fixes=None): # less significant byte is a bit index in the chunk (just like the # CHARSET matching). - charmap = bytes(charmap) # should be hashable + charmap = charmap.take_bytes() # should be hashable comps = {} mapping = bytearray(256) block = 0 diff --git a/Lib/re/_parser.py b/Lib/re/_parser.py index 35ab7ede2a7..bd189fe0695 100644 --- a/Lib/re/_parser.py +++ b/Lib/re/_parser.py @@ -455,7 +455,6 @@ def _parse_sub(source, state, verbose, nested): items = [] itemsappend = items.append sourcematch = source.match - start = source.tell() while True: itemsappend(_parse(source, state, verbose, nested + 1, not nested and not items)) diff --git a/Lib/rlcompleter.py b/Lib/rlcompleter.py index e75dd0a9e3d..6c6d9bb6b34 100644 --- a/Lib/rlcompleter.py +++ b/Lib/rlcompleter.py @@ -34,9 +34,9 @@ import inspect import keyword import re -import types import __main__ import warnings +import types __all__ = ["Completer"] @@ -189,7 +189,17 @@ def attr_matches(self, text): # arbitrary code or raise exceptions. matches.append(match) continue - if (value := getattr(thisobject, word, None)) is not None: + + if (isinstance(thisobject, types.ModuleType) + and + isinstance(thisobject.__dict__.get(word), + types.LazyImportType) + ): + value = thisobject.__dict__.get(word) + else: + value = getattr(thisobject, word, None) + + if value is not None: matches.append(self._callable_postfix(value, match)) else: matches.append(match) diff --git a/Lib/shelve.py b/Lib/shelve.py index 50584716e9e..9f6296667fd 100644 --- a/Lib/shelve.py +++ b/Lib/shelve.py @@ -56,12 +56,16 @@ the persistent dictionary on disk, if feasible). """ -from pickle import DEFAULT_PROTOCOL, Pickler, Unpickler -from io import BytesIO +from pickle import DEFAULT_PROTOCOL, dumps, loads import collections.abc -__all__ = ["Shelf", "BsdDbShelf", "DbfilenameShelf", "open"] +__all__ = ["ShelveError", "Shelf", "BsdDbShelf", "DbfilenameShelf", "open"] + + +class ShelveError(Exception): + pass + class _ClosedDict(collections.abc.MutableMapping): 'Marker for a closed dict. Access attempts raise a ValueError.' @@ -82,7 +86,7 @@ class Shelf(collections.abc.MutableMapping): """ def __init__(self, dict, protocol=None, writeback=False, - keyencoding="utf-8"): + keyencoding="utf-8", *, serializer=None, deserializer=None): self.dict = dict if protocol is None: protocol = DEFAULT_PROTOCOL @@ -91,6 +95,16 @@ def __init__(self, dict, protocol=None, writeback=False, self.cache = {} self.keyencoding = keyencoding + if serializer is None and deserializer is None: + self.serializer = dumps + self.deserializer = loads + elif (serializer is None) ^ (deserializer is None): + raise ShelveError("serializer and deserializer must be " + "defined together") + else: + self.serializer = serializer + self.deserializer = deserializer + def __iter__(self): for k in self.dict.keys(): yield k.decode(self.keyencoding) @@ -110,8 +124,8 @@ def __getitem__(self, key): try: value = self.cache[key] except KeyError: - f = BytesIO(self.dict[key.encode(self.keyencoding)]) - value = Unpickler(f).load() + f = self.dict[key.encode(self.keyencoding)] + value = self.deserializer(f) if self.writeback: self.cache[key] = value return value @@ -119,10 +133,8 @@ def __getitem__(self, key): def __setitem__(self, key, value): if self.writeback: self.cache[key] = value - f = BytesIO() - p = Pickler(f, self._protocol) - p.dump(value) - self.dict[key.encode(self.keyencoding)] = f.getvalue() + serialized_value = self.serializer(value, self._protocol) + self.dict[key.encode(self.keyencoding)] = serialized_value def __delitem__(self, key): del self.dict[key.encode(self.keyencoding)] @@ -171,6 +183,11 @@ def sync(self): if hasattr(self.dict, 'sync'): self.dict.sync() + def reorganize(self): + self.sync() + if hasattr(self.dict, 'reorganize'): + self.dict.reorganize() + class BsdDbShelf(Shelf): """Shelf implementation using the "BSD" db interface. @@ -186,33 +203,29 @@ class BsdDbShelf(Shelf): """ def __init__(self, dict, protocol=None, writeback=False, - keyencoding="utf-8"): - Shelf.__init__(self, dict, protocol, writeback, keyencoding) + keyencoding="utf-8", *, serializer=None, deserializer=None): + Shelf.__init__(self, dict, protocol, writeback, keyencoding, + serializer=serializer, deserializer=deserializer) def set_location(self, key): (key, value) = self.dict.set_location(key) - f = BytesIO(value) - return (key.decode(self.keyencoding), Unpickler(f).load()) + return (key.decode(self.keyencoding), self.deserializer(value)) def next(self): (key, value) = next(self.dict) - f = BytesIO(value) - return (key.decode(self.keyencoding), Unpickler(f).load()) + return (key.decode(self.keyencoding), self.deserializer(value)) def previous(self): (key, value) = self.dict.previous() - f = BytesIO(value) - return (key.decode(self.keyencoding), Unpickler(f).load()) + return (key.decode(self.keyencoding), self.deserializer(value)) def first(self): (key, value) = self.dict.first() - f = BytesIO(value) - return (key.decode(self.keyencoding), Unpickler(f).load()) + return (key.decode(self.keyencoding), self.deserializer(value)) def last(self): (key, value) = self.dict.last() - f = BytesIO(value) - return (key.decode(self.keyencoding), Unpickler(f).load()) + return (key.decode(self.keyencoding), self.deserializer(value)) class DbfilenameShelf(Shelf): @@ -222,9 +235,11 @@ class DbfilenameShelf(Shelf): See the module's __doc__ string for an overview of the interface. """ - def __init__(self, filename, flag='c', protocol=None, writeback=False): + def __init__(self, filename, flag='c', protocol=None, writeback=False, *, + serializer=None, deserializer=None): import dbm - Shelf.__init__(self, dbm.open(filename, flag), protocol, writeback) + Shelf.__init__(self, dbm.open(filename, flag), protocol, writeback, + serializer=serializer, deserializer=deserializer) def clear(self): """Remove all items from the shelf.""" @@ -233,8 +248,8 @@ def clear(self): self.cache.clear() self.dict.clear() - -def open(filename, flag='c', protocol=None, writeback=False): +def open(filename, flag='c', protocol=None, writeback=False, *, + serializer=None, deserializer=None): """Open a persistent dictionary for reading and writing. The filename parameter is the base filename for the underlying @@ -247,4 +262,5 @@ def open(filename, flag='c', protocol=None, writeback=False): See the module's __doc__ string for an overview of the interface. """ - return DbfilenameShelf(filename, flag, protocol, writeback) + return DbfilenameShelf(filename, flag, protocol, writeback, + serializer=serializer, deserializer=deserializer) diff --git a/Lib/site-packages/README.txt b/Lib/site-packages/README.txt new file mode 100644 index 00000000000..273f6251a7f --- /dev/null +++ b/Lib/site-packages/README.txt @@ -0,0 +1,2 @@ +This directory exists so that 3rd party packages can be installed +here. Read the source for site.py for more details. diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 4ccf292ddf2..ec72c694390 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -10,9 +10,12 @@ from argparse import ArgumentParser from code import InteractiveConsole from textwrap import dedent +from _colorize import get_theme, theme_no_color +from ._completer import completer -def execute(c, sql, suppress_errors=True): + +def execute(c, sql, suppress_errors=True, theme=theme_no_color): """Helper that wraps execution of SQL code. This is used both by the REPL and by direct execution from the CLI. @@ -25,11 +28,15 @@ def execute(c, sql, suppress_errors=True): for row in c.execute(sql): print(row) except sqlite3.Error as e: + t = theme.traceback tp = type(e).__name__ try: - print(f"{tp} ({e.sqlite_errorname}): {e}", file=sys.stderr) + tp += f" ({e.sqlite_errorname})" except AttributeError: - print(f"{tp}: {e}", file=sys.stderr) + pass + print( + f"{t.type}{tp}{t.reset}: {t.message}{e}{t.reset}", file=sys.stderr + ) if not suppress_errors: sys.exit(1) @@ -37,10 +44,11 @@ def execute(c, sql, suppress_errors=True): class SqliteInteractiveConsole(InteractiveConsole): """A simple SQLite REPL.""" - def __init__(self, connection): + def __init__(self, connection, use_color=False): super().__init__() self._con = connection self._cur = connection.cursor() + self._use_color = use_color def runsource(self, source, filename="", symbol="single"): """Override runsource, the core of the InteractiveConsole REPL. @@ -48,37 +56,42 @@ def runsource(self, source, filename="", symbol="single"): Return True if more input is needed; buffering is done automatically. Return False if input is a complete statement ready for execution. """ + theme = get_theme(force_no_color=not self._use_color) + if not source or source.isspace(): return False + # Remember to update CLI_COMMANDS in _completer.py if source[0] == ".": match source[1:].strip(): case "version": - print(f"{sqlite3.sqlite_version}") + print(sqlite3.sqlite_version) case "help": - print("Enter SQL code and press enter.") + t = theme.syntax + print(f"Enter SQL code or one of the below commands, and press enter.\n\n" + f"{t.builtin}.version{t.reset} Print underlying SQLite library version\n" + f"{t.builtin}.help{t.reset} Print this help message\n" + f"{t.builtin}.quit{t.reset} Exit the CLI, equivalent to CTRL-D\n") case "quit": sys.exit(0) case "": pass case _ as unknown: - self.write("Error: unknown command or invalid arguments:" - f' "{unknown}".\n') + t = theme.traceback + self.write(f'{t.type}Error{t.reset}: {t.message}unknown ' + f'command: "{unknown}"{t.reset}\n') else: if not sqlite3.complete_statement(source): return True - execute(self._cur, source) + execute(self._cur, source, theme=theme) return False def main(*args): - parser = ArgumentParser( - description="Python sqlite3 CLI", - color=True, - ) + parser = ArgumentParser(description="Python sqlite3 CLI") parser.add_argument( "filename", type=str, default=":memory:", nargs="?", help=( - "SQLite database to open (defaults to ':memory:'). " + "SQLite database to open (defaults to `:memory:`). " "A new database is created if the file does not previously exist." ), ) @@ -86,7 +99,7 @@ def main(*args): "sql", type=str, nargs="?", help=( "An SQL query to execute. " - "Any returned rows are printed to stdout." + "Any returned rows are printed to `stdout`." ), ) parser.add_argument( @@ -113,22 +126,26 @@ def main(*args): Each command will be run using execute() on the cursor. Type ".help" for more information; type ".quit" or {eofkey} to quit. """).strip() - sys.ps1 = "sqlite> " - sys.ps2 = " ... " + + theme = get_theme() + s = theme.syntax + + # Use RL_PROMPT_START_IGNORE (\001) and RL_PROMPT_END_IGNORE (\002) to + # bracket non-printing characters. This tells readline to ignore them + # when calculating screen space for redisplay during history scrolling. + sys.ps1 = f"\001{s.prompt}\002sqlite> \001{s.reset}\002" + sys.ps2 = f"\001{s.prompt}\002 ... \001{s.reset}\002" con = sqlite3.connect(args.filename, isolation_level=None) try: if args.sql: # SQL statement provided on the command-line; execute it directly. - execute(con, args.sql, suppress_errors=False) + execute(con, args.sql, suppress_errors=False, theme=theme) else: # No SQL provided; start the REPL. - console = SqliteInteractiveConsole(con) - try: - import readline # noqa: F401 - except ImportError: - pass - console.interact(banner, exitmsg="") + with completer(con): + console = SqliteInteractiveConsole(con, use_color=True) + console.interact(banner, exitmsg="") finally: con.close() diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py new file mode 100644 index 00000000000..ba580f968bf --- /dev/null +++ b/Lib/sqlite3/_completer.py @@ -0,0 +1,111 @@ +from _sqlite3 import OperationalError +from contextlib import contextmanager + +try: + from _sqlite3 import SQLITE_KEYWORDS +except ImportError: + SQLITE_KEYWORDS = () + +CLI_COMMANDS = ('.quit', '.help', '.version') + +_completion_matches = [] + + +def _complete(con, text, state): + global _completion_matches + + if state == 0: + if text.startswith("."): + _completion_matches = [ + c + " " for c in CLI_COMMANDS if c.startswith(text) + ] + else: + text_upper = text.upper() + _completion_matches = [ + c + " " for c in SQLITE_KEYWORDS if c.startswith(text_upper) + ] + + cursor = con.cursor() + schemata = tuple(row[1] for row + in cursor.execute("PRAGMA database_list")) + # tables, indexes, triggers, and views + # escape '_' which can appear in attached database names + select_clauses = ( + f"""\ + SELECT name || ' ' FROM \"{schema}\".sqlite_master + WHERE name LIKE REPLACE(:text, '_', '^_') || '%' ESCAPE '^'""" + for schema in schemata + ) + _completion_matches.extend( + row[0] + for row in cursor.execute( + " UNION ".join(select_clauses), {"text": text} + ) + ) + # columns + try: + select_clauses = ( + f"""\ + SELECT pti.name || ' ' FROM "{schema}".sqlite_master AS sm + JOIN pragma_table_xinfo(sm.name,'{schema}') AS pti + WHERE sm.type='table' AND + pti.name LIKE REPLACE(:text, '_', '^_') || '%' ESCAPE '^'""" + for schema in schemata + ) + _completion_matches.extend( + row[0] + for row in cursor.execute( + " UNION ".join(select_clauses), {"text": text} + ) + ) + except OperationalError: + # skip on SQLite<3.16.0 where pragma table-valued function is + # not supported yet + pass + # functions + try: + _completion_matches.extend( + row[0] for row in cursor.execute("""\ + SELECT DISTINCT UPPER(name) || '(' + FROM pragma_function_list() + WHERE name NOT IN ('->', '->>') AND + name LIKE REPLACE(:text, '_', '^_') || '%' ESCAPE '^'""", + {"text": text}, + ) + ) + except OperationalError: + # skip on SQLite<3.30.0 where function_list is not supported yet + pass + # schemata + text_lower = text.lower() + _completion_matches.extend(c for c in schemata + if c.lower().startswith(text_lower)) + _completion_matches = sorted(set(_completion_matches)) + try: + return _completion_matches[state] + except IndexError: + return None + + +@contextmanager +def completer(con): + try: + import readline + except ImportError: + yield + return + + old_completer = readline.get_completer() + def complete(text, state): + return _complete(con, text, state) + try: + readline.set_completer(complete) + if readline.backend == "editline": + # libedit uses "^I" instead of "tab" + command_string = "bind ^I rl_complete" + else: + command_string = "tab: complete" + readline.parse_and_bind(command_string) + yield + finally: + readline.set_completer(old_completer) diff --git a/Lib/sqlite3/dump.py b/Lib/sqlite3/dump.py index 57e6a3b4f1e..95e14a6655e 100644 --- a/Lib/sqlite3/dump.py +++ b/Lib/sqlite3/dump.py @@ -11,8 +11,12 @@ def _quote_name(name): return '"{0}"'.format(name.replace('"', '""')) +def _escape_single_quotes(value): + return value.replace("'", "''") + + def _quote_value(value): - return "'{0}'".format(value.replace("'", "''")) + return "'{0}'".format(_escape_single_quotes(value)) def _iterdump(connection, *, filter=None): @@ -80,11 +84,12 @@ def _iterdump(connection, *, filter=None): table_name_ident = _quote_name(table_name) res = cu.execute(f'PRAGMA table_info({table_name_ident})') column_names = [str(table_info[1]) for table_info in res.fetchall()] - q = "SELECT 'INSERT INTO {0} VALUES('{1}')' FROM {0};".format( - table_name_ident, + q = "SELECT 'INSERT INTO {0} VALUES('{1}')' FROM {2};".format( + _escape_single_quotes(table_name_ident), "','".join( "||quote({0})||".format(_quote_name(col)) for col in column_names - ) + ), + table_name_ident, ) query_res = cu.execute(q) for row in query_res: diff --git a/Lib/subprocess.py b/Lib/subprocess.py index 52b7b711770..5948cd841bc 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -250,6 +250,82 @@ def __repr__(self): else: _PopenSelector = selectors.SelectSelector + def _communicate_io_posix(selector, stdin, input_view, input_offset, + output_buffers, endtime, *, close_on_eof=False): + """ + Low-level POSIX I/O multiplexing loop used by Popen._communicate. + + Handles the select loop for reading/writing but does not manage + stream lifecycle or raise timeout exceptions. + + Args: + selector: A _PopenSelector with streams already registered + stdin: Writable file object for input, or None + input_view: memoryview of input bytes, or None + input_offset: Starting offset into input_view (for resume support) + output_buffers: Dict {file_object: list} to append read chunks to + endtime: Deadline timestamp, or None for no timeout + close_on_eof: If True, close output streams immediately when they + EOF rather than leaving them open for the caller to close. + Used by Popen._communicate() to match its historical behavior + of releasing fds as soon as the child closes the corresponding + pipe. + + Returns: + (new_input_offset, completed) + - new_input_offset: How many bytes of input were written + - completed: True if all I/O finished, False if timed out + + Note: + - Closes output streams on EOF only if close_on_eof=True + - Does NOT raise TimeoutExpired (caller handles) + - Appends to output_buffers lists in place + """ + stdin_fd = stdin.fileno() if stdin else None + + while selector.get_map(): + remaining = _deadline_remaining(endtime) + if remaining is not None and remaining <= 0: + return (input_offset, False) # Timed out + + ready = selector.select(remaining) + + # Check timeout after select (may have woken spuriously) + if endtime is not None and _time() > endtime: + return (input_offset, False) # Timed out + + for key, events in ready: + if key.fd == stdin_fd: + chunk = input_view[input_offset:input_offset + _PIPE_BUF] + try: + input_offset += os.write(key.fd, chunk) + except BrokenPipeError: + selector.unregister(key.fd) + try: + stdin.close() + except BrokenPipeError: + pass + else: + if input_offset >= len(input_view): + selector.unregister(key.fd) + try: + stdin.close() + except BrokenPipeError: + pass + elif key.fileobj in output_buffers: + data = os.read(key.fd, 32768) + if not data: + selector.unregister(key.fileobj) + if close_on_eof: + try: + key.fileobj.close() + except OSError: + pass + else: + output_buffers[key.fileobj].append(data) + + return (input_offset, True) # Completed + if _mswindows: # On Windows we just need to close `Popen._handle` when we no longer need @@ -289,6 +365,45 @@ def _cleanup(): DEVNULL = -3 +def _deadline_remaining(endtime): + """Calculate remaining time until deadline.""" + if endtime is None: + return None + return endtime - _time() + + +def _flush_stdin(stdin): + """Flush stdin, ignoring BrokenPipeError and closed file ValueError.""" + try: + stdin.flush() + except BrokenPipeError: + pass # communicate() must ignore BrokenPipeError. + except ValueError: + # Ignore ValueError: I/O operation on closed file. + if not stdin.closed: + raise + + +def _make_input_view(input_data): + """Convert input data to a byte memoryview for writing. + + Handles the case where input_data is already a memoryview with + non-byte elements (e.g., int32 array) by casting to a byte view. + This ensures len(view) returns the byte count, not element count. + """ + if not input_data: + return None + if isinstance(input_data, memoryview): + return input_data.cast("b") # ensure byte view for correct len() + return memoryview(input_data) + + +def _translate_newlines(data, encoding, errors): + """Decode bytes to str and translate newlines to \n.""" + data = data.decode(encoding, errors) + return data.replace("\r\n", "\n").replace("\r", "\n") + + # XXX This function is only used by multiprocessing and the test suite, # but it's here so that it can be imported when Python is compiled without # threads. @@ -381,8 +496,7 @@ def _text_encoding(): if sys.flags.utf8_mode: return "utf-8" - else: - return locale.getencoding() + return locale.getencoding() def call(*popenargs, timeout=None, **kwargs): @@ -750,6 +864,66 @@ def _use_posix_spawn(): return False +def _can_use_pidfd_open(): + # Availability: Linux >= 5.3 + if not hasattr(os, "pidfd_open"): + return False + try: + pidfd = os.pidfd_open(os.getpid(), 0) + except OSError as err: + if err.errno in {errno.EMFILE, errno.ENFILE}: + # transitory 'too many open files' + return True + # likely blocked by security policy like SECCOMP (EPERM, + # EACCES, ENOSYS) + return False + else: + os.close(pidfd) + return True + + +def _can_use_kqueue(): + # Availability: macOS, BSD + names = ( + "kqueue", + "KQ_EV_ADD", + "KQ_EV_ONESHOT", + "KQ_FILTER_PROC", + "KQ_NOTE_EXIT", + ) + if not all(hasattr(select, x) for x in names): + return False + kq = None + try: + kq = select.kqueue() + kev = select.kevent( + os.getpid(), + filter=select.KQ_FILTER_PROC, + flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT, + fflags=select.KQ_NOTE_EXIT, + ) + kq.control([kev], 1, 0) + return True + except OSError as err: + if err.errno in {errno.EMFILE, errno.ENFILE}: + # transitory 'too many open files' + return True + return False + finally: + if kq is not None: + kq.close() + + +_CAN_USE_PIDFD_OPEN = not _mswindows and _can_use_pidfd_open() +_CAN_USE_KQUEUE = not _mswindows and _can_use_kqueue() + +# Maximum timeout passed to poll() / kqueue.control() by Popen._wait(): +# very large values (e.g. timeout=float('inf')) overflow the C timestamp +# conversion (gh-154836). Longer waits are performed in bounded slices +# until the deadline, like asyncio's MAXIMUM_SELECT_TIMEOUT. +_MAXIMUM_WAIT_TIMEOUT = 24 * 3600 + + # These are primarily fail-safe knobs for negatives. A True value does not # guarantee the given libc/syscall API will be used. _USE_POSIX_SPAWN = _use_posix_spawn() @@ -1096,8 +1270,8 @@ def universal_newlines(self, universal_newlines): self.text_mode = bool(universal_newlines) def _translate_newlines(self, data, encoding, errors): - data = data.decode(encoding, errors) - return data.replace("\r\n", "\n").replace("\r", "\n") + # Subclass-overridable hook; defers to the module-level helper. + return _translate_newlines(data, encoding, errors) def __enter__(self): return self @@ -1224,7 +1398,7 @@ def communicate(self, input=None, timeout=None): # See the detailed comment in .wait(). if timeout is not None: sigint_timeout = min(self._sigint_wait_secs, - self._remaining_time(endtime)) + _deadline_remaining(endtime)) else: sigint_timeout = self._sigint_wait_secs self._sigint_wait_secs = 0 # nothing else should wait. @@ -1237,7 +1411,7 @@ def communicate(self, input=None, timeout=None): finally: self._communication_started = True try: - sts = self.wait(timeout=self._remaining_time(endtime)) + self.wait(timeout=_deadline_remaining(endtime)) except TimeoutExpired as exc: exc.timeout = timeout raise @@ -1251,14 +1425,6 @@ def poll(self): return self._internal_poll() - def _remaining_time(self, endtime): - """Convenience for _communicate when computing timeouts.""" - if endtime is None: - return None - else: - return endtime - _time() - - def _check_timeout(self, endtime, orig_timeout, stdout_seq, stderr_seq, skip_check_and_raise=False): """Convenience for checking if a timeout has expired.""" @@ -1284,7 +1450,7 @@ def wait(self, timeout=None): # generated SIGINT and will exit rapidly. if timeout is not None: sigint_timeout = min(self._sigint_wait_secs, - self._remaining_time(endtime)) + _deadline_remaining(endtime)) else: sigint_timeout = self._sigint_wait_secs self._sigint_wait_secs = 0 # nothing else should wait. @@ -1651,7 +1817,7 @@ def _communicate(self, input, endtime, orig_timeout): # thread remains writing and the fd left open in case the user # calls communicate again. if hasattr(self, "_stdin_thread"): - self._stdin_thread.join(self._remaining_time(endtime)) + self._stdin_thread.join(_deadline_remaining(endtime)) if self._stdin_thread.is_alive(): raise TimeoutExpired(self.args, orig_timeout) @@ -1659,11 +1825,11 @@ def _communicate(self, input, endtime, orig_timeout): # threads remain reading and the fds left open in case the user # calls communicate again. if self.stdout is not None: - self.stdout_thread.join(self._remaining_time(endtime)) + self.stdout_thread.join(_deadline_remaining(endtime)) if self.stdout_thread.is_alive(): raise TimeoutExpired(self.args, orig_timeout) if self.stderr is not None: - self.stderr_thread.join(self._remaining_time(endtime)) + self.stderr_thread.join(_deadline_remaining(endtime)) if self.stderr_thread.is_alive(): raise TimeoutExpired(self.args, orig_timeout) @@ -2048,14 +2214,121 @@ def _try_wait(self, wait_flags): sts = 0 return (pid, sts) + def _wait_pidfd(self, timeout): + """Wait for PID to terminate using pidfd_open() + poll(). + Linux >= 5.3 only. + """ + if not _CAN_USE_PIDFD_OPEN: + return False + try: + pidfd = os.pidfd_open(self.pid, 0) + except OSError: + # May be: + # - ESRCH: no such process + # - EMFILE, ENFILE: too many open files (usually 1024) + # - ENODEV: anonymous inode filesystem not supported + # - EPERM, EACCES, ENOSYS: undocumented; may happen if + # blocked by security policy like SECCOMP + return False + + try: + poller = select.poll() + poller.register(pidfd, select.POLLIN) + endtime = _time() + timeout + while True: + # Clamp very large timeouts (e.g. float('inf')): + # they overflow the C timestamp conversion in + # poll(). Wait in bounded slices until the real + # deadline (gh-154836). + delay = min(_deadline_remaining(endtime), + _MAXIMUM_WAIT_TIMEOUT) + events = poller.poll(max(delay, 0) * 1000) + if events: + return True + if _deadline_remaining(endtime) <= 0: + raise TimeoutExpired(self.args, timeout) + finally: + os.close(pidfd) + + def _wait_kqueue(self, timeout): + """Wait for PID to terminate using kqueue(). macOS and BSD only.""" + if not _CAN_USE_KQUEUE: + return False + try: + kq = select.kqueue() + except OSError: + # likely EMFILE / ENFILE (too many open files) + return False + + try: + kev = select.kevent( + self.pid, + filter=select.KQ_FILTER_PROC, + flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT, + fflags=select.KQ_NOTE_EXIT, + ) + changelist = [kev] + endtime = _time() + timeout + while True: + # Clamp very large timeouts (e.g. float('inf')): + # they overflow the C timestamp conversion in + # kqueue.control(). Wait in bounded slices until + # the real deadline (gh-154836). + delay = min(_deadline_remaining(endtime), + _MAXIMUM_WAIT_TIMEOUT) + try: + events = kq.control(changelist, 1, max(delay, 0)) + except OSError: + return False + if events: + return True + if _deadline_remaining(endtime) <= 0: + raise TimeoutExpired(self.args, timeout) + # The kevent was registered by the first control() + # call; don't re-add it on later slices. + changelist = None + finally: + kq.close() def _wait(self, timeout): - """Internal implementation of wait() on POSIX.""" + """Internal implementation of wait() on POSIX. + + Uses efficient pidfd_open() + poll() on Linux or kqueue() + on macOS/BSD when available. Falls back to polling + waitpid(WNOHANG) otherwise. + """ if self.returncode is not None: return self.returncode if timeout is not None: - endtime = _time() + timeout + if timeout < 0: + raise TimeoutExpired(self.args, timeout) + started = _time() + endtime = started + timeout + + # Try efficient wait first. + if self._wait_pidfd(timeout) or self._wait_kqueue(timeout): + # Process is gone. At this point os.waitpid(pid, 0) + # will return immediately, but in very rare races + # the PID may have been reused. + # os.waitpid(pid, WNOHANG) ensures we attempt a + # non-blocking reap without blocking indefinitely. + with self._waitpid_lock: + if self.returncode is not None: + return self.returncode # Another thread waited. + (pid, sts) = self._try_wait(os.WNOHANG) + assert pid == self.pid or pid == 0 + if pid == self.pid: + self._handle_exitstatus(sts) + return self.returncode + # os.waitpid(pid, WNOHANG) returned 0 instead + # of our PID, meaning PID has not yet exited, + # even though poll() / kqueue() said so. Very + # rare and mostly theoretical. Fallback to busy + # polling. + elapsed = _time() - started + endtime -= elapsed + # Enter a busy loop if we have a timeout. This busy loop was # cribbed from Lib/threading.py in Thread.wait() at r71065. delay = 0.0005 # 500 us -> initial delay of 1 ms @@ -2071,7 +2344,7 @@ def _wait(self, timeout): break finally: self._waitpid_lock.release() - remaining = self._remaining_time(endtime) + remaining = _deadline_remaining(endtime) if remaining <= 0: raise TimeoutExpired(self.args, timeout) delay = min(delay * 2, remaining, .05) @@ -2087,6 +2360,7 @@ def _wait(self, timeout): # http://bugs.python.org/issue14396. if pid == self.pid: self._handle_exitstatus(sts) + return self.returncode @@ -2094,14 +2368,7 @@ def _communicate(self, input, endtime, orig_timeout): if self.stdin and not self._communication_started: # Flush stdio buffer. This might block, if the user has # been writing to .stdin in an uncontrolled fashion. - try: - self.stdin.flush() - except BrokenPipeError: - pass # communicate() must ignore BrokenPipeError. - except ValueError: - # ignore ValueError: I/O operation on closed file. - if not self.stdin.closed: - raise + _flush_stdin(self.stdin) if not input: try: self.stdin.close() @@ -2126,11 +2393,8 @@ def _communicate(self, input, endtime, orig_timeout): self._save_input(input) - if self._input: - if not isinstance(self._input, memoryview): - input_view = memoryview(self._input) - else: - input_view = self._input.cast("b") # byte input required + input_view = _make_input_view(self._input) + input_offset = self._input_offset if self._input else 0 with _PopenSelector() as selector: if self.stdin and not self.stdin.closed and self._input: @@ -2140,43 +2404,31 @@ def _communicate(self, input, endtime, orig_timeout): if self.stderr and not self.stderr.closed: selector.register(self.stderr, selectors.EVENT_READ) - while selector.get_map(): - timeout = self._remaining_time(endtime) - if timeout is not None and timeout <= 0: - self._check_timeout(endtime, orig_timeout, - stdout, stderr, - skip_check_and_raise=True) - raise RuntimeError( # Impossible :) - '_check_timeout(..., skip_check_and_raise=True) ' - 'failed to raise TimeoutExpired.') - - ready = selector.select(timeout) - self._check_timeout(endtime, orig_timeout, stdout, stderr) - - # XXX Rewrite these to use non-blocking I/O on the file - # objects; they are no longer using C stdio! - - for key, events in ready: - if key.fileobj is self.stdin: - chunk = input_view[self._input_offset : - self._input_offset + _PIPE_BUF] - try: - self._input_offset += os.write(key.fd, chunk) - except BrokenPipeError: - selector.unregister(key.fileobj) - key.fileobj.close() - else: - if self._input_offset >= len(input_view): - selector.unregister(key.fileobj) - key.fileobj.close() - elif key.fileobj in (self.stdout, self.stderr): - data = os.read(key.fd, 32768) - if not data: - selector.unregister(key.fileobj) - key.fileobj.close() - self._fileobj2output[key.fileobj].append(data) + stdin_to_write = (self.stdin if self.stdin and self._input + and not self.stdin.closed else None) + # Persist the returned offset on self so a subsequent + # communicate() after a TimeoutExpired resumes mid-input + # rather than re-sending bytes the child already consumed. + new_offset, completed = _communicate_io_posix( + selector, + stdin_to_write, + input_view, + input_offset, + self._fileobj2output, + endtime, + close_on_eof=True) + if self._input: + self._input_offset = new_offset + + if not completed: + self._check_timeout(endtime, orig_timeout, stdout, stderr, + skip_check_and_raise=True) + raise RuntimeError( # Impossible :) + '_check_timeout(..., skip_check_and_raise=True) ' + 'failed to raise TimeoutExpired.') + try: - self.wait(timeout=self._remaining_time(endtime)) + self.wait(timeout=_deadline_remaining(endtime)) except TimeoutExpired as exc: exc.timeout = orig_timeout raise diff --git a/Lib/symtable.py b/Lib/symtable.py index 7a30e1ac4ca..c7152a70f5a 100644 --- a/Lib/symtable.py +++ b/Lib/symtable.py @@ -17,13 +17,13 @@ __all__ = ["symtable", "SymbolTableType", "SymbolTable", "Class", "Function", "Symbol"] -def symtable(code, filename, compile_type): +def symtable(code, filename, compile_type, *, module=None): """ Return the toplevel *SymbolTable* for the source code. *filename* is the name of the file with the code and *compile_type* is the *compile()* mode argument. """ - top = _symtable.symtable(code, filename, compile_type) + top = _symtable.symtable(code, filename, compile_type, module=module) return _newSymbolTable(top, filename) class SymbolTableFactory: @@ -184,6 +184,7 @@ class Function(SymbolTable): __frees = None __globals = None __nonlocals = None + __cells = None def __idents_matching(self, test_func): return tuple(ident for ident in self.get_identifiers() @@ -229,6 +230,14 @@ def get_frees(self): self.__frees = self.__idents_matching(is_free) return self.__frees + def get_cells(self): + """Return a tuple of cell variables in the function. + """ + if self.__cells is None: + is_cell = lambda x: _get_scope(x) == CELL + self.__cells = self.__idents_matching(is_cell) + return self.__cells + class Class(SymbolTable): @@ -255,11 +264,6 @@ def is_local_symbol(ident): if is_local_symbol(st.name): match st.type: case _symtable.TYPE_FUNCTION: - # generators are of type TYPE_FUNCTION with a ".0" - # parameter as a first parameter (which makes them - # distinguishable from a function named 'genexpr') - if st.name == 'genexpr' and '.0' in st.varnames: - continue d[st.name] = 1 case _symtable.TYPE_TYPE_PARAMETERS: # Get the function-def block in the annotation @@ -267,13 +271,6 @@ def is_local_symbol(ident): scope_name = st.name for c in st.children: if c.name == scope_name and c.type == _symtable.TYPE_FUNCTION: - # A generic generator of type TYPE_FUNCTION - # cannot be a direct child of 'st' (but it - # can be a descendant), e.g.: - # - # class A: - # type genexpr[genexpr] = (x for x in []) - assert scope_name != 'genexpr' or '.0' not in c.varnames d[scope_name] = 1 break self.__methods = tuple(d) @@ -354,6 +351,10 @@ def is_free(self): """ return bool(self.__scope == FREE) + def is_cell(self): + """Return *True* if the symbol is a cell variable.""" + return bool(self.__scope == CELL) + def is_free_class(self): """Return *True* if a class-scoped symbol is free from the perspective of a method.""" @@ -413,7 +414,7 @@ def get_namespace(self): _flags = [('USE', USE)] _flags.extend(kv for kv in globals().items() if kv[0].startswith('DEF_')) _scopes_names = ('FREE', 'LOCAL', 'GLOBAL_IMPLICIT', 'GLOBAL_EXPLICIT', 'CELL') -_scopes_value_to_name = {globals()[n]: n for n in _scopes_names} +_scopes_value_to_name = frozendict({globals()[n]: n for n in _scopes_names}) def main(args): diff --git a/Lib/tempfile.py b/Lib/tempfile.py index 6c036d2fd51..d14c187a275 100644 --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -43,6 +43,7 @@ import shutil as _shutil import errno as _errno from random import Random as _Random +import stat as _stat import sys as _sys import types as _types import weakref as _weakref @@ -273,15 +274,68 @@ def _dont_follow_symlinks(func, path, *args): elif not _os.path.islink(path): func(path, *args) -def _resetperms(path): +def _resetflags(path): try: chflags = _os.chflags except AttributeError: pass else: _dont_follow_symlinks(chflags, path, 0) + +def _resetperms(path): + _resetflags(path) _dont_follow_symlinks(_os.chmod, path, 0o700) +# True if TemporaryDirectory._rmtree() can work relative to open directories +# instead of resolving paths again. +_rmtree_use_dir_fd = ( + {_os.chmod, _os.unlink, _os.lstat} <= _os.supports_dir_fd + and _os.chmod in _os.supports_fd +) + +def _resetperms_fd(dir_fd, path): + # Same as _resetperms(), but for the directory referred to by dir_fd. + if dir_fd is None: + _resetperms(path) + return + _resetflags(path) + _os.chmod(dir_fd, 0o700) + +try: + _nofollow_mode = _os.O_RDONLY | _os.O_NONBLOCK | _os.O_NOFOLLOW +except AttributeError: + _nofollow_mode = None + +def _resetperms_at(name, dir_fd, path): + # Same as _resetperms(), but name is resolved relative to the directory + # file descriptor dir_fd. path is only used for os.chflags(), which + # doesn't support dir_fd or file descriptors. + if dir_fd is None: + _resetperms(path) + return + _resetflags(path) + if _os.chmod in _os.supports_follow_symlinks: + _os.chmod(name, 0o700, dir_fd=dir_fd, follow_symlinks=False) + else: + # dir_fd & follow_symlinks is not supported on this platform. + # Try chmod opening the file with O_NOFOLLOW. + if _nofollow_mode is not None: + try: + fd = _os.open(name, _nofollow_mode, dir_fd=dir_fd) + except OSError: + pass + else: + try: + _os.chmod(fd, 0o700) + finally: + _os.close(fd) + return + # If that did not work, we change by name, which is subject to a race + # condition. + stat = _os.lstat(name, dir_fd=dir_fd) + if not _stat.S_ISLNK(stat.st_mode): + _os.chmod(name, 0o700, dir_fd=dir_fd) + # User visible interfaces. @@ -656,7 +710,7 @@ def TemporaryFile(mode='w+b', buffering=-1, encoding=None, fd = None def opener(*args): nonlocal fd - flags2 = (flags | _os.O_TMPFILE) & ~_os.O_CREAT + flags2 = (flags | _os.O_TMPFILE) & ~_os.O_CREAT & ~_os.O_EXCL fd = _os.open(dir, flags2, 0o600) return fd try: @@ -691,7 +745,7 @@ def opener(*args): fd, name = _mkstemp_inner(dir, prefix, suffix, flags, output_type) try: _os.unlink(name) - except BaseException as e: + except BaseException: _os.close(fd) raise return fd @@ -913,8 +967,12 @@ def __init__(self, suffix=None, prefix=None, dir=None, ignore_errors=self._ignore_cleanup_errors, delete=self._delete) @classmethod - def _rmtree(cls, name, ignore_errors=False, repeated=False): - def onexc(func, path, exc): + def _rmtree(cls, name, ignore_errors=False, repeated=False, dir_fd=None, + fullname=None): + if fullname is None: + fullname = name + + def onexc(func, path, exc, direntry=None, dir_fd=None): # On DragonFly BSD, UF_NOUNLINK removal fails with EISDIR, not EPERM. if isinstance(exc, (PermissionError, IsADirectoryError)): if repeated and path == name: @@ -922,15 +980,30 @@ def onexc(func, path, exc): return raise + # fullpath is path as seen from the working directory + fullpath = fullname + path[len(name):] + # base is path relative to dir_fd, the directory rmtree() + # reached it through, or the whole path when there is none + if dir_fd is None or not _rmtree_use_dir_fd: + base, dir_fd = path, None + elif direntry is None: + base = path + else: + base = direntry.name + try: if path != name: - _resetperms(_os.path.dirname(path)) - _resetperms(path) + # The parent directory of path is the one referred to + # by dir_fd. + _resetperms_fd(dir_fd, _os.path.dirname(fullpath)) + _resetperms_at(base, dir_fd, fullpath) try: - _os.unlink(path) + _os.unlink(base, dir_fd=dir_fd) except IsADirectoryError: - cls._rmtree(path, ignore_errors=ignore_errors) + cls._rmtree(base, ignore_errors=ignore_errors, + repeated=(path == name), + dir_fd=dir_fd, fullname=fullpath) except PermissionError: # The PermissionError handler was originally added for # FreeBSD in directories, but it seems that it is raised @@ -939,21 +1012,27 @@ def onexc(func, path, exc): # raise NotADirectoryError and mask the PermissionError. # So we must re-raise the current PermissionError if # path is not a directory. - if not _os.path.isdir(path) or _os.path.isjunction(path): + if (not _os.path.isdir(fullpath) + or _os.path.isjunction(fullpath)): if ignore_errors: return raise - cls._rmtree(path, ignore_errors=ignore_errors, - repeated=(path == name)) + cls._rmtree(base, ignore_errors=ignore_errors, + repeated=(path == name), + dir_fd=dir_fd, fullname=fullpath) except FileNotFoundError: pass + except OSError: + if ignore_errors: + return + raise elif isinstance(exc, FileNotFoundError): pass else: if not ignore_errors: raise - _shutil.rmtree(name, onexc=onexc) + _shutil.rmtree(name, onexc=onexc, dir_fd=dir_fd, _onexc_kwargs=True) @classmethod def _cleanup(cls, name, warn_message, ignore_errors=False, delete=True): diff --git a/Lib/test/_crossinterp_definitions.py b/Lib/test/_crossinterp_definitions.py new file mode 100644 index 00000000000..5c134427825 --- /dev/null +++ b/Lib/test/_crossinterp_definitions.py @@ -0,0 +1,120 @@ +# This may be loaded as a module, in the current __main__ module, +# or in another __main__ module. + + +####################################### +# functions and generators + +from test._code_definitions import * + + +####################################### +# classes + +class Spam: + # minimal + pass + + +class SpamOkay: + def okay(self): + return True + + +class SpamFull: + + a: object + b: object + c: object + + @staticmethod + def staticmeth(cls): + return True + + @classmethod + def classmeth(cls): + return True + + def __new__(cls, *args, **kwargs): + return super().__new__(cls) + + def __init__(self, a, b, c): + self.a = a + self.b = b + self.c = c + + # __repr__ + # __str__ + # ... + + def __eq__(self, other): + if not isinstance(other, SpamFull): + return NotImplemented + return (self.a == other.a and + self.b == other.b and + self.c == other.c) + + @property + def prop(self): + return True + + +class SubSpamFull(SpamFull): + ... + + +class SubTuple(tuple): + ... + + +def class_eggs_inner(): + class EggsNested: + ... + return EggsNested +EggsNested = class_eggs_inner() + + +TOP_CLASSES = { + Spam: (), + SpamOkay: (), + SpamFull: (1, 2, 3), + SubSpamFull: (1, 2, 3), + SubTuple: ([1, 2, 3],), +} +CLASSES_WITHOUT_EQUALITY = [ + Spam, + SpamOkay, +] +BUILTIN_SUBCLASSES = [ + SubTuple, +] +NESTED_CLASSES = { + EggsNested: (), +} +CLASSES = { + **TOP_CLASSES, + **NESTED_CLASSES, +} + + +####################################### +# exceptions + +class MimimalError(Exception): + pass + + +class RichError(Exception): + def __init__(self, msg, value=None): + super().__init__(msg, value) + self.msg = msg + self.value = value + + def __eq__(self, other): + if not isinstance(other, RichError): + return NotImplemented + if self.msg != other.msg: + return False + if self.value != other.value: + return False + return True diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index f9e48c7c753..90e07c0d3e4 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -40,6 +40,7 @@ from test.support import threading_helper from test.support import warnings_helper from test.support import subTests +from test.support.script_helper import assert_python_failure, assert_python_ok # Skip tests if _multiprocessing wasn't built. _multiprocessing = import_helper.import_module('_multiprocessing') @@ -377,6 +378,7 @@ def test_current(self): self.assertEqual(current.ident, os.getpid()) self.assertEqual(current.exitcode, None) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_set_executable(self): if self.TYPE == 'threads': self.skipTest(f'test not appropriate for {self.TYPE}') @@ -393,6 +395,7 @@ def test_set_executable(self): p.join() self.assertEqual(p.exitcode, 0) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.requires_resource('cpu') def test_args_argument(self): # bpo-45735: Using list or tuple as *args* in constructor could @@ -440,6 +443,7 @@ def _test(cls, q, *args, **kwds): q.put(bytes(current.authkey)) q.put(current.pid) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_parent_process_attributes(self): if self.TYPE == "threads": self.skipTest('test not appropriate for {}'.format(self.TYPE)) @@ -460,6 +464,7 @@ def _test_send_parent_process(cls, wconn): from multiprocessing.process import parent_process wconn.send([parent_process().pid, parent_process().name]) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_parent_process(self): if self.TYPE == "threads": self.skipTest('test not appropriate for {}'.format(self.TYPE)) @@ -498,6 +503,7 @@ def _test_report_parent_status(cls, wconn): parent_process().join(timeout=support.SHORT_TIMEOUT) wconn.send("alive" if parent_process().is_alive() else "not alive") + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_process(self): q = self.Queue(1) e = self.Event() @@ -538,6 +544,7 @@ def test_process(self): self.assertNotIn(p, self.active_children()) close_queue(q) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless(threading._HAVE_THREAD_NATIVE_ID, "needs native_id") def test_process_mainthread_native_id(self): if self.TYPE == 'threads': @@ -578,6 +585,7 @@ def _sleep_no_int_handler(cls, event): def _test_sleep(cls, delay): time.sleep(delay) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def _kill_process(self, meth, target=None): if self.TYPE == 'threads': self.skipTest('test not appropriate for {}'.format(self.TYPE)) @@ -635,6 +643,7 @@ def handler(*args): return p.exitcode + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipIf(os.name == 'nt', "POSIX only") def test_interrupt(self): exitcode = self._kill_process(multiprocessing.Process.interrupt) @@ -643,15 +652,18 @@ def test_interrupt(self): # (KeyboardInterrupt in this case) # in multiprocessing.BaseProcess._bootstrap + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipIf(os.name == 'nt', "POSIX only") def test_interrupt_no_handler(self): exitcode = self._kill_process(multiprocessing.Process.interrupt, target=self._sleep_no_int_handler) self.assertEqual(exitcode, -signal.SIGINT) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_terminate(self): exitcode = self._kill_process(multiprocessing.Process.terminate) self.assertEqual(exitcode, -signal.SIGTERM) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_kill(self): exitcode = self._kill_process(multiprocessing.Process.kill) if os.name != 'nt': @@ -667,6 +679,7 @@ def test_cpu_count(self): self.assertIsInstance(cpus, int) self.assertGreaterEqual(cpus, 1) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_active_children(self): self.assertEqual(type(self.active_children()), list) @@ -695,6 +708,7 @@ def _test_recursion(cls, wconn, id): p.start() p.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_recursion(self): rconn, wconn = self.Pipe(duplex=False) self._test_recursion(wconn, []) @@ -719,6 +733,7 @@ def test_recursion(self): def _test_sentinel(cls, event): event.wait(10.0) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_sentinel(self): if self.TYPE == "threads": self.skipTest('test not appropriate for {}'.format(self.TYPE)) @@ -741,6 +756,7 @@ def _test_close(cls, rc=0, q=None): q.get() sys.exit(rc) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_close(self): if self.TYPE == "threads": self.skipTest('test not appropriate for {}'.format(self.TYPE)) @@ -773,6 +789,7 @@ def test_close(self): close_queue(q) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.requires_resource('walltime') def test_many_processes(self): if self.TYPE == 'threads': @@ -809,6 +826,7 @@ def test_many_processes(self): for p in procs: self.assertIn(p.exitcode, exitcodes) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_lose_target_ref(self): c = DummyCallable() wr = weakref.ref(c) @@ -871,6 +889,7 @@ def func2(): threading.Thread(target=func1).start() threading.Thread(target=func2, daemon=True).start() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_wait_for_threads(self): # A child process should wait for non-daemonic threads to end # before exiting @@ -895,6 +914,7 @@ def _test_error_on_stdio_flush(self, evt, break_std_streams={}): setattr(sys, stream_name, None) evt.set() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_error_on_stdio_flush_1(self): # Check that Process works with broken standard streams streams = [io.StringIO(), None] @@ -914,6 +934,7 @@ def test_error_on_stdio_flush_1(self): finally: setattr(sys, stream_name, old_stream) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_error_on_stdio_flush_2(self): # Same as test_error_on_stdio_flush_1(), but standard streams are # broken by the child process @@ -1064,6 +1085,7 @@ class _TestSubclassingProcess(BaseTestCase): ALLOWED_TYPES = ('processes',) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_subclassing(self): uppercaser = _UpperCaser() uppercaser.daemon = True @@ -1073,6 +1095,7 @@ def test_subclassing(self): uppercaser.stop() uppercaser.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_stderr_flush(self): # sys.stderr is flushed at process shutdown (issue #13812) if self.TYPE == "threads": @@ -1103,6 +1126,7 @@ def _test_sys_exit(cls, reason, testfn): sys.stderr = open(fd, 'w', encoding="utf-8", closefd=False) sys.exit(reason) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_sys_exit(self): # See Issue 13854 if self.TYPE == 'threads': @@ -1170,6 +1194,7 @@ def _test_put(cls, queue, child_can_start, parent_can_continue): queue.get() parent_can_continue.set() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_put(self): MAXSIZE = 6 queue = self.Queue(maxsize=MAXSIZE) @@ -1239,6 +1264,7 @@ def _test_get(cls, queue, child_can_start, parent_can_continue): queue.put(5) parent_can_continue.set() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_get(self): queue = self.Queue() child_can_start = self.Event() @@ -1301,6 +1327,7 @@ def _test_fork(cls, queue): # process cannot shutdown until the feeder thread has finished # pushing items onto the pipe. + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_fork(self): # Old versions of Queue would fail to create a new feeder # thread for a forked process if the original process had its @@ -1351,6 +1378,7 @@ def _test_task_done(cls, q): time.sleep(DELTA) q.task_done() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_task_done(self): queue = self.JoinableQueue() @@ -1394,6 +1422,7 @@ def test_no_import_lock_contention(self): self.fail("Probable regression on import lock contention;" " see Issue #22853") + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_timeout(self): q = multiprocessing.Queue() start = time.monotonic() @@ -1517,6 +1546,7 @@ def _acquire_event(lock, event): event.set() time.sleep(1.0) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_repr_lock(self): if self.TYPE != 'processes': self.skipTest('test not appropriate for {}'.format(self.TYPE)) @@ -1564,6 +1594,7 @@ def test_repr_lock(self): event.wait() self.assertEqual(f'', repr(lock)) p.terminate() + p.join() def test_lock(self): lock = self.Lock() @@ -1580,6 +1611,7 @@ def _test_lock_locked_2processes(cls, lock, event, res): res.value = lock.locked() event.set() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless(HAS_SHAREDCTYPES, 'needs sharedctypes') def test_lock_locked_2processes(self): if self.TYPE != 'processes': @@ -1606,6 +1638,7 @@ def _acquire_release(lock, timeout, l=None, n=1): for _ in range(n): lock.release() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_repr_rlock(self): if self.TYPE != 'processes': self.skipTest('test not appropriate for {}'.format(self.TYPE)) @@ -1665,6 +1698,7 @@ def test_rlock(self): self.assertFalse(lock.locked()) self.assertRaises((AssertionError, RuntimeError), lock.release) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless(HAS_SHAREDCTYPES, 'needs sharedctypes') def test_rlock_locked_2processes(self): if self.TYPE != 'processes': @@ -1776,6 +1810,7 @@ def check_invariant(self, cond): except NotImplementedError: pass + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_notify(self): cond = self.Condition() sleeping = self.Semaphore(0) @@ -1818,6 +1853,7 @@ def test_notify(self): threading_helper.join_thread(t) join_process(p) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_notify_all(self): cond = self.Condition() sleeping = self.Semaphore(0) @@ -1887,6 +1923,7 @@ def test_notify_all(self): # NOTE: join_process and join_thread are the same threading_helper.join_thread(w) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_notify_n(self): cond = self.Condition() sleeping = self.Semaphore(0) @@ -1960,6 +1997,7 @@ def _test_waitfor_f(cls, cond, state): if not result or state.value != 4: sys.exit(1) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless(HAS_SHAREDCTYPES, 'needs sharedctypes') def test_waitfor(self): # based on test in test/lock_tests.py @@ -1995,6 +2033,7 @@ def _test_waitfor_timeout_f(cls, cond, state, success, sem): if not result and (expected - CLOCK_RES) <= dt: success.value = True + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless(HAS_SHAREDCTYPES, 'needs sharedctypes') def test_waitfor_timeout(self): # based on test in test/lock_tests.py @@ -2027,6 +2066,7 @@ def _test_wait_result(cls, c, pid): if pid is not None: os.kill(pid, signal.SIGINT) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_wait_result(self): if isinstance(self, ProcessesMixin) and sys.platform != 'win32': pid = os.getpid() @@ -2055,6 +2095,7 @@ def _test_event(cls, event): time.sleep(TIMEOUT2) event.set() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_event(self): event = self.Event() wait = TimingWrapper(event.wait) @@ -2251,6 +2292,7 @@ def multipass(cls, barrier, results, n): pass assert not barrier.broken + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_barrier(self, passes=1): """ Test that a barrier is passed in lockstep @@ -2258,6 +2300,7 @@ def test_barrier(self, passes=1): results = [self.DummyList(), self.DummyList()] self.run_threads(self.multipass, (self.barrier, results, passes)) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_barrier_10(self): """ Test that a barrier works for 10 consecutive runs @@ -2269,6 +2312,7 @@ def _test_wait_return_f(cls, barrier, queue): res = barrier.wait() queue.put(res) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_wait_return(self): """ test the return value from barrier.wait @@ -2285,6 +2329,7 @@ def _test_action_f(cls, barrier, results): if len(results) != 1: raise RuntimeError + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_action(self): """ Test the 'action' callback @@ -2307,6 +2352,7 @@ def _test_abort_f(cls, barrier, results1, results2): except RuntimeError: barrier.abort() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_abort(self): """ Test that an abort will put the barrier in a broken state @@ -2337,6 +2383,7 @@ def _test_reset_f(cls, barrier, results1, results2, results3): barrier.wait() results3.append(True) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_reset(self): """ Test that a 'reset' on a barrier frees the waiting threads @@ -2372,6 +2419,7 @@ def _test_abort_and_reset_f(cls, barrier, barrier2, barrier.wait() results3.append(True) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_abort_and_reset(self): """ Test that a barrier can be reset after being broken. @@ -2398,6 +2446,7 @@ def _test_timeout_f(cls, barrier, results): except threading.BrokenBarrierError: results.append(True) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_timeout(self): """ Test wait(timeout) @@ -2417,6 +2466,7 @@ def _test_default_timeout_f(cls, barrier, results): except threading.BrokenBarrierError: results.append(True) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_default_timeout(self): """ Test the barrier's default timeout @@ -2438,6 +2488,7 @@ def _test_thousand_f(cls, barrier, passes, conn, lock): with lock: conn.send(i) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_thousand(self): if self.TYPE == 'manager': self.skipTest('test not appropriate for {}'.format(self.TYPE)) @@ -2479,7 +2530,7 @@ def _test(cls, values): for sv, cv in zip(values, cls.codes_values): sv.value = cv[2] - + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_value(self, raw=False): if raw: values = [self.RawValue(code, value) @@ -2527,6 +2578,12 @@ def test_getobj_getlock(self): self.assertNotHasAttr(arr5, 'get_lock') self.assertNotHasAttr(arr5, 'get_obj') + @unittest.skipIf(c_int is None, "requires _ctypes") + def test_invalid_typecode(self): + with self.assertRaisesRegex(TypeError, 'bad typecode'): + self.Value('x', None) + with self.assertRaisesRegex(TypeError, 'bad typecode'): + self.RawValue('x', None) class _TestArray(BaseTestCase): @@ -2537,6 +2594,7 @@ def f(cls, seq): for i in range(1, len(seq)): seq[i] += seq[i-1] + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipIf(c_int is None, "requires _ctypes") def test_array(self, raw=False): seq = [680, 626, 934, 821, 150, 233, 548, 982, 714, 831] @@ -2607,6 +2665,12 @@ def test_getobj_getlock_obj(self): self.assertNotHasAttr(arr5, 'get_lock') self.assertNotHasAttr(arr5, 'get_obj') + @unittest.skipIf(c_int is None, "requires _ctypes") + def test_invalid_typecode(self): + with self.assertRaisesRegex(TypeError, 'bad typecode'): + self.Array('x', []) + with self.assertRaisesRegex(TypeError, 'bad typecode'): + self.RawArray('x', []) # # # @@ -2842,8 +2906,9 @@ class _TestPool(BaseTestCase): @classmethod def setUpClass(cls): - super().setUpClass() - cls.pool = cls.Pool(4) + with warnings_helper.ignore_fork_in_thread_deprecation_warnings(): + super().setUpClass() + cls.pool = cls.Pool(4) @classmethod def tearDownClass(cls): @@ -2942,6 +3007,7 @@ def test_async(self): self.assertEqual(get(), 49) self.assertTimingAlmostEqual(get.elapsed, TIMEOUT1) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_async_timeout(self): p = self.Pool(3) try: @@ -3039,6 +3105,7 @@ def test_imap_unordered_handle_iterable_exception(self): self.assertIn(value, expected_values) expected_values.remove(value) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_make_pool(self): expected_error = (RemoteError if self.TYPE == 'manager' else ValueError) @@ -3054,6 +3121,7 @@ def test_make_pool(self): p.close() p.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_terminate(self): # Simulate slow tasks which take "forever" to complete sleep_time = support.LONG_TIMEOUT @@ -3071,6 +3139,7 @@ def test_terminate(self): p.terminate() p.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_empty_iterable(self): # See Issue 12157 p = self.Pool(1) @@ -3083,6 +3152,7 @@ def test_empty_iterable(self): p.close() p.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_context(self): if self.TYPE == 'processes': L = list(range(10)) @@ -3097,6 +3167,7 @@ def test_context(self): def _test_traceback(cls): raise RuntimeError(123) # some comment + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_traceback(self): # We want ensure that the traceback from the child process is # contained in the traceback raised in the main process. @@ -3136,9 +3207,11 @@ def test_traceback(self): p.join() @classmethod + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def _test_wrapped_exception(cls): raise RuntimeError('foo') + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_wrapped_exception(self): # Issue #20980: Should not wrap exception when using thread pool with self.Pool(1) as p: @@ -3146,6 +3219,7 @@ def test_wrapped_exception(self): p.apply(self._test_wrapped_exception) p.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_map_no_failfast(self): # Issue #23992: the fail-fast behaviour when an exception is raised # during map() would make Pool.join() deadlock, because a worker @@ -3181,6 +3255,7 @@ def test_release_task_refs(self): # they were released too. self.assertEqual(CountedObject.n_instances, 0) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_enter(self): if self.TYPE == 'manager': self.skipTest("test not applicable to manager") @@ -3197,6 +3272,7 @@ def test_enter(self): pass pool.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_resource_warning(self): if self.TYPE == 'manager': self.skipTest("test not applicable to manager") @@ -3222,6 +3298,7 @@ def unpickleable_result(): class _TestPoolWorkerErrors(BaseTestCase): ALLOWED_TYPES = ('processes', ) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_async_error_callback(self): p = multiprocessing.Pool(2) @@ -3237,6 +3314,7 @@ def errback(exc): p.close() p.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_unpickleable_result(self): from multiprocessing.pool import MaybeEncodingError p = multiprocessing.Pool(2) @@ -3262,6 +3340,7 @@ def errback(exc): class _TestPoolWorkerLifetime(BaseTestCase): ALLOWED_TYPES = ('processes', ) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_pool_worker_lifetime(self): p = multiprocessing.Pool(3, maxtasksperchild=10) self.assertEqual(3, len(p._pool)) @@ -3291,6 +3370,7 @@ def test_pool_worker_lifetime(self): p.close() p.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_pool_worker_lifetime_early_close(self): # Issue #10332: closing a pool whose workers have limited lifetimes # before all the tasks completed would make join() hang. @@ -3364,6 +3444,7 @@ class _TestMyManager(BaseTestCase): ALLOWED_TYPES = ('manager',) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.skip_if_sanitizer('TSan: leaks threads', thread=True) def test_mymanager(self): manager = MyManager(shutdown_timeout=SHUTDOWN_TIMEOUT) @@ -3376,6 +3457,7 @@ def test_mymanager(self): # which happens on slow buildbots. self.assertIn(manager._process.exitcode, (0, -signal.SIGTERM)) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.skip_if_sanitizer('TSan: leaks threads', thread=True) def test_mymanager_context(self): manager = MyManager(shutdown_timeout=SHUTDOWN_TIMEOUT) @@ -3386,6 +3468,7 @@ def test_mymanager_context(self): # which happens on slow buildbots. self.assertIn(manager._process.exitcode, (0, -signal.SIGTERM)) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.skip_if_sanitizer('TSan: leaks threads', thread=True) def test_mymanager_context_prestarted(self): manager = MyManager(shutdown_timeout=SHUTDOWN_TIMEOUT) @@ -3457,6 +3540,7 @@ def _putter(cls, address, authkey): # Note that xmlrpclib will deserialize object as a list not a tuple queue.put(tuple(cls.values)) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.skip_if_sanitizer('TSan: leaks threads', thread=True) def test_remote(self): authkey = os.urandom(32) @@ -3499,6 +3583,7 @@ def _putter(cls, address, authkey): queue = manager.get_queue() queue.put('hello world') + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.skip_if_sanitizer("TSan: leaks threads", thread=True) def test_rapid_restart(self): authkey = os.urandom(32) @@ -3552,12 +3637,14 @@ def recv(self): class TestManagerExceptions(unittest.TestCase): # Issue 106558: Manager exceptions avoids creating cyclic references. def setUp(self): - self.mgr = multiprocessing.Manager() + with warnings_helper.ignore_fork_in_thread_deprecation_warnings(): + self.mgr = multiprocessing.Manager() def tearDown(self): self.mgr.shutdown() self.mgr.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_queue_get(self): queue = self.mgr.Queue() if gc.isenabled(): @@ -3569,6 +3656,7 @@ def test_queue_get(self): wr = weakref.ref(e) self.assertEqual(wr(), None) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_dispatch(self): if gc.isenabled(): gc.disable() @@ -3595,6 +3683,7 @@ def _echo(cls, conn): conn.send_bytes(msg) conn.close() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_connection(self): conn, child_conn = self.Pipe() @@ -3687,6 +3776,7 @@ def test_duplex_false(self): self.assertRaises(OSError, writer.recv) self.assertRaises(OSError, writer.poll) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_spawn_close(self): # We test that a pipe connection can be closed by parent # process immediately after child is spawned. On Windows this @@ -3763,6 +3853,7 @@ def _writefd(cls, conn, data, create_dummy_fds=False): os.write(fd, data) os.close(fd) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless(HAS_REDUCTION, "test needs multiprocessing.reduction") def test_fd_transfer(self): if self.TYPE != 'processes': @@ -3782,6 +3873,7 @@ def test_fd_transfer(self): with open(os_helper.TESTFN, "rb") as f: self.assertEqual(f.read(), b"foo") + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless(HAS_REDUCTION, "test needs multiprocessing.reduction") @unittest.skipIf(sys.platform == "win32", "test semantics don't make sense on Windows") @@ -3819,6 +3911,7 @@ def test_large_fd_transfer(self): def _send_data_without_fd(self, conn): os.write(conn.fileno(), b"\0") + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless(HAS_REDUCTION, "test needs multiprocessing.reduction") @unittest.skipIf(sys.platform == "win32", "doesn't make sense on Windows") def test_missing_fd_transfer(self): @@ -3850,6 +3943,19 @@ def test_context(self): self.assertRaises(OSError, a.recv) self.assertRaises(OSError, b.recv) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() + def test_wait_empty(self): + if self.TYPE != 'processes': + self.skipTest('test not appropriate for {}'.format(self.TYPE)) + # gh-145587: wait() with empty list should respect timeout + timeout = 0.5 + start = time.monotonic() + res = self.connection.wait([], timeout=timeout) + duration = time.monotonic() - start + + self.assertEqual(res, []) + self.assertGreaterEqual(duration, timeout - 0.1) + class _TestListener(BaseTestCase): ALLOWED_TYPES = ('processes',) @@ -3918,6 +4024,7 @@ def _test(cls, address): conn.send('hello') conn.close() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_listener_client(self): for family in self.connection.families: l = self.connection.Listener(family=family) @@ -3929,6 +4036,7 @@ def test_listener_client(self): p.join() l.close() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_issue14725(self): l = self.connection.Listener() p = self.Process(target=self._test, args=(l.address,)) @@ -3974,6 +4082,7 @@ def _child_strings(cls, conn, strings): conn.send_bytes(s) conn.close() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_strings(self): strings = (b'hello', b'', b'a', b'b', b'', b'bye', b'', b'lop') a, b = self.Pipe() @@ -3997,6 +4106,7 @@ def _child_boundaries(cls, r): # read from it. r.poll(5) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_boundaries(self): r, w = self.Pipe(False) p = self.Process(target=self._child_boundaries, args=(r,)) @@ -4015,6 +4125,7 @@ def _child_dont_merge(cls, b): b.send_bytes(b'b') b.send_bytes(b'cd') + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_dont_merge(self): a, b = self.Pipe() self.assertEqual(a.poll(0.0), False) @@ -4083,6 +4194,7 @@ def _remote(cls, conn): conn.close() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_pickling(self): families = self.connection.families @@ -4141,6 +4253,7 @@ def child_access(cls, conn): conn.close() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_access(self): # On Windows, if we do not specify a destination pid when # using DupHandle then we need to be careful to use the @@ -4304,6 +4417,7 @@ def _double(cls, x, y, z, foo, arr, string): for i in range(len(arr)): arr[i] *= 2 + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_sharedctypes(self, lock=False): x = Value('i', 7, lock=lock) y = Value(c_double, 1.0/3.0, lock=lock) @@ -4578,6 +4692,7 @@ def test_shared_memory_pickle_unpickle_dead_object(self): with self.assertRaises(FileNotFoundError): pickle.loads(pickled_sms) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_shared_memory_across_processes(self): # bpo-40135: don't define shared memory block's name in case of # the failure when we run multiprocessing tests in parallel. @@ -4606,6 +4721,7 @@ def test_shared_memory_across_processes(self): sms.close() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipIf(os.name != "posix", "not feasible in non-posix platforms") def test_shared_memory_SharedMemoryServer_ignores_sigint(self): # bpo-36368: protect SharedMemoryManager server process from @@ -4652,6 +4768,7 @@ def test_shared_memory_SharedMemoryManager_reuses_resource_tracker(self): # properly released sl. self.assertFalse(err) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_shared_memory_SharedMemoryManager_basics(self): smm1 = multiprocessing.managers.SharedMemoryManager() with self.assertRaises(ValueError): @@ -4993,6 +5110,7 @@ class Foo(object): conn.close() os._exit(0) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_finalize(self): conn, child_conn = self.Pipe() @@ -5120,6 +5238,7 @@ def _test_level(cls, conn): logger = multiprocessing.get_logger() conn.send(logger.getEffectiveLevel()) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_level(self): LEVEL1 = 32 LEVEL2 = 37 @@ -5204,6 +5323,7 @@ def _killer(cls, pid): time.sleep(0.1) os.kill(pid, signal.SIGUSR1) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless(hasattr(signal, 'SIGUSR1'), 'requires SIGUSR1') def test_poll_eintr(self): got_signal = [False] @@ -5343,14 +5463,16 @@ def initializer(ns): @hashlib_helper.requires_hashdigest('sha256') class TestInitializers(unittest.TestCase): def setUp(self): - self.mgr = multiprocessing.Manager() - self.ns = self.mgr.Namespace() - self.ns.test = 0 + with warnings_helper.ignore_fork_in_thread_deprecation_warnings(): + self.mgr = multiprocessing.Manager() + self.ns = self.mgr.Namespace() + self.ns.test = 0 def tearDown(self): self.mgr.shutdown() self.mgr.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_manager_initializer(self): m = multiprocessing.managers.SyncManager() self.assertRaises(TypeError, m.start, 1) @@ -5359,6 +5481,7 @@ def test_manager_initializer(self): m.shutdown() m.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_pool_initializer(self): self.assertRaises(TypeError, multiprocessing.Pool, initializer=1) p = multiprocessing.Pool(1, initializer, (self.ns,)) @@ -5416,16 +5539,19 @@ def flush(self): class TestStdinBadfiledescriptor(unittest.TestCase): + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_queue_in_process(self): proc = multiprocessing.Process(target=_test_process) proc.start() proc.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_pool_in_process(self): p = multiprocessing.Process(target=pool_in_process) p.start() p.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_flushing(self): sio = io.StringIO() flike = _file_like(sio) @@ -5445,6 +5571,7 @@ def _child_test_wait(cls, w, slow): w.send((i, os.getpid())) w.close() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_wait(self, slow=False): from multiprocessing.connection import wait readers = [] @@ -5485,6 +5612,7 @@ def _child_test_wait_socket(cls, address, slow): s.sendall(('%s\n' % i).encode('ascii')) s.close() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_wait_socket(self, slow=False): from multiprocessing.connection import wait l = socket.create_server((socket_helper.HOST, 0)) @@ -5549,6 +5677,7 @@ def signal_and_sleep(cls, sem, period): sem.release() time.sleep(period) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.requires_resource('walltime') def test_wait_integer(self): from multiprocessing.connection import wait @@ -5593,6 +5722,7 @@ def test_wait_integer(self): p.terminate() p.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_neg_timeout(self): from multiprocessing.connection import wait a, b = multiprocessing.Pipe() @@ -5670,6 +5800,7 @@ def _test_timeout(cls, child, address): conn.send(456) conn.close() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_timeout(self): old_timeout = socket.getdefaulttimeout() try: @@ -5727,6 +5858,7 @@ def child(cls, n, conn): conn.send(len(util._afterfork_registry)) conn.close() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_lock(self): r, w = multiprocessing.Pipe(False) l = util.ForkAwareThreadLock() @@ -5778,6 +5910,7 @@ def _test_closefds(cls, conn, fd): s.close() conn.send(None) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_closefd(self): if not HAS_REDUCTION: raise unittest.SkipTest('requires fd pickling') @@ -5823,6 +5956,7 @@ def handler(signum, frame): conn.send(x) conn.send_bytes(b'x' * cls.CONN_MAX_SIZE) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless(hasattr(signal, 'SIGUSR1'), 'requires SIGUSR1') def test_ignore(self): conn, child_conn = multiprocessing.Pipe() @@ -5856,6 +5990,7 @@ def handler(signum, frame): a = l.accept() a.send('welcome') + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless(hasattr(signal, 'SIGUSR1'), 'requires SIGUSR1') def test_ignore_listener(self): conn, child_conn = multiprocessing.Pipe() @@ -5890,6 +6025,7 @@ def check_context(self, ctx): p.join() self.assertEqual(child_method, ctx.get_start_method()) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_context(self): for method in ('fork', 'spawn', 'forkserver'): try: @@ -5906,6 +6042,7 @@ def test_context(self): def _dummy_func(): pass + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_spawn_dont_set_context(self): # Run a process with spawn or forkserver context may change # the global start method, see gh-109263. @@ -5943,6 +6080,7 @@ def test_context_check_module_types(self): with self.assertRaisesRegex(TypeError, 'module_names must be a list of strings'): ctx.set_forkserver_preload([1, 2, 3]) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_set_get(self): multiprocessing.set_forkserver_preload(PRELOAD) count = 0 @@ -6000,6 +6138,7 @@ def test_preload_resources(self): print(err) self.fail("failed spawning forkserver or grandchild") + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipIf(sys.platform == "win32", "Only Spawn on windows so no risk of mixing") @only_run_in_spawn_testsuite("avoids redundant testing.") @@ -6033,6 +6172,7 @@ def _put_two_and_nest_once(cls, queue): process.start() process.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_nested_startmethod(self): # gh-108520: Regression test to ensure that child process can send its # arguments to another process @@ -6189,6 +6329,7 @@ def _is_resource_tracker_reused(conn, pid): reused &= _resource_tracker._check_alive() conn.send(reused) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_resource_tracker_reused(self): from multiprocessing.resource_tracker import _resource_tracker _resource_tracker.ensure_running() @@ -6467,6 +6608,7 @@ def test_empty_exceptions(self): with self.assertRaisesRegex(OSError, 'is closed'): q.empty() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_empty(self): queue = multiprocessing.SimpleQueue() child_can_start = multiprocessing.Event() @@ -6574,7 +6716,8 @@ def test_list(self): def setUp(self): self.manager = self.manager_class() - self.manager.start() + with warnings_helper.ignore_fork_in_thread_deprecation_warnings(): + self.manager.start() self.proc = None def tearDown(self): @@ -6623,6 +6766,7 @@ def _test_event(cls, obj): obj.clear() obj.wait(0.001) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_event(self): o = self.manager.Event() o.set() @@ -6635,6 +6779,7 @@ def _test_lock(cls, obj): obj.acquire() obj.locked() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_lock(self, lname="Lock"): o = getattr(self.manager, lname)() self.run_worker(self._test_lock, o) @@ -6647,6 +6792,7 @@ def _test_rlock(cls, obj): obj.release() obj.locked() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_rlock(self, lname="RLock"): o = getattr(self.manager, lname)() self.run_worker(self._test_rlock, o) @@ -6655,6 +6801,7 @@ def test_rlock(self, lname="RLock"): def _test_semaphore(cls, obj): obj.acquire() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_semaphore(self, sname="Semaphore"): o = getattr(self.manager, sname)() self.run_worker(self._test_semaphore, o) @@ -6668,6 +6815,7 @@ def _test_condition(cls, obj): obj.acquire() obj.release() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_condition(self): o = self.manager.Condition() self.run_worker(self._test_condition, o) @@ -6677,6 +6825,7 @@ def _test_barrier(cls, obj): assert obj.parties == 5 obj.reset() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_barrier(self): o = self.manager.Barrier(5) self.run_worker(self._test_barrier, o) @@ -6687,6 +6836,7 @@ def _test_pool(cls, obj): with obj: pass + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_pool(self): o = self.manager.Pool(processes=4) self.run_worker(self._test_pool, o) @@ -6701,6 +6851,7 @@ def _test_queue(cls, obj): assert obj.get() == 6 assert obj.empty() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_queue(self, qname="Queue"): o = getattr(self.manager, qname)(2) o.put(5) @@ -6709,6 +6860,7 @@ def test_queue(self, qname="Queue"): assert o.empty() assert not o.full() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_joinable_queue(self): self.test_queue("JoinableQueue") @@ -6743,6 +6895,7 @@ def _test_list(cls, obj): obj.clear() case.assertEqual(len(obj), 0) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_list(self): o = self.manager.list() o.append(5) @@ -6784,6 +6937,7 @@ def _test_dict(cls, obj): obj.clear() case.assertEqual(len(obj), 0) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_dict(self): o = self.manager.dict() o['foo'] = 5 @@ -6798,6 +6952,7 @@ def _test_value(cls, obj): case.assertEqual(obj.get(), 1) obj.set(2) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_value(self): o = self.manager.Value('i', 1) self.run_worker(self._test_value, o) @@ -6812,6 +6967,7 @@ def _test_array(cls, obj): case.assertEqual(len(obj), 2) case.assertListEqual(list(obj), [0, 1]) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_array(self): o = self.manager.Array('i', [0, 1]) self.run_worker(self._test_array, o) @@ -6822,6 +6978,7 @@ def _test_namespace(cls, obj): case.assertEqual(obj.x, 0) case.assertEqual(obj.y, 1) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_namespace(self): o = self.manager.Namespace() o.x = 0 @@ -6941,6 +7098,7 @@ def _test_set_comparisons(cls, obj): case.assertGreater(obj, {'a'}) case.assertGreaterEqual(obj, {'a', 'b'}) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_set(self): o = self.manager.set() self.run_worker(self._test_set_operator_symbols, o) @@ -6958,6 +7116,7 @@ def test_set_init(self): self.assertSetEqual(o, {"a", "b", "c"}) self.assertRaises(RemoteError, self.manager.set, 1234) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_set_contain_all_method(self): o = self.manager.set() set_methods = { @@ -7011,6 +7170,7 @@ def exit_handler(): f.write("deadbeef") atexit.register(exit_handler) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_atexit(self): # gh-83856 with os_helper.temp_dir() as temp_dir: @@ -7148,6 +7308,7 @@ def f(x): return x*x self.assertEqual("332833500", out.decode('utf-8').strip()) self.assertFalse(err, msg=err.decode('utf-8')) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_forked_thread_not_started(self): # gh-134381: Ensure that a thread that has not been started yet in # the parent process can be started within a forked child process. @@ -7298,8 +7459,9 @@ def Pool(cls, *args, **kwds): @classmethod def setUpClass(cls): - super().setUpClass() - cls.manager = multiprocessing.Manager() + with warnings_helper.ignore_fork_in_thread_deprecation_warnings(): + super().setUpClass() + cls.manager = multiprocessing.Manager() @classmethod def tearDownClass(cls): @@ -7468,6 +7630,64 @@ class SemLock(_multiprocessing.SemLock): _multiprocessing.sem_unlink(name) +@unittest.skipIf(sys.platform != "linux", "Linux only") +class ForkInThreads(unittest.TestCase): + + def test_fork(self): + code = """ + import os, sys, threading, time + + t = threading.Thread(target=time.sleep, args=(1,), daemon=True) + t.start() + + assert threading.active_count() == 2 + + pid = os.fork() + if pid < 0: + print("Fork failed") + elif pid == 0: + print("In child") + sys.exit(0) + print("In parent") + """ + + res = assert_python_ok("-c", code, PYTHONWARNINGS='always') + self.assertIn(b'In child', res.out) + self.assertIn(b'In parent', res.out) + self.assertIn(b'DeprecationWarning', res.err) + self.assertIn(b'is multi-threaded, use of fork() may lead to deadlocks in the child', res.err) + + res = assert_python_failure("-c", code, PYTHONWARNINGS='error') + self.assertIn(b'DeprecationWarning', res.err) + self.assertIn(b'is multi-threaded, use of fork() may lead to deadlocks in the child', res.err) + + def test_forkpty(self): + code = """ + import os, sys, threading, time + + t = threading.Thread(target=time.sleep, args=(1,), daemon=True) + t.start() + + assert threading.active_count() == 2 + + pid, _ = os.forkpty() + if pid < 0: + print(f"forkpty failed") + elif pid == 0: + print(f"In child") + sys.exit(0) + print(f"In parent") + """ + + res = assert_python_ok("-c", code, PYTHONWARNINGS='always') + self.assertIn(b'In parent', res.out) + self.assertIn(b'DeprecationWarning', res.err) + self.assertIn(b'is multi-threaded, use of forkpty() may lead to deadlocks in the child', res.err) + + res = assert_python_failure("-c", code, PYTHONWARNINGS='error') + self.assertIn(b'DeprecationWarning', res.err) + self.assertIn(b'is multi-threaded, use of forkpty() may lead to deadlocks in the child', res.err) + @unittest.skipUnless(HAS_SHMEM, "requires multiprocessing.shared_memory") class TestSharedMemoryNames(unittest.TestCase): @subTests('use_simple_format', (True, False)) @@ -7511,7 +7731,7 @@ def test_that_shared_memory_name_with_colons_has_no_resource_tracker_errors( print("SUCCESS") """ % use_simple_format) - rc, out, err = script_helper.assert_python_ok("-c", test_script) + rc, out, err = assert_python_ok("-c", test_script) self.assertIn(b"SUCCESS", out) self.assertNotIn(b"traceback", err.lower(), err) self.assertNotIn(b"resource_tracker.py", err, err) diff --git a/Lib/test/bisect_cmd.py b/Lib/test/bisect_cmd.py new file mode 100755 index 00000000000..aee2e8ac120 --- /dev/null +++ b/Lib/test/bisect_cmd.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +""" +Command line tool to bisect failing CPython tests. + +Find the test_os test method which alters the environment: + + ./python -m test.bisect_cmd --fail-env-changed test_os + +Find a reference leak in "test_os", write the list of failing tests into the +"bisect" file: + + ./python -m test.bisect_cmd -o bisect -R 3:3 test_os + +Load an existing list of tests from a file using -i option: + + ./python -m test --list-cases -m FileTests test_os > tests + ./python -m test.bisect_cmd -i tests test_os +""" + +import argparse +import datetime +import os.path +import math +import random +import subprocess +import sys +import tempfile +import time + + +def write_tests(filename, tests): + with open(filename, "w") as fp: + for name in tests: + print(name, file=fp) + fp.flush() + + +def write_output(filename, tests): + if not filename: + return + print("Writing %s tests into %s" % (len(tests), filename)) + write_tests(filename, tests) + return filename + + +def format_shell_args(args): + return ' '.join(args) + + +def python_cmd(): + cmd = [sys.executable] + cmd.extend(subprocess._args_from_interpreter_flags()) + cmd.extend(subprocess._optim_args_from_interpreter_flags()) + cmd.extend(('-X', 'faulthandler')) + return cmd + + +def list_cases(args): + cmd = python_cmd() + cmd.extend(['-m', 'test', '--list-cases']) + cmd.extend(args.test_args) + proc = subprocess.run(cmd, + stdout=subprocess.PIPE, + universal_newlines=True) + exitcode = proc.returncode + if exitcode: + cmd = format_shell_args(cmd) + print("Failed to list tests: %s failed with exit code %s" + % (cmd, exitcode)) + sys.exit(exitcode) + tests = proc.stdout.splitlines() + return tests + + +def run_tests(args, tests, huntrleaks=None): + tmp = tempfile.mktemp() + try: + write_tests(tmp, tests) + + cmd = python_cmd() + cmd.extend(['-u', '-m', 'test', '--matchfile', tmp]) + cmd.extend(args.test_args) + print("+ %s" % format_shell_args(cmd)) + + sys.stdout.flush() + sys.stderr.flush() + + proc = subprocess.run(cmd) + return proc.returncode + finally: + if os.path.exists(tmp): + os.unlink(tmp) + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('-i', '--input', + help='Test names produced by --list-tests written ' + 'into a file. If not set, run --list-tests') + parser.add_argument('-o', '--output', + help='Result of the bisection') + parser.add_argument('-n', '--max-tests', type=int, default=1, + help='Maximum number of tests to stop the bisection ' + '(default: 1)') + parser.add_argument('-N', '--max-iter', type=int, default=100, + help='Maximum number of bisection iterations ' + '(default: 100)') + # FIXME: document that following arguments are test arguments + + args, test_args = parser.parse_known_args() + args.test_args = test_args + return args + + +def main(): + args = parse_args() + for opt in ('-w', '--rerun', '--verbose2'): + if opt in args.test_args: + print(f"WARNING: {opt} option should not be used to bisect!") + print() + + if args.input: + with open(args.input) as fp: + tests = [line.strip() for line in fp] + else: + tests = list_cases(args) + + print("Start bisection with %s tests" % len(tests)) + print("Test arguments: %s" % format_shell_args(args.test_args)) + print("Bisection will stop when getting %s or less tests " + "(-n/--max-tests option), or after %s iterations " + "(-N/--max-iter option)" + % (args.max_tests, args.max_iter)) + output = write_output(args.output, tests) + print() + + start_time = time.monotonic() + iteration = 1 + try: + while len(tests) > args.max_tests and iteration <= args.max_iter: + ntest = len(tests) + ntest = max(ntest // 2, 1) + subtests = random.sample(tests, ntest) + + print(f"[+] Iteration {iteration}/{args.max_iter}: " + f"run {len(subtests)} tests/{len(tests)}") + print() + + exitcode = run_tests(args, subtests) + + print("ran %s tests/%s" % (ntest, len(tests))) + print("exit", exitcode) + if exitcode: + print("Tests failed: continuing with this subtest") + tests = subtests + output = write_output(args.output, tests) + else: + print("Tests succeeded: skipping this subtest, trying a new subset") + print() + iteration += 1 + except KeyboardInterrupt: + print() + print("Bisection interrupted!") + print() + + print("Tests (%s):" % len(tests)) + for test in tests: + print("* %s" % test) + print() + + if output: + print("Output written into %s" % output) + + dt = math.ceil(time.monotonic() - start_time) + if len(tests) <= args.max_tests: + print("Bisection completed in %s iterations and %s" + % (iteration, datetime.timedelta(seconds=dt))) + else: + print("Bisection failed after %s iterations and %s" + % (iteration, datetime.timedelta(seconds=dt))) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/Lib/test/cov.py b/Lib/test/cov.py new file mode 100644 index 00000000000..8717b1f20dd --- /dev/null +++ b/Lib/test/cov.py @@ -0,0 +1,48 @@ +"""A minimal hook for gathering line coverage of the standard library. + +Designed to be used with -Xpresite=test.cov:enable which means: + +* it can't import anything besides `sys` to avoid tainting gathered coverage +* filenames are not normalized + +To get gathered coverage back, look for 'test.cov' in `sys.modules` +instead of importing directly. That way you can determine if the module +was already in use. + +If you need to disable the hook, call the `disable()` function. +""" + +import sys + +mon = sys.monitoring + +FileName = str +LineNo = int +Location = tuple[FileName, LineNo] + +coverage: set[Location] = set() + + +# `types` and `typing` aren't imported to avoid invalid coverage +def add_line( + code: "types.CodeType", + lineno: int, +) -> "typing.Literal[sys.monitoring.DISABLE]": + coverage.add((code.co_filename, lineno)) + return mon.DISABLE + + +def enable(): + mon.use_tool_id(mon.COVERAGE_ID, "regrtest coverage") + mon.register_callback(mon.COVERAGE_ID, mon.events.LINE, add_line) + mon.set_events(mon.COVERAGE_ID, mon.events.LINE) + + +def disable(): + mon.set_events(mon.COVERAGE_ID, 0) + mon.register_callback(mon.COVERAGE_ID, mon.events.LINE, None) + mon.free_tool_id(mon.COVERAGE_ID) + + +if __name__ == "__main__": + enable() diff --git a/Lib/test/curses_tests.py b/Lib/test/curses_tests.py new file mode 100755 index 00000000000..40e832dec25 --- /dev/null +++ b/Lib/test/curses_tests.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# +# $Id: ncurses.py 36559 2004-07-18 05:56:09Z tim_one $ +# +# Interactive test suite for the curses module. +# This script displays various things and the user should verify whether +# they display correctly. +# + +import curses +from curses import textpad + +def test_textpad(stdscr, insert_mode=False): + ncols, nlines = 8, 3 + uly, ulx = 3, 2 + if insert_mode: + mode = 'insert mode' + else: + mode = 'overwrite mode' + + stdscr.addstr(uly-3, ulx, "Use Ctrl-G to end editing (%s)." % mode) + stdscr.addstr(uly-2, ulx, "Be sure to try typing in the lower-right corner.") + win = curses.newwin(nlines, ncols, uly, ulx) + textpad.rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols) + stdscr.refresh() + + box = textpad.Textbox(win, insert_mode) + contents = box.edit() + stdscr.addstr(uly+ncols+2, 0, "Text entered in the box\n") + stdscr.addstr(repr(contents)) + stdscr.addstr('\n') + stdscr.addstr('Press any key') + stdscr.getch() + + for i in range(3): + stdscr.move(uly+ncols+2 + i, 0) + stdscr.clrtoeol() + +def main(stdscr): + stdscr.clear() + test_textpad(stdscr, False) + test_textpad(stdscr, True) + + +if __name__ == '__main__': + curses.wrapper(main) diff --git a/Lib/test/dtracedata/call_stack.stp b/Lib/test/dtracedata/call_stack.stp index 54082c202f6..2901f44654b 100644 --- a/Lib/test/dtracedata/call_stack.stp +++ b/Lib/test/dtracedata/call_stack.stp @@ -10,32 +10,21 @@ function basename:string(path:string) return last_token; } -probe process.mark("function__entry") -{ - funcname = user_string($arg2); - - if (funcname == "start") { - tracing = 1; - } -} - -probe process.mark("function__entry"), process.mark("function__return") +probe @PYTHON_SYSTEMTAP_PROBE@("function__entry"), + @PYTHON_SYSTEMTAP_PROBE@("function__return") { filename = user_string($arg1); funcname = user_string($arg2); lineno = $arg3; - if (tracing) { + if (funcname == "start") { + if ($$name == "function__entry") { + tracing = 1; + } else { + tracing = 0; + } + } else if (tracing) { printf("%d\t%s:%s:%s:%d\n", gettimeofday_us(), $$name, basename(filename), funcname, lineno); } } - -probe process.mark("function__return") -{ - funcname = user_string($arg2); - - if (funcname == "start") { - tracing = 0; - } -} diff --git a/Lib/test/levenshtein_examples.json b/Lib/test/levenshtein_examples.json new file mode 100644 index 00000000000..a32672cfdba --- /dev/null +++ b/Lib/test/levenshtein_examples.json @@ -0,0 +1,50002 @@ +[ + [ + "", + "", + 0 + ], + [ + "", + "AabBbb", + 12 + ], + [ + "", + "AbaC", + 8 + ], + [ + "", + "B", + 2 + ], + [ + "", + "Ba", + 4 + ], + [ + "", + "CBaACCCa", + 16 + ], + [ + "", + "CCbBC", + 10 + ], + [ + "", + "CcbCbaaAB", + 18 + ], + [ + "", + "bAa", + 6 + ], + [ + "", + "bAaCABb", + 14 + ], + [ + "A", + "A", + 0 + ], + [ + "A", + "AA", + 2 + ], + [ + "A", + "AABCAabcC", + 16 + ], + [ + "A", + "AACB", + 6 + ], + [ + "A", + "AACC", + 6 + ], + [ + "A", + "ABAaBbc", + 12 + ], + [ + "A", + "ABCcC", + 8 + ], + [ + "A", + "ABa", + 4 + ], + [ + "A", + "ABbBabcaa", + 16 + ], + [ + "A", + "ABbbCBcA", + 14 + ], + [ + "A", + "ACCBcBbBb", + 16 + ], + [ + "A", + "ACa", + 4 + ], + [ + "A", + "ACb", + 4 + ], + [ + "A", + "ACcbbaAB", + 14 + ], + [ + "A", + "AaAaccAb", + 14 + ], + [ + "A", + "AaabbCC", + 12 + ], + [ + "A", + "AabB", + 6 + ], + [ + "A", + "AbBa", + 6 + ], + [ + "A", + "AbBaA", + 8 + ], + [ + "A", + "AbCBBCaC", + 14 + ], + [ + "A", + "AbCb", + 6 + ], + [ + "A", + "AbcB", + 6 + ], + [ + "A", + "B", + 2 + ], + [ + "A", + "BA", + 2 + ], + [ + "A", + "BABbCc", + 10 + ], + [ + "A", + "BABca", + 8 + ], + [ + "A", + "BB", + 4 + ], + [ + "A", + "BBaBBaa", + 13 + ], + [ + "A", + "BBbbaAA", + 12 + ], + [ + "A", + "BCCAAc", + 10 + ], + [ + "A", + "BCa", + 5 + ], + [ + "A", + "BCcaC", + 9 + ], + [ + "A", + "BCccaa", + 11 + ], + [ + "A", + "BaacCbC", + 13 + ], + [ + "A", + "BabCC", + 9 + ], + [ + "A", + "Bac", + 5 + ], + [ + "A", + "BbCbb", + 10 + ], + [ + "A", + "BbaBAC", + 10 + ], + [ + "A", + "Bbc", + 6 + ], + [ + "A", + "BcB", + 6 + ], + [ + "A", + "BcCAbCBA", + 14 + ], + [ + "A", + "BcaaCbCB", + 15 + ], + [ + "A", + "C", + 2 + ], + [ + "A", + "CA", + 2 + ], + [ + "A", + "CABAacB", + 12 + ], + [ + "A", + "CABBABBc", + 14 + ], + [ + "A", + "CAaaBc", + 10 + ], + [ + "A", + "CBAA", + 6 + ], + [ + "A", + "CBCcBaBAB", + 16 + ], + [ + "A", + "CBcbc", + 10 + ], + [ + "A", + "CC", + 4 + ], + [ + "A", + "CCA", + 4 + ], + [ + "A", + "CCB", + 6 + ], + [ + "A", + "CCBaACcC", + 14 + ], + [ + "A", + "CCBcAAacb", + 16 + ], + [ + "A", + "CaCCaA", + 10 + ], + [ + "A", + "CbAB", + 6 + ], + [ + "A", + "CbBaAACC", + 14 + ], + [ + "A", + "CbCCbB", + 12 + ], + [ + "A", + "CbacBb", + 11 + ], + [ + "A", + "CbbABc", + 10 + ], + [ + "A", + "CcC", + 6 + ], + [ + "A", + "CcCacAAAC", + 16 + ], + [ + "A", + "CccCbaCcb", + 17 + ], + [ + "A", + "a", + 1 + ], + [ + "A", + "aABABBa", + 12 + ], + [ + "A", + "aABBA", + 8 + ], + [ + "A", + "aABCaBCa", + 14 + ], + [ + "A", + "aACBabAC", + 14 + ], + [ + "A", + "aBCb", + 7 + ], + [ + "A", + "aBaB", + 7 + ], + [ + "A", + "aBbBb", + 9 + ], + [ + "A", + "aC", + 3 + ], + [ + "A", + "aCA", + 4 + ], + [ + "A", + "aaBBb", + 9 + ], + [ + "A", + "aaC", + 5 + ], + [ + "A", + "aaCb", + 7 + ], + [ + "A", + "aaCbABbb", + 14 + ], + [ + "A", + "aaCcbb", + 11 + ], + [ + "A", + "aaaaCcAB", + 14 + ], + [ + "A", + "aaabb", + 9 + ], + [ + "A", + "aaacCabCC", + 17 + ], + [ + "A", + "aacBccCAC", + 16 + ], + [ + "A", + "aacC", + 7 + ], + [ + "A", + "abCAb", + 8 + ], + [ + "A", + "abbBcaccc", + 17 + ], + [ + "A", + "acCAB", + 8 + ], + [ + "A", + "acacBcAA", + 14 + ], + [ + "A", + "b", + 2 + ], + [ + "A", + "bABACaB", + 12 + ], + [ + "A", + "bAbA", + 6 + ], + [ + "A", + "bAbaBc", + 10 + ], + [ + "A", + "bBAa", + 6 + ], + [ + "A", + "bBa", + 5 + ], + [ + "A", + "bBaCAab", + 12 + ], + [ + "A", + "bBaCaCBbB", + 17 + ], + [ + "A", + "bBbAbaBa", + 14 + ], + [ + "A", + "bBbacabAb", + 16 + ], + [ + "A", + "bCCbA", + 8 + ], + [ + "A", + "bCCbB", + 10 + ], + [ + "A", + "bCbbACc", + 12 + ], + [ + "A", + "bCbbc", + 10 + ], + [ + "A", + "baACBCB", + 12 + ], + [ + "A", + "babABb", + 10 + ], + [ + "A", + "bacB", + 7 + ], + [ + "A", + "bacacA", + 10 + ], + [ + "A", + "bb", + 4 + ], + [ + "A", + "bbCa", + 7 + ], + [ + "A", + "bbCbaa", + 11 + ], + [ + "A", + "bbbAcb", + 10 + ], + [ + "A", + "bc", + 4 + ], + [ + "A", + "bcAAcCAb", + 14 + ], + [ + "A", + "bcC", + 6 + ], + [ + "A", + "bcbbaab", + 13 + ], + [ + "A", + "c", + 2 + ], + [ + "A", + "cAB", + 4 + ], + [ + "A", + "cACAc", + 8 + ], + [ + "A", + "cAaBccaC", + 14 + ], + [ + "A", + "cAcAaCAc", + 14 + ], + [ + "A", + "cBAaC", + 8 + ], + [ + "A", + "cBAac", + 8 + ], + [ + "A", + "cBCAABbc", + 14 + ], + [ + "A", + "cBaCbab", + 13 + ], + [ + "A", + "cBacA", + 8 + ], + [ + "A", + "cBb", + 6 + ], + [ + "A", + "cC", + 4 + ], + [ + "A", + "cCBABaC", + 12 + ], + [ + "A", + "cCCCa", + 9 + ], + [ + "A", + "cCa", + 5 + ], + [ + "A", + "cCaAaCaCB", + 16 + ], + [ + "A", + "cCabbbCa", + 15 + ], + [ + "A", + "caacCacAb", + 16 + ], + [ + "A", + "cabbC", + 9 + ], + [ + "A", + "cb", + 4 + ], + [ + "A", + "cbC", + 6 + ], + [ + "A", + "cbCAaAaAB", + 16 + ], + [ + "A", + "cbbaaAbbB", + 16 + ], + [ + "A", + "cc", + 4 + ], + [ + "A", + "ccCcCBaB", + 15 + ], + [ + "A", + "ccabbC", + 11 + ], + [ + "AA", + "AA", + 0 + ], + [ + "AA", + "ACAA", + 4 + ], + [ + "AA", + "BAc", + 4 + ], + [ + "AA", + "BC", + 4 + ], + [ + "AA", + "BaccaAA", + 10 + ], + [ + "AA", + "Bba", + 5 + ], + [ + "AA", + "BbcbcccAB", + 16 + ], + [ + "AA", + "CAb", + 4 + ], + [ + "AA", + "Cc", + 4 + ], + [ + "AA", + "CcBACCa", + 11 + ], + [ + "AA", + "aCBa", + 6 + ], + [ + "AA", + "aaBA", + 5 + ], + [ + "AA", + "aaBac", + 8 + ], + [ + "AA", + "bAAabBA", + 10 + ], + [ + "AA", + "bABbacbb", + 13 + ], + [ + "AA", + "bBbBbc", + 12 + ], + [ + "AA", + "bC", + 4 + ], + [ + "AA", + "bCabb", + 9 + ], + [ + "AA", + "baAB", + 5 + ], + [ + "AA", + "bbBaBcCBB", + 17 + ], + [ + "AA", + "bbBbAaaB", + 13 + ], + [ + "AA", + "c", + 4 + ], + [ + "AA", + "cACaCbab", + 13 + ], + [ + "AA", + "cBbCbBcA", + 14 + ], + [ + "AA", + "cBba", + 7 + ], + [ + "AA", + "cCbBBCca", + 15 + ], + [ + "AA", + "cacaca", + 10 + ], + [ + "AA", + "ccbccAa", + 11 + ], + [ + "AAA", + "B", + 6 + ], + [ + "AAA", + "C", + 6 + ], + [ + "AAA", + "aBBbcAaBa", + 14 + ], + [ + "AAA", + "aacbbBbbc", + 16 + ], + [ + "AAA", + "abAb", + 5 + ], + [ + "AAA", + "cccaAbBcB", + 15 + ], + [ + "AAAAABA", + "CaBcAaa", + 10 + ], + [ + "AAABACbC", + "BBBC", + 11 + ], + [ + "AAABAbaaA", + "CBbbacb", + 12 + ], + [ + "AAABa", + "AA", + 6 + ], + [ + "AAABc", + "cbBabcaBA", + 14 + ], + [ + "AAAC", + "acBcCCcC", + 13 + ], + [ + "AAACAbAaa", + "BCc", + 16 + ], + [ + "AAACBACc", + "CaCabAc", + 10 + ], + [ + "AAACCbbcA", + "bc", + 14 + ], + [ + "AAACa", + "A", + 8 + ], + [ + "AAACbCaa", + "acABAAa", + 9 + ], + [ + "AAACbaaa", + "BBa", + 13 + ], + [ + "AAAa", + "bBaaCc", + 9 + ], + [ + "AAAaAB", + "BCB", + 10 + ], + [ + "AAAaAcC", + "CAbBcBBC", + 12 + ], + [ + "AAAaCAAb", + "aBbAbaaA", + 12 + ], + [ + "AAAaCaA", + "cBcABaAb", + 11 + ], + [ + "AAAaaaaA", + "ABBBcaA", + 10 + ], + [ + "AAAb", + "AcC", + 6 + ], + [ + "AAAb", + "B", + 7 + ], + [ + "AAAbAB", + "cCBaAbcc", + 11 + ], + [ + "AAAbBcbc", + "C", + 15 + ], + [ + "AAAbaAABb", + "cCcAaca", + 15 + ], + [ + "AAAbbaA", + "Cbcc", + 12 + ], + [ + "AAAbbc", + "ABcaCCbca", + 11 + ], + [ + "AAAbcAcBA", + "bBaACAC", + 13 + ], + [ + "AAAbcbCa", + "cabCCbCba", + 10 + ], + [ + "AAAcAcCC", + "cbBCAAB", + 13 + ], + [ + "AAAcBAbCB", + "cbbABC", + 12 + ], + [ + "AAB", + "CB", + 4 + ], + [ + "AAB", + "accAC", + 7 + ], + [ + "AAB", + "bCCbaBbAc", + 15 + ], + [ + "AAB", + "bbb", + 5 + ], + [ + "AAB", + "bcCBcC", + 10 + ], + [ + "AABA", + "AAc", + 4 + ], + [ + "AABA", + "cBCAbA", + 7 + ], + [ + "AABABCcCc", + "abAC", + 12 + ], + [ + "AABAaB", + "BAB", + 6 + ], + [ + "AABAbAa", + "BcaB", + 11 + ], + [ + "AABAcb", + "cca", + 10 + ], + [ + "AABBA", + "AbAaCBb", + 8 + ], + [ + "AABBAACcC", + "A", + 16 + ], + [ + "AABBBcb", + "B", + 12 + ], + [ + "AABBCaB", + "bAbBA", + 8 + ], + [ + "AABBCabC", + "BCC", + 10 + ], + [ + "AABBCcB", + "cABBBAACc", + 10 + ], + [ + "AABBabAA", + "A", + 14 + ], + [ + "AABBac", + "BACCCB", + 10 + ], + [ + "AABBc", + "CAaBaA", + 7 + ], + [ + "AABCb", + "CcaCab", + 8 + ], + [ + "AABCbccb", + "B", + 14 + ], + [ + "AABCcA", + "BabC", + 8 + ], + [ + "AABCcB", + "BAB", + 8 + ], + [ + "AABCca", + "AbBBC", + 7 + ], + [ + "AABCcbBB", + "Bab", + 12 + ], + [ + "AABa", + "ACCcBA", + 7 + ], + [ + "AABa", + "BCcbaAa", + 11 + ], + [ + "AABa", + "C", + 8 + ], + [ + "AABaAc", + "Ccaa", + 9 + ], + [ + "AABaC", + "aA", + 7 + ], + [ + "AABaCccb", + "AABaAAbc", + 8 + ], + [ + "AABabcc", + "ccaABC", + 11 + ], + [ + "AABacB", + "cCbaa", + 9 + ], + [ + "AABacBac", + "AcAa", + 10 + ], + [ + "AABaccb", + "CcbBbbc", + 12 + ], + [ + "AABbBa", + "bBAcbaaB", + 10 + ], + [ + "AABbCc", + "AccbaB", + 8 + ], + [ + "AABba", + "cBbAc", + 7 + ], + [ + "AABbaBa", + "BBaAbb", + 10 + ], + [ + "AABc", + "BccAAA", + 10 + ], + [ + "AABcABaaA", + "cbB", + 14 + ], + [ + "AABcAC", + "acabAC", + 7 + ], + [ + "AABcBbcb", + "baAACAbAB", + 11 + ], + [ + "AABcCB", + "ca", + 10 + ], + [ + "AABcaBC", + "AaAaab", + 8 + ], + [ + "AAC", + "CBabAaB", + 11 + ], + [ + "AAC", + "aBA", + 5 + ], + [ + "AAC", + "aCBCBbac", + 13 + ], + [ + "AAC", + "aCa", + 5 + ], + [ + "AAC", + "baaBAba", + 11 + ], + [ + "AAC", + "bbAbc", + 7 + ], + [ + "AACA", + "aaCB", + 4 + ], + [ + "AACA", + "ab", + 7 + ], + [ + "AACA", + "cc", + 7 + ], + [ + "AACAba", + "CBAAaAB", + 9 + ], + [ + "AACAba", + "b", + 10 + ], + [ + "AACAbbc", + "AbaCABCcB", + 8 + ], + [ + "AACBACbcB", + "b", + 16 + ], + [ + "AACBC", + "Abbc", + 6 + ], + [ + "AACBC", + "aaCcAb", + 8 + ], + [ + "AACBCA", + "A", + 10 + ], + [ + "AACBCcbaa", + "cbBca", + 12 + ], + [ + "AACBaB", + "AAcABBA", + 7 + ], + [ + "AACBc", + "aABCaACA", + 10 + ], + [ + "AACC", + "bB", + 8 + ], + [ + "AACCA", + "b", + 10 + ], + [ + "AACCAC", + "Ccab", + 8 + ], + [ + "AACCBBAac", + "abbabb", + 14 + ], + [ + "AACCCCbc", + "BBCCaB", + 11 + ], + [ + "AACCCc", + "bcCbaBACA", + 14 + ], + [ + "AACCaBcA", + "BaC", + 13 + ], + [ + "AACCaca", + "ccbaBBaB", + 13 + ], + [ + "AACCc", + "b", + 10 + ], + [ + "AACa", + "BAC", + 4 + ], + [ + "AACaAaBA", + "AbcbB", + 11 + ], + [ + "AACaac", + "C", + 10 + ], + [ + "AACacB", + "CcbCBaBb", + 11 + ], + [ + "AACb", + "AC", + 4 + ], + [ + "AACb", + "ac", + 6 + ], + [ + "AACbAC", + "BBCCBCa", + 10 + ], + [ + "AACbC", + "bb", + 8 + ], + [ + "AACbbcaa", + "bbaaBCBb", + 14 + ], + [ + "AACbc", + "AbcbbBcCB", + 11 + ], + [ + "AACbc", + "aCCB", + 6 + ], + [ + "AACbcAaAb", + "BABcaBBAA", + 12 + ], + [ + "AACbcb", + "cabBc", + 8 + ], + [ + "AACc", + "BbCC", + 5 + ], + [ + "AACc", + "bbb", + 8 + ], + [ + "AACcAABAC", + "CBAcBaABa", + 11 + ], + [ + "AACcAb", + "CBCbCACcb", + 11 + ], + [ + "AACcCAAc", + "AbAACbCC", + 11 + ], + [ + "AACcabbC", + "cAAccbAB", + 9 + ], + [ + "AACcbA", + "BCCCcaCc", + 12 + ], + [ + "AAa", + "ACCC", + 6 + ], + [ + "AAa", + "BcAC", + 6 + ], + [ + "AAa", + "CACcaAAAb", + 13 + ], + [ + "AAa", + "CAaCA", + 6 + ], + [ + "AAa", + "aCcBACb", + 11 + ], + [ + "AAa", + "abACAA", + 7 + ], + [ + "AAa", + "b", + 6 + ], + [ + "AAa", + "cAAaBA", + 6 + ], + [ + "AAa", + "cBaB", + 6 + ], + [ + "AAaA", + "ca", + 6 + ], + [ + "AAaACcb", + "ABCcb", + 6 + ], + [ + "AAaB", + "aCAC", + 6 + ], + [ + "AAaB", + "cbAb", + 6 + ], + [ + "AAaBA", + "bBAc", + 8 + ], + [ + "AAaBB", + "ABCCaCBb", + 9 + ], + [ + "AAaBa", + "AcB", + 6 + ], + [ + "AAaBaB", + "aCcb", + 9 + ], + [ + "AAaBacBaC", + "CacABc", + 12 + ], + [ + "AAaBbAb", + "cC", + 14 + ], + [ + "AAaBbcaa", + "caB", + 12 + ], + [ + "AAaC", + "aCbBcBCB", + 13 + ], + [ + "AAaCACAB", + "cBa", + 14 + ], + [ + "AAaCCaCaA", + "baACABCCA", + 10 + ], + [ + "AAaCCcbb", + "aa", + 13 + ], + [ + "AAaCaccb", + "AAA", + 11 + ], + [ + "AAaCbAAAc", + "babcbBccb", + 14 + ], + [ + "AAaCcB", + "aA", + 9 + ], + [ + "AAaa", + "Abcbb", + 8 + ], + [ + "AAaaA", + "ACaCAcaa", + 9 + ], + [ + "AAaaB", + "cAaaAB", + 4 + ], + [ + "AAaaBbaC", + "bbCaCBc", + 12 + ], + [ + "AAaaCBabC", + "bBb", + 14 + ], + [ + "AAaacacca", + "C", + 17 + ], + [ + "AAaaccbA", + "CB", + 14 + ], + [ + "AAabAbac", + "cbABacbbc", + 11 + ], + [ + "AAabBBaA", + "caCcAAB", + 13 + ], + [ + "AAabCA", + "CbACAA", + 9 + ], + [ + "AAabCbCA", + "ACb", + 10 + ], + [ + "AAabb", + "B", + 9 + ], + [ + "AAabc", + "cBc", + 7 + ], + [ + "AAacA", + "CBBcB", + 8 + ], + [ + "AAacA", + "Cc", + 8 + ], + [ + "AAacCBCBc", + "bAaacCc", + 9 + ], + [ + "AAacCaBa", + "a", + 14 + ], + [ + "AAacCbCCC", + "BaB", + 15 + ], + [ + "AAaccABAA", + "bBCb", + 16 + ], + [ + "AAb", + "AbB", + 3 + ], + [ + "AAb", + "B", + 5 + ], + [ + "AAb", + "aCBbBBa", + 11 + ], + [ + "AAb", + "bAAc", + 4 + ], + [ + "AAb", + "bABA", + 5 + ], + [ + "AAb", + "bBCbBbaCA", + 16 + ], + [ + "AAb", + "bbBBaB", + 10 + ], + [ + "AAb", + "bbCcAb", + 8 + ], + [ + "AAbA", + "Bbb", + 6 + ], + [ + "AAbAAaBCC", + "b", + 16 + ], + [ + "AAbABB", + "Ca", + 11 + ], + [ + "AAbAaBBcB", + "BABaCB", + 10 + ], + [ + "AAbBBb", + "a", + 11 + ], + [ + "AAbBCCb", + "bA", + 12 + ], + [ + "AAbBbBcC", + "C", + 14 + ], + [ + "AAbBbbBcc", + "aCA", + 16 + ], + [ + "AAbC", + "cBbBCBBb", + 12 + ], + [ + "AAbCbBcC", + "BaABcCaCc", + 11 + ], + [ + "AAbCcCbC", + "a", + 15 + ], + [ + "AAba", + "cBCabCC", + 11 + ], + [ + "AAba", + "cBcBCcaBc", + 15 + ], + [ + "AAbaCabC", + "Aa", + 12 + ], + [ + "AAbabb", + "cbBAca", + 10 + ], + [ + "AAbb", + "AcbaaAB", + 9 + ], + [ + "AAbb", + "bccb", + 6 + ], + [ + "AAbbAcAB", + "cbacaBBBA", + 14 + ], + [ + "AAbbBAaAC", + "bcbaabc", + 11 + ], + [ + "AAbbbB", + "bCa", + 10 + ], + [ + "AAbcBbA", + "aaccCbB", + 8 + ], + [ + "AAbcaBBBA", + "ABb", + 13 + ], + [ + "AAbcacaB", + "cAAAbBb", + 12 + ], + [ + "AAbcbacC", + "abbbAaCCa", + 10 + ], + [ + "AAc", + "ACAc", + 2 + ], + [ + "AAc", + "AbB", + 4 + ], + [ + "AAc", + "BAabA", + 7 + ], + [ + "AAc", + "aAbc", + 3 + ], + [ + "AAc", + "aaaaacAA", + 12 + ], + [ + "AAc", + "aab", + 4 + ], + [ + "AAc", + "c", + 4 + ], + [ + "AAcA", + "AAbAAA", + 6 + ], + [ + "AAcABCA", + "bBBAbab", + 11 + ], + [ + "AAcAC", + "cCAAa", + 8 + ], + [ + "AAcAac", + "cAcA", + 6 + ], + [ + "AAcAbBcB", + "AAAAB", + 8 + ], + [ + "AAcAccAb", + "Ccac", + 11 + ], + [ + "AAcBBBcCB", + "A", + 16 + ], + [ + "AAcBbA", + "Aa", + 9 + ], + [ + "AAcC", + "bCbcaCCa", + 12 + ], + [ + "AAcCabaB", + "cACAC", + 11 + ], + [ + "AAcaACCBA", + "abcBCca", + 11 + ], + [ + "AAcaCBAcB", + "cCaBCcC", + 11 + ], + [ + "AAcab", + "A", + 8 + ], + [ + "AAcab", + "AcaabCccc", + 12 + ], + [ + "AAcb", + "aCBCcCBcC", + 14 + ], + [ + "AAcbAccc", + "CcccbA", + 12 + ], + [ + "AAcbCCAcB", + "BbbCaACc", + 11 + ], + [ + "AAcbCca", + "a", + 12 + ], + [ + "AAcbc", + "caCcb", + 7 + ], + [ + "AAcc", + "CcaBbcC", + 10 + ], + [ + "AAccB", + "cCca", + 7 + ], + [ + "AAccb", + "aA", + 7 + ], + [ + "AAccccbAA", + "caAaa", + 14 + ], + [ + "AB", + "A", + 2 + ], + [ + "AB", + "AACca", + 8 + ], + [ + "AB", + "ABcAc", + 6 + ], + [ + "AB", + "ACCACCbC", + 13 + ], + [ + "AB", + "BAA", + 4 + ], + [ + "AB", + "BAbac", + 7 + ], + [ + "AB", + "BC", + 4 + ], + [ + "AB", + "BbaCAc", + 10 + ], + [ + "AB", + "Bbac", + 7 + ], + [ + "AB", + "BcA", + 6 + ], + [ + "AB", + "CAAacB", + 8 + ], + [ + "AB", + "CC", + 4 + ], + [ + "AB", + "CCaabBcb", + 13 + ], + [ + "AB", + "CCcB", + 6 + ], + [ + "AB", + "CbA", + 5 + ], + [ + "AB", + "aCAC", + 6 + ], + [ + "AB", + "bBA", + 4 + ], + [ + "AB", + "bCbB", + 6 + ], + [ + "AB", + "bCccbbb", + 13 + ], + [ + "AB", + "bbb", + 5 + ], + [ + "AB", + "bcAbAAA", + 11 + ], + [ + "AB", + "bcbC", + 7 + ], + [ + "AB", + "c", + 4 + ], + [ + "AB", + "cCabccbCB", + 15 + ], + [ + "AB", + "caCBB", + 7 + ], + [ + "AB", + "caaac", + 9 + ], + [ + "AB", + "cababCa", + 12 + ], + [ + "ABA", + "A", + 4 + ], + [ + "ABA", + "ACBcBa", + 7 + ], + [ + "ABA", + "AcAb", + 4 + ], + [ + "ABA", + "abbBaaC", + 10 + ], + [ + "ABA", + "c", + 6 + ], + [ + "ABA", + "caAaCCbc", + 13 + ], + [ + "ABAAB", + "cbB", + 7 + ], + [ + "ABAABacbC", + "Ba", + 14 + ], + [ + "ABAAaCAa", + "cABb", + 14 + ], + [ + "ABAAaaBb", + "BCACaB", + 8 + ], + [ + "ABAAabb", + "bAcABc", + 9 + ], + [ + "ABAAabbCC", + "cbcBc", + 15 + ], + [ + "ABAAbBCb", + "aCACb", + 9 + ], + [ + "ABAAc", + "AbCc", + 5 + ], + [ + "ABAAcC", + "A", + 10 + ], + [ + "ABABBCaBb", + "aAcBcc", + 12 + ], + [ + "ABABCAB", + "cbBBCaA", + 8 + ], + [ + "ABABaaCc", + "BBBBBAB", + 11 + ], + [ + "ABABbCCab", + "aAC", + 13 + ], + [ + "ABAC", + "abaAC", + 4 + ], + [ + "ABACAccCC", + "BBCBcbCAc", + 11 + ], + [ + "ABACCAACA", + "aAacA", + 11 + ], + [ + "ABACaa", + "BA", + 8 + ], + [ + "ABACb", + "b", + 8 + ], + [ + "ABACbaCcC", + "cCBBbCbC", + 12 + ], + [ + "ABAa", + "A", + 6 + ], + [ + "ABAaAbbc", + "ccaCBA", + 13 + ], + [ + "ABAaB", + "bb", + 8 + ], + [ + "ABAaCCAB", + "acba", + 12 + ], + [ + "ABAaaBCBB", + "BaCC", + 12 + ], + [ + "ABAacbAbA", + "ABAb", + 10 + ], + [ + "ABAb", + "AcA", + 4 + ], + [ + "ABAb", + "cCab", + 5 + ], + [ + "ABAbACCB", + "cBCB", + 10 + ], + [ + "ABAbBC", + "BBAacabC", + 9 + ], + [ + "ABAbCCb", + "BAA", + 10 + ], + [ + "ABAbaCBB", + "aaCbbbBB", + 9 + ], + [ + "ABAbaCcC", + "bac", + 10 + ], + [ + "ABAc", + "CBACCbBA", + 11 + ], + [ + "ABAcAcB", + "Ac", + 10 + ], + [ + "ABAca", + "aCcCaAbC", + 12 + ], + [ + "ABAcaBc", + "bCaabABAb", + 14 + ], + [ + "ABAcaac", + "BcA", + 9 + ], + [ + "ABAcabA", + "aa", + 11 + ], + [ + "ABAcb", + "cCa", + 9 + ], + [ + "ABB", + "acaccCcA", + 15 + ], + [ + "ABB", + "bAaAAC", + 10 + ], + [ + "ABB", + "bBbAA", + 7 + ], + [ + "ABB", + "bac", + 6 + ], + [ + "ABBAAaCc", + "CaCaCAaCc", + 9 + ], + [ + "ABBAAbb", + "acBbCba", + 9 + ], + [ + "ABBAAc", + "bBccabAcB", + 11 + ], + [ + "ABBAbbAA", + "abAaaa", + 10 + ], + [ + "ABBAcBaa", + "Ab", + 13 + ], + [ + "ABBAcbAc", + "abBccbA", + 6 + ], + [ + "ABBBAa", + "ccBbcBa", + 9 + ], + [ + "ABBBAbAcc", + "abCaCAbcc", + 10 + ], + [ + "ABBBB", + "cAaCBCbCb", + 12 + ], + [ + "ABBBCaB", + "BaAbBabBa", + 12 + ], + [ + "ABBCC", + "aBBbaBAab", + 13 + ], + [ + "ABBCCc", + "aAcAbB", + 11 + ], + [ + "ABBCCcab", + "AcBcbcbAc", + 10 + ], + [ + "ABBa", + "bA", + 6 + ], + [ + "ABBaACA", + "cbBcAB", + 9 + ], + [ + "ABBaACCcb", + "bbCCcCb", + 10 + ], + [ + "ABBaa", + "bcabCCC", + 12 + ], + [ + "ABBabAbAC", + "AbcbbAcCc", + 10 + ], + [ + "ABBaba", + "BCcbAaBCA", + 13 + ], + [ + "ABBacb", + "CAbcBAbBa", + 11 + ], + [ + "ABBb", + "Aabc", + 5 + ], + [ + "ABBb", + "aAcAb", + 6 + ], + [ + "ABBbBAbCA", + "CCBACAAb", + 14 + ], + [ + "ABBbCbb", + "bccbcAc", + 11 + ], + [ + "ABBbb", + "aAabbCbCC", + 11 + ], + [ + "ABBcBBC", + "bCaB", + 10 + ], + [ + "ABBcCCC", + "bABbaCCC", + 5 + ], + [ + "ABBcb", + "aCc", + 7 + ], + [ + "ABBcbcC", + "AACBbAC", + 8 + ], + [ + "ABBccBcAa", + "ABa", + 12 + ], + [ + "ABBccCBA", + "CCCACa", + 12 + ], + [ + "ABBccc", + "B", + 10 + ], + [ + "ABC", + "BAaACA", + 8 + ], + [ + "ABC", + "BCc", + 4 + ], + [ + "ABC", + "CAa", + 6 + ], + [ + "ABC", + "CBca", + 5 + ], + [ + "ABC", + "CbCbbCB", + 11 + ], + [ + "ABC", + "bacacB", + 10 + ], + [ + "ABC", + "c", + 5 + ], + [ + "ABCAB", + "bAaAc", + 8 + ], + [ + "ABCAC", + "BaCBaC", + 7 + ], + [ + "ABCAa", + "aABbb", + 8 + ], + [ + "ABCAbAacA", + "cbCAa", + 11 + ], + [ + "ABCBC", + "AB", + 6 + ], + [ + "ABCBa", + "CcAACBC", + 8 + ], + [ + "ABCBa", + "caBbccC", + 10 + ], + [ + "ABCCAa", + "A", + 10 + ], + [ + "ABCCC", + "CC", + 6 + ], + [ + "ABCCbcBc", + "AbCBcBB", + 6 + ], + [ + "ABCCcCBB", + "bbccAbBBc", + 11 + ], + [ + "ABCCccaB", + "aCcACCCa", + 10 + ], + [ + "ABCaBa", + "BBb", + 8 + ], + [ + "ABCaBa", + "bbAcAA", + 10 + ], + [ + "ABCaC", + "bcCcab", + 8 + ], + [ + "ABCaa", + "AbCcaAb", + 6 + ], + [ + "ABCab", + "b", + 8 + ], + [ + "ABCabB", + "AAc", + 9 + ], + [ + "ABCb", + "CBBBC", + 7 + ], + [ + "ABCb", + "aABCcBC", + 7 + ], + [ + "ABCbAA", + "Ba", + 9 + ], + [ + "ABCbBBbc", + "cabbbCbcB", + 11 + ], + [ + "ABCbC", + "cc", + 8 + ], + [ + "ABCbCaCCc", + "aABB", + 15 + ], + [ + "ABCbaaCbb", + "CBbBBABBB", + 12 + ], + [ + "ABCbaaac", + "aaCaCCCca", + 13 + ], + [ + "ABCbbA", + "BCaAa", + 7 + ], + [ + "ABCbcC", + "ccbBBa", + 11 + ], + [ + "ABCc", + "Aa", + 6 + ], + [ + "ABCcAabbB", + "cCCaBBc", + 11 + ], + [ + "ABCcBcbC", + "Bcbcc", + 8 + ], + [ + "ABCcb", + "CacBAb", + 9 + ], + [ + "ABCccaAcC", + "BaAcBbC", + 12 + ], + [ + "ABa", + "AaabCa", + 7 + ], + [ + "ABa", + "CaA", + 5 + ], + [ + "ABaABc", + "caC", + 9 + ], + [ + "ABaAC", + "Caca", + 8 + ], + [ + "ABaAb", + "bAaC", + 7 + ], + [ + "ABaB", + "c", + 8 + ], + [ + "ABaBa", + "aacCAc", + 10 + ], + [ + "ABaBb", + "acbaBCcb", + 8 + ], + [ + "ABaBccB", + "BacbBBBcb", + 11 + ], + [ + "ABaCAb", + "aba", + 8 + ], + [ + "ABaCCC", + "aCAaacbc", + 10 + ], + [ + "ABaCc", + "bB", + 8 + ], + [ + "ABaa", + "CCcCC", + 10 + ], + [ + "ABabAbb", + "BCAcB", + 9 + ], + [ + "ABabBC", + "acc", + 9 + ], + [ + "ABabBaAc", + "AbBCbaBBB", + 12 + ], + [ + "ABababAC", + "aabaaBAA", + 8 + ], + [ + "ABabb", + "bBb", + 6 + ], + [ + "ABabcBacC", + "aC", + 14 + ], + [ + "ABabcBcc", + "BAcB", + 9 + ], + [ + "ABacCac", + "bbaCBcbcB", + 11 + ], + [ + "ABacbAAab", + "bC", + 16 + ], + [ + "ABacbbaCC", + "CbCbcbBAA", + 13 + ], + [ + "ABb", + "AaAcB", + 7 + ], + [ + "ABb", + "B", + 4 + ], + [ + "ABb", + "BB", + 3 + ], + [ + "ABb", + "BBaCCAAA", + 14 + ], + [ + "ABb", + "bBBaA", + 7 + ], + [ + "ABb", + "baCaaBaa", + 13 + ], + [ + "ABbA", + "CaACAABa", + 12 + ], + [ + "ABbACAC", + "BAcBBaB", + 12 + ], + [ + "ABbACAaAa", + "Cca", + 14 + ], + [ + "ABbAaa", + "cbacaa", + 7 + ], + [ + "ABbAaaB", + "BBc", + 11 + ], + [ + "ABbAacbAC", + "bAbAaacaa", + 11 + ], + [ + "ABbAbA", + "aaAaB", + 9 + ], + [ + "ABbBAb", + "a", + 11 + ], + [ + "ABbBcCbcA", + "acaAacBAB", + 15 + ], + [ + "ABbCC", + "caACcc", + 9 + ], + [ + "ABbCaAbb", + "b", + 14 + ], + [ + "ABbCb", + "aABCBBca", + 10 + ], + [ + "ABbCbA", + "A", + 10 + ], + [ + "ABbCbA", + "AbCbB", + 4 + ], + [ + "ABbCbbBa", + "B", + 14 + ], + [ + "ABbCcB", + "AaCccab", + 8 + ], + [ + "ABbaACBCc", + "baBCbCbcB", + 11 + ], + [ + "ABbaabcCC", + "aaaacAAAA", + 15 + ], + [ + "ABbacAAA", + "CAAccb", + 13 + ], + [ + "ABbb", + "cAaC", + 8 + ], + [ + "ABbbA", + "BCcbb", + 8 + ], + [ + "ABbbC", + "ABBc", + 4 + ], + [ + "ABbbbB", + "BCabaCc", + 12 + ], + [ + "ABbc", + "bBcAAac", + 10 + ], + [ + "ABbcc", + "AbBABaaa", + 11 + ], + [ + "ABbccBA", + "cACaCcCC", + 11 + ], + [ + "ABc", + "a", + 5 + ], + [ + "ABc", + "aA", + 5 + ], + [ + "ABc", + "b", + 5 + ], + [ + "ABcA", + "CB", + 6 + ], + [ + "ABcAa", + "BcbAC", + 6 + ], + [ + "ABcAaB", + "ab", + 9 + ], + [ + "ABcAbAa", + "AcAABbb", + 9 + ], + [ + "ABcAc", + "c", + 8 + ], + [ + "ABcAcCAc", + "cAcBcCaC", + 8 + ], + [ + "ABcBBcCCB", + "BaAcBbC", + 12 + ], + [ + "ABcBbAb", + "aAbACcAca", + 13 + ], + [ + "ABcBcCCbB", + "cbAC", + 13 + ], + [ + "ABcC", + "Cabc", + 6 + ], + [ + "ABcC", + "c", + 6 + ], + [ + "ABcCAaCc", + "C", + 14 + ], + [ + "ABcCAbCba", + "CcBa", + 12 + ], + [ + "ABcCBb", + "CAb", + 8 + ], + [ + "ABcCCa", + "bCaca", + 7 + ], + [ + "ABcCCcBa", + "Cbc", + 12 + ], + [ + "ABcCbbBbC", + "b", + 16 + ], + [ + "ABcaBC", + "Bcc", + 7 + ], + [ + "ABcaBb", + "CB", + 9 + ], + [ + "ABcb", + "a", + 7 + ], + [ + "ABcbAcc", + "AbBa", + 9 + ], + [ + "ABcbBC", + "cCcAa", + 10 + ], + [ + "ABcbCacCb", + "ba", + 14 + ], + [ + "ABcc", + "bAAcbCa", + 9 + ], + [ + "ABccA", + "CA", + 7 + ], + [ + "ABcccb", + "CB", + 10 + ], + [ + "AC", + "A", + 2 + ], + [ + "AC", + "AAb", + 4 + ], + [ + "AC", + "AB", + 2 + ], + [ + "AC", + "ABBc", + 5 + ], + [ + "AC", + "ABaaaBc", + 11 + ], + [ + "AC", + "ACBaAbb", + 10 + ], + [ + "AC", + "AaBcBC", + 8 + ], + [ + "AC", + "AaCCCB", + 8 + ], + [ + "AC", + "AabB", + 6 + ], + [ + "AC", + "AacAaBc", + 11 + ], + [ + "AC", + "AbBCacbA", + 12 + ], + [ + "AC", + "AbbbcbACb", + 14 + ], + [ + "AC", + "B", + 4 + ], + [ + "AC", + "BBbA", + 8 + ], + [ + "AC", + "BCaa", + 6 + ], + [ + "AC", + "BbCaaA", + 10 + ], + [ + "AC", + "BcAab", + 8 + ], + [ + "AC", + "C", + 2 + ], + [ + "AC", + "CAcb", + 5 + ], + [ + "AC", + "Ca", + 4 + ], + [ + "AC", + "CabAB", + 8 + ], + [ + "AC", + "aABaABAB", + 14 + ], + [ + "AC", + "aAacbbcAC", + 14 + ], + [ + "AC", + "aBCaCBba", + 13 + ], + [ + "AC", + "aa", + 3 + ], + [ + "AC", + "aaaBBabAb", + 16 + ], + [ + "AC", + "baccAbcB", + 13 + ], + [ + "AC", + "c", + 3 + ], + [ + "AC", + "cACCaBA", + 10 + ], + [ + "AC", + "cCaaAacaA", + 15 + ], + [ + "AC", + "cbcbbcC", + 12 + ], + [ + "ACA", + "Aa", + 3 + ], + [ + "ACA", + "C", + 4 + ], + [ + "ACA", + "CbaaBc", + 11 + ], + [ + "ACA", + "aAABAAcb", + 12 + ], + [ + "ACA", + "cACCbaAaC", + 12 + ], + [ + "ACAA", + "AcaAAb", + 5 + ], + [ + "ACAABBaC", + "A", + 14 + ], + [ + "ACAABcA", + "bCBACacC", + 10 + ], + [ + "ACAAC", + "bB", + 10 + ], + [ + "ACAAC", + "cBaaCabaC", + 11 + ], + [ + "ACAAa", + "CAa", + 4 + ], + [ + "ACAAaCaA", + "AAacb", + 9 + ], + [ + "ACAAbcaAa", + "bba", + 14 + ], + [ + "ACABA", + "bAB", + 6 + ], + [ + "ACABA", + "c", + 9 + ], + [ + "ACABABA", + "CcC", + 12 + ], + [ + "ACABACaB", + "bcBAaAB", + 8 + ], + [ + "ACABbCaBc", + "cA", + 15 + ], + [ + "ACAC", + "aaBAA", + 7 + ], + [ + "ACACB", + "aaCcbaBC", + 11 + ], + [ + "ACACBB", + "cCb", + 8 + ], + [ + "ACACBbccA", + "BABbAc", + 10 + ], + [ + "ACACC", + "BaAA", + 8 + ], + [ + "ACACa", + "Bccc", + 8 + ], + [ + "ACACbB", + "aCaacAaC", + 11 + ], + [ + "ACACbab", + "cC", + 11 + ], + [ + "ACAaA", + "Aaabbc", + 9 + ], + [ + "ACAaB", + "caBBcB", + 9 + ], + [ + "ACAaBA", + "CAbCC", + 8 + ], + [ + "ACAaCBccb", + "c", + 16 + ], + [ + "ACAabbCAc", + "BB", + 16 + ], + [ + "ACAb", + "AaaCC", + 7 + ], + [ + "ACAbBCaCA", + "A", + 16 + ], + [ + "ACAbbaaBA", + "aacacABbc", + 15 + ], + [ + "ACAbcB", + "bcBCB", + 7 + ], + [ + "ACAc", + "CCbcaabA", + 12 + ], + [ + "ACAcBCc", + "AcaAbcb", + 8 + ], + [ + "ACAcaabA", + "CACbBC", + 10 + ], + [ + "ACB", + "ACbbaBb", + 8 + ], + [ + "ACB", + "aCbbc", + 6 + ], + [ + "ACB", + "baBacBAB", + 12 + ], + [ + "ACB", + "cabbCcBA", + 11 + ], + [ + "ACBA", + "CbCCCaCAc", + 14 + ], + [ + "ACBAC", + "CaABbbcBA", + 13 + ], + [ + "ACBACBB", + "cAABCbBC", + 9 + ], + [ + "ACBACCba", + "ACBCcCb", + 5 + ], + [ + "ACBAbCBc", + "bC", + 12 + ], + [ + "ACBAba", + "aAaCaC", + 10 + ], + [ + "ACBAcb", + "aCACc", + 6 + ], + [ + "ACBB", + "bAbb", + 6 + ], + [ + "ACBBAB", + "BbBCb", + 8 + ], + [ + "ACBBABBa", + "bbabA", + 11 + ], + [ + "ACBBBaA", + "bccbA", + 10 + ], + [ + "ACBBBaCA", + "bCCCB", + 12 + ], + [ + "ACBBcAccc", + "ab", + 16 + ], + [ + "ACBCCC", + "a", + 11 + ], + [ + "ACBCbaa", + "ACCAbCCb", + 10 + ], + [ + "ACBaA", + "BcBcBcA", + 9 + ], + [ + "ACBaBa", + "AaAa", + 6 + ], + [ + "ACBabaa", + "abcAbbb", + 10 + ], + [ + "ACBabbab", + "CAa", + 11 + ], + [ + "ACBac", + "BcbcCb", + 9 + ], + [ + "ACBb", + "bcCAca", + 10 + ], + [ + "ACBbCaaBB", + "Cc", + 15 + ], + [ + "ACBba", + "CcaAbbc", + 10 + ], + [ + "ACBbaAbAC", + "aCabbAC", + 7 + ], + [ + "ACBbcAcB", + "cb", + 13 + ], + [ + "ACBcAB", + "cb", + 9 + ], + [ + "ACBcaaAAb", + "ACBA", + 10 + ], + [ + "ACC", + "BcccCaAba", + 15 + ], + [ + "ACC", + "Ccb", + 5 + ], + [ + "ACC", + "aB", + 5 + ], + [ + "ACC", + "bBcABBbAC", + 14 + ], + [ + "ACC", + "cABA", + 6 + ], + [ + "ACC", + "caACC", + 4 + ], + [ + "ACCA", + "BaaBc", + 9 + ], + [ + "ACCA", + "BacAabAAa", + 14 + ], + [ + "ACCACABB", + "ABAAbBaBa", + 11 + ], + [ + "ACCAbB", + "CcbAacC", + 11 + ], + [ + "ACCAccAB", + "ACbCC", + 10 + ], + [ + "ACCBAAC", + "Ac", + 11 + ], + [ + "ACCBAaBC", + "a", + 14 + ], + [ + "ACCBB", + "BcCccabbA", + 13 + ], + [ + "ACCBBcac", + "CCB", + 10 + ], + [ + "ACCBCBC", + "AcaaCacba", + 11 + ], + [ + "ACCBac", + "aCACaCc", + 7 + ], + [ + "ACCBb", + "BaCac", + 8 + ], + [ + "ACCBbcbAC", + "AAaCaCBCC", + 12 + ], + [ + "ACCBcCBB", + "aBBABb", + 10 + ], + [ + "ACCBcaaCa", + "CBb", + 14 + ], + [ + "ACCBcc", + "bbAa", + 11 + ], + [ + "ACCCAaCcC", + "CBAc", + 12 + ], + [ + "ACCCBac", + "CcCBACBB", + 9 + ], + [ + "ACCCC", + "CACCACc", + 5 + ], + [ + "ACCCb", + "CbBaCcb", + 9 + ], + [ + "ACCCbaCAc", + "abACCC", + 11 + ], + [ + "ACCaAACa", + "C", + 14 + ], + [ + "ACCaACAa", + "a", + 14 + ], + [ + "ACCaB", + "A", + 8 + ], + [ + "ACCaCA", + "a", + 10 + ], + [ + "ACCaaAc", + "A", + 12 + ], + [ + "ACCac", + "a", + 8 + ], + [ + "ACCbA", + "aCCBa", + 3 + ], + [ + "ACCbB", + "BaAa", + 10 + ], + [ + "ACCbC", + "CcbaCC", + 7 + ], + [ + "ACCbCba", + "a", + 12 + ], + [ + "ACCbaBbb", + "aA", + 14 + ], + [ + "ACCbba", + "aaCBCbccB", + 11 + ], + [ + "ACCbbbC", + "BCbABAbaA", + 13 + ], + [ + "ACCbcBa", + "aCBaBcbC", + 9 + ], + [ + "ACCbcbccB", + "caAAbaCcb", + 13 + ], + [ + "ACCcAccCc", + "AccAb", + 11 + ], + [ + "ACCcBcB", + "AcBcAAca", + 9 + ], + [ + "ACCcC", + "a", + 9 + ], + [ + "ACCcaC", + "cCA", + 8 + ], + [ + "ACCcacC", + "BBaaAcAab", + 14 + ], + [ + "ACa", + "AcbCAbaB", + 10 + ], + [ + "ACa", + "Bca", + 3 + ], + [ + "ACa", + "ba", + 4 + ], + [ + "ACa", + "baAAaba", + 10 + ], + [ + "ACaA", + "A", + 6 + ], + [ + "ACaA", + "ABCAba", + 6 + ], + [ + "ACaA", + "acbCc", + 8 + ], + [ + "ACaA", + "cc", + 7 + ], + [ + "ACaAC", + "cABCBAc", + 7 + ], + [ + "ACaAcABa", + "cCAACC", + 10 + ], + [ + "ACaB", + "ABAbccB", + 9 + ], + [ + "ACaB", + "bAAAaAbAA", + 13 + ], + [ + "ACaBBBCB", + "AaAC", + 10 + ], + [ + "ACaBBaAa", + "AcaBcBbcB", + 9 + ], + [ + "ACaBCB", + "cc", + 10 + ], + [ + "ACaBCBb", + "b", + 12 + ], + [ + "ACaBacCc", + "bb", + 15 + ], + [ + "ACaBcb", + "BcCc", + 9 + ], + [ + "ACaCACc", + "c", + 12 + ], + [ + "ACaCBccBc", + "CcABCcBcb", + 9 + ], + [ + "ACaCCBA", + "AAcacC", + 8 + ], + [ + "ACaa", + "BAAc", + 7 + ], + [ + "ACaaBAbc", + "ACAccba", + 9 + ], + [ + "ACaabb", + "BCAb", + 7 + ], + [ + "ACaacBa", + "cBCBAc", + 11 + ], + [ + "ACabABBA", + "CAcCbB", + 10 + ], + [ + "ACabC", + "B", + 9 + ], + [ + "ACabac", + "BACcbCcbB", + 10 + ], + [ + "ACabbcA", + "aCacc", + 7 + ], + [ + "ACaccaa", + "AA", + 11 + ], + [ + "ACb", + "A", + 4 + ], + [ + "ACb", + "cBAab", + 6 + ], + [ + "ACb", + "ccbccbB", + 11 + ], + [ + "ACbA", + "AaBacA", + 7 + ], + [ + "ACbA", + "BaCCbA", + 5 + ], + [ + "ACbA", + "abACa", + 7 + ], + [ + "ACbACcbaB", + "ccc", + 14 + ], + [ + "ACbAbaCBC", + "acbaba", + 9 + ], + [ + "ACbB", + "aCACBAbb", + 9 + ], + [ + "ACbBA", + "CACB", + 6 + ], + [ + "ACbBCC", + "bcbCaAB", + 11 + ], + [ + "ACbBa", + "B", + 8 + ], + [ + "ACbBaCab", + "cbB", + 11 + ], + [ + "ACbCCbBb", + "aaca", + 14 + ], + [ + "ACbCaA", + "bAbC", + 8 + ], + [ + "ACbCb", + "c", + 9 + ], + [ + "ACbCc", + "ABc", + 5 + ], + [ + "ACbCcbA", + "accAAAaCc", + 15 + ], + [ + "ACba", + "acABAcbC", + 11 + ], + [ + "ACba", + "bCabba", + 6 + ], + [ + "ACbaAaA", + "BaaAbcC", + 12 + ], + [ + "ACbaAbB", + "AaBbbbb", + 8 + ], + [ + "ACbaAc", + "ABaBbCBcB", + 12 + ], + [ + "ACbaCAC", + "bBcaC", + 8 + ], + [ + "ACbaCaB", + "cbaA", + 8 + ], + [ + "ACbaaaB", + "CAcA", + 10 + ], + [ + "ACbabABB", + "BCBABBAc", + 11 + ], + [ + "ACbabcac", + "ABBA", + 11 + ], + [ + "ACbbBB", + "cAbbaC", + 8 + ], + [ + "ACbbBb", + "abAAbCab", + 10 + ], + [ + "ACbbCc", + "AccCbBAAB", + 11 + ], + [ + "ACbbca", + "cbABC", + 9 + ], + [ + "ACbbccB", + "b", + 12 + ], + [ + "ACbcBBc", + "CBacBbCbA", + 11 + ], + [ + "ACbcCba", + "abBBc", + 10 + ], + [ + "ACbcCccA", + "BBaA", + 13 + ], + [ + "ACc", + "AABbabcbb", + 14 + ], + [ + "ACc", + "ABbCcC", + 6 + ], + [ + "ACc", + "ABcBaB", + 8 + ], + [ + "ACc", + "a", + 5 + ], + [ + "ACc", + "b", + 6 + ], + [ + "ACc", + "baCBbbCbc", + 13 + ], + [ + "ACc", + "bcAcC", + 6 + ], + [ + "ACc", + "c", + 4 + ], + [ + "ACcA", + "CbcBBcbb", + 13 + ], + [ + "ACcA", + "aBBA", + 5 + ], + [ + "ACcAAb", + "ccAa", + 6 + ], + [ + "ACcABac", + "cA", + 10 + ], + [ + "ACcAc", + "baC", + 8 + ], + [ + "ACcBCBbbB", + "aA", + 17 + ], + [ + "ACcBa", + "ABBbBAbca", + 12 + ], + [ + "ACcBa", + "abbaca", + 9 + ], + [ + "ACcBba", + "baaAcAcA", + 12 + ], + [ + "ACcBbaa", + "CbCcCCBAC", + 12 + ], + [ + "ACcC", + "AcCABAC", + 8 + ], + [ + "ACcCACb", + "CABB", + 9 + ], + [ + "ACcCC", + "ccabBBC", + 11 + ], + [ + "ACcCa", + "Bcb", + 8 + ], + [ + "ACcCacAa", + "CbCba", + 10 + ], + [ + "ACcCc", + "bbBCC", + 7 + ], + [ + "ACca", + "cBBCBcbC", + 12 + ], + [ + "ACcaCAb", + "aaA", + 9 + ], + [ + "ACcaaAb", + "CaCAc", + 8 + ], + [ + "ACcabc", + "AAACb", + 8 + ], + [ + "ACcac", + "abBCBCc", + 9 + ], + [ + "ACcbB", + "caBaa", + 10 + ], + [ + "ACcbC", + "aCA", + 7 + ], + [ + "ACcbaBb", + "aCACCCAaA", + 12 + ], + [ + "ACcbbC", + "cacb", + 8 + ], + [ + "ACcbcCBB", + "A", + 14 + ], + [ + "ACcc", + "Cca", + 4 + ], + [ + "ACcca", + "accBbB", + 8 + ], + [ + "ACccabBC", + "Bb", + 14 + ], + [ + "ACccb", + "C", + 8 + ], + [ + "ACccbCccb", + "BbCacaA", + 14 + ], + [ + "ACccca", + "A", + 10 + ], + [ + "Aa", + "A", + 2 + ], + [ + "Aa", + "AAcac", + 6 + ], + [ + "Aa", + "ABBa", + 4 + ], + [ + "Aa", + "AbAabCcB", + 12 + ], + [ + "Aa", + "AbCCCCbcC", + 16 + ], + [ + "Aa", + "AbccAA", + 9 + ], + [ + "Aa", + "B", + 4 + ], + [ + "Aa", + "BA", + 3 + ], + [ + "Aa", + "BB", + 4 + ], + [ + "Aa", + "BBaccBAc", + 14 + ], + [ + "Aa", + "BCAaBb", + 8 + ], + [ + "Aa", + "BaBBAc", + 10 + ], + [ + "Aa", + "BaBcCaBc", + 13 + ], + [ + "Aa", + "BaCc", + 6 + ], + [ + "Aa", + "BabcCcAb", + 14 + ], + [ + "Aa", + "BbBB", + 8 + ], + [ + "Aa", + "C", + 4 + ], + [ + "Aa", + "CAabcaaAB", + 14 + ], + [ + "Aa", + "CAbbCbCB", + 14 + ], + [ + "Aa", + "CBBaBccb", + 14 + ], + [ + "Aa", + "CBCAccba", + 12 + ], + [ + "Aa", + "a", + 2 + ], + [ + "Aa", + "aAc", + 4 + ], + [ + "Aa", + "aacb", + 5 + ], + [ + "Aa", + "acAAacaB", + 12 + ], + [ + "Aa", + "acaA", + 5 + ], + [ + "Aa", + "acbc", + 7 + ], + [ + "Aa", + "b", + 4 + ], + [ + "Aa", + "bBA", + 5 + ], + [ + "Aa", + "bBABbc", + 10 + ], + [ + "Aa", + "baABcc", + 10 + ], + [ + "Aa", + "babbc", + 8 + ], + [ + "Aa", + "bbabBCac", + 13 + ], + [ + "Aa", + "cAAB", + 5 + ], + [ + "Aa", + "cC", + 4 + ], + [ + "Aa", + "caCAB", + 8 + ], + [ + "Aa", + "ccabB", + 8 + ], + [ + "AaA", + "Acbc", + 6 + ], + [ + "AaA", + "BBAcB", + 8 + ], + [ + "AaA", + "BaAbb", + 6 + ], + [ + "AaA", + "CAcBB", + 8 + ], + [ + "AaA", + "Cc", + 6 + ], + [ + "AaA", + "a", + 4 + ], + [ + "AaA", + "bAcaab", + 7 + ], + [ + "AaA", + "bCbCac", + 10 + ], + [ + "AaA", + "bCcaBAa", + 10 + ], + [ + "AaA", + "cAbaCAbB", + 10 + ], + [ + "AaAA", + "bcacBb", + 10 + ], + [ + "AaAABca", + "aACbAbaa", + 9 + ], + [ + "AaAAbA", + "CAbA", + 6 + ], + [ + "AaAAbBCcc", + "bbcAb", + 14 + ], + [ + "AaAAcbcbB", + "aAcCCBba", + 11 + ], + [ + "AaABbA", + "BBaACcACA", + 12 + ], + [ + "AaABc", + "cCcaBB", + 9 + ], + [ + "AaABcBAbb", + "BABAbaB", + 11 + ], + [ + "AaACCAb", + "C", + 12 + ], + [ + "AaACCaac", + "aabBbAbCA", + 13 + ], + [ + "AaACa", + "BBb", + 10 + ], + [ + "AaAaABB", + "CABcCCcCA", + 16 + ], + [ + "AaAaBbCc", + "B", + 14 + ], + [ + "AaAaa", + "cba", + 8 + ], + [ + "AaAabb", + "CACAA", + 9 + ], + [ + "AaAaccCB", + "bAaacBbaA", + 12 + ], + [ + "AaAb", + "cABAcb", + 6 + ], + [ + "AaAbAaAb", + "a", + 14 + ], + [ + "AaAbAbbB", + "aA", + 12 + ], + [ + "AaAbBba", + "CCBac", + 12 + ], + [ + "AaAbb", + "BbaB", + 8 + ], + [ + "AaAbbc", + "cAAcCC", + 8 + ], + [ + "AaAbc", + "a", + 8 + ], + [ + "AaAc", + "aAaCA", + 6 + ], + [ + "AaAcA", + "BCAAAba", + 8 + ], + [ + "AaAccaBbc", + "baAcAACaa", + 11 + ], + [ + "AaB", + "AbbAbb", + 8 + ], + [ + "AaB", + "Bca", + 6 + ], + [ + "AaB", + "bbaAb", + 7 + ], + [ + "AaB", + "bcABBBCc", + 12 + ], + [ + "AaB", + "caAb", + 5 + ], + [ + "AaBA", + "cBAB", + 6 + ], + [ + "AaBACC", + "BCAAbB", + 10 + ], + [ + "AaBACaBA", + "cAc", + 13 + ], + [ + "AaBAa", + "c", + 10 + ], + [ + "AaBAc", + "CcACba", + 10 + ], + [ + "AaBAcBcaa", + "cb", + 15 + ], + [ + "AaBBAC", + "ACAca", + 9 + ], + [ + "AaBBAcbcc", + "CbCcBb", + 14 + ], + [ + "AaBBbcA", + "A", + 12 + ], + [ + "AaBCcbCB", + "CCcAb", + 11 + ], + [ + "AaBa", + "abA", + 4 + ], + [ + "AaBa", + "bbbCabA", + 10 + ], + [ + "AaBaaaCBa", + "A", + 16 + ], + [ + "AaBabBCb", + "aBac", + 9 + ], + [ + "AaBbB", + "bBaa", + 8 + ], + [ + "AaBba", + "ab", + 6 + ], + [ + "AaBbba", + "caAbBAa", + 7 + ], + [ + "AaBbbaac", + "Cbcca", + 12 + ], + [ + "AaBbbbA", + "AabcBAa", + 7 + ], + [ + "AaBc", + "bAaaBaCCB", + 11 + ], + [ + "AaBcA", + "aCbCbB", + 9 + ], + [ + "AaBcAbBC", + "CBCACaBBA", + 12 + ], + [ + "AaBccbA", + "BBcaaccb", + 10 + ], + [ + "AaC", + "AACcabcAa", + 13 + ], + [ + "AaC", + "ABAaabA", + 10 + ], + [ + "AaC", + "Acc", + 3 + ], + [ + "AaC", + "aCCB", + 5 + ], + [ + "AaC", + "bbbbbbAA", + 15 + ], + [ + "AaCA", + "CCc", + 6 + ], + [ + "AaCAAAcac", + "ABB", + 16 + ], + [ + "AaCAAbaa", + "CCbABc", + 11 + ], + [ + "AaCACCa", + "BCcBACc", + 10 + ], + [ + "AaCACCbcc", + "CAacc", + 10 + ], + [ + "AaCAcAcB", + "bbA", + 14 + ], + [ + "AaCB", + "BbBC", + 8 + ], + [ + "AaCB", + "bCCbBAA", + 10 + ], + [ + "AaCBa", + "ACAbC", + 7 + ], + [ + "AaCBaC", + "Ba", + 8 + ], + [ + "AaCBaaAAC", + "CaBc", + 13 + ], + [ + "AaCBbBBa", + "CBbAcCBb", + 12 + ], + [ + "AaCCAAC", + "ACaBb", + 9 + ], + [ + "AaCCAbcBB", + "BCaab", + 13 + ], + [ + "AaCCCB", + "AaaAA", + 8 + ], + [ + "AaCCCBB", + "bB", + 11 + ], + [ + "AaCCcbB", + "BCcaCcB", + 9 + ], + [ + "AaCCcc", + "cbc", + 9 + ], + [ + "AaCaAAa", + "ccccBbcaC", + 15 + ], + [ + "AaCaCccb", + "aCB", + 11 + ], + [ + "AaCaaaA", + "a", + 12 + ], + [ + "AaCaac", + "CBabAA", + 10 + ], + [ + "AaCac", + "AbaBC", + 7 + ], + [ + "AaCbbbBaB", + "ccC", + 16 + ], + [ + "AaCbcCb", + "ccAAaB", + 12 + ], + [ + "AaCc", + "a", + 6 + ], + [ + "AaCcABAa", + "B", + 14 + ], + [ + "AaCcAbC", + "Bb", + 12 + ], + [ + "AaCcBBcc", + "cBb", + 11 + ], + [ + "AaCcaCBaA", + "ABca", + 12 + ], + [ + "AaCcab", + "bBcB", + 9 + ], + [ + "AaCcacA", + "BBBAcbC", + 13 + ], + [ + "AaCcbBbCa", + "BBBAaabCb", + 14 + ], + [ + "AaCcc", + "abcBAb", + 10 + ], + [ + "AaCccBBCb", + "cB", + 14 + ], + [ + "AaCccbC", + "baaaaAb", + 11 + ], + [ + "Aaa", + "BaAA", + 5 + ], + [ + "Aaa", + "CbabcBBB", + 14 + ], + [ + "Aaa", + "aAa", + 2 + ], + [ + "Aaa", + "aBB", + 5 + ], + [ + "Aaa", + "bAAbBaBcc", + 13 + ], + [ + "Aaa", + "ca", + 4 + ], + [ + "AaaA", + "aBabbB", + 9 + ], + [ + "AaaA", + "baCb", + 6 + ], + [ + "AaaAAC", + "a", + 10 + ], + [ + "AaaAAaC", + "b", + 14 + ], + [ + "AaaACAcbA", + "CBAcAC", + 12 + ], + [ + "AaaACa", + "BABcccC", + 11 + ], + [ + "AaaAbBAab", + "bacccCc", + 16 + ], + [ + "AaaAcBCB", + "Bbbb", + 14 + ], + [ + "AaaAcb", + "AcabAA", + 8 + ], + [ + "AaaB", + "aaCAC", + 6 + ], + [ + "AaaBABa", + "bBaC", + 11 + ], + [ + "AaaBCaCC", + "c", + 15 + ], + [ + "AaaBa", + "CBCBc", + 8 + ], + [ + "AaaC", + "aaAc", + 3 + ], + [ + "AaaC", + "b", + 8 + ], + [ + "AaaC", + "caaBCAABc", + 12 + ], + [ + "AaaCB", + "ABcbBCCA", + 12 + ], + [ + "AaaCcbcc", + "B", + 15 + ], + [ + "Aaaa", + "abCBCBa", + 11 + ], + [ + "AaaaA", + "CccAB", + 9 + ], + [ + "AaaaBcb", + "BCabBBBC", + 11 + ], + [ + "AaaaCCC", + "BBcaacaCB", + 11 + ], + [ + "AaaacbC", + "CcAcBACcb", + 13 + ], + [ + "AaabA", + "cB", + 9 + ], + [ + "AaabACBca", + "bAc", + 12 + ], + [ + "AaabCAC", + "bCaacCbB", + 10 + ], + [ + "Aaaba", + "BbccCaa", + 12 + ], + [ + "AaabaBba", + "aBaB", + 9 + ], + [ + "Aaababb", + "BcbBCcc", + 13 + ], + [ + "AaabcBb", + "abab", + 8 + ], + [ + "Aaac", + "BAbaab", + 6 + ], + [ + "AaacACBc", + "ACbbCaABA", + 12 + ], + [ + "AaaccCBcb", + "CBabaCACb", + 11 + ], + [ + "AaacccCc", + "c", + 14 + ], + [ + "Aab", + "BcBaaCBC", + 12 + ], + [ + "Aab", + "CBBaACcBA", + 15 + ], + [ + "Aab", + "a", + 4 + ], + [ + "AabA", + "acCbacA", + 9 + ], + [ + "AabAAcBBB", + "aa", + 15 + ], + [ + "AabAbBbcB", + "AcCcA", + 14 + ], + [ + "AabAccB", + "AcabBCb", + 8 + ], + [ + "AabBABc", + "AAabbCcCc", + 9 + ], + [ + "AabBCAaC", + "aBcCbc", + 10 + ], + [ + "AabBCC", + "A", + 10 + ], + [ + "AabBaBcBB", + "cbcc", + 14 + ], + [ + "AabBacAaa", + "aAc", + 13 + ], + [ + "AabBba", + "bcabb", + 8 + ], + [ + "AabBbc", + "CcAaA", + 12 + ], + [ + "AabBcBca", + "Ca", + 13 + ], + [ + "AabBcbc", + "ac", + 10 + ], + [ + "AabCbA", + "B", + 11 + ], + [ + "AabCbC", + "ccBbc", + 8 + ], + [ + "Aaba", + "BBaa", + 6 + ], + [ + "AabaACCC", + "aaBBcccCa", + 10 + ], + [ + "AabaCCccc", + "BaB", + 15 + ], + [ + "AabbA", + "AB", + 7 + ], + [ + "AabbABCBb", + "aBB", + 12 + ], + [ + "AabbB", + "AA", + 7 + ], + [ + "AabbB", + "bCCC", + 10 + ], + [ + "AabbbbbC", + "bBbBaB", + 10 + ], + [ + "AabbcCACc", + "AAa", + 14 + ], + [ + "Aabc", + "bBCbc", + 6 + ], + [ + "Aabc", + "bbA", + 6 + ], + [ + "AabcAAbCc", + "Aa", + 14 + ], + [ + "AabcBBb", + "bAbAcCBB", + 8 + ], + [ + "AabcBb", + "bb", + 8 + ], + [ + "AabcC", + "BbbBcbbC", + 10 + ], + [ + "AabcbCa", + "CB", + 12 + ], + [ + "Aac", + "Bccc", + 6 + ], + [ + "Aac", + "CBABCbaA", + 12 + ], + [ + "Aac", + "Cb", + 6 + ], + [ + "Aac", + "a", + 4 + ], + [ + "Aac", + "aBbCCaaa", + 13 + ], + [ + "Aac", + "baBAcA", + 8 + ], + [ + "Aac", + "c", + 4 + ], + [ + "Aac", + "caAABac", + 8 + ], + [ + "AacA", + "ACcBA", + 4 + ], + [ + "AacA", + "BaaAccBcA", + 11 + ], + [ + "AacAABbaa", + "a", + 16 + ], + [ + "AacABAB", + "CCc", + 12 + ], + [ + "AacAcbbC", + "cbCa", + 12 + ], + [ + "AacBBCc", + "BcBAA", + 10 + ], + [ + "AacBaA", + "AAa", + 7 + ], + [ + "AacBbaCBB", + "CcbC", + 12 + ], + [ + "AacBbcccc", + "CbACCaBab", + 18 + ], + [ + "AacC", + "BcBcAbb", + 12 + ], + [ + "AacCaBcaB", + "CAAb", + 13 + ], + [ + "AacCacC", + "CaABC", + 9 + ], + [ + "AacCcCcAC", + "CCBBabBA", + 16 + ], + [ + "AacCcc", + "Aab", + 8 + ], + [ + "AacaaCc", + "bAaA", + 10 + ], + [ + "AacacB", + "aaBBB", + 7 + ], + [ + "AacacBC", + "CBBcAcba", + 10 + ], + [ + "AacacBb", + "AAabbcaC", + 10 + ], + [ + "Aacb", + "c", + 6 + ], + [ + "AacbAAA", + "BbcBaCCCb", + 14 + ], + [ + "Aacbaa", + "aCBbbCaAc", + 12 + ], + [ + "Aacc", + "ACAA", + 6 + ], + [ + "Aacc", + "CBAcB", + 7 + ], + [ + "AaccAB", + "baAcAcbcc", + 11 + ], + [ + "AaccABc", + "bB", + 12 + ], + [ + "AaccAaAb", + "acb", + 10 + ], + [ + "AaccB", + "ac", + 6 + ], + [ + "AaccBcc", + "BC", + 11 + ], + [ + "AaccC", + "bAbacBAa", + 10 + ], + [ + "AaccaAc", + "Bc", + 12 + ], + [ + "AaccaBa", + "B", + 12 + ], + [ + "AaccaCcBb", + "bABCca", + 13 + ], + [ + "Aaccac", + "c", + 10 + ], + [ + "AacccAa", + "AC", + 11 + ], + [ + "AacccAcC", + "ab", + 14 + ], + [ + "AacccBBcC", + "Bb", + 15 + ], + [ + "Ab", + "A", + 2 + ], + [ + "Ab", + "AA", + 2 + ], + [ + "Ab", + "AaabCAAA", + 12 + ], + [ + "Ab", + "AbACAaCAA", + 14 + ], + [ + "Ab", + "AbCCAAACc", + 14 + ], + [ + "Ab", + "AbCbAa", + 8 + ], + [ + "Ab", + "Ac", + 2 + ], + [ + "Ab", + "AcaABbc", + 10 + ], + [ + "Ab", + "BAcCa", + 8 + ], + [ + "Ab", + "BBc", + 5 + ], + [ + "Ab", + "BbcAcCCaa", + 16 + ], + [ + "Ab", + "CABa", + 5 + ], + [ + "Ab", + "CAaB", + 5 + ], + [ + "Ab", + "CCBcAbbc", + 12 + ], + [ + "Ab", + "CaBc", + 6 + ], + [ + "Ab", + "CbBCAbaBc", + 14 + ], + [ + "Ab", + "aCAccA", + 10 + ], + [ + "Ab", + "aab", + 3 + ], + [ + "Ab", + "acaCBAa", + 12 + ], + [ + "Ab", + "bA", + 4 + ], + [ + "Ab", + "bCBc", + 7 + ], + [ + "Ab", + "c", + 4 + ], + [ + "Ab", + "cBBB", + 7 + ], + [ + "Ab", + "cC", + 4 + ], + [ + "Ab", + "cCAaBbbBC", + 14 + ], + [ + "Ab", + "cCaAcc", + 10 + ], + [ + "Ab", + "caA", + 5 + ], + [ + "Ab", + "caCacCcBC", + 16 + ], + [ + "Ab", + "cbcacAC", + 12 + ], + [ + "Ab", + "ccAbB", + 6 + ], + [ + "Ab", + "ccb", + 4 + ], + [ + "AbA", + "BCBAaC", + 9 + ], + [ + "AbA", + "BbBAaA", + 8 + ], + [ + "AbA", + "CcBa", + 6 + ], + [ + "AbA", + "aAC", + 5 + ], + [ + "AbA", + "b", + 4 + ], + [ + "AbA", + "caABB", + 7 + ], + [ + "AbA", + "ccc", + 6 + ], + [ + "AbAA", + "CBba", + 6 + ], + [ + "AbAAC", + "BAAC", + 3 + ], + [ + "AbAAC", + "bbab", + 7 + ], + [ + "AbAACaAb", + "C", + 14 + ], + [ + "AbAACbbBB", + "BCbaa", + 13 + ], + [ + "AbAAbC", + "caCaAAAbA", + 11 + ], + [ + "AbAAcbBa", + "BabBBCa", + 11 + ], + [ + "AbAB", + "Ab", + 4 + ], + [ + "AbAB", + "Bcb", + 6 + ], + [ + "AbAB", + "ac", + 7 + ], + [ + "AbABCa", + "cC", + 10 + ], + [ + "AbABa", + "BAaaCB", + 9 + ], + [ + "AbACABc", + "aBCACA", + 8 + ], + [ + "AbACBCA", + "aBB", + 10 + ], + [ + "AbACaa", + "AcAba", + 6 + ], + [ + "AbACbA", + "A", + 10 + ], + [ + "AbACbAA", + "aACA", + 7 + ], + [ + "AbACccccb", + "cCbbCBA", + 15 + ], + [ + "AbAaABB", + "BcaaA", + 9 + ], + [ + "AbAaAb", + "CB", + 11 + ], + [ + "AbAaC", + "AacbbACba", + 11 + ], + [ + "AbAaCAab", + "CabaacCaA", + 9 + ], + [ + "AbAaaa", + "AaC", + 8 + ], + [ + "AbAacaaac", + "abACABAc", + 8 + ], + [ + "AbAb", + "bbccBB", + 9 + ], + [ + "AbAbCba", + "BCbA", + 8 + ], + [ + "AbAbabac", + "bc", + 12 + ], + [ + "AbAbbBaab", + "bbabb", + 10 + ], + [ + "AbAc", + "BcbBCbC", + 11 + ], + [ + "AbAcACAa", + "cBcBBA", + 11 + ], + [ + "AbAcB", + "c", + 8 + ], + [ + "AbAcbCbca", + "AcBaaB", + 13 + ], + [ + "AbAccAbBc", + "aACcAACbA", + 11 + ], + [ + "AbAccBCa", + "BACCAaA", + 10 + ], + [ + "AbB", + "AABC", + 4 + ], + [ + "AbB", + "AbCbCCc", + 9 + ], + [ + "AbB", + "BcaAbAAAb", + 13 + ], + [ + "AbB", + "bBBA", + 5 + ], + [ + "AbB", + "cAaaaB", + 8 + ], + [ + "AbB", + "cbbABB", + 7 + ], + [ + "AbBAcCAC", + "CAAbcCC", + 9 + ], + [ + "AbBB", + "AAAAC", + 8 + ], + [ + "AbBBabc", + "Bcb", + 10 + ], + [ + "AbBBbCaA", + "AaCccAAA", + 11 + ], + [ + "AbBBcaAaa", + "bcbBbcCb", + 13 + ], + [ + "AbBCAacC", + "c", + 14 + ], + [ + "AbBCAcaa", + "aa", + 12 + ], + [ + "AbBCBbA", + "abbcBb", + 5 + ], + [ + "AbBaABC", + "AabAAbc", + 6 + ], + [ + "AbBaBB", + "AAaAca", + 9 + ], + [ + "AbBaCC", + "CAb", + 10 + ], + [ + "AbBaaAb", + "ACcBcca", + 11 + ], + [ + "AbBabBa", + "bccBAbb", + 10 + ], + [ + "AbBb", + "A", + 6 + ], + [ + "AbBb", + "BbaAB", + 7 + ], + [ + "AbBbA", + "babCBABbb", + 11 + ], + [ + "AbBbAABc", + "AbAa", + 9 + ], + [ + "AbBbCb", + "bcAACCcac", + 15 + ], + [ + "AbBbb", + "aCAC", + 9 + ], + [ + "AbBbbacbC", + "CcbccAb", + 14 + ], + [ + "AbBbcb", + "Ab", + 8 + ], + [ + "AbBbcbabC", + "Ba", + 14 + ], + [ + "AbBcBaaCc", + "BbcAB", + 13 + ], + [ + "AbBcaaCCA", + "aba", + 13 + ], + [ + "AbBcb", + "BAaAcb", + 6 + ], + [ + "AbBcc", + "AaccBc", + 6 + ], + [ + "AbBcccB", + "cB", + 10 + ], + [ + "AbC", + "BccBcC", + 9 + ], + [ + "AbC", + "C", + 4 + ], + [ + "AbCAA", + "CCcCcacBa", + 14 + ], + [ + "AbCABa", + "ACBC", + 6 + ], + [ + "AbCACa", + "BcaBacAA", + 12 + ], + [ + "AbCACcBbB", + "C", + 16 + ], + [ + "AbCAc", + "BbCbcBCAb", + 12 + ], + [ + "AbCB", + "Caa", + 8 + ], + [ + "AbCB", + "acbAc", + 7 + ], + [ + "AbCB", + "caaaba", + 10 + ], + [ + "AbCBbB", + "BacABACaB", + 13 + ], + [ + "AbCBbbC", + "cACb", + 10 + ], + [ + "AbCCcAbc", + "abB", + 12 + ], + [ + "AbCCca", + "C", + 10 + ], + [ + "AbCCcacba", + "aaAaabcb", + 13 + ], + [ + "AbCa", + "CcAaCAaA", + 10 + ], + [ + "AbCa", + "aBcccBBa", + 11 + ], + [ + "AbCaA", + "bBbAB", + 8 + ], + [ + "AbCaAAbab", + "BCbCcb", + 13 + ], + [ + "AbCaC", + "AbABC", + 4 + ], + [ + "AbCaabBCB", + "Cb", + 14 + ], + [ + "AbCabbc", + "BcCaB", + 9 + ], + [ + "AbCacAacb", + "aaAAAAC", + 12 + ], + [ + "AbCb", + "A", + 6 + ], + [ + "AbCb", + "BbaBcaCb", + 10 + ], + [ + "AbCbC", + "Cc", + 7 + ], + [ + "AbCbaB", + "aCac", + 7 + ], + [ + "AbCc", + "bAaCac", + 6 + ], + [ + "AbCcBBACC", + "cACC", + 10 + ], + [ + "AbCcbaBCA", + "cccCAbbCA", + 11 + ], + [ + "Aba", + "ACA", + 3 + ], + [ + "Aba", + "C", + 6 + ], + [ + "Aba", + "aB", + 4 + ], + [ + "Aba", + "bccccA", + 11 + ], + [ + "Aba", + "cabbcC", + 9 + ], + [ + "AbaAACBCb", + "CACabC", + 13 + ], + [ + "AbaAabca", + "CBCBCA", + 12 + ], + [ + "AbaAb", + "cb", + 8 + ], + [ + "AbaB", + "aCaB", + 3 + ], + [ + "AbaBAaCbC", + "cbbacCbA", + 10 + ], + [ + "AbaBAbaB", + "cbaC", + 12 + ], + [ + "AbaBBcbAA", + "bCaaC", + 14 + ], + [ + "AbaBCa", + "B", + 10 + ], + [ + "AbaBCbaB", + "acBcb", + 10 + ], + [ + "AbaBaca", + "acAAABBB", + 13 + ], + [ + "AbaBcCba", + "accBCABAB", + 12 + ], + [ + "AbaC", + "BAAb", + 7 + ], + [ + "AbaC", + "aA", + 6 + ], + [ + "AbaCb", + "B", + 9 + ], + [ + "AbaCbBc", + "ACBAaCAcA", + 11 + ], + [ + "Abaa", + "aAcBb", + 8 + ], + [ + "Abaa", + "cbbAaabCc", + 12 + ], + [ + "AbaaAbab", + "CCaaBc", + 11 + ], + [ + "AbaaAc", + "bAba", + 8 + ], + [ + "AbaaabCBb", + "aa", + 14 + ], + [ + "Abab", + "bbbBb", + 6 + ], + [ + "Abab", + "cab", + 4 + ], + [ + "AbabBBb", + "CaAAc", + 12 + ], + [ + "AbabCC", + "CACcAabA", + 12 + ], + [ + "AbabcCbb", + "AbcCcbB", + 7 + ], + [ + "AbacAa", + "bCAAabcCa", + 10 + ], + [ + "AbacB", + "CBaCBAcBa", + 11 + ], + [ + "AbacBCc", + "B", + 12 + ], + [ + "AbacBbBCC", + "bC", + 14 + ], + [ + "AbacaaCB", + "B", + 14 + ], + [ + "AbacaaaBC", + "AacAa", + 9 + ], + [ + "Abacb", + "AcccbAb", + 8 + ], + [ + "Abb", + "Aba", + 2 + ], + [ + "Abb", + "Ac", + 4 + ], + [ + "Abb", + "BCcba", + 8 + ], + [ + "AbbA", + "BCb", + 6 + ], + [ + "AbbABab", + "baacCBb", + 11 + ], + [ + "AbbACbab", + "ccAcA", + 12 + ], + [ + "AbbAcA", + "CabbA", + 7 + ], + [ + "AbbB", + "b", + 6 + ], + [ + "AbbBbCbAC", + "bCCaBA", + 13 + ], + [ + "AbbBbb", + "BBcAbaAbB", + 11 + ], + [ + "AbbBcaaBa", + "aAAAaCAba", + 13 + ], + [ + "AbbCACaB", + "CbBBAbcB", + 9 + ], + [ + "AbbCB", + "AAC", + 6 + ], + [ + "AbbCBbCA", + "aAa", + 14 + ], + [ + "AbbCCA", + "bBBcbaAcC", + 13 + ], + [ + "AbbCCaBCB", + "AabbCB", + 9 + ], + [ + "AbbCCcAAb", + "ABCbbBCC", + 13 + ], + [ + "AbbCaCCa", + "B", + 15 + ], + [ + "AbbCbBC", + "Cc", + 11 + ], + [ + "AbbCcA", + "BaaCcBac", + 11 + ], + [ + "AbbaBacBC", + "BaCaCBcC", + 10 + ], + [ + "Abbaa", + "aCbBbc", + 8 + ], + [ + "Abbb", + "bBab", + 5 + ], + [ + "Abbbba", + "BbbaA", + 6 + ], + [ + "AbbcBbA", + "cCbCcaC", + 11 + ], + [ + "Abc", + "CBAaABCCc", + 13 + ], + [ + "Abc", + "Cab", + 5 + ], + [ + "Abc", + "ba", + 4 + ], + [ + "Abc", + "cCBCa", + 8 + ], + [ + "AbcAAAa", + "CBcBBAabc", + 11 + ], + [ + "AbcABaa", + "cCcBCb", + 10 + ], + [ + "AbcAcA", + "B", + 11 + ], + [ + "AbcB", + "AbaCBBaA", + 9 + ], + [ + "AbcBAA", + "BAAb", + 8 + ], + [ + "AbcBBaBB", + "CCBACAA", + 13 + ], + [ + "AbcBCcAA", + "abbABCcCa", + 8 + ], + [ + "AbcCACAbB", + "aBcCb", + 10 + ], + [ + "AbcCAacab", + "a", + 16 + ], + [ + "AbcCa", + "cBAaab", + 9 + ], + [ + "AbcCbbc", + "ACccaBbAB", + 10 + ], + [ + "AbcaA", + "cac", + 6 + ], + [ + "AbcaBABac", + "aA", + 14 + ], + [ + "AbcaBbBcA", + "CBbA", + 11 + ], + [ + "AbcaCAAAB", + "ac", + 15 + ], + [ + "AbcacabA", + "ABbaAC", + 10 + ], + [ + "Abcb", + "AbCCcbb", + 6 + ], + [ + "AbcbA", + "b", + 8 + ], + [ + "AbcbABc", + "bCBCabbBb", + 12 + ], + [ + "AbcbB", + "A", + 8 + ], + [ + "AbcbbC", + "bABbBAa", + 10 + ], + [ + "Abcc", + "bAABaA", + 9 + ], + [ + "Abcc", + "cAABAAa", + 11 + ], + [ + "AbccB", + "cA", + 8 + ], + [ + "Abccc", + "bba", + 8 + ], + [ + "Ac", + "ACbAa", + 7 + ], + [ + "Ac", + "AbB", + 4 + ], + [ + "Ac", + "BCB", + 5 + ], + [ + "Ac", + "BCacCBaBb", + 15 + ], + [ + "Ac", + "BaaCB", + 8 + ], + [ + "Ac", + "BacBbABCa", + 15 + ], + [ + "Ac", + "Bacc", + 5 + ], + [ + "Ac", + "BbaBaACcC", + 14 + ], + [ + "Ac", + "BccaAa", + 10 + ], + [ + "Ac", + "CA", + 4 + ], + [ + "Ac", + "CABacA", + 8 + ], + [ + "Ac", + "CABbbCA", + 11 + ], + [ + "Ac", + "CAa", + 4 + ], + [ + "Ac", + "CBCaAcBaB", + 14 + ], + [ + "Ac", + "CCA", + 5 + ], + [ + "Ac", + "CCcbBBc", + 12 + ], + [ + "Ac", + "CbBBBBbBA", + 18 + ], + [ + "Ac", + "Cbba", + 8 + ], + [ + "Ac", + "aCaa", + 6 + ], + [ + "Ac", + "aCbca", + 7 + ], + [ + "Ac", + "aaBC", + 6 + ], + [ + "Ac", + "aaCAa", + 8 + ], + [ + "Ac", + "aaaA", + 7 + ], + [ + "Ac", + "aabca", + 7 + ], + [ + "Ac", + "abcAAaB", + 11 + ], + [ + "Ac", + "acaBBbaAC", + 15 + ], + [ + "Ac", + "bAaa", + 6 + ], + [ + "Ac", + "bCCCCC", + 11 + ], + [ + "Ac", + "baAcB", + 6 + ], + [ + "Ac", + "bc", + 2 + ], + [ + "Ac", + "cB", + 4 + ], + [ + "Ac", + "cBaaaAAAb", + 16 + ], + [ + "Ac", + "cC", + 3 + ], + [ + "Ac", + "cCAcbCA", + 10 + ], + [ + "Ac", + "cCcCAAA", + 12 + ], + [ + "Ac", + "cbAAcc", + 8 + ], + [ + "AcA", + "ABAACCbc", + 12 + ], + [ + "AcA", + "BBBb", + 8 + ], + [ + "AcA", + "CcabCab", + 11 + ], + [ + "AcA", + "acA", + 1 + ], + [ + "AcA", + "bAbCc", + 7 + ], + [ + "AcA", + "bccBB", + 8 + ], + [ + "AcA", + "cCCA", + 5 + ], + [ + "AcAAAb", + "Acba", + 7 + ], + [ + "AcAABAc", + "bCaa", + 11 + ], + [ + "AcAABBc", + "c", + 12 + ], + [ + "AcAACA", + "cCb", + 8 + ], + [ + "AcAACBb", + "AbaA", + 9 + ], + [ + "AcAAbb", + "abBbA", + 9 + ], + [ + "AcAAcBaB", + "caB", + 10 + ], + [ + "AcABCCba", + "Ca", + 12 + ], + [ + "AcABa", + "aAbBBCA", + 9 + ], + [ + "AcAC", + "c", + 6 + ], + [ + "AcACB", + "aBBCbBabC", + 13 + ], + [ + "AcACCBA", + "c", + 12 + ], + [ + "AcACCaACA", + "aCAAAcABC", + 12 + ], + [ + "AcACaA", + "Ac", + 8 + ], + [ + "AcACcC", + "bABBAcC", + 8 + ], + [ + "AcACccA", + "CaacccB", + 8 + ], + [ + "AcAa", + "b", + 8 + ], + [ + "AcAaaCBA", + "abB", + 12 + ], + [ + "AcAac", + "BBCCC", + 9 + ], + [ + "AcAb", + "cCBBcBCAc", + 14 + ], + [ + "AcAbA", + "cCbbBcb", + 11 + ], + [ + "AcAbBabb", + "BBCBa", + 12 + ], + [ + "AcAbBcABC", + "baCcCcb", + 15 + ], + [ + "AcAbCB", + "ccCCaC", + 10 + ], + [ + "AcAba", + "CcBcBcC", + 11 + ], + [ + "AcAbb", + "bAAbc", + 6 + ], + [ + "AcAcB", + "aAcBAbC", + 8 + ], + [ + "AcAcCB", + "bc", + 10 + ], + [ + "AcAcCCCbb", + "BAaBC", + 14 + ], + [ + "AcAcbacA", + "cb", + 12 + ], + [ + "AcB", + "AbCbCa", + 8 + ], + [ + "AcB", + "CCABca", + 8 + ], + [ + "AcB", + "aBA", + 5 + ], + [ + "AcB", + "b", + 5 + ], + [ + "AcB", + "bCB", + 3 + ], + [ + "AcB", + "cBBaB", + 8 + ], + [ + "AcB", + "caBcC", + 7 + ], + [ + "AcB", + "caCCCaA", + 12 + ], + [ + "AcBAAa", + "ac", + 9 + ], + [ + "AcBAbbAA", + "ACCB", + 12 + ], + [ + "AcBB", + "AAAccBcA", + 10 + ], + [ + "AcBB", + "BaccCCACc", + 15 + ], + [ + "AcBB", + "bBCA", + 8 + ], + [ + "AcBB", + "cCAaaBaBb", + 12 + ], + [ + "AcBBAC", + "AaCA", + 8 + ], + [ + "AcBBACA", + "aAA", + 9 + ], + [ + "AcBBACCaC", + "abBcCAC", + 8 + ], + [ + "AcBBaB", + "aC", + 10 + ], + [ + "AcBBbbaBc", + "CC", + 16 + ], + [ + "AcBBbcb", + "ABBAAc", + 8 + ], + [ + "AcBBcabc", + "a", + 14 + ], + [ + "AcBC", + "ACabcCbBB", + 12 + ], + [ + "AcBC", + "AacAcBCcA", + 10 + ], + [ + "AcBC", + "bCbcAbbA", + 13 + ], + [ + "AcBCAcb", + "AB", + 10 + ], + [ + "AcBCCaAbc", + "ca", + 14 + ], + [ + "AcBCCb", + "aCcCAccc", + 11 + ], + [ + "AcBCaccAA", + "cBAcc", + 9 + ], + [ + "AcBCb", + "aBbaBac", + 11 + ], + [ + "AcBCcbCb", + "AAABCa", + 11 + ], + [ + "AcBCcbbaB", + "bBbC", + 14 + ], + [ + "AcBa", + "BBbCBAA", + 10 + ], + [ + "AcBaCCbCa", + "bbBBb", + 14 + ], + [ + "AcBaCCc", + "aaCcca", + 8 + ], + [ + "AcBaacCb", + "BBbbCbcC", + 13 + ], + [ + "AcBbAb", + "BabcbBbc", + 11 + ], + [ + "AcBbAc", + "cbAbC", + 7 + ], + [ + "AcBbaac", + "bA", + 11 + ], + [ + "AcBbbbB", + "ACaa", + 11 + ], + [ + "AcBc", + "abAcbb", + 7 + ], + [ + "AcBcAcA", + "aaCBAaAAa", + 11 + ], + [ + "AcBcBBB", + "a", + 13 + ], + [ + "AcBcCba", + "BCb", + 8 + ], + [ + "AcBca", + "Ab", + 7 + ], + [ + "AcBcbbA", + "ABb", + 8 + ], + [ + "AcBcbbA", + "BB", + 11 + ], + [ + "AcBcbcA", + "B", + 12 + ], + [ + "AcBccbbbC", + "CbBAAbB", + 13 + ], + [ + "AcBcccC", + "b", + 13 + ], + [ + "AcC", + "ACAbc", + 6 + ], + [ + "AcC", + "bBBA", + 8 + ], + [ + "AcC", + "bbCBACbcA", + 14 + ], + [ + "AcC", + "c", + 4 + ], + [ + "AcC", + "cBBa", + 8 + ], + [ + "AcCACC", + "bbAcCCcc", + 8 + ], + [ + "AcCACaCA", + "CBcCCB", + 11 + ], + [ + "AcCAb", + "bCBAACBC", + 12 + ], + [ + "AcCAbA", + "Bac", + 11 + ], + [ + "AcCAbcaBb", + "Cb", + 14 + ], + [ + "AcCAcCCCA", + "AcCC", + 10 + ], + [ + "AcCBA", + "a", + 9 + ], + [ + "AcCBBaBb", + "AC", + 12 + ], + [ + "AcCBba", + "CaaBCAa", + 10 + ], + [ + "AcCCAA", + "BcA", + 8 + ], + [ + "AcCCBcc", + "bbBACCa", + 13 + ], + [ + "AcCCCbb", + "AcaAc", + 9 + ], + [ + "AcCCb", + "bb", + 8 + ], + [ + "AcCCba", + "BC", + 10 + ], + [ + "AcCCcBACB", + "cb", + 15 + ], + [ + "AcCCccCB", + "baaCaAaB", + 12 + ], + [ + "AcCa", + "AAaCcC", + 8 + ], + [ + "AcCaAcccA", + "AaaaBaCBB", + 13 + ], + [ + "AcCaBCACC", + "a", + 16 + ], + [ + "AcCaC", + "CAcC", + 6 + ], + [ + "AcCaa", + "Ccab", + 6 + ], + [ + "AcCabb", + "BbCBa", + 9 + ], + [ + "AcCb", + "A", + 6 + ], + [ + "AcCbA", + "BaCcaC", + 9 + ], + [ + "AcCbA", + "aA", + 7 + ], + [ + "AcCbBb", + "CCAcb", + 7 + ], + [ + "AcCba", + "cBBbCbCc", + 12 + ], + [ + "AcCbac", + "cCBa", + 5 + ], + [ + "AcCbbbc", + "BbBbABCBc", + 12 + ], + [ + "AcCbc", + "ABBaaAc", + 10 + ], + [ + "AcCc", + "BBBAA", + 10 + ], + [ + "AcCc", + "CABBBCaCb", + 13 + ], + [ + "AcCc", + "acCbACCcc", + 11 + ], + [ + "AcCcA", + "AbCbabBc", + 11 + ], + [ + "AcCcABb", + "cCBaAc", + 9 + ], + [ + "AcCcBB", + "AAaCAaA", + 10 + ], + [ + "AcCcabBCB", + "BcA", + 15 + ], + [ + "AcCcb", + "bC", + 8 + ], + [ + "AcCccABB", + "bbCbbCB", + 12 + ], + [ + "AcCccbBBb", + "ACABAa", + 12 + ], + [ + "Aca", + "BaAA", + 6 + ], + [ + "Aca", + "CCBCCB", + 11 + ], + [ + "Aca", + "cA", + 3 + ], + [ + "AcaAAA", + "CcbbaB", + 9 + ], + [ + "AcaABAaCb", + "CcCACC", + 12 + ], + [ + "AcaABCa", + "bacAcA", + 9 + ], + [ + "AcaAcBC", + "ccbAaa", + 10 + ], + [ + "AcaBACCb", + "ACacBaBAb", + 8 + ], + [ + "AcaBAabCb", + "ACACAbaCB", + 9 + ], + [ + "AcaBC", + "ABacC", + 4 + ], + [ + "AcaBCa", + "AbBC", + 6 + ], + [ + "AcaBacc", + "CBB", + 11 + ], + [ + "AcaBcba", + "AbAbCBcC", + 10 + ], + [ + "AcaC", + "Aca", + 2 + ], + [ + "AcaCB", + "C", + 8 + ], + [ + "AcaCBB", + "cbacccC", + 11 + ], + [ + "AcaCBb", + "CCcaBC", + 8 + ], + [ + "AcaCa", + "bCA", + 7 + ], + [ + "AcaCaC", + "BB", + 12 + ], + [ + "AcaCbcC", + "aBBCccaC", + 9 + ], + [ + "AcaCc", + "CaaCBBb", + 10 + ], + [ + "AcaCc", + "aBaCb", + 5 + ], + [ + "AcaCcAc", + "acA", + 8 + ], + [ + "Acaa", + "BACcCA", + 7 + ], + [ + "AcaaA", + "bacbCca", + 10 + ], + [ + "Acaaa", + "ACccca", + 6 + ], + [ + "AcaaaaaBA", + "b", + 17 + ], + [ + "AcaaabbAC", + "AACbc", + 12 + ], + [ + "AcaabBaAc", + "aCAbBBCaA", + 10 + ], + [ + "AcaacaAAB", + "CA", + 15 + ], + [ + "AcaacbaC", + "A", + 14 + ], + [ + "Acab", + "AB", + 5 + ], + [ + "AcabA", + "BCcca", + 8 + ], + [ + "AcabBAcB", + "B", + 14 + ], + [ + "AcabCaab", + "cCCaBB", + 9 + ], + [ + "AcabcC", + "AC", + 8 + ], + [ + "Acabca", + "cCCC", + 9 + ], + [ + "AcacaAbbb", + "abB", + 13 + ], + [ + "AcacaBC", + "bbaCB", + 9 + ], + [ + "AcacaCCCc", + "aBbbcACA", + 14 + ], + [ + "AcaccB", + "aaBc", + 7 + ], + [ + "Acb", + "BcbA", + 4 + ], + [ + "Acb", + "BccCaB", + 9 + ], + [ + "Acb", + "aC", + 4 + ], + [ + "Acb", + "bAccAa", + 8 + ], + [ + "Acb", + "ccAcbaB", + 8 + ], + [ + "AcbA", + "cA", + 4 + ], + [ + "AcbAAcaA", + "Cba", + 11 + ], + [ + "AcbAB", + "cbBbAa", + 8 + ], + [ + "AcbACBAaA", + "cc", + 15 + ], + [ + "AcbB", + "C", + 7 + ], + [ + "AcbBa", + "aAABbACcB", + 13 + ], + [ + "AcbBaCb", + "bCcacBbA", + 12 + ], + [ + "AcbBaba", + "CbbCBaB", + 9 + ], + [ + "AcbBb", + "abBbA", + 5 + ], + [ + "AcbC", + "bbCaA", + 8 + ], + [ + "AcbCAC", + "CcACABBb", + 10 + ], + [ + "AcbCAc", + "aaBCaAbAa", + 12 + ], + [ + "AcbCBcc", + "aA", + 13 + ], + [ + "AcbCaCb", + "ccAABC", + 10 + ], + [ + "AcbCcBCa", + "bbbcAcB", + 11 + ], + [ + "AcbCcbCa", + "CcaABBcCa", + 11 + ], + [ + "Acba", + "aACbbBcc", + 11 + ], + [ + "AcbaCAA", + "bAaBbaca", + 10 + ], + [ + "Acbab", + "acBc", + 6 + ], + [ + "AcbbAcCa", + "cCb", + 12 + ], + [ + "AcbbC", + "B", + 9 + ], + [ + "Acbbb", + "cCbCAACA", + 13 + ], + [ + "AcbbcAB", + "cca", + 9 + ], + [ + "Acbc", + "AbCaCB", + 8 + ], + [ + "Acbc", + "Bac", + 6 + ], + [ + "AcbcBB", + "ABBBB", + 5 + ], + [ + "AcbcBcA", + "AaaBaba", + 11 + ], + [ + "AcbcCbcbA", + "ac", + 15 + ], + [ + "Acc", + "BbBCABBb", + 14 + ], + [ + "Acc", + "aAbBbBca", + 12 + ], + [ + "Acc", + "aa", + 5 + ], + [ + "Acc", + "aaBcC", + 6 + ], + [ + "Acc", + "bCbAaCab", + 13 + ], + [ + "AccA", + "BCBA", + 5 + ], + [ + "AccA", + "aC", + 6 + ], + [ + "AccAB", + "Bcb", + 7 + ], + [ + "AccABcbBC", + "AAcBcAcc", + 9 + ], + [ + "AccACBbA", + "c", + 14 + ], + [ + "AccAbcC", + "cbbB", + 10 + ], + [ + "AccBbab", + "bcAaaaA", + 10 + ], + [ + "AccC", + "BBbca", + 8 + ], + [ + "AccCBBACa", + "b", + 17 + ], + [ + "AccCCCcaa", + "aabC", + 15 + ], + [ + "AccCcAA", + "CCbAaABCA", + 13 + ], + [ + "Acca", + "C", + 7 + ], + [ + "Acca", + "aB", + 7 + ], + [ + "AccaCABA", + "CBBBc", + 13 + ], + [ + "AccaaAbbC", + "BaAB", + 13 + ], + [ + "Accb", + "baBaaCaCa", + 15 + ], + [ + "Accbbbb", + "bA", + 12 + ], + [ + "Accbcc", + "aaAAcb", + 9 + ], + [ + "Accc", + "ABbAB", + 8 + ], + [ + "AcccAC", + "BAAbCacBb", + 13 + ], + [ + "AcccAbbB", + "BaBCa", + 14 + ], + [ + "AcccBAAbC", + "CCbCb", + 13 + ], + [ + "AcccCACac", + "BAbccaC", + 11 + ], + [ + "AcccCabab", + "bBAAaBaA", + 13 + ], + [ + "AcccCb", + "BACacbcC", + 9 + ], + [ + "AcccaCb", + "CBBCcaCB", + 8 + ], + [ + "Acccac", + "acCaAcaCb", + 9 + ], + [ + "B", + "A", + 2 + ], + [ + "B", + "AAABB", + 8 + ], + [ + "B", + "AAAcc", + 10 + ], + [ + "B", + "AABBcC", + 10 + ], + [ + "B", + "AABCBb", + 10 + ], + [ + "B", + "AAC", + 6 + ], + [ + "B", + "AACA", + 8 + ], + [ + "B", + "AACaaaa", + 14 + ], + [ + "B", + "AAcaBa", + 10 + ], + [ + "B", + "AAcacbab", + 15 + ], + [ + "B", + "ABBc", + 6 + ], + [ + "B", + "ABCBcBCB", + 14 + ], + [ + "B", + "ABaAba", + 10 + ], + [ + "B", + "ABaB", + 6 + ], + [ + "B", + "ABaBba", + 10 + ], + [ + "B", + "ABaaCcBa", + 14 + ], + [ + "B", + "ABc", + 4 + ], + [ + "B", + "ACB", + 4 + ], + [ + "B", + "ACCCCCBc", + 14 + ], + [ + "B", + "ACbbC", + 9 + ], + [ + "B", + "ACbbCA", + 11 + ], + [ + "B", + "AaA", + 6 + ], + [ + "B", + "AaACbbaaB", + 16 + ], + [ + "B", + "AaAaCCbaa", + 17 + ], + [ + "B", + "AaBaBcba", + 14 + ], + [ + "B", + "AabcAaa", + 13 + ], + [ + "B", + "AacCacAb", + 15 + ], + [ + "B", + "AaccbBacB", + 16 + ], + [ + "B", + "Ab", + 3 + ], + [ + "B", + "AbBc", + 6 + ], + [ + "B", + "AbaBCcaBa", + 16 + ], + [ + "B", + "Abc", + 5 + ], + [ + "B", + "AcA", + 6 + ], + [ + "B", + "AcBA", + 6 + ], + [ + "B", + "AcBaBbCc", + 14 + ], + [ + "B", + "AcCc", + 8 + ], + [ + "B", + "AcaC", + 8 + ], + [ + "B", + "AccBacc", + 12 + ], + [ + "B", + "B", + 0 + ], + [ + "B", + "BAA", + 4 + ], + [ + "B", + "BBAbCc", + 10 + ], + [ + "B", + "BBAccA", + 10 + ], + [ + "B", + "BBBCb", + 8 + ], + [ + "B", + "BBa", + 4 + ], + [ + "B", + "BBaAcacab", + 16 + ], + [ + "B", + "BBc", + 4 + ], + [ + "B", + "BC", + 2 + ], + [ + "B", + "BCC", + 4 + ], + [ + "B", + "BCCbaBA", + 12 + ], + [ + "B", + "BCbCb", + 8 + ], + [ + "B", + "BCcC", + 6 + ], + [ + "B", + "BCcCccC", + 12 + ], + [ + "B", + "BCcb", + 6 + ], + [ + "B", + "BCcc", + 6 + ], + [ + "B", + "Ba", + 2 + ], + [ + "B", + "BaBCAaCAa", + 16 + ], + [ + "B", + "Baca", + 6 + ], + [ + "B", + "BacbcaCaC", + 16 + ], + [ + "B", + "BbcCCbAbb", + 16 + ], + [ + "B", + "BcACbAAC", + 14 + ], + [ + "B", + "BcACbbbc", + 14 + ], + [ + "B", + "BcabCBbab", + 16 + ], + [ + "B", + "C", + 2 + ], + [ + "B", + "CAAb", + 7 + ], + [ + "B", + "CAAba", + 9 + ], + [ + "B", + "CACcA", + 10 + ], + [ + "B", + "CB", + 2 + ], + [ + "B", + "CBCcAcbb", + 14 + ], + [ + "B", + "CBc", + 4 + ], + [ + "B", + "CBcA", + 6 + ], + [ + "B", + "CBcCC", + 8 + ], + [ + "B", + "CCaaCaac", + 16 + ], + [ + "B", + "CCcBBCb", + 12 + ], + [ + "B", + "Ca", + 4 + ], + [ + "B", + "CaCC", + 8 + ], + [ + "B", + "Caa", + 6 + ], + [ + "B", + "Cab", + 5 + ], + [ + "B", + "Cacbc", + 9 + ], + [ + "B", + "Cb", + 3 + ], + [ + "B", + "Cbc", + 5 + ], + [ + "B", + "CbcaAaACC", + 17 + ], + [ + "B", + "CbccCbca", + 15 + ], + [ + "B", + "Cc", + 4 + ], + [ + "B", + "CcAacCAA", + 16 + ], + [ + "B", + "CcBb", + 6 + ], + [ + "B", + "CcbBb", + 8 + ], + [ + "B", + "Ccc", + 6 + ], + [ + "B", + "a", + 2 + ], + [ + "B", + "aAA", + 6 + ], + [ + "B", + "aAACAb", + 11 + ], + [ + "B", + "aABCB", + 8 + ], + [ + "B", + "aACaCCBbA", + 16 + ], + [ + "B", + "aB", + 2 + ], + [ + "B", + "aBBbc", + 8 + ], + [ + "B", + "aBabc", + 8 + ], + [ + "B", + "aBcbCA", + 10 + ], + [ + "B", + "aBccCCBC", + 14 + ], + [ + "B", + "aC", + 4 + ], + [ + "B", + "aCBBAAc", + 12 + ], + [ + "B", + "aCBcA", + 8 + ], + [ + "B", + "aCCaAabB", + 14 + ], + [ + "B", + "aCCcbbac", + 15 + ], + [ + "B", + "aCa", + 6 + ], + [ + "B", + "aCaBABACa", + 16 + ], + [ + "B", + "aCcbCA", + 11 + ], + [ + "B", + "aaBBac", + 10 + ], + [ + "B", + "aaBCC", + 8 + ], + [ + "B", + "aab", + 5 + ], + [ + "B", + "aabBcbBa", + 14 + ], + [ + "B", + "aacbAB", + 10 + ], + [ + "B", + "abA", + 5 + ], + [ + "B", + "abAAA", + 9 + ], + [ + "B", + "abBBCBBcb", + 16 + ], + [ + "B", + "abC", + 5 + ], + [ + "B", + "abCcacAC", + 15 + ], + [ + "B", + "ac", + 4 + ], + [ + "B", + "acAbAB", + 10 + ], + [ + "B", + "acBcBba", + 12 + ], + [ + "B", + "b", + 1 + ], + [ + "B", + "bA", + 3 + ], + [ + "B", + "bAAbcbb", + 13 + ], + [ + "B", + "bABAacc", + 12 + ], + [ + "B", + "bACA", + 7 + ], + [ + "B", + "bAaAbBbcb", + 16 + ], + [ + "B", + "bAc", + 5 + ], + [ + "B", + "bB", + 2 + ], + [ + "B", + "bC", + 3 + ], + [ + "B", + "bCAbccaCA", + 17 + ], + [ + "B", + "bCCA", + 7 + ], + [ + "B", + "bCabCB", + 10 + ], + [ + "B", + "bCabcCa", + 13 + ], + [ + "B", + "ba", + 3 + ], + [ + "B", + "baba", + 7 + ], + [ + "B", + "babaa", + 9 + ], + [ + "B", + "bb", + 3 + ], + [ + "B", + "bbc", + 5 + ], + [ + "B", + "bbcbCCCb", + 15 + ], + [ + "B", + "bc", + 3 + ], + [ + "B", + "bcCBCacb", + 14 + ], + [ + "B", + "bcaAcbbB", + 14 + ], + [ + "B", + "c", + 2 + ], + [ + "B", + "cABAAb", + 10 + ], + [ + "B", + "cAaaACc", + 14 + ], + [ + "B", + "cAcCA", + 10 + ], + [ + "B", + "cAccC", + 10 + ], + [ + "B", + "cB", + 2 + ], + [ + "B", + "cBBBa", + 8 + ], + [ + "B", + "cBaCCba", + 12 + ], + [ + "B", + "cBbbaAaBC", + 16 + ], + [ + "B", + "cBcCaA", + 10 + ], + [ + "B", + "cC", + 4 + ], + [ + "B", + "cCABB", + 8 + ], + [ + "B", + "cCBAacA", + 12 + ], + [ + "B", + "cCbBBAccB", + 16 + ], + [ + "B", + "cCbcaBaca", + 16 + ], + [ + "B", + "cCcBaC", + 10 + ], + [ + "B", + "cCcbcaCcc", + 17 + ], + [ + "B", + "cabBca", + 10 + ], + [ + "B", + "cabccaAAB", + 16 + ], + [ + "B", + "cb", + 3 + ], + [ + "B", + "cbAACcCCb", + 17 + ], + [ + "B", + "cbAC", + 7 + ], + [ + "B", + "cbBbCC", + 10 + ], + [ + "B", + "cbC", + 5 + ], + [ + "B", + "cbCaba", + 11 + ], + [ + "B", + "cbCbBBaa", + 14 + ], + [ + "B", + "cbaaAAA", + 13 + ], + [ + "B", + "cbaaCAb", + 13 + ], + [ + "B", + "cbbCaccA", + 15 + ], + [ + "B", + "cbbb", + 7 + ], + [ + "B", + "cbcCa", + 9 + ], + [ + "B", + "ccABaB", + 10 + ], + [ + "B", + "ccAc", + 8 + ], + [ + "B", + "ccCbCbc", + 13 + ], + [ + "B", + "ccabaC", + 11 + ], + [ + "B", + "ccb", + 5 + ], + [ + "BA", + "A", + 2 + ], + [ + "BA", + "ABaabAbAc", + 14 + ], + [ + "BA", + "B", + 2 + ], + [ + "BA", + "BBABbCAaB", + 14 + ], + [ + "BA", + "BBcaC", + 7 + ], + [ + "BA", + "BBcacAbcb", + 14 + ], + [ + "BA", + "BCbCbbcAB", + 14 + ], + [ + "BA", + "CACCAaac", + 14 + ], + [ + "BA", + "CAaaacb", + 12 + ], + [ + "BA", + "CBC", + 4 + ], + [ + "BA", + "aACbbcba", + 14 + ], + [ + "BA", + "ab", + 4 + ], + [ + "BA", + "abCAa", + 7 + ], + [ + "BA", + "b", + 3 + ], + [ + "BA", + "bABaAca", + 10 + ], + [ + "BA", + "baCCBbC", + 12 + ], + [ + "BA", + "bbBcB", + 8 + ], + [ + "BA", + "bbbCC", + 9 + ], + [ + "BA", + "bbbaBA", + 8 + ], + [ + "BA", + "bbbbCBb", + 12 + ], + [ + "BA", + "cBABaC", + 8 + ], + [ + "BA", + "cBBbbA", + 8 + ], + [ + "BA", + "cC", + 4 + ], + [ + "BA", + "ccB", + 6 + ], + [ + "BA", + "ccCa", + 7 + ], + [ + "BAA", + "A", + 4 + ], + [ + "BAA", + "abA", + 4 + ], + [ + "BAA", + "acCAc", + 8 + ], + [ + "BAA", + "bB", + 5 + ], + [ + "BAA", + "bbcACCCAB", + 13 + ], + [ + "BAA", + "bcBC", + 7 + ], + [ + "BAA", + "cBaCaBc", + 10 + ], + [ + "BAAA", + "B", + 6 + ], + [ + "BAAA", + "BBCacaC", + 10 + ], + [ + "BAAA", + "bBAc", + 5 + ], + [ + "BAAAAAA", + "CbcbAc", + 12 + ], + [ + "BAAAACb", + "cbcACabBc", + 13 + ], + [ + "BAAAC", + "cbCBcbCC", + 12 + ], + [ + "BAAAaCCAB", + "bcCB", + 12 + ], + [ + "BAAAabC", + "CCaaaAcc", + 11 + ], + [ + "BAAAbcBa", + "aCcAAAcc", + 12 + ], + [ + "BAABC", + "bccab", + 9 + ], + [ + "BAABaC", + "Cbb", + 11 + ], + [ + "BAABabAb", + "ACC", + 14 + ], + [ + "BAAC", + "bB", + 7 + ], + [ + "BAAC", + "ccaBab", + 10 + ], + [ + "BAAC", + "ccbC", + 6 + ], + [ + "BAACAA", + "a", + 11 + ], + [ + "BAACaC", + "Cac", + 7 + ], + [ + "BAAaA", + "a", + 8 + ], + [ + "BAAaACaC", + "bb", + 15 + ], + [ + "BAAaB", + "ccAcbA", + 9 + ], + [ + "BAAaa", + "b", + 9 + ], + [ + "BAAaa", + "cacCaBc", + 11 + ], + [ + "BAAaaac", + "c", + 12 + ], + [ + "BAAaabB", + "aaAAA", + 9 + ], + [ + "BAAaac", + "BCaCcCAB", + 12 + ], + [ + "BAAacA", + "ccBAcCAc", + 11 + ], + [ + "BAAb", + "AA", + 4 + ], + [ + "BAAbAC", + "BbAccCba", + 10 + ], + [ + "BAAbCbbC", + "CCCBBc", + 11 + ], + [ + "BAAbacbC", + "aBAabc", + 9 + ], + [ + "BAAbbACC", + "CCABb", + 11 + ], + [ + "BAAcCBa", + "ACcA", + 9 + ], + [ + "BAAcaBBCa", + "AccB", + 12 + ], + [ + "BAAcac", + "Bb", + 10 + ], + [ + "BAAcc", + "CACbabA", + 11 + ], + [ + "BAB", + "AcbBaaBa", + 11 + ], + [ + "BAB", + "BB", + 2 + ], + [ + "BAB", + "Caa", + 5 + ], + [ + "BAB", + "cc", + 6 + ], + [ + "BABA", + "abbccc", + 11 + ], + [ + "BABA", + "caCca", + 8 + ], + [ + "BABB", + "bBCCBcAcb", + 13 + ], + [ + "BABBAAc", + "caAaa", + 10 + ], + [ + "BABBbCA", + "bAca", + 9 + ], + [ + "BABBc", + "BCCcAcCC", + 11 + ], + [ + "BABCB", + "AB", + 6 + ], + [ + "BABCcBbA", + "CBb", + 10 + ], + [ + "BABa", + "abbbaccAa", + 14 + ], + [ + "BABabAB", + "b", + 12 + ], + [ + "BABacA", + "abcBA", + 8 + ], + [ + "BABb", + "cC", + 8 + ], + [ + "BABbC", + "BaA", + 7 + ], + [ + "BABbb", + "Ccc", + 10 + ], + [ + "BABc", + "aB", + 5 + ], + [ + "BABcACCaC", + "CbbBaCbaC", + 10 + ], + [ + "BABcBA", + "CA", + 9 + ], + [ + "BABcBCaBc", + "CBAaBCC", + 11 + ], + [ + "BABcBcBa", + "bCBACcc", + 11 + ], + [ + "BABcaBB", + "bAaabb", + 7 + ], + [ + "BABcb", + "CcCAaC", + 11 + ], + [ + "BABcbcaa", + "BbbCcAAab", + 11 + ], + [ + "BABccba", + "cCbbbA", + 10 + ], + [ + "BAC", + "BB", + 4 + ], + [ + "BAC", + "BBacAbC", + 8 + ], + [ + "BAC", + "BaacBaC", + 9 + ], + [ + "BAC", + "BbbBaB", + 9 + ], + [ + "BAC", + "CAAaa", + 8 + ], + [ + "BAC", + "CBBbBccbc", + 15 + ], + [ + "BAC", + "b", + 5 + ], + [ + "BAC", + "babAAcA", + 10 + ], + [ + "BACACcCcc", + "cbB", + 16 + ], + [ + "BACB", + "AaCABa", + 7 + ], + [ + "BACBAACba", + "BaA", + 13 + ], + [ + "BACBCAC", + "A", + 12 + ], + [ + "BACBaB", + "Bcbc", + 8 + ], + [ + "BACBbCbB", + "ACaAAaCb", + 12 + ], + [ + "BACC", + "ab", + 7 + ], + [ + "BACCBcbaa", + "ab", + 15 + ], + [ + "BACCCAaCA", + "bCCcBb", + 12 + ], + [ + "BACCbABA", + "AAAAcAB", + 10 + ], + [ + "BACCbAac", + "AcBCaacB", + 10 + ], + [ + "BACCc", + "BbAAcA", + 7 + ], + [ + "BACCc", + "aBbbBBCa", + 12 + ], + [ + "BACaC", + "CB", + 8 + ], + [ + "BACab", + "bCA", + 6 + ], + [ + "BACb", + "AcBC", + 6 + ], + [ + "BACbAbCbC", + "BbC", + 12 + ], + [ + "BACbbaaAc", + "ACabC", + 11 + ], + [ + "BACcBbcCb", + "AcbB", + 11 + ], + [ + "BACcbBbc", + "AC", + 12 + ], + [ + "BACccabb", + "bAcAAa", + 10 + ], + [ + "BACcccA", + "aacC", + 10 + ], + [ + "BAa", + "AC", + 4 + ], + [ + "BAa", + "AbCBcA", + 9 + ], + [ + "BAa", + "CabbAabb", + 11 + ], + [ + "BAa", + "cBaAa", + 4 + ], + [ + "BAaA", + "BCB", + 6 + ], + [ + "BAaABbB", + "ABabABcbC", + 9 + ], + [ + "BAaAbCc", + "acb", + 10 + ], + [ + "BAaAcCb", + "AbbC", + 10 + ], + [ + "BAaAcbB", + "Aa", + 10 + ], + [ + "BAaBBaAb", + "bBaBcaAAa", + 9 + ], + [ + "BAaBaB", + "A", + 10 + ], + [ + "BAaBbCc", + "cacBaAcaB", + 13 + ], + [ + "BAaBbabc", + "A", + 14 + ], + [ + "BAaBcCaaa", + "ab", + 15 + ], + [ + "BAaBcCb", + "aCAA", + 11 + ], + [ + "BAaCB", + "BBb", + 7 + ], + [ + "BAaCB", + "aABACA", + 7 + ], + [ + "BAaCaA", + "acB", + 9 + ], + [ + "BAaCabCCa", + "b", + 16 + ], + [ + "BAaCbAC", + "bA", + 10 + ], + [ + "BAaCbB", + "aCcbbAAA", + 13 + ], + [ + "BAaCcc", + "bc", + 9 + ], + [ + "BAaaACaA", + "Ccbcaa", + 12 + ], + [ + "BAaaCCb", + "bCBC", + 11 + ], + [ + "BAab", + "c", + 8 + ], + [ + "BAab", + "ccaba", + 6 + ], + [ + "BAabAbBB", + "ABaBaaBa", + 10 + ], + [ + "BAabCA", + "Cb", + 10 + ], + [ + "BAabCCA", + "CCbBcb", + 11 + ], + [ + "BAac", + "bBbcaca", + 8 + ], + [ + "BAacBb", + "cABAca", + 9 + ], + [ + "BAacbBbCb", + "bBAca", + 13 + ], + [ + "BAacbaBC", + "ABCbcaCAc", + 12 + ], + [ + "BAaccAba", + "BAbBbCA", + 11 + ], + [ + "BAb", + "Aa", + 4 + ], + [ + "BAb", + "AbaccbCbC", + 14 + ], + [ + "BAb", + "BaCa", + 5 + ], + [ + "BAb", + "CCaBA", + 8 + ], + [ + "BAb", + "CbCb", + 5 + ], + [ + "BAb", + "caAAC", + 8 + ], + [ + "BAb", + "ccCAAAABC", + 15 + ], + [ + "BAbABaBbC", + "CAAcAb", + 11 + ], + [ + "BAbACcA", + "aBaaCcc", + 8 + ], + [ + "BAbAa", + "ABbCbabac", + 11 + ], + [ + "BAbAa", + "c", + 10 + ], + [ + "BAbAc", + "cBc", + 7 + ], + [ + "BAbAcABa", + "ac", + 13 + ], + [ + "BAbAcbB", + "aCAcBB", + 6 + ], + [ + "BAbBA", + "BcaCBcaBa", + 11 + ], + [ + "BAbBAB", + "ABBCCb", + 8 + ], + [ + "BAbBBaB", + "abaA", + 9 + ], + [ + "BAbBCc", + "aaAcAcAAC", + 14 + ], + [ + "BAbBc", + "ABcAAABb", + 10 + ], + [ + "BAbCCbBC", + "bc", + 13 + ], + [ + "BAbCbBacB", + "acBca", + 12 + ], + [ + "BAbCc", + "BBbcBAab", + 11 + ], + [ + "BAbCcB", + "CbacAaaa", + 14 + ], + [ + "BAbCcBB", + "ccaBBcACC", + 14 + ], + [ + "BAba", + "Bc", + 6 + ], + [ + "BAbaBCAB", + "bbCbBa", + 11 + ], + [ + "BAbaBbBc", + "baAC", + 11 + ], + [ + "BAbaCc", + "C", + 10 + ], + [ + "BAbabca", + "cBabaCb", + 8 + ], + [ + "BAbacCaa", + "CCa", + 11 + ], + [ + "BAbb", + "baBaBaB", + 9 + ], + [ + "BAbbBAA", + "ACbcccc", + 12 + ], + [ + "BAbc", + "B", + 6 + ], + [ + "BAbc", + "bABCB", + 5 + ], + [ + "BAbc", + "bcBAb", + 6 + ], + [ + "BAbcAcA", + "ccA", + 8 + ], + [ + "BAbcaBab", + "CaACACbcB", + 13 + ], + [ + "BAbccAcB", + "BA", + 12 + ], + [ + "BAbccaBA", + "CCCcaa", + 10 + ], + [ + "BAc", + "AAAccAa", + 10 + ], + [ + "BAc", + "Bbc", + 2 + ], + [ + "BAc", + "ba", + 4 + ], + [ + "BAcA", + "Ac", + 4 + ], + [ + "BAcAA", + "acbCAA", + 7 + ], + [ + "BAcAACc", + "accA", + 9 + ], + [ + "BAcAAb", + "AbC", + 10 + ], + [ + "BAcAB", + "aAacCcbac", + 13 + ], + [ + "BAcACCb", + "cccBC", + 10 + ], + [ + "BAcACcBA", + "BCAbccA", + 7 + ], + [ + "BAcAcC", + "aAcBb", + 8 + ], + [ + "BAcBa", + "CCAbBCc", + 10 + ], + [ + "BAcBcba", + "aaa", + 11 + ], + [ + "BAcCBCbaA", + "bCb", + 13 + ], + [ + "BAcCC", + "ACCaC", + 5 + ], + [ + "BAcCCBAbA", + "b", + 16 + ], + [ + "BAcCaB", + "cCBBCaa", + 10 + ], + [ + "BAcCbC", + "CAB", + 9 + ], + [ + "BAcCbaC", + "CCBBaCabB", + 14 + ], + [ + "BAcCbc", + "Cb", + 8 + ], + [ + "BAcCcAACa", + "aacaCaA", + 11 + ], + [ + "BAcaBBBc", + "a", + 14 + ], + [ + "BAcaBc", + "CAbaa", + 8 + ], + [ + "BAcaC", + "cAcaCA", + 4 + ], + [ + "BAcaab", + "ACbA", + 8 + ], + [ + "BAcab", + "BaCbca", + 7 + ], + [ + "BAcbA", + "AbA", + 4 + ], + [ + "BAcbA", + "bACBCbbCA", + 10 + ], + [ + "BAcbAcCB", + "ac", + 13 + ], + [ + "BAcbBCC", + "bBaba", + 11 + ], + [ + "BAcbCC", + "c", + 10 + ], + [ + "BAcbCc", + "B", + 10 + ], + [ + "BAcbabcB", + "aAa", + 12 + ], + [ + "BAcbbba", + "Cb", + 11 + ], + [ + "BAcbcABbA", + "baABcBCa", + 10 + ], + [ + "BAccAAA", + "AaaC", + 10 + ], + [ + "BAccAAca", + "BBCbbCcab", + 11 + ], + [ + "BAccAbCB", + "Cba", + 13 + ], + [ + "BAccCAb", + "cCBcCcac", + 11 + ], + [ + "BAccCAbC", + "BCabCBA", + 11 + ], + [ + "BAccCCcA", + "CbbBaCB", + 14 + ], + [ + "BAccCcCB", + "bcCAB", + 9 + ], + [ + "BAcca", + "cBcCBBcbC", + 13 + ], + [ + "BAccaAaC", + "cbBcAaA", + 10 + ], + [ + "BAcccCbb", + "Bb", + 12 + ], + [ + "BB", + "A", + 4 + ], + [ + "BB", + "AACBAacBB", + 14 + ], + [ + "BB", + "ABaA", + 6 + ], + [ + "BB", + "ACbbCcc", + 12 + ], + [ + "BB", + "AbAABC", + 9 + ], + [ + "BB", + "AbbbBBa", + 10 + ], + [ + "BB", + "BC", + 2 + ], + [ + "BB", + "Ba", + 2 + ], + [ + "BB", + "BbcAaACB", + 12 + ], + [ + "BB", + "CAcaACbBb", + 15 + ], + [ + "BB", + "CBabcc", + 9 + ], + [ + "BB", + "CaCBb", + 7 + ], + [ + "BB", + "CaaAAaAAc", + 18 + ], + [ + "BB", + "Cab", + 5 + ], + [ + "BB", + "Cb", + 3 + ], + [ + "BB", + "CbAAC", + 9 + ], + [ + "BB", + "CbcbCcBc", + 13 + ], + [ + "BB", + "Ccccc", + 10 + ], + [ + "BB", + "a", + 4 + ], + [ + "BB", + "aAAbcc", + 11 + ], + [ + "BB", + "aACBABa", + 10 + ], + [ + "BB", + "aACbBCaac", + 15 + ], + [ + "BB", + "aAcCCb", + 11 + ], + [ + "BB", + "aCACcBC", + 12 + ], + [ + "BB", + "aCa", + 6 + ], + [ + "BB", + "aa", + 4 + ], + [ + "BB", + "bAABCCabC", + 15 + ], + [ + "BB", + "bABBCb", + 8 + ], + [ + "BB", + "bBacaAb", + 11 + ], + [ + "BB", + "bc", + 3 + ], + [ + "BB", + "cBBcCBaAa", + 14 + ], + [ + "BB", + "cCaAaBc", + 12 + ], + [ + "BB", + "cCb", + 5 + ], + [ + "BB", + "cCc", + 6 + ], + [ + "BB", + "caBcCA", + 10 + ], + [ + "BB", + "cac", + 6 + ], + [ + "BB", + "cbb", + 4 + ], + [ + "BB", + "ccaC", + 8 + ], + [ + "BBA", + "BB", + 2 + ], + [ + "BBA", + "BCCaaAb", + 10 + ], + [ + "BBA", + "Bb", + 3 + ], + [ + "BBA", + "Bba", + 2 + ], + [ + "BBA", + "CBbBab", + 7 + ], + [ + "BBA", + "aaBbAc", + 7 + ], + [ + "BBA", + "ab", + 5 + ], + [ + "BBA", + "bAAaBCbA", + 11 + ], + [ + "BBA", + "cbcBbC", + 9 + ], + [ + "BBAACb", + "BBaBbb", + 5 + ], + [ + "BBAAb", + "abaBbac", + 10 + ], + [ + "BBAAbbCc", + "a", + 15 + ], + [ + "BBAAcAb", + "cCBCaBCA", + 12 + ], + [ + "BBAAcbba", + "aCACbcCc", + 13 + ], + [ + "BBABA", + "bbccbbb", + 11 + ], + [ + "BBABACCCB", + "c", + 17 + ], + [ + "BBABBAca", + "aCCAAcCA", + 13 + ], + [ + "BBABBaBA", + "CaAaaa", + 11 + ], + [ + "BBABBaaBB", + "CccbccAc", + 16 + ], + [ + "BBABa", + "b", + 9 + ], + [ + "BBABcC", + "aCAA", + 10 + ], + [ + "BBABccb", + "C", + 13 + ], + [ + "BBAC", + "bBCAcBaC", + 9 + ], + [ + "BBACB", + "bAb", + 6 + ], + [ + "BBACC", + "CBccc", + 6 + ], + [ + "BBACC", + "aCBBBB", + 10 + ], + [ + "BBACaBC", + "C", + 12 + ], + [ + "BBACcCbBa", + "CCCcCC", + 12 + ], + [ + "BBAbA", + "AbCaCcB", + 12 + ], + [ + "BBAbCcacC", + "aBaacBcb", + 11 + ], + [ + "BBAba", + "BCbBcB", + 9 + ], + [ + "BBAbcB", + "BBc", + 6 + ], + [ + "BBAbcBBaA", + "bCcAB", + 13 + ], + [ + "BBAbcC", + "BacA", + 7 + ], + [ + "BBAcA", + "bBcaAacbb", + 11 + ], + [ + "BBAcC", + "cC", + 6 + ], + [ + "BBB", + "BaBbB", + 4 + ], + [ + "BBB", + "Bbcbc", + 6 + ], + [ + "BBB", + "CC", + 6 + ], + [ + "BBB", + "CCCAbc", + 11 + ], + [ + "BBB", + "CcbcBc", + 9 + ], + [ + "BBB", + "aaAbb", + 8 + ], + [ + "BBBAAA", + "AA", + 8 + ], + [ + "BBBAAAcB", + "BAcAcCCA", + 11 + ], + [ + "BBBACc", + "AaBCBb", + 10 + ], + [ + "BBBAbAaAB", + "aAcBABA", + 13 + ], + [ + "BBBAccBbC", + "ccbCbAb", + 14 + ], + [ + "BBBB", + "baABcbb", + 9 + ], + [ + "BBBBCCacA", + "bbCCc", + 10 + ], + [ + "BBBBcCA", + "bBAabcB", + 10 + ], + [ + "BBBBccCCC", + "C", + 16 + ], + [ + "BBBCABbb", + "cAAaAB", + 12 + ], + [ + "BBBCab", + "abBACbBc", + 10 + ], + [ + "BBBa", + "CCabbCcaB", + 14 + ], + [ + "BBBa", + "cAa", + 6 + ], + [ + "BBBaCA", + "AAbbaaACB", + 12 + ], + [ + "BBBaaBb", + "cBcaac", + 8 + ], + [ + "BBBab", + "bAa", + 7 + ], + [ + "BBBac", + "aaCcCBcAB", + 15 + ], + [ + "BBBb", + "B", + 6 + ], + [ + "BBBbCCb", + "BbBaCc", + 6 + ], + [ + "BBBba", + "BCaC", + 8 + ], + [ + "BBBbcBcCA", + "BbBB", + 11 + ], + [ + "BBBbccc", + "A", + 14 + ], + [ + "BBBc", + "cCAba", + 9 + ], + [ + "BBBcAB", + "AbCAAbc", + 10 + ], + [ + "BBBcBAa", + "AbBCCa", + 8 + ], + [ + "BBBcBabbB", + "aCC", + 16 + ], + [ + "BBBcbBA", + "AbACCCbCb", + 14 + ], + [ + "BBBcc", + "CCbbabCb", + 12 + ], + [ + "BBBccBaaB", + "BcCB", + 11 + ], + [ + "BBC", + "CaAAAaCbB", + 16 + ], + [ + "BBC", + "Cbcbcc", + 9 + ], + [ + "BBC", + "abBBacA", + 9 + ], + [ + "BBC", + "b", + 5 + ], + [ + "BBC", + "bCBAA", + 7 + ], + [ + "BBC", + "bcCaAbb", + 11 + ], + [ + "BBC", + "cba", + 5 + ], + [ + "BBCA", + "BAcCC", + 6 + ], + [ + "BBCAbbbc", + "BAa", + 12 + ], + [ + "BBCAcb", + "Ca", + 9 + ], + [ + "BBCBb", + "aac", + 9 + ], + [ + "BBCBbc", + "CaaBB", + 9 + ], + [ + "BBCBcaBb", + "ccCcCbBC", + 11 + ], + [ + "BBCBcb", + "CBcCBcacC", + 10 + ], + [ + "BBCC", + "C", + 6 + ], + [ + "BBCCBC", + "aCbbA", + 9 + ], + [ + "BBCCBab", + "cBCaaC", + 8 + ], + [ + "BBCCCbaCB", + "AAA", + 17 + ], + [ + "BBCCaBbB", + "BaBAcCbC", + 11 + ], + [ + "BBCCccBCB", + "bCaaCA", + 13 + ], + [ + "BBCa", + "aB", + 6 + ], + [ + "BBCaa", + "AccACcAAC", + 14 + ], + [ + "BBCabCCBB", + "BcAcCaCC", + 12 + ], + [ + "BBCbB", + "BBb", + 4 + ], + [ + "BBCbB", + "CB", + 6 + ], + [ + "BBCbB", + "caABAb", + 10 + ], + [ + "BBCbc", + "BAAbcaaBc", + 11 + ], + [ + "BBCc", + "BabAb", + 7 + ], + [ + "BBCc", + "cbabab", + 10 + ], + [ + "BBCcbACcB", + "Ab", + 15 + ], + [ + "BBCccCAba", + "aCcCCBAb", + 9 + ], + [ + "BBa", + "AcBcBAb", + 9 + ], + [ + "BBa", + "Ccb", + 6 + ], + [ + "BBa", + "aCCcbCAc", + 14 + ], + [ + "BBa", + "bBaCBa", + 6 + ], + [ + "BBa", + "cbAcBBaA", + 10 + ], + [ + "BBaA", + "bAAaBAAAB", + 12 + ], + [ + "BBaAAccbb", + "CABB", + 14 + ], + [ + "BBaACBaB", + "cbcC", + 13 + ], + [ + "BBaAab", + "BCB", + 9 + ], + [ + "BBaAcAAcb", + "cbCBBCAcb", + 11 + ], + [ + "BBaAcB", + "ACbAcb", + 7 + ], + [ + "BBaBbcB", + "C", + 13 + ], + [ + "BBaCB", + "cC", + 8 + ], + [ + "BBaCCA", + "ABAcc", + 7 + ], + [ + "BBaCccb", + "Cbcbb", + 9 + ], + [ + "BBaa", + "ba", + 5 + ], + [ + "BBaaCCBA", + "AbCbC", + 12 + ], + [ + "BBaaCca", + "aaBBcbAB", + 13 + ], + [ + "BBaaa", + "B", + 8 + ], + [ + "BBaab", + "bbAcbB", + 7 + ], + [ + "BBaabcA", + "BACBAC", + 10 + ], + [ + "BBaacbcaB", + "Abacab", + 10 + ], + [ + "BBaacc", + "C", + 11 + ], + [ + "BBabBa", + "BbBBcAACB", + 13 + ], + [ + "BBabcBab", + "abB", + 10 + ], + [ + "BBabcbb", + "BbbCbA", + 6 + ], + [ + "BBac", + "cBAB", + 5 + ], + [ + "BBacAcaAa", + "Bba", + 13 + ], + [ + "BBacCaBBa", + "aa", + 14 + ], + [ + "BBacCc", + "bcaBA", + 9 + ], + [ + "BBacaABc", + "Cbbaac", + 10 + ], + [ + "BBacb", + "AbbaCBCb", + 9 + ], + [ + "BBacb", + "abCACCBBA", + 14 + ], + [ + "BBb", + "BAcaAaca", + 14 + ], + [ + "BBb", + "BBa", + 2 + ], + [ + "BBb", + "CAcBCAAc", + 14 + ], + [ + "BBb", + "aBACcCCC", + 14 + ], + [ + "BBb", + "acbCc", + 8 + ], + [ + "BBbA", + "ca", + 7 + ], + [ + "BBbAACb", + "ACCbb", + 10 + ], + [ + "BBbABAaAB", + "cAbaCba", + 13 + ], + [ + "BBbABbac", + "cAB", + 12 + ], + [ + "BBbAabbCA", + "A", + 16 + ], + [ + "BBbAbcB", + "AcAAcbABa", + 12 + ], + [ + "BBbAcaAB", + "cBB", + 12 + ], + [ + "BBbAcc", + "bBAb", + 7 + ], + [ + "BBbB", + "ccABABc", + 10 + ], + [ + "BBbBB", + "a", + 10 + ], + [ + "BBbBBbba", + "aACbb", + 12 + ], + [ + "BBbBBcacc", + "AcAa", + 15 + ], + [ + "BBbBCbBc", + "BbABC", + 9 + ], + [ + "BBbBaa", + "bbBbbbaAb", + 9 + ], + [ + "BBbCCCCbc", + "ccaAB", + 15 + ], + [ + "BBbCCcbAc", + "bABcac", + 11 + ], + [ + "BBbCbcb", + "Ab", + 12 + ], + [ + "BBba", + "a", + 6 + ], + [ + "BBbaBc", + "ac", + 8 + ], + [ + "BBbab", + "AcAB", + 8 + ], + [ + "BBbabb", + "CCbAa", + 9 + ], + [ + "BBbabbab", + "bCBBbaA", + 9 + ], + [ + "BBbb", + "BA", + 6 + ], + [ + "BBbbACc", + "A", + 12 + ], + [ + "BBbbCCBB", + "AaCcBAAA", + 15 + ], + [ + "BBbcABBbC", + "AaC", + 14 + ], + [ + "BBbcB", + "ACbA", + 8 + ], + [ + "BBbcCac", + "cbb", + 11 + ], + [ + "BBbccCCBB", + "BbaBcb", + 12 + ], + [ + "BBc", + "ABCbAA", + 9 + ], + [ + "BBc", + "BbbABAACA", + 13 + ], + [ + "BBc", + "BcccAbBaC", + 13 + ], + [ + "BBc", + "aBaCccA", + 10 + ], + [ + "BBc", + "aC", + 5 + ], + [ + "BBc", + "aCacBbCCa", + 14 + ], + [ + "BBcAAA", + "Acbc", + 10 + ], + [ + "BBcAAac", + "acCcAbc", + 9 + ], + [ + "BBcABAA", + "BbBaABaC", + 7 + ], + [ + "BBcB", + "AAacBC", + 8 + ], + [ + "BBcB", + "aAaabB", + 10 + ], + [ + "BBcB", + "acb", + 5 + ], + [ + "BBcB", + "bAcAAbac", + 12 + ], + [ + "BBcBACa", + "CBACA", + 6 + ], + [ + "BBcBCB", + "c", + 10 + ], + [ + "BBcBbCBA", + "cBaaAca", + 12 + ], + [ + "BBcBbcA", + "bBbca", + 6 + ], + [ + "BBcC", + "AcAcAcCc", + 12 + ], + [ + "BBcCBCac", + "ababc", + 12 + ], + [ + "BBcCC", + "ccB", + 7 + ], + [ + "BBcCCabAA", + "BCC", + 12 + ], + [ + "BBcCCcab", + "bCcc", + 10 + ], + [ + "BBcCa", + "BBAA", + 5 + ], + [ + "BBcCcb", + "BCb", + 6 + ], + [ + "BBca", + "b", + 7 + ], + [ + "BBcaA", + "AaCCAbCCC", + 15 + ], + [ + "BBcaA", + "b", + 9 + ], + [ + "BBcaACbB", + "AbCb", + 11 + ], + [ + "BBcaC", + "CCccCCCC", + 12 + ], + [ + "BBcaa", + "aBaa", + 4 + ], + [ + "BBcaa", + "bAAAC", + 8 + ], + [ + "BBcaa", + "ccCCcABA", + 12 + ], + [ + "BBcaaCBB", + "BbCcbC", + 10 + ], + [ + "BBcab", + "aaBc", + 8 + ], + [ + "BBcbA", + "BcBabbA", + 6 + ], + [ + "BBcbaA", + "caBbCcaa", + 9 + ], + [ + "BBcbbaB", + "ccCbbAccB", + 10 + ], + [ + "BBcbbabC", + "BB", + 12 + ], + [ + "BBcc", + "AcbAAB", + 11 + ], + [ + "BBcc", + "CCcBccBbC", + 12 + ], + [ + "BBccAb", + "aCACbAC", + 11 + ], + [ + "BBccB", + "BcB", + 4 + ], + [ + "BBccc", + "BBAbCAcbB", + 11 + ], + [ + "BBcccCAAB", + "ABABBAca", + 14 + ], + [ + "BC", + "A", + 4 + ], + [ + "BC", + "AAcba", + 9 + ], + [ + "BC", + "AC", + 2 + ], + [ + "BC", + "ACBACCA", + 10 + ], + [ + "BC", + "AaA", + 6 + ], + [ + "BC", + "AaCABABAb", + 16 + ], + [ + "BC", + "Ac", + 3 + ], + [ + "BC", + "AcCCcc", + 10 + ], + [ + "BC", + "B", + 2 + ], + [ + "BC", + "BAbC", + 4 + ], + [ + "BC", + "BC", + 0 + ], + [ + "BC", + "BCCCC", + 6 + ], + [ + "BC", + "BbAaaBAbb", + 16 + ], + [ + "BC", + "BbB", + 4 + ], + [ + "BC", + "BcB", + 3 + ], + [ + "BC", + "C", + 2 + ], + [ + "BC", + "CA", + 4 + ], + [ + "BC", + "CaaaCAcaC", + 16 + ], + [ + "BC", + "CbCC", + 5 + ], + [ + "BC", + "CcC", + 4 + ], + [ + "BC", + "CcCCCab", + 12 + ], + [ + "BC", + "CcaAbbbcb", + 16 + ], + [ + "BC", + "CccAb", + 9 + ], + [ + "BC", + "aAA", + 6 + ], + [ + "BC", + "aAbbcbac", + 14 + ], + [ + "BC", + "aCbAaAbBB", + 16 + ], + [ + "BC", + "abbba", + 9 + ], + [ + "BC", + "acAACAC", + 12 + ], + [ + "BC", + "acbCA", + 7 + ], + [ + "BC", + "acccbAAB", + 15 + ], + [ + "BC", + "bAABa", + 8 + ], + [ + "BC", + "bBBAcBbCC", + 14 + ], + [ + "BC", + "bC", + 1 + ], + [ + "BC", + "bCaCaCaa", + 13 + ], + [ + "BC", + "bCbaCa", + 9 + ], + [ + "BC", + "bCbaaaAAC", + 15 + ], + [ + "BC", + "babCAbca", + 13 + ], + [ + "BC", + "babbBBaB", + 14 + ], + [ + "BC", + "bbCA", + 5 + ], + [ + "BC", + "c", + 3 + ], + [ + "BC", + "cAAB", + 8 + ], + [ + "BC", + "cABCBCAB", + 12 + ], + [ + "BC", + "cBaACA", + 8 + ], + [ + "BC", + "cBbaAcB", + 11 + ], + [ + "BC", + "cCCBb", + 8 + ], + [ + "BC", + "cb", + 4 + ], + [ + "BC", + "cbBBB", + 8 + ], + [ + "BC", + "cba", + 5 + ], + [ + "BC", + "cbaB", + 7 + ], + [ + "BC", + "cbaCbBAC", + 12 + ], + [ + "BC", + "cbbCabA", + 11 + ], + [ + "BC", + "ccAACb", + 10 + ], + [ + "BC", + "ccccbb", + 11 + ], + [ + "BCA", + "A", + 4 + ], + [ + "BCA", + "AbcCbCaA", + 11 + ], + [ + "BCA", + "abccbBCC", + 12 + ], + [ + "BCAABABCB", + "abBbBaB", + 11 + ], + [ + "BCAABc", + "bBACcBa", + 9 + ], + [ + "BCAAbBc", + "AaABC", + 8 + ], + [ + "BCAAbbbc", + "A", + 14 + ], + [ + "BCAAcbb", + "CAaCAcBbC", + 9 + ], + [ + "BCAAccAa", + "Aaaa", + 10 + ], + [ + "BCABBA", + "AbBba", + 8 + ], + [ + "BCABBBAa", + "bCAbBA", + 6 + ], + [ + "BCABa", + "b", + 9 + ], + [ + "BCABcB", + "bcAAbCb", + 7 + ], + [ + "BCABcCCc", + "CACcbBAc", + 10 + ], + [ + "BCABccC", + "B", + 12 + ], + [ + "BCABccaB", + "cAaCbBCc", + 13 + ], + [ + "BCAC", + "CAcCB", + 6 + ], + [ + "BCACBaACB", + "aaCaC", + 11 + ], + [ + "BCACBaba", + "BbAaAbc", + 9 + ], + [ + "BCACBb", + "aCbCA", + 8 + ], + [ + "BCACb", + "CAABCa", + 8 + ], + [ + "BCACcABBB", + "C", + 16 + ], + [ + "BCACcCAC", + "aBcAaaB", + 12 + ], + [ + "BCACcb", + "AC", + 8 + ], + [ + "BCAa", + "A", + 6 + ], + [ + "BCAa", + "Cc", + 6 + ], + [ + "BCAacBCB", + "CaCC", + 9 + ], + [ + "BCAbBAbAc", + "aBcbB", + 13 + ], + [ + "BCAbBaCB", + "caCba", + 11 + ], + [ + "BCAbCB", + "ccBcBCC", + 10 + ], + [ + "BCAbaCC", + "aB", + 12 + ], + [ + "BCAbbAcBA", + "A", + 16 + ], + [ + "BCAbbBA", + "BBAAAbac", + 10 + ], + [ + "BCAbc", + "Cb", + 6 + ], + [ + "BCAbcaBCC", + "ccBacBbA", + 13 + ], + [ + "BCAbccbb", + "bAcaCCbAB", + 12 + ], + [ + "BCAc", + "BaBb", + 6 + ], + [ + "BCAcC", + "Ccc", + 5 + ], + [ + "BCAcCabcc", + "CBC", + 14 + ], + [ + "BCAcabbBc", + "bcbC", + 12 + ], + [ + "BCB", + "caa", + 6 + ], + [ + "BCB", + "ccB", + 3 + ], + [ + "BCBA", + "B", + 6 + ], + [ + "BCBA", + "bAB", + 5 + ], + [ + "BCBAACcc", + "aBB", + 14 + ], + [ + "BCBACbAbA", + "ccABAa", + 11 + ], + [ + "BCBACbCb", + "cBA", + 11 + ], + [ + "BCBAaBcA", + "baC", + 12 + ], + [ + "BCBAabB", + "Bcb", + 9 + ], + [ + "BCBAb", + "CccbcCcBb", + 13 + ], + [ + "BCBAcbbC", + "AccaABA", + 13 + ], + [ + "BCBB", + "A", + 8 + ], + [ + "BCBBA", + "ACAC", + 8 + ], + [ + "BCBBACa", + "AbbAA", + 9 + ], + [ + "BCBBbCbC", + "Ccb", + 11 + ], + [ + "BCBBbab", + "cCAccAbb", + 11 + ], + [ + "BCBBcCa", + "BCCCBbA", + 9 + ], + [ + "BCBBcaCBA", + "CbBCCBacA", + 10 + ], + [ + "BCBBccB", + "B", + 12 + ], + [ + "BCBC", + "CbaBa", + 7 + ], + [ + "BCBC", + "cabAcCaCc", + 13 + ], + [ + "BCBCAaB", + "CAcb", + 9 + ], + [ + "BCBCBaAA", + "b", + 15 + ], + [ + "BCBCBbbc", + "CBBA", + 10 + ], + [ + "BCBCbbBC", + "CACcaACaB", + 14 + ], + [ + "BCBCc", + "aBAA", + 8 + ], + [ + "BCBaACb", + "aCB", + 9 + ], + [ + "BCBaAcb", + "AaC", + 11 + ], + [ + "BCBaC", + "BC", + 6 + ], + [ + "BCBaCCabc", + "aAcbC", + 12 + ], + [ + "BCBb", + "AaaBabaAa", + 14 + ], + [ + "BCBbAABB", + "A", + 14 + ], + [ + "BCBbBCaA", + "B", + 14 + ], + [ + "BCBbc", + "AbBba", + 6 + ], + [ + "BCBbcbBC", + "ca", + 14 + ], + [ + "BCBcB", + "bbC", + 7 + ], + [ + "BCBcaACa", + "bbCaabC", + 10 + ], + [ + "BCBcabB", + "aaBaaaAac", + 14 + ], + [ + "BCC", + "AabbC", + 7 + ], + [ + "BCC", + "AbbBBa", + 10 + ], + [ + "BCC", + "CCBcABb", + 11 + ], + [ + "BCCA", + "bcbbc", + 8 + ], + [ + "BCCA", + "cBBbacAc", + 11 + ], + [ + "BCCAAbbC", + "ccCB", + 12 + ], + [ + "BCCABc", + "c", + 10 + ], + [ + "BCCAaba", + "CAbB", + 8 + ], + [ + "BCCB", + "cBca", + 7 + ], + [ + "BCCBCC", + "ACCBaa", + 6 + ], + [ + "BCCBbcAAA", + "aaaCCB", + 16 + ], + [ + "BCCCAbA", + "CabCcbc", + 10 + ], + [ + "BCCCCAB", + "BC", + 10 + ], + [ + "BCCCCBAB", + "cABCcc", + 13 + ], + [ + "BCCCb", + "cCCAc", + 6 + ], + [ + "BCCaAcbB", + "Cabccaa", + 12 + ], + [ + "BCCaB", + "cAaAcAa", + 12 + ], + [ + "BCCaBABCc", + "a", + 16 + ], + [ + "BCCaCBCAA", + "cBbCaccaA", + 9 + ], + [ + "BCCac", + "CbC", + 7 + ], + [ + "BCCbAc", + "AcCAaBcCc", + 12 + ], + [ + "BCCbCbaB", + "caa", + 13 + ], + [ + "BCCbaBBbC", + "baBAB", + 11 + ], + [ + "BCCbb", + "aAAcc", + 10 + ], + [ + "BCCbbb", + "BCaB", + 7 + ], + [ + "BCCbc", + "BBaAbbBCC", + 13 + ], + [ + "BCCbc", + "caBaAbbC", + 11 + ], + [ + "BCCcaAccC", + "bacbC", + 11 + ], + [ + "BCCcbAcBa", + "CA", + 14 + ], + [ + "BCCcbaBc", + "CcCcCAba", + 9 + ], + [ + "BCCccbaC", + "C", + 14 + ], + [ + "BCa", + "CBbbAac", + 10 + ], + [ + "BCa", + "aAbCCb", + 9 + ], + [ + "BCa", + "bbcAB", + 7 + ], + [ + "BCa", + "c", + 5 + ], + [ + "BCaABBaAB", + "acCcBAC", + 13 + ], + [ + "BCaAa", + "AC", + 8 + ], + [ + "BCaAaCac", + "aaAB", + 11 + ], + [ + "BCaAba", + "a", + 10 + ], + [ + "BCaAcAbC", + "B", + 14 + ], + [ + "BCaBAC", + "bCAcCAaB", + 10 + ], + [ + "BCaBcCB", + "BaAA", + 10 + ], + [ + "BCaCAc", + "CBAaBc", + 8 + ], + [ + "BCaCCCcc", + "ccbaBCaC", + 12 + ], + [ + "BCaCbbbc", + "aaAAa", + 14 + ], + [ + "BCaa", + "bc", + 6 + ], + [ + "BCaaAbaB", + "ccCA", + 13 + ], + [ + "BCaaab", + "cAbBBa", + 11 + ], + [ + "BCaaabC", + "Bbbcb", + 10 + ], + [ + "BCaabcbA", + "BabcCAb", + 8 + ], + [ + "BCabaB", + "BaBAbcB", + 7 + ], + [ + "BCabacC", + "a", + 12 + ], + [ + "BCabbbABB", + "BBCcABb", + 10 + ], + [ + "BCabbcCCC", + "ABcCAcbc", + 13 + ], + [ + "BCabcaAAA", + "Baa", + 12 + ], + [ + "BCacBBAAB", + "aBC", + 14 + ], + [ + "BCacBBbB", + "CBCB", + 10 + ], + [ + "BCacCB", + "bB", + 9 + ], + [ + "BCacCba", + "BaBbaAAba", + 10 + ], + [ + "BCb", + "BBa", + 4 + ], + [ + "BCb", + "bBCCa", + 6 + ], + [ + "BCb", + "bCAAAAC", + 11 + ], + [ + "BCbA", + "Bccc", + 5 + ], + [ + "BCbAAC", + "aA", + 9 + ], + [ + "BCbAcbcCB", + "cbBcba", + 11 + ], + [ + "BCbBCacaC", + "AACB", + 16 + ], + [ + "BCbBa", + "cbbBC", + 6 + ], + [ + "BCbBaa", + "aCAAbACc", + 12 + ], + [ + "BCbBca", + "bBaB", + 8 + ], + [ + "BCbCA", + "cB", + 8 + ], + [ + "BCbCccA", + "aaABcCAB", + 11 + ], + [ + "BCbaAABaa", + "AA", + 14 + ], + [ + "BCbaBAb", + "cAAAcbbaA", + 15 + ], + [ + "BCbaCbBAc", + "CBCcabcAb", + 10 + ], + [ + "BCbaaCCaa", + "cCbac", + 11 + ], + [ + "BCbaabbBC", + "abcCBbB", + 11 + ], + [ + "BCbaccCC", + "bbBc", + 11 + ], + [ + "BCbbc", + "AcCbBaACc", + 11 + ], + [ + "BCbbcbbB", + "AbbCca", + 11 + ], + [ + "BCbc", + "cbcCCcBAA", + 13 + ], + [ + "BCbcBbaC", + "babCBCACb", + 9 + ], + [ + "BCbcaACBB", + "aab", + 14 + ], + [ + "BCbccccB", + "Ca", + 14 + ], + [ + "BCc", + "AAaCBcAbc", + 13 + ], + [ + "BCc", + "AcBbBbcba", + 14 + ], + [ + "BCc", + "CAcB", + 6 + ], + [ + "BCc", + "bABBC", + 7 + ], + [ + "BCcA", + "Aac", + 6 + ], + [ + "BCcA", + "bbbCc", + 7 + ], + [ + "BCcACaB", + "CBcbcaBAA", + 11 + ], + [ + "BCcACaCca", + "cca", + 12 + ], + [ + "BCcAcb", + "bbcacbb", + 6 + ], + [ + "BCcBBAccb", + "cBB", + 12 + ], + [ + "BCcBBacB", + "B", + 14 + ], + [ + "BCcBBc", + "ca", + 10 + ], + [ + "BCcBac", + "Cb", + 9 + ], + [ + "BCcBbBAb", + "BBcBBcA", + 7 + ], + [ + "BCcBbBCc", + "A", + 16 + ], + [ + "BCcBc", + "a", + 10 + ], + [ + "BCcCB", + "CABB", + 6 + ], + [ + "BCcCC", + "baB", + 9 + ], + [ + "BCcCcaaB", + "CAaa", + 10 + ], + [ + "BCca", + "aBcCcA", + 5 + ], + [ + "BCcaAc", + "aaAAA", + 9 + ], + [ + "BCcaBbAb", + "A", + 14 + ], + [ + "BCcab", + "CA", + 7 + ], + [ + "BCcacCbcc", + "CBcabaBCC", + 11 + ], + [ + "BCcb", + "CCbBACCcc", + 12 + ], + [ + "BCcb", + "Ca", + 6 + ], + [ + "BCcb", + "bC", + 5 + ], + [ + "BCcbAc", + "B", + 10 + ], + [ + "BCcbAcBAA", + "AacbBc", + 12 + ], + [ + "BCcbBac", + "Aaa", + 12 + ], + [ + "BCcbaC", + "cB", + 9 + ], + [ + "BCcbc", + "BBa", + 7 + ], + [ + "BCccAac", + "ABB", + 12 + ], + [ + "BCccBb", + "Cc", + 8 + ], + [ + "BCccCC", + "BBA", + 10 + ], + [ + "Ba", + "AAACAbbb", + 15 + ], + [ + "Ba", + "AAaBBbBaa", + 14 + ], + [ + "Ba", + "ABBaAabBa", + 14 + ], + [ + "Ba", + "AaccaB", + 10 + ], + [ + "Ba", + "AcABCBc", + 12 + ], + [ + "Ba", + "AcB", + 6 + ], + [ + "Ba", + "AcaAcb", + 10 + ], + [ + "Ba", + "B", + 2 + ], + [ + "Ba", + "BAcBc", + 7 + ], + [ + "Ba", + "Ba", + 0 + ], + [ + "Ba", + "BcAACc", + 9 + ], + [ + "Ba", + "BcCa", + 4 + ], + [ + "Ba", + "BcbBb", + 8 + ], + [ + "Ba", + "C", + 4 + ], + [ + "Ba", + "CABbaBcb", + 12 + ], + [ + "Ba", + "CBBAAB", + 9 + ], + [ + "Ba", + "CC", + 4 + ], + [ + "Ba", + "CCCcABBC", + 14 + ], + [ + "Ba", + "CbCbCa", + 9 + ], + [ + "Ba", + "Cc", + 4 + ], + [ + "Ba", + "a", + 2 + ], + [ + "Ba", + "aA", + 3 + ], + [ + "Ba", + "aB", + 4 + ], + [ + "Ba", + "aCB", + 6 + ], + [ + "Ba", + "aCbCa", + 7 + ], + [ + "Ba", + "acBbb", + 8 + ], + [ + "Ba", + "b", + 3 + ], + [ + "Ba", + "bcBCAbcCB", + 15 + ], + [ + "Ba", + "c", + 4 + ], + [ + "Ba", + "cAaAABbCB", + 16 + ], + [ + "Ba", + "cBBa", + 4 + ], + [ + "Ba", + "cbBaCAb", + 10 + ], + [ + "Ba", + "ccBcACA", + 11 + ], + [ + "Ba", + "ccC", + 6 + ], + [ + "BaA", + "CACaA", + 6 + ], + [ + "BaA", + "CBAbbaca", + 11 + ], + [ + "BaA", + "aaBB", + 6 + ], + [ + "BaA", + "bA", + 3 + ], + [ + "BaA", + "baACAbcA", + 11 + ], + [ + "BaA", + "cbc", + 6 + ], + [ + "BaA", + "ccBAAB", + 7 + ], + [ + "BaAAA", + "BB", + 8 + ], + [ + "BaAAa", + "ca", + 8 + ], + [ + "BaAAaCBaA", + "ACCC", + 14 + ], + [ + "BaAAcCA", + "abCCBA", + 9 + ], + [ + "BaAAcc", + "aaAAbac", + 6 + ], + [ + "BaAAccaCA", + "CcABcBCc", + 12 + ], + [ + "BaAB", + "BBAB", + 2 + ], + [ + "BaAB", + "ac", + 6 + ], + [ + "BaAB", + "bBB", + 5 + ], + [ + "BaABAACBA", + "bAb", + 14 + ], + [ + "BaABCaACb", + "bCbabAaBb", + 13 + ], + [ + "BaABa", + "abC", + 7 + ], + [ + "BaABcbab", + "cAaca", + 10 + ], + [ + "BaACAcCc", + "BC", + 12 + ], + [ + "BaACB", + "bcAa", + 7 + ], + [ + "BaACBAbAC", + "Bac", + 13 + ], + [ + "BaACaCbC", + "AACACCC", + 6 + ], + [ + "BaAa", + "BBCC", + 6 + ], + [ + "BaAa", + "BcbccbCC", + 14 + ], + [ + "BaAa", + "aAbB", + 6 + ], + [ + "BaAaAaAB", + "Ccab", + 13 + ], + [ + "BaAaaAAb", + "ABBCBAcB", + 13 + ], + [ + "BaAacA", + "bACCCb", + 9 + ], + [ + "BaAb", + "AbCBcCba", + 12 + ], + [ + "BaAbCBa", + "CCCcAb", + 13 + ], + [ + "BaAbab", + "aAbaBCc", + 7 + ], + [ + "BaAbcCCB", + "CBcA", + 13 + ], + [ + "BaAcAB", + "BabCCCaC", + 10 + ], + [ + "BaAcAa", + "abaCCCBAA", + 11 + ], + [ + "BaAcAbbC", + "ABaBBAC", + 10 + ], + [ + "BaAcB", + "CBAACbCa", + 9 + ], + [ + "BaAcCbb", + "Abb", + 8 + ], + [ + "BaAcCbca", + "BcCbCAAcB", + 12 + ], + [ + "BaAcbcb", + "c", + 12 + ], + [ + "BaB", + "CACAb", + 8 + ], + [ + "BaB", + "aCACaB", + 8 + ], + [ + "BaB", + "bc", + 5 + ], + [ + "BaB", + "cBBB", + 4 + ], + [ + "BaB", + "cCacaaB", + 10 + ], + [ + "BaBA", + "cABACbbA", + 10 + ], + [ + "BaBAA", + "bbAacbA", + 9 + ], + [ + "BaBAB", + "acAaABBA", + 11 + ], + [ + "BaBABabC", + "C", + 14 + ], + [ + "BaBACA", + "ACAcbbccb", + 15 + ], + [ + "BaBAc", + "BaBABaaC", + 7 + ], + [ + "BaBB", + "aBBCaA", + 8 + ], + [ + "BaBBaABA", + "AaabaABb", + 7 + ], + [ + "BaBBb", + "BC", + 8 + ], + [ + "BaBBc", + "C", + 9 + ], + [ + "BaBBcAaAc", + "C", + 17 + ], + [ + "BaBBcbcb", + "bbabb", + 10 + ], + [ + "BaBCAbab", + "A", + 14 + ], + [ + "BaBCBC", + "BA", + 9 + ], + [ + "BaBCaCac", + "cACc", + 10 + ], + [ + "BaBCc", + "cBcCbAab", + 13 + ], + [ + "BaBa", + "bBBABAabb", + 11 + ], + [ + "BaBa", + "bCAbaAbCb", + 13 + ], + [ + "BaBaBB", + "C", + 12 + ], + [ + "BaBb", + "CC", + 8 + ], + [ + "BaBb", + "bbAbCcbBc", + 13 + ], + [ + "BaBbABA", + "ababcCAc", + 11 + ], + [ + "BaBbBb", + "cCAAb", + 10 + ], + [ + "BaBbC", + "BBACac", + 8 + ], + [ + "BaBbabbCb", + "b", + 16 + ], + [ + "BaBc", + "CB", + 6 + ], + [ + "BaBcACac", + "AbCaBA", + 11 + ], + [ + "BaBcB", + "AaabaaBAa", + 13 + ], + [ + "BaBcBbbAa", + "BCACcab", + 13 + ], + [ + "BaBcCA", + "CbCACA", + 8 + ], + [ + "BaBcCA", + "bCa", + 8 + ], + [ + "BaBcaCCbB", + "BCC", + 12 + ], + [ + "BaBcacC", + "ACB", + 12 + ], + [ + "BaC", + "ABAbc", + 6 + ], + [ + "BaC", + "ABbb", + 6 + ], + [ + "BaC", + "ACa", + 5 + ], + [ + "BaC", + "Aaba", + 6 + ], + [ + "BaC", + "BACccaaBA", + 13 + ], + [ + "BaC", + "BC", + 2 + ], + [ + "BaC", + "Bccb", + 5 + ], + [ + "BaC", + "a", + 4 + ], + [ + "BaC", + "bCbbBcB", + 12 + ], + [ + "BaC", + "baBcbC", + 7 + ], + [ + "BaC", + "cBAabACAa", + 12 + ], + [ + "BaC", + "cBaBAaCc", + 10 + ], + [ + "BaCAA", + "cbbcBBbb", + 14 + ], + [ + "BaCAAbbB", + "BAcB", + 10 + ], + [ + "BaCAB", + "cC", + 8 + ], + [ + "BaCABCB", + "bB", + 11 + ], + [ + "BaCABccaB", + "CAacBCCA", + 12 + ], + [ + "BaCAaAc", + "bCa", + 9 + ], + [ + "BaCB", + "B", + 6 + ], + [ + "BaCBa", + "BbAbaBC", + 9 + ], + [ + "BaCBaCcCC", + "aCCAc", + 11 + ], + [ + "BaCBbCB", + "acB", + 9 + ], + [ + "BaCBc", + "ABCB", + 6 + ], + [ + "BaCBcBC", + "cc", + 11 + ], + [ + "BaCCAcAb", + "Bab", + 10 + ], + [ + "BaCCC", + "CcBCabAAa", + 14 + ], + [ + "BaCCCAcb", + "bbcAaA", + 12 + ], + [ + "BaCCCbb", + "bCCaaCb", + 9 + ], + [ + "BaCCbAA", + "acCBcBB", + 10 + ], + [ + "BaCCc", + "Ca", + 8 + ], + [ + "BaCCcCcc", + "baAbcaCc", + 8 + ], + [ + "BaCa", + "bbAba", + 6 + ], + [ + "BaCaAAaca", + "CCbCb", + 15 + ], + [ + "BaCaCCABa", + "bCaC", + 11 + ], + [ + "BaCb", + "bbaA", + 7 + ], + [ + "BaCb", + "cABC", + 7 + ], + [ + "BaCbAB", + "BAcbaAC", + 6 + ], + [ + "BaCbBA", + "bcAacBaa", + 10 + ], + [ + "BaCbBCaa", + "acBCAAcA", + 11 + ], + [ + "BaCbBcbB", + "bc", + 12 + ], + [ + "BaCbbbBaa", + "CcCAcCCC", + 16 + ], + [ + "BaCc", + "Bc", + 4 + ], + [ + "BaCc", + "CCBcbAAA", + 13 + ], + [ + "BaCcBbAb", + "BcaAaBa", + 11 + ], + [ + "BaCcCcab", + "abBAcBA", + 12 + ], + [ + "BaCcCcbAc", + "BacCAaaaA", + 11 + ], + [ + "BaCcaAabB", + "BBc", + 14 + ], + [ + "BaCcaBAa", + "AaCb", + 11 + ], + [ + "BaCccaaaA", + "cBacBCA", + 12 + ], + [ + "Baa", + "A", + 5 + ], + [ + "Baa", + "AcccabBa", + 12 + ], + [ + "Baa", + "BaaBCABcB", + 12 + ], + [ + "Baa", + "CAAa", + 5 + ], + [ + "Baa", + "CBBcBcC", + 12 + ], + [ + "Baa", + "CCABBacBb", + 14 + ], + [ + "Baa", + "CbABbAcbb", + 15 + ], + [ + "Baa", + "CbCaAbc", + 10 + ], + [ + "Baa", + "bBaBbabAc", + 12 + ], + [ + "Baa", + "bBccbbaAC", + 13 + ], + [ + "Baa", + "bcAbaAACA", + 14 + ], + [ + "BaaA", + "CaBBc", + 8 + ], + [ + "BaaA", + "baBabC", + 7 + ], + [ + "BaaABa", + "bCbcaa", + 9 + ], + [ + "BaaACcAAc", + "bccAaa", + 11 + ], + [ + "BaaAaCc", + "cB", + 13 + ], + [ + "BaaB", + "C", + 8 + ], + [ + "BaaB", + "cAa", + 5 + ], + [ + "BaaBAbAb", + "baccCC", + 13 + ], + [ + "BaaBBcA", + "ccC", + 12 + ], + [ + "BaaC", + "bCb", + 7 + ], + [ + "BaaC", + "cacAcCBcC", + 13 + ], + [ + "BaaCACAaB", + "cAaCabab", + 9 + ], + [ + "BaaCBCBBc", + "ac", + 14 + ], + [ + "BaaCbAC", + "AbCAbca", + 11 + ], + [ + "BaaCcCa", + "cBACb", + 11 + ], + [ + "BaaaBcCab", + "BaBbbbAaC", + 11 + ], + [ + "BaaaC", + "accb", + 8 + ], + [ + "Baaaa", + "B", + 8 + ], + [ + "Baaaa", + "CaA", + 7 + ], + [ + "BaaababB", + "bCAcBCB", + 11 + ], + [ + "BaaacbA", + "aBCba", + 8 + ], + [ + "Baab", + "cCAAcBa", + 11 + ], + [ + "Baab", + "ccaAACcAC", + 15 + ], + [ + "BaabA", + "b", + 8 + ], + [ + "BaabAA", + "A", + 10 + ], + [ + "BaabBAcC", + "aaBCaab", + 10 + ], + [ + "BaabBcc", + "cCCAaCbc", + 12 + ], + [ + "BaabCBCC", + "CcCaB", + 14 + ], + [ + "BaabCa", + "cAaAB", + 9 + ], + [ + "BaabCb", + "CA", + 10 + ], + [ + "Baabacaa", + "bAcbccB", + 10 + ], + [ + "Baabb", + "bBbAbbA", + 7 + ], + [ + "BaabbAaA", + "BBA", + 11 + ], + [ + "BaabcAb", + "aBaC", + 10 + ], + [ + "Baac", + "AaCacBCA", + 10 + ], + [ + "Baac", + "CcBaBAb", + 9 + ], + [ + "Baac", + "a", + 6 + ], + [ + "Baac", + "aaAB", + 5 + ], + [ + "Baac", + "cCcBC", + 9 + ], + [ + "Baac", + "ccCb", + 8 + ], + [ + "BaacCABbA", + "cA", + 14 + ], + [ + "Bab", + "BaCABaCa", + 11 + ], + [ + "Bab", + "C", + 6 + ], + [ + "Bab", + "a", + 4 + ], + [ + "Bab", + "acabAacac", + 14 + ], + [ + "Bab", + "bbCaccBaB", + 13 + ], + [ + "BabA", + "a", + 6 + ], + [ + "BabA", + "cbAc", + 6 + ], + [ + "BabAAccaB", + "abb", + 13 + ], + [ + "BabACA", + "b", + 10 + ], + [ + "BabAaC", + "baCCBcC", + 9 + ], + [ + "BabAb", + "Aa", + 8 + ], + [ + "BabAbACA", + "CBAaCaaa", + 12 + ], + [ + "BabAbCA", + "CccAab", + 12 + ], + [ + "BabBBAaCa", + "AcCAccAB", + 15 + ], + [ + "BabBa", + "ABbACcCcb", + 15 + ], + [ + "BabBcA", + "ACC", + 10 + ], + [ + "BabBcBcA", + "acbCABcB", + 10 + ], + [ + "BabC", + "aABb", + 6 + ], + [ + "BabCaBca", + "AA", + 14 + ], + [ + "Baba", + "BaBCbCBB", + 10 + ], + [ + "BabaBaAA", + "AaB", + 11 + ], + [ + "BabacAbAb", + "cCaA", + 14 + ], + [ + "Babb", + "CcAa", + 8 + ], + [ + "Babb", + "aaACcbc", + 10 + ], + [ + "BabbA", + "CaCb", + 6 + ], + [ + "BabbAaaBA", + "BbCcACC", + 13 + ], + [ + "BabbAccb", + "cacBB", + 11 + ], + [ + "BabbBcBA", + "cAabCbba", + 11 + ], + [ + "BabbCBb", + "aCCcBcB", + 10 + ], + [ + "BabbCCbCA", + "AbAcBa", + 12 + ], + [ + "BabbcBAAa", + "aCacA", + 12 + ], + [ + "BabcBAbcc", + "CabA", + 12 + ], + [ + "BabcCbCA", + "cbC", + 10 + ], + [ + "BabcaC", + "ABB", + 10 + ], + [ + "Babcac", + "bcccAbBC", + 11 + ], + [ + "Bac", + "BBcaBcC", + 8 + ], + [ + "Bac", + "CbcAbAbB", + 14 + ], + [ + "Bac", + "bacBAccb", + 11 + ], + [ + "Bac", + "bbACab", + 9 + ], + [ + "BacAa", + "bBCb", + 8 + ], + [ + "BacAaaCaB", + "ccaaAb", + 10 + ], + [ + "BacAb", + "aBBaBa", + 9 + ], + [ + "BacAbA", + "caAaBbCAb", + 11 + ], + [ + "BacBBa", + "AbBBAAA", + 10 + ], + [ + "BacBCAB", + "cCBaBCc", + 10 + ], + [ + "BacBab", + "CbBABcB", + 10 + ], + [ + "BacCAaBAB", + "ccAb", + 12 + ], + [ + "BacCBBCc", + "B", + 14 + ], + [ + "BacCCaA", + "bcaBA", + 9 + ], + [ + "BacCCcbB", + "cCbaaCBB", + 12 + ], + [ + "BacCa", + "cCBBB", + 10 + ], + [ + "BacCab", + "bA", + 10 + ], + [ + "BacCabcAc", + "CCbBC", + 12 + ], + [ + "BacCccBa", + "ABCBA", + 10 + ], + [ + "BacaB", + "bbac", + 7 + ], + [ + "BacaC", + "AAC", + 6 + ], + [ + "BacaCaBB", + "bB", + 13 + ], + [ + "BacaCbA", + "caBCAa", + 9 + ], + [ + "BacbB", + "CcaACc", + 10 + ], + [ + "BacbC", + "AAabcbbCB", + 10 + ], + [ + "BacbCbBca", + "cCcCC", + 13 + ], + [ + "BacbbCaAa", + "aCACaC", + 11 + ], + [ + "Bacc", + "b", + 7 + ], + [ + "BaccACa", + "B", + 12 + ], + [ + "BaccAaacb", + "A", + 16 + ], + [ + "BaccBcCBB", + "acb", + 13 + ], + [ + "BaccCB", + "BaC", + 6 + ], + [ + "Bacca", + "Ac", + 7 + ], + [ + "BaccaCC", + "CcbA", + 11 + ], + [ + "Baccb", + "aCc", + 5 + ], + [ + "Baccba", + "cbaaBcbc", + 9 + ], + [ + "Bb", + "AAABBB", + 9 + ], + [ + "Bb", + "AAccaCAaa", + 18 + ], + [ + "Bb", + "AaA", + 6 + ], + [ + "Bb", + "AaAAc", + 10 + ], + [ + "Bb", + "B", + 2 + ], + [ + "Bb", + "BACBbABA", + 12 + ], + [ + "Bb", + "BBaBb", + 6 + ], + [ + "Bb", + "BacCBA", + 9 + ], + [ + "Bb", + "BbCBCABAC", + 14 + ], + [ + "Bb", + "Bba", + 2 + ], + [ + "Bb", + "BcBAACA", + 11 + ], + [ + "Bb", + "CC", + 4 + ], + [ + "Bb", + "CcaaBa", + 10 + ], + [ + "Bb", + "a", + 4 + ], + [ + "Bb", + "aAbbb", + 7 + ], + [ + "Bb", + "aBa", + 4 + ], + [ + "Bb", + "aBbcCbAAC", + 14 + ], + [ + "Bb", + "aCCAB", + 9 + ], + [ + "Bb", + "aCccacCB", + 15 + ], + [ + "Bb", + "aacA", + 8 + ], + [ + "Bb", + "abBaBbAa", + 12 + ], + [ + "Bb", + "b", + 2 + ], + [ + "Bb", + "bAcCbc", + 9 + ], + [ + "Bb", + "bBaCBc", + 9 + ], + [ + "Bb", + "bBbBCccc", + 12 + ], + [ + "Bb", + "bCCc", + 7 + ], + [ + "Bb", + "bCaBcC", + 10 + ], + [ + "Bb", + "bCcbbc", + 9 + ], + [ + "Bb", + "baB", + 4 + ], + [ + "Bb", + "bc", + 3 + ], + [ + "Bb", + "bcCaacCC", + 15 + ], + [ + "Bb", + "bcaBccAcb", + 14 + ], + [ + "Bb", + "cab", + 4 + ], + [ + "Bb", + "cbAACCBb", + 12 + ], + [ + "Bb", + "cbabCcbb", + 13 + ], + [ + "Bb", + "cc", + 4 + ], + [ + "BbA", + "BCaAcABab", + 14 + ], + [ + "BbA", + "C", + 6 + ], + [ + "BbA", + "aacCbAc", + 10 + ], + [ + "BbA", + "cbCcbaAAA", + 13 + ], + [ + "BbAAaA", + "AbbBccC", + 11 + ], + [ + "BbAAaA", + "bBc", + 10 + ], + [ + "BbABB", + "BBAACAa", + 9 + ], + [ + "BbABBbAa", + "acaCCbbcB", + 14 + ], + [ + "BbABC", + "c", + 9 + ], + [ + "BbABa", + "accBbBcBb", + 12 + ], + [ + "BbABaacaA", + "b", + 16 + ], + [ + "BbAC", + "CaCCBcAa", + 12 + ], + [ + "BbAC", + "caAcaab", + 11 + ], + [ + "BbACAbcB", + "aaaAB", + 11 + ], + [ + "BbAa", + "ACcA", + 7 + ], + [ + "BbAa", + "cCA", + 6 + ], + [ + "BbAaAaaa", + "cBbbcCB", + 14 + ], + [ + "BbAaCab", + "cA", + 12 + ], + [ + "BbAaa", + "Bbbcacaa", + 7 + ], + [ + "BbAac", + "b", + 8 + ], + [ + "BbAb", + "CccCbabaC", + 13 + ], + [ + "BbAbC", + "CABA", + 7 + ], + [ + "BbAbC", + "ccA", + 8 + ], + [ + "BbAbCab", + "bcabABC", + 10 + ], + [ + "BbAbCcBab", + "CAaAbcbAC", + 12 + ], + [ + "BbAbCcaAC", + "ABbcCa", + 11 + ], + [ + "BbAbbcbb", + "B", + 14 + ], + [ + "BbAbc", + "aABB", + 7 + ], + [ + "BbAcaaaa", + "bBccaac", + 8 + ], + [ + "BbAcbca", + "acCCcbBCB", + 13 + ], + [ + "BbB", + "ACbcc", + 8 + ], + [ + "BbB", + "Acaca", + 10 + ], + [ + "BbB", + "bBcb", + 5 + ], + [ + "BbBA", + "CA", + 6 + ], + [ + "BbBABCcA", + "aaC", + 13 + ], + [ + "BbBAaBa", + "b", + 12 + ], + [ + "BbBAb", + "aaCCcb", + 10 + ], + [ + "BbBAbABAc", + "cBbbBCcB", + 12 + ], + [ + "BbBBCbca", + "cbBcCbC", + 7 + ], + [ + "BbBBa", + "cC", + 10 + ], + [ + "BbBBaBBba", + "bAC", + 15 + ], + [ + "BbBBabAb", + "cbB", + 12 + ], + [ + "BbBC", + "cbbbcaA", + 9 + ], + [ + "BbBCAbb", + "baCCbBbb", + 9 + ], + [ + "BbBCbAb", + "BbCcBbbac", + 9 + ], + [ + "BbBa", + "aCaaaA", + 10 + ], + [ + "BbBa", + "caCcb", + 10 + ], + [ + "BbBaC", + "BA", + 7 + ], + [ + "BbBaCbCcB", + "baAbCbaCc", + 11 + ], + [ + "BbBaaCBbc", + "bcAAAbC", + 11 + ], + [ + "BbBacBCca", + "c", + 16 + ], + [ + "BbBbA", + "BbaaA", + 4 + ], + [ + "BbBbBAb", + "CbcCbAaCc", + 13 + ], + [ + "BbBbBa", + "A", + 11 + ], + [ + "BbBbBbC", + "cBbacbBa", + 10 + ], + [ + "BbBbbbBAb", + "AabCcBa", + 13 + ], + [ + "BbBcBc", + "bcaaC", + 9 + ], + [ + "BbBcaAC", + "cB", + 12 + ], + [ + "BbBcabAa", + "BaBAaACc", + 10 + ], + [ + "BbBcbbC", + "bCBAabbaa", + 11 + ], + [ + "BbC", + "A", + 6 + ], + [ + "BbC", + "Cc", + 5 + ], + [ + "BbC", + "aAAAc", + 9 + ], + [ + "BbC", + "aAAbca", + 9 + ], + [ + "BbC", + "bAaBabC", + 8 + ], + [ + "BbC", + "bBCcCa", + 8 + ], + [ + "BbC", + "baCACBbc", + 11 + ], + [ + "BbC", + "bcBccBcBa", + 14 + ], + [ + "BbCAACcac", + "AcCbB", + 14 + ], + [ + "BbCAACcb", + "b", + 14 + ], + [ + "BbCAaCaC", + "BBc", + 12 + ], + [ + "BbCAbc", + "aaa", + 11 + ], + [ + "BbCAcCaCC", + "CCcB", + 13 + ], + [ + "BbCAcaab", + "BCbACaABc", + 9 + ], + [ + "BbCBCB", + "bAAccbCc", + 11 + ], + [ + "BbCBCbAc", + "CcACA", + 11 + ], + [ + "BbCCAAab", + "ccCCC", + 12 + ], + [ + "BbCCABABa", + "Acaab", + 14 + ], + [ + "BbCCBBb", + "aacCb", + 9 + ], + [ + "BbCCCBCC", + "BCA", + 12 + ], + [ + "BbCCa", + "aaaCAc", + 9 + ], + [ + "BbCCcCcCc", + "AaAAA", + 18 + ], + [ + "BbCCcbc", + "BaaBCc", + 9 + ], + [ + "BbCa", + "aCC", + 6 + ], + [ + "BbCaB", + "A", + 9 + ], + [ + "BbCaBAA", + "AbAABc", + 9 + ], + [ + "BbCaCbACB", + "AAA", + 15 + ], + [ + "BbCaCcC", + "aAAcA", + 11 + ], + [ + "BbCaCcaC", + "A", + 15 + ], + [ + "BbCbC", + "bAACAAcAC", + 13 + ], + [ + "BbCbCCb", + "AbBAA", + 11 + ], + [ + "BbCbaAB", + "C", + 12 + ], + [ + "BbCbaB", + "aAcC", + 11 + ], + [ + "BbCbaBB", + "Cabb", + 8 + ], + [ + "BbCbbBc", + "b", + 12 + ], + [ + "BbCbbb", + "cCcAcBAcA", + 16 + ], + [ + "BbCbcc", + "AAACbaaAa", + 14 + ], + [ + "BbCc", + "A", + 8 + ], + [ + "BbCc", + "aac", + 6 + ], + [ + "BbCcCACB", + "cB", + 12 + ], + [ + "BbCcaBBC", + "CB", + 12 + ], + [ + "BbCccBCB", + "cCcbBb", + 9 + ], + [ + "Bba", + "ABBBcACb", + 12 + ], + [ + "Bba", + "AbAcAbCcA", + 14 + ], + [ + "Bba", + "CaBCAaaaB", + 14 + ], + [ + "Bba", + "CacBBCBA", + 12 + ], + [ + "Bba", + "aBAcbCC", + 10 + ], + [ + "Bba", + "aCCcAaAc", + 14 + ], + [ + "Bba", + "abcA", + 5 + ], + [ + "Bba", + "bcCa", + 5 + ], + [ + "BbaAB", + "BbB", + 4 + ], + [ + "BbaABacc", + "bccbc", + 11 + ], + [ + "BbaAbBCAA", + "caAcCbc", + 12 + ], + [ + "BbaAcAb", + "bacACCbC", + 9 + ], + [ + "BbaB", + "aBbAaCb", + 7 + ], + [ + "BbaB", + "bBbBAbBC", + 9 + ], + [ + "BbaBCB", + "bcA", + 9 + ], + [ + "BbaBcbacc", + "CCBAC", + 14 + ], + [ + "BbaC", + "C", + 6 + ], + [ + "BbaCBC", + "C", + 10 + ], + [ + "BbaCCCb", + "Cca", + 11 + ], + [ + "BbaCa", + "AcCC", + 8 + ], + [ + "BbaCa", + "ccbA", + 9 + ], + [ + "BbaCcBba", + "ACb", + 11 + ], + [ + "BbaCcC", + "cbCCAAbcc", + 11 + ], + [ + "Bbaa", + "abbaAA", + 6 + ], + [ + "BbaaAbAA", + "Acc", + 14 + ], + [ + "BbaaBAb", + "CBbaCbaBB", + 9 + ], + [ + "BbaacA", + "ccAbCabAa", + 12 + ], + [ + "BbaacBcbC", + "cabbCaC", + 12 + ], + [ + "Bbab", + "BCBb", + 4 + ], + [ + "BbabA", + "Cbc", + 8 + ], + [ + "BbabBc", + "AcA", + 11 + ], + [ + "BbabC", + "ab", + 6 + ], + [ + "BbabCcBA", + "AabACBc", + 9 + ], + [ + "Bbaba", + "AAa", + 7 + ], + [ + "BbabbBb", + "b", + 12 + ], + [ + "Bbac", + "bbb", + 5 + ], + [ + "BbacB", + "CBcBB", + 7 + ], + [ + "BbacCc", + "Cb", + 10 + ], + [ + "Bbacaa", + "AaAaac", + 8 + ], + [ + "Bbb", + "ACCCCc", + 12 + ], + [ + "Bbb", + "BCABAa", + 9 + ], + [ + "Bbb", + "BabbbBcCA", + 12 + ], + [ + "Bbb", + "acaCCaa", + 14 + ], + [ + "Bbb", + "bAAcBa", + 10 + ], + [ + "Bbb", + "ccacA", + 10 + ], + [ + "BbbA", + "ACccA", + 8 + ], + [ + "BbbAAB", + "baBAb", + 7 + ], + [ + "BbbABccc", + "AbaABca", + 8 + ], + [ + "BbbAC", + "bcAaBBBca", + 14 + ], + [ + "BbbAaAba", + "a", + 14 + ], + [ + "BbbB", + "Bcc", + 6 + ], + [ + "BbbBCCA", + "cBBb", + 11 + ], + [ + "BbbBCba", + "BA", + 11 + ], + [ + "BbbBcaa", + "b", + 12 + ], + [ + "BbbCAaA", + "cbbBaBaCA", + 9 + ], + [ + "BbbCAb", + "AACC", + 10 + ], + [ + "BbbCAc", + "cbaB", + 9 + ], + [ + "BbbCBcac", + "bAbaBCba", + 10 + ], + [ + "BbbCCB", + "C", + 10 + ], + [ + "BbbCCBAc", + "cbbBcCCAC", + 8 + ], + [ + "Bbba", + "AAaAB", + 9 + ], + [ + "Bbba", + "abAc", + 6 + ], + [ + "BbbaAC", + "aBABcAAaB", + 12 + ], + [ + "BbbaC", + "AbBaaAaA", + 11 + ], + [ + "BbbaabCCA", + "CbcAb", + 13 + ], + [ + "BbbacCbb", + "AaccA", + 11 + ], + [ + "BbbbCAaA", + "cACCaCcAC", + 15 + ], + [ + "BbbbbAc", + "AbbcCb", + 10 + ], + [ + "BbbbcA", + "AaCcBabb", + 14 + ], + [ + "BbbbcbbA", + "bAA", + 12 + ], + [ + "Bbbc", + "CbccaAc", + 10 + ], + [ + "BbbcABb", + "bbbcc", + 7 + ], + [ + "BbbcB", + "bbacbbc", + 8 + ], + [ + "BbbcbAcbB", + "aCaCbaAc", + 13 + ], + [ + "Bbbcbcab", + "caCaAAbc", + 14 + ], + [ + "BbcA", + "CBaA", + 5 + ], + [ + "BbcA", + "CBcAB", + 5 + ], + [ + "BbcABcB", + "AcA", + 10 + ], + [ + "BbcACB", + "BaaCCBBC", + 10 + ], + [ + "BbcACCcB", + "bcbbaaBba", + 14 + ], + [ + "BbcAbCa", + "CbbaCba", + 9 + ], + [ + "BbcAcaB", + "Aca", + 8 + ], + [ + "BbcAccCab", + "abCCaA", + 11 + ], + [ + "BbcB", + "aACCccCaA", + 16 + ], + [ + "BbcBbB", + "baacAca", + 11 + ], + [ + "BbcBbbaC", + "Ab", + 14 + ], + [ + "BbcC", + "AccAcaBaC", + 14 + ], + [ + "BbcCB", + "bBca", + 6 + ], + [ + "BbcCBaB", + "AcCcaCbb", + 11 + ], + [ + "BbcCaab", + "cACaC", + 10 + ], + [ + "BbcaBbCCb", + "acBBcc", + 11 + ], + [ + "BbcaCcaa", + "AbcccaBB", + 9 + ], + [ + "BbcaaACb", + "A", + 14 + ], + [ + "BbcbACAa", + "a", + 14 + ], + [ + "BbcbBCCA", + "AcaCB", + 12 + ], + [ + "BbcbCcCCc", + "a", + 18 + ], + [ + "Bbcbaa", + "a", + 10 + ], + [ + "BbcbbbBab", + "aaBcBcCB", + 14 + ], + [ + "BbcbccB", + "bAACb", + 10 + ], + [ + "Bbcc", + "caAACBBb", + 15 + ], + [ + "BbccBcb", + "BCAbccb", + 8 + ], + [ + "BbccC", + "CaaAC", + 8 + ], + [ + "Bbcca", + "bBcb", + 6 + ], + [ + "BbccccCC", + "aAbcc", + 12 + ], + [ + "Bc", + "A", + 4 + ], + [ + "Bc", + "AB", + 4 + ], + [ + "Bc", + "ABAccAcC", + 12 + ], + [ + "Bc", + "ABBbBBbb", + 14 + ], + [ + "Bc", + "AaA", + 6 + ], + [ + "Bc", + "AaaBaCcCc", + 14 + ], + [ + "Bc", + "AabBBC", + 9 + ], + [ + "Bc", + "Aac", + 4 + ], + [ + "Bc", + "Aba", + 5 + ], + [ + "Bc", + "Abccbabbc", + 15 + ], + [ + "Bc", + "AcAbBC", + 9 + ], + [ + "Bc", + "B", + 2 + ], + [ + "Bc", + "BACaACBC", + 13 + ], + [ + "Bc", + "BBcBCbCB", + 12 + ], + [ + "Bc", + "BcBAAAaA", + 12 + ], + [ + "Bc", + "BcaaABCcc", + 14 + ], + [ + "Bc", + "C", + 3 + ], + [ + "Bc", + "CBCccCbaA", + 14 + ], + [ + "Bc", + "CCaBABaBA", + 16 + ], + [ + "Bc", + "CaBCBbBbc", + 14 + ], + [ + "Bc", + "CbaBCbb", + 11 + ], + [ + "Bc", + "a", + 4 + ], + [ + "Bc", + "aC", + 3 + ], + [ + "Bc", + "abAbBBcc", + 12 + ], + [ + "Bc", + "abaCAABA", + 14 + ], + [ + "Bc", + "abaabAbc", + 13 + ], + [ + "Bc", + "abcbBBB", + 11 + ], + [ + "Bc", + "ac", + 2 + ], + [ + "Bc", + "acCCcb", + 10 + ], + [ + "Bc", + "acCacaaAa", + 16 + ], + [ + "Bc", + "ba", + 3 + ], + [ + "Bc", + "cC", + 3 + ], + [ + "Bc", + "cb", + 4 + ], + [ + "Bc", + "cbACCabca", + 15 + ], + [ + "BcA", + "AC", + 5 + ], + [ + "BcA", + "BAaB", + 5 + ], + [ + "BcA", + "BBc", + 4 + ], + [ + "BcA", + "CBababB", + 11 + ], + [ + "BcA", + "CcB", + 4 + ], + [ + "BcA", + "CcCb", + 6 + ], + [ + "BcA", + "a", + 5 + ], + [ + "BcA", + "bAcab", + 6 + ], + [ + "BcA", + "bBAbb", + 7 + ], + [ + "BcA", + "bbbc", + 7 + ], + [ + "BcA", + "c", + 4 + ], + [ + "BcAABaB", + "bACcAc", + 10 + ], + [ + "BcAAaA", + "BcCAbaBb", + 8 + ], + [ + "BcAB", + "aBCCcACc", + 10 + ], + [ + "BcABAc", + "BaAa", + 7 + ], + [ + "BcABaAaA", + "aCCaCcb", + 13 + ], + [ + "BcABaa", + "CaaBBaa", + 7 + ], + [ + "BcAC", + "BCAbcbCAa", + 11 + ], + [ + "BcACAA", + "aCb", + 9 + ], + [ + "BcACB", + "bCcC", + 6 + ], + [ + "BcACBaBCa", + "BCcBAbACc", + 10 + ], + [ + "BcACC", + "aa", + 9 + ], + [ + "BcAa", + "Cb", + 7 + ], + [ + "BcAaA", + "CBBc", + 9 + ], + [ + "BcAaA", + "abBbBBcB", + 14 + ], + [ + "BcAaACA", + "cbCABAaCb", + 10 + ], + [ + "BcAaCCBB", + "cBbCaBbA", + 11 + ], + [ + "BcAaCbB", + "ACB", + 8 + ], + [ + "BcAaaaC", + "cC", + 10 + ], + [ + "BcAb", + "BB", + 5 + ], + [ + "BcAb", + "CC", + 7 + ], + [ + "BcAbAbCac", + "bab", + 13 + ], + [ + "BcAbB", + "B", + 8 + ], + [ + "BcAbabBA", + "acb", + 12 + ], + [ + "BcAbabBBB", + "BA", + 14 + ], + [ + "BcAbabC", + "BBbaCAB", + 10 + ], + [ + "BcAbbcC", + "AaAcbC", + 8 + ], + [ + "BcAbc", + "BAC", + 5 + ], + [ + "BcAbc", + "BbbcbBabB", + 11 + ], + [ + "BcAbcAAC", + "A", + 14 + ], + [ + "BcAc", + "bC", + 6 + ], + [ + "BcAcAaACb", + "aAbBBB", + 14 + ], + [ + "BcAcB", + "caaAbbccB", + 12 + ], + [ + "BcAcaccb", + "cbAbAb", + 11 + ], + [ + "BcAcba", + "aA", + 10 + ], + [ + "BcAcc", + "BCaCBcAcC", + 9 + ], + [ + "BcB", + "AcCCAC", + 10 + ], + [ + "BcB", + "BBAcaAaB", + 10 + ], + [ + "BcB", + "BabAcacCA", + 14 + ], + [ + "BcB", + "aaBcAb", + 7 + ], + [ + "BcB", + "abcC", + 5 + ], + [ + "BcB", + "bcBBCbab", + 11 + ], + [ + "BcB", + "cAAc", + 8 + ], + [ + "BcB", + "ca", + 4 + ], + [ + "BcB", + "caBaaAAb", + 13 + ], + [ + "BcBA", + "CAabC", + 9 + ], + [ + "BcBA", + "baaC", + 7 + ], + [ + "BcBAAc", + "cBBaaabBB", + 14 + ], + [ + "BcBAB", + "aCaAaBb", + 9 + ], + [ + "BcBABCB", + "aaB", + 11 + ], + [ + "BcBAccAc", + "c", + 14 + ], + [ + "BcBBBA", + "ACc", + 11 + ], + [ + "BcBBC", + "cCABb", + 7 + ], + [ + "BcBBbb", + "bbBAa", + 8 + ], + [ + "BcBBbcAaA", + "CA", + 15 + ], + [ + "BcBC", + "c", + 6 + ], + [ + "BcBCA", + "cAaCC", + 8 + ], + [ + "BcBCAAAcc", + "abc", + 15 + ], + [ + "BcBCBBC", + "BbC", + 9 + ], + [ + "BcBCbAc", + "cBBCaC", + 8 + ], + [ + "BcBCcbccc", + "BcBC", + 10 + ], + [ + "BcBCccAa", + "acb", + 13 + ], + [ + "BcBa", + "aCB", + 5 + ], + [ + "BcBaAcAAc", + "bccBBBcBc", + 11 + ], + [ + "BcBaC", + "acBbcABAa", + 11 + ], + [ + "BcBab", + "CbCACC", + 10 + ], + [ + "BcBb", + "acAABAA", + 10 + ], + [ + "BcBbA", + "Ccba", + 5 + ], + [ + "BcBbAbABB", + "a", + 17 + ], + [ + "BcBbbCAa", + "baACcBCcC", + 14 + ], + [ + "BcBbbCa", + "AcCBAa", + 9 + ], + [ + "BcBbc", + "CcABb", + 6 + ], + [ + "BcBc", + "cBBcc", + 6 + ], + [ + "BcBcAbbc", + "babC", + 11 + ], + [ + "BcBcBc", + "cAAc", + 8 + ], + [ + "BcC", + "ABabcbC", + 8 + ], + [ + "BcC", + "Ac", + 4 + ], + [ + "BcC", + "C", + 4 + ], + [ + "BcC", + "acbcaAaa", + 13 + ], + [ + "BcC", + "bAbB", + 7 + ], + [ + "BcCA", + "ccaBaaAac", + 14 + ], + [ + "BcCAABBB", + "BCabBB", + 6 + ], + [ + "BcCAaAC", + "acCaB", + 8 + ], + [ + "BcCAaBc", + "Bcab", + 7 + ], + [ + "BcCAbB", + "ACaccCB", + 10 + ], + [ + "BcCAbaACc", + "aCAAcCBC", + 12 + ], + [ + "BcCAbcbA", + "bccAbCC", + 7 + ], + [ + "BcCAcA", + "BbB", + 10 + ], + [ + "BcCBABA", + "accb", + 10 + ], + [ + "BcCBAaC", + "b", + 13 + ], + [ + "BcCBbabcc", + "cCb", + 12 + ], + [ + "BcCBc", + "ba", + 9 + ], + [ + "BcCBcaAcc", + "B", + 16 + ], + [ + "BcCBcaCbC", + "CA", + 15 + ], + [ + "BcCCAA", + "caaA", + 7 + ], + [ + "BcCCBaaa", + "AaaCBb", + 12 + ], + [ + "BcCCCAA", + "b", + 13 + ], + [ + "BcCCaaaA", + "BccaAabBa", + 9 + ], + [ + "BcCCbCac", + "AcAa", + 12 + ], + [ + "BcCa", + "AB", + 8 + ], + [ + "BcCa", + "cB", + 6 + ], + [ + "BcCaACC", + "cbACaA", + 9 + ], + [ + "BcCaACCc", + "aaaCb", + 11 + ], + [ + "BcCaBbCAC", + "AbaAc", + 12 + ], + [ + "BcCaBc", + "CCAAB", + 8 + ], + [ + "BcCaCacc", + "CaCbbbc", + 10 + ], + [ + "BcCaaC", + "AcC", + 8 + ], + [ + "BcCb", + "aBacB", + 6 + ], + [ + "BcCbAcCb", + "CAA", + 12 + ], + [ + "BcCbCCCa", + "CBCC", + 9 + ], + [ + "BcCbaBabc", + "abcbB", + 13 + ], + [ + "BcCbbb", + "ccBCb", + 6 + ], + [ + "BcCc", + "bBb", + 7 + ], + [ + "BcCcBBb", + "aCCC", + 10 + ], + [ + "BcCcBcAb", + "aacbBbC", + 13 + ], + [ + "BcCca", + "CCcCaaB", + 8 + ], + [ + "Bca", + "AcbCBab", + 10 + ], + [ + "Bca", + "Bb", + 4 + ], + [ + "Bca", + "Bcc", + 2 + ], + [ + "Bca", + "CAAbA", + 9 + ], + [ + "Bca", + "CcBc", + 6 + ], + [ + "Bca", + "aAAccCBB", + 14 + ], + [ + "Bca", + "aCbbBBcCb", + 14 + ], + [ + "Bca", + "ac", + 4 + ], + [ + "Bca", + "c", + 4 + ], + [ + "BcaAAbACa", + "cAaacBB", + 13 + ], + [ + "BcaAaC", + "B", + 10 + ], + [ + "BcaB", + "A", + 7 + ], + [ + "BcaB", + "ABC", + 7 + ], + [ + "BcaB", + "AcAacA", + 8 + ], + [ + "BcaB", + "CAba", + 7 + ], + [ + "BcaB", + "cc", + 6 + ], + [ + "BcaBACC", + "aabAcbb", + 10 + ], + [ + "BcaBACbA", + "cB", + 12 + ], + [ + "BcaBAc", + "CAcBaCC", + 10 + ], + [ + "BcaBb", + "bABA", + 6 + ], + [ + "BcaBcab", + "aAaA", + 10 + ], + [ + "BcaC", + "abbAb", + 8 + ], + [ + "BcaC", + "ba", + 5 + ], + [ + "BcaC", + "ccaCBCB", + 8 + ], + [ + "BcaCBAbA", + "BA", + 12 + ], + [ + "BcaCC", + "CAbBbB", + 12 + ], + [ + "BcaCCbb", + "bbb", + 9 + ], + [ + "BcaCc", + "AaCABAACA", + 13 + ], + [ + "Bcaa", + "C", + 7 + ], + [ + "Bcaa", + "CbBABaa", + 8 + ], + [ + "Bcaa", + "cbbBcc", + 10 + ], + [ + "BcaaAc", + "AcaCCb", + 8 + ], + [ + "BcaaBBa", + "bBBCAAb", + 12 + ], + [ + "BcaaBcB", + "cCBbbAAcA", + 13 + ], + [ + "Bcaaba", + "ACbcA", + 10 + ], + [ + "Bcaabc", + "Bba", + 8 + ], + [ + "BcaacbcC", + "CBBBbCCA", + 12 + ], + [ + "Bcab", + "AcABabB", + 8 + ], + [ + "Bcab", + "abA", + 6 + ], + [ + "BcabB", + "BcbCc", + 6 + ], + [ + "BcabBB", + "BbAbbbA", + 7 + ], + [ + "BcabCA", + "b", + 10 + ], + [ + "BcabCac", + "BbAAaca", + 9 + ], + [ + "Bcaba", + "aCcbA", + 6 + ], + [ + "BcabaABA", + "CAbB", + 10 + ], + [ + "Bcac", + "CcCAbaaa", + 12 + ], + [ + "Bcac", + "ccc", + 4 + ], + [ + "BcacAC", + "abBc", + 9 + ], + [ + "BcacAaca", + "acbAbc", + 10 + ], + [ + "BcacCB", + "cbabacC", + 9 + ], + [ + "BcacaBC", + "ACCac", + 9 + ], + [ + "BcacabbA", + "Bcb", + 10 + ], + [ + "Bcacc", + "b", + 9 + ], + [ + "Bcb", + "AaAcacaB", + 13 + ], + [ + "Bcb", + "Cc", + 4 + ], + [ + "Bcb", + "aBBC", + 6 + ], + [ + "BcbAACc", + "aBBcA", + 11 + ], + [ + "BcbAAb", + "ab", + 9 + ], + [ + "BcbAC", + "AcC", + 6 + ], + [ + "BcbAaaacc", + "cc", + 14 + ], + [ + "BcbB", + "BbcbBbBC", + 8 + ], + [ + "BcbB", + "aB", + 6 + ], + [ + "BcbBcB", + "bCbb", + 7 + ], + [ + "BcbCA", + "bCc", + 6 + ], + [ + "BcbCAAba", + "CCaa", + 10 + ], + [ + "BcbCBCA", + "BcBa", + 7 + ], + [ + "BcbCC", + "b", + 8 + ], + [ + "BcbCCcc", + "Abb", + 12 + ], + [ + "BcbCabC", + "A", + 13 + ], + [ + "BcbCcB", + "cbaccCa", + 9 + ], + [ + "Bcba", + "ACa", + 5 + ], + [ + "Bcbb", + "baaAa", + 9 + ], + [ + "BcbbA", + "BBb", + 5 + ], + [ + "BcbbC", + "ccaBAABCC", + 12 + ], + [ + "BcbcA", + "bcBcAAcB", + 8 + ], + [ + "BcbcB", + "Ccc", + 6 + ], + [ + "Bcc", + "Ab", + 6 + ], + [ + "Bcc", + "BCAbacBc", + 10 + ], + [ + "Bcc", + "aacb", + 6 + ], + [ + "Bcc", + "baAa", + 7 + ], + [ + "BccABAbcA", + "Aba", + 13 + ], + [ + "BccACA", + "aa", + 10 + ], + [ + "BccAcAbbC", + "caC", + 13 + ], + [ + "BccBA", + "BACCAC", + 7 + ], + [ + "BccBAAbb", + "aac", + 14 + ], + [ + "BccBCbAA", + "BBCCC", + 10 + ], + [ + "BccBaAaa", + "AACAbbbbB", + 16 + ], + [ + "BccBac", + "CCabACa", + 10 + ], + [ + "BccC", + "ac", + 6 + ], + [ + "BccC", + "bCA", + 6 + ], + [ + "BccCa", + "caaBBaaB", + 14 + ], + [ + "BccCab", + "Cbcbac", + 8 + ], + [ + "BccCacBC", + "BBbA", + 13 + ], + [ + "BccCcA", + "BcCBCAB", + 6 + ], + [ + "Bcca", + "AAb", + 8 + ], + [ + "BccaA", + "cCC", + 7 + ], + [ + "BccaA", + "ccc", + 6 + ], + [ + "BccaBbB", + "AA", + 13 + ], + [ + "Bccac", + "CcbaCCa", + 9 + ], + [ + "Bccb", + "AcbC", + 6 + ], + [ + "BccbB", + "bbccA", + 7 + ], + [ + "BccbBCAA", + "AA", + 12 + ], + [ + "BccbBaCbb", + "CbcBBCAA", + 11 + ], + [ + "BccbacAAc", + "CAac", + 12 + ], + [ + "BccbbACa", + "acCAbCAc", + 10 + ], + [ + "BcccAC", + "cccBbBaac", + 12 + ], + [ + "BcccBB", + "bABCc", + 10 + ], + [ + "BcccBBb", + "Cbacbaa", + 11 + ], + [ + "C", + "A", + 2 + ], + [ + "C", + "AAA", + 6 + ], + [ + "C", + "AAAb", + 8 + ], + [ + "C", + "AAB", + 6 + ], + [ + "C", + "AAacba", + 11 + ], + [ + "C", + "AAbb", + 8 + ], + [ + "C", + "ABA", + 6 + ], + [ + "C", + "ABAB", + 8 + ], + [ + "C", + "ABAcBAabA", + 17 + ], + [ + "C", + "ABCB", + 6 + ], + [ + "C", + "ABCbbbBAC", + 16 + ], + [ + "C", + "ABabc", + 9 + ], + [ + "C", + "ACCCcAcAa", + 16 + ], + [ + "C", + "ACc", + 4 + ], + [ + "C", + "ACcbaABc", + 14 + ], + [ + "C", + "AaAAa", + 10 + ], + [ + "C", + "AaAa", + 8 + ], + [ + "C", + "AaAbcaBb", + 15 + ], + [ + "C", + "AabbbC", + 10 + ], + [ + "C", + "AacCCABaB", + 16 + ], + [ + "C", + "AacaaaB", + 13 + ], + [ + "C", + "Ab", + 4 + ], + [ + "C", + "AbBbBACab", + 16 + ], + [ + "C", + "AbBcCcBAb", + 16 + ], + [ + "C", + "AbC", + 4 + ], + [ + "C", + "AbCCaCbc", + 14 + ], + [ + "C", + "Abb", + 6 + ], + [ + "C", + "AbbCcBb", + 12 + ], + [ + "C", + "Acbc", + 7 + ], + [ + "C", + "B", + 2 + ], + [ + "C", + "BAAbAbCa", + 14 + ], + [ + "C", + "BAAc", + 7 + ], + [ + "C", + "BACcBbAcB", + 16 + ], + [ + "C", + "BBBaAcB", + 13 + ], + [ + "C", + "BBacaaA", + 13 + ], + [ + "C", + "BC", + 2 + ], + [ + "C", + "BCCbA", + 8 + ], + [ + "C", + "BCba", + 6 + ], + [ + "C", + "Ba", + 4 + ], + [ + "C", + "BaA", + 6 + ], + [ + "C", + "BaBCCBcb", + 14 + ], + [ + "C", + "BaBcbbCBA", + 16 + ], + [ + "C", + "BaaC", + 6 + ], + [ + "C", + "BabbcCBa", + 14 + ], + [ + "C", + "BacccCB", + 12 + ], + [ + "C", + "BbABc", + 9 + ], + [ + "C", + "BbAaAAC", + 12 + ], + [ + "C", + "BbBBcc", + 11 + ], + [ + "C", + "BbCBCB", + 10 + ], + [ + "C", + "BbcCCbAC", + 14 + ], + [ + "C", + "BcB", + 5 + ], + [ + "C", + "BcBBBbAaa", + 17 + ], + [ + "C", + "BcCAcacAB", + 16 + ], + [ + "C", + "BcCB", + 6 + ], + [ + "C", + "BcacCaB", + 12 + ], + [ + "C", + "BccAaa", + 11 + ], + [ + "C", + "BccaAa", + 11 + ], + [ + "C", + "C", + 0 + ], + [ + "C", + "CA", + 2 + ], + [ + "C", + "CAAbCa", + 10 + ], + [ + "C", + "CABBB", + 8 + ], + [ + "C", + "CAaAa", + 8 + ], + [ + "C", + "CAbAABb", + 12 + ], + [ + "C", + "CBBCaABbB", + 16 + ], + [ + "C", + "CBaacB", + 10 + ], + [ + "C", + "CBc", + 4 + ], + [ + "C", + "CC", + 2 + ], + [ + "C", + "CCA", + 4 + ], + [ + "C", + "CCBbccA", + 12 + ], + [ + "C", + "CCc", + 4 + ], + [ + "C", + "CCcbbCBCc", + 16 + ], + [ + "C", + "CCcc", + 6 + ], + [ + "C", + "CCccCaCa", + 14 + ], + [ + "C", + "Ca", + 2 + ], + [ + "C", + "CaA", + 4 + ], + [ + "C", + "Caab", + 6 + ], + [ + "C", + "Cacaacba", + 14 + ], + [ + "C", + "Cb", + 2 + ], + [ + "C", + "CbaCBcAb", + 14 + ], + [ + "C", + "CbbBCbC", + 12 + ], + [ + "C", + "Cc", + 2 + ], + [ + "C", + "CcAcCA", + 10 + ], + [ + "C", + "CcBBCbBB", + 14 + ], + [ + "C", + "CcCBBbB", + 12 + ], + [ + "C", + "a", + 2 + ], + [ + "C", + "aA", + 4 + ], + [ + "C", + "aAABBb", + 12 + ], + [ + "C", + "aACBaca", + 12 + ], + [ + "C", + "aAaCB", + 8 + ], + [ + "C", + "aAaaBcaB", + 15 + ], + [ + "C", + "aAabb", + 10 + ], + [ + "C", + "aAccAABB", + 15 + ], + [ + "C", + "aBAaa", + 10 + ], + [ + "C", + "aBbab", + 10 + ], + [ + "C", + "aCBccCbBc", + 16 + ], + [ + "C", + "aCCBcbcBc", + 16 + ], + [ + "C", + "aCCC", + 6 + ], + [ + "C", + "aCcB", + 6 + ], + [ + "C", + "aCcccA", + 10 + ], + [ + "C", + "aa", + 4 + ], + [ + "C", + "aaBA", + 8 + ], + [ + "C", + "aaBAbA", + 12 + ], + [ + "C", + "aaBabBB", + 14 + ], + [ + "C", + "aaCAC", + 8 + ], + [ + "C", + "aaCBb", + 8 + ], + [ + "C", + "aaaBcBBc", + 15 + ], + [ + "C", + "aaab", + 8 + ], + [ + "C", + "aab", + 6 + ], + [ + "C", + "aacCCabb", + 14 + ], + [ + "C", + "abBbAcaCc", + 16 + ], + [ + "C", + "abbbAaaC", + 14 + ], + [ + "C", + "abcBBB", + 11 + ], + [ + "C", + "ac", + 3 + ], + [ + "C", + "b", + 2 + ], + [ + "C", + "bAAAccb", + 13 + ], + [ + "C", + "bAAaCBaCa", + 16 + ], + [ + "C", + "bAaABACCb", + 16 + ], + [ + "C", + "bAbAcCAc", + 14 + ], + [ + "C", + "bBABA", + 10 + ], + [ + "C", + "bBB", + 6 + ], + [ + "C", + "bBacA", + 9 + ], + [ + "C", + "bBcbbCabb", + 16 + ], + [ + "C", + "bC", + 2 + ], + [ + "C", + "bCcAcACba", + 16 + ], + [ + "C", + "ba", + 4 + ], + [ + "C", + "baAA", + 8 + ], + [ + "C", + "baAba", + 10 + ], + [ + "C", + "baCABab", + 12 + ], + [ + "C", + "baaA", + 8 + ], + [ + "C", + "baaB", + 8 + ], + [ + "C", + "baaCbCb", + 12 + ], + [ + "C", + "bbABBCA", + 12 + ], + [ + "C", + "bbABcCb", + 12 + ], + [ + "C", + "bbCab", + 8 + ], + [ + "C", + "bbacCaabC", + 16 + ], + [ + "C", + "bbbcaAb", + 13 + ], + [ + "C", + "bc", + 3 + ], + [ + "C", + "bcAca", + 9 + ], + [ + "C", + "bcBAABAAC", + 16 + ], + [ + "C", + "bcBcA", + 9 + ], + [ + "C", + "bcC", + 4 + ], + [ + "C", + "bcCAb", + 8 + ], + [ + "C", + "bcaAb", + 9 + ], + [ + "C", + "bcacBAC", + 12 + ], + [ + "C", + "bcb", + 5 + ], + [ + "C", + "bcbBba", + 11 + ], + [ + "C", + "bcbacaAA", + 15 + ], + [ + "C", + "bccC", + 6 + ], + [ + "C", + "bccccCA", + 12 + ], + [ + "C", + "c", + 1 + ], + [ + "C", + "cAc", + 5 + ], + [ + "C", + "cAcBCbCb", + 14 + ], + [ + "C", + "cAcaABCA", + 14 + ], + [ + "C", + "cBAA", + 7 + ], + [ + "C", + "cBBB", + 7 + ], + [ + "C", + "cBc", + 5 + ], + [ + "C", + "cBccbAA", + 13 + ], + [ + "C", + "cC", + 2 + ], + [ + "C", + "cCBAAB", + 10 + ], + [ + "C", + "cCaACAAC", + 14 + ], + [ + "C", + "cCbA", + 6 + ], + [ + "C", + "cCbbABa", + 12 + ], + [ + "C", + "cCcabCbBb", + 16 + ], + [ + "C", + "ca", + 3 + ], + [ + "C", + "caCACCCaB", + 16 + ], + [ + "C", + "cabCa", + 8 + ], + [ + "C", + "cacACaBB", + 14 + ], + [ + "C", + "cacBaACc", + 14 + ], + [ + "C", + "cbACabc", + 12 + ], + [ + "C", + "cbCCB", + 8 + ], + [ + "C", + "cbcC", + 6 + ], + [ + "C", + "cc", + 3 + ], + [ + "C", + "ccBAbBBC", + 14 + ], + [ + "C", + "ccBaC", + 8 + ], + [ + "C", + "ccaCbbCbb", + 16 + ], + [ + "C", + "ccabC", + 8 + ], + [ + "C", + "ccbBbCCc", + 14 + ], + [ + "CA", + "A", + 2 + ], + [ + "CA", + "AAa", + 4 + ], + [ + "CA", + "Aaacbb", + 11 + ], + [ + "CA", + "AcC", + 5 + ], + [ + "CA", + "BBaB", + 7 + ], + [ + "CA", + "BcAcbBC", + 11 + ], + [ + "CA", + "C", + 2 + ], + [ + "CA", + "CBCAa", + 6 + ], + [ + "CA", + "CacbbBaa", + 13 + ], + [ + "CA", + "CcBcBAB", + 10 + ], + [ + "CA", + "CcbCaBB", + 11 + ], + [ + "CA", + "a", + 3 + ], + [ + "CA", + "aAABAaC", + 12 + ], + [ + "CA", + "aABBa", + 8 + ], + [ + "CA", + "abbCC", + 8 + ], + [ + "CA", + "acbb", + 7 + ], + [ + "CA", + "bAbAAcbA", + 13 + ], + [ + "CA", + "bAcBcACC", + 13 + ], + [ + "CA", + "bCBaBBaB", + 13 + ], + [ + "CA", + "bbbB", + 8 + ], + [ + "CA", + "cAaaaA", + 9 + ], + [ + "CA", + "caCBCcaaA", + 14 + ], + [ + "CAA", + "CCbc", + 6 + ], + [ + "CAA", + "CbCcba", + 9 + ], + [ + "CAA", + "bb", + 6 + ], + [ + "CAA", + "c", + 5 + ], + [ + "CAA", + "cACAaa", + 7 + ], + [ + "CAA", + "cAaabbacA", + 13 + ], + [ + "CAAA", + "bb", + 8 + ], + [ + "CAAAAAcCB", + "bC", + 16 + ], + [ + "CAAAB", + "ba", + 9 + ], + [ + "CAAABaBAc", + "CaaC", + 12 + ], + [ + "CAAACa", + "AAcbAAc", + 9 + ], + [ + "CAAACaAC", + "aABBcbcBa", + 15 + ], + [ + "CAAAaBAa", + "BCBc", + 14 + ], + [ + "CAAAba", + "CacBaB", + 8 + ], + [ + "CAAAbb", + "cCaCb", + 8 + ], + [ + "CAAB", + "CbcCbBBA", + 12 + ], + [ + "CAABAAcBC", + "bccb", + 14 + ], + [ + "CAABAAcbB", + "ABBa", + 13 + ], + [ + "CAABCaaBa", + "aBCACcBca", + 12 + ], + [ + "CAABCcAAa", + "ccCB", + 15 + ], + [ + "CAABbaA", + "AbBaAac", + 10 + ], + [ + "CAAC", + "cacABc", + 7 + ], + [ + "CAACAAC", + "Ab", + 12 + ], + [ + "CAACAbca", + "Bb", + 14 + ], + [ + "CAACCAc", + "CAAB", + 8 + ], + [ + "CAACb", + "CA", + 6 + ], + [ + "CAAa", + "cBCC", + 7 + ], + [ + "CAAaBaAC", + "AaccBBB", + 13 + ], + [ + "CAAaaA", + "CaAaACca", + 7 + ], + [ + "CAAab", + "b", + 8 + ], + [ + "CAAabac", + "Accbbc", + 8 + ], + [ + "CAAbCbCcC", + "B", + 17 + ], + [ + "CAAbb", + "caC", + 8 + ], + [ + "CAAbbAbb", + "BC", + 15 + ], + [ + "CAAcBB", + "aCBCbAAb", + 13 + ], + [ + "CAAccAcC", + "abA", + 13 + ], + [ + "CAB", + "ABabCcc", + 12 + ], + [ + "CAB", + "ABccbC", + 10 + ], + [ + "CAB", + "CABba", + 4 + ], + [ + "CAB", + "CcbB", + 4 + ], + [ + "CAB", + "aababa", + 10 + ], + [ + "CAB", + "bC", + 6 + ], + [ + "CAB", + "cAbbABAac", + 13 + ], + [ + "CABA", + "CaabCCc", + 10 + ], + [ + "CABAAa", + "abC", + 10 + ], + [ + "CABACacA", + "cAbaaABC", + 10 + ], + [ + "CABACcA", + "B", + 12 + ], + [ + "CABAa", + "aac", + 8 + ], + [ + "CABAcBa", + "b", + 13 + ], + [ + "CABBAa", + "bbBcabcC", + 13 + ], + [ + "CABBBAa", + "B", + 12 + ], + [ + "CABBBCaa", + "ACb", + 12 + ], + [ + "CABBBb", + "cacaAACa", + 14 + ], + [ + "CABC", + "cCbacA", + 9 + ], + [ + "CABCAcaC", + "B", + 14 + ], + [ + "CABa", + "B", + 6 + ], + [ + "CABaCc", + "BCaaaAAcb", + 11 + ], + [ + "CABab", + "baB", + 6 + ], + [ + "CABacBA", + "BaACb", + 10 + ], + [ + "CABb", + "bCcBaCba", + 10 + ], + [ + "CABbABcBA", + "aBaAccabA", + 10 + ], + [ + "CABbbC", + "CbACCAcc", + 11 + ], + [ + "CABcAaAb", + "bb", + 13 + ], + [ + "CABcaa", + "C", + 10 + ], + [ + "CABcaa", + "aCaBABAab", + 10 + ], + [ + "CABcabCAa", + "AcbaBcCA", + 11 + ], + [ + "CABcbCAc", + "ABAcc", + 9 + ], + [ + "CABcbbA", + "BBbcA", + 8 + ], + [ + "CABcbcacA", + "cb", + 14 + ], + [ + "CAC", + "aCAAaABb", + 12 + ], + [ + "CAC", + "abCCACaBc", + 12 + ], + [ + "CAC", + "bbcbB", + 9 + ], + [ + "CAC", + "caBAa", + 7 + ], + [ + "CAC", + "ccBaBcb", + 11 + ], + [ + "CACAACccb", + "bc", + 16 + ], + [ + "CACABBA", + "cCACAcCcC", + 10 + ], + [ + "CACACAcBB", + "aBAACCc", + 12 + ], + [ + "CACAabc", + "cbAaBbbbB", + 13 + ], + [ + "CACAcaAC", + "ACcACc", + 8 + ], + [ + "CACAcbC", + "Cc", + 10 + ], + [ + "CACB", + "Cbc", + 5 + ], + [ + "CACBb", + "C", + 8 + ], + [ + "CACC", + "bCa", + 6 + ], + [ + "CACCAAB", + "bACabAC", + 8 + ], + [ + "CACCAb", + "abBBB", + 10 + ], + [ + "CACCCBA", + "cabaac", + 12 + ], + [ + "CACCc", + "ba", + 9 + ], + [ + "CACaAcc", + "acaBbCbb", + 13 + ], + [ + "CACaCBBc", + "a", + 14 + ], + [ + "CACaabcBb", + "aCabba", + 10 + ], + [ + "CACacaABb", + "b", + 16 + ], + [ + "CACb", + "ABbb", + 6 + ], + [ + "CACb", + "AabbaCa", + 11 + ], + [ + "CACbA", + "bCbbC", + 8 + ], + [ + "CACbaCB", + "caacaaC", + 9 + ], + [ + "CACbaaCa", + "bBacacB", + 12 + ], + [ + "CACbbBBc", + "Bc", + 12 + ], + [ + "CACbc", + "Cc", + 6 + ], + [ + "CACbcAcac", + "cCAcA", + 10 + ], + [ + "CACbcba", + "caABaCb", + 10 + ], + [ + "CACcBabCc", + "baAbbaBB", + 13 + ], + [ + "CACcCacC", + "ABcC", + 10 + ], + [ + "CACccCAC", + "B", + 16 + ], + [ + "CAa", + "Ab", + 4 + ], + [ + "CAa", + "CabcCc", + 9 + ], + [ + "CAa", + "aaCAbaBc", + 10 + ], + [ + "CAa", + "bABABaCA", + 12 + ], + [ + "CAa", + "bBAaB", + 6 + ], + [ + "CAa", + "bca", + 4 + ], + [ + "CAa", + "cbC", + 5 + ], + [ + "CAaAAaA", + "abAbbcbaC", + 14 + ], + [ + "CAaAAbaaB", + "CAcBB", + 11 + ], + [ + "CAaAcb", + "bBCBB", + 11 + ], + [ + "CAaBAA", + "AacAcBABB", + 11 + ], + [ + "CAaBB", + "Cc", + 8 + ], + [ + "CAaBBACa", + "CAaacBccC", + 9 + ], + [ + "CAaBC", + "b", + 9 + ], + [ + "CAaBbbaBA", + "BBB", + 13 + ], + [ + "CAaC", + "AaBCacAba", + 14 + ], + [ + "CAaC", + "BcccCbBb", + 13 + ], + [ + "CAaCBbcab", + "CbC", + 13 + ], + [ + "CAaCc", + "AA", + 7 + ], + [ + "CAaCcabB", + "cBABCb", + 11 + ], + [ + "CAaCccCc", + "A", + 14 + ], + [ + "CAaa", + "bcABBaAb", + 10 + ], + [ + "CAaa", + "cbabABAC", + 12 + ], + [ + "CAaaB", + "b", + 9 + ], + [ + "CAaabBcC", + "a", + 14 + ], + [ + "CAaabbCac", + "bAC", + 14 + ], + [ + "CAaabcaB", + "cabCacbaA", + 12 + ], + [ + "CAabABbAa", + "bcbaCa", + 13 + ], + [ + "CAabac", + "baaABbB", + 10 + ], + [ + "CAabb", + "b", + 8 + ], + [ + "CAabb", + "cACACBaBB", + 10 + ], + [ + "CAac", + "aaB", + 5 + ], + [ + "CAac", + "cccBB", + 9 + ], + [ + "CAacAB", + "a", + 10 + ], + [ + "CAaccC", + "cacbc", + 6 + ], + [ + "CAb", + "ACCCAcaA", + 12 + ], + [ + "CAb", + "Aca", + 6 + ], + [ + "CAb", + "BCcBBAA", + 10 + ], + [ + "CAb", + "C", + 4 + ], + [ + "CAb", + "CCAcbAB", + 8 + ], + [ + "CAb", + "CcCbCBC", + 10 + ], + [ + "CAb", + "acAc", + 5 + ], + [ + "CAb", + "bab", + 3 + ], + [ + "CAb", + "cA", + 3 + ], + [ + "CAbAAaAA", + "abc", + 13 + ], + [ + "CAbAc", + "ABBCCB", + 10 + ], + [ + "CAbB", + "acC", + 7 + ], + [ + "CAbBBBB", + "BCcaBbabB", + 10 + ], + [ + "CAbBCCBAC", + "baA", + 14 + ], + [ + "CAbBab", + "cbABcBc", + 10 + ], + [ + "CAbBc", + "bBcAcCA", + 11 + ], + [ + "CAbBc", + "bCAcbba", + 7 + ], + [ + "CAbC", + "AaAc", + 6 + ], + [ + "CAbC", + "cBAC", + 5 + ], + [ + "CAbCBaBb", + "cAbcBa", + 6 + ], + [ + "CAbCCC", + "c", + 11 + ], + [ + "CAbCabbA", + "BbAABACba", + 12 + ], + [ + "CAbCcc", + "babBA", + 9 + ], + [ + "CAba", + "abbB", + 6 + ], + [ + "CAbaAbBBC", + "CbBBAbB", + 9 + ], + [ + "CAbaAcCCC", + "AcbB", + 14 + ], + [ + "CAbaBb", + "A", + 10 + ], + [ + "CAbaa", + "cc", + 9 + ], + [ + "CAbacbba", + "cBCccabA", + 10 + ], + [ + "CAbbAA", + "BAAaCbC", + 12 + ], + [ + "CAbbAAcC", + "CBBACbBCB", + 12 + ], + [ + "CAbbBCcB", + "Abca", + 10 + ], + [ + "CAbbBbC", + "ABcaca", + 11 + ], + [ + "CAbbbAC", + "bcB", + 11 + ], + [ + "CAbbcCABA", + "CbB", + 12 + ], + [ + "CAbc", + "B", + 7 + ], + [ + "CAbcA", + "CAcaccACb", + 10 + ], + [ + "CAbcACCB", + "BB", + 13 + ], + [ + "CAbcBAa", + "bccBACB", + 10 + ], + [ + "CAbcCca", + "cAc", + 9 + ], + [ + "CAbcaBbca", + "ccaacBaaa", + 12 + ], + [ + "CAc", + "AAcacbcC", + 12 + ], + [ + "CAc", + "AbbAbbcc", + 12 + ], + [ + "CAc", + "BAB", + 4 + ], + [ + "CAc", + "ab", + 5 + ], + [ + "CAc", + "b", + 6 + ], + [ + "CAc", + "bbaaac", + 9 + ], + [ + "CAcAaCcaC", + "AAa", + 12 + ], + [ + "CAcAbaaA", + "ccBcBc", + 12 + ], + [ + "CAcAcC", + "BAbCb", + 9 + ], + [ + "CAcAca", + "CCbb", + 9 + ], + [ + "CAcAcbbAc", + "Cca", + 13 + ], + [ + "CAcB", + "cBcaaCcB", + 10 + ], + [ + "CAcBbCccb", + "BBAbAbAB", + 15 + ], + [ + "CAcBbbAaA", + "aACbbCbaa", + 9 + ], + [ + "CAcBbcbCa", + "bcBc", + 12 + ], + [ + "CAcBcBC", + "abCaCb", + 11 + ], + [ + "CAcC", + "CBcB", + 4 + ], + [ + "CAcCACCA", + "cAAa", + 10 + ], + [ + "CAcCaCBCC", + "aac", + 14 + ], + [ + "CAca", + "AaBACbAab", + 13 + ], + [ + "CAcaABcab", + "aCCbC", + 14 + ], + [ + "CAcaaa", + "bbccA", + 9 + ], + [ + "CAcaacBB", + "c", + 14 + ], + [ + "CAcb", + "B", + 7 + ], + [ + "CAcb", + "aBCBAb", + 8 + ], + [ + "CAcbBA", + "bBAaba", + 9 + ], + [ + "CAcbBB", + "CCcCBA", + 6 + ], + [ + "CAcbBacb", + "AbbbB", + 10 + ], + [ + "CAcbCCaaC", + "bccB", + 14 + ], + [ + "CAcbCbc", + "bAcCA", + 8 + ], + [ + "CAcbbabC", + "abAccCaac", + 11 + ], + [ + "CAcbcA", + "BBca", + 8 + ], + [ + "CAcbcB", + "Ab", + 8 + ], + [ + "CAcc", + "b", + 8 + ], + [ + "CAccAaAcc", + "bccbAB", + 12 + ], + [ + "CAccBaAC", + "abbAAaB", + 13 + ], + [ + "CAccBaaCc", + "BCAb", + 15 + ], + [ + "CAccBbA", + "bcBbBcBc", + 13 + ], + [ + "CAccac", + "aAc", + 8 + ], + [ + "CB", + "A", + 4 + ], + [ + "CB", + "ABac", + 6 + ], + [ + "CB", + "AC", + 4 + ], + [ + "CB", + "ACbbcbb", + 11 + ], + [ + "CB", + "ACcCCBAC", + 12 + ], + [ + "CB", + "AaAAcBc", + 11 + ], + [ + "CB", + "AaBCab", + 9 + ], + [ + "CB", + "BCc", + 4 + ], + [ + "CB", + "BaCaB", + 6 + ], + [ + "CB", + "BccbcabCB", + 14 + ], + [ + "CB", + "C", + 2 + ], + [ + "CB", + "CAAAb", + 7 + ], + [ + "CB", + "CCba", + 5 + ], + [ + "CB", + "CaAcA", + 8 + ], + [ + "CB", + "CcCaAccb", + 13 + ], + [ + "CB", + "a", + 4 + ], + [ + "CB", + "aAa", + 6 + ], + [ + "CB", + "aBCBaBbC", + 12 + ], + [ + "CB", + "aCbcAc", + 9 + ], + [ + "CB", + "ac", + 4 + ], + [ + "CB", + "bAa", + 6 + ], + [ + "CB", + "bBAAbcb", + 12 + ], + [ + "CB", + "bCB", + 2 + ], + [ + "CB", + "bCaCCcaCA", + 16 + ], + [ + "CB", + "ba", + 4 + ], + [ + "CB", + "baBCcCc", + 12 + ], + [ + "CB", + "bbcbCB", + 8 + ], + [ + "CB", + "bcaab", + 8 + ], + [ + "CB", + "c", + 3 + ], + [ + "CB", + "cACca", + 8 + ], + [ + "CB", + "cBCbAB", + 8 + ], + [ + "CB", + "cCB", + 2 + ], + [ + "CB", + "cbb", + 4 + ], + [ + "CB", + "cbbBbACCA", + 15 + ], + [ + "CBA", + "ba", + 4 + ], + [ + "CBA", + "cbcBb", + 7 + ], + [ + "CBAA", + "ABBaBcB", + 11 + ], + [ + "CBAA", + "Ba", + 5 + ], + [ + "CBAA", + "bcc", + 7 + ], + [ + "CBAABa", + "BAaBA", + 4 + ], + [ + "CBAAC", + "CBAac", + 2 + ], + [ + "CBAAa", + "cccbAb", + 9 + ], + [ + "CBAAaac", + "bcAAb", + 10 + ], + [ + "CBAAbCc", + "bbaABBB", + 9 + ], + [ + "CBAAbbBC", + "CbBAaAc", + 10 + ], + [ + "CBAAc", + "AAA", + 6 + ], + [ + "CBAAc", + "bCbBbAAaC", + 9 + ], + [ + "CBAB", + "A", + 6 + ], + [ + "CBABAbcBA", + "b", + 16 + ], + [ + "CBABAcBc", + "cAb", + 12 + ], + [ + "CBABCacb", + "aCCbbBABA", + 14 + ], + [ + "CBABaa", + "cBbcCcc", + 11 + ], + [ + "CBABbbCa", + "BbAaA", + 11 + ], + [ + "CBABbcCCC", + "cCAC", + 12 + ], + [ + "CBABc", + "cCc", + 7 + ], + [ + "CBABccBbc", + "caAaaACbb", + 13 + ], + [ + "CBACAA", + "CCc", + 8 + ], + [ + "CBACAcC", + "aCbbc", + 10 + ], + [ + "CBACa", + "BACaBBaB", + 10 + ], + [ + "CBACbcB", + "a", + 13 + ], + [ + "CBACc", + "baCCCA", + 9 + ], + [ + "CBACcAccA", + "aAb", + 15 + ], + [ + "CBAaAC", + "ccbC", + 9 + ], + [ + "CBAaBaC", + "bCccccCb", + 14 + ], + [ + "CBAaCB", + "acabB", + 8 + ], + [ + "CBAaCa", + "Cabb", + 8 + ], + [ + "CBAaCc", + "cBBBBBaC", + 11 + ], + [ + "CBAabb", + "bbCabaBa", + 10 + ], + [ + "CBAacCa", + "Bccbacb", + 11 + ], + [ + "CBAb", + "cCAcbc", + 7 + ], + [ + "CBAbCcaB", + "aC", + 13 + ], + [ + "CBAba", + "CbbabA", + 5 + ], + [ + "CBAbabb", + "BABa", + 7 + ], + [ + "CBAbba", + "ba", + 8 + ], + [ + "CBAcAAbcb", + "BBBBBbABA", + 15 + ], + [ + "CBAcC", + "acbabBA", + 11 + ], + [ + "CBAccB", + "cb", + 9 + ], + [ + "CBAccab", + "cbB", + 11 + ], + [ + "CBB", + "Aa", + 6 + ], + [ + "CBB", + "BbAaC", + 9 + ], + [ + "CBB", + "CB", + 2 + ], + [ + "CBB", + "CCA", + 4 + ], + [ + "CBB", + "CcbCb", + 6 + ], + [ + "CBB", + "baBB", + 4 + ], + [ + "CBB", + "cAcBBccA", + 11 + ], + [ + "CBB", + "cBccaB", + 7 + ], + [ + "CBBABB", + "BbBb", + 6 + ], + [ + "CBBABC", + "AABcaac", + 10 + ], + [ + "CBBAbACb", + "CCBCbAbcB", + 8 + ], + [ + "CBBBA", + "bB", + 7 + ], + [ + "CBBBBaAC", + "ca", + 13 + ], + [ + "CBBBBbbcC", + "BaAbCaB", + 14 + ], + [ + "CBBBb", + "bCbaBBAb", + 7 + ], + [ + "CBBBbCbcA", + "ABCCcABc", + 13 + ], + [ + "CBBC", + "ACaca", + 8 + ], + [ + "CBBC", + "cB", + 5 + ], + [ + "CBBCAAB", + "b", + 13 + ], + [ + "CBBCAb", + "CBA", + 6 + ], + [ + "CBBCCAB", + "AbCa", + 10 + ], + [ + "CBBCCa", + "c", + 11 + ], + [ + "CBBCabAa", + "Cca", + 11 + ], + [ + "CBBaCc", + "CbbA", + 7 + ], + [ + "CBBab", + "acBCcA", + 9 + ], + [ + "CBBabb", + "BBCcBb", + 7 + ], + [ + "CBBacBacB", + "caCCccBc", + 12 + ], + [ + "CBBbAb", + "Bbab", + 5 + ], + [ + "CBBba", + "bcCcb", + 9 + ], + [ + "CBBbaAcbc", + "ABcABaAAc", + 11 + ], + [ + "CBBbbbabc", + "baAaBC", + 12 + ], + [ + "CBBbcaaC", + "bABABABB", + 13 + ], + [ + "CBBc", + "abCABc", + 6 + ], + [ + "CBBcACB", + "abBbC", + 9 + ], + [ + "CBBcBcc", + "A", + 14 + ], + [ + "CBBcbA", + "ccbBAca", + 9 + ], + [ + "CBBcbaBc", + "C", + 14 + ], + [ + "CBBcbaC", + "bBcAAbaaC", + 9 + ], + [ + "CBBccBCCB", + "bacCC", + 11 + ], + [ + "CBBccc", + "aBabc", + 8 + ], + [ + "CBC", + "ACB", + 4 + ], + [ + "CBC", + "CAbCabB", + 9 + ], + [ + "CBC", + "aCaAaAC", + 10 + ], + [ + "CBCAA", + "C", + 8 + ], + [ + "CBCAABa", + "BBcCbBa", + 7 + ], + [ + "CBCAACB", + "bBBcAAB", + 7 + ], + [ + "CBCACAa", + "ABbA", + 10 + ], + [ + "CBCAaCA", + "BbaAbac", + 10 + ], + [ + "CBCAacCc", + "cCb", + 12 + ], + [ + "CBCB", + "aAAaA", + 10 + ], + [ + "CBCBBA", + "acc", + 11 + ], + [ + "CBCBBCAA", + "ABC", + 12 + ], + [ + "CBCBC", + "aBccbCB", + 8 + ], + [ + "CBCBCC", + "BAbc", + 8 + ], + [ + "CBCC", + "CbaaA", + 7 + ], + [ + "CBCCBC", + "aACAA", + 10 + ], + [ + "CBCCBCcCb", + "Ba", + 16 + ], + [ + "CBCCCB", + "cacbAbbaA", + 15 + ], + [ + "CBCCCbaB", + "cacAbc", + 12 + ], + [ + "CBCCacb", + "a", + 12 + ], + [ + "CBCCbcc", + "BbbBcbCBc", + 11 + ], + [ + "CBCCcCbA", + "CBaAa", + 11 + ], + [ + "CBCa", + "aaAbbabA", + 13 + ], + [ + "CBCa", + "b", + 7 + ], + [ + "CBCaA", + "ab", + 8 + ], + [ + "CBCaAaBaa", + "acBbAa", + 12 + ], + [ + "CBCaB", + "caa", + 7 + ], + [ + "CBCacBBBB", + "BcabACCb", + 12 + ], + [ + "CBCb", + "AabACCa", + 11 + ], + [ + "CBCb", + "aABc", + 7 + ], + [ + "CBCbBa", + "abaCaB", + 9 + ], + [ + "CBCbbc", + "CcBCBCcbA", + 9 + ], + [ + "CBCbccaA", + "AbCbbCba", + 9 + ], + [ + "CBCc", + "bbcCBBAcb", + 12 + ], + [ + "CBCcB", + "BAa", + 8 + ], + [ + "CBCcBbb", + "ccaCBbA", + 8 + ], + [ + "CBCcccacB", + "BA", + 15 + ], + [ + "CBa", + "ACaba", + 5 + ], + [ + "CBa", + "CAaB", + 4 + ], + [ + "CBa", + "CBBCccc", + 10 + ], + [ + "CBa", + "CacAccB", + 11 + ], + [ + "CBa", + "c", + 5 + ], + [ + "CBaAAa", + "caCcaBAA", + 9 + ], + [ + "CBaAC", + "CBb", + 6 + ], + [ + "CBaAbaaAA", + "CbbcC", + 13 + ], + [ + "CBaB", + "aabBAbbC", + 12 + ], + [ + "CBaBBAa", + "AaAcA", + 10 + ], + [ + "CBaBCABC", + "AAA", + 13 + ], + [ + "CBaBaCcB", + "cBaAbBAAa", + 12 + ], + [ + "CBaBaa", + "AAB", + 9 + ], + [ + "CBaCAbC", + "BbCaCC", + 7 + ], + [ + "CBaCBba", + "bcABB", + 10 + ], + [ + "CBaCCAB", + "cBcBAcA", + 10 + ], + [ + "CBaCabccB", + "Bba", + 14 + ], + [ + "CBaaA", + "AacbaC", + 10 + ], + [ + "CBaaAcac", + "aacbcbACc", + 13 + ], + [ + "CBaaB", + "CbaAcB", + 4 + ], + [ + "CBaaBc", + "Ba", + 8 + ], + [ + "CBaaCaCa", + "c", + 15 + ], + [ + "CBaaaBB", + "B", + 12 + ], + [ + "CBaab", + "BaabBcba", + 10 + ], + [ + "CBaab", + "bBB", + 7 + ], + [ + "CBaabAB", + "CBCC", + 10 + ], + [ + "CBaacACCC", + "a", + 16 + ], + [ + "CBab", + "CB", + 4 + ], + [ + "CBab", + "aBcc", + 6 + ], + [ + "CBabACCaA", + "A", + 16 + ], + [ + "CBabCbbaC", + "AcbAc", + 12 + ], + [ + "CBacA", + "acBCCaCB", + 10 + ], + [ + "CBacaCBCB", + "aB", + 14 + ], + [ + "CBacbacB", + "CaBa", + 9 + ], + [ + "CBb", + "Cbb", + 1 + ], + [ + "CBb", + "a", + 6 + ], + [ + "CBb", + "bBbcc", + 6 + ], + [ + "CBb", + "bCaaaA", + 10 + ], + [ + "CBbA", + "bAaCbBC", + 10 + ], + [ + "CBbAAa", + "aabB", + 10 + ], + [ + "CBbAB", + "A", + 8 + ], + [ + "CBbAB", + "bbb", + 6 + ], + [ + "CBbACca", + "CbCAcB", + 7 + ], + [ + "CBbACcbC", + "bCcbBAb", + 12 + ], + [ + "CBbBB", + "C", + 8 + ], + [ + "CBbBB", + "CBCBcCBa", + 8 + ], + [ + "CBbBBCaBB", + "AbAca", + 13 + ], + [ + "CBbCA", + "aca", + 8 + ], + [ + "CBbCCB", + "BAbC", + 8 + ], + [ + "CBbCCCa", + "Ca", + 10 + ], + [ + "CBbCCb", + "CACbBBb", + 8 + ], + [ + "CBbCbbBc", + "ABaA", + 14 + ], + [ + "CBbaAcAAc", + "ccAACB", + 12 + ], + [ + "CBbaAca", + "bB", + 12 + ], + [ + "CBbaB", + "B", + 8 + ], + [ + "CBbaBBCaC", + "baaCcA", + 12 + ], + [ + "CBbaC", + "c", + 9 + ], + [ + "CBbaCa", + "cbbcAa", + 6 + ], + [ + "CBbaCbB", + "AcCcACbA", + 10 + ], + [ + "CBbabC", + "abB", + 8 + ], + [ + "CBbb", + "C", + 6 + ], + [ + "CBbb", + "a", + 8 + ], + [ + "CBbbbaB", + "B", + 12 + ], + [ + "CBbc", + "BaAABc", + 9 + ], + [ + "CBbcBAAB", + "cAbCAA", + 8 + ], + [ + "CBbcbb", + "cBCaC", + 8 + ], + [ + "CBc", + "Bcab", + 6 + ], + [ + "CBc", + "CBAAacc", + 8 + ], + [ + "CBc", + "abaCa", + 8 + ], + [ + "CBc", + "cAabccaaA", + 14 + ], + [ + "CBcA", + "CCA", + 3 + ], + [ + "CBcAAaccC", + "abBcAbaBC", + 10 + ], + [ + "CBcAaCCCb", + "aB", + 15 + ], + [ + "CBcAbAA", + "BabCcAAc", + 11 + ], + [ + "CBcAcB", + "CbbAAA", + 7 + ], + [ + "CBcB", + "a", + 8 + ], + [ + "CBcB", + "bcCBCCcbb", + 11 + ], + [ + "CBcBA", + "cBbCb", + 7 + ], + [ + "CBcBBABcc", + "bbBcabA", + 13 + ], + [ + "CBcBC", + "cAbCAAbcA", + 13 + ], + [ + "CBcBaaACB", + "cCc", + 14 + ], + [ + "CBcBb", + "c", + 8 + ], + [ + "CBcC", + "bCBBbBa", + 10 + ], + [ + "CBcC", + "c", + 6 + ], + [ + "CBcCC", + "aB", + 8 + ], + [ + "CBca", + "cC", + 6 + ], + [ + "CBcaAa", + "A", + 10 + ], + [ + "CBcaAba", + "AaAAaaCaA", + 13 + ], + [ + "CBcaBCc", + "AcBcb", + 9 + ], + [ + "CBcabbbba", + "bCCBc", + 15 + ], + [ + "CBcacCbCC", + "bCAaab", + 13 + ], + [ + "CBcb", + "Acb", + 4 + ], + [ + "CBcb", + "CbcABCA", + 8 + ], + [ + "CBcb", + "cBAac", + 7 + ], + [ + "CBcbCbc", + "cBBaBc", + 7 + ], + [ + "CBcbCcaBc", + "BAACAaC", + 11 + ], + [ + "CBcba", + "abCA", + 7 + ], + [ + "CBcbab", + "ABBabBC", + 9 + ], + [ + "CBcbbaC", + "ABcCa", + 8 + ], + [ + "CBcbcAAA", + "aaAbb", + 14 + ], + [ + "CBcbcbC", + "c", + 12 + ], + [ + "CBccaC", + "CAAa", + 8 + ], + [ + "CBccaCaaa", + "ccbBabcBB", + 15 + ], + [ + "CBccac", + "aB", + 10 + ], + [ + "CBccbaAbb", + "bBaacaC", + 14 + ], + [ + "CBccbbc", + "ACaAaCB", + 13 + ], + [ + "CC", + "AA", + 4 + ], + [ + "CC", + "AAaacAc", + 12 + ], + [ + "CC", + "ACACbCABB", + 14 + ], + [ + "CC", + "ACBaCAb", + 10 + ], + [ + "CC", + "ACaaB", + 8 + ], + [ + "CC", + "ACcCAaabb", + 14 + ], + [ + "CC", + "AacCBBc", + 11 + ], + [ + "CC", + "BaAACABCC", + 14 + ], + [ + "CC", + "BaBcaCbb", + 13 + ], + [ + "CC", + "Bc", + 3 + ], + [ + "CC", + "CBC", + 2 + ], + [ + "CC", + "CCAAAbab", + 12 + ], + [ + "CC", + "CCc", + 2 + ], + [ + "CC", + "Cb", + 2 + ], + [ + "CC", + "CbbBb", + 8 + ], + [ + "CC", + "CcAc", + 5 + ], + [ + "CC", + "a", + 4 + ], + [ + "CC", + "aACcC", + 6 + ], + [ + "CC", + "aBC", + 4 + ], + [ + "CC", + "aBbc", + 7 + ], + [ + "CC", + "aa", + 4 + ], + [ + "CC", + "bA", + 4 + ], + [ + "CC", + "c", + 3 + ], + [ + "CC", + "cBBB", + 7 + ], + [ + "CC", + "cBcCbBcab", + 15 + ], + [ + "CC", + "cCabBBC", + 10 + ], + [ + "CC", + "cCbBB", + 7 + ], + [ + "CCA", + "A", + 4 + ], + [ + "CCA", + "BaaabCA", + 10 + ], + [ + "CCA", + "CBaCacAB", + 10 + ], + [ + "CCA", + "aba", + 5 + ], + [ + "CCA", + "bb", + 6 + ], + [ + "CCAA", + "ABBCA", + 8 + ], + [ + "CCAA", + "BcCbBA", + 7 + ], + [ + "CCAA", + "babAabAA", + 12 + ], + [ + "CCAA", + "cAcC", + 7 + ], + [ + "CCAABbAAc", + "CA", + 14 + ], + [ + "CCAABc", + "BacACaaB", + 11 + ], + [ + "CCAACAbaa", + "BA", + 16 + ], + [ + "CCAAaB", + "CaCCcCa", + 10 + ], + [ + "CCAAbabbc", + "cc", + 15 + ], + [ + "CCAAcCCcA", + "ABcACaBc", + 13 + ], + [ + "CCAAcb", + "C", + 10 + ], + [ + "CCAB", + "bbc", + 8 + ], + [ + "CCABAAB", + "BCABbccB", + 8 + ], + [ + "CCABBCcbB", + "CabB", + 11 + ], + [ + "CCABCCAaa", + "CBCc", + 11 + ], + [ + "CCABb", + "Bcc", + 9 + ], + [ + "CCABcCBAa", + "bbBa", + 13 + ], + [ + "CCABcaC", + "caCc", + 10 + ], + [ + "CCABcbBaC", + "cAAaAAaC", + 11 + ], + [ + "CCAC", + "aBAccBA", + 11 + ], + [ + "CCACb", + "BABcBc", + 10 + ], + [ + "CCACbACc", + "Aabbbac", + 11 + ], + [ + "CCACcaCB", + "ABbCACbB", + 11 + ], + [ + "CCAa", + "aACCbaaac", + 11 + ], + [ + "CCAb", + "ACCBba", + 6 + ], + [ + "CCAbBcABA", + "cAaCBc", + 12 + ], + [ + "CCAbBcb", + "baCB", + 10 + ], + [ + "CCAbCAb", + "BaCCaa", + 10 + ], + [ + "CCAbaC", + "BBbcaa", + 10 + ], + [ + "CCAbaCBa", + "bcccA", + 12 + ], + [ + "CCAcAB", + "ccAAbCa", + 9 + ], + [ + "CCAcACA", + "acBA", + 9 + ], + [ + "CCAcBAa", + "aCacBaBAA", + 8 + ], + [ + "CCAcC", + "BbcBbC", + 9 + ], + [ + "CCAcCB", + "cBcaBBBA", + 11 + ], + [ + "CCAcaC", + "CcaB", + 6 + ], + [ + "CCAcaaca", + "cC", + 13 + ], + [ + "CCAcacccc", + "cAcabcB", + 9 + ], + [ + "CCAcbCb", + "ACCa", + 9 + ], + [ + "CCAcbbCBc", + "CcAbBCa", + 8 + ], + [ + "CCB", + "ACbaAACcB", + 12 + ], + [ + "CCB", + "Cc", + 3 + ], + [ + "CCB", + "abaAB", + 8 + ], + [ + "CCB", + "baCac", + 8 + ], + [ + "CCBA", + "Aaaaa", + 9 + ], + [ + "CCBAA", + "a", + 9 + ], + [ + "CCBABcc", + "BCABb", + 8 + ], + [ + "CCBAC", + "bCCcaCAc", + 9 + ], + [ + "CCBAbAC", + "bCA", + 10 + ], + [ + "CCBAc", + "BC", + 7 + ], + [ + "CCBAcaacb", + "aCCAA", + 13 + ], + [ + "CCBBCBccb", + "BBAa", + 14 + ], + [ + "CCBBbB", + "CbcAACa", + 11 + ], + [ + "CCBBccC", + "c", + 12 + ], + [ + "CCBC", + "CBCaABac", + 9 + ], + [ + "CCBCAaAA", + "CaC", + 12 + ], + [ + "CCBCBCcc", + "bcCccbAbb", + 13 + ], + [ + "CCBCb", + "c", + 9 + ], + [ + "CCBaAaB", + "bACAC", + 11 + ], + [ + "CCBaAaB", + "cbcCBca", + 11 + ], + [ + "CCBaAcba", + "BaaaCAA", + 11 + ], + [ + "CCBaBbbBC", + "a", + 16 + ], + [ + "CCBaCcAa", + "ccBbACCb", + 10 + ], + [ + "CCBbAAAB", + "CCcaBabc", + 11 + ], + [ + "CCBbACc", + "bbbaacBb", + 12 + ], + [ + "CCBbB", + "cBBBACC", + 10 + ], + [ + "CCBbBC", + "bAcCCab", + 12 + ], + [ + "CCBbabCB", + "bCcaB", + 10 + ], + [ + "CCBbca", + "bc", + 8 + ], + [ + "CCBcB", + "ACbbCa", + 8 + ], + [ + "CCBca", + "BacCaaAA", + 12 + ], + [ + "CCBcbbC", + "cB", + 11 + ], + [ + "CCC", + "CcaB", + 5 + ], + [ + "CCC", + "baaaC", + 8 + ], + [ + "CCCAABC", + "CC", + 10 + ], + [ + "CCCAbbBCb", + "cBBaaBab", + 12 + ], + [ + "CCCAc", + "BaccCaB", + 9 + ], + [ + "CCCBAbcc", + "accAb", + 10 + ], + [ + "CCCBBcab", + "ACCA", + 11 + ], + [ + "CCCBCaAcC", + "aaCcAbAa", + 14 + ], + [ + "CCCBbbbC", + "aCb", + 12 + ], + [ + "CCCBcBBaa", + "aBACAbAA", + 14 + ], + [ + "CCCBccaB", + "ccBcc", + 8 + ], + [ + "CCCC", + "bBcAaAbaC", + 15 + ], + [ + "CCCCA", + "BBACa", + 7 + ], + [ + "CCCCA", + "bAcAbbC", + 13 + ], + [ + "CCCCA", + "cccAC", + 7 + ], + [ + "CCCCAbaa", + "CaBaccBB", + 14 + ], + [ + "CCCCBC", + "ABbBc", + 9 + ], + [ + "CCCCCa", + "BCACA", + 7 + ], + [ + "CCCCaBCA", + "Bacbcab", + 13 + ], + [ + "CCCCbaC", + "b", + 12 + ], + [ + "CCCCbbba", + "BAbcAbcBb", + 14 + ], + [ + "CCCCcA", + "bbCCbbc", + 10 + ], + [ + "CCCCcBAC", + "acACBcC", + 9 + ], + [ + "CCCa", + "bAb", + 8 + ], + [ + "CCCaABacb", + "bacBBcAA", + 14 + ], + [ + "CCCaCcA", + "bAcAC", + 10 + ], + [ + "CCCaCcbBA", + "bAbc", + 15 + ], + [ + "CCCaaCaBA", + "AbAbBCb", + 15 + ], + [ + "CCCabaaab", + "cAbca", + 12 + ], + [ + "CCCacacb", + "CAcCBcA", + 10 + ], + [ + "CCCb", + "C", + 6 + ], + [ + "CCCbBBB", + "A", + 14 + ], + [ + "CCCba", + "BbAcaC", + 10 + ], + [ + "CCCbbAA", + "a", + 13 + ], + [ + "CCCbbaA", + "cCB", + 10 + ], + [ + "CCCbbcc", + "BCA", + 12 + ], + [ + "CCCc", + "CBbBBaBBa", + 16 + ], + [ + "CCCcBBba", + "CcBbcAa", + 9 + ], + [ + "CCCcBaA", + "CCCBAAAcA", + 9 + ], + [ + "CCa", + "Ca", + 2 + ], + [ + "CCa", + "CbabbAAcA", + 14 + ], + [ + "CCa", + "bbbCBA", + 9 + ], + [ + "CCa", + "cCAcbAbCC", + 14 + ], + [ + "CCaA", + "c", + 7 + ], + [ + "CCaAA", + "BBACcB", + 11 + ], + [ + "CCaAAb", + "cBa", + 9 + ], + [ + "CCaABbBaA", + "CcACab", + 11 + ], + [ + "CCaAcb", + "ABbBCaCa", + 13 + ], + [ + "CCaBAcbAC", + "CBbc", + 11 + ], + [ + "CCaBBA", + "bAbBB", + 8 + ], + [ + "CCaCAAbA", + "AA", + 12 + ], + [ + "CCaCACBcb", + "caC", + 13 + ], + [ + "CCaCAaA", + "BCAaCaa", + 7 + ], + [ + "CCaCB", + "CAAA", + 7 + ], + [ + "CCaCBcAC", + "bcCa", + 12 + ], + [ + "CCaCCC", + "CACcBa", + 8 + ], + [ + "CCaCaCc", + "BcCba", + 11 + ], + [ + "CCaCaCcBA", + "a", + 16 + ], + [ + "CCaCabaAa", + "CcccAacAC", + 11 + ], + [ + "CCaCb", + "bAcaCbCAb", + 11 + ], + [ + "CCaCbaaaC", + "CaCBCBBc", + 10 + ], + [ + "CCaaBbbA", + "BAAc", + 14 + ], + [ + "CCaaCbCab", + "aBbB", + 13 + ], + [ + "CCaaaaccC", + "AABcca", + 12 + ], + [ + "CCaacBa", + "a", + 12 + ], + [ + "CCabA", + "acbCAAbb", + 10 + ], + [ + "CCabA", + "bbaacCA", + 10 + ], + [ + "CCabCAac", + "ABABA", + 12 + ], + [ + "CCabcCbc", + "BCbc", + 9 + ], + [ + "CCac", + "a", + 6 + ], + [ + "CCacAac", + "bbBbaC", + 11 + ], + [ + "CCacAbCcb", + "bCbBa", + 14 + ], + [ + "CCacBca", + "C", + 12 + ], + [ + "CCacCb", + "cBAC", + 8 + ], + [ + "CCacab", + "Aa", + 9 + ], + [ + "CCacbb", + "CAb", + 7 + ], + [ + "CCacbcB", + "Cca", + 9 + ], + [ + "CCb", + "BAbBAaB", + 12 + ], + [ + "CCb", + "CAAAbca", + 10 + ], + [ + "CCb", + "bAc", + 6 + ], + [ + "CCb", + "bcabBaB", + 11 + ], + [ + "CCb", + "cbCcaAaac", + 15 + ], + [ + "CCbA", + "AABAC", + 7 + ], + [ + "CCbA", + "BbBCabCaa", + 13 + ], + [ + "CCbAAa", + "BcABbC", + 11 + ], + [ + "CCbABCC", + "a", + 13 + ], + [ + "CCbACCBa", + "BbbaBbC", + 12 + ], + [ + "CCbAaaA", + "aB", + 12 + ], + [ + "CCbAbC", + "cAacBCa", + 10 + ], + [ + "CCbAbbBB", + "abcaA", + 13 + ], + [ + "CCbB", + "Aa", + 8 + ], + [ + "CCbB", + "cbaBCBBA", + 10 + ], + [ + "CCbBBB", + "CACCacCBc", + 12 + ], + [ + "CCbBCC", + "BbCAbcAA", + 12 + ], + [ + "CCbBbaBa", + "BcbccCAc", + 13 + ], + [ + "CCbCBCbB", + "cCbbB", + 7 + ], + [ + "CCbCbA", + "ABACcA", + 8 + ], + [ + "CCbCbcacC", + "bBC", + 13 + ], + [ + "CCba", + "aBaAcAaCC", + 15 + ], + [ + "CCba", + "caCAcc", + 9 + ], + [ + "CCba", + "cbaCcba", + 7 + ], + [ + "CCbaB", + "Ba", + 7 + ], + [ + "CCbaBB", + "CCaccA", + 8 + ], + [ + "CCbaCC", + "aBbcaa", + 10 + ], + [ + "CCbaaBcAB", + "a", + 16 + ], + [ + "CCbaba", + "ACbaBAAb", + 8 + ], + [ + "CCbb", + "aBbacACBa", + 14 + ], + [ + "CCbbACcA", + "CBabaAA", + 9 + ], + [ + "CCbbAabCa", + "cAABb", + 13 + ], + [ + "CCbbBabb", + "aBBcc", + 13 + ], + [ + "CCbbaBC", + "cBC", + 9 + ], + [ + "CCbbca", + "bAC", + 9 + ], + [ + "CCbbcaCBA", + "CCaAaBBAB", + 10 + ], + [ + "CCbcAac", + "a", + 12 + ], + [ + "CCbcBC", + "aabbAc", + 9 + ], + [ + "CCbcBca", + "B", + 12 + ], + [ + "CCbcCAc", + "Bb", + 12 + ], + [ + "CCbcb", + "cBaC", + 8 + ], + [ + "CCbcc", + "aBaccBcB", + 11 + ], + [ + "CCc", + "A", + 6 + ], + [ + "CCc", + "Ac", + 4 + ], + [ + "CCc", + "Bc", + 4 + ], + [ + "CCc", + "Cacbb", + 6 + ], + [ + "CCc", + "bCcbAbbAB", + 14 + ], + [ + "CCcAC", + "CABCA", + 7 + ], + [ + "CCcACBCC", + "B", + 14 + ], + [ + "CCcACc", + "cACCCA", + 8 + ], + [ + "CCcAaaba", + "CCCBcABca", + 9 + ], + [ + "CCcAc", + "abaaaCCc", + 13 + ], + [ + "CCcB", + "aCAaBc", + 8 + ], + [ + "CCcBAB", + "CacAbbcbB", + 11 + ], + [ + "CCcBAcC", + "bB", + 12 + ], + [ + "CCcBbAb", + "aBbC", + 10 + ], + [ + "CCcBcaB", + "cBbb", + 9 + ], + [ + "CCcCaaAC", + "CAAbccC", + 12 + ], + [ + "CCcCb", + "B", + 9 + ], + [ + "CCcCbacba", + "AAa", + 15 + ], + [ + "CCcCcABB", + "cAcBabacb", + 13 + ], + [ + "CCcCcccAB", + "BAaACCCAB", + 11 + ], + [ + "CCca", + "Acc", + 5 + ], + [ + "CCca", + "a", + 6 + ], + [ + "CCcaBCabB", + "cAabc", + 11 + ], + [ + "CCcaC", + "bbCACAA", + 10 + ], + [ + "CCcaCca", + "CabacBb", + 9 + ], + [ + "CCcaaBaB", + "ccaAabb", + 9 + ], + [ + "CCcaaabbc", + "BCaCC", + 13 + ], + [ + "CCcabA", + "bCCAAcaBC", + 9 + ], + [ + "CCcacbB", + "Babb", + 9 + ], + [ + "CCcb", + "C", + 6 + ], + [ + "CCcbB", + "abB", + 6 + ], + [ + "CCcbBBC", + "aAAaCCCc", + 14 + ], + [ + "CCcbC", + "ACcBBBBc", + 10 + ], + [ + "CCcbC", + "a", + 10 + ], + [ + "CCcbCACab", + "Accba", + 11 + ], + [ + "CCcbcaaaA", + "ccaaAbaaB", + 12 + ], + [ + "CCccAAB", + "CCaaa", + 8 + ], + [ + "CCccB", + "AACBAbC", + 11 + ], + [ + "CCcca", + "baAaCccaa", + 10 + ], + [ + "CCccaAA", + "b", + 14 + ], + [ + "CCccbb", + "babC", + 10 + ], + [ + "Ca", + "ABCCb", + 8 + ], + [ + "Ca", + "AaCcCBaab", + 14 + ], + [ + "Ca", + "Aac", + 4 + ], + [ + "Ca", + "BAAABca", + 11 + ], + [ + "Ca", + "BACaCCb", + 10 + ], + [ + "Ca", + "BAc", + 5 + ], + [ + "Ca", + "BBAA", + 7 + ], + [ + "Ca", + "BCB", + 4 + ], + [ + "Ca", + "BCBbBaAC", + 12 + ], + [ + "Ca", + "BaCCAcBAa", + 14 + ], + [ + "Ca", + "BabB", + 6 + ], + [ + "Ca", + "BababbBb", + 14 + ], + [ + "Ca", + "BbCB", + 6 + ], + [ + "Ca", + "Bbb", + 6 + ], + [ + "Ca", + "CAbc", + 5 + ], + [ + "Ca", + "CBAbac", + 8 + ], + [ + "Ca", + "CBB", + 4 + ], + [ + "Ca", + "CBaA", + 4 + ], + [ + "Ca", + "CCAB", + 5 + ], + [ + "Ca", + "CCaCBaaA", + 12 + ], + [ + "Ca", + "CCbaAA", + 8 + ], + [ + "Ca", + "CCbb", + 6 + ], + [ + "Ca", + "Cb", + 2 + ], + [ + "Ca", + "aACCba", + 8 + ], + [ + "Ca", + "aB", + 4 + ], + [ + "Ca", + "aBBA", + 7 + ], + [ + "Ca", + "aac", + 4 + ], + [ + "Ca", + "acBacBcAA", + 15 + ], + [ + "Ca", + "b", + 4 + ], + [ + "Ca", + "bAbBbbAa", + 14 + ], + [ + "Ca", + "bBCA", + 5 + ], + [ + "Ca", + "bbBAAAa", + 12 + ], + [ + "Ca", + "c", + 3 + ], + [ + "Ca", + "cCBCBbAAa", + 14 + ], + [ + "Ca", + "caBabaBc", + 13 + ], + [ + "Ca", + "ccCaCCC", + 10 + ], + [ + "CaA", + "BcBcAb", + 9 + ], + [ + "CaA", + "CCBBA", + 6 + ], + [ + "CaA", + "aBa", + 5 + ], + [ + "CaA", + "aBcbcb", + 11 + ], + [ + "CaA", + "bBcCBcbcc", + 16 + ], + [ + "CaA", + "bCABC", + 7 + ], + [ + "CaA", + "bCcBaAaA", + 10 + ], + [ + "CaA", + "c", + 5 + ], + [ + "CaAA", + "bacCbbA", + 10 + ], + [ + "CaAABbc", + "c", + 12 + ], + [ + "CaAAC", + "c", + 9 + ], + [ + "CaAACB", + "bB", + 10 + ], + [ + "CaAAa", + "AcCbA", + 9 + ], + [ + "CaAAbcCa", + "a", + 14 + ], + [ + "CaAB", + "BCaCaCB", + 7 + ], + [ + "CaAB", + "Ba", + 6 + ], + [ + "CaAB", + "bCb", + 7 + ], + [ + "CaABBc", + "b", + 11 + ], + [ + "CaABCBA", + "bBb", + 11 + ], + [ + "CaABa", + "B", + 8 + ], + [ + "CaABbACB", + "BA", + 12 + ], + [ + "CaABbC", + "AAacAc", + 9 + ], + [ + "CaAC", + "BBCbCcaaC", + 11 + ], + [ + "CaAC", + "aCAABab", + 9 + ], + [ + "CaAC", + "acCBbcbA", + 13 + ], + [ + "CaACB", + "B", + 8 + ], + [ + "CaACbacC", + "BAbabAAB", + 12 + ], + [ + "CaACbc", + "CBA", + 8 + ], + [ + "CaACcBACc", + "aAcc", + 10 + ], + [ + "CaAa", + "Bababa", + 7 + ], + [ + "CaAa", + "ccAC", + 5 + ], + [ + "CaAaAC", + "ACa", + 9 + ], + [ + "CaAaACCc", + "BaabbBCbC", + 12 + ], + [ + "CaAaBbB", + "cbAb", + 9 + ], + [ + "CaAaa", + "cAAACBa", + 7 + ], + [ + "CaAab", + "BBbcbaa", + 12 + ], + [ + "CaAabcABB", + "ACcBacC", + 14 + ], + [ + "CaAbC", + "bC", + 6 + ], + [ + "CaAbbBa", + "c", + 13 + ], + [ + "CaAc", + "Ac", + 4 + ], + [ + "CaAc", + "abB", + 6 + ], + [ + "CaAcCAb", + "cABCaAA", + 9 + ], + [ + "CaAcCa", + "bBabcCCC", + 10 + ], + [ + "CaAca", + "bcAcBAABA", + 13 + ], + [ + "CaAcb", + "aBbACB", + 8 + ], + [ + "CaAcc", + "ACc", + 5 + ], + [ + "CaB", + "B", + 4 + ], + [ + "CaB", + "C", + 4 + ], + [ + "CaB", + "acbCAcC", + 11 + ], + [ + "CaB", + "bbAaAaBB", + 12 + ], + [ + "CaB", + "cabbb", + 6 + ], + [ + "CaBA", + "cbcAB", + 7 + ], + [ + "CaBAAB", + "BBAC", + 8 + ], + [ + "CaBAAB", + "a", + 10 + ], + [ + "CaBAAaBB", + "AACB", + 10 + ], + [ + "CaBAbCb", + "baA", + 10 + ], + [ + "CaBAcBCcb", + "A", + 16 + ], + [ + "CaBAcc", + "AACCaA", + 11 + ], + [ + "CaBAcc", + "bAcBbA", + 11 + ], + [ + "CaBB", + "aCCBbcaba", + 13 + ], + [ + "CaBB", + "babABcb", + 9 + ], + [ + "CaBBBa", + "Cc", + 10 + ], + [ + "CaBBBbBB", + "CAAaC", + 13 + ], + [ + "CaBBCccC", + "bAac", + 13 + ], + [ + "CaBBaA", + "a", + 10 + ], + [ + "CaBBabBcc", + "b", + 16 + ], + [ + "CaBBcC", + "BA", + 10 + ], + [ + "CaBBccac", + "accCBA", + 11 + ], + [ + "CaBC", + "CaaacC", + 6 + ], + [ + "CaBCACabA", + "CbcABbc", + 10 + ], + [ + "CaBCBbC", + "ccbBbaC", + 8 + ], + [ + "CaBCCcaCc", + "CaC", + 12 + ], + [ + "CaBaaBcA", + "C", + 14 + ], + [ + "CaBacBc", + "c", + 12 + ], + [ + "CaBba", + "bbcCaAa", + 10 + ], + [ + "CaBbac", + "A", + 11 + ], + [ + "CaBcAc", + "CAAab", + 8 + ], + [ + "CaBcBAbB", + "BABCCca", + 12 + ], + [ + "CaBcaA", + "bcC", + 9 + ], + [ + "CaBccaAc", + "bBa", + 12 + ], + [ + "CaC", + "aC", + 2 + ], + [ + "CaCA", + "bcBA", + 6 + ], + [ + "CaCAAbCcB", + "aBCc", + 11 + ], + [ + "CaCABCcbA", + "bB", + 16 + ], + [ + "CaCACCaA", + "BCBBaaBCA", + 13 + ], + [ + "CaCAbBA", + "bCBb", + 10 + ], + [ + "CaCB", + "AB", + 5 + ], + [ + "CaCB", + "bAb", + 6 + ], + [ + "CaCBa", + "ACabCC", + 8 + ], + [ + "CaCBa", + "abBaa", + 6 + ], + [ + "CaCBaaC", + "bcACBa", + 8 + ], + [ + "CaCBbBBb", + "bab", + 12 + ], + [ + "CaCBc", + "cAC", + 6 + ], + [ + "CaCCbB", + "babA", + 8 + ], + [ + "CaCCbc", + "BAbcCcBB", + 11 + ], + [ + "CaCa", + "Aab", + 6 + ], + [ + "CaCa", + "CCccAab", + 9 + ], + [ + "CaCa", + "CcbcAAC", + 10 + ], + [ + "CaCa", + "abCCa", + 6 + ], + [ + "CaCaAB", + "aBb", + 9 + ], + [ + "CaCb", + "bCab", + 4 + ], + [ + "CaCbBcCCC", + "CaBBab", + 11 + ], + [ + "CaCbbCAcb", + "aABcAc", + 10 + ], + [ + "CaCbbaB", + "ABB", + 10 + ], + [ + "CaCc", + "A", + 7 + ], + [ + "CaCc", + "ccBbc", + 7 + ], + [ + "CaCcAAb", + "baCBCB", + 9 + ], + [ + "CaCcB", + "AA", + 9 + ], + [ + "CaCcBa", + "cAc", + 8 + ], + [ + "CaCcaa", + "CABabbB", + 11 + ], + [ + "CaCcbBA", + "CAbccCaa", + 9 + ], + [ + "Caa", + "Acc", + 6 + ], + [ + "Caa", + "BAcBcBCbb", + 16 + ], + [ + "Caa", + "abbAb", + 9 + ], + [ + "CaaABcBab", + "acACccaCa", + 12 + ], + [ + "CaaAaAa", + "bbaabAc", + 9 + ], + [ + "CaaAaAaba", + "aaCBc", + 13 + ], + [ + "CaaAbCcAb", + "bc", + 14 + ], + [ + "CaaBAab", + "CcC", + 12 + ], + [ + "CaaBcB", + "Caaa", + 6 + ], + [ + "CaaC", + "AAbBaC", + 7 + ], + [ + "CaaCA", + "A", + 8 + ], + [ + "CaaCbCB", + "c", + 13 + ], + [ + "Caaa", + "BBaBcA", + 9 + ], + [ + "CaaaCabaC", + "CAa", + 13 + ], + [ + "Caaab", + "a", + 8 + ], + [ + "CaaabAbc", + "ACc", + 12 + ], + [ + "CaaabCbB", + "ABcB", + 11 + ], + [ + "Caaabcc", + "babAaAC", + 10 + ], + [ + "Caaac", + "AAbbccabb", + 15 + ], + [ + "Caab", + "BacAc", + 7 + ], + [ + "CaacAaCb", + "AbccbacB", + 10 + ], + [ + "CaacCb", + "acbcCABBB", + 13 + ], + [ + "Caacaccbc", + "bCcbB", + 13 + ], + [ + "CaacccACC", + "baBCAaCA", + 12 + ], + [ + "Cab", + "AbBCbcBbc", + 14 + ], + [ + "Cab", + "AcC", + 6 + ], + [ + "Cab", + "BCA", + 5 + ], + [ + "Cab", + "BaB", + 3 + ], + [ + "Cab", + "c", + 5 + ], + [ + "CabA", + "b", + 6 + ], + [ + "CabAabA", + "cbCaBBb", + 10 + ], + [ + "CabAbcCB", + "acbCa", + 10 + ], + [ + "CabB", + "AACcCBcab", + 14 + ], + [ + "CabB", + "BabB", + 2 + ], + [ + "CabB", + "Ccba", + 4 + ], + [ + "CabBCa", + "AaCaab", + 10 + ], + [ + "CabBacbC", + "C", + 14 + ], + [ + "CabBbab", + "Bc", + 12 + ], + [ + "CabBbc", + "BbBCbcBca", + 12 + ], + [ + "CabBcC", + "AaAcCAC", + 9 + ], + [ + "CabCB", + "AaCBCab", + 8 + ], + [ + "CabCaAABb", + "BbaB", + 12 + ], + [ + "CabCb", + "BcCbacbb", + 10 + ], + [ + "Caba", + "AA", + 6 + ], + [ + "Caba", + "CBbBBbCC", + 12 + ], + [ + "CabaAAcbc", + "aA", + 14 + ], + [ + "CabaAaab", + "Bccb", + 13 + ], + [ + "CabaBCc", + "acBcBb", + 11 + ], + [ + "CabaCAC", + "bAaa", + 10 + ], + [ + "CababAAcB", + "a", + 16 + ], + [ + "Cabb", + "aBbABaA", + 11 + ], + [ + "CabbAAABB", + "ccA", + 15 + ], + [ + "CabbACc", + "ABCCca", + 10 + ], + [ + "CabbB", + "BabCaAC", + 10 + ], + [ + "CabbB", + "CbBbAccbc", + 12 + ], + [ + "CabbCaa", + "bbaBbcacC", + 10 + ], + [ + "CabbaAba", + "CABaCBAbA", + 9 + ], + [ + "CabbbAACB", + "Babac", + 12 + ], + [ + "CabcB", + "aBAbaA", + 9 + ], + [ + "CabcBaC", + "Cac", + 8 + ], + [ + "CabcaABb", + "cBBc", + 12 + ], + [ + "CabcaBcB", + "bAABcbCA", + 12 + ], + [ + "Cabcb", + "Ba", + 8 + ], + [ + "Cabccb", + "BBBA", + 11 + ], + [ + "Cac", + "A", + 5 + ], + [ + "Cac", + "BAcb", + 5 + ], + [ + "Cac", + "BCcCCac", + 8 + ], + [ + "Cac", + "Bc", + 4 + ], + [ + "Cac", + "CBaacCcCb", + 12 + ], + [ + "Cac", + "cC", + 4 + ], + [ + "Cac", + "cbACbC", + 9 + ], + [ + "CacA", + "BaBabBAA", + 12 + ], + [ + "CacA", + "CacbC", + 4 + ], + [ + "CacAb", + "acCAb", + 4 + ], + [ + "CacAcAa", + "ACbcC", + 10 + ], + [ + "CacAcBc", + "caABbAAAA", + 13 + ], + [ + "CacBBBC", + "B", + 12 + ], + [ + "CacBCa", + "CcbcBA", + 7 + ], + [ + "CacBaCbca", + "aaBaaaBc", + 11 + ], + [ + "CacBbbB", + "CaCbbaBbb", + 7 + ], + [ + "CacBc", + "bBbAC", + 9 + ], + [ + "CacC", + "AcC", + 3 + ], + [ + "CacCbaAcc", + "bCaAaCB", + 13 + ], + [ + "CacaAc", + "aAbC", + 8 + ], + [ + "Cacab", + "AaBCAa", + 8 + ], + [ + "Cacab", + "c", + 8 + ], + [ + "CacacC", + "AABCCAA", + 12 + ], + [ + "Cacb", + "ac", + 4 + ], + [ + "CacbB", + "BCbcBaa", + 9 + ], + [ + "CacbC", + "bBbCaBBB", + 11 + ], + [ + "CacbC", + "cCAAcCa", + 9 + ], + [ + "CacbCAccB", + "AAaBBcbBc", + 14 + ], + [ + "Cacc", + "AACc", + 4 + ], + [ + "CaccAC", + "BabBAaC", + 8 + ], + [ + "CaccBca", + "aCbb", + 10 + ], + [ + "CaccaAbbC", + "BBbC", + 13 + ], + [ + "CaccaCCa", + "cc", + 12 + ], + [ + "Cb", + "A", + 4 + ], + [ + "Cb", + "AACB", + 5 + ], + [ + "Cb", + "AACCbcBc", + 12 + ], + [ + "Cb", + "ABcAcaCcC", + 16 + ], + [ + "Cb", + "ACCBAaCaA", + 15 + ], + [ + "Cb", + "AaBCBCc", + 11 + ], + [ + "Cb", + "Aaa", + 6 + ], + [ + "Cb", + "AacabCcB", + 13 + ], + [ + "Cb", + "Ac", + 4 + ], + [ + "Cb", + "BABB", + 7 + ], + [ + "Cb", + "BBcAabC", + 11 + ], + [ + "Cb", + "Ba", + 4 + ], + [ + "Cb", + "BaaB", + 7 + ], + [ + "Cb", + "BbCaabCCC", + 14 + ], + [ + "Cb", + "CA", + 2 + ], + [ + "Cb", + "CCcBaAB", + 11 + ], + [ + "Cb", + "Cbb", + 2 + ], + [ + "Cb", + "CcBccBacC", + 15 + ], + [ + "Cb", + "aBcaCb", + 8 + ], + [ + "Cb", + "aCBCcAACA", + 15 + ], + [ + "Cb", + "aaAbc", + 8 + ], + [ + "Cb", + "aaCBBaB", + 11 + ], + [ + "Cb", + "aaaB", + 7 + ], + [ + "Cb", + "abBaC", + 8 + ], + [ + "Cb", + "abaaaCABA", + 15 + ], + [ + "Cb", + "acBbAAca", + 13 + ], + [ + "Cb", + "acbbabbCC", + 15 + ], + [ + "Cb", + "bcCCBA", + 9 + ], + [ + "Cb", + "c", + 3 + ], + [ + "Cb", + "cAbcbCA", + 11 + ], + [ + "Cb", + "cAcCbBA", + 10 + ], + [ + "Cb", + "cB", + 2 + ], + [ + "Cb", + "cBcB", + 6 + ], + [ + "Cb", + "caacb", + 7 + ], + [ + "Cb", + "cbCBBbacA", + 14 + ], + [ + "Cb", + "cbcbccA", + 11 + ], + [ + "CbA", + "AAC", + 6 + ], + [ + "CbA", + "BBCBaCC", + 10 + ], + [ + "CbA", + "BBcBB", + 8 + ], + [ + "CbA", + "baaA", + 6 + ], + [ + "CbA", + "c", + 5 + ], + [ + "CbA", + "caBAbA", + 7 + ], + [ + "CbAABaAC", + "A", + 14 + ], + [ + "CbAACACcC", + "ba", + 15 + ], + [ + "CbAACa", + "AaCaAccab", + 11 + ], + [ + "CbAAaAB", + "BCaBCcaaC", + 12 + ], + [ + "CbAAb", + "bacAabaAB", + 11 + ], + [ + "CbAAcAb", + "caC", + 11 + ], + [ + "CbAAcC", + "ABbcaCa", + 10 + ], + [ + "CbABACcaB", + "Babb", + 14 + ], + [ + "CbABC", + "b", + 8 + ], + [ + "CbABaBaAC", + "acaC", + 12 + ], + [ + "CbAC", + "A", + 6 + ], + [ + "CbAC", + "c", + 7 + ], + [ + "CbACaB", + "aBcA", + 9 + ], + [ + "CbACb", + "bcABcCC", + 10 + ], + [ + "CbACc", + "BbabcaBCA", + 13 + ], + [ + "CbACcABba", + "baABAcBc", + 12 + ], + [ + "CbAaAaaa", + "ACbBACbbc", + 13 + ], + [ + "CbAaabbC", + "A", + 14 + ], + [ + "CbAacB", + "c", + 10 + ], + [ + "CbAacBB", + "CacbBBbBA", + 12 + ], + [ + "CbAb", + "baBac", + 8 + ], + [ + "CbAbBaC", + "aCAaACb", + 11 + ], + [ + "CbAba", + "CbaA", + 4 + ], + [ + "CbAbb", + "Bc", + 9 + ], + [ + "CbAc", + "a", + 7 + ], + [ + "CbAcAC", + "bcbCB", + 8 + ], + [ + "CbAcB", + "aac", + 7 + ], + [ + "CbAcBCcaa", + "ccABcacAc", + 11 + ], + [ + "CbAcCb", + "AC", + 8 + ], + [ + "CbAcaAAA", + "accCbbACC", + 15 + ], + [ + "CbAcc", + "AABbcBBc", + 11 + ], + [ + "CbB", + "ABcCCBb", + 10 + ], + [ + "CbB", + "B", + 4 + ], + [ + "CbB", + "BB", + 3 + ], + [ + "CbB", + "BBb", + 4 + ], + [ + "CbB", + "ababacC", + 11 + ], + [ + "CbB", + "bAaaABc", + 12 + ], + [ + "CbB", + "cCb", + 4 + ], + [ + "CbBABACb", + "BCcBAA", + 10 + ], + [ + "CbBABBaA", + "BacaabAc", + 13 + ], + [ + "CbBAb", + "cB", + 7 + ], + [ + "CbBB", + "CcBCBCcAC", + 12 + ], + [ + "CbBBB", + "BCCcb", + 9 + ], + [ + "CbBBaC", + "ab", + 10 + ], + [ + "CbBBcCCBb", + "B", + 16 + ], + [ + "CbBCCbbB", + "cC", + 13 + ], + [ + "CbBa", + "a", + 6 + ], + [ + "CbBa", + "aAb", + 7 + ], + [ + "CbBabA", + "CcCB", + 9 + ], + [ + "CbBb", + "aBccCBA", + 11 + ], + [ + "CbBbC", + "CCCbbC", + 5 + ], + [ + "CbBbbba", + "BaCCABCcC", + 15 + ], + [ + "CbBbc", + "C", + 8 + ], + [ + "CbBc", + "aAaAAbBA", + 12 + ], + [ + "CbBcB", + "b", + 8 + ], + [ + "CbC", + "AcacbAB", + 11 + ], + [ + "CbC", + "BCAc", + 5 + ], + [ + "CbC", + "Bc", + 4 + ], + [ + "CbC", + "CbB", + 2 + ], + [ + "CbC", + "b", + 4 + ], + [ + "CbCAAbb", + "bAAaC", + 8 + ], + [ + "CbCABcaC", + "aCcAACB", + 12 + ], + [ + "CbCACA", + "baAac", + 8 + ], + [ + "CbCAa", + "cCbabBb", + 10 + ], + [ + "CbCB", + "BAcbcAba", + 11 + ], + [ + "CbCBBb", + "cAbACcA", + 11 + ], + [ + "CbCBC", + "cacCccCb", + 11 + ], + [ + "CbCBCAB", + "bBBcC", + 9 + ], + [ + "CbCBcCAaa", + "aAAaAa", + 14 + ], + [ + "CbCCCc", + "BcBCaba", + 10 + ], + [ + "CbCCbAccA", + "cCbBB", + 13 + ], + [ + "CbCCcCCaB", + "bcaaab", + 11 + ], + [ + "CbCa", + "cBBaCacA", + 10 + ], + [ + "CbCaAaB", + "acaa", + 9 + ], + [ + "CbCaB", + "ACbaa", + 6 + ], + [ + "CbCaB", + "AcbAa", + 7 + ], + [ + "CbCaBCb", + "bcA", + 10 + ], + [ + "CbCaa", + "ABCBCcAb", + 10 + ], + [ + "CbCaa", + "ba", + 6 + ], + [ + "CbCaaaC", + "bAcBcBaAa", + 12 + ], + [ + "CbCb", + "Bbc", + 5 + ], + [ + "CbCbaBa", + "AbacBbabc", + 10 + ], + [ + "CbCbcC", + "Ca", + 10 + ], + [ + "CbCc", + "ACC", + 5 + ], + [ + "CbCc", + "Cabbb", + 6 + ], + [ + "CbCcAaBaA", + "cbC", + 13 + ], + [ + "CbCcAcAc", + "AcBaCaAC", + 11 + ], + [ + "CbCcBAb", + "c", + 12 + ], + [ + "CbCcBC", + "CacBbCAc", + 10 + ], + [ + "CbCcBac", + "AAbAC", + 11 + ], + [ + "Cba", + "ABBBbaab", + 12 + ], + [ + "Cba", + "AcbAbB", + 8 + ], + [ + "Cba", + "C", + 4 + ], + [ + "Cba", + "CCCbbbC", + 10 + ], + [ + "Cba", + "CCaaAa", + 8 + ], + [ + "Cba", + "CcBCCAAAA", + 14 + ], + [ + "Cba", + "aABbaC", + 8 + ], + [ + "CbaA", + "CAbCBaB", + 8 + ], + [ + "CbaA", + "CBb", + 5 + ], + [ + "CbaABAC", + "aBCBCBc", + 12 + ], + [ + "CbaAaBCaB", + "AcCcB", + 12 + ], + [ + "CbaAcBb", + "C", + 12 + ], + [ + "CbaAcc", + "acaCaaccA", + 10 + ], + [ + "CbaBA", + "ab", + 7 + ], + [ + "CbaBBcBaB", + "bB", + 14 + ], + [ + "CbaBa", + "CBC", + 6 + ], + [ + "CbaBb", + "CCCbcB", + 8 + ], + [ + "CbaBba", + "BC", + 10 + ], + [ + "CbaBcAcbC", + "cc", + 14 + ], + [ + "CbaC", + "B", + 7 + ], + [ + "CbaCA", + "BAbaAacA", + 9 + ], + [ + "CbaCABBb", + "cAB", + 11 + ], + [ + "CbaCABcbA", + "BbaCbC", + 10 + ], + [ + "CbaCAaCc", + "a", + 14 + ], + [ + "CbaCAcCba", + "BCcaB", + 12 + ], + [ + "CbaCbCA", + "ccBBAcBc", + 12 + ], + [ + "CbaaA", + "BA", + 7 + ], + [ + "CbaaBA", + "AcccAAaac", + 14 + ], + [ + "CbabAACaC", + "cABaCcbB", + 13 + ], + [ + "CbabACa", + "A", + 12 + ], + [ + "CbabAac", + "aaaB", + 9 + ], + [ + "CbabAcC", + "cbb", + 9 + ], + [ + "CbabBBBcc", + "c", + 16 + ], + [ + "CbabBCCab", + "bB", + 14 + ], + [ + "CbabC", + "CacBB", + 7 + ], + [ + "CbabbBbb", + "BaAABCC", + 11 + ], + [ + "Cbabc", + "aACbC", + 7 + ], + [ + "CbabcB", + "CBcbAcb", + 6 + ], + [ + "CbacAab", + "c", + 12 + ], + [ + "CbacCAB", + "cCC", + 10 + ], + [ + "CbacCCb", + "caaC", + 9 + ], + [ + "CbacCaCAB", + "Cbbba", + 12 + ], + [ + "Cbacaca", + "BACAc", + 8 + ], + [ + "Cbb", + "Ababcb", + 8 + ], + [ + "Cbb", + "Bb", + 3 + ], + [ + "Cbb", + "CACbbBCb", + 10 + ], + [ + "Cbb", + "CBaABccaA", + 14 + ], + [ + "Cbb", + "abcb", + 4 + ], + [ + "Cbb", + "bACBBCCaa", + 14 + ], + [ + "Cbb", + "caCCACBaB", + 14 + ], + [ + "Cbb", + "cbAcABac", + 12 + ], + [ + "CbbA", + "cAACAA", + 9 + ], + [ + "CbbAACbC", + "ccaABC", + 9 + ], + [ + "CbbABCCA", + "CBCABCA", + 5 + ], + [ + "CbbAb", + "B", + 9 + ], + [ + "CbbAbbac", + "bCBbca", + 10 + ], + [ + "CbbAcaCbC", + "CAbBbcAc", + 11 + ], + [ + "CbbB", + "A", + 8 + ], + [ + "CbbB", + "C", + 6 + ], + [ + "CbbBA", + "BBCabAB", + 10 + ], + [ + "CbbBAbcA", + "CcCCAaBb", + 12 + ], + [ + "CbbBAcCba", + "BBbbCaBA", + 11 + ], + [ + "CbbBaBcB", + "cCabACB", + 10 + ], + [ + "CbbCCacBc", + "cbbAcacab", + 8 + ], + [ + "CbbaAacC", + "bcbcbbaBa", + 13 + ], + [ + "CbbaCBAb", + "aBCBBcC", + 13 + ], + [ + "CbbbBb", + "BA", + 10 + ], + [ + "CbbbaA", + "aacCccBbB", + 15 + ], + [ + "CbbbaACba", + "aCBba", + 12 + ], + [ + "Cbbbab", + "AABAbbc", + 11 + ], + [ + "Cbbc", + "bcaAb", + 9 + ], + [ + "Cbbc", + "cba", + 5 + ], + [ + "CbbcBcAc", + "BbC", + 12 + ], + [ + "CbbcCBacB", + "AaC", + 15 + ], + [ + "Cbbcbb", + "bBaBCBA", + 10 + ], + [ + "CbbcbcabC", + "AcBcBcba", + 10 + ], + [ + "Cbbcc", + "B", + 9 + ], + [ + "Cbbcc", + "CcbA", + 6 + ], + [ + "CbbccBAaB", + "cCA", + 13 + ], + [ + "Cbbccc", + "BcA", + 9 + ], + [ + "Cbc", + "A", + 6 + ], + [ + "Cbc", + "BAa", + 6 + ], + [ + "Cbc", + "BaabcacC", + 12 + ], + [ + "Cbc", + "CBbAc", + 4 + ], + [ + "Cbc", + "aCCbab", + 8 + ], + [ + "Cbc", + "baB", + 6 + ], + [ + "Cbc", + "bcB", + 4 + ], + [ + "Cbc", + "caACbaC", + 9 + ], + [ + "Cbc", + "cbabc", + 5 + ], + [ + "CbcAB", + "AabCABAb", + 9 + ], + [ + "CbcAaaBa", + "aACA", + 12 + ], + [ + "CbcB", + "CBcC", + 3 + ], + [ + "CbcBCbB", + "CAAacC", + 11 + ], + [ + "CbcBb", + "aaCACAAcC", + 15 + ], + [ + "CbcBcb", + "AabcC", + 9 + ], + [ + "CbcBcbC", + "aCa", + 13 + ], + [ + "CbcCAAc", + "aaBBbbc", + 12 + ], + [ + "CbcCAB", + "bbaCCcc", + 9 + ], + [ + "CbcCB", + "c", + 8 + ], + [ + "CbcCBB", + "aB", + 10 + ], + [ + "CbcCCa", + "BB", + 11 + ], + [ + "CbcCcA", + "aCaacCaAa", + 10 + ], + [ + "Cbca", + "cABAAaaA", + 12 + ], + [ + "CbcaC", + "bCcabCa", + 8 + ], + [ + "Cbcb", + "AA", + 8 + ], + [ + "Cbcb", + "AaCc", + 7 + ], + [ + "Cbcb", + "CbBAa", + 6 + ], + [ + "CbcbA", + "bccbACcC", + 10 + ], + [ + "CbcbB", + "aC", + 9 + ], + [ + "CbcbBCBC", + "cBB", + 10 + ], + [ + "CbcbC", + "a", + 10 + ], + [ + "Cbcbb", + "cccAB", + 6 + ], + [ + "Cbcbcbc", + "CbaccAcCC", + 9 + ], + [ + "Cbcc", + "BbAcAabc", + 10 + ], + [ + "Cbcc", + "bbBBA", + 8 + ], + [ + "CbccCAcBA", + "acCA", + 12 + ], + [ + "CbccaACc", + "AC", + 12 + ], + [ + "Cc", + "AAbABC", + 11 + ], + [ + "Cc", + "AAcbCAca", + 12 + ], + [ + "Cc", + "ABaC", + 7 + ], + [ + "Cc", + "AC", + 3 + ], + [ + "Cc", + "ACCcCa", + 8 + ], + [ + "Cc", + "AaCCC", + 7 + ], + [ + "Cc", + "BBCBCbbb", + 13 + ], + [ + "Cc", + "BbAbcC", + 10 + ], + [ + "Cc", + "Bc", + 2 + ], + [ + "Cc", + "Bccba", + 7 + ], + [ + "Cc", + "CABACbAcB", + 14 + ], + [ + "Cc", + "CAaCbCA", + 11 + ], + [ + "Cc", + "CCCCAaC", + 11 + ], + [ + "Cc", + "CCbCaAb", + 11 + ], + [ + "Cc", + "Ccb", + 2 + ], + [ + "Cc", + "a", + 4 + ], + [ + "Cc", + "aAABCBacb", + 14 + ], + [ + "Cc", + "aAAccbC", + 11 + ], + [ + "Cc", + "aBBCbc", + 8 + ], + [ + "Cc", + "aCAcaCca", + 12 + ], + [ + "Cc", + "bB", + 4 + ], + [ + "Cc", + "baABAABbC", + 17 + ], + [ + "Cc", + "bbacA", + 8 + ], + [ + "Cc", + "bc", + 2 + ], + [ + "Cc", + "c", + 2 + ], + [ + "Cc", + "cAaAAbc", + 11 + ], + [ + "Cc", + "cBbbBbcA", + 13 + ], + [ + "Cc", + "ca", + 3 + ], + [ + "Cc", + "ccAC", + 5 + ], + [ + "Cc", + "ccCb", + 5 + ], + [ + "CcA", + "BbcbaAB", + 10 + ], + [ + "CcA", + "aCA", + 3 + ], + [ + "CcA", + "bAAbca", + 9 + ], + [ + "CcA", + "bBccA", + 5 + ], + [ + "CcA", + "bCCBb", + 7 + ], + [ + "CcA", + "cBAbaC", + 9 + ], + [ + "CcAA", + "bcACbc", + 8 + ], + [ + "CcAAAAC", + "abCa", + 12 + ], + [ + "CcAAAB", + "bBAbaC", + 9 + ], + [ + "CcAAAcCb", + "cAcBcBBC", + 11 + ], + [ + "CcAABaC", + "BbC", + 10 + ], + [ + "CcAABbc", + "BCaAbab", + 9 + ], + [ + "CcAACB", + "b", + 11 + ], + [ + "CcAAaC", + "CAbaab", + 7 + ], + [ + "CcAB", + "BbcCBc", + 8 + ], + [ + "CcAB", + "accAaa", + 7 + ], + [ + "CcABACACb", + "abBBbCAA", + 12 + ], + [ + "CcABBccA", + "cc", + 12 + ], + [ + "CcABb", + "a", + 9 + ], + [ + "CcABbabBA", + "cCA", + 14 + ], + [ + "CcABcAaa", + "Ba", + 12 + ], + [ + "CcACAbc", + "AbBB", + 11 + ], + [ + "CcACbaCA", + "abbBaBbBa", + 16 + ], + [ + "CcACcCAa", + "AaB", + 13 + ], + [ + "CcAa", + "aAbBCCc", + 13 + ], + [ + "CcAa", + "ccBC", + 5 + ], + [ + "CcAaA", + "cbcC", + 8 + ], + [ + "CcAaB", + "babCBbCAC", + 15 + ], + [ + "CcAaCABA", + "cbaBaCCCb", + 12 + ], + [ + "CcAaCcbcB", + "aBbaCAAC", + 13 + ], + [ + "CcAac", + "Cabaac", + 5 + ], + [ + "CcAacBAaC", + "bCACB", + 12 + ], + [ + "CcAb", + "b", + 6 + ], + [ + "CcAbBcBaa", + "CBc", + 12 + ], + [ + "CcAbaaA", + "cCa", + 10 + ], + [ + "CcAbcaB", + "bcA", + 9 + ], + [ + "CcAc", + "AAaaABABA", + 16 + ], + [ + "CcAcAB", + "aabAabCcb", + 14 + ], + [ + "CcAcCcC", + "cACbbac", + 10 + ], + [ + "CcAcCcc", + "A", + 12 + ], + [ + "CcB", + "AbcBBCAac", + 14 + ], + [ + "CcB", + "Ac", + 4 + ], + [ + "CcB", + "Ccb", + 1 + ], + [ + "CcB", + "ba", + 6 + ], + [ + "CcB", + "cab", + 4 + ], + [ + "CcB", + "cbc", + 5 + ], + [ + "CcBABc", + "cAAC", + 7 + ], + [ + "CcBACb", + "ac", + 10 + ], + [ + "CcBAaAa", + "aCCcB", + 12 + ], + [ + "CcBB", + "bab", + 7 + ], + [ + "CcBBA", + "B", + 8 + ], + [ + "CcBBAb", + "aBCAAc", + 10 + ], + [ + "CcBBCAAC", + "A", + 14 + ], + [ + "CcBBbac", + "Bb", + 10 + ], + [ + "CcBCBaa", + "bbbbB", + 11 + ], + [ + "CcBCCaAbC", + "aAaCaCA", + 14 + ], + [ + "CcBCCbABb", + "BbBCbccaC", + 14 + ], + [ + "CcBCaacCB", + "CAbCcbA", + 11 + ], + [ + "CcBCc", + "C", + 8 + ], + [ + "CcBCc", + "cBcba", + 7 + ], + [ + "CcBCccbb", + "abaAAAa", + 15 + ], + [ + "CcBaBb", + "BAabCcBcB", + 12 + ], + [ + "CcBaa", + "abBac", + 6 + ], + [ + "CcBabaabc", + "bCB", + 15 + ], + [ + "CcBacBC", + "aa", + 12 + ], + [ + "CcBb", + "AAB", + 6 + ], + [ + "CcBb", + "BaAcbA", + 9 + ], + [ + "CcBbC", + "A", + 10 + ], + [ + "CcBbaCbb", + "abcBCCBab", + 11 + ], + [ + "CcBbbaB", + "baCb", + 11 + ], + [ + "CcBc", + "aBabCbbcB", + 13 + ], + [ + "CcBc", + "cabcACa", + 10 + ], + [ + "CcBcABAC", + "AAC", + 10 + ], + [ + "CcBcBCBaa", + "CBA", + 13 + ], + [ + "CcC", + "CBAacacBa", + 13 + ], + [ + "CcC", + "CBbcac", + 7 + ], + [ + "CcCAA", + "cB", + 8 + ], + [ + "CcCABC", + "ACA", + 8 + ], + [ + "CcCACCb", + "CbAbCccC", + 11 + ], + [ + "CcCAaCAaB", + "ABbAAaBb", + 12 + ], + [ + "CcCAbB", + "abb", + 8 + ], + [ + "CcCAbCBc", + "cbaCBaB", + 11 + ], + [ + "CcCB", + "ABbCBAc", + 10 + ], + [ + "CcCB", + "bcaabaccc", + 14 + ], + [ + "CcCB", + "cbBaC", + 8 + ], + [ + "CcCBA", + "CbAbcb", + 9 + ], + [ + "CcCBCcCA", + "BbCcaa", + 10 + ], + [ + "CcCBb", + "aBBC", + 8 + ], + [ + "CcCBc", + "BCBb", + 6 + ], + [ + "CcCC", + "b", + 8 + ], + [ + "CcCCAB", + "CCbB", + 6 + ], + [ + "CcCCBACCa", + "ABAABAAAb", + 14 + ], + [ + "CcCCacbB", + "aACA", + 13 + ], + [ + "CcCaA", + "baa", + 7 + ], + [ + "CcCaaaaA", + "bCbbACC", + 13 + ], + [ + "CcCacAB", + "BCCBacCcb", + 10 + ], + [ + "CcCacbC", + "ACBabB", + 9 + ], + [ + "CcCbB", + "BABCbb", + 7 + ], + [ + "CcCbCA", + "aBcA", + 8 + ], + [ + "CcCbCbBbc", + "ccabB", + 11 + ], + [ + "CcCba", + "BCb", + 6 + ], + [ + "CcCbaAbB", + "aBbcaCB", + 11 + ], + [ + "CcCbcaC", + "CcBbaCbCb", + 9 + ], + [ + "CcCc", + "cBAACa", + 9 + ], + [ + "CcCcAB", + "aCBbA", + 9 + ], + [ + "CcCcAc", + "baBacaB", + 11 + ], + [ + "CcCcBB", + "bcbBbcAbB", + 11 + ], + [ + "CcCcCCAb", + "BCBBaABab", + 13 + ], + [ + "CcCca", + "bCaBAB", + 10 + ], + [ + "CcCcaB", + "aAacCCb", + 10 + ], + [ + "CcCcbCb", + "BBBCBaCC", + 12 + ], + [ + "CcCcbCb", + "cC", + 10 + ], + [ + "CcCccCbbc", + "cabBaCB", + 14 + ], + [ + "Cca", + "AbbaBABCA", + 16 + ], + [ + "Cca", + "AcacBcc", + 10 + ], + [ + "Cca", + "a", + 4 + ], + [ + "Cca", + "b", + 6 + ], + [ + "Cca", + "bAbaCBB", + 12 + ], + [ + "CcaAB", + "B", + 8 + ], + [ + "CcaABA", + "a", + 10 + ], + [ + "CcaAC", + "BaCbAbaAa", + 12 + ], + [ + "CcaACB", + "BcBbAAA", + 10 + ], + [ + "CcaACCA", + "bCacCB", + 8 + ], + [ + "CcaAa", + "cCC", + 8 + ], + [ + "CcaAbCCC", + "AbCBCb", + 10 + ], + [ + "CcaAcb", + "bcBAa", + 8 + ], + [ + "CcaAcc", + "babAAAcbB", + 13 + ], + [ + "CcaB", + "BbaCbBbCB", + 14 + ], + [ + "CcaBA", + "ACCCABCBC", + 12 + ], + [ + "CcaBaBC", + "bacAbAab", + 11 + ], + [ + "CcaBaa", + "A", + 11 + ], + [ + "CcaBcBAB", + "Ac", + 13 + ], + [ + "CcaBcC", + "A", + 11 + ], + [ + "CcaCAaabB", + "CABCacccB", + 11 + ], + [ + "CcaCCAAA", + "BCAaCCcc", + 10 + ], + [ + "CcaCa", + "BAa", + 7 + ], + [ + "CcaCabAcb", + "BaaaBAC", + 10 + ], + [ + "CcaCacbba", + "bccAB", + 13 + ], + [ + "CcaCbbA", + "CcB", + 9 + ], + [ + "CcaCcBc", + "cbaCa", + 9 + ], + [ + "Ccaa", + "AabAcaC", + 10 + ], + [ + "CcaaA", + "aaCAA", + 7 + ], + [ + "CcaaAAbc", + "aABCaCacc", + 13 + ], + [ + "CcaaBC", + "abBCaC", + 10 + ], + [ + "Ccaaa", + "cc", + 7 + ], + [ + "CcaaaAB", + "bc", + 12 + ], + [ + "Ccaaaa", + "ABbbbb", + 12 + ], + [ + "Ccaaabccc", + "cA", + 15 + ], + [ + "CcaabbCCA", + "Bbc", + 14 + ], + [ + "Ccab", + "AC", + 7 + ], + [ + "Ccab", + "Cba", + 4 + ], + [ + "Ccab", + "abBAaBB", + 11 + ], + [ + "Ccab", + "abbBCaBb", + 11 + ], + [ + "CcabACcb", + "cCAaC", + 10 + ], + [ + "Ccac", + "cbCCA", + 8 + ], + [ + "CcacB", + "cCBbaCB", + 7 + ], + [ + "CcacBabBC", + "CaBBaB", + 8 + ], + [ + "CcacC", + "AbbacBa", + 10 + ], + [ + "CcacCABB", + "ba", + 14 + ], + [ + "CcacCaA", + "CaAC", + 8 + ], + [ + "CcacaBB", + "B", + 12 + ], + [ + "CcacbC", + "cCb", + 7 + ], + [ + "Ccb", + "AcbABaCC", + 12 + ], + [ + "Ccb", + "CAA", + 4 + ], + [ + "Ccb", + "CbAcC", + 6 + ], + [ + "Ccb", + "c", + 4 + ], + [ + "CcbA", + "BccAc", + 6 + ], + [ + "CcbA", + "aabCBb", + 10 + ], + [ + "CcbA", + "bABb", + 7 + ], + [ + "CcbAACA", + "bcc", + 11 + ], + [ + "CcbAACB", + "acBBAcac", + 10 + ], + [ + "CcbAAaA", + "ccBCaCa", + 8 + ], + [ + "CcbACBA", + "A", + 12 + ], + [ + "CcbACC", + "bbaC", + 7 + ], + [ + "CcbAaBcAA", + "bAcac", + 11 + ], + [ + "CcbAc", + "AcbbBBBa", + 12 + ], + [ + "CcbBAbCcC", + "AAb", + 14 + ], + [ + "CcbBaCC", + "ABcA", + 11 + ], + [ + "CcbBac", + "BCAAA", + 10 + ], + [ + "CcbBbBBA", + "AbB", + 12 + ], + [ + "CcbBbbbbb", + "bb", + 14 + ], + [ + "CcbBc", + "cBAcacB", + 11 + ], + [ + "CcbBcAC", + "BbCcAbcbB", + 11 + ], + [ + "CcbBcaA", + "bbcca", + 8 + ], + [ + "CcbBcc", + "bbcc", + 5 + ], + [ + "CcbCA", + "a", + 9 + ], + [ + "CcbCA", + "aabAA", + 6 + ], + [ + "CcbCbB", + "CACab", + 7 + ], + [ + "CcbCbCA", + "BcaCaCAb", + 8 + ], + [ + "CcbCc", + "AC", + 8 + ], + [ + "CcbCc", + "cBbbc", + 5 + ], + [ + "CcbCcC", + "c", + 10 + ], + [ + "CcbaccBCb", + "bbCBA", + 13 + ], + [ + "CcbbABb", + "CbCbacB", + 8 + ], + [ + "CcbbaAbbB", + "bc", + 16 + ], + [ + "Ccbbb", + "CBAcACB", + 9 + ], + [ + "CcbbbC", + "cBacabCb", + 11 + ], + [ + "CcbbbCbc", + "BbCAaBaB", + 15 + ], + [ + "CcbbcABBa", + "BcAcCcBCB", + 13 + ], + [ + "Ccbc", + "ABAaaBAb", + 15 + ], + [ + "Ccc", + "B", + 6 + ], + [ + "Ccc", + "BABa", + 8 + ], + [ + "Ccc", + "BcaBAc", + 8 + ], + [ + "Ccc", + "CBB", + 4 + ], + [ + "Ccc", + "CC", + 3 + ], + [ + "Ccc", + "aaaaCC", + 10 + ], + [ + "Ccc", + "ac", + 4 + ], + [ + "Ccc", + "bb", + 6 + ], + [ + "CccABA", + "AAbBcA", + 10 + ], + [ + "CccACCB", + "AcbaC", + 9 + ], + [ + "CccAaB", + "baCaBcCa", + 12 + ], + [ + "CccAb", + "aABBc", + 10 + ], + [ + "CccAcbbca", + "c", + 16 + ], + [ + "CccBBBcB", + "bBbbCa", + 11 + ], + [ + "CccBBbC", + "Bba", + 10 + ], + [ + "CccBa", + "B", + 8 + ], + [ + "CccBa", + "caBA", + 5 + ], + [ + "CccBaaAB", + "C", + 14 + ], + [ + "CccBbBCb", + "a", + 16 + ], + [ + "CccBbca", + "BacAbCBbC", + 12 + ], + [ + "CccCAa", + "CcBcBCCc", + 8 + ], + [ + "CccCCa", + "Ba", + 10 + ], + [ + "CccCa", + "aA", + 9 + ], + [ + "CccCb", + "baA", + 10 + ], + [ + "Ccca", + "A", + 7 + ], + [ + "CccaABbBb", + "aAcB", + 12 + ], + [ + "CccaBBCb", + "BBbaaCCC", + 12 + ], + [ + "Cccaa", + "BbbC", + 10 + ], + [ + "CccacCBb", + "ccBa", + 10 + ], + [ + "CccbABBbb", + "B", + 16 + ], + [ + "Cccbbc", + "CCcBcBa", + 7 + ], + [ + "Cccc", + "BaCbb", + 9 + ], + [ + "CcccAB", + "aCBAB", + 7 + ], + [ + "CcccAbbA", + "BCBcBc", + 12 + ], + [ + "CcccBacc", + "BCAcB", + 11 + ], + [ + "CcccBcCB", + "cCAb", + 12 + ], + [ + "a", + "AA", + 3 + ], + [ + "a", + "AABaCbca", + 14 + ], + [ + "a", + "AACaBBAC", + 14 + ], + [ + "a", + "ABCaa", + 8 + ], + [ + "a", + "ABaaBCCb", + 14 + ], + [ + "a", + "ABab", + 6 + ], + [ + "a", + "ABcB", + 7 + ], + [ + "a", + "ACbcCBCB", + 15 + ], + [ + "a", + "ACbcaA", + 10 + ], + [ + "a", + "AaAA", + 6 + ], + [ + "a", + "AaABacaAA", + 16 + ], + [ + "a", + "AaAbBca", + 12 + ], + [ + "a", + "AabBCaab", + 14 + ], + [ + "a", + "AabCbCb", + 12 + ], + [ + "a", + "AaccB", + 8 + ], + [ + "a", + "Ab", + 3 + ], + [ + "a", + "AbBBaB", + 10 + ], + [ + "a", + "AbCb", + 7 + ], + [ + "a", + "AbCc", + 7 + ], + [ + "a", + "AbbbBacAa", + 16 + ], + [ + "a", + "AbcCaCbac", + 16 + ], + [ + "a", + "AcABaA", + 10 + ], + [ + "a", + "AcACCA", + 11 + ], + [ + "a", + "AcCbc", + 9 + ], + [ + "a", + "AcaB", + 6 + ], + [ + "a", + "B", + 2 + ], + [ + "a", + "BA", + 3 + ], + [ + "a", + "BABAAcBBc", + 17 + ], + [ + "a", + "BABaBa", + 10 + ], + [ + "a", + "BAC", + 5 + ], + [ + "a", + "BAaAbAc", + 12 + ], + [ + "a", + "BAabccCBc", + 16 + ], + [ + "a", + "BAbaaa", + 10 + ], + [ + "a", + "BAbb", + 7 + ], + [ + "a", + "BBAcbBB", + 13 + ], + [ + "a", + "BBBaCab", + 12 + ], + [ + "a", + "BBC", + 6 + ], + [ + "a", + "BBaCbBAC", + 14 + ], + [ + "a", + "BBcca", + 8 + ], + [ + "a", + "BCA", + 5 + ], + [ + "a", + "BCBab", + 8 + ], + [ + "a", + "BaAAAcAbc", + 16 + ], + [ + "a", + "BaAcABBC", + 14 + ], + [ + "a", + "BaBcaC", + 10 + ], + [ + "a", + "BaC", + 4 + ], + [ + "a", + "BaabBAabb", + 16 + ], + [ + "a", + "BbCaCa", + 10 + ], + [ + "a", + "BbcACCA", + 13 + ], + [ + "a", + "Bbcba", + 8 + ], + [ + "a", + "BcAb", + 7 + ], + [ + "a", + "BcBABC", + 11 + ], + [ + "a", + "BcBaAacC", + 14 + ], + [ + "a", + "Bca", + 4 + ], + [ + "a", + "Bcabba", + 10 + ], + [ + "a", + "BccCbbAb", + 15 + ], + [ + "a", + "C", + 2 + ], + [ + "a", + "CA", + 3 + ], + [ + "a", + "CABAcBbc", + 15 + ], + [ + "a", + "CABCB", + 9 + ], + [ + "a", + "CAbC", + 7 + ], + [ + "a", + "CAba", + 6 + ], + [ + "a", + "CAcCAa", + 10 + ], + [ + "a", + "CAcc", + 7 + ], + [ + "a", + "CBA", + 5 + ], + [ + "a", + "CBABB", + 9 + ], + [ + "a", + "CBAa", + 6 + ], + [ + "a", + "CBBCcB", + 12 + ], + [ + "a", + "CBBcA", + 9 + ], + [ + "a", + "CBaCAC", + 10 + ], + [ + "a", + "CBbaCCbC", + 14 + ], + [ + "a", + "CCBc", + 8 + ], + [ + "a", + "CCcaC", + 8 + ], + [ + "a", + "Ca", + 2 + ], + [ + "a", + "CaAaC", + 8 + ], + [ + "a", + "CaBacbb", + 12 + ], + [ + "a", + "CaabacC", + 12 + ], + [ + "a", + "CbBc", + 8 + ], + [ + "a", + "CbbcAB", + 11 + ], + [ + "a", + "CcBBaaCc", + 14 + ], + [ + "a", + "CcBbabBAc", + 16 + ], + [ + "a", + "CcaACcB", + 12 + ], + [ + "a", + "Ccca", + 6 + ], + [ + "a", + "a", + 0 + ], + [ + "a", + "aA", + 2 + ], + [ + "a", + "aAACAbCC", + 14 + ], + [ + "a", + "aACABcAb", + 14 + ], + [ + "a", + "aAbaB", + 8 + ], + [ + "a", + "aBB", + 4 + ], + [ + "a", + "aBBCb", + 8 + ], + [ + "a", + "aBaCbb", + 10 + ], + [ + "a", + "aBb", + 4 + ], + [ + "a", + "aC", + 2 + ], + [ + "a", + "aCCCCcab", + 14 + ], + [ + "a", + "aCaBBABa", + 14 + ], + [ + "a", + "aCaC", + 6 + ], + [ + "a", + "aCb", + 4 + ], + [ + "a", + "aCcBaAa", + 12 + ], + [ + "a", + "aa", + 2 + ], + [ + "a", + "aaBbC", + 8 + ], + [ + "a", + "aaCA", + 6 + ], + [ + "a", + "ab", + 2 + ], + [ + "a", + "abACBBAcC", + 16 + ], + [ + "a", + "abBbBba", + 12 + ], + [ + "a", + "abCaAaCbC", + 16 + ], + [ + "a", + "aba", + 4 + ], + [ + "a", + "abcac", + 8 + ], + [ + "a", + "acAcBAbaB", + 16 + ], + [ + "a", + "acBcAA", + 10 + ], + [ + "a", + "accCCCc", + 12 + ], + [ + "a", + "b", + 2 + ], + [ + "a", + "bA", + 3 + ], + [ + "a", + "bAAaB", + 8 + ], + [ + "a", + "bABBa", + 8 + ], + [ + "a", + "bACca", + 8 + ], + [ + "a", + "bAbAccb", + 13 + ], + [ + "a", + "bAccbcaAB", + 16 + ], + [ + "a", + "bBAca", + 8 + ], + [ + "a", + "bBBC", + 8 + ], + [ + "a", + "bBBCaccC", + 14 + ], + [ + "a", + "bBCb", + 8 + ], + [ + "a", + "bBbC", + 8 + ], + [ + "a", + "bBbCABa", + 12 + ], + [ + "a", + "bC", + 4 + ], + [ + "a", + "bCBacAcC", + 14 + ], + [ + "a", + "bCC", + 6 + ], + [ + "a", + "bCCbaccA", + 14 + ], + [ + "a", + "bCbA", + 7 + ], + [ + "a", + "bCbBab", + 10 + ], + [ + "a", + "bCcaCbc", + 12 + ], + [ + "a", + "bCcaaCbB", + 14 + ], + [ + "a", + "ba", + 2 + ], + [ + "a", + "baa", + 4 + ], + [ + "a", + "baacacaba", + 16 + ], + [ + "a", + "babcA", + 8 + ], + [ + "a", + "babcCAA", + 12 + ], + [ + "a", + "bb", + 4 + ], + [ + "a", + "bbAc", + 7 + ], + [ + "a", + "bbB", + 6 + ], + [ + "a", + "bbBA", + 7 + ], + [ + "a", + "bbBBcaBc", + 14 + ], + [ + "a", + "bbBbAaAAc", + 16 + ], + [ + "a", + "bbCCaACb", + 14 + ], + [ + "a", + "bbCaca", + 10 + ], + [ + "a", + "bbaC", + 6 + ], + [ + "a", + "bbbAcAB", + 13 + ], + [ + "a", + "bbbBAABC", + 15 + ], + [ + "a", + "bbcCBcB", + 14 + ], + [ + "a", + "bcAca", + 8 + ], + [ + "a", + "bcAcacCB", + 14 + ], + [ + "a", + "bcaAcBAbC", + 16 + ], + [ + "a", + "bcb", + 6 + ], + [ + "a", + "bcc", + 6 + ], + [ + "a", + "c", + 2 + ], + [ + "a", + "cAC", + 5 + ], + [ + "a", + "cACBACca", + 14 + ], + [ + "a", + "cACaCa", + 10 + ], + [ + "a", + "cAa", + 4 + ], + [ + "a", + "cBAaa", + 8 + ], + [ + "a", + "cBbBbab", + 12 + ], + [ + "a", + "cC", + 4 + ], + [ + "a", + "cCBA", + 7 + ], + [ + "a", + "cCCa", + 6 + ], + [ + "a", + "cCCbaa", + 10 + ], + [ + "a", + "cCaCcb", + 10 + ], + [ + "a", + "cCacac", + 10 + ], + [ + "a", + "cCcBbCBAa", + 16 + ], + [ + "a", + "cabbcaBBc", + 16 + ], + [ + "a", + "cb", + 4 + ], + [ + "a", + "cbAcAab", + 12 + ], + [ + "a", + "cbAcCB", + 11 + ], + [ + "a", + "cbBaC", + 8 + ], + [ + "a", + "cbCcbC", + 12 + ], + [ + "a", + "cbaCbbBb", + 14 + ], + [ + "a", + "cbbAaCA", + 12 + ], + [ + "a", + "ccACCaBAa", + 16 + ], + [ + "a", + "ccACbaC", + 12 + ], + [ + "a", + "ccAbAC", + 11 + ], + [ + "a", + "ccAbbaBA", + 14 + ], + [ + "a", + "ccCBaCcB", + 14 + ], + [ + "a", + "ccac", + 6 + ], + [ + "aA", + "AAc", + 3 + ], + [ + "aA", + "ABcACbCcb", + 15 + ], + [ + "aA", + "ACcbCBA", + 11 + ], + [ + "aA", + "AacACCa", + 10 + ], + [ + "aA", + "B", + 4 + ], + [ + "aA", + "BabBBcCbC", + 16 + ], + [ + "aA", + "Babb", + 6 + ], + [ + "aA", + "BcAcB", + 8 + ], + [ + "aA", + "CAacB", + 8 + ], + [ + "aA", + "CBABBaAB", + 12 + ], + [ + "aA", + "CaBbbbA", + 10 + ], + [ + "aA", + "CcAabB", + 10 + ], + [ + "aA", + "CcaAb", + 6 + ], + [ + "aA", + "a", + 2 + ], + [ + "aA", + "aCAcAb", + 8 + ], + [ + "aA", + "aCBb", + 6 + ], + [ + "aA", + "aCacaAA", + 10 + ], + [ + "aA", + "aaACBacA", + 12 + ], + [ + "aA", + "aaC", + 3 + ], + [ + "aA", + "abbAaBC", + 10 + ], + [ + "aA", + "abbBBbc", + 12 + ], + [ + "aA", + "bAc", + 4 + ], + [ + "aA", + "bB", + 4 + ], + [ + "aA", + "bBCBC", + 10 + ], + [ + "aA", + "baCBAbCa", + 12 + ], + [ + "aA", + "cABBaA", + 8 + ], + [ + "aA", + "cBc", + 6 + ], + [ + "aA", + "cabbabbA", + 12 + ], + [ + "aA", + "ccbcB", + 10 + ], + [ + "aAA", + "aACbBCCb", + 12 + ], + [ + "aAA", + "ac", + 4 + ], + [ + "aAA", + "bAc", + 4 + ], + [ + "aAAA", + "AaAC", + 4 + ], + [ + "aAAAA", + "c", + 10 + ], + [ + "aAAAAC", + "CbaaCCBBa", + 14 + ], + [ + "aAAAAaAA", + "cbAabBb", + 13 + ], + [ + "aAAABb", + "BABBaB", + 9 + ], + [ + "aAAAC", + "bC", + 8 + ], + [ + "aAAAaAb", + "AAC", + 10 + ], + [ + "aAAAabBca", + "AB", + 14 + ], + [ + "aAAAcCbCB", + "CcBbABBB", + 15 + ], + [ + "aAAB", + "A", + 6 + ], + [ + "aAAB", + "bcabAb", + 7 + ], + [ + "aAAB", + "c", + 8 + ], + [ + "aAAB", + "cCAB", + 4 + ], + [ + "aAABCcCcC", + "CbBBCAB", + 14 + ], + [ + "aAABbBACb", + "aACb", + 10 + ], + [ + "aAACC", + "ccbbaBA", + 13 + ], + [ + "aAACaCa", + "ACbabAc", + 11 + ], + [ + "aAAaAAaa", + "ACBcbbcaA", + 14 + ], + [ + "aAAaaaB", + "BABbaaAbA", + 10 + ], + [ + "aAAabaAbA", + "bcCBbaBB", + 13 + ], + [ + "aAAacaaC", + "a", + 14 + ], + [ + "aAAb", + "aCAA", + 4 + ], + [ + "aAAbBbaA", + "BaaaAbb", + 11 + ], + [ + "aAAbCbCcC", + "cBC", + 14 + ], + [ + "aAAbbA", + "bcbbaBa", + 10 + ], + [ + "aAAc", + "AccBbB", + 10 + ], + [ + "aAAcA", + "BBBABa", + 9 + ], + [ + "aAAcA", + "bb", + 10 + ], + [ + "aAAcAA", + "aCAA", + 5 + ], + [ + "aAAcAa", + "CbCAa", + 7 + ], + [ + "aAAcAaa", + "bAAACb", + 8 + ], + [ + "aAAcB", + "AbcACA", + 8 + ], + [ + "aAAcBBba", + "BcaAcACb", + 11 + ], + [ + "aAAcCCbCB", + "C", + 16 + ], + [ + "aAAcCaCc", + "caACAaAa", + 10 + ], + [ + "aAAcCb", + "bbAbA", + 10 + ], + [ + "aAAcabaab", + "cAACAbb", + 8 + ], + [ + "aAAcbACab", + "bCcAaBbCC", + 15 + ], + [ + "aAAcc", + "aaAC", + 4 + ], + [ + "aAB", + "AAaAcCaa", + 12 + ], + [ + "aAB", + "AbCBB", + 7 + ], + [ + "aAB", + "AcaAB", + 4 + ], + [ + "aAB", + "CAbaA", + 7 + ], + [ + "aAB", + "CCBAcbaCC", + 15 + ], + [ + "aAB", + "aBcbbB", + 8 + ], + [ + "aAB", + "aCCcaBCC", + 11 + ], + [ + "aAB", + "bcbccABCA", + 14 + ], + [ + "aABA", + "cCac", + 8 + ], + [ + "aABAB", + "AaaBBcCA", + 11 + ], + [ + "aABACAa", + "CcBBbA", + 10 + ], + [ + "aABAaA", + "C", + 12 + ], + [ + "aABAb", + "BbBCC", + 8 + ], + [ + "aABAb", + "ca", + 9 + ], + [ + "aABAbab", + "a", + 12 + ], + [ + "aABAc", + "AabAabaB", + 10 + ], + [ + "aABAcCbbA", + "aB", + 14 + ], + [ + "aABB", + "CCAaaccbA", + 14 + ], + [ + "aABB", + "aCac", + 6 + ], + [ + "aABB", + "cb", + 7 + ], + [ + "aABBBBCc", + "cBAcbb", + 13 + ], + [ + "aABBBBa", + "CaBaCCBB", + 11 + ], + [ + "aABBCc", + "cc", + 9 + ], + [ + "aABBacCA", + "BBbacBc", + 9 + ], + [ + "aABBbA", + "aCBbC", + 6 + ], + [ + "aABBbaB", + "CbbBCbacb", + 10 + ], + [ + "aABCAa", + "bBbC", + 9 + ], + [ + "aABCB", + "cAB", + 6 + ], + [ + "aABCCcbB", + "CBbaCcbc", + 9 + ], + [ + "aABCaca", + "Bb", + 12 + ], + [ + "aABCbCba", + "CabccBaB", + 11 + ], + [ + "aABaBA", + "BC", + 10 + ], + [ + "aABaBbAAA", + "B", + 16 + ], + [ + "aABb", + "BC", + 6 + ], + [ + "aABbAa", + "bCbBcC", + 10 + ], + [ + "aABbBbCca", + "ABCCBAbAc", + 12 + ], + [ + "aABbaA", + "bcBC", + 10 + ], + [ + "aABbac", + "AbBbABAAb", + 12 + ], + [ + "aABbccca", + "BcaA", + 11 + ], + [ + "aABcACB", + "A", + 12 + ], + [ + "aABccB", + "A", + 10 + ], + [ + "aAC", + "BA", + 4 + ], + [ + "aAC", + "CAbbaab", + 11 + ], + [ + "aAC", + "CbC", + 4 + ], + [ + "aAC", + "b", + 6 + ], + [ + "aAC", + "bACCbC", + 8 + ], + [ + "aAC", + "bcaBacc", + 10 + ], + [ + "aACABAcCa", + "ccCB", + 13 + ], + [ + "aACBA", + "Aa", + 7 + ], + [ + "aACBACA", + "BaCBa", + 8 + ], + [ + "aACBBcBC", + "A", + 14 + ], + [ + "aACCA", + "Cb", + 8 + ], + [ + "aACCAAb", + "Bca", + 12 + ], + [ + "aACCBc", + "AcbAaCB", + 9 + ], + [ + "aACCa", + "ACccABA", + 10 + ], + [ + "aACCbAA", + "AAaa", + 9 + ], + [ + "aACCc", + "BBcc", + 7 + ], + [ + "aACCc", + "cbaBAC", + 10 + ], + [ + "aACaaA", + "bcACaCbA", + 8 + ], + [ + "aACacACa", + "aaBccaB", + 10 + ], + [ + "aACacCb", + "C", + 12 + ], + [ + "aACaccCAC", + "cC", + 14 + ], + [ + "aACbAC", + "AB", + 9 + ], + [ + "aACbBBcb", + "b", + 14 + ], + [ + "aACbbbaBC", + "aBBACCB", + 13 + ], + [ + "aACcaBb", + "ABabA", + 9 + ], + [ + "aACcbCa", + "b", + 12 + ], + [ + "aAa", + "BbBb", + 8 + ], + [ + "aAa", + "CBCBAabb", + 12 + ], + [ + "aAa", + "Cc", + 6 + ], + [ + "aAa", + "bCaaCBcAa", + 12 + ], + [ + "aAa", + "bc", + 6 + ], + [ + "aAaA", + "b", + 8 + ], + [ + "aAaAcc", + "B", + 12 + ], + [ + "aAaAccAb", + "BCAbabcAA", + 11 + ], + [ + "aAaBBCb", + "Cbbc", + 11 + ], + [ + "aAaBaBBBc", + "cAcCCCA", + 16 + ], + [ + "aAaBaBa", + "bBbBc", + 10 + ], + [ + "aAaCB", + "cCc", + 8 + ], + [ + "aAaCCCc", + "bbcBabaAA", + 16 + ], + [ + "aAaCaaAc", + "BCaA", + 10 + ], + [ + "aAaa", + "bb", + 8 + ], + [ + "aAaaAcCA", + "cBbcccB", + 13 + ], + [ + "aAaaBbbb", + "bCca", + 14 + ], + [ + "aAaaC", + "aCAC", + 5 + ], + [ + "aAaaCBa", + "aaCBCc", + 8 + ], + [ + "aAaaaA", + "aBACbaBAC", + 10 + ], + [ + "aAaabBcb", + "AcCAcBA", + 12 + ], + [ + "aAab", + "C", + 8 + ], + [ + "aAab", + "bCaB", + 5 + ], + [ + "aAab", + "bbaacCCB", + 12 + ], + [ + "aAabC", + "ccbBa", + 9 + ], + [ + "aAabCAc", + "aBCb", + 9 + ], + [ + "aAabCCBaa", + "bBC", + 14 + ], + [ + "aAabbbbB", + "CBAACBCCb", + 13 + ], + [ + "aAabbcCaA", + "A", + 16 + ], + [ + "aAac", + "cCBbCAaBB", + 14 + ], + [ + "aAacB", + "cAcbcBa", + 8 + ], + [ + "aAacBcCBa", + "cba", + 13 + ], + [ + "aAb", + "A", + 4 + ], + [ + "aAb", + "a", + 4 + ], + [ + "aAb", + "b", + 4 + ], + [ + "aAbA", + "bccaaaCAB", + 13 + ], + [ + "aAbABAA", + "bCBAcabBb", + 14 + ], + [ + "aAbAC", + "cACcCa", + 8 + ], + [ + "aAbACaBAA", + "CB", + 14 + ], + [ + "aAbAb", + "cBBabc", + 8 + ], + [ + "aAbAcCb", + "c", + 12 + ], + [ + "aAbBB", + "AC", + 8 + ], + [ + "aAbBaBcAA", + "C", + 17 + ], + [ + "aAbCAcbCC", + "BAAaA", + 14 + ], + [ + "aAbCBA", + "aCBaCABc", + 9 + ], + [ + "aAbCCACCb", + "AbcacbAb", + 11 + ], + [ + "aAbCCC", + "Aca", + 9 + ], + [ + "aAbCa", + "bABc", + 6 + ], + [ + "aAbCbCa", + "AabbBCbA", + 8 + ], + [ + "aAbCbbaba", + "CcBbccaB", + 14 + ], + [ + "aAbCc", + "cCa", + 8 + ], + [ + "aAba", + "BbcbcCBcC", + 16 + ], + [ + "aAbaBA", + "aBAAAA", + 7 + ], + [ + "aAbaBBba", + "Cb", + 14 + ], + [ + "aAbaCAA", + "cAB", + 11 + ], + [ + "aAbaaBA", + "bcCbaccBA", + 10 + ], + [ + "aAbb", + "bBBaACCBC", + 13 + ], + [ + "aAbbABabC", + "cbACbB", + 12 + ], + [ + "aAbbBab", + "caaA", + 11 + ], + [ + "aAbbCAA", + "BBbBcbA", + 8 + ], + [ + "aAbbCbCCa", + "AAB", + 14 + ], + [ + "aAbbCbcb", + "CBc", + 11 + ], + [ + "aAbbaB", + "a", + 10 + ], + [ + "aAbbacAb", + "BccBb", + 11 + ], + [ + "aAbbcbA", + "A", + 12 + ], + [ + "aAbcCaA", + "CCc", + 11 + ], + [ + "aAbcaABCA", + "BCAcbCab", + 13 + ], + [ + "aAbcaAa", + "CCababB", + 11 + ], + [ + "aAbcbbB", + "baba", + 10 + ], + [ + "aAc", + "aBbBCbac", + 11 + ], + [ + "aAc", + "abbAbaC", + 9 + ], + [ + "aAcA", + "A", + 6 + ], + [ + "aAcA", + "ACABBCB", + 10 + ], + [ + "aAcAAb", + "ACC", + 9 + ], + [ + "aAcAAbB", + "cAbbBaCBb", + 13 + ], + [ + "aAcAabacA", + "aCbBcA", + 9 + ], + [ + "aAcAacaba", + "caACb", + 11 + ], + [ + "aAcAbaCac", + "bBc", + 14 + ], + [ + "aAcAcAbaB", + "bcCCa", + 13 + ], + [ + "aAcB", + "aaaCC", + 6 + ], + [ + "aAcB", + "caC", + 6 + ], + [ + "aAcBACBB", + "a", + 14 + ], + [ + "aAcBC", + "aCca", + 6 + ], + [ + "aAcBbABB", + "AA", + 12 + ], + [ + "aAcBbAaBC", + "BbCbaCcaC", + 14 + ], + [ + "aAcCAaC", + "b", + 14 + ], + [ + "aAcCCcAc", + "baabCBAaA", + 12 + ], + [ + "aAcCccBB", + "AaAa", + 14 + ], + [ + "aAcaAaB", + "AcC", + 10 + ], + [ + "aAcaBaA", + "ACcaaA", + 5 + ], + [ + "aAcaaac", + "BaABaBaBA", + 10 + ], + [ + "aAcb", + "AABbA", + 5 + ], + [ + "aAcb", + "aABC", + 4 + ], + [ + "aAcbA", + "cbcAAC", + 8 + ], + [ + "aAcbBb", + "CbcccCBcC", + 14 + ], + [ + "aAcbBcca", + "A", + 14 + ], + [ + "aAcba", + "acBcBBBAc", + 12 + ], + [ + "aAcba", + "b", + 8 + ], + [ + "aAcc", + "CACC", + 4 + ], + [ + "aAccac", + "ACABCBBAC", + 12 + ], + [ + "aAccbCaba", + "BCBbBc", + 14 + ], + [ + "aB", + "A", + 3 + ], + [ + "aB", + "AA", + 3 + ], + [ + "aB", + "AB", + 1 + ], + [ + "aB", + "ABAb", + 5 + ], + [ + "aB", + "ACcb", + 6 + ], + [ + "aB", + "Aabaaa", + 9 + ], + [ + "aB", + "AccabABB", + 12 + ], + [ + "aB", + "B", + 2 + ], + [ + "aB", + "BBcbB", + 8 + ], + [ + "aB", + "BCc", + 6 + ], + [ + "aB", + "Bca", + 6 + ], + [ + "aB", + "C", + 4 + ], + [ + "aB", + "CAAaa", + 8 + ], + [ + "aB", + "CbccACC", + 13 + ], + [ + "aB", + "Cc", + 4 + ], + [ + "aB", + "CcAcCC", + 11 + ], + [ + "aB", + "aCbBBc", + 8 + ], + [ + "aB", + "aaAbCb", + 9 + ], + [ + "aB", + "aaaAb", + 7 + ], + [ + "aB", + "aabCACc", + 11 + ], + [ + "aB", + "abAC", + 5 + ], + [ + "aB", + "bB", + 2 + ], + [ + "aB", + "bBB", + 4 + ], + [ + "aB", + "bb", + 3 + ], + [ + "aB", + "bbbca", + 9 + ], + [ + "aB", + "bbc", + 5 + ], + [ + "aB", + "bc", + 4 + ], + [ + "aB", + "bcb", + 5 + ], + [ + "aB", + "cB", + 2 + ], + [ + "aB", + "cCC", + 6 + ], + [ + "aB", + "cc", + 4 + ], + [ + "aBA", + "BAaAaaCBc", + 14 + ], + [ + "aBAAA", + "CBCAbbcAA", + 10 + ], + [ + "aBAAB", + "BacBABcAc", + 10 + ], + [ + "aBAAaB", + "caAc", + 9 + ], + [ + "aBAAacaa", + "acabBBcBC", + 13 + ], + [ + "aBAAbCcB", + "BCB", + 10 + ], + [ + "aBAAcbCc", + "cBCBBcAAB", + 14 + ], + [ + "aBAB", + "aaCc", + 6 + ], + [ + "aBAB", + "bbaAbA", + 8 + ], + [ + "aBABa", + "bacAa", + 6 + ], + [ + "aBABaABc", + "AAaCCC", + 10 + ], + [ + "aBABcaABA", + "CBcbB", + 12 + ], + [ + "aBACC", + "a", + 8 + ], + [ + "aBACa", + "c", + 9 + ], + [ + "aBACba", + "C", + 10 + ], + [ + "aBACcAA", + "B", + 12 + ], + [ + "aBAa", + "aaB", + 5 + ], + [ + "aBAaC", + "cCACbbBb", + 14 + ], + [ + "aBAabaAB", + "cacCbb", + 13 + ], + [ + "aBAabba", + "AaBABbCBc", + 9 + ], + [ + "aBAacCbbb", + "aBaCaAc", + 10 + ], + [ + "aBAacc", + "aAaAAcABC", + 10 + ], + [ + "aBAb", + "aBBbAac", + 8 + ], + [ + "aBAbAC", + "aABAbCAc", + 5 + ], + [ + "aBAbaaab", + "CCbbAaba", + 11 + ], + [ + "aBAbbB", + "aCacbAB", + 7 + ], + [ + "aBAbbcbb", + "bcBAb", + 11 + ], + [ + "aBAcABbb", + "Ca", + 14 + ], + [ + "aBAcBa", + "CbBcCaa", + 9 + ], + [ + "aBAcBbC", + "aaAaBac", + 7 + ], + [ + "aBAca", + "AACACCcAa", + 11 + ], + [ + "aBAcabBA", + "BBACb", + 9 + ], + [ + "aBAcba", + "bBaaCcB", + 10 + ], + [ + "aBAccCaba", + "CAcabBBbC", + 14 + ], + [ + "aBAcca", + "ABAABbCa", + 8 + ], + [ + "aBB", + "AaAbaAaBb", + 13 + ], + [ + "aBB", + "AaCaB", + 6 + ], + [ + "aBB", + "Acbc", + 6 + ], + [ + "aBB", + "aC", + 4 + ], + [ + "aBB", + "aa", + 4 + ], + [ + "aBB", + "bbCAAaBBB", + 12 + ], + [ + "aBBA", + "BAAc", + 6 + ], + [ + "aBBABAaAA", + "AbCAcbAA", + 10 + ], + [ + "aBBABaaaa", + "bCC", + 17 + ], + [ + "aBBABca", + "bA", + 11 + ], + [ + "aBBACC", + "caaCB", + 9 + ], + [ + "aBBAc", + "aCAA", + 6 + ], + [ + "aBBB", + "AcBcBa", + 7 + ], + [ + "aBBBA", + "CcbaAB", + 9 + ], + [ + "aBBBABAB", + "BBA", + 10 + ], + [ + "aBBBACB", + "Aa", + 12 + ], + [ + "aBBBAaA", + "bB", + 11 + ], + [ + "aBBBB", + "AaBb", + 6 + ], + [ + "aBBBCCBbb", + "BCBca", + 12 + ], + [ + "aBBBaBaAa", + "cCCB", + 16 + ], + [ + "aBBC", + "AbabA", + 7 + ], + [ + "aBBC", + "CcbcBBCb", + 10 + ], + [ + "aBBC", + "cCbbBa", + 9 + ], + [ + "aBBCBac", + "AbacacaCC", + 12 + ], + [ + "aBBCBbab", + "bb", + 12 + ], + [ + "aBBCaCCAB", + "ccBAcA", + 12 + ], + [ + "aBBCbAb", + "cACACb", + 10 + ], + [ + "aBBCbB", + "aCabbA", + 8 + ], + [ + "aBBCcabCb", + "CCacAacc", + 14 + ], + [ + "aBBaB", + "BCaccaa", + 10 + ], + [ + "aBBaB", + "bACCbA", + 11 + ], + [ + "aBBaCCab", + "CBcAbcC", + 12 + ], + [ + "aBBaa", + "a", + 8 + ], + [ + "aBBabAb", + "bACbcAa", + 11 + ], + [ + "aBBac", + "BbAa", + 6 + ], + [ + "aBBacABac", + "CbCbc", + 13 + ], + [ + "aBBb", + "BCAaBa", + 9 + ], + [ + "aBBbA", + "aaa", + 7 + ], + [ + "aBBbB", + "bbBaba", + 7 + ], + [ + "aBBbba", + "bCCcacbB", + 14 + ], + [ + "aBBc", + "BccbaCCac", + 14 + ], + [ + "aBBcABB", + "BBcAbCBAB", + 8 + ], + [ + "aBBcBAab", + "CcCBBA", + 12 + ], + [ + "aBBcBBA", + "cC", + 12 + ], + [ + "aBBcBcb", + "BBBca", + 6 + ], + [ + "aBBcaB", + "AbBAa", + 6 + ], + [ + "aBBccB", + "abcac", + 7 + ], + [ + "aBBcca", + "bABb", + 10 + ], + [ + "aBBcccbCB", + "bCcab", + 12 + ], + [ + "aBC", + "AABCBaAAC", + 13 + ], + [ + "aBC", + "CcaCAB", + 10 + ], + [ + "aBC", + "aACCc", + 6 + ], + [ + "aBC", + "aaaCcCcB", + 12 + ], + [ + "aBC", + "bABBAa", + 9 + ], + [ + "aBC", + "bC", + 3 + ], + [ + "aBC", + "baCCa", + 6 + ], + [ + "aBC", + "bc", + 4 + ], + [ + "aBCA", + "AbbcbAb", + 9 + ], + [ + "aBCA", + "CBaAa", + 6 + ], + [ + "aBCAa", + "BbCCAbAAb", + 12 + ], + [ + "aBCAcBAa", + "BC", + 12 + ], + [ + "aBCAcBCAC", + "AABaCbBcA", + 12 + ], + [ + "aBCAccBCB", + "b", + 17 + ], + [ + "aBCBA", + "b", + 9 + ], + [ + "aBCBBCa", + "aCCAaa", + 8 + ], + [ + "aBCBC", + "bCccB", + 8 + ], + [ + "aBCBC", + "bbB", + 7 + ], + [ + "aBCBabb", + "a", + 12 + ], + [ + "aBCCcc", + "caAAc", + 10 + ], + [ + "aBCaB", + "cB", + 7 + ], + [ + "aBCaCa", + "aCA", + 7 + ], + [ + "aBCaCbC", + "BcaaBaABA", + 13 + ], + [ + "aBCab", + "cAacAcCaA", + 12 + ], + [ + "aBCababA", + "ccbABc", + 11 + ], + [ + "aBCac", + "CbCBbaAB", + 11 + ], + [ + "aBCb", + "acabaC", + 8 + ], + [ + "aBCbb", + "C", + 8 + ], + [ + "aBCbbB", + "bCcBb", + 7 + ], + [ + "aBCbcCBc", + "cBaCAc", + 10 + ], + [ + "aBCc", + "bABcbCa", + 9 + ], + [ + "aBCcAc", + "cCAaaAc", + 10 + ], + [ + "aBCcAcAC", + "BCcBaca", + 8 + ], + [ + "aBCcCBaa", + "b", + 15 + ], + [ + "aBCcCBbcC", + "ba", + 16 + ], + [ + "aBCcaBAaC", + "bBbcaCC", + 10 + ], + [ + "aBa", + "cBCbb", + 8 + ], + [ + "aBa", + "caB", + 4 + ], + [ + "aBaA", + "caBAC", + 5 + ], + [ + "aBaAaAcC", + "CAbBbbabc", + 13 + ], + [ + "aBaAcCBBA", + "AcAcc", + 12 + ], + [ + "aBaAcCC", + "Bc", + 10 + ], + [ + "aBaB", + "CBBbc", + 7 + ], + [ + "aBaB", + "bBAaA", + 6 + ], + [ + "aBaBC", + "CBbcaaBA", + 10 + ], + [ + "aBaBbA", + "CCabBBAaC", + 11 + ], + [ + "aBaBbCcc", + "aB", + 12 + ], + [ + "aBaBc", + "CcCAb", + 10 + ], + [ + "aBaC", + "A", + 7 + ], + [ + "aBaC", + "aa", + 4 + ], + [ + "aBaC", + "accAbAC", + 8 + ], + [ + "aBaCCAb", + "cbBC", + 11 + ], + [ + "aBaCaA", + "Aa", + 9 + ], + [ + "aBaCaC", + "CbaCccAc", + 9 + ], + [ + "aBaCaabAc", + "bCcACb", + 13 + ], + [ + "aBaCba", + "baB", + 8 + ], + [ + "aBaa", + "CBaBAABbB", + 12 + ], + [ + "aBaa", + "Cb", + 7 + ], + [ + "aBaaA", + "aAAaABCB", + 9 + ], + [ + "aBaabC", + "bCBbAabAC", + 9 + ], + [ + "aBaabCba", + "bCCaB", + 12 + ], + [ + "aBaabaB", + "AA", + 12 + ], + [ + "aBaacBbca", + "cCc", + 14 + ], + [ + "aBabACAA", + "acABb", + 12 + ], + [ + "aBabBcb", + "bAbA", + 10 + ], + [ + "aBabbaBbc", + "cbaccAb", + 12 + ], + [ + "aBac", + "c", + 6 + ], + [ + "aBacAAcA", + "aaCB", + 11 + ], + [ + "aBacBB", + "cb", + 9 + ], + [ + "aBacC", + "ACCCcAb", + 11 + ], + [ + "aBacCB", + "abccAac", + 9 + ], + [ + "aBaccCAa", + "aBcacaaCb", + 10 + ], + [ + "aBb", + "BCBAabbac", + 13 + ], + [ + "aBb", + "Ccb", + 4 + ], + [ + "aBb", + "aaac", + 6 + ], + [ + "aBb", + "bbcbbbAaC", + 15 + ], + [ + "aBb", + "cbCaBb", + 6 + ], + [ + "aBb", + "cbaB", + 6 + ], + [ + "aBbA", + "bbaaAbab", + 11 + ], + [ + "aBbACC", + "cCcccBCC", + 12 + ], + [ + "aBbAcCBCA", + "cCBab", + 12 + ], + [ + "aBbB", + "BacA", + 8 + ], + [ + "aBbB", + "aCcCb", + 7 + ], + [ + "aBbBA", + "aAcC", + 8 + ], + [ + "aBbBB", + "aBCc", + 6 + ], + [ + "aBbBCbBAa", + "Ac", + 16 + ], + [ + "aBbBa", + "bAa", + 6 + ], + [ + "aBbBbAbC", + "Abc", + 11 + ], + [ + "aBbBbaBA", + "AbbBCccC", + 10 + ], + [ + "aBbBbcBcb", + "abaBAAAAB", + 12 + ], + [ + "aBbC", + "bCbBAcBc", + 12 + ], + [ + "aBbCABBA", + "Caa", + 12 + ], + [ + "aBbCACaCb", + "A", + 16 + ], + [ + "aBbCBCaBC", + "aCcc", + 12 + ], + [ + "aBbCaa", + "cbAC", + 9 + ], + [ + "aBbCbbbBA", + "aA", + 14 + ], + [ + "aBbCbcCAb", + "BcAC", + 12 + ], + [ + "aBbCc", + "AcACCCba", + 12 + ], + [ + "aBba", + "BcAcCBB", + 12 + ], + [ + "aBbaAACBA", + "BacacbaC", + 12 + ], + [ + "aBbaAB", + "bbCc", + 9 + ], + [ + "aBbaBcC", + "c", + 12 + ], + [ + "aBbaCcbC", + "bbbbcab", + 10 + ], + [ + "aBbaaCcaA", + "bBABBAbCc", + 13 + ], + [ + "aBbabCc", + "CaCB", + 10 + ], + [ + "aBbb", + "Bb", + 4 + ], + [ + "aBbb", + "cccAbB", + 9 + ], + [ + "aBbbA", + "AAC", + 9 + ], + [ + "aBbbAB", + "C", + 12 + ], + [ + "aBbbCab", + "AaCBacaba", + 10 + ], + [ + "aBbbbBA", + "BcbbBBA", + 5 + ], + [ + "aBbcCBaCc", + "C", + 16 + ], + [ + "aBbcCba", + "BbAcaBac", + 9 + ], + [ + "aBc", + "A", + 5 + ], + [ + "aBc", + "Bac", + 4 + ], + [ + "aBc", + "bCCc", + 6 + ], + [ + "aBc", + "baC", + 5 + ], + [ + "aBcAA", + "ABc", + 5 + ], + [ + "aBcAABabC", + "cab", + 12 + ], + [ + "aBcAaAccc", + "BacacaA", + 12 + ], + [ + "aBcAabAA", + "aBcB", + 9 + ], + [ + "aBcAc", + "bCCbbCc", + 11 + ], + [ + "aBcB", + "c", + 6 + ], + [ + "aBcBBBA", + "BbBBaAAab", + 12 + ], + [ + "aBcBBCAbb", + "AAA", + 15 + ], + [ + "aBcBaa", + "bbB", + 9 + ], + [ + "aBcBbbCb", + "cAcBCca", + 11 + ], + [ + "aBcC", + "CCAAA", + 10 + ], + [ + "aBcCABCc", + "BCbbcC", + 9 + ], + [ + "aBcCBbAc", + "cAbABaAa", + 12 + ], + [ + "aBcCCbA", + "cbA", + 8 + ], + [ + "aBcCcbaAC", + "aBBbBaB", + 11 + ], + [ + "aBca", + "BacbCCcB", + 11 + ], + [ + "aBca", + "CBAcCcCA", + 11 + ], + [ + "aBcaCC", + "c", + 10 + ], + [ + "aBcaaBB", + "BAbBaACbc", + 12 + ], + [ + "aBcbABAAa", + "bCCc", + 16 + ], + [ + "aBcbCAb", + "abA", + 8 + ], + [ + "aBcba", + "cAbCC", + 9 + ], + [ + "aBccABa", + "baBbCAAB", + 9 + ], + [ + "aBccAbac", + "CAaaCbb", + 14 + ], + [ + "aBccacCBa", + "ccbc", + 12 + ], + [ + "aBcccC", + "CccbACAcc", + 12 + ], + [ + "aC", + "AABacca", + 11 + ], + [ + "aC", + "ABABACC", + 11 + ], + [ + "aC", + "AcaBa", + 8 + ], + [ + "aC", + "AcaCBcaCB", + 14 + ], + [ + "aC", + "BBbbBcA", + 13 + ], + [ + "aC", + "BCABB", + 8 + ], + [ + "aC", + "Ba", + 4 + ], + [ + "aC", + "BaACBc", + 8 + ], + [ + "aC", + "BacccCb", + 10 + ], + [ + "aC", + "BcACaaCa", + 12 + ], + [ + "aC", + "BcB", + 5 + ], + [ + "aC", + "CAAcA", + 8 + ], + [ + "aC", + "CBbB", + 8 + ], + [ + "aC", + "CCA", + 4 + ], + [ + "aC", + "CaAA", + 6 + ], + [ + "aC", + "a", + 2 + ], + [ + "aC", + "aACaBBaB", + 12 + ], + [ + "aC", + "abCBcb", + 8 + ], + [ + "aC", + "abcabC", + 8 + ], + [ + "aC", + "bA", + 4 + ], + [ + "aC", + "bABAabc", + 11 + ], + [ + "aC", + "bBAAbCAcB", + 15 + ], + [ + "aC", + "bBaaaA", + 10 + ], + [ + "aC", + "bCBA", + 6 + ], + [ + "aC", + "baaaCC", + 8 + ], + [ + "aC", + "babcC", + 6 + ], + [ + "aC", + "bbc", + 5 + ], + [ + "aC", + "caCbAcaCc", + 14 + ], + [ + "aC", + "cbCaCaB", + 10 + ], + [ + "aCA", + "BCcBBaBc", + 13 + ], + [ + "aCA", + "CAACacC", + 10 + ], + [ + "aCA", + "bAaaC", + 8 + ], + [ + "aCA", + "babAaaBaC", + 14 + ], + [ + "aCA", + "baccbBBC", + 13 + ], + [ + "aCA", + "cBCc", + 6 + ], + [ + "aCAAA", + "aB", + 8 + ], + [ + "aCAAAaAba", + "BAbB", + 14 + ], + [ + "aCAABBBBb", + "BBABB", + 11 + ], + [ + "aCAACBcA", + "aA", + 12 + ], + [ + "aCAAaaa", + "AcbacaACb", + 12 + ], + [ + "aCAAbCBbA", + "cBc", + 15 + ], + [ + "aCAAcbC", + "CAaAcCcCc", + 10 + ], + [ + "aCAB", + "AbabAb", + 7 + ], + [ + "aCAB", + "BA", + 6 + ], + [ + "aCAB", + "CBAC", + 6 + ], + [ + "aCABAcBac", + "CCBCcbCC", + 10 + ], + [ + "aCABBC", + "ABb", + 7 + ], + [ + "aCABaAaC", + "BCaaABcC", + 9 + ], + [ + "aCABaBca", + "c", + 14 + ], + [ + "aCABaa", + "C", + 10 + ], + [ + "aCACACAC", + "baAbACC", + 8 + ], + [ + "aCACBBC", + "aCACBaAac", + 7 + ], + [ + "aCACBaaA", + "acCcCB", + 10 + ], + [ + "aCACCca", + "aBA", + 10 + ], + [ + "aCACa", + "baBbABB", + 10 + ], + [ + "aCACcbaa", + "CbAbBCBb", + 14 + ], + [ + "aCAa", + "bBCCcc", + 10 + ], + [ + "aCAaCba", + "AabAc", + 9 + ], + [ + "aCAab", + "CCCcCCAA", + 13 + ], + [ + "aCAacCAAa", + "abAcBacA", + 10 + ], + [ + "aCAb", + "Aaac", + 6 + ], + [ + "aCAbCC", + "bca", + 9 + ], + [ + "aCAbCC", + "cCABABac", + 10 + ], + [ + "aCAbCaCca", + "cACA", + 12 + ], + [ + "aCAbaBab", + "BABbA", + 11 + ], + [ + "aCAbca", + "B", + 11 + ], + [ + "aCAcBbbC", + "acaAbACAB", + 12 + ], + [ + "aCAcaCB", + "CbC", + 10 + ], + [ + "aCAcacc", + "aA", + 10 + ], + [ + "aCAcbA", + "aa", + 9 + ], + [ + "aCAcbabB", + "Aabbca", + 12 + ], + [ + "aCAccCa", + "AaBca", + 8 + ], + [ + "aCB", + "A", + 5 + ], + [ + "aCB", + "aAaCbBcA", + 10 + ], + [ + "aCB", + "aBABAaccA", + 14 + ], + [ + "aCB", + "aBcAb", + 6 + ], + [ + "aCB", + "abCbBA", + 6 + ], + [ + "aCB", + "b", + 5 + ], + [ + "aCB", + "cCcaBbc", + 10 + ], + [ + "aCBA", + "BCcCccBA", + 10 + ], + [ + "aCBA", + "cCCbcA", + 7 + ], + [ + "aCBAabBa", + "aA", + 12 + ], + [ + "aCBAb", + "B", + 8 + ], + [ + "aCBAbb", + "c", + 11 + ], + [ + "aCBB", + "bAb", + 7 + ], + [ + "aCBBAAB", + "bcbCAAAbc", + 11 + ], + [ + "aCBBBAab", + "CBcBbBcb", + 9 + ], + [ + "aCBBCcC", + "aBaCAa", + 8 + ], + [ + "aCBBabca", + "bCaaC", + 11 + ], + [ + "aCBBbb", + "cb", + 9 + ], + [ + "aCBBcBBC", + "c", + 14 + ], + [ + "aCBCCa", + "AaaABcA", + 10 + ], + [ + "aCBCc", + "bacBbaaaA", + 13 + ], + [ + "aCBa", + "cAA", + 6 + ], + [ + "aCBaABA", + "ba", + 11 + ], + [ + "aCBaAC", + "a", + 10 + ], + [ + "aCBaacBc", + "cacacCBB", + 10 + ], + [ + "aCBb", + "Ba", + 6 + ], + [ + "aCBb", + "CaAc", + 8 + ], + [ + "aCBb", + "CaBb", + 4 + ], + [ + "aCBbABbA", + "CbBcaA", + 10 + ], + [ + "aCBbACC", + "cAaBBC", + 10 + ], + [ + "aCBbBAcBB", + "aabBbCcaB", + 9 + ], + [ + "aCBbBc", + "aBabCbbaC", + 10 + ], + [ + "aCBbcccA", + "cCA", + 11 + ], + [ + "aCBc", + "ABAAaC", + 10 + ], + [ + "aCBc", + "BCcc", + 4 + ], + [ + "aCBcB", + "bBCCAC", + 10 + ], + [ + "aCBcaC", + "A", + 11 + ], + [ + "aCC", + "AB", + 5 + ], + [ + "aCC", + "Ac", + 4 + ], + [ + "aCC", + "abABbCBc", + 11 + ], + [ + "aCC", + "cBAcBCcb", + 12 + ], + [ + "aCC", + "cCCcBaa", + 10 + ], + [ + "aCCAA", + "C", + 8 + ], + [ + "aCCAB", + "bbCB", + 6 + ], + [ + "aCCABabA", + "aCa", + 10 + ], + [ + "aCCAcCbA", + "cBbCc", + 13 + ], + [ + "aCCAccAA", + "CB", + 14 + ], + [ + "aCCBBB", + "cccacc", + 10 + ], + [ + "aCCBCcA", + "aAc", + 10 + ], + [ + "aCCBCcCC", + "acBCbA", + 9 + ], + [ + "aCCC", + "ACbCcBa", + 8 + ], + [ + "aCCCBAC", + "AB", + 11 + ], + [ + "aCCCBCBa", + "a", + 14 + ], + [ + "aCCCBaAAa", + "CcBb", + 13 + ], + [ + "aCCCCB", + "cCABAcbA", + 12 + ], + [ + "aCCCCbCc", + "B", + 15 + ], + [ + "aCCCCcCb", + "AAB", + 14 + ], + [ + "aCCCab", + "BAaBcaC", + 10 + ], + [ + "aCCCc", + "aBabBBaa", + 14 + ], + [ + "aCCaA", + "aAAbb", + 8 + ], + [ + "aCCaA", + "cAA", + 6 + ], + [ + "aCCaBCcb", + "BAcc", + 12 + ], + [ + "aCCaC", + "CcacABac", + 10 + ], + [ + "aCCbBBbCB", + "baAcABCC", + 13 + ], + [ + "aCCbbAC", + "BAcbcB", + 11 + ], + [ + "aCCbbbaaB", + "abbC", + 12 + ], + [ + "aCCc", + "bAa", + 8 + ], + [ + "aCCc", + "cbaB", + 8 + ], + [ + "aCCcABBCB", + "bbC", + 14 + ], + [ + "aCCcBBABA", + "bABaacb", + 15 + ], + [ + "aCCcBCCBc", + "a", + 16 + ], + [ + "aCCcbaa", + "B", + 13 + ], + [ + "aCCccBBa", + "baAB", + 14 + ], + [ + "aCa", + "B", + 6 + ], + [ + "aCa", + "BCcC", + 6 + ], + [ + "aCa", + "bbbbCBA", + 11 + ], + [ + "aCaAAACa", + "AcCAA", + 10 + ], + [ + "aCaAAb", + "BCBccaB", + 10 + ], + [ + "aCaABBc", + "aCAbcbB", + 8 + ], + [ + "aCaABbAb", + "aBc", + 12 + ], + [ + "aCaAb", + "aCc", + 6 + ], + [ + "aCaAcCBb", + "baCcbb", + 9 + ], + [ + "aCaAcaC", + "BBCcCcbc", + 11 + ], + [ + "aCaB", + "BacaaAcBA", + 11 + ], + [ + "aCaB", + "C", + 6 + ], + [ + "aCaBAC", + "ab", + 9 + ], + [ + "aCaBBa", + "B", + 10 + ], + [ + "aCaBCB", + "a", + 10 + ], + [ + "aCaBCCB", + "cAccc", + 10 + ], + [ + "aCaBa", + "aC", + 6 + ], + [ + "aCaBacB", + "AAbcaB", + 9 + ], + [ + "aCaC", + "BBcc", + 7 + ], + [ + "aCaC", + "CAAaCAB", + 9 + ], + [ + "aCaC", + "accBcAAbA", + 14 + ], + [ + "aCaCAAac", + "CcCAB", + 10 + ], + [ + "aCaCBaaBb", + "CC", + 14 + ], + [ + "aCaCacAba", + "cbaAbBB", + 13 + ], + [ + "aCaCccca", + "CbACAc", + 11 + ], + [ + "aCaa", + "BAbB", + 8 + ], + [ + "aCaaC", + "ABacC", + 5 + ], + [ + "aCaac", + "AcabAC", + 6 + ], + [ + "aCaacB", + "CA", + 9 + ], + [ + "aCaacacbC", + "b", + 16 + ], + [ + "aCab", + "AcccBcB", + 11 + ], + [ + "aCab", + "BaaacBbbA", + 12 + ], + [ + "aCabBacCB", + "AAc", + 14 + ], + [ + "aCabCBBB", + "CBBcBbBBB", + 10 + ], + [ + "aCabCbbb", + "CBCCCcA", + 12 + ], + [ + "aCabaA", + "bABbaB", + 8 + ], + [ + "aCabbbCA", + "Ba", + 14 + ], + [ + "aCabcb", + "bBBb", + 9 + ], + [ + "aCacA", + "BbAB", + 9 + ], + [ + "aCacCa", + "baABcBcAb", + 12 + ], + [ + "aCacCcAaC", + "CbAbBa", + 14 + ], + [ + "aCacbC", + "CcbABcb", + 10 + ], + [ + "aCb", + "AaBaAc", + 10 + ], + [ + "aCb", + "BB", + 5 + ], + [ + "aCb", + "a", + 4 + ], + [ + "aCb", + "cCbBC", + 6 + ], + [ + "aCbA", + "CcCccBc", + 11 + ], + [ + "aCbA", + "bBaC", + 8 + ], + [ + "aCbAa", + "AABbbcCAC", + 13 + ], + [ + "aCbBAA", + "BcCC", + 11 + ], + [ + "aCbBAB", + "bcCBb", + 8 + ], + [ + "aCbBB", + "B", + 8 + ], + [ + "aCbBCaB", + "aCcAcaAbB", + 9 + ], + [ + "aCbBb", + "CBbA", + 6 + ], + [ + "aCbCBB", + "CAaACaA", + 11 + ], + [ + "aCbCBc", + "ABaBAcAcC", + 13 + ], + [ + "aCbCcCBCB", + "CBbc", + 13 + ], + [ + "aCba", + "aCba", + 0 + ], + [ + "aCbaa", + "BacbBccb", + 11 + ], + [ + "aCbb", + "bBcCBAAcc", + 15 + ], + [ + "aCbbCCCB", + "AaAC", + 13 + ], + [ + "aCbbaBAa", + "BbB", + 11 + ], + [ + "aCbbc", + "BBCAb", + 8 + ], + [ + "aCbbcAcB", + "accCaaB", + 9 + ], + [ + "aCbca", + "AbAc", + 7 + ], + [ + "aCbcb", + "cBcacaa", + 11 + ], + [ + "aCbcc", + "cBCbBAbB", + 12 + ], + [ + "aCbcca", + "CBbCaaCC", + 11 + ], + [ + "aCbccb", + "cAAab", + 9 + ], + [ + "aCc", + "AaAbC", + 7 + ], + [ + "aCc", + "BCbb", + 6 + ], + [ + "aCc", + "CCABBCAa", + 13 + ], + [ + "aCc", + "aAB", + 4 + ], + [ + "aCc", + "bbAcb", + 8 + ], + [ + "aCc", + "bbCcccb", + 10 + ], + [ + "aCc", + "caCAC", + 5 + ], + [ + "aCcA", + "a", + 6 + ], + [ + "aCcA", + "aC", + 4 + ], + [ + "aCcA", + "aaAAaaCBa", + 13 + ], + [ + "aCcA", + "cABb", + 8 + ], + [ + "aCcAAA", + "AbaBACBC", + 13 + ], + [ + "aCcABAc", + "BC", + 11 + ], + [ + "aCcABCB", + "b", + 13 + ], + [ + "aCcACA", + "baCcaaAa", + 7 + ], + [ + "aCcAaAba", + "aCAca", + 8 + ], + [ + "aCcAbB", + "cBCAB", + 7 + ], + [ + "aCcB", + "CCbcBaBBb", + 12 + ], + [ + "aCcBbCAC", + "CACbBb", + 11 + ], + [ + "aCcCC", + "A", + 9 + ], + [ + "aCcCCa", + "CcCabCc", + 8 + ], + [ + "aCcCabC", + "BcACaABCC", + 10 + ], + [ + "aCcCcAaA", + "baaAacBAb", + 13 + ], + [ + "aCcCca", + "cBCbCCAcb", + 11 + ], + [ + "aCcaAABb", + "BCACAAb", + 8 + ], + [ + "aCcaCAA", + "AabCbBBAC", + 12 + ], + [ + "aCcacbabC", + "a", + 16 + ], + [ + "aCcbAb", + "CbbcbaCAB", + 11 + ], + [ + "aCcbB", + "aA", + 8 + ], + [ + "aCcbBbAcC", + "bBBc", + 11 + ], + [ + "aCcbaCbca", + "bcBCCcbC", + 12 + ], + [ + "aCcbb", + "ccAabbABa", + 13 + ], + [ + "aCcbbaccb", + "AaCaCaCC", + 12 + ], + [ + "aCcbcBBA", + "CaBccABCa", + 11 + ], + [ + "aCcbcb", + "AcaacBCc", + 10 + ], + [ + "aCcc", + "baCACCa", + 8 + ], + [ + "aCccAaBa", + "BAaBcB", + 12 + ], + [ + "aCccAb", + "bBbaAcCC", + 13 + ], + [ + "aCccBB", + "CCa", + 9 + ], + [ + "aCccaaABC", + "CcBac", + 11 + ], + [ + "aCcccAaba", + "ccBbaA", + 11 + ], + [ + "aa", + "AACcaACB", + 13 + ], + [ + "aa", + "AcBCa", + 7 + ], + [ + "aa", + "Bbaba", + 6 + ], + [ + "aa", + "Bbc", + 6 + ], + [ + "aa", + "BcaBA", + 7 + ], + [ + "aa", + "C", + 4 + ], + [ + "aa", + "CBcC", + 8 + ], + [ + "aa", + "CbB", + 6 + ], + [ + "aa", + "CbBC", + 8 + ], + [ + "aa", + "a", + 2 + ], + [ + "aa", + "aABACBBb", + 13 + ], + [ + "aa", + "aBC", + 4 + ], + [ + "aa", + "aBcaaCAC", + 12 + ], + [ + "aa", + "aCbbAaaCa", + 14 + ], + [ + "aa", + "aaAaA", + 6 + ], + [ + "aa", + "aaAbcb", + 8 + ], + [ + "aa", + "aabCb", + 6 + ], + [ + "aa", + "abCBc", + 8 + ], + [ + "aa", + "abCbAb", + 9 + ], + [ + "aa", + "acAAbAAB", + 13 + ], + [ + "aa", + "bAaB", + 5 + ], + [ + "aa", + "bB", + 4 + ], + [ + "aa", + "bBAaBCA", + 11 + ], + [ + "aa", + "bCaaBbCA", + 12 + ], + [ + "aa", + "baCCBCBB", + 14 + ], + [ + "aa", + "bbAabBcCc", + 15 + ], + [ + "aa", + "bbcAC", + 9 + ], + [ + "aa", + "cAaabc", + 8 + ], + [ + "aa", + "cB", + 4 + ], + [ + "aa", + "cBB", + 6 + ], + [ + "aa", + "cBcBAAbA", + 14 + ], + [ + "aa", + "cCAB", + 7 + ], + [ + "aa", + "cCB", + 6 + ], + [ + "aa", + "caccAcB", + 11 + ], + [ + "aa", + "cb", + 4 + ], + [ + "aa", + "cbbcCB", + 12 + ], + [ + "aaA", + "A", + 4 + ], + [ + "aaA", + "ACBACBC", + 11 + ], + [ + "aaA", + "bCCA", + 6 + ], + [ + "aaA", + "bcaaAB", + 6 + ], + [ + "aaAA", + "aab", + 4 + ], + [ + "aaAAaaAaB", + "BAA", + 14 + ], + [ + "aaAAbbAC", + "cCbBbCAab", + 14 + ], + [ + "aaAAcC", + "cabbCa", + 9 + ], + [ + "aaAB", + "cAa", + 6 + ], + [ + "aaAB", + "cCaBCcB", + 10 + ], + [ + "aaABAbAC", + "CCAB", + 12 + ], + [ + "aaABAbb", + "CC", + 14 + ], + [ + "aaABB", + "Ccb", + 9 + ], + [ + "aaABCC", + "acABACB", + 6 + ], + [ + "aaABCb", + "CABbAaa", + 12 + ], + [ + "aaABaaA", + "CaaaBBabc", + 9 + ], + [ + "aaABbbCbc", + "B", + 16 + ], + [ + "aaAC", + "Ac", + 5 + ], + [ + "aaAC", + "bccBBBb", + 14 + ], + [ + "aaACa", + "cbAAaCB", + 9 + ], + [ + "aaAaAcbBC", + "A", + 16 + ], + [ + "aaAaaaAb", + "bcBc", + 16 + ], + [ + "aaAabBCbc", + "B", + 16 + ], + [ + "aaAabCaAa", + "BbbbcA", + 13 + ], + [ + "aaAabbbaC", + "bbbCaAbC", + 14 + ], + [ + "aaAac", + "cBCCc", + 8 + ], + [ + "aaAbaBbBa", + "baaCCa", + 12 + ], + [ + "aaAbcbBa", + "BbBbaaC", + 12 + ], + [ + "aaAc", + "bbCBca", + 10 + ], + [ + "aaAccCbC", + "BBa", + 15 + ], + [ + "aaB", + "bAAa", + 6 + ], + [ + "aaB", + "cccCAcAaC", + 15 + ], + [ + "aaBA", + "ABccb", + 9 + ], + [ + "aaBA", + "aca", + 5 + ], + [ + "aaBABCc", + "Aaa", + 10 + ], + [ + "aaBABaCc", + "acBaac", + 7 + ], + [ + "aaBABbCAc", + "cCb", + 16 + ], + [ + "aaBB", + "BbbBbcAB", + 12 + ], + [ + "aaBB", + "bAaAaAB", + 8 + ], + [ + "aaBB", + "bBabBA", + 7 + ], + [ + "aaBBa", + "caaB", + 6 + ], + [ + "aaBC", + "BacaAB", + 8 + ], + [ + "aaBCAaaaB", + "aaACA", + 10 + ], + [ + "aaBCC", + "BAAaBb", + 9 + ], + [ + "aaBCCa", + "CA", + 9 + ], + [ + "aaBCCbAac", + "AcaaBacc", + 13 + ], + [ + "aaBa", + "BabbcC", + 9 + ], + [ + "aaBaAaCa", + "aBbcAcAb", + 11 + ], + [ + "aaBaC", + "bcCAbac", + 9 + ], + [ + "aaBaCc", + "BCAaCCb", + 9 + ], + [ + "aaBaa", + "AAb", + 7 + ], + [ + "aaBaa", + "ccba", + 7 + ], + [ + "aaBaaAcBA", + "aCcBCAc", + 12 + ], + [ + "aaBabACBC", + "CCCc", + 15 + ], + [ + "aaBabbBA", + "aAbB", + 9 + ], + [ + "aaBacaB", + "BCABCCCbc", + 13 + ], + [ + "aaBbAaBA", + "BAc", + 12 + ], + [ + "aaBbb", + "aA", + 7 + ], + [ + "aaBbcA", + "CBcb", + 8 + ], + [ + "aaBbcbb", + "C", + 13 + ], + [ + "aaBcBaBa", + "aaBaaaB", + 6 + ], + [ + "aaBccACb", + "bCCBccC", + 10 + ], + [ + "aaC", + "ABcAca", + 9 + ], + [ + "aaC", + "BAbaCaa", + 9 + ], + [ + "aaC", + "CaCbcCc", + 10 + ], + [ + "aaC", + "Cc", + 5 + ], + [ + "aaC", + "aCa", + 4 + ], + [ + "aaC", + "caBBCb", + 8 + ], + [ + "aaCA", + "B", + 8 + ], + [ + "aaCAA", + "BcbBC", + 10 + ], + [ + "aaCABbb", + "CCcbBAb", + 9 + ], + [ + "aaCACa", + "Bb", + 12 + ], + [ + "aaCAcbc", + "AaaAAa", + 9 + ], + [ + "aaCAccaAB", + "CcCABcA", + 10 + ], + [ + "aaCC", + "B", + 8 + ], + [ + "aaCCC", + "ABAcC", + 6 + ], + [ + "aaCCCc", + "cCAaaAbAb", + 14 + ], + [ + "aaCCa", + "CAA", + 7 + ], + [ + "aaCCbaCaB", + "cCCcCAccC", + 13 + ], + [ + "aaCCcbCA", + "acAcb", + 9 + ], + [ + "aaCa", + "aCBca", + 5 + ], + [ + "aaCa", + "cAAca", + 5 + ], + [ + "aaCaABa", + "ba", + 11 + ], + [ + "aaCaABbc", + "B", + 14 + ], + [ + "aaCaBaA", + "aBcAa", + 8 + ], + [ + "aaCaa", + "Ac", + 8 + ], + [ + "aaCacA", + "CbabcA", + 8 + ], + [ + "aaCacAAb", + "CcCAc", + 10 + ], + [ + "aaCacaBC", + "BBaCcA", + 11 + ], + [ + "aaCacaba", + "ACbc", + 11 + ], + [ + "aaCbAAB", + "ABc", + 12 + ], + [ + "aaCbabb", + "Bc", + 13 + ], + [ + "aaCbabc", + "C", + 12 + ], + [ + "aaCbbAAc", + "BaBa", + 12 + ], + [ + "aaCcAcc", + "aC", + 10 + ], + [ + "aaCcaBcB", + "ccAaAbaBC", + 12 + ], + [ + "aaCcaC", + "AAABCccC", + 8 + ], + [ + "aaCccbAaa", + "B", + 17 + ], + [ + "aaa", + "ABAbCbC", + 12 + ], + [ + "aaa", + "abABCcbA", + 12 + ], + [ + "aaa", + "abaC", + 4 + ], + [ + "aaa", + "baa", + 2 + ], + [ + "aaa", + "cCbC", + 8 + ], + [ + "aaaA", + "BaCacbbc", + 12 + ], + [ + "aaaA", + "aaaabAC", + 6 + ], + [ + "aaaAAb", + "abCC", + 10 + ], + [ + "aaaAAcb", + "cbABcb", + 8 + ], + [ + "aaaABb", + "BBCCbBaba", + 14 + ], + [ + "aaaACcA", + "aaa", + 8 + ], + [ + "aaaAac", + "CCAaC", + 7 + ], + [ + "aaaAb", + "ACc", + 9 + ], + [ + "aaaB", + "b", + 7 + ], + [ + "aaaBBB", + "CcACCaCB", + 13 + ], + [ + "aaaBCAbBc", + "baCaab", + 12 + ], + [ + "aaaBaBCAb", + "BaBccAb", + 9 + ], + [ + "aaaBbAC", + "baBCAc", + 7 + ], + [ + "aaaBbBA", + "a", + 12 + ], + [ + "aaaC", + "aca", + 4 + ], + [ + "aaaCAAcAA", + "cCacb", + 13 + ], + [ + "aaaCBacCc", + "aA", + 15 + ], + [ + "aaaCaBcCC", + "ACAbBb", + 13 + ], + [ + "aaaCaaC", + "C", + 12 + ], + [ + "aaaCaaCb", + "bC", + 14 + ], + [ + "aaaCb", + "bcBbaB", + 11 + ], + [ + "aaaCbC", + "AabAc", + 8 + ], + [ + "aaaCcB", + "A", + 11 + ], + [ + "aaaa", + "CacBC", + 8 + ], + [ + "aaaaB", + "aCBcbC", + 9 + ], + [ + "aaaaBBCc", + "aCcCcaab", + 14 + ], + [ + "aaaaBCc", + "bcbaB", + 10 + ], + [ + "aaaaC", + "CBb", + 10 + ], + [ + "aaabCBaAB", + "cbBccA", + 14 + ], + [ + "aaac", + "BAaB", + 5 + ], + [ + "aaacCCB", + "aaAaacA", + 8 + ], + [ + "aab", + "CcCcaACB", + 12 + ], + [ + "aab", + "ab", + 2 + ], + [ + "aab", + "b", + 4 + ], + [ + "aab", + "bBb", + 4 + ], + [ + "aab", + "ccbCc", + 8 + ], + [ + "aabAA", + "CBCcBcac", + 14 + ], + [ + "aabACB", + "AACA", + 7 + ], + [ + "aabACB", + "Ca", + 10 + ], + [ + "aabAacba", + "bCbcBC", + 11 + ], + [ + "aabAbBc", + "AbCaCcCB", + 13 + ], + [ + "aabBAA", + "CbaACAbAA", + 10 + ], + [ + "aabBBcB", + "CbACCCc", + 13 + ], + [ + "aabCAAc", + "CBaA", + 10 + ], + [ + "aabCAcB", + "BBCA", + 9 + ], + [ + "aabaCc", + "Acab", + 9 + ], + [ + "aabb", + "a", + 6 + ], + [ + "aabbA", + "bA", + 6 + ], + [ + "aabbCAA", + "AaAAAbbC", + 11 + ], + [ + "aabbacA", + "AbC", + 10 + ], + [ + "aabbcABba", + "AC", + 16 + ], + [ + "aabc", + "A", + 7 + ], + [ + "aabc", + "Baab", + 4 + ], + [ + "aabcA", + "cabBcCc", + 8 + ], + [ + "aabcBcB", + "Ab", + 11 + ], + [ + "aabcaAA", + "abccacA", + 6 + ], + [ + "aabcbBc", + "BaCaaaA", + 12 + ], + [ + "aabcbaaac", + "BbCbccCc", + 11 + ], + [ + "aac", + "BCC", + 5 + ], + [ + "aac", + "BbCaCBbBc", + 14 + ], + [ + "aac", + "C", + 5 + ], + [ + "aac", + "a", + 4 + ], + [ + "aacA", + "CABCabaa", + 12 + ], + [ + "aacAaCCA", + "CBCbB", + 13 + ], + [ + "aacAaCCAb", + "BBCAB", + 13 + ], + [ + "aacAcb", + "baAacaCAB", + 9 + ], + [ + "aacB", + "ABcCcBAca", + 13 + ], + [ + "aacB", + "CACAABc", + 10 + ], + [ + "aacB", + "c", + 6 + ], + [ + "aacBAabc", + "caCBa", + 9 + ], + [ + "aacBc", + "aBCaCbAc", + 8 + ], + [ + "aacC", + "Ba", + 6 + ], + [ + "aacC", + "Bc", + 6 + ], + [ + "aacC", + "CAbBAbCaA", + 14 + ], + [ + "aacCCb", + "ccBAc", + 10 + ], + [ + "aacCc", + "CaB", + 8 + ], + [ + "aacaA", + "Bbaa", + 7 + ], + [ + "aacaa", + "cCB", + 8 + ], + [ + "aacaaCbBc", + "BCab", + 13 + ], + [ + "aacabaBA", + "cbbA", + 9 + ], + [ + "aacac", + "ABBCabcc", + 10 + ], + [ + "aacacAaB", + "aAbbA", + 11 + ], + [ + "aacb", + "BCcAb", + 6 + ], + [ + "aacbAAC", + "BbabBAC", + 8 + ], + [ + "aacbAcA", + "acbccC", + 6 + ], + [ + "aacbCABAC", + "AAbaCAbbA", + 11 + ], + [ + "aacbb", + "BBAbAC", + 10 + ], + [ + "aacc", + "acACBAa", + 10 + ], + [ + "aaccAaABA", + "BAbBacAC", + 14 + ], + [ + "aacccAccB", + "BBCAb", + 14 + ], + [ + "ab", + "ABca", + 6 + ], + [ + "ab", + "ACA", + 5 + ], + [ + "ab", + "AaB", + 3 + ], + [ + "ab", + "B", + 3 + ], + [ + "ab", + "BAabAa", + 8 + ], + [ + "ab", + "BAacbb", + 8 + ], + [ + "ab", + "Ba", + 4 + ], + [ + "ab", + "BbaBBa", + 9 + ], + [ + "ab", + "CACABaC", + 12 + ], + [ + "ab", + "CCbaA", + 8 + ], + [ + "ab", + "CbBACbc", + 11 + ], + [ + "ab", + "CbcbcbB", + 12 + ], + [ + "ab", + "CcB", + 5 + ], + [ + "ab", + "CcacBC", + 9 + ], + [ + "ab", + "aBBAbbaB", + 12 + ], + [ + "ab", + "aBCbBCACa", + 14 + ], + [ + "ab", + "abAbbc", + 8 + ], + [ + "ab", + "abB", + 2 + ], + [ + "ab", + "bAbCcBAaA", + 15 + ], + [ + "ab", + "baBcBa", + 9 + ], + [ + "ab", + "bb", + 2 + ], + [ + "ab", + "c", + 4 + ], + [ + "ab", + "cAAabacc", + 12 + ], + [ + "ab", + "cABcCCb", + 11 + ], + [ + "ab", + "cCbbbCa", + 12 + ], + [ + "ab", + "cbcb", + 6 + ], + [ + "ab", + "ccb", + 4 + ], + [ + "ab", + "cccaaCAA", + 14 + ], + [ + "abA", + "AaacB", + 8 + ], + [ + "abA", + "BAB", + 5 + ], + [ + "abA", + "abca", + 3 + ], + [ + "abA", + "bAAcC", + 8 + ], + [ + "abA", + "bBbaa", + 7 + ], + [ + "abA", + "bbbAcabb", + 12 + ], + [ + "abA", + "cAbCCcbb", + 13 + ], + [ + "abA", + "cBaaC", + 8 + ], + [ + "abAAB", + "BAbACC", + 7 + ], + [ + "abAAcaCa", + "c", + 14 + ], + [ + "abAB", + "AaCBBbb", + 10 + ], + [ + "abABAA", + "aBABbaabA", + 8 + ], + [ + "abABcAaC", + "aCbAcb", + 10 + ], + [ + "abABcb", + "b", + 10 + ], + [ + "abAC", + "BAC", + 3 + ], + [ + "abACAC", + "CBCaaCcA", + 12 + ], + [ + "abACC", + "BbCbBb", + 10 + ], + [ + "abACCBBa", + "abb", + 11 + ], + [ + "abACCaABb", + "aAcbb", + 10 + ], + [ + "abACb", + "Ca", + 8 + ], + [ + "abACbBAaA", + "cAAcCaCaB", + 13 + ], + [ + "abACccA", + "bBaCBC", + 9 + ], + [ + "abAaAaCCC", + "cACCbAbCC", + 13 + ], + [ + "abAaBAAC", + "ABca", + 11 + ], + [ + "abAacCCaa", + "acbCAC", + 11 + ], + [ + "abAbAcCc", + "b", + 14 + ], + [ + "abAbbBC", + "CAbbBbBb", + 8 + ], + [ + "abAc", + "bC", + 5 + ], + [ + "abAcBBbBA", + "CabccbcaA", + 11 + ], + [ + "abAccAbba", + "caCACCC", + 14 + ], + [ + "abAccBA", + "ACAbbABaa", + 12 + ], + [ + "abAccCbBa", + "cbccBbBac", + 8 + ], + [ + "abB", + "BAAA", + 7 + ], + [ + "abB", + "BACbb", + 6 + ], + [ + "abB", + "BcaBabA", + 10 + ], + [ + "abB", + "a", + 4 + ], + [ + "abB", + "cbBaACaAB", + 14 + ], + [ + "abBA", + "AACBCAbA", + 11 + ], + [ + "abBAA", + "bA", + 6 + ], + [ + "abBAAcaA", + "AACCC", + 11 + ], + [ + "abBAAcbb", + "bCCcbb", + 8 + ], + [ + "abBAB", + "aa", + 7 + ], + [ + "abBABBA", + "BBCBccbaA", + 12 + ], + [ + "abBAaBa", + "bbaBCcbc", + 11 + ], + [ + "abBAaCacb", + "bbaca", + 10 + ], + [ + "abBAaCcc", + "ccCACB", + 12 + ], + [ + "abBAc", + "bB", + 6 + ], + [ + "abBAc", + "bBB", + 6 + ], + [ + "abBB", + "BbaBbCbb", + 10 + ], + [ + "abBBCc", + "b", + 10 + ], + [ + "abBBaAcBc", + "bbCaB", + 11 + ], + [ + "abBBaBbCc", + "Cac", + 14 + ], + [ + "abBBac", + "BAc", + 7 + ], + [ + "abBBb", + "A", + 9 + ], + [ + "abBBb", + "ACAAc", + 9 + ], + [ + "abBBbB", + "b", + 10 + ], + [ + "abBCACBBc", + "B", + 16 + ], + [ + "abBCBAB", + "cCA", + 10 + ], + [ + "abBCCb", + "AbaCcCcA", + 9 + ], + [ + "abBCCcc", + "BcBCCbCbb", + 11 + ], + [ + "abBCac", + "cCcbACcC", + 11 + ], + [ + "abBCcCbA", + "bC", + 12 + ], + [ + "abBa", + "cbabba", + 5 + ], + [ + "abBaA", + "CA", + 8 + ], + [ + "abBaAAc", + "BB", + 11 + ], + [ + "abBaBcabA", + "BBBAabAba", + 10 + ], + [ + "abBaCbB", + "CaCaacbC", + 9 + ], + [ + "abBabAb", + "Aacb", + 9 + ], + [ + "abBacbc", + "cAccCaaa", + 15 + ], + [ + "abBacca", + "bAACac", + 10 + ], + [ + "abBb", + "ABCaACb", + 10 + ], + [ + "abBb", + "BAABc", + 7 + ], + [ + "abBb", + "c", + 8 + ], + [ + "abBb", + "cAB", + 6 + ], + [ + "abBbBB", + "Aaccb", + 10 + ], + [ + "abBbC", + "aaccbac", + 9 + ], + [ + "abBbCBBa", + "C", + 14 + ], + [ + "abBbb", + "BBA", + 7 + ], + [ + "abBbba", + "CBbAAb", + 9 + ], + [ + "abBc", + "AABBCcBcc", + 12 + ], + [ + "abBc", + "bcbbCABca", + 12 + ], + [ + "abBcB", + "CbCaCca", + 10 + ], + [ + "abBcBBCA", + "BCCCacAac", + 15 + ], + [ + "abBcaBa", + "bcBCCc", + 11 + ], + [ + "abC", + "ACAbb", + 7 + ], + [ + "abC", + "Cc", + 5 + ], + [ + "abC", + "aAabBb", + 8 + ], + [ + "abC", + "b", + 4 + ], + [ + "abC", + "bccBAC", + 9 + ], + [ + "abC", + "bccabCBc", + 10 + ], + [ + "abC", + "cBA", + 5 + ], + [ + "abC", + "cCB", + 6 + ], + [ + "abCA", + "Acb", + 6 + ], + [ + "abCA", + "cc", + 7 + ], + [ + "abCAA", + "BAcbCCBc", + 11 + ], + [ + "abCAAcbCB", + "acCabB", + 9 + ], + [ + "abCAaA", + "AcbaAB", + 8 + ], + [ + "abCB", + "BABBbAABA", + 13 + ], + [ + "abCBB", + "Bb", + 7 + ], + [ + "abCBCB", + "baCaaa", + 10 + ], + [ + "abCC", + "CA", + 6 + ], + [ + "abCC", + "CCcCAcaA", + 13 + ], + [ + "abCCAacb", + "babaA", + 12 + ], + [ + "abCCBB", + "baB", + 8 + ], + [ + "abCCBBC", + "aAc", + 11 + ], + [ + "abCCCBaAC", + "CCBbCbBA", + 13 + ], + [ + "abCCaAca", + "bBCBCaC", + 10 + ], + [ + "abCCbBBaA", + "Ab", + 15 + ], + [ + "abCCc", + "BcbBabBc", + 12 + ], + [ + "abCCcAcBB", + "BaAAAaAB", + 13 + ], + [ + "abCa", + "ABA", + 5 + ], + [ + "abCaBC", + "bCCaCaA", + 10 + ], + [ + "abCaC", + "acc", + 6 + ], + [ + "abCaCAbB", + "aCaba", + 8 + ], + [ + "abCabA", + "ACba", + 6 + ], + [ + "abCabbccb", + "CcAcA", + 14 + ], + [ + "abCacaaC", + "aC", + 12 + ], + [ + "abCb", + "cBCBaA", + 8 + ], + [ + "abCbBCcAa", + "a", + 16 + ], + [ + "abCbaCC", + "CAabaabBB", + 14 + ], + [ + "abCbbba", + "CcbBCBAA", + 12 + ], + [ + "abCc", + "Ca", + 6 + ], + [ + "abCcAbbC", + "B", + 15 + ], + [ + "abCcCCC", + "BbAaca", + 11 + ], + [ + "abCcCc", + "ACacA", + 8 + ], + [ + "abCcaA", + "aca", + 6 + ], + [ + "abCcaABB", + "AbcBb", + 8 + ], + [ + "aba", + "CBb", + 5 + ], + [ + "aba", + "CBbbaAb", + 10 + ], + [ + "aba", + "aCccA", + 7 + ], + [ + "aba", + "acaB", + 4 + ], + [ + "aba", + "bcCCa", + 8 + ], + [ + "aba", + "cBB", + 5 + ], + [ + "abaAAA", + "cAbB", + 10 + ], + [ + "abaACCcC", + "abacCBA", + 7 + ], + [ + "abaAaccC", + "CCaacc", + 8 + ], + [ + "abaAbb", + "a", + 10 + ], + [ + "abaBCABc", + "aCcccB", + 11 + ], + [ + "abaBaCca", + "aAcAbaB", + 12 + ], + [ + "abaC", + "Bba", + 4 + ], + [ + "abaCACccC", + "BBBBB", + 17 + ], + [ + "abaCBABaB", + "abacbacc", + 9 + ], + [ + "abaCCbcA", + "AcBBc", + 11 + ], + [ + "abaCbB", + "cC", + 10 + ], + [ + "abaCbaCB", + "AAcC", + 11 + ], + [ + "abaCbaa", + "aCCaCCA", + 9 + ], + [ + "abaaACcB", + "cBBCBBAbC", + 16 + ], + [ + "abab", + "CcaCC", + 8 + ], + [ + "abab", + "aCAAaaaB", + 11 + ], + [ + "abab", + "acBbA", + 6 + ], + [ + "ababA", + "CBBC", + 8 + ], + [ + "ababACcC", + "BC", + 13 + ], + [ + "ababCBBbB", + "CbC", + 14 + ], + [ + "ababa", + "CABCcCcb", + 14 + ], + [ + "ababcCb", + "aCAbCc", + 7 + ], + [ + "abacA", + "ABb", + 8 + ], + [ + "abacABc", + "babCCAbb", + 8 + ], + [ + "abacAc", + "BacbCaaB", + 11 + ], + [ + "abacBBa", + "CbCAa", + 9 + ], + [ + "abacBbaCC", + "a", + 16 + ], + [ + "abacCb", + "b", + 10 + ], + [ + "abaca", + "cAB", + 9 + ], + [ + "abacaCbBc", + "bACcACB", + 10 + ], + [ + "abaccABb", + "cBcCc", + 12 + ], + [ + "abaccaAcA", + "B", + 17 + ], + [ + "abacccc", + "Cba", + 10 + ], + [ + "abb", + "B", + 5 + ], + [ + "abb", + "CbBBAc", + 9 + ], + [ + "abb", + "aC", + 4 + ], + [ + "abb", + "bbbcaca", + 10 + ], + [ + "abbAAABBb", + "CbCcB", + 14 + ], + [ + "abbAcB", + "BbaaBc", + 8 + ], + [ + "abbBBCcB", + "aaa", + 14 + ], + [ + "abbBbAbcc", + "c", + 16 + ], + [ + "abbBbbbcC", + "aBbBaB", + 10 + ], + [ + "abbCBA", + "aA", + 8 + ], + [ + "abbCBA", + "bb", + 8 + ], + [ + "abbCBabac", + "CBAB", + 12 + ], + [ + "abbCaBB", + "Ab", + 11 + ], + [ + "abbCaBC", + "aBcc", + 9 + ], + [ + "abbCbbBB", + "cbbaaca", + 12 + ], + [ + "abbCc", + "bC", + 6 + ], + [ + "abbaA", + "BabaAAaBb", + 11 + ], + [ + "abbaBaaBc", + "CAcBbc", + 13 + ], + [ + "abbaBbc", + "aABbAcc", + 8 + ], + [ + "abbaCcba", + "Cba", + 10 + ], + [ + "abbbA", + "ACAcc", + 9 + ], + [ + "abbbBAbAA", + "aCACcAbA", + 10 + ], + [ + "abbbCCCaC", + "acBCBABcC", + 13 + ], + [ + "abbbb", + "cCcbBcb", + 9 + ], + [ + "abbbc", + "C", + 9 + ], + [ + "abbbca", + "BBAcBbAB", + 12 + ], + [ + "abbcABB", + "ac", + 10 + ], + [ + "abbcB", + "ABbcCB", + 4 + ], + [ + "abbcBac", + "bACca", + 9 + ], + [ + "abbccAa", + "AcC", + 10 + ], + [ + "abbccb", + "cbCb", + 7 + ], + [ + "abc", + "AbC", + 2 + ], + [ + "abc", + "BAAb", + 7 + ], + [ + "abc", + "C", + 5 + ], + [ + "abc", + "aB", + 3 + ], + [ + "abcAA", + "CAAabC", + 11 + ], + [ + "abcAaaccb", + "cAcAAcbcA", + 11 + ], + [ + "abcAbB", + "AbcCacc", + 8 + ], + [ + "abcAcaB", + "acBcBb", + 7 + ], + [ + "abcAcbbb", + "CaABc", + 13 + ], + [ + "abcB", + "AaCcAbCa", + 11 + ], + [ + "abcB", + "bbab", + 5 + ], + [ + "abcBaBb", + "cBBa", + 8 + ], + [ + "abcBacc", + "ACBa", + 8 + ], + [ + "abcBb", + "BBbcccBB", + 9 + ], + [ + "abcBbBAb", + "cB", + 12 + ], + [ + "abcC", + "BCc", + 5 + ], + [ + "abcCAB", + "CbCcC", + 8 + ], + [ + "abcCBAc", + "ACCBaA", + 7 + ], + [ + "abcCaBcCB", + "AAba", + 15 + ], + [ + "abcCbBCA", + "Ba", + 13 + ], + [ + "abca", + "cbaccAA", + 9 + ], + [ + "abcaCB", + "CCaaAB", + 8 + ], + [ + "abcb", + "a", + 6 + ], + [ + "abcbbC", + "BBc", + 9 + ], + [ + "abcbbbcb", + "b", + 14 + ], + [ + "abccC", + "bbcBab", + 8 + ], + [ + "abccCBabC", + "AbABaaabc", + 10 + ], + [ + "abccCaB", + "Bbaa", + 10 + ], + [ + "abccCcAa", + "BCbb", + 13 + ], + [ + "abccaB", + "aCABaba", + 9 + ], + [ + "abccaC", + "ABbCB", + 9 + ], + [ + "abccaCcBB", + "AAB", + 14 + ], + [ + "abccbCC", + "BAc", + 11 + ], + [ + "ac", + "A", + 3 + ], + [ + "ac", + "AAaCcbBa", + 12 + ], + [ + "ac", + "AAcCbbCca", + 15 + ], + [ + "ac", + "ACAcCa", + 9 + ], + [ + "ac", + "ACCBbAcCc", + 15 + ], + [ + "ac", + "B", + 4 + ], + [ + "ac", + "BAab", + 6 + ], + [ + "ac", + "BCCBbccb", + 14 + ], + [ + "ac", + "BCc", + 4 + ], + [ + "ac", + "BaBbbAA", + 12 + ], + [ + "ac", + "BcCACbB", + 12 + ], + [ + "ac", + "CBA", + 6 + ], + [ + "ac", + "CccBcaB", + 12 + ], + [ + "ac", + "aB", + 2 + ], + [ + "ac", + "aBBCacBbC", + 14 + ], + [ + "ac", + "aC", + 1 + ], + [ + "ac", + "aCcCC", + 6 + ], + [ + "ac", + "aaCaCaca", + 12 + ], + [ + "ac", + "ab", + 2 + ], + [ + "ac", + "acacCAbBA", + 14 + ], + [ + "ac", + "bACaaCCCC", + 15 + ], + [ + "ac", + "bAbaCCcBC", + 14 + ], + [ + "ac", + "bCCaAaa", + 12 + ], + [ + "ac", + "cBAcc", + 7 + ], + [ + "acA", + "AaCCAaabc", + 13 + ], + [ + "acA", + "aABBBACc", + 12 + ], + [ + "acA", + "ac", + 2 + ], + [ + "acA", + "cBcBb", + 8 + ], + [ + "acAA", + "bc", + 6 + ], + [ + "acAAABBa", + "cbcC", + 13 + ], + [ + "acAACBA", + "BcbaCbbA", + 8 + ], + [ + "acAB", + "ccbAAAa", + 10 + ], + [ + "acABCCA", + "BCcABBAa", + 9 + ], + [ + "acABc", + "BBcABbCba", + 11 + ], + [ + "acAC", + "A", + 6 + ], + [ + "acACCB", + "AAcBCba", + 9 + ], + [ + "acAaaAcba", + "BabCcbCbb", + 15 + ], + [ + "acAaaaBc", + "BacCBc", + 10 + ], + [ + "acAabACa", + "acaCB", + 8 + ], + [ + "acAabBbA", + "cabCb", + 8 + ], + [ + "acAacCc", + "BBCC", + 11 + ], + [ + "acAb", + "cc", + 6 + ], + [ + "acAbabcBa", + "ABABcaB", + 11 + ], + [ + "acAc", + "ABa", + 6 + ], + [ + "acAc", + "CcBCaa", + 9 + ], + [ + "acAc", + "aBbCaB", + 8 + ], + [ + "acAcBCCa", + "BaB", + 13 + ], + [ + "acAcCAc", + "cAbab", + 9 + ], + [ + "acAcaC", + "B", + 12 + ], + [ + "acAcabB", + "baaacbB", + 7 + ], + [ + "acAcbcAC", + "ccaCbCcc", + 8 + ], + [ + "acB", + "ABcAcBAcb", + 13 + ], + [ + "acB", + "acb", + 1 + ], + [ + "acB", + "bCccA", + 8 + ], + [ + "acB", + "cbabb", + 7 + ], + [ + "acBA", + "BcAaBCBAA", + 11 + ], + [ + "acBAcc", + "AAabb", + 10 + ], + [ + "acBB", + "AaaAbB", + 7 + ], + [ + "acBB", + "cA", + 6 + ], + [ + "acBBAb", + "bCcAbBCAC", + 11 + ], + [ + "acBBAbacB", + "aaCBB", + 12 + ], + [ + "acBBBbAcb", + "BAa", + 14 + ], + [ + "acBBCC", + "ccAbcBbCb", + 10 + ], + [ + "acBBCc", + "bcCaAbAa", + 13 + ], + [ + "acBBbBa", + "Cba", + 9 + ], + [ + "acBBcCC", + "ABbb", + 10 + ], + [ + "acBCBcabA", + "caCbca", + 9 + ], + [ + "acBCCbab", + "ccA", + 12 + ], + [ + "acBCa", + "BabbAcb", + 10 + ], + [ + "acBa", + "AC", + 6 + ], + [ + "acBaA", + "ABBCC", + 7 + ], + [ + "acBaAacc", + "bbab", + 13 + ], + [ + "acBaBCab", + "ac", + 12 + ], + [ + "acBaBaC", + "bbccbac", + 10 + ], + [ + "acBacCbAc", + "bBBACbaAA", + 11 + ], + [ + "acBb", + "CA", + 7 + ], + [ + "acBb", + "bCBAAB", + 8 + ], + [ + "acBbAc", + "aaaBCA", + 8 + ], + [ + "acBbCca", + "acA", + 9 + ], + [ + "acBcCcaB", + "CC", + 13 + ], + [ + "acBcabcAC", + "Aac", + 13 + ], + [ + "acC", + "AABcbaC", + 9 + ], + [ + "acC", + "AcBAaC", + 7 + ], + [ + "acC", + "CabBabaBC", + 14 + ], + [ + "acC", + "b", + 6 + ], + [ + "acC", + "baBCBCbAC", + 13 + ], + [ + "acC", + "baaCBCBC", + 11 + ], + [ + "acCA", + "C", + 6 + ], + [ + "acCA", + "aabBCaaaC", + 13 + ], + [ + "acCAAaCB", + "ba", + 14 + ], + [ + "acCAB", + "BccCcCbB", + 10 + ], + [ + "acCAa", + "c", + 8 + ], + [ + "acCAabBcc", + "Bb", + 16 + ], + [ + "acCAb", + "abB", + 7 + ], + [ + "acCAcc", + "cccCCacCC", + 10 + ], + [ + "acCB", + "B", + 6 + ], + [ + "acCBAcb", + "acaBcA", + 6 + ], + [ + "acCBBA", + "caccBbabC", + 9 + ], + [ + "acCBBb", + "aCbc", + 7 + ], + [ + "acCBCbaBC", + "bCabBc", + 11 + ], + [ + "acCBaaAc", + "BAAABCA", + 13 + ], + [ + "acCC", + "BAB", + 8 + ], + [ + "acCCABBb", + "Bbcca", + 13 + ], + [ + "acCCCaAb", + "acCCbAcca", + 9 + ], + [ + "acCCab", + "aB", + 9 + ], + [ + "acCCbA", + "CCAbCbBA", + 9 + ], + [ + "acCCbBcc", + "aAAaA", + 14 + ], + [ + "acCCcaC", + "AcbBabcCb", + 13 + ], + [ + "acCCcab", + "ccab", + 6 + ], + [ + "acCa", + "aaAcAbBbB", + 14 + ], + [ + "acCaAA", + "BAbcAcbC", + 13 + ], + [ + "acCaABcbb", + "A", + 16 + ], + [ + "acCaC", + "abBACBCB", + 10 + ], + [ + "acCaCa", + "aBa", + 8 + ], + [ + "acCb", + "aACBa", + 5 + ], + [ + "acCbBBa", + "BbbACCccb", + 15 + ], + [ + "acCbCCCBb", + "B", + 16 + ], + [ + "acCbbBB", + "bcca", + 11 + ], + [ + "acCc", + "A", + 7 + ], + [ + "acCc", + "AABBa", + 9 + ], + [ + "acCcA", + "CBBcaBCCA", + 11 + ], + [ + "acCcAcB", + "b", + 13 + ], + [ + "acCcB", + "BBCCBA", + 7 + ], + [ + "acCcaAA", + "b", + 14 + ], + [ + "acCcacB", + "cbAccBcb", + 10 + ], + [ + "acCcc", + "AC", + 7 + ], + [ + "aca", + "ACAB", + 5 + ], + [ + "aca", + "AaAc", + 6 + ], + [ + "aca", + "cAAbacBCa", + 12 + ], + [ + "aca", + "cbCCabAaB", + 14 + ], + [ + "acaA", + "C", + 7 + ], + [ + "acaA", + "aCbcACABa", + 11 + ], + [ + "acaAAAbc", + "B", + 15 + ], + [ + "acaABAcA", + "Cacca", + 10 + ], + [ + "acaAcb", + "cbc", + 8 + ], + [ + "acaBA", + "baBCbCBBC", + 13 + ], + [ + "acaBcacBc", + "b", + 17 + ], + [ + "acaBccaC", + "C", + 14 + ], + [ + "acaC", + "bccCAccA", + 12 + ], + [ + "acaCAaBB", + "Bc", + 14 + ], + [ + "acaCBBB", + "A", + 13 + ], + [ + "acaCaA", + "BCB", + 10 + ], + [ + "acaCaC", + "c", + 10 + ], + [ + "acaCbC", + "acaabCaa", + 6 + ], + [ + "acaCcaAB", + "ABAaaaba", + 12 + ], + [ + "acaCcbcB", + "CcbACBBc", + 10 + ], + [ + "acaCcbcc", + "C", + 14 + ], + [ + "acaa", + "AabBa", + 6 + ], + [ + "acaaBAA", + "cBBbaAB", + 10 + ], + [ + "acaaCC", + "cAACA", + 6 + ], + [ + "acaaaCCA", + "AaC", + 11 + ], + [ + "acaacAbB", + "bCbb", + 12 + ], + [ + "acaaca", + "cBACccCa", + 11 + ], + [ + "acaacb", + "ccCbBca", + 10 + ], + [ + "acabCb", + "ccaCbA", + 6 + ], + [ + "acabaBCbB", + "cab", + 12 + ], + [ + "acabcA", + "AB", + 10 + ], + [ + "acacA", + "bcBACcaa", + 10 + ], + [ + "acacbA", + "A", + 10 + ], + [ + "acacbABa", + "bCbcBa", + 9 + ], + [ + "acacbBa", + "BAbA", + 10 + ], + [ + "acb", + "AabAbA", + 8 + ], + [ + "acb", + "BCCa", + 7 + ], + [ + "acb", + "cBCB", + 6 + ], + [ + "acbA", + "AcBCbCa", + 8 + ], + [ + "acbA", + "aAaBCAB", + 9 + ], + [ + "acbAA", + "bc", + 8 + ], + [ + "acbACbbA", + "a", + 14 + ], + [ + "acbAbacc", + "caAcB", + 10 + ], + [ + "acbB", + "aBAb", + 5 + ], + [ + "acbBBC", + "caB", + 8 + ], + [ + "acbBaAAb", + "bbBcABBaC", + 13 + ], + [ + "acbBaC", + "CaA", + 9 + ], + [ + "acbBc", + "abB", + 4 + ], + [ + "acbC", + "ABABaBbca", + 13 + ], + [ + "acbC", + "bbBbcaaA", + 13 + ], + [ + "acbCA", + "A", + 8 + ], + [ + "acbCA", + "cBb", + 7 + ], + [ + "acbCBb", + "acAbCCaBc", + 8 + ], + [ + "acbCaaAaB", + "CaCc", + 14 + ], + [ + "acbCabC", + "cBACbcBC", + 10 + ], + [ + "acbaBaA", + "BBcA", + 9 + ], + [ + "acbabAa", + "CaAbcbc", + 10 + ], + [ + "acbabbaB", + "c", + 14 + ], + [ + "acbb", + "AAa", + 7 + ], + [ + "acbb", + "AbCaBCcBA", + 13 + ], + [ + "acbb", + "BbAca", + 9 + ], + [ + "acbb", + "CBaCBcCbA", + 12 + ], + [ + "acbbA", + "AcCAAbc", + 9 + ], + [ + "acbbAbA", + "CCCaccab", + 13 + ], + [ + "acbbcABBB", + "BcCBbb", + 11 + ], + [ + "acbc", + "aBAbB", + 6 + ], + [ + "acbcAa", + "BaCC", + 10 + ], + [ + "acbcBbc", + "B", + 12 + ], + [ + "acbcCAC", + "ACaAab", + 11 + ], + [ + "acbcCab", + "BAb", + 10 + ], + [ + "acbcaacbc", + "CbccCCBcb", + 11 + ], + [ + "acbcbbcc", + "CcAbaCCbb", + 13 + ], + [ + "acbccACA", + "b", + 14 + ], + [ + "acbccBcB", + "CCcc", + 10 + ], + [ + "acbccCB", + "bbbaaCAAB", + 12 + ], + [ + "acc", + "C", + 5 + ], + [ + "acc", + "bAac", + 5 + ], + [ + "accA", + "CBBaA", + 8 + ], + [ + "accAA", + "C", + 9 + ], + [ + "accABbb", + "cBbAAbaC", + 12 + ], + [ + "accAac", + "CCcacb", + 7 + ], + [ + "accAb", + "bcbAbB", + 6 + ], + [ + "accAcbAa", + "B", + 15 + ], + [ + "accB", + "bBbAb", + 9 + ], + [ + "accBC", + "bCCCCab", + 10 + ], + [ + "accBbcCaa", + "aaCcaBACB", + 12 + ], + [ + "accC", + "CCcbBc", + 8 + ], + [ + "accC", + "aa", + 6 + ], + [ + "accCAcACa", + "AABbCCc", + 14 + ], + [ + "accCCaCB", + "aCBba", + 11 + ], + [ + "accCCcA", + "cBbA", + 10 + ], + [ + "accCaB", + "aBaB", + 6 + ], + [ + "accCbbC", + "cBBcaa", + 12 + ], + [ + "acca", + "BBCAcbaba", + 13 + ], + [ + "acca", + "BBcCC", + 7 + ], + [ + "accaACAAc", + "cbbCABA", + 12 + ], + [ + "accaaAcAb", + "ccB", + 13 + ], + [ + "accaaC", + "CbA", + 10 + ], + [ + "accab", + "AAaAcA", + 9 + ], + [ + "accabbcB", + "aa", + 12 + ], + [ + "accac", + "C", + 9 + ], + [ + "accb", + "CBCAcCaBB", + 13 + ], + [ + "accbAA", + "Ba", + 10 + ], + [ + "accbAa", + "Ca", + 9 + ], + [ + "accbBac", + "caC", + 9 + ], + [ + "accbCcBAc", + "cbA", + 12 + ], + [ + "accbaAc", + "ACAaCAAA", + 11 + ], + [ + "accbb", + "CCcC", + 7 + ], + [ + "acccAB", + "cb", + 9 + ], + [ + "acccC", + "baaaaB", + 10 + ], + [ + "acccaAaB", + "C", + 15 + ], + [ + "acccc", + "cAAC", + 7 + ], + [ + "b", + "A", + 2 + ], + [ + "b", + "AAbAaCb", + 12 + ], + [ + "b", + "ABBBC", + 9 + ], + [ + "b", + "ABbCCBC", + 12 + ], + [ + "b", + "ABc", + 5 + ], + [ + "b", + "ABcba", + 8 + ], + [ + "b", + "ACAAC", + 10 + ], + [ + "b", + "ACBbAcabc", + 16 + ], + [ + "b", + "ACBbCBcCC", + 16 + ], + [ + "b", + "ACCaA", + 10 + ], + [ + "b", + "Aa", + 4 + ], + [ + "b", + "AaAbBCA", + 12 + ], + [ + "b", + "Aac", + 6 + ], + [ + "b", + "AacaBc", + 11 + ], + [ + "b", + "AbaC", + 6 + ], + [ + "b", + "Abcba", + 8 + ], + [ + "b", + "Ac", + 4 + ], + [ + "b", + "AcCbB", + 8 + ], + [ + "b", + "AcCbBca", + 12 + ], + [ + "b", + "AcaaCaAa", + 16 + ], + [ + "b", + "B", + 1 + ], + [ + "b", + "BBA", + 5 + ], + [ + "b", + "BBaaabB", + 12 + ], + [ + "b", + "BBbaBcba", + 14 + ], + [ + "b", + "BCBaC", + 9 + ], + [ + "b", + "BCcAABb", + 12 + ], + [ + "b", + "Ba", + 3 + ], + [ + "b", + "BaC", + 5 + ], + [ + "b", + "BaCbBc", + 10 + ], + [ + "b", + "Bac", + 5 + ], + [ + "b", + "BacBAc", + 11 + ], + [ + "b", + "Bb", + 2 + ], + [ + "b", + "BbA", + 4 + ], + [ + "b", + "BbBacbb", + 12 + ], + [ + "b", + "BbacaCc", + 12 + ], + [ + "b", + "BbbcAcAab", + 16 + ], + [ + "b", + "Bc", + 3 + ], + [ + "b", + "BcBA", + 7 + ], + [ + "b", + "BcBCAAaCB", + 17 + ], + [ + "b", + "BcC", + 5 + ], + [ + "b", + "BcCa", + 7 + ], + [ + "b", + "C", + 2 + ], + [ + "b", + "CAB", + 5 + ], + [ + "b", + "CACCBca", + 13 + ], + [ + "b", + "CBABaBAB", + 15 + ], + [ + "b", + "CBCCa", + 9 + ], + [ + "b", + "CBCCcC", + 11 + ], + [ + "b", + "CBCcaBBAB", + 17 + ], + [ + "b", + "CBaBaaCbB", + 16 + ], + [ + "b", + "CBaabA", + 10 + ], + [ + "b", + "CCBABBcC", + 15 + ], + [ + "b", + "CCC", + 6 + ], + [ + "b", + "CCbCA", + 8 + ], + [ + "b", + "Ca", + 4 + ], + [ + "b", + "CaCBcA", + 11 + ], + [ + "b", + "Cabacc", + 10 + ], + [ + "b", + "CabccCBCB", + 16 + ], + [ + "b", + "CacAbAAac", + 16 + ], + [ + "b", + "CbA", + 4 + ], + [ + "b", + "CbcBbA", + 10 + ], + [ + "b", + "Cc", + 4 + ], + [ + "b", + "CcAABbA", + 12 + ], + [ + "b", + "CcabBB", + 10 + ], + [ + "b", + "CcacCcAcC", + 18 + ], + [ + "b", + "CcbCACB", + 12 + ], + [ + "b", + "Ccbc", + 6 + ], + [ + "b", + "a", + 2 + ], + [ + "b", + "aA", + 4 + ], + [ + "b", + "aABBCcC", + 13 + ], + [ + "b", + "aABccbaCc", + 16 + ], + [ + "b", + "aACCCCba", + 14 + ], + [ + "b", + "aACCa", + 10 + ], + [ + "b", + "aAaBA", + 9 + ], + [ + "b", + "aAacBab", + 12 + ], + [ + "b", + "aB", + 3 + ], + [ + "b", + "aBCAac", + 11 + ], + [ + "b", + "aBCBC", + 9 + ], + [ + "b", + "aBCbacab", + 14 + ], + [ + "b", + "aBaCa", + 9 + ], + [ + "b", + "aBc", + 5 + ], + [ + "b", + "aC", + 4 + ], + [ + "b", + "aCBA", + 7 + ], + [ + "b", + "aCCa", + 8 + ], + [ + "b", + "aa", + 4 + ], + [ + "b", + "aabCCBca", + 14 + ], + [ + "b", + "aabbAb", + 10 + ], + [ + "b", + "aabbcC", + 10 + ], + [ + "b", + "ab", + 2 + ], + [ + "b", + "abB", + 4 + ], + [ + "b", + "abBBcb", + 10 + ], + [ + "b", + "abCCBCA", + 12 + ], + [ + "b", + "abcACcaa", + 14 + ], + [ + "b", + "abcBccCa", + 14 + ], + [ + "b", + "abca", + 6 + ], + [ + "b", + "ac", + 4 + ], + [ + "b", + "acACCaAca", + 18 + ], + [ + "b", + "acC", + 6 + ], + [ + "b", + "aca", + 6 + ], + [ + "b", + "acaabCC", + 12 + ], + [ + "b", + "b", + 0 + ], + [ + "b", + "bACAbB", + 10 + ], + [ + "b", + "bBAccb", + 10 + ], + [ + "b", + "bBBC", + 6 + ], + [ + "b", + "bBbaAA", + 10 + ], + [ + "b", + "bC", + 2 + ], + [ + "b", + "bCA", + 4 + ], + [ + "b", + "bCBCAbaCA", + 16 + ], + [ + "b", + "bCCbA", + 8 + ], + [ + "b", + "bCaAC", + 8 + ], + [ + "b", + "bCaCAB", + 10 + ], + [ + "b", + "bCabBB", + 10 + ], + [ + "b", + "bCbBbcA", + 12 + ], + [ + "b", + "bCcbAb", + 10 + ], + [ + "b", + "baBACBaaB", + 16 + ], + [ + "b", + "baCCbBC", + 12 + ], + [ + "b", + "baabBABb", + 14 + ], + [ + "b", + "baabbaCBB", + 16 + ], + [ + "b", + "baaccc", + 10 + ], + [ + "b", + "babBAcb", + 12 + ], + [ + "b", + "bb", + 2 + ], + [ + "b", + "bcBACbaBb", + 16 + ], + [ + "b", + "bcCa", + 6 + ], + [ + "b", + "bcCcAcbc", + 14 + ], + [ + "b", + "c", + 2 + ], + [ + "b", + "cABCaB", + 11 + ], + [ + "b", + "cAC", + 6 + ], + [ + "b", + "cAbBbBcc", + 14 + ], + [ + "b", + "cAbaBABBc", + 16 + ], + [ + "b", + "cAbaCBAa", + 14 + ], + [ + "b", + "cBAAAcaA", + 15 + ], + [ + "b", + "cBBcbAabb", + 16 + ], + [ + "b", + "cBCCAcb", + 12 + ], + [ + "b", + "cBCCa", + 9 + ], + [ + "b", + "cBCaCAc", + 13 + ], + [ + "b", + "cBbaBbcB", + 14 + ], + [ + "b", + "cBbbBaAa", + 14 + ], + [ + "b", + "cCAACba", + 12 + ], + [ + "b", + "cCaccAaCb", + 16 + ], + [ + "b", + "cCcbA", + 8 + ], + [ + "b", + "caA", + 6 + ], + [ + "b", + "caAb", + 6 + ], + [ + "b", + "caAccaCbb", + 16 + ], + [ + "b", + "caB", + 5 + ], + [ + "b", + "cabBbcA", + 12 + ], + [ + "b", + "cacbbBAb", + 14 + ], + [ + "b", + "cb", + 2 + ], + [ + "b", + "cbAa", + 6 + ], + [ + "b", + "cbAaCa", + 10 + ], + [ + "b", + "cbcBAcCbA", + 16 + ], + [ + "b", + "ccCaacc", + 14 + ], + [ + "b", + "ccCbaaaa", + 14 + ], + [ + "b", + "ccaC", + 8 + ], + [ + "b", + "ccbbcb", + 10 + ], + [ + "bA", + "AAAcACAbA", + 14 + ], + [ + "bA", + "AAbB", + 6 + ], + [ + "bA", + "ABaC", + 6 + ], + [ + "bA", + "ACBbbA", + 8 + ], + [ + "bA", + "ACBbccB", + 12 + ], + [ + "bA", + "ACbbBB", + 10 + ], + [ + "bA", + "Accc", + 8 + ], + [ + "bA", + "BAcCabc", + 11 + ], + [ + "bA", + "BCCcccB", + 13 + ], + [ + "bA", + "BabcCBCaa", + 15 + ], + [ + "bA", + "BbCaCBca", + 13 + ], + [ + "bA", + "BbbcbCCB", + 14 + ], + [ + "bA", + "Bbca", + 5 + ], + [ + "bA", + "C", + 4 + ], + [ + "bA", + "CAbABbBC", + 12 + ], + [ + "bA", + "CB", + 4 + ], + [ + "bA", + "CBCBa", + 8 + ], + [ + "bA", + "CbAaAaB", + 10 + ], + [ + "bA", + "CbcAAc", + 8 + ], + [ + "bA", + "CcAAcbcBC", + 16 + ], + [ + "bA", + "aAc", + 4 + ], + [ + "bA", + "aBCCcAcb", + 13 + ], + [ + "bA", + "aCb", + 6 + ], + [ + "bA", + "aCcbbc", + 10 + ], + [ + "bA", + "bAbcaAA", + 10 + ], + [ + "bA", + "bAc", + 2 + ], + [ + "bA", + "ba", + 1 + ], + [ + "bA", + "bb", + 2 + ], + [ + "bA", + "cAcCABaAb", + 15 + ], + [ + "bA", + "cCAABbBb", + 14 + ], + [ + "bA", + "cCC", + 6 + ], + [ + "bA", + "cabc", + 6 + ], + [ + "bA", + "ccCbcC", + 10 + ], + [ + "bAA", + "AabBB", + 8 + ], + [ + "bAA", + "BBAAaBCbC", + 13 + ], + [ + "bAA", + "C", + 6 + ], + [ + "bAA", + "CBaBcBb", + 12 + ], + [ + "bAA", + "CCBAc", + 7 + ], + [ + "bAA", + "abBBBAA", + 8 + ], + [ + "bAA", + "baacAbb", + 9 + ], + [ + "bAA", + "bcbBccAb", + 12 + ], + [ + "bAAA", + "C", + 8 + ], + [ + "bAAA", + "bBAAC", + 4 + ], + [ + "bAAAA", + "c", + 10 + ], + [ + "bAAAAbaCa", + "abacbC", + 12 + ], + [ + "bAAACbbb", + "Aaa", + 12 + ], + [ + "bAAACccC", + "CC", + 12 + ], + [ + "bAAAacAc", + "caC", + 12 + ], + [ + "bAAAbABC", + "CBCbBa", + 12 + ], + [ + "bAAB", + "bC", + 6 + ], + [ + "bAABaCA", + "A", + 12 + ], + [ + "bAABb", + "CaBBB", + 6 + ], + [ + "bAABcbbB", + "CbACCB", + 11 + ], + [ + "bAAC", + "AabbbaBA", + 13 + ], + [ + "bAAC", + "Acb", + 6 + ], + [ + "bAAC", + "aaB", + 6 + ], + [ + "bAACcACbc", + "Aaa", + 14 + ], + [ + "bAAa", + "AaABbabC", + 11 + ], + [ + "bAAa", + "bCAbbaa", + 7 + ], + [ + "bAAaCba", + "bAca", + 7 + ], + [ + "bAAaaaa", + "BC", + 13 + ], + [ + "bAAbAab", + "CBcA", + 12 + ], + [ + "bAAbCa", + "C", + 10 + ], + [ + "bAAbb", + "A", + 8 + ], + [ + "bAAc", + "CCAbca", + 8 + ], + [ + "bAAc", + "cCBAba", + 9 + ], + [ + "bAAcA", + "bbc", + 6 + ], + [ + "bAAccABbb", + "aAc", + 13 + ], + [ + "bAAccaCC", + "A", + 14 + ], + [ + "bAB", + "ACBcB", + 7 + ], + [ + "bAB", + "CbAA", + 4 + ], + [ + "bAB", + "bbABC", + 4 + ], + [ + "bABAC", + "BCaabaCBC", + 12 + ], + [ + "bABACCCB", + "abB", + 12 + ], + [ + "bABAaB", + "cCCCBbc", + 13 + ], + [ + "bABAaCBBA", + "BAbA", + 11 + ], + [ + "bABAaba", + "A", + 12 + ], + [ + "bABAba", + "BBBAccBA", + 9 + ], + [ + "bABAbbCbB", + "bA", + 14 + ], + [ + "bABAcCCb", + "abCaB", + 11 + ], + [ + "bABBCCC", + "bbCbBc", + 10 + ], + [ + "bABBa", + "bA", + 6 + ], + [ + "bABBcAc", + "bAC", + 9 + ], + [ + "bABC", + "AbBca", + 7 + ], + [ + "bABCBcAc", + "cBCAA", + 10 + ], + [ + "bABCC", + "aBCa", + 5 + ], + [ + "bABCCbbbC", + "cABACBBa", + 10 + ], + [ + "bABCaBCa", + "CaBbB", + 10 + ], + [ + "bABCbaB", + "ACCaCBc", + 10 + ], + [ + "bABa", + "BA", + 5 + ], + [ + "bABa", + "BAB", + 3 + ], + [ + "bABaCBBAa", + "Aa", + 14 + ], + [ + "bABb", + "acA", + 7 + ], + [ + "bABb", + "ccCA", + 8 + ], + [ + "bABbAB", + "ABCBCACc", + 11 + ], + [ + "bABbAC", + "bcBaCcAb", + 10 + ], + [ + "bABbBCB", + "cb", + 12 + ], + [ + "bABbaB", + "cAcAbbCAb", + 11 + ], + [ + "bABbaBAA", + "C", + 16 + ], + [ + "bABbbABBB", + "cAcb", + 14 + ], + [ + "bABbbbC", + "a", + 13 + ], + [ + "bABbcCab", + "ccccaa", + 11 + ], + [ + "bAC", + "AcAbc", + 7 + ], + [ + "bAC", + "bCcbAabb", + 12 + ], + [ + "bACA", + "ACaAAAaC", + 12 + ], + [ + "bACA", + "CAc", + 5 + ], + [ + "bACA", + "c", + 7 + ], + [ + "bACACAAa", + "cbBbC", + 14 + ], + [ + "bACAb", + "Cbc", + 8 + ], + [ + "bACAcc", + "BacBcAB", + 9 + ], + [ + "bACB", + "cBccC", + 8 + ], + [ + "bACBAC", + "BCCABaC", + 6 + ], + [ + "bACBBaA", + "bBACB", + 8 + ], + [ + "bACBCbb", + "b", + 12 + ], + [ + "bACBa", + "BCC", + 7 + ], + [ + "bACBaAcAb", + "C", + 16 + ], + [ + "bACBabABB", + "aCA", + 13 + ], + [ + "bACBcB", + "accBbaCcC", + 13 + ], + [ + "bACCaCcBb", + "aBaBcb", + 11 + ], + [ + "bACaBC", + "bA", + 8 + ], + [ + "bACaCCBAB", + "aaBacb", + 13 + ], + [ + "bACaCcC", + "aBCac", + 8 + ], + [ + "bACab", + "aBcBcAB", + 10 + ], + [ + "bACac", + "Bc", + 7 + ], + [ + "bACac", + "cBAacCAcB", + 10 + ], + [ + "bACbAA", + "B", + 11 + ], + [ + "bACbBbc", + "CbcAcBcA", + 11 + ], + [ + "bACc", + "bcA", + 5 + ], + [ + "bACc", + "cCb", + 6 + ], + [ + "bACcABb", + "BaC", + 10 + ], + [ + "bACcCaaC", + "AAbAbb", + 13 + ], + [ + "bAa", + "AaaACbAcc", + 14 + ], + [ + "bAa", + "AcAabb", + 8 + ], + [ + "bAa", + "bCbBCABBA", + 13 + ], + [ + "bAaA", + "aACcbaB", + 10 + ], + [ + "bAaAA", + "BacbCbBaa", + 14 + ], + [ + "bAaAAAa", + "cabAbA", + 9 + ], + [ + "bAaABAAC", + "AB", + 12 + ], + [ + "bAaAcA", + "cbC", + 11 + ], + [ + "bAaB", + "B", + 6 + ], + [ + "bAaB", + "acCCbb", + 11 + ], + [ + "bAaBAA", + "a", + 10 + ], + [ + "bAaBAcac", + "AABcaa", + 7 + ], + [ + "bAaBC", + "CBcaaaBa", + 10 + ], + [ + "bAaBa", + "C", + 10 + ], + [ + "bAaBaCBAB", + "CCbCCC", + 15 + ], + [ + "bAaBb", + "bCAA", + 7 + ], + [ + "bAaBbBc", + "AC", + 11 + ], + [ + "bAaBbc", + "cbbcAaab", + 10 + ], + [ + "bAaBc", + "a", + 8 + ], + [ + "bAaBc", + "cbCCBBbcc", + 12 + ], + [ + "bAaC", + "ABA", + 6 + ], + [ + "bAaC", + "BAaACAB", + 7 + ], + [ + "bAaCCa", + "CbCBB", + 10 + ], + [ + "bAaCa", + "bCBaABcbB", + 13 + ], + [ + "bAaCab", + "caBC", + 9 + ], + [ + "bAaa", + "AAcCbAC", + 11 + ], + [ + "bAaa", + "bBAAb", + 5 + ], + [ + "bAaaA", + "AbA", + 6 + ], + [ + "bAaaB", + "CABBC", + 8 + ], + [ + "bAaaB", + "bABBaC", + 6 + ], + [ + "bAaabaAc", + "cBaa", + 12 + ], + [ + "bAab", + "CcC", + 8 + ], + [ + "bAab", + "bAc", + 4 + ], + [ + "bAabAbC", + "A", + 12 + ], + [ + "bAabBACA", + "cCca", + 14 + ], + [ + "bAabBBCb", + "CbaCbA", + 12 + ], + [ + "bAabCAB", + "aA", + 10 + ], + [ + "bAabb", + "AaacAAA", + 11 + ], + [ + "bAabbC", + "babAb", + 6 + ], + [ + "bAabcA", + "bA", + 8 + ], + [ + "bAabcbc", + "BaAabcAaa", + 9 + ], + [ + "bAabccAa", + "aCbaB", + 12 + ], + [ + "bAac", + "ccACbbAA", + 13 + ], + [ + "bAacA", + "BBaACC", + 8 + ], + [ + "bAacAb", + "bacccBCc", + 10 + ], + [ + "bAacAbBca", + "C", + 17 + ], + [ + "bAacC", + "bbaAb", + 6 + ], + [ + "bAaccCB", + "c", + 12 + ], + [ + "bAb", + "ABCCAACCA", + 15 + ], + [ + "bAb", + "CCBAA", + 7 + ], + [ + "bAbACaC", + "bABcCAb", + 6 + ], + [ + "bAbAaC", + "BAbbA", + 6 + ], + [ + "bAbAabcab", + "CAabcBa", + 10 + ], + [ + "bAbAbcA", + "CAABBca", + 8 + ], + [ + "bAbBC", + "AACcbCca", + 11 + ], + [ + "bAbBCbaB", + "acacab", + 11 + ], + [ + "bAbBaCBcb", + "BaA", + 14 + ], + [ + "bAbBbbBbb", + "cAaacAC", + 16 + ], + [ + "bAbCAABaB", + "ACcBAccB", + 11 + ], + [ + "bAbCbCcCA", + "aBB", + 15 + ], + [ + "bAbCcB", + "aCAbA", + 10 + ], + [ + "bAba", + "CaAAb", + 8 + ], + [ + "bAbaCC", + "Bba", + 7 + ], + [ + "bAbab", + "CbCAcBcB", + 10 + ], + [ + "bAbb", + "acAbCBb", + 8 + ], + [ + "bAbb", + "b", + 6 + ], + [ + "bAbb", + "bAaAcBBab", + 11 + ], + [ + "bAbbB", + "AaACcBacC", + 14 + ], + [ + "bAbbaa", + "cCBC", + 11 + ], + [ + "bAbbbbBc", + "cABcBBC", + 9 + ], + [ + "bAbbbc", + "aCA", + 11 + ], + [ + "bAbc", + "Accbacc", + 9 + ], + [ + "bAbcBB", + "A", + 10 + ], + [ + "bAbcCaA", + "bACaCAcbC", + 11 + ], + [ + "bAbcCab", + "ABC", + 9 + ], + [ + "bAbcCbAA", + "BcBb", + 11 + ], + [ + "bAbcbA", + "CBCBCac", + 11 + ], + [ + "bAc", + "aaC", + 4 + ], + [ + "bAcA", + "bb", + 6 + ], + [ + "bAcAABA", + "aAAbca", + 9 + ], + [ + "bAcAAbA", + "BABabc", + 8 + ], + [ + "bAcAAbb", + "aC", + 12 + ], + [ + "bAcAbc", + "CAAcaaBa", + 10 + ], + [ + "bAcAc", + "BaC", + 7 + ], + [ + "bAcB", + "bcBC", + 4 + ], + [ + "bAcB", + "bccaA", + 6 + ], + [ + "bAcBAaaCa", + "aABCccaBa", + 12 + ], + [ + "bAcBAbBcC", + "B", + 16 + ], + [ + "bAcBBBABa", + "caA", + 14 + ], + [ + "bAcBCCbac", + "accBBAB", + 12 + ], + [ + "bAcBabbC", + "ca", + 12 + ], + [ + "bAcBbAb", + "cbaBA", + 9 + ], + [ + "bAcBbaA", + "AaBaaA", + 6 + ], + [ + "bAcC", + "Acac", + 5 + ], + [ + "bAcC", + "bba", + 6 + ], + [ + "bAcCAa", + "C", + 10 + ], + [ + "bAcCaCc", + "abbCcb", + 11 + ], + [ + "bAcCb", + "Bbcacac", + 9 + ], + [ + "bAcCbACCB", + "aA", + 15 + ], + [ + "bAcCcACA", + "ABCAaBB", + 11 + ], + [ + "bAcaAbA", + "baCCbBca", + 10 + ], + [ + "bAcac", + "BBbAbCccA", + 11 + ], + [ + "bAcb", + "aa", + 7 + ], + [ + "bAcb", + "cAbbb", + 6 + ], + [ + "bAcbAbBAC", + "CbBc", + 12 + ], + [ + "bAcbBbcb", + "cCcBBCaa", + 11 + ], + [ + "bAcc", + "aCcABC", + 9 + ], + [ + "bAccA", + "ccBaaCA", + 9 + ], + [ + "bAccCabB", + "aABCAAaAA", + 13 + ], + [ + "bAccCbb", + "BaAAcB", + 10 + ], + [ + "bAcca", + "CBbcBBBba", + 13 + ], + [ + "bAccaACaa", + "CCcBCaA", + 10 + ], + [ + "bAccaCCA", + "aABAABBCb", + 13 + ], + [ + "bAcccAC", + "ABC", + 10 + ], + [ + "bAcccAbaA", + "CBcCAbcC", + 11 + ], + [ + "bAcccaB", + "BccACBBC", + 10 + ], + [ + "bB", + "ACaAbC", + 10 + ], + [ + "bB", + "BCbCb", + 7 + ], + [ + "bB", + "Baa", + 5 + ], + [ + "bB", + "BbbbBAb", + 10 + ], + [ + "bB", + "Bcba", + 6 + ], + [ + "bB", + "CAAcBb", + 10 + ], + [ + "bB", + "CbbC", + 5 + ], + [ + "bB", + "aACCab", + 11 + ], + [ + "bB", + "aBaccb", + 10 + ], + [ + "bB", + "aBbC", + 6 + ], + [ + "bB", + "aCACcaA", + 14 + ], + [ + "bB", + "aaAABaCCc", + 16 + ], + [ + "bB", + "ac", + 4 + ], + [ + "bB", + "acACccAac", + 18 + ], + [ + "bB", + "bAAacb", + 9 + ], + [ + "bB", + "bBCccbcC", + 12 + ], + [ + "bB", + "bBbbcb", + 8 + ], + [ + "bB", + "bCbBAAbCc", + 14 + ], + [ + "bB", + "ba", + 2 + ], + [ + "bB", + "baAbcC", + 9 + ], + [ + "bB", + "bcBbACaa", + 12 + ], + [ + "bB", + "c", + 4 + ], + [ + "bB", + "cBCcc", + 8 + ], + [ + "bB", + "cCBa", + 6 + ], + [ + "bB", + "caBc", + 6 + ], + [ + "bB", + "caCCACb", + 13 + ], + [ + "bBA", + "AB", + 4 + ], + [ + "bBA", + "B", + 4 + ], + [ + "bBA", + "abBCbccC", + 12 + ], + [ + "bBA", + "bCaaa", + 7 + ], + [ + "bBA", + "cBB", + 4 + ], + [ + "bBAA", + "cAb", + 6 + ], + [ + "bBAAba", + "C", + 12 + ], + [ + "bBAAcb", + "BCcBABB", + 10 + ], + [ + "bBAB", + "ccaa", + 7 + ], + [ + "bBABbBc", + "Bb", + 10 + ], + [ + "bBAC", + "aCBb", + 8 + ], + [ + "bBACA", + "bbcaBB", + 8 + ], + [ + "bBACBA", + "CA", + 8 + ], + [ + "bBACBABc", + "ca", + 14 + ], + [ + "bBACCbCB", + "B", + 14 + ], + [ + "bBACbaC", + "aabAbaCC", + 9 + ], + [ + "bBAa", + "CBBbBaa", + 7 + ], + [ + "bBAa", + "CabC", + 8 + ], + [ + "bBAa", + "cCcCCCA", + 13 + ], + [ + "bBAaBAcB", + "CbCaaAbAA", + 12 + ], + [ + "bBAaBcAaB", + "ACcb", + 13 + ], + [ + "bBAaC", + "acBaAcC", + 8 + ], + [ + "bBAaCB", + "bcBbCBBca", + 12 + ], + [ + "bBAaCc", + "Aca", + 9 + ], + [ + "bBAac", + "AcccCbBc", + 14 + ], + [ + "bBAacA", + "aBCbaAAA", + 10 + ], + [ + "bBAbCA", + "cBAaaCACb", + 10 + ], + [ + "bBAba", + "B", + 8 + ], + [ + "bBAbac", + "cBbAaccb", + 10 + ], + [ + "bBAc", + "Bc", + 4 + ], + [ + "bBAcAbca", + "baB", + 12 + ], + [ + "bBAcBA", + "aAa", + 9 + ], + [ + "bBAcC", + "abbBbCBc", + 10 + ], + [ + "bBAcC", + "c", + 8 + ], + [ + "bBAca", + "Ba", + 6 + ], + [ + "bBAca", + "b", + 8 + ], + [ + "bBAca", + "c", + 8 + ], + [ + "bBB", + "ABACC", + 8 + ], + [ + "bBB", + "AcaBacAc", + 14 + ], + [ + "bBB", + "BCCBCB", + 7 + ], + [ + "bBB", + "Cba", + 5 + ], + [ + "bBB", + "bACbcbAc", + 12 + ], + [ + "bBBABBb", + "Cbab", + 10 + ], + [ + "bBBAa", + "c", + 10 + ], + [ + "bBBBB", + "AacCA", + 10 + ], + [ + "bBBBBAa", + "AbbBAB", + 8 + ], + [ + "bBBBCb", + "B", + 10 + ], + [ + "bBBBabC", + "abBacCCBA", + 13 + ], + [ + "bBBBc", + "AB", + 8 + ], + [ + "bBBC", + "B", + 6 + ], + [ + "bBBC", + "abAbBBcab", + 11 + ], + [ + "bBBCABbB", + "caaCC", + 14 + ], + [ + "bBBCAbB", + "BcBCBCcBc", + 10 + ], + [ + "bBBa", + "abbCAA", + 8 + ], + [ + "bBBaB", + "cacCCbAA", + 14 + ], + [ + "bBBaC", + "Bcc", + 7 + ], + [ + "bBBaaBC", + "bcbcbBba", + 11 + ], + [ + "bBBaaBb", + "a", + 12 + ], + [ + "bBBaabc", + "caaBba", + 10 + ], + [ + "bBBabac", + "aaAA", + 11 + ], + [ + "bBBb", + "Accaab", + 10 + ], + [ + "bBBb", + "aAacAbca", + 14 + ], + [ + "bBBbAcBB", + "BBAbBAb", + 9 + ], + [ + "bBBbBbbbB", + "cc", + 18 + ], + [ + "bBBbCa", + "BcC", + 8 + ], + [ + "bBBbaB", + "caAaaC", + 10 + ], + [ + "bBBbaBbCa", + "a", + 16 + ], + [ + "bBBbacabB", + "Ba", + 14 + ], + [ + "bBBbcc", + "bcA", + 8 + ], + [ + "bBBc", + "ABBbACc", + 8 + ], + [ + "bBBc", + "BAAb", + 7 + ], + [ + "bBBcA", + "caB", + 8 + ], + [ + "bBBcaca", + "CBBBbABcb", + 10 + ], + [ + "bBC", + "AB", + 4 + ], + [ + "bBC", + "aaCaCAcBc", + 15 + ], + [ + "bBC", + "bcaBACB", + 8 + ], + [ + "bBC", + "cAB", + 6 + ], + [ + "bBC", + "cBaCcbca", + 12 + ], + [ + "bBCA", + "AAc", + 7 + ], + [ + "bBCA", + "bCCACBc", + 8 + ], + [ + "bBCA", + "babCCBAC", + 9 + ], + [ + "bBCA", + "cBaabcA", + 9 + ], + [ + "bBCAB", + "ACcBBBBab", + 13 + ], + [ + "bBCABBA", + "BCabbCAB", + 9 + ], + [ + "bBCAbAb", + "caAbA", + 8 + ], + [ + "bBCAbBba", + "CbbBbbC", + 10 + ], + [ + "bBCAcBcCA", + "B", + 16 + ], + [ + "bBCBa", + "cbAAB", + 8 + ], + [ + "bBCBaBCaa", + "bA", + 15 + ], + [ + "bBCBaC", + "aABCAca", + 10 + ], + [ + "bBCBabBC", + "cCAc", + 12 + ], + [ + "bBCC", + "baAbacaA", + 12 + ], + [ + "bBCCA", + "aa", + 9 + ], + [ + "bBCCCCABc", + "babacAA", + 13 + ], + [ + "bBCCCb", + "ACc", + 9 + ], + [ + "bBCCbAb", + "a", + 13 + ], + [ + "bBCaABB", + "CbACac", + 10 + ], + [ + "bBCaCCC", + "ABccBC", + 8 + ], + [ + "bBCaabcAA", + "CBABCC", + 13 + ], + [ + "bBCac", + "BA", + 7 + ], + [ + "bBCacbCA", + "BaCBcCBC", + 10 + ], + [ + "bBCb", + "bBcCA", + 4 + ], + [ + "bBCbAAaAC", + "CCbCCA", + 12 + ], + [ + "bBCbBa", + "AbBaACCB", + 10 + ], + [ + "bBCbb", + "aCcaC", + 9 + ], + [ + "bBCc", + "ccB", + 7 + ], + [ + "bBCcaAAa", + "caabAbC", + 13 + ], + [ + "bBa", + "Bba", + 2 + ], + [ + "bBa", + "b", + 4 + ], + [ + "bBa", + "bBcAca", + 6 + ], + [ + "bBa", + "bCA", + 3 + ], + [ + "bBa", + "cACaAbBa", + 10 + ], + [ + "bBa", + "ccBcbcAcc", + 15 + ], + [ + "bBaABCcC", + "ABA", + 12 + ], + [ + "bBaACBaB", + "BCBBABbb", + 10 + ], + [ + "bBaAaA", + "Bb", + 10 + ], + [ + "bBaAaBc", + "BAbcaACBc", + 8 + ], + [ + "bBaAaC", + "cCBACaaA", + 10 + ], + [ + "bBaAb", + "bACbbacAB", + 10 + ], + [ + "bBaBCAAaa", + "CACBAc", + 13 + ], + [ + "bBaBCCcA", + "bb", + 13 + ], + [ + "bBaBCaAaB", + "bCAbCbbB", + 10 + ], + [ + "bBaBCcBcB", + "cabcc", + 11 + ], + [ + "bBaBa", + "BC", + 8 + ], + [ + "bBaBbAbAB", + "aBAAbCCBA", + 12 + ], + [ + "bBaBbb", + "BaCAbbb", + 7 + ], + [ + "bBaC", + "BAaAcb", + 8 + ], + [ + "bBaCbcC", + "bA", + 11 + ], + [ + "bBaCcAA", + "cAbb", + 12 + ], + [ + "bBaaBb", + "Caa", + 8 + ], + [ + "bBaaBcBbb", + "cABAaA", + 15 + ], + [ + "bBaaCABa", + "bcbaBcbC", + 11 + ], + [ + "bBaaCa", + "bAAC", + 6 + ], + [ + "bBaabbbbc", + "cbA", + 16 + ], + [ + "bBaacaC", + "bccaBC", + 8 + ], + [ + "bBabAcBA", + "ccaAbA", + 9 + ], + [ + "bBabB", + "BabcBBB", + 8 + ], + [ + "bBabC", + "ABc", + 7 + ], + [ + "bBabaAC", + "ccaCb", + 12 + ], + [ + "bBac", + "AbaAcAaB", + 11 + ], + [ + "bBac", + "BBCc", + 3 + ], + [ + "bBacbBcb", + "bCbc", + 9 + ], + [ + "bBb", + "AA", + 6 + ], + [ + "bBb", + "AbCaacc", + 12 + ], + [ + "bBb", + "CaaCCCaA", + 16 + ], + [ + "bBb", + "Ccac", + 8 + ], + [ + "bBb", + "a", + 6 + ], + [ + "bBb", + "aCC", + 6 + ], + [ + "bBb", + "baAC", + 6 + ], + [ + "bBb", + "cBA", + 4 + ], + [ + "bBb", + "caBaCBa", + 11 + ], + [ + "bBbA", + "C", + 8 + ], + [ + "bBbA", + "CBcBAA", + 7 + ], + [ + "bBbA", + "caBBABaBa", + 13 + ], + [ + "bBbAAa", + "aabbb", + 10 + ], + [ + "bBbB", + "AcbAA", + 8 + ], + [ + "bBbB", + "a", + 8 + ], + [ + "bBbBBaBAc", + "c", + 16 + ], + [ + "bBbBBba", + "Bbc", + 10 + ], + [ + "bBbBb", + "B", + 8 + ], + [ + "bBbCA", + "abBBcBBbc", + 12 + ], + [ + "bBbCCca", + "aCaCBCB", + 11 + ], + [ + "bBba", + "aAA", + 7 + ], + [ + "bBba", + "abB", + 6 + ], + [ + "bBbaA", + "cBCBCcCCB", + 15 + ], + [ + "bBbaC", + "bc", + 7 + ], + [ + "bBbaCba", + "aBBccAaB", + 10 + ], + [ + "bBbabBAC", + "bCBa", + 11 + ], + [ + "bBbacbBCA", + "BA", + 14 + ], + [ + "bBbbCA", + "bACbBAa", + 8 + ], + [ + "bBbbbc", + "c", + 10 + ], + [ + "bBbcAAba", + "cC", + 14 + ], + [ + "bBbcBBCa", + "b", + 14 + ], + [ + "bBbcCC", + "aacCBb", + 10 + ], + [ + "bBbcCC", + "acabCCb", + 9 + ], + [ + "bBbcbA", + "BcBCabB", + 9 + ], + [ + "bBbcbA", + "cBaCcCbB", + 10 + ], + [ + "bBbcbBa", + "BbA", + 9 + ], + [ + "bBc", + "A", + 6 + ], + [ + "bBc", + "ACA", + 6 + ], + [ + "bBc", + "CC", + 5 + ], + [ + "bBc", + "bCbA", + 5 + ], + [ + "bBc", + "bbcA", + 3 + ], + [ + "bBcA", + "BaCba", + 7 + ], + [ + "bBcAACbB", + "bCba", + 10 + ], + [ + "bBcABCb", + "BAca", + 9 + ], + [ + "bBcAa", + "Ca", + 7 + ], + [ + "bBcAcBabB", + "ccaCAb", + 11 + ], + [ + "bBcAccBAc", + "cAc", + 12 + ], + [ + "bBcB", + "Ca", + 7 + ], + [ + "bBcBAbB", + "A", + 12 + ], + [ + "bBcBAca", + "BcbC", + 8 + ], + [ + "bBcBc", + "C", + 9 + ], + [ + "bBcCCbcb", + "CAAcacCaC", + 14 + ], + [ + "bBcCaAb", + "CbbbCa", + 9 + ], + [ + "bBcCbAAa", + "c", + 14 + ], + [ + "bBcaaaCAC", + "ACBbc", + 16 + ], + [ + "bBcb", + "bCCac", + 7 + ], + [ + "bBcba", + "bba", + 4 + ], + [ + "bBcbaB", + "BaCCbc", + 10 + ], + [ + "bBcbaBAbB", + "AAb", + 13 + ], + [ + "bBcbaBbAB", + "c", + 16 + ], + [ + "bBcc", + "CcBABA", + 10 + ], + [ + "bBcc", + "aAc", + 6 + ], + [ + "bBccACBbA", + "CaCCCB", + 12 + ], + [ + "bBccBc", + "Caac", + 9 + ], + [ + "bBccC", + "bAcc", + 4 + ], + [ + "bBccb", + "bccAbBbC", + 10 + ], + [ + "bBccb", + "caBAA", + 10 + ], + [ + "bBccbBbCb", + "AAAbbaBA", + 15 + ], + [ + "bBccbBccC", + "c", + 16 + ], + [ + "bBccbb", + "AaABa", + 11 + ], + [ + "bBcccB", + "bcCAca", + 7 + ], + [ + "bC", + "A", + 4 + ], + [ + "bC", + "ACBBaBa", + 12 + ], + [ + "bC", + "Abc", + 3 + ], + [ + "bC", + "BAAcb", + 8 + ], + [ + "bC", + "BAa", + 5 + ], + [ + "bC", + "BAcaBCAcc", + 15 + ], + [ + "bC", + "BCbcCA", + 8 + ], + [ + "bC", + "BacAcb", + 10 + ], + [ + "bC", + "CC", + 2 + ], + [ + "bC", + "CbBcAbcc", + 13 + ], + [ + "bC", + "aBaCA", + 7 + ], + [ + "bC", + "aCCbCABBB", + 14 + ], + [ + "bC", + "aaBBCAA", + 11 + ], + [ + "bC", + "aaCbaAcB", + 13 + ], + [ + "bC", + "acCcbcB", + 11 + ], + [ + "bC", + "b", + 2 + ], + [ + "bC", + "bBBBC", + 6 + ], + [ + "bC", + "bBb", + 4 + ], + [ + "bC", + "bC", + 0 + ], + [ + "bC", + "baAAcb", + 9 + ], + [ + "bC", + "bac", + 3 + ], + [ + "bC", + "bbBb", + 6 + ], + [ + "bC", + "c", + 3 + ], + [ + "bC", + "cABAac", + 10 + ], + [ + "bC", + "cABcACACa", + 15 + ], + [ + "bC", + "cAaCb", + 8 + ], + [ + "bC", + "cBbB", + 6 + ], + [ + "bC", + "cBcCbcc", + 11 + ], + [ + "bC", + "cBcCcAa", + 11 + ], + [ + "bC", + "cccCBcaba", + 16 + ], + [ + "bCA", + "BbCaCc", + 7 + ], + [ + "bCA", + "ac", + 5 + ], + [ + "bCA", + "bCB", + 2 + ], + [ + "bCA", + "ba", + 3 + ], + [ + "bCA", + "cbcbAc", + 7 + ], + [ + "bCAAA", + "Cc", + 8 + ], + [ + "bCAAA", + "cBabbA", + 9 + ], + [ + "bCAAABc", + "Aabab", + 11 + ], + [ + "bCAABaCbc", + "Cab", + 12 + ], + [ + "bCAABccA", + "AaacCBBcB", + 13 + ], + [ + "bCAAC", + "bBAaCcc", + 7 + ], + [ + "bCAACaCa", + "CcAACaC", + 5 + ], + [ + "bCAAaBbCA", + "CbaAcB", + 13 + ], + [ + "bCAB", + "bAAA", + 4 + ], + [ + "bCABB", + "bA", + 6 + ], + [ + "bCABBCAA", + "bBCbcCc", + 11 + ], + [ + "bCABBcB", + "bBbACbaBb", + 11 + ], + [ + "bCABa", + "AaAa", + 6 + ], + [ + "bCABaBAcc", + "CBbaCCba", + 13 + ], + [ + "bCAC", + "ABCcc", + 6 + ], + [ + "bCAC", + "caCa", + 6 + ], + [ + "bCAC", + "cbCcb", + 6 + ], + [ + "bCACBA", + "cbac", + 10 + ], + [ + "bCACBB", + "Bcb", + 9 + ], + [ + "bCACa", + "c", + 9 + ], + [ + "bCACaaC", + "Cacaaa", + 6 + ], + [ + "bCACaaa", + "cC", + 11 + ], + [ + "bCACb", + "B", + 9 + ], + [ + "bCACbA", + "Cb", + 8 + ], + [ + "bCAaCB", + "aCAbcCbB", + 8 + ], + [ + "bCAaCbac", + "a", + 14 + ], + [ + "bCAaccaCC", + "b", + 16 + ], + [ + "bCAbAAcA", + "bBBCAabAc", + 10 + ], + [ + "bCAbab", + "ca", + 9 + ], + [ + "bCAbb", + "CBAcBAcBB", + 12 + ], + [ + "bCAc", + "BaCAaaBa", + 11 + ], + [ + "bCAcBA", + "CBBcCCA", + 10 + ], + [ + "bCAcC", + "BbaccaCA", + 10 + ], + [ + "bCAcC", + "BcA", + 6 + ], + [ + "bCAcc", + "cbCAB", + 6 + ], + [ + "bCB", + "CCabaC", + 9 + ], + [ + "bCB", + "abaaAbbAC", + 15 + ], + [ + "bCB", + "bCAca", + 6 + ], + [ + "bCB", + "cBAB", + 5 + ], + [ + "bCBA", + "BccBbcAA", + 10 + ], + [ + "bCBAAa", + "c", + 11 + ], + [ + "bCBBBCbc", + "CAbbbB", + 10 + ], + [ + "bCBBBcB", + "BBaBCBbC", + 10 + ], + [ + "bCBBa", + "CccaaC", + 9 + ], + [ + "bCBBbCc", + "bBaAB", + 10 + ], + [ + "bCBC", + "AbaCcaCB", + 10 + ], + [ + "bCBC", + "CcbaCaA", + 10 + ], + [ + "bCBC", + "aCAbcbCC", + 10 + ], + [ + "bCBCAB", + "C", + 10 + ], + [ + "bCBCACBb", + "bBacBCaA", + 11 + ], + [ + "bCBCBcbc", + "bA", + 14 + ], + [ + "bCBCaAc", + "a", + 12 + ], + [ + "bCBCbC", + "A", + 12 + ], + [ + "bCBa", + "Ba", + 4 + ], + [ + "bCBaAb", + "CABCCcb", + 10 + ], + [ + "bCBaabC", + "ACb", + 10 + ], + [ + "bCBabCa", + "cAbaccCC", + 11 + ], + [ + "bCBacABab", + "ccAacAaCc", + 11 + ], + [ + "bCBacaAB", + "ACbABB", + 10 + ], + [ + "bCBb", + "aaAbAbbC", + 11 + ], + [ + "bCBb", + "cBabcA", + 9 + ], + [ + "bCBb", + "cBcccaA", + 12 + ], + [ + "bCBbAaAb", + "BabA", + 11 + ], + [ + "bCBbAac", + "CBcca", + 8 + ], + [ + "bCBbAcC", + "CBbBbb", + 8 + ], + [ + "bCBbC", + "CA", + 8 + ], + [ + "bCBbaCCA", + "BAAab", + 13 + ], + [ + "bCBbabb", + "BcCBA", + 10 + ], + [ + "bCBbbCb", + "ABcbAba", + 10 + ], + [ + "bCBc", + "bbBACc", + 6 + ], + [ + "bCBcB", + "bCAbccACb", + 10 + ], + [ + "bCBcBBc", + "b", + 12 + ], + [ + "bCBca", + "cAABaCCa", + 11 + ], + [ + "bCBcabA", + "CAcbbcBcB", + 14 + ], + [ + "bCBcba", + "ABAcbc", + 8 + ], + [ + "bCBcccbC", + "Cbbbc", + 10 + ], + [ + "bCC", + "A", + 6 + ], + [ + "bCC", + "ABBbCCC", + 8 + ], + [ + "bCC", + "AaC", + 4 + ], + [ + "bCC", + "BB", + 5 + ], + [ + "bCC", + "a", + 6 + ], + [ + "bCC", + "aCbBaaCB", + 12 + ], + [ + "bCC", + "b", + 4 + ], + [ + "bCC", + "bbBaCaCAc", + 12 + ], + [ + "bCC", + "cB", + 5 + ], + [ + "bCCAACA", + "bc", + 11 + ], + [ + "bCCAAbAa", + "aC", + 14 + ], + [ + "bCCACBb", + "BcAA", + 10 + ], + [ + "bCCAc", + "bcBCABabC", + 10 + ], + [ + "bCCBACAa", + "cCB", + 11 + ], + [ + "bCCBC", + "CbbbACC", + 10 + ], + [ + "bCCBaBa", + "BC", + 11 + ], + [ + "bCCBbBAA", + "ACBbbBc", + 9 + ], + [ + "bCCCABcb", + "BaBbbAb", + 12 + ], + [ + "bCCCa", + "aA", + 9 + ], + [ + "bCCCbccc", + "aCAA", + 14 + ], + [ + "bCCa", + "AabcB", + 9 + ], + [ + "bCCa", + "BccBa", + 5 + ], + [ + "bCCa", + "bCACAaBb", + 8 + ], + [ + "bCCaA", + "cbAbaAA", + 8 + ], + [ + "bCCaAbBB", + "CcC", + 13 + ], + [ + "bCCaBa", + "AcCabCAc", + 9 + ], + [ + "bCCaCc", + "abAcCA", + 9 + ], + [ + "bCCaa", + "cA", + 8 + ], + [ + "bCCaaCaa", + "cbcc", + 14 + ], + [ + "bCCab", + "B", + 9 + ], + [ + "bCCaccbbB", + "cC", + 15 + ], + [ + "bCCb", + "cBBaBB", + 10 + ], + [ + "bCCbA", + "bcBC", + 6 + ], + [ + "bCCbACBBB", + "CA", + 14 + ], + [ + "bCCbB", + "aaBcCbAA", + 10 + ], + [ + "bCCbCc", + "aACbC", + 6 + ], + [ + "bCCbcA", + "caACc", + 10 + ], + [ + "bCCcAcAB", + "aCAB", + 10 + ], + [ + "bCCcc", + "aACCcacB", + 8 + ], + [ + "bCCccC", + "aabAb", + 12 + ], + [ + "bCa", + "Cb", + 4 + ], + [ + "bCa", + "aBbb", + 7 + ], + [ + "bCa", + "bBACAbba", + 10 + ], + [ + "bCa", + "baba", + 4 + ], + [ + "bCa", + "cbacb", + 7 + ], + [ + "bCa", + "ccaCaCCc", + 12 + ], + [ + "bCaA", + "AABbbA", + 9 + ], + [ + "bCaA", + "CBc", + 6 + ], + [ + "bCaAC", + "ACaaABaCa", + 10 + ], + [ + "bCaBAc", + "bbbBcbcAc", + 10 + ], + [ + "bCaBBBA", + "caCcBBCcB", + 12 + ], + [ + "bCaBBC", + "cacbBC", + 6 + ], + [ + "bCaBbCAC", + "aaCAcCAbC", + 11 + ], + [ + "bCaCAabc", + "bBaB", + 11 + ], + [ + "bCaCAcaC", + "bcccaa", + 8 + ], + [ + "bCaCCC", + "cAbabbACA", + 14 + ], + [ + "bCaCcB", + "ABbbcCABC", + 12 + ], + [ + "bCaCcac", + "CBbCAA", + 10 + ], + [ + "bCaa", + "a", + 6 + ], + [ + "bCaa", + "aBC", + 7 + ], + [ + "bCaabBC", + "CCABcA", + 10 + ], + [ + "bCabAacbC", + "BAB", + 14 + ], + [ + "bCabCc", + "CA", + 9 + ], + [ + "bCabaAAB", + "A", + 14 + ], + [ + "bCabbAacb", + "Cba", + 12 + ], + [ + "bCabbC", + "CAacA", + 9 + ], + [ + "bCac", + "C", + 6 + ], + [ + "bCac", + "cC", + 6 + ], + [ + "bCacAbBa", + "a", + 14 + ], + [ + "bCacBA", + "abB", + 8 + ], + [ + "bCacCcB", + "aAabBBbC", + 13 + ], + [ + "bCaccBAa", + "CB", + 12 + ], + [ + "bCb", + "aCBc", + 5 + ], + [ + "bCb", + "ab", + 4 + ], + [ + "bCb", + "ccA", + 5 + ], + [ + "bCbA", + "ab", + 6 + ], + [ + "bCbAAAB", + "AccbcBaB", + 10 + ], + [ + "bCbAB", + "BcBB", + 5 + ], + [ + "bCbACcbc", + "b", + 14 + ], + [ + "bCbAa", + "acCBABB", + 9 + ], + [ + "bCbAbcBc", + "b", + 14 + ], + [ + "bCbB", + "b", + 6 + ], + [ + "bCbBabC", + "cbacBCaBc", + 10 + ], + [ + "bCbBacaA", + "aBAaBCaa", + 11 + ], + [ + "bCbBbbbC", + "a", + 16 + ], + [ + "bCbBbcAb", + "ba", + 13 + ], + [ + "bCbCAbB", + "CABBbb", + 9 + ], + [ + "bCbCCCAcb", + "a", + 17 + ], + [ + "bCbCbb", + "AAACCCB", + 11 + ], + [ + "bCbCccc", + "abbAbC", + 11 + ], + [ + "bCbaACBbb", + "bCCccaa", + 13 + ], + [ + "bCbaAc", + "BA", + 9 + ], + [ + "bCbaaBc", + "CbAAcaBc", + 7 + ], + [ + "bCbac", + "aAcBABbaA", + 13 + ], + [ + "bCbac", + "b", + 8 + ], + [ + "bCbacABa", + "cCaac", + 10 + ], + [ + "bCbb", + "bbCBabAC", + 9 + ], + [ + "bCbbb", + "AcBc", + 8 + ], + [ + "bCbbba", + "AaBbc", + 9 + ], + [ + "bCbc", + "ABb", + 6 + ], + [ + "bCbc", + "CCb", + 4 + ], + [ + "bCbcB", + "AbAab", + 9 + ], + [ + "bCbcCBA", + "BCbBacC", + 9 + ], + [ + "bCbcCC", + "BbbABA", + 9 + ], + [ + "bCbcCbAC", + "ccccC", + 10 + ], + [ + "bCbca", + "bcbBCBa", + 6 + ], + [ + "bCbcaAAAA", + "A", + 16 + ], + [ + "bCbcaca", + "bBBAcc", + 8 + ], + [ + "bCbcbbBbC", + "CaC", + 14 + ], + [ + "bCc", + "BabBBCb", + 10 + ], + [ + "bCc", + "CCBcAA", + 8 + ], + [ + "bCc", + "CCCcCBcC", + 12 + ], + [ + "bCc", + "aaAABCCaB", + 14 + ], + [ + "bCc", + "aabbbaaB", + 14 + ], + [ + "bCc", + "bccB", + 3 + ], + [ + "bCcABC", + "aAcBcCC", + 10 + ], + [ + "bCcACCBa", + "cac", + 12 + ], + [ + "bCcACCaC", + "AbcaC", + 9 + ], + [ + "bCcB", + "BBB", + 5 + ], + [ + "bCcBA", + "CcAbcCbaA", + 11 + ], + [ + "bCcBB", + "cBb", + 5 + ], + [ + "bCcC", + "ACBaB", + 8 + ], + [ + "bCcCb", + "ccbBac", + 10 + ], + [ + "bCcCcCCCB", + "BCAaAa", + 15 + ], + [ + "bCca", + "CCaaaBbc", + 12 + ], + [ + "bCcaA", + "cACB", + 9 + ], + [ + "bCcaBbac", + "bBa", + 10 + ], + [ + "bCcaCc", + "A", + 11 + ], + [ + "bCcaCcC", + "bcCB", + 8 + ], + [ + "bCcaaa", + "B", + 11 + ], + [ + "bCcaabCB", + "bbAbcaAA", + 13 + ], + [ + "bCcacBCA", + "aACBccC", + 11 + ], + [ + "bCcbAaBBc", + "c", + 16 + ], + [ + "bCcbCCbb", + "bBbacaC", + 11 + ], + [ + "bCcbbaCab", + "BC", + 15 + ], + [ + "bCcbbc", + "bBBCB", + 9 + ], + [ + "bCcbc", + "acBCAcc", + 9 + ], + [ + "bCcc", + "CAbacbA", + 10 + ], + [ + "bCccbbbc", + "bBABAaBAc", + 13 + ], + [ + "ba", + "ABbbC", + 8 + ], + [ + "ba", + "BA", + 2 + ], + [ + "ba", + "BAABBb", + 10 + ], + [ + "ba", + "BacAAbA", + 11 + ], + [ + "ba", + "BbAaaCACc", + 14 + ], + [ + "ba", + "BcCAAC", + 10 + ], + [ + "ba", + "Bcc", + 5 + ], + [ + "ba", + "CCccCCa", + 12 + ], + [ + "ba", + "CaAbAb", + 9 + ], + [ + "ba", + "Cba", + 2 + ], + [ + "ba", + "Cca", + 4 + ], + [ + "ba", + "Ccac", + 6 + ], + [ + "ba", + "a", + 2 + ], + [ + "ba", + "aBABA", + 8 + ], + [ + "ba", + "aBaABB", + 9 + ], + [ + "ba", + "aCcaCbAbB", + 15 + ], + [ + "ba", + "bBC", + 4 + ], + [ + "ba", + "bBCC", + 6 + ], + [ + "ba", + "baABc", + 6 + ], + [ + "ba", + "baCAbB", + 8 + ], + [ + "ba", + "cA", + 3 + ], + [ + "ba", + "cABA", + 6 + ], + [ + "ba", + "cBbCCaB", + 10 + ], + [ + "ba", + "cBc", + 5 + ], + [ + "ba", + "cCCBcC", + 11 + ], + [ + "ba", + "caBBCAca", + 13 + ], + [ + "ba", + "cbBbb", + 8 + ], + [ + "ba", + "ccb", + 6 + ], + [ + "baA", + "AbAAaCaa", + 11 + ], + [ + "baA", + "B", + 5 + ], + [ + "baA", + "BBB", + 5 + ], + [ + "baA", + "a", + 4 + ], + [ + "baA", + "aCaaB", + 7 + ], + [ + "baA", + "cAaBCBaCB", + 15 + ], + [ + "baA", + "cBBcccca", + 14 + ], + [ + "baA", + "ccABbC", + 10 + ], + [ + "baAA", + "BBAaCacBC", + 14 + ], + [ + "baAA", + "cbbbCcbac", + 15 + ], + [ + "baAAA", + "CCcC", + 10 + ], + [ + "baAAABbC", + "BcACCbBa", + 11 + ], + [ + "baAAAbAC", + "caBb", + 12 + ], + [ + "baAAa", + "cACaAB", + 8 + ], + [ + "baAAc", + "bC", + 7 + ], + [ + "baAAcCa", + "CCABACcBC", + 12 + ], + [ + "baABBcc", + "cbcaaABc", + 9 + ], + [ + "baABCaCA", + "CABCbCAC", + 8 + ], + [ + "baABaBaAA", + "CAabb", + 13 + ], + [ + "baABbAcC", + "BcCBCCC", + 10 + ], + [ + "baAC", + "bCbAbBCA", + 10 + ], + [ + "baACCCcaA", + "Cb", + 16 + ], + [ + "baACca", + "bBcB", + 8 + ], + [ + "baAa", + "AbAab", + 6 + ], + [ + "baAaCAcc", + "acAbB", + 11 + ], + [ + "baAbCAc", + "baAacAac", + 5 + ], + [ + "baAbaAaba", + "cBA", + 15 + ], + [ + "baAbabAb", + "CC", + 16 + ], + [ + "baAcABAC", + "aabCBbBcB", + 13 + ], + [ + "baAcB", + "bbcb", + 5 + ], + [ + "baAcCcaaa", + "ccAA", + 12 + ], + [ + "baAca", + "aaAcCc", + 6 + ], + [ + "baAcaA", + "C", + 11 + ], + [ + "baAcab", + "B", + 11 + ], + [ + "baAcc", + "AbacACCCb", + 10 + ], + [ + "baB", + "BbccbbBbC", + 14 + ], + [ + "baB", + "acbAB", + 5 + ], + [ + "baB", + "accCa", + 10 + ], + [ + "baB", + "cA", + 5 + ], + [ + "baB", + "ca", + 4 + ], + [ + "baB", + "ccBACC", + 10 + ], + [ + "baBA", + "aa", + 5 + ], + [ + "baBAC", + "aA", + 6 + ], + [ + "baBAbccc", + "BCBaC", + 11 + ], + [ + "baBB", + "bCCaAca", + 10 + ], + [ + "baBB", + "cacbb", + 6 + ], + [ + "baBBC", + "BbBAA", + 7 + ], + [ + "baBBCAAc", + "bBaabAccc", + 11 + ], + [ + "baBBCcBCa", + "ABbBca", + 9 + ], + [ + "baBBaa", + "BCABAa", + 6 + ], + [ + "baBBbAb", + "bACa", + 10 + ], + [ + "baBBbCbBC", + "cBABabC", + 11 + ], + [ + "baBBcaBcC", + "CCCb", + 16 + ], + [ + "baBBcb", + "BBCbCCb", + 9 + ], + [ + "baBCcbbA", + "BBacCC", + 11 + ], + [ + "baBa", + "aA", + 5 + ], + [ + "baBa", + "bBbcAAb", + 10 + ], + [ + "baBa", + "cCbbbcBC", + 12 + ], + [ + "baBaABBb", + "bBcB", + 10 + ], + [ + "baBaaC", + "A", + 11 + ], + [ + "baBaaC", + "bBbAcc", + 7 + ], + [ + "baBbC", + "ACAc", + 8 + ], + [ + "baBbcCbaC", + "C", + 16 + ], + [ + "baBc", + "BAAbCa", + 8 + ], + [ + "baBcAAC", + "aBAAbACa", + 8 + ], + [ + "baBcC", + "Abc", + 6 + ], + [ + "baBcCCBBA", + "bbacBaaaC", + 14 + ], + [ + "baBcCCBb", + "Bccacb", + 9 + ], + [ + "baBcaAc", + "bc", + 10 + ], + [ + "baBcbccA", + "bCCa", + 11 + ], + [ + "baBccBC", + "C", + 12 + ], + [ + "baBccccb", + "AAbbaBCb", + 11 + ], + [ + "baC", + "AABBAb", + 10 + ], + [ + "baC", + "AB", + 5 + ], + [ + "baC", + "BBa", + 5 + ], + [ + "baC", + "CAaCCcbb", + 12 + ], + [ + "baC", + "CC", + 4 + ], + [ + "baC", + "caBcB", + 7 + ], + [ + "baCA", + "Bc", + 6 + ], + [ + "baCACBCac", + "a", + 16 + ], + [ + "baCAaAaBa", + "bcBcCab", + 12 + ], + [ + "baCAbc", + "CCc", + 8 + ], + [ + "baCB", + "B", + 6 + ], + [ + "baCBACbBa", + "b", + 16 + ], + [ + "baCBC", + "CAAcaCaB", + 12 + ], + [ + "baCBbCB", + "abc", + 9 + ], + [ + "baCCCCBB", + "b", + 14 + ], + [ + "baCCCcC", + "bbcAcABac", + 13 + ], + [ + "baCCbAB", + "CbBAC", + 9 + ], + [ + "baCCc", + "BcACCa", + 6 + ], + [ + "baCCc", + "ab", + 8 + ], + [ + "baCa", + "BAabaACcC", + 12 + ], + [ + "baCa", + "CCAABc", + 11 + ], + [ + "baCaAAB", + "aCB", + 8 + ], + [ + "baCaCa", + "caC", + 7 + ], + [ + "baCaCcbAb", + "bCB", + 13 + ], + [ + "baCb", + "CAbA", + 7 + ], + [ + "baCbCAB", + "aCBccCcC", + 11 + ], + [ + "baCbCC", + "ACCbA", + 8 + ], + [ + "baCbbCb", + "bACAc", + 8 + ], + [ + "baCbc", + "aaAbAb", + 8 + ], + [ + "baCbcACBc", + "CACcBb", + 11 + ], + [ + "baCc", + "BbcaaB", + 8 + ], + [ + "baCcA", + "bbbBcB", + 8 + ], + [ + "baCcaaC", + "cCCA", + 10 + ], + [ + "baCcb", + "BaCacAa", + 7 + ], + [ + "baa", + "ACbBca", + 8 + ], + [ + "baa", + "BaC", + 3 + ], + [ + "baaABBcBb", + "ABcacAC", + 14 + ], + [ + "baaAC", + "aCAb", + 6 + ], + [ + "baaAaBB", + "ccABBac", + 12 + ], + [ + "baaAb", + "bAcBabC", + 8 + ], + [ + "baaAcA", + "B", + 11 + ], + [ + "baaBAA", + "CbabaCCAA", + 8 + ], + [ + "baaBAAabB", + "cCB", + 16 + ], + [ + "baaBBBb", + "aC", + 12 + ], + [ + "baaBabb", + "cAaB", + 9 + ], + [ + "baaBb", + "CBbaBAbC", + 9 + ], + [ + "baaBcaa", + "CcCCbb", + 13 + ], + [ + "baaCCcAB", + "cCA", + 11 + ], + [ + "baaCaac", + "aCAaCCbA", + 11 + ], + [ + "baaCbA", + "ccBAAcC", + 12 + ], + [ + "baaa", + "CaCbCb", + 10 + ], + [ + "baaa", + "bBbBAAC", + 10 + ], + [ + "baaaAc", + "CBBbBBB", + 13 + ], + [ + "baaaCA", + "aab", + 8 + ], + [ + "baaaaabB", + "BCbbccA", + 15 + ], + [ + "baaacac", + "ABa", + 11 + ], + [ + "baab", + "aABbABC", + 11 + ], + [ + "baabA", + "cACC", + 9 + ], + [ + "baabaBaab", + "aCcA", + 15 + ], + [ + "baac", + "ACBbca", + 10 + ], + [ + "baacACa", + "bccCB", + 8 + ], + [ + "baacBbA", + "AaaCCbaaA", + 9 + ], + [ + "baacCc", + "CACBAA", + 11 + ], + [ + "baacaCcb", + "BBbbbAaBA", + 16 + ], + [ + "baacabBAA", + "aaaaBaB", + 9 + ], + [ + "bab", + "Cc", + 6 + ], + [ + "bab", + "CcBC", + 7 + ], + [ + "bab", + "a", + 4 + ], + [ + "bab", + "aabbAaBca", + 13 + ], + [ + "babA", + "AB", + 6 + ], + [ + "babA", + "bCb", + 4 + ], + [ + "babA", + "cccABCc", + 12 + ], + [ + "babAAaBC", + "Cb", + 14 + ], + [ + "babAaAA", + "bc", + 12 + ], + [ + "babAaBA", + "BbcBAbC", + 10 + ], + [ + "babBBB", + "b", + 10 + ], + [ + "babBa", + "BB", + 7 + ], + [ + "babBbAbAC", + "CbabB", + 12 + ], + [ + "babC", + "BCB", + 6 + ], + [ + "babC", + "CABB", + 6 + ], + [ + "babCABaa", + "BbcCAab", + 9 + ], + [ + "babCB", + "bcCCaABBC", + 12 + ], + [ + "babCBb", + "AAcbb", + 7 + ], + [ + "babCCCc", + "cCAbbca", + 12 + ], + [ + "babCCa", + "c", + 11 + ], + [ + "babCabCab", + "caac", + 13 + ], + [ + "babCbcc", + "AaAAcA", + 10 + ], + [ + "baba", + "abBcbAc", + 9 + ], + [ + "babaBA", + "CbcBbBabB", + 11 + ], + [ + "babaC", + "CbAaAA", + 8 + ], + [ + "babaCBaa", + "BbCa", + 9 + ], + [ + "babaa", + "C", + 10 + ], + [ + "babaaCBCC", + "BbaaAc", + 10 + ], + [ + "babb", + "AcB", + 6 + ], + [ + "babbA", + "BabaAAaB", + 9 + ], + [ + "babbCaaCB", + "cCaAAaBc", + 15 + ], + [ + "babbCbC", + "AA", + 13 + ], + [ + "babbCc", + "acbACaB", + 10 + ], + [ + "babbbbA", + "acA", + 10 + ], + [ + "babbcccAA", + "bAA", + 12 + ], + [ + "babcacA", + "c", + 12 + ], + [ + "bac", + "CBC", + 5 + ], + [ + "bac", + "CcBc", + 6 + ], + [ + "bac", + "Ccbca", + 8 + ], + [ + "bac", + "aacAc", + 6 + ], + [ + "bac", + "ba", + 2 + ], + [ + "bacABcBc", + "ABccc", + 8 + ], + [ + "bacAbca", + "aBCACc", + 9 + ], + [ + "bacBCbBb", + "ccAbcABA", + 12 + ], + [ + "bacBCc", + "cBcBB", + 8 + ], + [ + "bacBaBc", + "AcbcAcA", + 10 + ], + [ + "bacC", + "bbAbaCAB", + 11 + ], + [ + "bacCACCA", + "b", + 14 + ], + [ + "bacCAa", + "b", + 10 + ], + [ + "bacCCCccC", + "CCBbcacA", + 13 + ], + [ + "bacCbaBcB", + "Cab", + 13 + ], + [ + "bacCcBC", + "aBcbcccB", + 10 + ], + [ + "bacaCC", + "BAaAbcAC", + 10 + ], + [ + "bacaCcA", + "BcBAc", + 9 + ], + [ + "bacabaAA", + "AAACCBaC", + 13 + ], + [ + "bacacBa", + "BCAAaAA", + 11 + ], + [ + "bacbABaB", + "bCABBAc", + 10 + ], + [ + "bacbCAbbb", + "BaA", + 13 + ], + [ + "bacc", + "aAcAA", + 7 + ], + [ + "baccAA", + "C", + 11 + ], + [ + "baccAa", + "abCCB", + 9 + ], + [ + "baccBAA", + "ccCCbcCC", + 13 + ], + [ + "baccBBc", + "CAb", + 12 + ], + [ + "baccBbBaa", + "AbcaCcbb", + 12 + ], + [ + "baccCaCA", + "CAccb", + 11 + ], + [ + "baccaBc", + "AccABCbCC", + 11 + ], + [ + "baccccCB", + "cC", + 12 + ], + [ + "bb", + "A", + 4 + ], + [ + "bb", + "AABcCbCB", + 13 + ], + [ + "bb", + "AC", + 4 + ], + [ + "bb", + "Aabc", + 6 + ], + [ + "bb", + "AbC", + 4 + ], + [ + "bb", + "AbCC", + 6 + ], + [ + "bb", + "AbCCbbB", + 10 + ], + [ + "bb", + "Acaa", + 8 + ], + [ + "bb", + "B", + 3 + ], + [ + "bb", + "BAbcaB", + 9 + ], + [ + "bb", + "BCAC", + 7 + ], + [ + "bb", + "Bbccc", + 7 + ], + [ + "bb", + "CC", + 4 + ], + [ + "bb", + "a", + 4 + ], + [ + "bb", + "aBcCcc", + 11 + ], + [ + "bb", + "aaBBCc", + 10 + ], + [ + "bb", + "acCc", + 8 + ], + [ + "bb", + "accBa", + 9 + ], + [ + "bb", + "bA", + 2 + ], + [ + "bb", + "bAc", + 4 + ], + [ + "bb", + "bCbAcbaC", + 12 + ], + [ + "bb", + "baAB", + 5 + ], + [ + "bb", + "cAbBAbC", + 10 + ], + [ + "bb", + "cB", + 3 + ], + [ + "bb", + "cBC", + 5 + ], + [ + "bb", + "cbAacCaca", + 16 + ], + [ + "bb", + "cbBbABC", + 10 + ], + [ + "bbA", + "ABC", + 5 + ], + [ + "bbA", + "BBCabAA", + 9 + ], + [ + "bbA", + "BcCC", + 7 + ], + [ + "bbA", + "CB", + 5 + ], + [ + "bbA", + "bBbaababa", + 13 + ], + [ + "bbA", + "cCabBCAc", + 11 + ], + [ + "bbAA", + "B", + 7 + ], + [ + "bbAA", + "CC", + 8 + ], + [ + "bbAAaBb", + "AcC", + 12 + ], + [ + "bbAAb", + "BBBaaACAb", + 10 + ], + [ + "bbAAc", + "bBaCb", + 6 + ], + [ + "bbAAcAca", + "CCbBaccCB", + 12 + ], + [ + "bbAAcBca", + "bcA", + 11 + ], + [ + "bbAAcbCb", + "bAabCBAAC", + 12 + ], + [ + "bbAB", + "AaABCCB", + 10 + ], + [ + "bbAB", + "AbabABcC", + 8 + ], + [ + "bbAB", + "cBb", + 6 + ], + [ + "bbABACAbC", + "Ccabbc", + 13 + ], + [ + "bbABBC", + "CaAA", + 10 + ], + [ + "bbABCBA", + "ACb", + 9 + ], + [ + "bbABCBCbc", + "b", + 16 + ], + [ + "bbABab", + "CAcaba", + 8 + ], + [ + "bbABc", + "Bac", + 6 + ], + [ + "bbABcBBc", + "Aa", + 14 + ], + [ + "bbABcb", + "c", + 10 + ], + [ + "bbACABB", + "acC", + 12 + ], + [ + "bbACCcCbb", + "CCcC", + 10 + ], + [ + "bbAa", + "CbCaC", + 6 + ], + [ + "bbAa", + "cBcBBC", + 10 + ], + [ + "bbAaC", + "bBcCcAB", + 10 + ], + [ + "bbAaCaBAB", + "aabBc", + 13 + ], + [ + "bbAabBcca", + "BbaabB", + 8 + ], + [ + "bbAaccbAb", + "aCbBAAc", + 14 + ], + [ + "bbAb", + "B", + 7 + ], + [ + "bbAb", + "baaCABAa", + 11 + ], + [ + "bbAb", + "caAABCaac", + 15 + ], + [ + "bbAbAcaCA", + "cacab", + 13 + ], + [ + "bbAbB", + "ccAaCc", + 10 + ], + [ + "bbAbBc", + "AbcAbaBa", + 8 + ], + [ + "bbAbC", + "BBa", + 7 + ], + [ + "bbAbC", + "BcAcaCaaa", + 13 + ], + [ + "bbAbcBA", + "cAbbcBC", + 8 + ], + [ + "bbAc", + "bB", + 5 + ], + [ + "bbAcBabb", + "b", + 14 + ], + [ + "bbAcCaAC", + "CccacCABB", + 12 + ], + [ + "bbAcaA", + "caCabBaAB", + 13 + ], + [ + "bbAcabA", + "bBcbCb", + 9 + ], + [ + "bbB", + "Ac", + 6 + ], + [ + "bbB", + "aCbba", + 6 + ], + [ + "bbB", + "cAcbbBb", + 8 + ], + [ + "bbBA", + "B", + 6 + ], + [ + "bbBACc", + "AcCCBa", + 12 + ], + [ + "bbBAab", + "caAaBCbCb", + 14 + ], + [ + "bbBAbaa", + "aa", + 10 + ], + [ + "bbBBA", + "A", + 8 + ], + [ + "bbBBaaC", + "aCB", + 12 + ], + [ + "bbBBbc", + "ab", + 10 + ], + [ + "bbBCCCAbA", + "aAcCBBc", + 14 + ], + [ + "bbBCCaa", + "AACaAbAA", + 14 + ], + [ + "bbBCaaAC", + "bBAc", + 9 + ], + [ + "bbBCcbcbc", + "BBBaCcbBc", + 7 + ], + [ + "bbBa", + "BBcbc", + 7 + ], + [ + "bbBa", + "Cc", + 8 + ], + [ + "bbBaAcBaC", + "Bcc", + 13 + ], + [ + "bbBaCacbC", + "BAAaBcCa", + 13 + ], + [ + "bbBbA", + "Bbca", + 6 + ], + [ + "bbBbB", + "bBCaCbABa", + 11 + ], + [ + "bbBbBc", + "ABbBbc", + 5 + ], + [ + "bbBbCcBa", + "ba", + 12 + ], + [ + "bbBbaaaBB", + "C", + 18 + ], + [ + "bbBbabc", + "ABACBBacC", + 11 + ], + [ + "bbBbbcCA", + "AaabaCabb", + 15 + ], + [ + "bbBbc", + "Bab", + 7 + ], + [ + "bbBc", + "AaCcBCbCb", + 15 + ], + [ + "bbBcBCA", + "acaba", + 11 + ], + [ + "bbBcCc", + "BAc", + 8 + ], + [ + "bbBcab", + "cAabaC", + 10 + ], + [ + "bbBcbAba", + "b", + 14 + ], + [ + "bbBccBaCB", + "AcbAaCc", + 12 + ], + [ + "bbC", + "A", + 6 + ], + [ + "bbC", + "AAccbCBAA", + 14 + ], + [ + "bbC", + "BCAC", + 5 + ], + [ + "bbC", + "BacBCA", + 8 + ], + [ + "bbC", + "acBbCB", + 7 + ], + [ + "bbCAAb", + "aBAAcAc", + 9 + ], + [ + "bbCABbc", + "aCa", + 11 + ], + [ + "bbCACA", + "cBc", + 10 + ], + [ + "bbCAaBb", + "babcacCA", + 10 + ], + [ + "bbCAaa", + "cBaBbCa", + 11 + ], + [ + "bbCBCca", + "bC", + 10 + ], + [ + "bbCBacAB", + "aC", + 13 + ], + [ + "bbCBcBBcb", + "bCCCaa", + 13 + ], + [ + "bbCC", + "cAbBccA", + 9 + ], + [ + "bbCCAA", + "b", + 10 + ], + [ + "bbCCCBc", + "CCBAcbab", + 14 + ], + [ + "bbCCaccaA", + "ba", + 14 + ], + [ + "bbCCbBA", + "A", + 12 + ], + [ + "bbCCcABa", + "CBBbC", + 13 + ], + [ + "bbCCcCc", + "BbaabbaCc", + 11 + ], + [ + "bbCa", + "BC", + 5 + ], + [ + "bbCa", + "abBbA", + 6 + ], + [ + "bbCaAB", + "CCACcabAC", + 12 + ], + [ + "bbCaBabbC", + "CaBABbacC", + 10 + ], + [ + "bbCaaC", + "BbaBCcbCA", + 11 + ], + [ + "bbCaaCA", + "BcBBB", + 12 + ], + [ + "bbCabA", + "BaBBA", + 8 + ], + [ + "bbCabcAA", + "bc", + 12 + ], + [ + "bbCbAaBaA", + "CbAaAAc", + 9 + ], + [ + "bbCbCB", + "AaBB", + 9 + ], + [ + "bbCbaACb", + "Abbc", + 11 + ], + [ + "bbCbcaBA", + "AB", + 13 + ], + [ + "bbCc", + "aBaC", + 6 + ], + [ + "bbCcAA", + "ccaBAabBA", + 14 + ], + [ + "bbCcBaA", + "aAbCCabAb", + 11 + ], + [ + "bbCcaABc", + "a", + 14 + ], + [ + "bbCcc", + "BBAAA", + 8 + ], + [ + "bbCccBCCa", + "cacaCc", + 12 + ], + [ + "bba", + "ABBcccabc", + 14 + ], + [ + "bba", + "Bb", + 3 + ], + [ + "bba", + "C", + 6 + ], + [ + "bba", + "CBACA", + 8 + ], + [ + "bba", + "aCCAABCcb", + 17 + ], + [ + "bba", + "bbba", + 2 + ], + [ + "bbaA", + "CaCcCbb", + 14 + ], + [ + "bbaACCB", + "bAcAccABC", + 10 + ], + [ + "bbaAaaacc", + "CbaACB", + 11 + ], + [ + "bbaAcBA", + "cBbcaa", + 10 + ], + [ + "bbaB", + "Acc", + 8 + ], + [ + "bbaBAccC", + "cBBAb", + 11 + ], + [ + "bbaBBCc", + "C", + 12 + ], + [ + "bbaBCaAb", + "cCbA", + 12 + ], + [ + "bbaBaAc", + "cABCaA", + 9 + ], + [ + "bbaBbCbCb", + "aBccbBC", + 11 + ], + [ + "bbaBbaAbC", + "acaccca", + 15 + ], + [ + "bbaBc", + "bAbbCc", + 6 + ], + [ + "bbaCbCB", + "CCCA", + 10 + ], + [ + "bbaCbbC", + "c", + 13 + ], + [ + "bbaCc", + "CbCaBAaC", + 11 + ], + [ + "bbaCcBccb", + "C", + 16 + ], + [ + "bbab", + "ACB", + 7 + ], + [ + "bbab", + "CbbCbCbcC", + 12 + ], + [ + "bbabAbca", + "cBbbAB", + 10 + ], + [ + "bbabBcc", + "CAa", + 12 + ], + [ + "bbabC", + "abBAaAba", + 9 + ], + [ + "bbabC", + "cBb", + 7 + ], + [ + "bbabc", + "AAccbABAc", + 12 + ], + [ + "bbacC", + "aabaacA", + 8 + ], + [ + "bbacacb", + "acbCBaAcC", + 12 + ], + [ + "bbacccaa", + "AbBABACaa", + 9 + ], + [ + "bbb", + "ABC", + 5 + ], + [ + "bbb", + "C", + 6 + ], + [ + "bbb", + "aCBc", + 7 + ], + [ + "bbbA", + "bccbbcCCB", + 12 + ], + [ + "bbbABA", + "a", + 11 + ], + [ + "bbbACCCa", + "CBbcC", + 10 + ], + [ + "bbbAaBB", + "B", + 12 + ], + [ + "bbbAaCac", + "cCc", + 12 + ], + [ + "bbbAcbcC", + "aBBACacb", + 9 + ], + [ + "bbbB", + "ABAc", + 7 + ], + [ + "bbbB", + "AbABcABAB", + 12 + ], + [ + "bbbB", + "BB", + 5 + ], + [ + "bbbB", + "aC", + 8 + ], + [ + "bbbB", + "cCCcBcbCb", + 14 + ], + [ + "bbbBBBBCb", + "BcaBa", + 14 + ], + [ + "bbbBbA", + "bbA", + 6 + ], + [ + "bbbBbCBcB", + "bbcBcCC", + 9 + ], + [ + "bbbCa", + "CCB", + 8 + ], + [ + "bbbCccAA", + "bBAa", + 10 + ], + [ + "bbbaABaac", + "abaCbCB", + 13 + ], + [ + "bbbaAa", + "b", + 10 + ], + [ + "bbbaB", + "AC", + 9 + ], + [ + "bbbaa", + "CBBccC", + 10 + ], + [ + "bbbaaBba", + "Baa", + 11 + ], + [ + "bbbaacbb", + "cBbaC", + 10 + ], + [ + "bbbacBbBc", + "bcbcbCCC", + 10 + ], + [ + "bbbb", + "AbaACaC", + 12 + ], + [ + "bbbbbAB", + "aaAaCb", + 13 + ], + [ + "bbbbc", + "BBbC", + 5 + ], + [ + "bbbcAB", + "aBAbBBa", + 11 + ], + [ + "bbbcAcCAC", + "AaBbcCCcb", + 12 + ], + [ + "bbbcB", + "bcCc", + 6 + ], + [ + "bbbcBa", + "CAAccB", + 10 + ], + [ + "bbbcCB", + "aAbBABa", + 10 + ], + [ + "bbbca", + "cBbcabCC", + 9 + ], + [ + "bbbcbBAbc", + "AAacbcc", + 12 + ], + [ + "bbbcbcbc", + "Cbbaac", + 10 + ], + [ + "bbc", + "ACBaABaCC", + 15 + ], + [ + "bbc", + "BcCBaBcCc", + 14 + ], + [ + "bbc", + "CcB", + 6 + ], + [ + "bbc", + "bCCcBAac", + 11 + ], + [ + "bbc", + "c", + 4 + ], + [ + "bbcA", + "AAC", + 7 + ], + [ + "bbcACCca", + "bAABA", + 11 + ], + [ + "bbcAbBa", + "CaaA", + 11 + ], + [ + "bbcAbca", + "CCCCc", + 11 + ], + [ + "bbcAcbBab", + "CBacCca", + 12 + ], + [ + "bbcB", + "BaAbbccBb", + 10 + ], + [ + "bbcBAa", + "baaacc", + 10 + ], + [ + "bbcBCa", + "CaBabc", + 11 + ], + [ + "bbcCA", + "CAb", + 8 + ], + [ + "bbcCBACB", + "bcBBCaBc", + 10 + ], + [ + "bbcCBbAA", + "CcAc", + 12 + ], + [ + "bbcCC", + "BcbCc", + 6 + ], + [ + "bbcCCcB", + "b", + 12 + ], + [ + "bbca", + "AbbcaAcCa", + 10 + ], + [ + "bbca", + "acaBbbB", + 11 + ], + [ + "bbcaACCab", + "aACBab", + 8 + ], + [ + "bbcaAb", + "b", + 10 + ], + [ + "bbcaCab", + "B", + 13 + ], + [ + "bbcaaBCA", + "BCaaC", + 8 + ], + [ + "bbcaacCC", + "bCCcBACB", + 11 + ], + [ + "bbcab", + "bABaa", + 6 + ], + [ + "bbcabAB", + "BcC", + 11 + ], + [ + "bbcb", + "abAA", + 6 + ], + [ + "bbcbB", + "bCbAab", + 7 + ], + [ + "bbcbBccAa", + "bA", + 14 + ], + [ + "bbcbCcAB", + "cacaCCaBA", + 10 + ], + [ + "bbcc", + "B", + 7 + ], + [ + "bbcc", + "CcBBaABa", + 14 + ], + [ + "bbccAcBCB", + "CbacCAaB", + 11 + ], + [ + "bbccC", + "bac", + 6 + ], + [ + "bbccCa", + "baacc", + 7 + ], + [ + "bbccCcA", + "CbbbC", + 10 + ], + [ + "bbccaca", + "bb", + 10 + ], + [ + "bbccb", + "ABCA", + 8 + ], + [ + "bbccbB", + "Cbccbcb", + 5 + ], + [ + "bbccbBB", + "cbCB", + 8 + ], + [ + "bc", + "ACcCb", + 8 + ], + [ + "bc", + "Aa", + 4 + ], + [ + "bc", + "Ac", + 2 + ], + [ + "bc", + "BBAA", + 7 + ], + [ + "bc", + "BcA", + 3 + ], + [ + "bc", + "BcbBaCaCB", + 15 + ], + [ + "bc", + "CACcCBc", + 11 + ], + [ + "bc", + "CBBaAccB", + 13 + ], + [ + "bc", + "CaBa", + 7 + ], + [ + "bc", + "CcCaaABa", + 14 + ], + [ + "bc", + "a", + 4 + ], + [ + "bc", + "aAA", + 6 + ], + [ + "bc", + "aACBcB", + 9 + ], + [ + "bc", + "aBacc", + 7 + ], + [ + "bc", + "aBbAa", + 8 + ], + [ + "bc", + "acb", + 4 + ], + [ + "bc", + "b", + 2 + ], + [ + "bc", + "bA", + 2 + ], + [ + "bc", + "bBCcbA", + 8 + ], + [ + "bc", + "c", + 2 + ], + [ + "bc", + "cBb", + 5 + ], + [ + "bc", + "caaabc", + 8 + ], + [ + "bc", + "ccAB", + 6 + ], + [ + "bcA", + "ABbA", + 5 + ], + [ + "bcA", + "BBB", + 5 + ], + [ + "bcA", + "aabaAAB", + 10 + ], + [ + "bcA", + "acacbc", + 9 + ], + [ + "bcA", + "cbaACAB", + 9 + ], + [ + "bcAA", + "BaCc", + 7 + ], + [ + "bcAABAab", + "CB", + 13 + ], + [ + "bcAABbbBa", + "ABcccc", + 14 + ], + [ + "bcAABcb", + "c", + 12 + ], + [ + "bcAAbc", + "B", + 11 + ], + [ + "bcAB", + "a", + 7 + ], + [ + "bcAB", + "bcB", + 2 + ], + [ + "bcABAABcB", + "Cc", + 15 + ], + [ + "bcABAbBC", + "c", + 14 + ], + [ + "bcABAcB", + "bcCBCAbCa", + 9 + ], + [ + "bcAC", + "AAaC", + 5 + ], + [ + "bcACABab", + "c", + 14 + ], + [ + "bcACBbBaC", + "baaAbBbB", + 10 + ], + [ + "bcACa", + "bbaAcCBB", + 10 + ], + [ + "bcACbCa", + "aaaBBAbC", + 13 + ], + [ + "bcAa", + "BC", + 6 + ], + [ + "bcAa", + "b", + 6 + ], + [ + "bcAaAabA", + "abBB", + 13 + ], + [ + "bcAaBABA", + "CABac", + 10 + ], + [ + "bcAaBAaC", + "AaC", + 10 + ], + [ + "bcAaBCc", + "ca", + 10 + ], + [ + "bcAaaC", + "cbA", + 9 + ], + [ + "bcAabc", + "BAABb", + 7 + ], + [ + "bcAb", + "BcbbbA", + 7 + ], + [ + "bcAbAAAAb", + "abCBaAABc", + 11 + ], + [ + "bcAbACaB", + "cCaCAC", + 10 + ], + [ + "bcAbacAbc", + "BccaBBCbb", + 12 + ], + [ + "bcAc", + "BCBcaBCaB", + 13 + ], + [ + "bcAca", + "A", + 8 + ], + [ + "bcAcba", + "CcAc", + 6 + ], + [ + "bcAcbccB", + "Abbac", + 10 + ], + [ + "bcAccA", + "bbabbAcC", + 11 + ], + [ + "bcAccab", + "CCACcbcC", + 10 + ], + [ + "bcB", + "AaCbCccbb", + 13 + ], + [ + "bcB", + "BaAACbA", + 11 + ], + [ + "bcB", + "CAAABA", + 10 + ], + [ + "bcB", + "aaAaBccC", + 13 + ], + [ + "bcB", + "bAAAACCA", + 13 + ], + [ + "bcB", + "cAbcb", + 5 + ], + [ + "bcBA", + "bBBAb", + 4 + ], + [ + "bcBAAc", + "BccA", + 7 + ], + [ + "bcBACabcB", + "abBA", + 14 + ], + [ + "bcBAab", + "baaaBbcc", + 11 + ], + [ + "bcBAcAC", + "aCCC", + 10 + ], + [ + "bcBBC", + "c", + 8 + ], + [ + "bcBBCbAbA", + "bcCCb", + 10 + ], + [ + "bcBBaA", + "ABCBb", + 9 + ], + [ + "bcBBabABb", + "BcbaBBBCa", + 11 + ], + [ + "bcBBbB", + "bbbCC", + 8 + ], + [ + "bcBCAaaaB", + "CC", + 15 + ], + [ + "bcBCBC", + "aCB", + 8 + ], + [ + "bcBCCAB", + "ABCbc", + 10 + ], + [ + "bcBCCB", + "cAb", + 9 + ], + [ + "bcBCCcbA", + "c", + 14 + ], + [ + "bcBCa", + "CBccAbA", + 10 + ], + [ + "bcBa", + "abaBbcaba", + 11 + ], + [ + "bcBaBB", + "CB", + 9 + ], + [ + "bcBaCc", + "aaCBa", + 9 + ], + [ + "bcBaa", + "BcBB", + 5 + ], + [ + "bcBabcbc", + "accAbcB", + 8 + ], + [ + "bcBac", + "BB", + 7 + ], + [ + "bcBbAB", + "aCAbA", + 7 + ], + [ + "bcBbACCB", + "BAbcCc", + 10 + ], + [ + "bcBbACCac", + "CB", + 15 + ], + [ + "bcBbBCB", + "BcCbb", + 8 + ], + [ + "bcBbbAcac", + "ACbccac", + 9 + ], + [ + "bcBbc", + "caC", + 7 + ], + [ + "bcBc", + "A", + 8 + ], + [ + "bcBcaa", + "BbbCAaBB", + 10 + ], + [ + "bcBccBCBA", + "ABaCCAba", + 13 + ], + [ + "bcBccbcC", + "aC", + 14 + ], + [ + "bcC", + "ACabb", + 9 + ], + [ + "bcC", + "Ac", + 4 + ], + [ + "bcC", + "CACac", + 8 + ], + [ + "bcC", + "CAaCbCbb", + 13 + ], + [ + "bcC", + "CacBAbbac", + 15 + ], + [ + "bcC", + "a", + 6 + ], + [ + "bcC", + "abBBcB", + 8 + ], + [ + "bcC", + "acaAAaCbb", + 14 + ], + [ + "bcC", + "bBCaBBC", + 9 + ], + [ + "bcC", + "bbbCCABb", + 11 + ], + [ + "bcC", + "cBBBBCac", + 13 + ], + [ + "bcC", + "cCAAb", + 8 + ], + [ + "bcC", + "ccBcB", + 7 + ], + [ + "bcCAAA", + "CCCBbBAC", + 11 + ], + [ + "bcCAaA", + "AbCabC", + 9 + ], + [ + "bcCAb", + "CbBccBC", + 9 + ], + [ + "bcCAcAa", + "cB", + 12 + ], + [ + "bcCAcbC", + "AcAAbc", + 7 + ], + [ + "bcCB", + "a", + 8 + ], + [ + "bcCBAB", + "AaBAcB", + 8 + ], + [ + "bcCBBcb", + "CaCbaa", + 11 + ], + [ + "bcCBC", + "cb", + 7 + ], + [ + "bcCBaCAAc", + "aBb", + 16 + ], + [ + "bcCBaCb", + "aaaacC", + 11 + ], + [ + "bcCCAbB", + "BBcCAcA", + 8 + ], + [ + "bcCCa", + "BACbBaAcC", + 13 + ], + [ + "bcCCaa", + "cBcC", + 9 + ], + [ + "bcCCcbA", + "AAaAcCCA", + 12 + ], + [ + "bcCa", + "bCCC", + 3 + ], + [ + "bcCaaAB", + "cAAc", + 9 + ], + [ + "bcCaacC", + "BbcaccCCB", + 10 + ], + [ + "bcCab", + "BcCABbCAC", + 10 + ], + [ + "bcCacAA", + "CcbB", + 10 + ], + [ + "bcCacCaa", + "ABc", + 14 + ], + [ + "bcCacba", + "Bc", + 11 + ], + [ + "bcCb", + "aCbcBbCc", + 10 + ], + [ + "bcCbB", + "bAbCc", + 8 + ], + [ + "bcCbBCbaB", + "BC", + 14 + ], + [ + "bcCbaa", + "BbAACBAaC", + 10 + ], + [ + "bcCbbc", + "baAA", + 10 + ], + [ + "bcCc", + "AAA", + 8 + ], + [ + "bcCcBB", + "BaAccBC", + 8 + ], + [ + "bcCcaCCca", + "AaCc", + 12 + ], + [ + "bcCcbAAc", + "Ca", + 13 + ], + [ + "bca", + "AB", + 6 + ], + [ + "bca", + "AcaaBC", + 8 + ], + [ + "bca", + "CACBbcC", + 10 + ], + [ + "bca", + "bccb", + 4 + ], + [ + "bcaA", + "aBaCBBAa", + 12 + ], + [ + "bcaAAbCbC", + "CCcAA", + 13 + ], + [ + "bcaABAa", + "Ac", + 12 + ], + [ + "bcaACb", + "bAbCaCB", + 8 + ], + [ + "bcaAa", + "BbCcc", + 9 + ], + [ + "bcaAaaAC", + "acaaABc", + 8 + ], + [ + "bcaAba", + "BabBAaccb", + 14 + ], + [ + "bcaB", + "bCBCBAA", + 9 + ], + [ + "bcaBBAC", + "Cb", + 12 + ], + [ + "bcaCA", + "ACA", + 5 + ], + [ + "bcaCA", + "aB", + 8 + ], + [ + "bcaCAABcB", + "bBAbCcbc", + 12 + ], + [ + "bcaCB", + "AbaACbaC", + 10 + ], + [ + "bcaCBa", + "BaAbAB", + 9 + ], + [ + "bcaCa", + "cCbccbA", + 9 + ], + [ + "bcaCbBA", + "bcBabAb", + 8 + ], + [ + "bcaCbCbaA", + "bbCBb", + 11 + ], + [ + "bcaCcB", + "cAab", + 8 + ], + [ + "bcaa", + "CC", + 7 + ], + [ + "bcaa", + "aaBA", + 7 + ], + [ + "bcaaA", + "BAAacaAba", + 11 + ], + [ + "bcaaABBA", + "baa", + 10 + ], + [ + "bcaaAcbC", + "aBACAcaab", + 13 + ], + [ + "bcaaBaAb", + "b", + 14 + ], + [ + "bcaacb", + "acCb", + 7 + ], + [ + "bcab", + "a", + 6 + ], + [ + "bcab", + "cCAabCBCb", + 13 + ], + [ + "bcabAA", + "BAAcca", + 9 + ], + [ + "bcabC", + "Aac", + 7 + ], + [ + "bcabb", + "aAaA", + 8 + ], + [ + "bcac", + "CCAAbCbC", + 12 + ], + [ + "bcb", + "A", + 6 + ], + [ + "bcb", + "aBabCcB", + 9 + ], + [ + "bcb", + "abACACc", + 11 + ], + [ + "bcbA", + "AcbAcbCaB", + 11 + ], + [ + "bcbAAA", + "a", + 11 + ], + [ + "bcbAAAB", + "a", + 13 + ], + [ + "bcbACB", + "CBbcbCca", + 9 + ], + [ + "bcbACCA", + "AabaACAcB", + 11 + ], + [ + "bcbACCC", + "c", + 12 + ], + [ + "bcbAbCbB", + "caA", + 12 + ], + [ + "bcbAc", + "BACCccbCb", + 13 + ], + [ + "bcbBA", + "baaaa", + 7 + ], + [ + "bcbBAABCa", + "cBAcaa", + 10 + ], + [ + "bcbCAaAC", + "bB", + 13 + ], + [ + "bcbCAc", + "caccbaABC", + 11 + ], + [ + "bcbCB", + "c", + 8 + ], + [ + "bcbCcCa", + "CCCbACb", + 11 + ], + [ + "bcbaAACb", + "aBAcA", + 11 + ], + [ + "bcbaBBB", + "AA", + 13 + ], + [ + "bcbaccb", + "ACACCB", + 9 + ], + [ + "bcbb", + "cBcACA", + 9 + ], + [ + "bcbb", + "cCCBaaA", + 12 + ], + [ + "bcbbABC", + "CAAaa", + 11 + ], + [ + "bcbbB", + "ABB", + 7 + ], + [ + "bcbbBc", + "cCc", + 8 + ], + [ + "bcbbCBabA", + "cCccbccBA", + 12 + ], + [ + "bcbc", + "AAA", + 8 + ], + [ + "bcbc", + "Ca", + 7 + ], + [ + "bcbcAC", + "bBAaaacb", + 12 + ], + [ + "bcc", + "C", + 5 + ], + [ + "bcc", + "CaACa", + 9 + ], + [ + "bcc", + "bACb", + 5 + ], + [ + "bcc", + "bbcCcAa", + 8 + ], + [ + "bccA", + "C", + 7 + ], + [ + "bccA", + "aAb", + 8 + ], + [ + "bccA", + "ab", + 8 + ], + [ + "bccABAba", + "acacC", + 13 + ], + [ + "bccAC", + "BbbBcA", + 8 + ], + [ + "bccAbaA", + "aAc", + 12 + ], + [ + "bccAbcbcb", + "CbbcbbA", + 11 + ], + [ + "bccB", + "BcacaCB", + 7 + ], + [ + "bccB", + "cAcb", + 5 + ], + [ + "bccBABc", + "bAacbb", + 10 + ], + [ + "bccBBB", + "aAbbbbC", + 11 + ], + [ + "bccBBCbBa", + "bbbaabCbB", + 11 + ], + [ + "bccBBc", + "cCcbac", + 6 + ], + [ + "bccBBccA", + "bB", + 12 + ], + [ + "bccBCCAC", + "bABBCAabC", + 9 + ], + [ + "bccBCCBc", + "ACBaa", + 13 + ], + [ + "bccBa", + "CAcbB", + 7 + ], + [ + "bccBbA", + "aBCcBBBC", + 9 + ], + [ + "bccCA", + "BaBAAba", + 11 + ], + [ + "bccCAbbB", + "ccC", + 10 + ], + [ + "bccCBab", + "c", + 12 + ], + [ + "bccCCAaBB", + "bBa", + 14 + ], + [ + "bccCaBCC", + "Bb", + 14 + ], + [ + "bcca", + "AaAaabA", + 12 + ], + [ + "bccaCBb", + "bAbcabA", + 9 + ], + [ + "bccaCCC", + "CCBCBbAAA", + 16 + ], + [ + "bccaa", + "C", + 9 + ], + [ + "bccaab", + "cAcc", + 9 + ], + [ + "bccbAbab", + "b", + 14 + ], + [ + "bccbBCcCA", + "B", + 16 + ], + [ + "bccbBb", + "ccba", + 6 + ], + [ + "bccbC", + "bBC", + 5 + ], + [ + "bccbbCC", + "bBccA", + 9 + ], + [ + "bccbbbCB", + "BbcABC", + 10 + ], + [ + "bccbccAc", + "bBC", + 12 + ], + [ + "bccc", + "ACcca", + 5 + ], + [ + "bcccAA", + "bCBAbca", + 10 + ], + [ + "bcccAaBca", + "BaC", + 14 + ], + [ + "bcccBBCaA", + "bcAcaCcb", + 10 + ], + [ + "bcccC", + "aAAaa", + 10 + ], + [ + "bcccCCCbc", + "Bac", + 15 + ], + [ + "c", + "A", + 2 + ], + [ + "c", + "AA", + 4 + ], + [ + "c", + "AABacBC", + 12 + ], + [ + "c", + "AAbBBaBB", + 16 + ], + [ + "c", + "AB", + 4 + ], + [ + "c", + "ABA", + 6 + ], + [ + "c", + "ABBaCCBBB", + 17 + ], + [ + "c", + "ABBbBBCBA", + 17 + ], + [ + "c", + "ABbCb", + 9 + ], + [ + "c", + "ABbaaba", + 14 + ], + [ + "c", + "AC", + 3 + ], + [ + "c", + "ACABa", + 9 + ], + [ + "c", + "ACCB", + 7 + ], + [ + "c", + "ACCcbCCbC", + 16 + ], + [ + "c", + "ACbBBC", + 11 + ], + [ + "c", + "Aa", + 4 + ], + [ + "c", + "AaABA", + 10 + ], + [ + "c", + "Aaa", + 6 + ], + [ + "c", + "AaaaBbBC", + 15 + ], + [ + "c", + "AaabCAb", + 13 + ], + [ + "c", + "Ab", + 4 + ], + [ + "c", + "AbaCAAA", + 13 + ], + [ + "c", + "AcCACaAA", + 14 + ], + [ + "c", + "AcCCBAcaB", + 16 + ], + [ + "c", + "Acb", + 4 + ], + [ + "c", + "AcccbCacB", + 16 + ], + [ + "c", + "B", + 2 + ], + [ + "c", + "BA", + 4 + ], + [ + "c", + "BAAAAbB", + 14 + ], + [ + "c", + "BACA", + 7 + ], + [ + "c", + "BACbAC", + 11 + ], + [ + "c", + "BAbBbAc", + 12 + ], + [ + "c", + "BAbb", + 8 + ], + [ + "c", + "BAcBb", + 8 + ], + [ + "c", + "BB", + 4 + ], + [ + "c", + "BBAACAb", + 13 + ], + [ + "c", + "BBABBC", + 11 + ], + [ + "c", + "BBBcBccc", + 14 + ], + [ + "c", + "BBbbc", + 8 + ], + [ + "c", + "BBbcbbac", + 14 + ], + [ + "c", + "BBcBc", + 8 + ], + [ + "c", + "BC", + 3 + ], + [ + "c", + "BCA", + 5 + ], + [ + "c", + "BCAAcBC", + 12 + ], + [ + "c", + "BCAbCb", + 11 + ], + [ + "c", + "BCBc", + 6 + ], + [ + "c", + "BCa", + 5 + ], + [ + "c", + "BCcbaB", + 10 + ], + [ + "c", + "Ba", + 4 + ], + [ + "c", + "BaAaaA", + 12 + ], + [ + "c", + "BaBacca", + 12 + ], + [ + "c", + "BaCCBaCAC", + 17 + ], + [ + "c", + "BaaBcAcBb", + 16 + ], + [ + "c", + "BaaccC", + 10 + ], + [ + "c", + "BabCcAAcA", + 16 + ], + [ + "c", + "BbABABaBC", + 17 + ], + [ + "c", + "BbCa", + 7 + ], + [ + "c", + "BbaAacbb", + 14 + ], + [ + "c", + "BbaCAAcBa", + 16 + ], + [ + "c", + "BbcBb", + 8 + ], + [ + "c", + "Bbcc", + 6 + ], + [ + "c", + "Bc", + 2 + ], + [ + "c", + "BcBa", + 6 + ], + [ + "c", + "Bcb", + 4 + ], + [ + "c", + "Bcccb", + 8 + ], + [ + "c", + "C", + 1 + ], + [ + "c", + "CB", + 3 + ], + [ + "c", + "CBABBC", + 11 + ], + [ + "c", + "CBAcCA", + 10 + ], + [ + "c", + "CBBAbbA", + 13 + ], + [ + "c", + "CBCBb", + 9 + ], + [ + "c", + "CBCaBa", + 11 + ], + [ + "c", + "CBb", + 5 + ], + [ + "c", + "CCCBaaCB", + 15 + ], + [ + "c", + "CCCCCC", + 11 + ], + [ + "c", + "Ca", + 3 + ], + [ + "c", + "CaAb", + 7 + ], + [ + "c", + "CaCa", + 7 + ], + [ + "c", + "Cab", + 5 + ], + [ + "c", + "CabC", + 7 + ], + [ + "c", + "CbAC", + 7 + ], + [ + "c", + "CbabCAaAc", + 16 + ], + [ + "c", + "CcAABA", + 10 + ], + [ + "c", + "CccaaAa", + 12 + ], + [ + "c", + "a", + 2 + ], + [ + "c", + "aAAa", + 8 + ], + [ + "c", + "aAAc", + 6 + ], + [ + "c", + "aACbcB", + 10 + ], + [ + "c", + "aAbACcCA", + 14 + ], + [ + "c", + "aAbab", + 10 + ], + [ + "c", + "aB", + 4 + ], + [ + "c", + "aBAC", + 7 + ], + [ + "c", + "aBBbbc", + 10 + ], + [ + "c", + "aBaaA", + 10 + ], + [ + "c", + "aBb", + 6 + ], + [ + "c", + "aBcABcCCb", + 16 + ], + [ + "c", + "aBcBBCBbc", + 16 + ], + [ + "c", + "aCa", + 5 + ], + [ + "c", + "aa", + 4 + ], + [ + "c", + "aaCBCbC", + 13 + ], + [ + "c", + "aabcBAAcc", + 16 + ], + [ + "c", + "aabcBcA", + 12 + ], + [ + "c", + "aacABB", + 10 + ], + [ + "c", + "aacb", + 6 + ], + [ + "c", + "aaccAca", + 12 + ], + [ + "c", + "ab", + 4 + ], + [ + "c", + "abAa", + 8 + ], + [ + "c", + "abCAcC", + 10 + ], + [ + "c", + "abCCAB", + 11 + ], + [ + "c", + "abCc", + 6 + ], + [ + "c", + "ac", + 2 + ], + [ + "c", + "accbaBBAB", + 16 + ], + [ + "c", + "b", + 2 + ], + [ + "c", + "bAAccBAa", + 14 + ], + [ + "c", + "bAaBAbcCc", + 16 + ], + [ + "c", + "bAcacAbb", + 14 + ], + [ + "c", + "bBAaCa", + 11 + ], + [ + "c", + "bBAcaCaca", + 16 + ], + [ + "c", + "bBBCbaB", + 13 + ], + [ + "c", + "bBBa", + 8 + ], + [ + "c", + "bBab", + 8 + ], + [ + "c", + "bBcCbcABC", + 16 + ], + [ + "c", + "bBcCca", + 10 + ], + [ + "c", + "bCBC", + 7 + ], + [ + "c", + "bCC", + 5 + ], + [ + "c", + "bCCAbcBBa", + 16 + ], + [ + "c", + "bCCa", + 7 + ], + [ + "c", + "bCabaBC", + 13 + ], + [ + "c", + "bCc", + 4 + ], + [ + "c", + "bCcA", + 6 + ], + [ + "c", + "baaAB", + 10 + ], + [ + "c", + "baaABBC", + 13 + ], + [ + "c", + "baaAccba", + 14 + ], + [ + "c", + "bbBbbBBa", + 16 + ], + [ + "c", + "bbCccAAC", + 14 + ], + [ + "c", + "bc", + 2 + ], + [ + "c", + "bcAbC", + 8 + ], + [ + "c", + "bcBAcAcB", + 14 + ], + [ + "c", + "bcbBbCcb", + 14 + ], + [ + "c", + "bccAAa", + 10 + ], + [ + "c", + "c", + 0 + ], + [ + "c", + "cAABBb", + 10 + ], + [ + "c", + "cAACa", + 8 + ], + [ + "c", + "cABc", + 6 + ], + [ + "c", + "cAaCBc", + 10 + ], + [ + "c", + "cAacbCCA", + 14 + ], + [ + "c", + "cBCCAbA", + 12 + ], + [ + "c", + "cBacAc", + 10 + ], + [ + "c", + "cBcCBb", + 10 + ], + [ + "c", + "cCAB", + 6 + ], + [ + "c", + "cCCbbBBcA", + 16 + ], + [ + "c", + "cCcaBbcba", + 16 + ], + [ + "c", + "ca", + 2 + ], + [ + "c", + "caC", + 4 + ], + [ + "c", + "caa", + 4 + ], + [ + "c", + "cabA", + 6 + ], + [ + "c", + "cb", + 2 + ], + [ + "c", + "cbCaBBab", + 14 + ], + [ + "c", + "cbCbAa", + 10 + ], + [ + "c", + "cbbBbCcC", + 14 + ], + [ + "c", + "cbcaAAC", + 12 + ], + [ + "c", + "cc", + 2 + ], + [ + "c", + "ccB", + 4 + ], + [ + "c", + "ccCBaBAC", + 14 + ], + [ + "c", + "ccc", + 4 + ], + [ + "cA", + "A", + 2 + ], + [ + "cA", + "AAbaCB", + 10 + ], + [ + "cA", + "AC", + 4 + ], + [ + "cA", + "Ac", + 4 + ], + [ + "cA", + "Acc", + 4 + ], + [ + "cA", + "BABCB", + 8 + ], + [ + "cA", + "BBc", + 6 + ], + [ + "cA", + "BaaBCbA", + 11 + ], + [ + "cA", + "Bbb", + 6 + ], + [ + "cA", + "BbbAAccB", + 14 + ], + [ + "cA", + "Bc", + 4 + ], + [ + "cA", + "C", + 3 + ], + [ + "cA", + "CAaCAABb", + 13 + ], + [ + "cA", + "CAaacAcc", + 12 + ], + [ + "cA", + "CbaAccc", + 11 + ], + [ + "cA", + "Cbbb", + 7 + ], + [ + "cA", + "CcC", + 4 + ], + [ + "cA", + "aB", + 4 + ], + [ + "cA", + "aC", + 4 + ], + [ + "cA", + "ab", + 4 + ], + [ + "cA", + "abAAcc", + 10 + ], + [ + "cA", + "abC", + 6 + ], + [ + "cA", + "bBCAbCa", + 11 + ], + [ + "cA", + "bCbbBCAC", + 13 + ], + [ + "cA", + "bCcCBcCA", + 12 + ], + [ + "cA", + "bcaCcbC", + 11 + ], + [ + "cA", + "cABaBCb", + 10 + ], + [ + "cA", + "cBaAB", + 6 + ], + [ + "cA", + "cCBababA", + 12 + ], + [ + "cA", + "ccCBbBab", + 13 + ], + [ + "cA", + "cca", + 3 + ], + [ + "cAA", + "AaaCaBcCb", + 16 + ], + [ + "cAA", + "AbBC", + 8 + ], + [ + "cAA", + "a", + 5 + ], + [ + "cAA", + "cb", + 4 + ], + [ + "cAAA", + "cBaC", + 5 + ], + [ + "cAAAA", + "bcA", + 8 + ], + [ + "cAAAC", + "aBAaCcCca", + 13 + ], + [ + "cAAAacbCB", + "BBaACbA", + 12 + ], + [ + "cAAAbabc", + "BCAacBCBc", + 10 + ], + [ + "cAAAbbc", + "AAbb", + 6 + ], + [ + "cAAAcB", + "bbAB", + 8 + ], + [ + "cAAB", + "ABCaCbCbC", + 15 + ], + [ + "cAAB", + "Aab", + 4 + ], + [ + "cAABaBAa", + "bAaBaBA", + 5 + ], + [ + "cAAC", + "CAB", + 5 + ], + [ + "cAACA", + "CaABCCbAb", + 10 + ], + [ + "cAACAC", + "BC", + 10 + ], + [ + "cAACaCc", + "AcabA", + 9 + ], + [ + "cAACb", + "BCAcca", + 8 + ], + [ + "cAACbABCc", + "CCbAB", + 9 + ], + [ + "cAACbb", + "aAaC", + 7 + ], + [ + "cAACcAA", + "bAaCcacb", + 8 + ], + [ + "cAAa", + "caABcbBAB", + 12 + ], + [ + "cAAaB", + "BBaAb", + 7 + ], + [ + "cAAaBAAA", + "aCb", + 14 + ], + [ + "cAAaCbCc", + "CC", + 12 + ], + [ + "cAAaba", + "bcb", + 10 + ], + [ + "cAAabcCCA", + "aAa", + 13 + ], + [ + "cAAbB", + "AAcaBaa", + 10 + ], + [ + "cAAbBA", + "caCcABAB", + 9 + ], + [ + "cAAbBBaCa", + "CabcB", + 12 + ], + [ + "cAAbC", + "BAbbb", + 6 + ], + [ + "cAAba", + "CCBAabaAB", + 10 + ], + [ + "cAAc", + "bCcCCB", + 10 + ], + [ + "cAAcABCcb", + "BCBB", + 13 + ], + [ + "cAAcB", + "ca", + 7 + ], + [ + "cAAcCCAc", + "Cb", + 14 + ], + [ + "cAAcaabB", + "A", + 14 + ], + [ + "cAAccaaB", + "BBbbAcBc", + 15 + ], + [ + "cAB", + "B", + 4 + ], + [ + "cAB", + "BCAAacb", + 10 + ], + [ + "cAB", + "CaCbCca", + 11 + ], + [ + "cAB", + "CbacbCcbB", + 14 + ], + [ + "cAB", + "b", + 5 + ], + [ + "cAB", + "baabbCA", + 12 + ], + [ + "cAB", + "cbbaBbCb", + 11 + ], + [ + "cAB", + "cccBBCBC", + 12 + ], + [ + "cAB", + "cccCCCCb", + 13 + ], + [ + "cABA", + "CA", + 5 + ], + [ + "cABAABc", + "acabCCb", + 11 + ], + [ + "cABABAACA", + "cBaBCcaa", + 10 + ], + [ + "cABAa", + "c", + 8 + ], + [ + "cABAcaBb", + "BCBaBCBc", + 11 + ], + [ + "cABBAaCac", + "aAacACCcc", + 10 + ], + [ + "cABBCaBbc", + "bBAaBcACC", + 13 + ], + [ + "cABBc", + "B", + 8 + ], + [ + "cABBcacb", + "bBcCcCAA", + 13 + ], + [ + "cABC", + "B", + 6 + ], + [ + "cABCAB", + "bAcb", + 8 + ], + [ + "cABCAc", + "cBCBa", + 6 + ], + [ + "cABCAca", + "ABaCCCbcc", + 12 + ], + [ + "cABCBbbCA", + "ABBccc", + 11 + ], + [ + "cABCa", + "CaA", + 7 + ], + [ + "cABaA", + "aAAbaBb", + 9 + ], + [ + "cABaAaA", + "BAB", + 10 + ], + [ + "cABaBAC", + "acCa", + 12 + ], + [ + "cABaCAbbA", + "A", + 16 + ], + [ + "cABac", + "BaCc", + 6 + ], + [ + "cABbACA", + "BbccCBCBc", + 15 + ], + [ + "cABba", + "AAC", + 8 + ], + [ + "cABc", + "AaccB", + 7 + ], + [ + "cABcABc", + "cAcBBca", + 6 + ], + [ + "cABcBcb", + "cbBCa", + 8 + ], + [ + "cABcb", + "Cac", + 6 + ], + [ + "cABcba", + "BAab", + 8 + ], + [ + "cAC", + "BCaCAbaCa", + 13 + ], + [ + "cAC", + "C", + 4 + ], + [ + "cAC", + "CABbA", + 7 + ], + [ + "cAC", + "CAcABbA", + 10 + ], + [ + "cAC", + "bAa", + 4 + ], + [ + "cAC", + "cBBac", + 6 + ], + [ + "cACAACCbc", + "CAcABb", + 10 + ], + [ + "cACABcACC", + "CAc", + 12 + ], + [ + "cACACbC", + "BbCcAa", + 11 + ], + [ + "cACAabC", + "BbcbBcAAa", + 14 + ], + [ + "cACBCaC", + "C", + 12 + ], + [ + "cACBCcCBb", + "bBaaBaCbC", + 14 + ], + [ + "cACBaaB", + "bccBcBaB", + 9 + ], + [ + "cACCBBcb", + "Ba", + 14 + ], + [ + "cACCaa", + "aBcCaab", + 7 + ], + [ + "cACCc", + "ccAa", + 7 + ], + [ + "cACa", + "aBBcB", + 9 + ], + [ + "cACa", + "cAbCCA", + 5 + ], + [ + "cACaB", + "cAbbaBB", + 6 + ], + [ + "cACac", + "cBCBbbbB", + 12 + ], + [ + "cACbCBB", + "bBbAC", + 12 + ], + [ + "cACbaCc", + "AacCACBaB", + 11 + ], + [ + "cACbaba", + "acAACBcCC", + 11 + ], + [ + "cACbbC", + "Cba", + 8 + ], + [ + "cACbbb", + "ccacBccbA", + 11 + ], + [ + "cACbcb", + "b", + 10 + ], + [ + "cACc", + "Cc", + 4 + ], + [ + "cACc", + "bAba", + 6 + ], + [ + "cACcA", + "Caccc", + 5 + ], + [ + "cACcAcc", + "bbB", + 14 + ], + [ + "cACca", + "acabb", + 9 + ], + [ + "cACcaA", + "bB", + 12 + ], + [ + "cACccAacA", + "bcCBabCc", + 14 + ], + [ + "cAa", + "BacbCAbcA", + 13 + ], + [ + "cAa", + "Bbcb", + 8 + ], + [ + "cAa", + "C", + 5 + ], + [ + "cAa", + "aCBcCAcc", + 12 + ], + [ + "cAa", + "aCacc", + 8 + ], + [ + "cAa", + "caBcA", + 6 + ], + [ + "cAa", + "cacAbaAb", + 10 + ], + [ + "cAa", + "ccccAcaBC", + 12 + ], + [ + "cAaA", + "CbbaAaba", + 10 + ], + [ + "cAaAA", + "CaCbcbab", + 13 + ], + [ + "cAaAA", + "acAc", + 7 + ], + [ + "cAaAABAcb", + "bAba", + 14 + ], + [ + "cAaACBaa", + "cbBabcaac", + 11 + ], + [ + "cAaACc", + "ccbBB", + 10 + ], + [ + "cAaAaaB", + "CBCbBccBa", + 15 + ], + [ + "cAaAc", + "ABCACcb", + 10 + ], + [ + "cAaB", + "C", + 7 + ], + [ + "cAaB", + "aBaBA", + 6 + ], + [ + "cAaBCBbAA", + "aAc", + 14 + ], + [ + "cAaBaA", + "a", + 10 + ], + [ + "cAaC", + "BcbAbbCb", + 10 + ], + [ + "cAaC", + "CbB", + 7 + ], + [ + "cAaCAc", + "ac", + 8 + ], + [ + "cAaCaAc", + "bBBBbaC", + 12 + ], + [ + "cAaCaC", + "ABBa", + 8 + ], + [ + "cAaCc", + "c", + 8 + ], + [ + "cAaaAAA", + "BBCAa", + 11 + ], + [ + "cAaaaBCA", + "AcABc", + 10 + ], + [ + "cAaaacAB", + "aAbCB", + 10 + ], + [ + "cAaaacaa", + "ABB", + 14 + ], + [ + "cAaacB", + "AaCBbACB", + 10 + ], + [ + "cAaaca", + "bAbbc", + 8 + ], + [ + "cAabACa", + "bcaabaaAb", + 9 + ], + [ + "cAabCA", + "CbACAAcb", + 11 + ], + [ + "cAaba", + "a", + 8 + ], + [ + "cAabcacaa", + "aA", + 15 + ], + [ + "cAacAaBBC", + "bBaBAbcaC", + 12 + ], + [ + "cAaca", + "AcacAA", + 7 + ], + [ + "cAaca", + "BcCcBBBA", + 13 + ], + [ + "cAacc", + "BBCaCA", + 9 + ], + [ + "cAacc", + "a", + 8 + ], + [ + "cAacccAb", + "ccbbBACA", + 13 + ], + [ + "cAb", + "B", + 5 + ], + [ + "cAb", + "BAbABAC", + 10 + ], + [ + "cAb", + "BBAAc", + 8 + ], + [ + "cAb", + "CBABCa", + 8 + ], + [ + "cAb", + "CbBBaA", + 10 + ], + [ + "cAb", + "caBACCa", + 10 + ], + [ + "cAbA", + "Cacc", + 6 + ], + [ + "cAbA", + "aAAACA", + 8 + ], + [ + "cAbABbab", + "aCA", + 13 + ], + [ + "cAbAC", + "BAAc", + 5 + ], + [ + "cAbACB", + "bAcCC", + 8 + ], + [ + "cAbACcCaa", + "cBa", + 13 + ], + [ + "cAbAb", + "acBC", + 9 + ], + [ + "cAbAbA", + "bACaC", + 9 + ], + [ + "cAbAbAAAa", + "A", + 16 + ], + [ + "cAbBB", + "AbaaC", + 8 + ], + [ + "cAbBaAAAA", + "BabaaBAa", + 9 + ], + [ + "cAbBaB", + "AcB", + 8 + ], + [ + "cAbaBbBC", + "a", + 14 + ], + [ + "cAbab", + "Ba", + 7 + ], + [ + "cAbabb", + "bC", + 10 + ], + [ + "cAbbB", + "aBBcaAA", + 13 + ], + [ + "cAbbBCAcB", + "aabbaC", + 11 + ], + [ + "cAbbBaca", + "CbCAcBaC", + 12 + ], + [ + "cAbbBbcC", + "CB", + 13 + ], + [ + "cAbbb", + "cCbaBAAAc", + 13 + ], + [ + "cAbcAcBA", + "AcA", + 10 + ], + [ + "cAbcaCccC", + "ABbAbbcB", + 12 + ], + [ + "cAbcbB", + "AAABaac", + 11 + ], + [ + "cAbccBCBc", + "AAACAcbAc", + 12 + ], + [ + "cAc", + "AbBCcaBB", + 13 + ], + [ + "cAc", + "BCCcCaBac", + 13 + ], + [ + "cAc", + "CB", + 5 + ], + [ + "cAc", + "bBb", + 6 + ], + [ + "cAc", + "bCaCaCb", + 11 + ], + [ + "cAc", + "bcb", + 6 + ], + [ + "cAc", + "cAAcaa", + 6 + ], + [ + "cAcA", + "CaaB", + 6 + ], + [ + "cAcAA", + "cbAcBbB", + 8 + ], + [ + "cAcABAAc", + "aABaBBc", + 9 + ], + [ + "cAcACcBb", + "cacbCccC", + 7 + ], + [ + "cAcAb", + "aBbAA", + 8 + ], + [ + "cAcAcAcCc", + "caaAAcbc", + 7 + ], + [ + "cAcAcCB", + "ccbcbBbC", + 10 + ], + [ + "cAcBBaBCb", + "C", + 16 + ], + [ + "cAcBCb", + "abCbAAB", + 11 + ], + [ + "cAcBCcBba", + "A", + 16 + ], + [ + "cAcBbA", + "aBabBACbB", + 13 + ], + [ + "cAcBbB", + "ABBcacBBA", + 10 + ], + [ + "cAcCAB", + "CBBbc", + 11 + ], + [ + "cAcCBCa", + "CCA", + 9 + ], + [ + "cAcCBcBBa", + "CacACBBBB", + 8 + ], + [ + "cAcCCBCBA", + "ccCcbcbc", + 8 + ], + [ + "cAcCbCcAB", + "caC", + 13 + ], + [ + "cAcCcbaaA", + "CcBaa", + 9 + ], + [ + "cAca", + "BAcbCbC", + 10 + ], + [ + "cAcaAa", + "caAAaab", + 7 + ], + [ + "cAcaBAAC", + "BBB", + 14 + ], + [ + "cAcaaa", + "BaBaBC", + 9 + ], + [ + "cAcaaca", + "aaCBA", + 10 + ], + [ + "cAcabCbCB", + "BacBcbABb", + 11 + ], + [ + "cAcabaB", + "Cc", + 11 + ], + [ + "cAcac", + "acbA", + 7 + ], + [ + "cAcbABCBB", + "aABc", + 12 + ], + [ + "cAcbBCBac", + "bCbAAacB", + 13 + ], + [ + "cAcbbccAc", + "AABAaBbCb", + 16 + ], + [ + "cAcc", + "aCa", + 6 + ], + [ + "cAcc", + "bCAAcaCC", + 10 + ], + [ + "cAcc", + "cc", + 4 + ], + [ + "cAccCB", + "aAABca", + 9 + ], + [ + "cAccaBCAb", + "BBcBCCAbb", + 12 + ], + [ + "cAccaaABa", + "bCAcB", + 13 + ], + [ + "cAcccAA", + "bCAb", + 11 + ], + [ + "cAcccaCaA", + "abaB", + 14 + ], + [ + "cAcccacab", + "BbCCbaB", + 13 + ], + [ + "cB", + "AbAB", + 6 + ], + [ + "cB", + "AbBbCA", + 10 + ], + [ + "cB", + "B", + 2 + ], + [ + "cB", + "BACbBc", + 9 + ], + [ + "cB", + "BAcB", + 4 + ], + [ + "cB", + "BBaBaaccC", + 16 + ], + [ + "cB", + "BcAaA", + 8 + ], + [ + "cB", + "C", + 3 + ], + [ + "cB", + "CAA", + 5 + ], + [ + "cB", + "CAAA", + 7 + ], + [ + "cB", + "CAacabCCC", + 15 + ], + [ + "cB", + "CBBB", + 5 + ], + [ + "cB", + "CCBCABaaA", + 15 + ], + [ + "cB", + "Cc", + 3 + ], + [ + "cB", + "CcBAc", + 6 + ], + [ + "cB", + "a", + 4 + ], + [ + "cB", + "aACCba", + 10 + ], + [ + "cB", + "aacbcC", + 9 + ], + [ + "cB", + "acaaAABB", + 12 + ], + [ + "cB", + "b", + 3 + ], + [ + "cB", + "ba", + 4 + ], + [ + "cB", + "baacB", + 6 + ], + [ + "cB", + "bc", + 4 + ], + [ + "cB", + "cABAbCBc", + 12 + ], + [ + "cB", + "cCAB", + 4 + ], + [ + "cB", + "caAAbbBC", + 12 + ], + [ + "cB", + "caBAAa", + 8 + ], + [ + "cBA", + "Ac", + 6 + ], + [ + "cBA", + "CB", + 3 + ], + [ + "cBA", + "a", + 5 + ], + [ + "cBA", + "ac", + 6 + ], + [ + "cBA", + "b", + 5 + ], + [ + "cBA", + "bbaBac", + 9 + ], + [ + "cBA", + "cAaabbab", + 12 + ], + [ + "cBAA", + "AAB", + 6 + ], + [ + "cBAA", + "aCabbA", + 8 + ], + [ + "cBAAB", + "CCa", + 8 + ], + [ + "cBAABa", + "bA", + 9 + ], + [ + "cBAABccc", + "caBcbAAa", + 13 + ], + [ + "cBAAbCcC", + "Bcc", + 11 + ], + [ + "cBAAcAbAC", + "bBaacb", + 10 + ], + [ + "cBAB", + "cBBcaCCb", + 10 + ], + [ + "cBABaBCc", + "BAABc", + 7 + ], + [ + "cBABaac", + "baCbbb", + 12 + ], + [ + "cBABbBAaA", + "c", + 16 + ], + [ + "cBABba", + "AaAccABac", + 12 + ], + [ + "cBABca", + "aBcB", + 7 + ], + [ + "cBACBAAcA", + "BBCbCCBb", + 13 + ], + [ + "cBACa", + "cbaabBBcA", + 12 + ], + [ + "cBACc", + "cBbBaCb", + 7 + ], + [ + "cBACcBBC", + "AbaaBbcC", + 11 + ], + [ + "cBACcbCaa", + "ab", + 15 + ], + [ + "cBAa", + "cCCBaAc", + 8 + ], + [ + "cBAaBcaB", + "b", + 15 + ], + [ + "cBAacbBa", + "aaBb", + 11 + ], + [ + "cBAbCaBA", + "ccaB", + 9 + ], + [ + "cBAba", + "B", + 8 + ], + [ + "cBAbaa", + "CaccCBA", + 12 + ], + [ + "cBAbb", + "ca", + 7 + ], + [ + "cBAbbCaCc", + "aaCA", + 13 + ], + [ + "cBAbca", + "aaACbA", + 9 + ], + [ + "cBAc", + "BA", + 4 + ], + [ + "cBAc", + "aCCcbbbcC", + 13 + ], + [ + "cBAcAb", + "BCabCbcc", + 13 + ], + [ + "cBAcAbaa", + "AAC", + 12 + ], + [ + "cBAcBBc", + "cCcaaa", + 10 + ], + [ + "cBAcCCb", + "cCcAc", + 9 + ], + [ + "cBAcb", + "CaAAACba", + 10 + ], + [ + "cBB", + "BacacBcCA", + 14 + ], + [ + "cBB", + "Bc", + 4 + ], + [ + "cBB", + "bAACA", + 10 + ], + [ + "cBB", + "bCbBBC", + 7 + ], + [ + "cBBA", + "AcAAcABa", + 11 + ], + [ + "cBBA", + "Cca", + 6 + ], + [ + "cBBABC", + "bCcbaAB", + 9 + ], + [ + "cBBABC", + "cCCcBcBc", + 9 + ], + [ + "cBBAac", + "CbbbcB", + 9 + ], + [ + "cBBBABBA", + "AAAbAAAB", + 13 + ], + [ + "cBBBCA", + "caacccAbA", + 13 + ], + [ + "cBBBCacAB", + "Accab", + 13 + ], + [ + "cBBBcbA", + "cB", + 10 + ], + [ + "cBBC", + "acaC", + 6 + ], + [ + "cBBCBaCbA", + "CBcBaAc", + 10 + ], + [ + "cBBCCAca", + "ABCcCa", + 8 + ], + [ + "cBBCb", + "cBBbaaC", + 8 + ], + [ + "cBBCbB", + "aCbBaCaB", + 8 + ], + [ + "cBBaBaAac", + "C", + 17 + ], + [ + "cBBaaCbBb", + "baCCBa", + 11 + ], + [ + "cBBabaAc", + "b", + 14 + ], + [ + "cBBac", + "ba", + 7 + ], + [ + "cBBacC", + "aBcCAC", + 8 + ], + [ + "cBBbAaabA", + "aaA", + 12 + ], + [ + "cBBbB", + "BCcAACb", + 11 + ], + [ + "cBBbBaBcA", + "bbCCCc", + 13 + ], + [ + "cBBbCbc", + "ACccCBaCB", + 13 + ], + [ + "cBBbbaBcb", + "Cc", + 15 + ], + [ + "cBBbbbb", + "acBbaBca", + 10 + ], + [ + "cBBbbcAC", + "bCaBbbcbB", + 9 + ], + [ + "cBBbbcacb", + "BcCcaaBA", + 13 + ], + [ + "cBBbca", + "BbA", + 7 + ], + [ + "cBBcAccA", + "AcbbAC", + 11 + ], + [ + "cBBcBb", + "BaCb", + 7 + ], + [ + "cBBcaBcB", + "Aa", + 14 + ], + [ + "cBBcbC", + "bcCBacb", + 8 + ], + [ + "cBC", + "cACacBc", + 9 + ], + [ + "cBC", + "cCa", + 4 + ], + [ + "cBCA", + "C", + 6 + ], + [ + "cBCACaCaC", + "ccBBBCaac", + 9 + ], + [ + "cBCAaac", + "cB", + 10 + ], + [ + "cBCAacA", + "aaAcA", + 8 + ], + [ + "cBCAcCccC", + "cbB", + 15 + ], + [ + "cBCAcbC", + "CcAbCbbaA", + 13 + ], + [ + "cBCB", + "bbabCAAbB", + 13 + ], + [ + "cBCBCbCa", + "bcCAaCCa", + 10 + ], + [ + "cBCBacA", + "AcbBaA", + 7 + ], + [ + "cBCBbCBa", + "c", + 14 + ], + [ + "cBCCABcC", + "Baa", + 13 + ], + [ + "cBCCB", + "a", + 10 + ], + [ + "cBCCBcaBa", + "AcAAba", + 13 + ], + [ + "cBCCC", + "A", + 10 + ], + [ + "cBCCcBc", + "ccaBCB", + 9 + ], + [ + "cBCaB", + "BBbAA", + 7 + ], + [ + "cBCab", + "aaABbC", + 10 + ], + [ + "cBCacA", + "abcbAbAa", + 11 + ], + [ + "cBCb", + "aCbCc", + 6 + ], + [ + "cBCb", + "b", + 6 + ], + [ + "cBCb", + "cAA", + 6 + ], + [ + "cBCbCAc", + "ACacAB", + 9 + ], + [ + "cBCbCca", + "aaB", + 13 + ], + [ + "cBCbabA", + "bbBbABB", + 9 + ], + [ + "cBCbbBBbc", + "aCaBBbbb", + 10 + ], + [ + "cBCbcCC", + "ACBbBBc", + 10 + ], + [ + "cBCc", + "AcaCBCb", + 8 + ], + [ + "cBCcAbcac", + "aBcac", + 10 + ], + [ + "cBCcbccaa", + "bBbCaBB", + 13 + ], + [ + "cBa", + "b", + 5 + ], + [ + "cBa", + "bAAaA", + 8 + ], + [ + "cBaAA", + "baaCbca", + 11 + ], + [ + "cBaAAB", + "BcbB", + 8 + ], + [ + "cBaAACacA", + "bBa", + 14 + ], + [ + "cBaABacb", + "ACb", + 11 + ], + [ + "cBaB", + "BabB", + 4 + ], + [ + "cBaB", + "CBBBC", + 5 + ], + [ + "cBaBBAB", + "B", + 12 + ], + [ + "cBaBBCc", + "BacCACa", + 10 + ], + [ + "cBaBaABBB", + "bcc", + 17 + ], + [ + "cBaBbCBC", + "cAAB", + 11 + ], + [ + "cBaBc", + "bbC", + 7 + ], + [ + "cBaC", + "BacB", + 5 + ], + [ + "cBaC", + "bA", + 6 + ], + [ + "cBaCA", + "CAcabbb", + 11 + ], + [ + "cBaCAbCC", + "B", + 14 + ], + [ + "cBaCAccbc", + "bbccAcCBB", + 10 + ], + [ + "cBaCCC", + "ab", + 10 + ], + [ + "cBaCaBC", + "Cb", + 11 + ], + [ + "cBaCab", + "BC", + 8 + ], + [ + "cBaa", + "BAab", + 5 + ], + [ + "cBaaA", + "Bb", + 8 + ], + [ + "cBaaCabBb", + "aCACcCbbb", + 11 + ], + [ + "cBaaaBBac", + "bcaAbBcC", + 10 + ], + [ + "cBaab", + "acAacAbCc", + 11 + ], + [ + "cBab", + "ACCAB", + 7 + ], + [ + "cBabB", + "C", + 9 + ], + [ + "cBabBCaca", + "C", + 16 + ], + [ + "cBabBcab", + "CbcAAAa", + 12 + ], + [ + "cBabCaA", + "aaAbAaac", + 10 + ], + [ + "cBabbBCBb", + "C", + 16 + ], + [ + "cBac", + "A", + 7 + ], + [ + "cBacCAB", + "B", + 12 + ], + [ + "cBacabC", + "aAAaB", + 10 + ], + [ + "cBacac", + "c", + 10 + ], + [ + "cBb", + "BbbA", + 5 + ], + [ + "cBb", + "CAbcC", + 7 + ], + [ + "cBb", + "acAB", + 5 + ], + [ + "cBbACBB", + "bCCCAbbC", + 12 + ], + [ + "cBbACC", + "ACa", + 8 + ], + [ + "cBbAcabB", + "aA", + 14 + ], + [ + "cBbC", + "abAabbbc", + 12 + ], + [ + "cBbCAACc", + "bBB", + 13 + ], + [ + "cBbCAB", + "cbCb", + 5 + ], + [ + "cBbCAa", + "cacaBBAb", + 11 + ], + [ + "cBbCAaA", + "AB", + 12 + ], + [ + "cBbCAb", + "bC", + 8 + ], + [ + "cBbCBAAA", + "AbBbc", + 12 + ], + [ + "cBbCCC", + "BCba", + 8 + ], + [ + "cBbCc", + "ccaCBA", + 8 + ], + [ + "cBbCccaC", + "bA", + 13 + ], + [ + "cBba", + "cb", + 4 + ], + [ + "cBbaACaa", + "cC", + 12 + ], + [ + "cBbaCbc", + "ccaaca", + 9 + ], + [ + "cBbaaac", + "b", + 12 + ], + [ + "cBbaab", + "BbbBcC", + 9 + ], + [ + "cBbaccBA", + "c", + 14 + ], + [ + "cBbb", + "CCcCaa", + 10 + ], + [ + "cBbb", + "abCb", + 5 + ], + [ + "cBbbA", + "A", + 8 + ], + [ + "cBbbA", + "B", + 8 + ], + [ + "cBbbBAAb", + "AAaa", + 13 + ], + [ + "cBbbBBacb", + "cCaCCcAac", + 14 + ], + [ + "cBbbBCaa", + "AaBc", + 13 + ], + [ + "cBbbCa", + "CB", + 9 + ], + [ + "cBbbcBB", + "bAACBBb", + 10 + ], + [ + "cBbcBaCBC", + "abA", + 15 + ], + [ + "cBbcCAC", + "b", + 12 + ], + [ + "cBbcCb", + "caBBbb", + 7 + ], + [ + "cBbcaBAAc", + "bB", + 14 + ], + [ + "cBbcbAAb", + "aba", + 13 + ], + [ + "cBbcbcBB", + "AccaA", + 12 + ], + [ + "cBbccCcc", + "aaCBabcBB", + 15 + ], + [ + "cBbccb", + "caaABcBb", + 9 + ], + [ + "cBc", + "BCaAAAbB", + 14 + ], + [ + "cBc", + "CAAC", + 6 + ], + [ + "cBc", + "aAc", + 4 + ], + [ + "cBc", + "aBaAAc", + 8 + ], + [ + "cBc", + "aabbAc", + 9 + ], + [ + "cBc", + "aac", + 4 + ], + [ + "cBc", + "c", + 4 + ], + [ + "cBcA", + "bC", + 6 + ], + [ + "cBcAAbbcb", + "BAbb", + 10 + ], + [ + "cBcACAA", + "a", + 13 + ], + [ + "cBcACb", + "Bc", + 8 + ], + [ + "cBcAaACbc", + "abbB", + 14 + ], + [ + "cBcBaaC", + "AbC", + 11 + ], + [ + "cBcBbCC", + "aBbCaba", + 11 + ], + [ + "cBcBbCbbA", + "cBa", + 13 + ], + [ + "cBcBbb", + "Acb", + 8 + ], + [ + "cBcCaaAcb", + "baccACCCc", + 13 + ], + [ + "cBcCbcCa", + "aCa", + 12 + ], + [ + "cBcCccaAa", + "BCaCCabbA", + 12 + ], + [ + "cBcaAaC", + "bACBCAB", + 12 + ], + [ + "cBcaAbBCb", + "baCbbaa", + 12 + ], + [ + "cBcaAbc", + "aa", + 11 + ], + [ + "cBcaAcaaA", + "CACbcC", + 14 + ], + [ + "cBcaBcBc", + "aBbBCACc", + 11 + ], + [ + "cBcaCcCA", + "Bcc", + 10 + ], + [ + "cBcabC", + "bAAAabb", + 10 + ], + [ + "cBcbACbBB", + "ACBc", + 12 + ], + [ + "cBcbBAbAC", + "A", + 16 + ], + [ + "cBcbC", + "AAbBcCa", + 10 + ], + [ + "cBcbcaCb", + "cCbcCB", + 6 + ], + [ + "cBcc", + "bAcAcaaba", + 14 + ], + [ + "cBcc", + "cacc", + 2 + ], + [ + "cBccaAac", + "aab", + 12 + ], + [ + "cBccaBCB", + "Caa", + 13 + ], + [ + "cBccacbCA", + "bBCBAca", + 11 + ], + [ + "cBccbc", + "CAbA", + 9 + ], + [ + "cBcccCAC", + "BA", + 12 + ], + [ + "cC", + "A", + 4 + ], + [ + "cC", + "ABCCCa", + 9 + ], + [ + "cC", + "AbAacCAc", + 12 + ], + [ + "cC", + "AccAAABc", + 13 + ], + [ + "cC", + "BBAabcC", + 10 + ], + [ + "cC", + "BBc", + 5 + ], + [ + "cC", + "BBcaACaA", + 12 + ], + [ + "cC", + "BCABBCAaC", + 15 + ], + [ + "cC", + "BaAbcaBc", + 13 + ], + [ + "cC", + "BaBaccb", + 11 + ], + [ + "cC", + "CBA", + 5 + ], + [ + "cC", + "CacCac", + 8 + ], + [ + "cC", + "CcACcacbB", + 14 + ], + [ + "cC", + "CcBbb", + 8 + ], + [ + "cC", + "aACbbCAB", + 13 + ], + [ + "cC", + "aBABCBbc", + 14 + ], + [ + "cC", + "aCCcAAca", + 13 + ], + [ + "cC", + "aaBaBbAA", + 16 + ], + [ + "cC", + "acAb", + 6 + ], + [ + "cC", + "b", + 4 + ], + [ + "cC", + "bA", + 4 + ], + [ + "cC", + "bACBaA", + 10 + ], + [ + "cC", + "bacAcbc", + 11 + ], + [ + "cC", + "bcaCBaB", + 10 + ], + [ + "cC", + "c", + 2 + ], + [ + "cC", + "cA", + 2 + ], + [ + "cC", + "cABAc", + 7 + ], + [ + "cC", + "cAbc", + 5 + ], + [ + "cC", + "cBA", + 4 + ], + [ + "cC", + "cBCAccaCb", + 14 + ], + [ + "cC", + "cabcBCB", + 10 + ], + [ + "cC", + "cccaBa", + 9 + ], + [ + "cCA", + "c", + 4 + ], + [ + "cCAABb", + "ACbaBC", + 7 + ], + [ + "cCAACb", + "bbC", + 10 + ], + [ + "cCAAacBb", + "a", + 14 + ], + [ + "cCAAc", + "ABbCcbBb", + 14 + ], + [ + "cCAAc", + "AC", + 7 + ], + [ + "cCAB", + "AAaCAABab", + 12 + ], + [ + "cCABbAb", + "Ab", + 10 + ], + [ + "cCABbaBaA", + "aacC", + 15 + ], + [ + "cCAC", + "bCbbbbAC", + 10 + ], + [ + "cCACAb", + "AbC", + 10 + ], + [ + "cCACCc", + "cccbAabc", + 9 + ], + [ + "cCACc", + "BbAcaB", + 9 + ], + [ + "cCAa", + "CBA", + 5 + ], + [ + "cCAaabBbA", + "bCCCcacb", + 13 + ], + [ + "cCAac", + "BBA", + 8 + ], + [ + "cCAacBCc", + "CCBBaA", + 11 + ], + [ + "cCAacCCbA", + "bCACCa", + 9 + ], + [ + "cCAbA", + "aBBcaccbA", + 11 + ], + [ + "cCAbABC", + "bbACCaACB", + 13 + ], + [ + "cCAbACcc", + "AbBAccbb", + 11 + ], + [ + "cCAbCacAA", + "BcABBAC", + 12 + ], + [ + "cCAbbaA", + "CcACabA", + 8 + ], + [ + "cCAbcA", + "BABABC", + 10 + ], + [ + "cCAcAcbaB", + "BccBaCC", + 13 + ], + [ + "cCAcCAB", + "bcccCbbab", + 11 + ], + [ + "cCAcaabBA", + "bBcbCCBa", + 13 + ], + [ + "cCAcbBBa", + "AaBca", + 10 + ], + [ + "cCAcbcB", + "c", + 12 + ], + [ + "cCB", + "BBCAbba", + 11 + ], + [ + "cCB", + "BCcab", + 7 + ], + [ + "cCB", + "CC", + 3 + ], + [ + "cCB", + "CbBa", + 5 + ], + [ + "cCB", + "a", + 6 + ], + [ + "cCB", + "aC", + 4 + ], + [ + "cCBA", + "abcA", + 6 + ], + [ + "cCBACb", + "abBccaC", + 11 + ], + [ + "cCBAbCCa", + "a", + 14 + ], + [ + "cCBAbaC", + "aAbaAa", + 10 + ], + [ + "cCBAcCB", + "CbaCB", + 6 + ], + [ + "cCBBAb", + "cBcaC", + 7 + ], + [ + "cCBBBc", + "Cc", + 8 + ], + [ + "cCBBaa", + "CBC", + 8 + ], + [ + "cCBCBAc", + "ABCBCBbCB", + 9 + ], + [ + "cCBCa", + "AcacaBCAc", + 10 + ], + [ + "cCBCbB", + "Cc", + 9 + ], + [ + "cCBCcbB", + "cCBaAaBc", + 8 + ], + [ + "cCBaAac", + "C", + 12 + ], + [ + "cCBaCc", + "CcbBBbB", + 10 + ], + [ + "cCBac", + "bb", + 9 + ], + [ + "cCBbAC", + "abAbC", + 8 + ], + [ + "cCBbAaabb", + "BCc", + 16 + ], + [ + "cCBbAcCAC", + "acBCCbA", + 12 + ], + [ + "cCBbAccB", + "abA", + 12 + ], + [ + "cCBbbB", + "bbaaCCBbB", + 11 + ], + [ + "cCBc", + "ABbacCc", + 10 + ], + [ + "cCBc", + "cAcBb", + 5 + ], + [ + "cCBcABc", + "ac", + 11 + ], + [ + "cCBcACBa", + "cACcB", + 10 + ], + [ + "cCBcACCa", + "BACAAaa", + 11 + ], + [ + "cCBca", + "cbCacC", + 6 + ], + [ + "cCBcb", + "BCbAABAba", + 12 + ], + [ + "cCBcbCaCb", + "cbBCa", + 10 + ], + [ + "cCC", + "BbacaAb", + 12 + ], + [ + "cCC", + "CB", + 4 + ], + [ + "cCC", + "accaCCCcb", + 12 + ], + [ + "cCC", + "bAbcCcbcb", + 13 + ], + [ + "cCC", + "bB", + 6 + ], + [ + "cCC", + "bCBbABBCc", + 14 + ], + [ + "cCC", + "baAAb", + 10 + ], + [ + "cCC", + "cABBc", + 7 + ], + [ + "cCC", + "cCCbaac", + 8 + ], + [ + "cCCAbbbaa", + "BccccA", + 15 + ], + [ + "cCCAc", + "ccAbbb", + 9 + ], + [ + "cCCBBaBC", + "bcaA", + 13 + ], + [ + "cCCBCAaA", + "baB", + 13 + ], + [ + "cCCBc", + "bAccaAcB", + 11 + ], + [ + "cCCCC", + "cBC", + 6 + ], + [ + "cCCCaA", + "cCbB", + 8 + ], + [ + "cCCCaac", + "BCAcCb", + 11 + ], + [ + "cCCCbA", + "CcBBBBb", + 11 + ], + [ + "cCCCbb", + "ABccCcA", + 10 + ], + [ + "cCCCbbbAa", + "cA", + 14 + ], + [ + "cCCa", + "AccBc", + 7 + ], + [ + "cCCaA", + "BcACCA", + 6 + ], + [ + "cCCaAAAC", + "aBbcC", + 12 + ], + [ + "cCCaCB", + "CaaAcbCAB", + 12 + ], + [ + "cCCaccA", + "bc", + 12 + ], + [ + "cCCaccAC", + "abAACcCa", + 12 + ], + [ + "cCCb", + "C", + 6 + ], + [ + "cCCb", + "cAcBa", + 6 + ], + [ + "cCCbBCBab", + "CCcaca", + 11 + ], + [ + "cCCbb", + "BCBbbcA", + 8 + ], + [ + "cCCbc", + "bCCC", + 5 + ], + [ + "cCCbcbabA", + "aaCCc", + 14 + ], + [ + "cCCcABBB", + "aBBbABcb", + 11 + ], + [ + "cCCcCCcAa", + "bBCab", + 15 + ], + [ + "cCCcaaC", + "aBC", + 10 + ], + [ + "cCa", + "BacAaCAcA", + 13 + ], + [ + "cCa", + "C", + 4 + ], + [ + "cCa", + "CAACAAaC", + 11 + ], + [ + "cCa", + "aBBcbc", + 10 + ], + [ + "cCa", + "aCBab", + 6 + ], + [ + "cCa", + "aaCCAAAb", + 12 + ], + [ + "cCa", + "bBAcCcB", + 10 + ], + [ + "cCa", + "cBBcaCB", + 9 + ], + [ + "cCaA", + "CCaca", + 4 + ], + [ + "cCaA", + "cBcccC", + 9 + ], + [ + "cCaAAACA", + "cBBbacc", + 12 + ], + [ + "cCaAAacCc", + "cbCaBcA", + 12 + ], + [ + "cCaABCAAA", + "A", + 16 + ], + [ + "cCaAb", + "aBaAB", + 5 + ], + [ + "cCaAbAABA", + "CCabcAAbb", + 8 + ], + [ + "cCaAc", + "ABAbbBBB", + 15 + ], + [ + "cCaAcACaa", + "caaBcBc", + 11 + ], + [ + "cCaAcCBca", + "ABcc", + 12 + ], + [ + "cCaAcbA", + "aBCBcCCba", + 12 + ], + [ + "cCaB", + "ABABAbBBc", + 15 + ], + [ + "cCaBBaAb", + "aA", + 12 + ], + [ + "cCaBBaCAC", + "bC", + 15 + ], + [ + "cCaBBba", + "aa", + 10 + ], + [ + "cCaBCbb", + "BcaCB", + 8 + ], + [ + "cCaBb", + "Ac", + 9 + ], + [ + "cCaC", + "AC", + 5 + ], + [ + "cCaCA", + "BBBbbCAC", + 12 + ], + [ + "cCaCBBBCb", + "B", + 16 + ], + [ + "cCaCCB", + "BcBac", + 9 + ], + [ + "cCaCaB", + "AbcA", + 10 + ], + [ + "cCaCaaaA", + "abcCcc", + 14 + ], + [ + "cCaCc", + "aA", + 8 + ], + [ + "cCaa", + "CcCbcbA", + 9 + ], + [ + "cCaaBCb", + "A", + 13 + ], + [ + "cCaab", + "a", + 8 + ], + [ + "cCabB", + "AbbA", + 8 + ], + [ + "cCabB", + "AcaaACCb", + 11 + ], + [ + "cCabC", + "aBcB", + 8 + ], + [ + "cCabaBC", + "aB", + 10 + ], + [ + "cCabc", + "cabcccAB", + 10 + ], + [ + "cCabcBa", + "abccAaAB", + 12 + ], + [ + "cCac", + "AacBAaA", + 10 + ], + [ + "cCacacaa", + "CACbAbcC", + 13 + ], + [ + "cCacba", + "aAbAbBAc", + 13 + ], + [ + "cCb", + "AccbcbcB", + 11 + ], + [ + "cCb", + "BaC", + 6 + ], + [ + "cCb", + "BabCcAA", + 12 + ], + [ + "cCb", + "aCaBbBbC", + 12 + ], + [ + "cCb", + "accCcBAbA", + 12 + ], + [ + "cCb", + "cC", + 2 + ], + [ + "cCbAB", + "bcaac", + 8 + ], + [ + "cCbAB", + "cca", + 6 + ], + [ + "cCbAc", + "baB", + 7 + ], + [ + "cCbAcaB", + "AaBB", + 10 + ], + [ + "cCbBAc", + "bbBBBCa", + 10 + ], + [ + "cCbBBAbC", + "BCa", + 13 + ], + [ + "cCbBa", + "AAcCCaAB", + 11 + ], + [ + "cCbBacc", + "aBAbCAc", + 11 + ], + [ + "cCbBb", + "aacaBa", + 9 + ], + [ + "cCbBb", + "baAAacaCA", + 17 + ], + [ + "cCbBbc", + "acaAbC", + 8 + ], + [ + "cCbC", + "B", + 7 + ], + [ + "cCbC", + "CcBaA", + 7 + ], + [ + "cCbC", + "aC", + 6 + ], + [ + "cCbCCbCaA", + "acBBbCcC", + 12 + ], + [ + "cCbCaCBa", + "aBBAAbaA", + 13 + ], + [ + "cCbCac", + "CBAB", + 8 + ], + [ + "cCbCcaCc", + "cAbbAab", + 10 + ], + [ + "cCbCcbcbA", + "cbb", + 12 + ], + [ + "cCbaACaB", + "AB", + 12 + ], + [ + "cCbaAcaa", + "BCcCcb", + 12 + ], + [ + "cCbaB", + "aCCbc", + 7 + ], + [ + "cCbbAaaba", + "CccBCA", + 14 + ], + [ + "cCbbAbBC", + "acaAcBC", + 9 + ], + [ + "cCbbBC", + "bca", + 10 + ], + [ + "cCbbCB", + "AaBb", + 9 + ], + [ + "cCbbCBc", + "aBBAbba", + 12 + ], + [ + "cCbbCaA", + "A", + 12 + ], + [ + "cCbbabAC", + "B", + 15 + ], + [ + "cCbbccCb", + "bAcCc", + 10 + ], + [ + "cCbcABAa", + "ABaACa", + 11 + ], + [ + "cCbcB", + "bBBcbbA", + 10 + ], + [ + "cCbcbc", + "Aa", + 12 + ], + [ + "cCbccbB", + "abaaBc", + 11 + ], + [ + "cCbccbbaC", + "aCBAcBB", + 11 + ], + [ + "cCc", + "ABcAcB", + 8 + ], + [ + "cCc", + "BbaaC", + 9 + ], + [ + "cCc", + "CAbAabcAc", + 14 + ], + [ + "cCc", + "CccC", + 4 + ], + [ + "cCcA", + "CBaCbc", + 9 + ], + [ + "cCcAAC", + "cCbaCa", + 7 + ], + [ + "cCcAAbbcB", + "A", + 16 + ], + [ + "cCcAB", + "BCCa", + 6 + ], + [ + "cCcACAaCB", + "cccb", + 12 + ], + [ + "cCcAbbBbc", + "CBbaAb", + 12 + ], + [ + "cCcAcAa", + "c", + 12 + ], + [ + "cCcBC", + "CBBAbBAcC", + 13 + ], + [ + "cCcBCcbcb", + "C", + 16 + ], + [ + "cCcC", + "aAcbBCb", + 10 + ], + [ + "cCcCAb", + "bAaCcaCCB", + 11 + ], + [ + "cCcCaCaCB", + "AC", + 15 + ], + [ + "cCcCcB", + "b", + 11 + ], + [ + "cCcaAbCC", + "cAAaCC", + 7 + ], + [ + "cCcaBA", + "C", + 10 + ], + [ + "cCcaBBB", + "BCaCCcB", + 10 + ], + [ + "cCcaC", + "CCAcbCcCc", + 11 + ], + [ + "cCcaCa", + "AAcb", + 10 + ], + [ + "cCcaabba", + "A", + 15 + ], + [ + "cCcabc", + "bc", + 8 + ], + [ + "cCcb", + "BBaAbbaAb", + 16 + ], + [ + "cCcbAbcb", + "AcCbcAAA", + 12 + ], + [ + "cCcbBCCBC", + "cC", + 14 + ], + [ + "cCcbCCB", + "BAbABC", + 12 + ], + [ + "cCcbCaaC", + "cbbcccbca", + 12 + ], + [ + "cCcbaCca", + "baBCABcC", + 13 + ], + [ + "cCcbaba", + "cCBBA", + 7 + ], + [ + "cCcc", + "a", + 8 + ], + [ + "cCccBaa", + "CBcBA", + 7 + ], + [ + "cCccCaCbA", + "caABbAcB", + 13 + ], + [ + "cCccaA", + "abb", + 12 + ], + [ + "cCccbA", + "BBabb", + 10 + ], + [ + "cCccbB", + "abAcB", + 8 + ], + [ + "cCcccAAb", + "aa", + 14 + ], + [ + "cCcccC", + "CBcBaAAc", + 12 + ], + [ + "cCcccCA", + "bcCACb", + 9 + ], + [ + "ca", + "AC", + 4 + ], + [ + "ca", + "ACA", + 4 + ], + [ + "ca", + "ACACb", + 8 + ], + [ + "ca", + "ACa", + 3 + ], + [ + "ca", + "Aaaa", + 6 + ], + [ + "ca", + "BAacabbab", + 14 + ], + [ + "ca", + "BB", + 4 + ], + [ + "ca", + "BBcaAbBcb", + 14 + ], + [ + "ca", + "BacCBC", + 10 + ], + [ + "ca", + "BbA", + 5 + ], + [ + "ca", + "BbBbBC", + 12 + ], + [ + "ca", + "BbCACC", + 10 + ], + [ + "ca", + "Bcbc", + 6 + ], + [ + "ca", + "BccA", + 5 + ], + [ + "ca", + "C", + 3 + ], + [ + "ca", + "CA", + 2 + ], + [ + "ca", + "CAC", + 4 + ], + [ + "ca", + "CCcbBaaab", + 14 + ], + [ + "ca", + "CbCaAaB", + 11 + ], + [ + "ca", + "a", + 2 + ], + [ + "ca", + "aBAA", + 7 + ], + [ + "ca", + "aC", + 4 + ], + [ + "ca", + "aaAbcACCc", + 15 + ], + [ + "ca", + "acccCcBc", + 14 + ], + [ + "ca", + "bAAaCCAc", + 14 + ], + [ + "ca", + "bBcAABbbB", + 15 + ], + [ + "ca", + "bbb", + 6 + ], + [ + "ca", + "cBC", + 4 + ], + [ + "ca", + "cCbba", + 6 + ], + [ + "ca", + "ccBBaba", + 10 + ], + [ + "caA", + "B", + 6 + ], + [ + "caA", + "CcCcAA", + 7 + ], + [ + "caA", + "aCAbCCCA", + 12 + ], + [ + "caA", + "aCcCbbAAC", + 13 + ], + [ + "caA", + "bBbac", + 8 + ], + [ + "caA", + "bCACCACC", + 12 + ], + [ + "caA", + "bbaA", + 4 + ], + [ + "caA", + "bbbBA", + 8 + ], + [ + "caA", + "cbAacAbbc", + 12 + ], + [ + "caAA", + "bAbaa", + 7 + ], + [ + "caAAAB", + "ab", + 9 + ], + [ + "caAAAcaA", + "CacaAcA", + 6 + ], + [ + "caAACbc", + "CBBCABbbb", + 13 + ], + [ + "caAB", + "A", + 6 + ], + [ + "caAB", + "BCAcbbB", + 10 + ], + [ + "caAB", + "CAaAab", + 6 + ], + [ + "caABabc", + "ACbbCCC", + 12 + ], + [ + "caABb", + "AaCaBC", + 7 + ], + [ + "caAC", + "CbCcB", + 8 + ], + [ + "caAC", + "bBBBCCBA", + 14 + ], + [ + "caACABAA", + "aa", + 13 + ], + [ + "caACBa", + "BbbBcAc", + 13 + ], + [ + "caACCBAaa", + "bbAcA", + 13 + ], + [ + "caACCac", + "a", + 12 + ], + [ + "caAa", + "abBcaB", + 10 + ], + [ + "caAa", + "acaCcBc", + 10 + ], + [ + "caAaBaBc", + "bcbCa", + 14 + ], + [ + "caAaa", + "ac", + 8 + ], + [ + "caAaaaaa", + "ccc", + 14 + ], + [ + "caAac", + "ACcACAbc", + 9 + ], + [ + "caAbAAcBb", + "c", + 16 + ], + [ + "caAbAcACB", + "CBCCaAaaB", + 13 + ], + [ + "caAbBBAbC", + "bacC", + 13 + ], + [ + "caAbBCb", + "bbaaaCc", + 11 + ], + [ + "caAbbBa", + "bAC", + 12 + ], + [ + "caAbbb", + "bbCACabaC", + 13 + ], + [ + "caAc", + "baCBB", + 8 + ], + [ + "caAcCc", + "ACc", + 6 + ], + [ + "caAcbbaCB", + "cbcBc", + 12 + ], + [ + "caAcc", + "BBbabc", + 9 + ], + [ + "caAcccaAb", + "Acab", + 10 + ], + [ + "caB", + "CbAcacAa", + 12 + ], + [ + "caB", + "ccBA", + 4 + ], + [ + "caBAA", + "aCbc", + 8 + ], + [ + "caBABbbC", + "CB", + 13 + ], + [ + "caBAabAcC", + "aCA", + 14 + ], + [ + "caBAbAAc", + "bcCBBa", + 12 + ], + [ + "caBAbaAb", + "ba", + 12 + ], + [ + "caBAcB", + "BaaaBA", + 9 + ], + [ + "caBBA", + "Bc", + 8 + ], + [ + "caBBA", + "CacB", + 5 + ], + [ + "caBBaBAB", + "acAbaa", + 11 + ], + [ + "caBBbaaA", + "bB", + 13 + ], + [ + "caBC", + "CBaaBaAa", + 11 + ], + [ + "caBCBaBC", + "aABaB", + 8 + ], + [ + "caBCac", + "b", + 11 + ], + [ + "caBCc", + "CBCabB", + 9 + ], + [ + "caBa", + "CaBabAC", + 7 + ], + [ + "caBa", + "CcbccA", + 9 + ], + [ + "caBaB", + "BcA", + 8 + ], + [ + "caBaCAb", + "AacCAA", + 8 + ], + [ + "caBaCc", + "cCCA", + 8 + ], + [ + "caBb", + "AaCbCAAa", + 12 + ], + [ + "caBb", + "BCBC", + 6 + ], + [ + "caBb", + "b", + 6 + ], + [ + "caBbCaa", + "caCCbb", + 8 + ], + [ + "caBbCbbBB", + "aBCBCabca", + 11 + ], + [ + "caBbCcBC", + "cccBaB", + 11 + ], + [ + "caBbac", + "bac", + 6 + ], + [ + "caBbbB", + "BcBBA", + 9 + ], + [ + "caBbbcBa", + "b", + 14 + ], + [ + "caBc", + "bA", + 7 + ], + [ + "caBc", + "bBA", + 6 + ], + [ + "caBc", + "cc", + 4 + ], + [ + "caBcAA", + "ccacac", + 7 + ], + [ + "caBcacCb", + "bBc", + 12 + ], + [ + "caC", + "BCbb", + 7 + ], + [ + "caC", + "BbBcA", + 9 + ], + [ + "caC", + "babCacB", + 10 + ], + [ + "caC", + "bb", + 6 + ], + [ + "caC", + "cCbAaa", + 8 + ], + [ + "caCAB", + "CBA", + 7 + ], + [ + "caCAC", + "AcaaCaacc", + 10 + ], + [ + "caCAb", + "BaC", + 6 + ], + [ + "caCAccca", + "bACc", + 11 + ], + [ + "caCBA", + "aBBBBcA", + 10 + ], + [ + "caCBC", + "BcBBabA", + 11 + ], + [ + "caCBC", + "ac", + 7 + ], + [ + "caCBba", + "cBA", + 7 + ], + [ + "caCBbaBB", + "a", + 14 + ], + [ + "caCBbc", + "A", + 11 + ], + [ + "caCC", + "CaBC", + 3 + ], + [ + "caCCAa", + "Baabb", + 10 + ], + [ + "caCCAcaa", + "aC", + 12 + ], + [ + "caCCC", + "bCBaAAbC", + 11 + ], + [ + "caCCaa", + "acabCacA", + 7 + ], + [ + "caCCbCc", + "aCBcC", + 7 + ], + [ + "caCCcc", + "cbaCAacc", + 6 + ], + [ + "caCaBB", + "ccbBaBA", + 8 + ], + [ + "caCaBbcAA", + "baCcAB", + 10 + ], + [ + "caCaCAaaC", + "ABB", + 16 + ], + [ + "caCabcBa", + "CAcCABcc", + 10 + ], + [ + "caCb", + "BcAcABAB", + 11 + ], + [ + "caCb", + "bAbBCb", + 7 + ], + [ + "caCbAAcb", + "aaBbAB", + 9 + ], + [ + "caCbCBBb", + "A", + 15 + ], + [ + "caCbbC", + "cCcc", + 7 + ], + [ + "caCbbcbC", + "CBAAbCC", + 10 + ], + [ + "caCbc", + "B", + 9 + ], + [ + "caCcCaaB", + "AacCbcca", + 11 + ], + [ + "caa", + "CCAcAcaab", + 12 + ], + [ + "caa", + "b", + 6 + ], + [ + "caa", + "bAB", + 5 + ], + [ + "caa", + "bacAa", + 5 + ], + [ + "caaA", + "BaCbcC", + 10 + ], + [ + "caaAABaac", + "AC", + 15 + ], + [ + "caaACA", + "cB", + 10 + ], + [ + "caaAa", + "CaBaAbcB", + 9 + ], + [ + "caaAa", + "bbC", + 10 + ], + [ + "caaAbbc", + "Aa", + 11 + ], + [ + "caaB", + "bAc", + 7 + ], + [ + "caaB", + "cB", + 4 + ], + [ + "caaBA", + "C", + 9 + ], + [ + "caaBAabC", + "BACB", + 11 + ], + [ + "caaBCC", + "Ab", + 10 + ], + [ + "caaBCc", + "CCb", + 9 + ], + [ + "caaCCAa", + "bCccbA", + 11 + ], + [ + "caaCaBcBb", + "aCabaa", + 11 + ], + [ + "caaCbaB", + "CCc", + 11 + ], + [ + "caaCc", + "b", + 10 + ], + [ + "caaCcc", + "cbA", + 9 + ], + [ + "caaa", + "AcCc", + 8 + ], + [ + "caaa", + "bCabaCaa", + 9 + ], + [ + "caaaCb", + "AabC", + 7 + ], + [ + "caaaaC", + "CaaBCCaC", + 7 + ], + [ + "caaab", + "BCC", + 10 + ], + [ + "caaac", + "AaBbACcBa", + 13 + ], + [ + "caab", + "C", + 7 + ], + [ + "caabACbb", + "bb", + 12 + ], + [ + "caabB", + "AbaCbac", + 10 + ], + [ + "caabaABA", + "cbacccaA", + 10 + ], + [ + "caabcbcBB", + "abaAAaC", + 14 + ], + [ + "caac", + "ABb", + 7 + ], + [ + "caacAABaB", + "CbBcbaB", + 10 + ], + [ + "caacaccC", + "BAaccaBA", + 11 + ], + [ + "caacbaAc", + "Abccbc", + 10 + ], + [ + "cab", + "CB", + 4 + ], + [ + "cab", + "a", + 4 + ], + [ + "cab", + "abCCb", + 7 + ], + [ + "cab", + "bAaaBBA", + 11 + ], + [ + "cab", + "bBbCABC", + 11 + ], + [ + "cabAA", + "c", + 8 + ], + [ + "cabACCCCb", + "b", + 16 + ], + [ + "cabACc", + "bBBAb", + 9 + ], + [ + "cabBAaaB", + "BbCCBaBc", + 12 + ], + [ + "cabBC", + "A", + 9 + ], + [ + "cabBCa", + "C", + 10 + ], + [ + "cabBCcBbA", + "BbCbAAaAb", + 16 + ], + [ + "cabBaA", + "cCa", + 8 + ], + [ + "cabC", + "BBCabBCa", + 9 + ], + [ + "cabC", + "BCBb", + 7 + ], + [ + "cabC", + "Baccb", + 7 + ], + [ + "cabCCAcC", + "cCcA", + 9 + ], + [ + "cabCCaBca", + "BCbBc", + 11 + ], + [ + "cabCaba", + "a", + 12 + ], + [ + "cabCb", + "cB", + 7 + ], + [ + "cabCcbB", + "BBbAcBaac", + 13 + ], + [ + "cabaBBb", + "AbC", + 11 + ], + [ + "cabaa", + "ccBCAbAB", + 10 + ], + [ + "cababBBaB", + "AcBCAC", + 14 + ], + [ + "cabac", + "BCa", + 8 + ], + [ + "cabb", + "CACcCcac", + 14 + ], + [ + "cabb", + "a", + 6 + ], + [ + "cabbBBA", + "CCab", + 11 + ], + [ + "cabbBc", + "aCccAAcc", + 12 + ], + [ + "cabbbACA", + "bcaCC", + 11 + ], + [ + "cabc", + "BCBcBca", + 10 + ], + [ + "cabc", + "CaaBBAABA", + 14 + ], + [ + "cabcBCc", + "BABAC", + 10 + ], + [ + "cabca", + "aAaC", + 8 + ], + [ + "cac", + "BBbaabba", + 14 + ], + [ + "cac", + "bb", + 6 + ], + [ + "cac", + "bcBa", + 6 + ], + [ + "cac", + "cBaaBa", + 8 + ], + [ + "cacABBaB", + "BaBAabBbA", + 11 + ], + [ + "cacAaA", + "bc", + 10 + ], + [ + "cacAaaaBb", + "Aba", + 14 + ], + [ + "cacAbbcCB", + "AcCcAaBC", + 11 + ], + [ + "cacAc", + "cAcaAB", + 5 + ], + [ + "cacB", + "BCcbAAcbB", + 11 + ], + [ + "cacB", + "c", + 6 + ], + [ + "cacBAA", + "BbbCCab", + 12 + ], + [ + "cacBAc", + "bbaBB", + 10 + ], + [ + "cacBaAa", + "aBBBCbbbA", + 15 + ], + [ + "cacBab", + "AC", + 10 + ], + [ + "cacCCaB", + "ABCc", + 10 + ], + [ + "caca", + "bCbbAa", + 9 + ], + [ + "cacaAbAAA", + "bc", + 16 + ], + [ + "cacaAcc", + "bc", + 12 + ], + [ + "cacaBc", + "BaabCbbB", + 12 + ], + [ + "cacaaBc", + "caAacCAAa", + 11 + ], + [ + "cacaabBCa", + "AACaccAc", + 13 + ], + [ + "cacacBAbC", + "C", + 16 + ], + [ + "cacaca", + "aB", + 10 + ], + [ + "cacbAAaCa", + "b", + 16 + ], + [ + "cacbB", + "AbBaCB", + 9 + ], + [ + "cacbCcbcB", + "cc", + 14 + ], + [ + "cacbc", + "BbAACAAC", + 13 + ], + [ + "cacc", + "CAccbBacC", + 11 + ], + [ + "caccA", + "ccc", + 4 + ], + [ + "caccbc", + "ab", + 8 + ], + [ + "cb", + "AAaBCAa", + 13 + ], + [ + "cb", + "AAaC", + 8 + ], + [ + "cb", + "ABAcbc", + 8 + ], + [ + "cb", + "ABccA", + 8 + ], + [ + "cb", + "ACABcb", + 8 + ], + [ + "cb", + "Aa", + 4 + ], + [ + "cb", + "AbBAaB", + 10 + ], + [ + "cb", + "BCcbbACB", + 12 + ], + [ + "cb", + "BabCaAAB", + 14 + ], + [ + "cb", + "BbBCACCBb", + 15 + ], + [ + "cb", + "C", + 3 + ], + [ + "cb", + "CAAbCCB", + 11 + ], + [ + "cb", + "CABBCa", + 10 + ], + [ + "cb", + "CCACAAB", + 12 + ], + [ + "cb", + "aBcA", + 6 + ], + [ + "cb", + "aCBAaabaa", + 15 + ], + [ + "cb", + "aCBaacAac", + 16 + ], + [ + "cb", + "aCC", + 5 + ], + [ + "cb", + "aaBcCC", + 10 + ], + [ + "cb", + "acBAC", + 7 + ], + [ + "cb", + "bCBcaCbA", + 12 + ], + [ + "cb", + "bcCCC", + 8 + ], + [ + "cb", + "cBCaBCC", + 11 + ], + [ + "cb", + "cCabBBabA", + 14 + ], + [ + "cb", + "cb", + 0 + ], + [ + "cb", + "cccCa", + 8 + ], + [ + "cbA", + "AaAaB", + 8 + ], + [ + "cbA", + "AcACCa", + 9 + ], + [ + "cbA", + "BbCBABAA", + 12 + ], + [ + "cbA", + "aBcCCAAca", + 14 + ], + [ + "cbA", + "aaab", + 7 + ], + [ + "cbAA", + "BAbBc", + 8 + ], + [ + "cbAAaBcc", + "cABABCbb", + 10 + ], + [ + "cbAAcB", + "a", + 11 + ], + [ + "cbAAcC", + "cacC", + 5 + ], + [ + "cbAB", + "CAABa", + 5 + ], + [ + "cbABBb", + "acBA", + 9 + ], + [ + "cbABc", + "cbcAaCb", + 7 + ], + [ + "cbAC", + "C", + 6 + ], + [ + "cbACAbc", + "aABCAbA", + 8 + ], + [ + "cbACcAC", + "bbcAcbB", + 10 + ], + [ + "cbACcBBC", + "cABbcbbaB", + 12 + ], + [ + "cbACcb", + "cabbaAC", + 10 + ], + [ + "cbAaB", + "babCC", + 9 + ], + [ + "cbAaBc", + "Acc", + 8 + ], + [ + "cbAaBcCA", + "C", + 14 + ], + [ + "cbAaCBbB", + "A", + 14 + ], + [ + "cbAabcC", + "BBBCAc", + 11 + ], + [ + "cbAbAaBbc", + "bC", + 15 + ], + [ + "cbAbBBAA", + "bcaBbAc", + 9 + ], + [ + "cbAbCBA", + "acABcaC", + 10 + ], + [ + "cbAbCBBB", + "AAaaa", + 14 + ], + [ + "cbAbCaA", + "cbAcBB", + 7 + ], + [ + "cbAbaCc", + "BACabcaa", + 11 + ], + [ + "cbAbcCA", + "ccC", + 8 + ], + [ + "cbAbcaB", + "cc", + 10 + ], + [ + "cbAbcbAbB", + "AccBbbcA", + 13 + ], + [ + "cbAbcbb", + "caCaAB", + 10 + ], + [ + "cbAc", + "BABcCB", + 9 + ], + [ + "cbAc", + "bacb", + 5 + ], + [ + "cbAcCBb", + "ABac", + 10 + ], + [ + "cbAcbcccB", + "CCC", + 15 + ], + [ + "cbB", + "Aaa", + 6 + ], + [ + "cbB", + "CbC", + 3 + ], + [ + "cbB", + "caCaB", + 6 + ], + [ + "cbBA", + "BcBBccBA", + 9 + ], + [ + "cbBA", + "aCcbB", + 6 + ], + [ + "cbBAAc", + "bcABA", + 8 + ], + [ + "cbBAB", + "AbcBc", + 8 + ], + [ + "cbBABc", + "Cbba", + 7 + ], + [ + "cbBACACa", + "aA", + 13 + ], + [ + "cbBACBccA", + "Ca", + 15 + ], + [ + "cbBAba", + "AcAca", + 8 + ], + [ + "cbBAbb", + "CbaaC", + 8 + ], + [ + "cbBB", + "cAb", + 5 + ], + [ + "cbBB", + "cAba", + 5 + ], + [ + "cbBBbB", + "BBBBcaA", + 9 + ], + [ + "cbBBbca", + "cACABCb", + 10 + ], + [ + "cbBBcACBb", + "AACbBbbb", + 13 + ], + [ + "cbBCCaA", + "AccCcBc", + 11 + ], + [ + "cbBCacA", + "cB", + 10 + ], + [ + "cbBCb", + "CAAbBA", + 9 + ], + [ + "cbBa", + "aaCC", + 8 + ], + [ + "cbBa", + "cBaB", + 4 + ], + [ + "cbBa", + "cccB", + 6 + ], + [ + "cbBaBcAc", + "Aab", + 13 + ], + [ + "cbBaCAC", + "CCcCCc", + 10 + ], + [ + "cbBac", + "c", + 8 + ], + [ + "cbBacA", + "bCaaaBaa", + 12 + ], + [ + "cbBb", + "AABA", + 6 + ], + [ + "cbBb", + "CCaa", + 7 + ], + [ + "cbBbAbB", + "cC", + 12 + ], + [ + "cbBbCACCC", + "Ccc", + 14 + ], + [ + "cbBba", + "ccb", + 6 + ], + [ + "cbBbbb", + "BccCbaAAB", + 13 + ], + [ + "cbBc", + "A", + 8 + ], + [ + "cbBcCAa", + "acab", + 11 + ], + [ + "cbBcCCbAA", + "caAbaCaBc", + 14 + ], + [ + "cbBcaCB", + "bA", + 11 + ], + [ + "cbBcbaccC", + "CaAACBAAA", + 16 + ], + [ + "cbBcbc", + "ABBBAAc", + 9 + ], + [ + "cbBcc", + "CACaBC", + 10 + ], + [ + "cbC", + "AcABa", + 7 + ], + [ + "cbC", + "bCCBBa", + 10 + ], + [ + "cbCAbaAa", + "ccBCaC", + 11 + ], + [ + "cbCB", + "AABCCaaA", + 13 + ], + [ + "cbCBABAAb", + "a", + 17 + ], + [ + "cbCBAcacb", + "CAa", + 12 + ], + [ + "cbCBC", + "cbcaCaaba", + 11 + ], + [ + "cbCBccbcc", + "BcCbA", + 11 + ], + [ + "cbCC", + "B", + 7 + ], + [ + "cbCCBBCB", + "baBaCbC", + 11 + ], + [ + "cbCCBccBb", + "BaAaC", + 16 + ], + [ + "cbCCC", + "AACACB", + 8 + ], + [ + "cbCCbABBB", + "CcaABC", + 11 + ], + [ + "cbCCbBac", + "CAaA", + 12 + ], + [ + "cbCa", + "CBA", + 5 + ], + [ + "cbCa", + "bbcacb", + 7 + ], + [ + "cbCaB", + "cbBaCb", + 5 + ], + [ + "cbCbAaCCa", + "ACAAb", + 13 + ], + [ + "cbCbaC", + "BACcBC", + 8 + ], + [ + "cbCbaCcaC", + "AaaaBC", + 14 + ], + [ + "cbCbb", + "A", + 10 + ], + [ + "cbCbbCAAc", + "caccc", + 12 + ], + [ + "cbCbcC", + "B", + 11 + ], + [ + "cbCcAAcA", + "AcaAaCcCa", + 13 + ], + [ + "cbCcCAc", + "babCC", + 9 + ], + [ + "cbCcCB", + "BCABA", + 9 + ], + [ + "cbCcCbA", + "cB", + 11 + ], + [ + "cbCcb", + "AacBB", + 8 + ], + [ + "cbCcbca", + "BCB", + 10 + ], + [ + "cbCccca", + "AAA", + 13 + ], + [ + "cba", + "A", + 5 + ], + [ + "cba", + "b", + 4 + ], + [ + "cba", + "bBCaaCc", + 11 + ], + [ + "cba", + "cAAbc", + 6 + ], + [ + "cba", + "caABABbab", + 12 + ], + [ + "cba", + "ccAB", + 5 + ], + [ + "cbaAa", + "cBbbcB", + 8 + ], + [ + "cbaAc", + "cAAc", + 3 + ], + [ + "cbaBACBAc", + "a", + 16 + ], + [ + "cbaBCAAC", + "BAAcCc", + 12 + ], + [ + "cbaBCbbcC", + "BcAaabac", + 12 + ], + [ + "cbaBbCa", + "BBACcA", + 10 + ], + [ + "cbaC", + "BBCaACabc", + 13 + ], + [ + "cbaC", + "aaA", + 6 + ], + [ + "cbaCABA", + "BbAaBccBC", + 11 + ], + [ + "cbaCCA", + "cbCABA", + 6 + ], + [ + "cbaCCCa", + "a", + 12 + ], + [ + "cbaCacac", + "AcBBbAA", + 13 + ], + [ + "cbaCcaA", + "C", + 12 + ], + [ + "cbaa", + "aaBcCBcBa", + 13 + ], + [ + "cbaaA", + "cBaa", + 3 + ], + [ + "cbaabCcC", + "bB", + 13 + ], + [ + "cbabAA", + "Bc", + 11 + ], + [ + "cbabBbc", + "AbcacAC", + 11 + ], + [ + "cbabC", + "CBbaAa", + 7 + ], + [ + "cbabbC", + "aCacAcBAC", + 13 + ], + [ + "cbac", + "cCbBcACa", + 10 + ], + [ + "cbacAAAA", + "BcAbcAbc", + 12 + ], + [ + "cbacAAacc", + "ab", + 16 + ], + [ + "cbacAC", + "BaaABAbC", + 10 + ], + [ + "cbacACa", + "AcBbcBaC", + 10 + ], + [ + "cbacB", + "CaBAa", + 9 + ], + [ + "cbacCCab", + "Acbc", + 12 + ], + [ + "cbacCabBB", + "CCcc", + 14 + ], + [ + "cbb", + "BBbB", + 5 + ], + [ + "cbb", + "CcAaAC", + 10 + ], + [ + "cbb", + "bBcABAcCC", + 15 + ], + [ + "cbb", + "cbccAbA", + 8 + ], + [ + "cbbABaCBa", + "Ca", + 14 + ], + [ + "cbbAa", + "aBBcAa", + 6 + ], + [ + "cbbAaC", + "cbc", + 7 + ], + [ + "cbbAbB", + "BABaBA", + 9 + ], + [ + "cbbAbaaB", + "CCababBa", + 10 + ], + [ + "cbbBAC", + "AacCcb", + 12 + ], + [ + "cbbBbA", + "AbCAACc", + 12 + ], + [ + "cbbBbb", + "b", + 10 + ], + [ + "cbbBbbbCA", + "bBB", + 13 + ], + [ + "cbbBcBbBb", + "BaABc", + 14 + ], + [ + "cbbBcC", + "C", + 10 + ], + [ + "cbbCAaB", + "A", + 12 + ], + [ + "cbbCbC", + "cBaCCABb", + 10 + ], + [ + "cbbCcbA", + "baCBaBab", + 12 + ], + [ + "cbbaCA", + "aCAbbbCcA", + 9 + ], + [ + "cbbaCb", + "ABaCaB", + 8 + ], + [ + "cbbaaC", + "BcAAbAba", + 11 + ], + [ + "cbbabc", + "BaAaacCAc", + 14 + ], + [ + "cbbacB", + "b", + 10 + ], + [ + "cbbb", + "CC", + 7 + ], + [ + "cbbb", + "bBcCaCAC", + 14 + ], + [ + "cbbbB", + "abacb", + 7 + ], + [ + "cbbbBBcb", + "cA", + 14 + ], + [ + "cbbbaBbC", + "CbAABAC", + 8 + ], + [ + "cbbbc", + "c", + 8 + ], + [ + "cbbc", + "aCBAcAc", + 10 + ], + [ + "cbbc", + "abBBCAB", + 10 + ], + [ + "cbbcBC", + "BAB", + 9 + ], + [ + "cbbcBcCCb", + "bc", + 14 + ], + [ + "cbc", + "AACAAbCCB", + 14 + ], + [ + "cbc", + "BcbaA", + 6 + ], + [ + "cbc", + "CbBbACB", + 10 + ], + [ + "cbc", + "b", + 4 + ], + [ + "cbc", + "cBCcBaC", + 9 + ], + [ + "cbcA", + "ABBAcC", + 9 + ], + [ + "cbcAA", + "c", + 8 + ], + [ + "cbcACAabb", + "bccBcbAaA", + 13 + ], + [ + "cbcAaAAb", + "BBccBbCA", + 13 + ], + [ + "cbcAb", + "CCCbbbcba", + 13 + ], + [ + "cbcB", + "Aba", + 6 + ], + [ + "cbcB", + "bCaaaAba", + 14 + ], + [ + "cbcBacB", + "acbB", + 9 + ], + [ + "cbcBbb", + "cBa", + 8 + ], + [ + "cbcBcc", + "bbCcA", + 7 + ], + [ + "cbcC", + "aABacCCC", + 11 + ], + [ + "cbcCA", + "bBAaba", + 10 + ], + [ + "cbcCBcbc", + "acBa", + 12 + ], + [ + "cbcCcBa", + "CbbACa", + 8 + ], + [ + "cbcaB", + "bCaAAc", + 9 + ], + [ + "cbcabBAac", + "bcBCbCBb", + 13 + ], + [ + "cbcb", + "bba", + 6 + ], + [ + "cbcbCCA", + "AaaCCabcc", + 15 + ], + [ + "cbcbaCBb", + "BccCAb", + 9 + ], + [ + "cbcbbC", + "BAaa", + 11 + ], + [ + "cbcbcaCC", + "BaBbbB", + 14 + ], + [ + "cbcccaCc", + "acAAAa", + 13 + ], + [ + "cc", + "AAAbBbC", + 13 + ], + [ + "cc", + "ABBaccbbB", + 14 + ], + [ + "cc", + "ABa", + 6 + ], + [ + "cc", + "ABaA", + 8 + ], + [ + "cc", + "BAbbAab", + 14 + ], + [ + "cc", + "BAbc", + 6 + ], + [ + "cc", + "BBacCb", + 9 + ], + [ + "cc", + "BCbaB", + 9 + ], + [ + "cc", + "BaBcBAAc", + 12 + ], + [ + "cc", + "BaC", + 5 + ], + [ + "cc", + "Bbca", + 6 + ], + [ + "cc", + "CAbbcb", + 9 + ], + [ + "cc", + "CCAcCb", + 9 + ], + [ + "cc", + "Ccb", + 3 + ], + [ + "cc", + "aAaBCaA", + 13 + ], + [ + "cc", + "aB", + 4 + ], + [ + "cc", + "aBA", + 6 + ], + [ + "cc", + "acCcCabA", + 12 + ], + [ + "cc", + "b", + 4 + ], + [ + "cc", + "bACABABA", + 15 + ], + [ + "cc", + "baCaAAAc", + 13 + ], + [ + "cc", + "bacAbacB", + 12 + ], + [ + "cc", + "bcbA", + 6 + ], + [ + "cc", + "cABcAA", + 8 + ], + [ + "cc", + "cACac", + 6 + ], + [ + "cc", + "cAbba", + 8 + ], + [ + "cc", + "cB", + 2 + ], + [ + "cc", + "cCac", + 4 + ], + [ + "cc", + "cCbbAcaA", + 12 + ], + [ + "cc", + "cb", + 2 + ], + [ + "cc", + "ccBc", + 4 + ], + [ + "cc", + "ccCab", + 6 + ], + [ + "ccA", + "AcCccAcc", + 10 + ], + [ + "ccA", + "CBAaC", + 7 + ], + [ + "ccA", + "b", + 6 + ], + [ + "ccAABB", + "ccCBBbC", + 7 + ], + [ + "ccAAb", + "BaABC", + 8 + ], + [ + "ccAAbaA", + "BaA", + 9 + ], + [ + "ccAAbbbaB", + "AABBCc", + 12 + ], + [ + "ccAB", + "BcA", + 4 + ], + [ + "ccABA", + "BcBBcbBA", + 8 + ], + [ + "ccABBAA", + "bCCac", + 12 + ], + [ + "ccABcAA", + "AABBaBaA", + 11 + ], + [ + "ccAC", + "CAACBCaCA", + 13 + ], + [ + "ccAC", + "bA", + 6 + ], + [ + "ccACAAcb", + "AbabCa", + 12 + ], + [ + "ccACABbAc", + "BCbAb", + 12 + ], + [ + "ccACB", + "CBbabcAC", + 11 + ], + [ + "ccACBAaBa", + "ca", + 14 + ], + [ + "ccACBca", + "Cb", + 11 + ], + [ + "ccACC", + "CcaAba", + 7 + ], + [ + "ccACC", + "aCc", + 6 + ], + [ + "ccACaABcb", + "CBBCCABCc", + 10 + ], + [ + "ccACaBbAC", + "cBaAaB", + 11 + ], + [ + "ccACcAAB", + "AbCbaCAbb", + 13 + ], + [ + "ccACcbb", + "abCcBCb", + 9 + ], + [ + "ccAa", + "bca", + 4 + ], + [ + "ccAa", + "c", + 6 + ], + [ + "ccAaACB", + "BCabaaA", + 11 + ], + [ + "ccAaAbccA", + "AAbAAb", + 12 + ], + [ + "ccAaC", + "bAaaACAB", + 11 + ], + [ + "ccAcAC", + "c", + 10 + ], + [ + "ccAcAb", + "AbABBCbBA", + 14 + ], + [ + "ccAcb", + "bcAb", + 4 + ], + [ + "ccAcccc", + "ccCca", + 7 + ], + [ + "ccB", + "B", + 4 + ], + [ + "ccB", + "CcBAB", + 5 + ], + [ + "ccB", + "aAa", + 6 + ], + [ + "ccB", + "b", + 5 + ], + [ + "ccB", + "cAaAbaa", + 11 + ], + [ + "ccB", + "cBbCACaAB", + 13 + ], + [ + "ccBA", + "B", + 6 + ], + [ + "ccBA", + "CAac", + 7 + ], + [ + "ccBACbca", + "Abb", + 12 + ], + [ + "ccBAc", + "baaaCaaB", + 14 + ], + [ + "ccBB", + "BcC", + 6 + ], + [ + "ccBBA", + "BbACCaA", + 11 + ], + [ + "ccBBbAa", + "b", + 12 + ], + [ + "ccBBbb", + "cBbBb", + 4 + ], + [ + "ccBBbcC", + "CAbacA", + 10 + ], + [ + "ccBC", + "AaBAA", + 8 + ], + [ + "ccBC", + "Baba", + 7 + ], + [ + "ccBC", + "b", + 7 + ], + [ + "ccBC", + "cbba", + 5 + ], + [ + "ccBCAaBc", + "bBCCCAA", + 12 + ], + [ + "ccBCAbC", + "abC", + 9 + ], + [ + "ccBCBBcac", + "BCabBA", + 12 + ], + [ + "ccBCC", + "AbbC", + 7 + ], + [ + "ccBCCCa", + "BaCaB", + 10 + ], + [ + "ccBCCaBcB", + "AcC", + 14 + ], + [ + "ccBCCc", + "aBb", + 10 + ], + [ + "ccBCbBcC", + "ca", + 14 + ], + [ + "ccBCcCaA", + "bcbBAcaBA", + 10 + ], + [ + "ccBa", + "ACCaABcCa", + 12 + ], + [ + "ccBaC", + "AC", + 7 + ], + [ + "ccBaaAa", + "Cc", + 11 + ], + [ + "ccBab", + "BACC", + 9 + ], + [ + "ccBabcaBA", + "CBbc", + 11 + ], + [ + "ccBacCAbB", + "cCCcA", + 11 + ], + [ + "ccBbAABB", + "ccb", + 10 + ], + [ + "ccBbAaB", + "Cc", + 11 + ], + [ + "ccBbaCAb", + "Bcc", + 13 + ], + [ + "ccBbab", + "cCbBc", + 7 + ], + [ + "ccBbbbcB", + "Aacaaa", + 16 + ], + [ + "ccBcb", + "bbcACCb", + 9 + ], + [ + "ccC", + "Ab", + 6 + ], + [ + "ccC", + "CBcCaAbAc", + 13 + ], + [ + "ccC", + "aaCB", + 6 + ], + [ + "ccC", + "bBBAA", + 10 + ], + [ + "ccCAA", + "aCC", + 7 + ], + [ + "ccCAcCB", + "baAAbBac", + 14 + ], + [ + "ccCBAa", + "AABbcA", + 10 + ], + [ + "ccCBCc", + "caBcB", + 7 + ], + [ + "ccCBbC", + "BbbA", + 9 + ], + [ + "ccCC", + "A", + 8 + ], + [ + "ccCC", + "CCaBcACBC", + 11 + ], + [ + "ccCC", + "aab", + 8 + ], + [ + "ccCCAaa", + "B", + 14 + ], + [ + "ccCCBAaaC", + "Bb", + 16 + ], + [ + "ccCCBbCaA", + "AcACCaB", + 10 + ], + [ + "ccCCCaaCb", + "BaAbBB", + 16 + ], + [ + "ccCCacA", + "ACAAa", + 10 + ], + [ + "ccCa", + "acbBa", + 6 + ], + [ + "ccCaa", + "aCBCa", + 7 + ], + [ + "ccCab", + "CCAb", + 4 + ], + [ + "ccCbBB", + "AAcabCa", + 10 + ], + [ + "ccCba", + "aacAbCBb", + 11 + ], + [ + "ccCbbCBA", + "AaCBcAb", + 12 + ], + [ + "ccCbc", + "aCbBCcb", + 10 + ], + [ + "ccCbcCcA", + "aAA", + 14 + ], + [ + "ccCbcaaAC", + "abB", + 16 + ], + [ + "ccCcAbCA", + "AB", + 13 + ], + [ + "ccCcAccA", + "CCABcCCbc", + 13 + ], + [ + "ccCcBaA", + "caC", + 10 + ], + [ + "ccCcBbc", + "aaacAca", + 12 + ], + [ + "ccCcBcC", + "CBCCCBAA", + 9 + ], + [ + "ccCcabcb", + "baAb", + 12 + ], + [ + "ccCccbABc", + "b", + 16 + ], + [ + "cca", + "ABc", + 6 + ], + [ + "cca", + "aa", + 4 + ], + [ + "cca", + "bACA", + 6 + ], + [ + "ccaA", + "AcBa", + 5 + ], + [ + "ccaA", + "Ca", + 5 + ], + [ + "ccaABaccb", + "AAaaCBb", + 10 + ], + [ + "ccaACAC", + "BAc", + 11 + ], + [ + "ccaAba", + "Ccb", + 7 + ], + [ + "ccaAc", + "B", + 10 + ], + [ + "ccaAcCB", + "ccBB", + 8 + ], + [ + "ccaB", + "BbcB", + 6 + ], + [ + "ccaBC", + "CbbbB", + 8 + ], + [ + "ccaCA", + "ABBbb", + 10 + ], + [ + "ccaCAB", + "cCbcb", + 7 + ], + [ + "ccaCAcAB", + "acbb", + 11 + ], + [ + "ccaCAcBb", + "ccCbb", + 7 + ], + [ + "ccaCCCcCC", + "AACAc", + 13 + ], + [ + "ccaCaABb", + "bA", + 14 + ], + [ + "ccaCc", + "babb", + 8 + ], + [ + "ccaCcAAaB", + "BcAAaB", + 8 + ], + [ + "ccaCcB", + "B", + 10 + ], + [ + "ccaaBCB", + "AAA", + 12 + ], + [ + "ccaaaa", + "Aa", + 9 + ], + [ + "ccaab", + "acbbC", + 8 + ], + [ + "ccaac", + "BCBBb", + 9 + ], + [ + "ccab", + "C", + 7 + ], + [ + "ccab", + "aabBBcABa", + 14 + ], + [ + "ccabaB", + "CACba", + 7 + ], + [ + "ccabaCa", + "ccAac", + 6 + ], + [ + "ccababcA", + "abc", + 10 + ], + [ + "ccacAbBBB", + "Cb", + 15 + ], + [ + "ccacb", + "cbCA", + 7 + ], + [ + "ccb", + "A", + 6 + ], + [ + "ccb", + "Aa", + 6 + ], + [ + "ccb", + "Aca", + 4 + ], + [ + "ccb", + "acBAcb", + 6 + ], + [ + "ccb", + "acBccbAcA", + 12 + ], + [ + "ccbA", + "ba", + 5 + ], + [ + "ccbAAA", + "BAaca", + 9 + ], + [ + "ccbAACAc", + "CaaCBcBAa", + 14 + ], + [ + "ccbABAA", + "BBaccbb", + 13 + ], + [ + "ccbACa", + "cabACc", + 4 + ], + [ + "ccbAaCA", + "cCccaCCA", + 7 + ], + [ + "ccbAb", + "bbbCaACa", + 12 + ], + [ + "ccbAbA", + "cbcaBb", + 8 + ], + [ + "ccbAbBB", + "cBcbA", + 8 + ], + [ + "ccbAcC", + "AbBCcaacB", + 12 + ], + [ + "ccbBbcb", + "bCbCAC", + 10 + ], + [ + "ccbC", + "ACccBAc", + 8 + ], + [ + "ccbC", + "CB", + 6 + ], + [ + "ccbC", + "aCAA", + 7 + ], + [ + "ccbCC", + "AcCcCcb", + 8 + ], + [ + "ccbCa", + "bcBBcb", + 8 + ], + [ + "ccbCaACca", + "BBcaA", + 12 + ], + [ + "ccbCbbB", + "b", + 12 + ], + [ + "ccba", + "CAbbBc", + 9 + ], + [ + "ccba", + "Caa", + 5 + ], + [ + "ccba", + "bA", + 5 + ], + [ + "ccba", + "cbc", + 4 + ], + [ + "ccbabbC", + "AB", + 12 + ], + [ + "ccbbBbAb", + "CBc", + 13 + ], + [ + "ccbbabB", + "bCcaCa", + 11 + ], + [ + "ccbbcB", + "ACbcaaA", + 11 + ], + [ + "ccbbccCAC", + "CcccBcBC", + 10 + ], + [ + "ccbcABBCb", + "aAcAacC", + 12 + ], + [ + "ccbcAbBbc", + "a", + 17 + ], + [ + "ccbcB", + "CBaCAAcBc", + 12 + ], + [ + "ccbcBBAbB", + "cBccCc", + 13 + ], + [ + "ccbcCaab", + "cCaAAB", + 10 + ], + [ + "ccbcCbcB", + "CCCa", + 12 + ], + [ + "ccbcba", + "CA", + 10 + ], + [ + "ccc", + "AcBcbBcb", + 10 + ], + [ + "ccc", + "BbcCbB", + 9 + ], + [ + "ccc", + "CACABCbb", + 13 + ], + [ + "ccc", + "ac", + 4 + ], + [ + "ccc", + "baAABCbcc", + 13 + ], + [ + "ccc", + "c", + 4 + ], + [ + "ccc", + "cabACBAbB", + 15 + ], + [ + "cccA", + "cbBAbbB", + 10 + ], + [ + "cccAAA", + "AcB", + 10 + ], + [ + "cccAABCcA", + "ba", + 16 + ], + [ + "cccAC", + "ABCaa", + 8 + ], + [ + "cccAaccc", + "bcCC", + 12 + ], + [ + "cccAbC", + "cBbCAb", + 7 + ], + [ + "cccB", + "bACBcb", + 8 + ], + [ + "cccB", + "c", + 6 + ], + [ + "cccBAA", + "cbB", + 8 + ], + [ + "cccBAbb", + "bCCacb", + 9 + ], + [ + "cccBBAC", + "bbbcB", + 12 + ], + [ + "cccBBCc", + "CAaaAb", + 13 + ], + [ + "cccC", + "ACbB", + 7 + ], + [ + "cccC", + "bbbA", + 8 + ], + [ + "cccCabBA", + "AaBc", + 12 + ], + [ + "cccCbbcc", + "CcC", + 11 + ], + [ + "cccCccaC", + "b", + 16 + ], + [ + "ccca", + "cCcCAa", + 5 + ], + [ + "cccaAb", + "Abb", + 9 + ], + [ + "cccaCbA", + "cBcCBBbaA", + 9 + ], + [ + "cccaaa", + "BaABcbcca", + 14 + ], + [ + "cccacB", + "abBB", + 10 + ], + [ + "cccb", + "AABc", + 8 + ], + [ + "cccb", + "CbBAAbA", + 11 + ], + [ + "cccbABcc", + "a", + 15 + ], + [ + "cccbB", + "bcCAcaaba", + 11 + ], + [ + "cccbaBC", + "cbACAac", + 10 + ], + [ + "cccbabBb", + "ACc", + 13 + ], + [ + "cccbbA", + "bbcAc", + 10 + ], + [ + "cccbcaCC", + "AbCACBcCb", + 13 + ], + [ + "cccbcaaaC", + "baCcCc", + 14 + ], + [ + "ccccBa", + "abcbaaCbA", + 13 + ], + [ + "ccccCBcCA", + "Cbbab", + 15 + ], + [ + "ccccCCc", + "aabB", + 14 + ], + [ + "ccccaCA", + "Cb", + 12 + ], + [ + "cccccb", + "a", + 12 + ], + [ + "ccccccAcc", + "AB", + 16 + ] +] \ No newline at end of file diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py index 2c404f6d80b..64c035307e6 100644 --- a/Lib/test/libregrtest/cmdline.py +++ b/Lib/test/libregrtest/cmdline.py @@ -15,73 +15,73 @@ Run Python regression tests. If no arguments or options are provided, finds all files matching -the pattern "test_*" in the Lib/test subdirectory and runs -them in alphabetical order (but see -M and -u, below, for exceptions). +the pattern `test_*` in the `Lib/test` subdirectory and runs +them in alphabetical order (but see `-M` and `-u`, below, for exceptions). For more rigorous testing, it is useful to use the following command line: -python -E -Wd -m test [options] [test_name1 ...] +`python -E -Wd -m test [options] [test_name1 ...]` """ EPILOG = """\ Additional option details: --r randomizes test execution order. You can use --randseed=int to provide an -int seed value for the randomizer. The randseed value will be used +`-r` randomizes test execution order. You can use `--randseed=int` to provide an +int seed value for the randomizer. The `randseed` value will be used to set seeds for all random usages in tests -(including randomizing the tests order if -r is set). +(including randomizing the tests order if `-r` is set). By default we always set random seed, but do not randomize test order. --s On the first invocation of regrtest using -s, the first test file found +`-s` On the first invocation of regrtest using `-s`, the first test file found or the first test file given on the command line is run, and the name of -the next test is recorded in a file named pynexttest. If run from the -Python build directory, pynexttest is located in the 'build' subdirectory, -otherwise it is located in tempfile.gettempdir(). On subsequent runs, -the test in pynexttest is run, and the next test is written to pynexttest. -When the last test has been run, pynexttest is deleted. In this way it +the next test is recorded in a file named `pynexttest`. If run from the +Python build directory, `pynexttest` is located in the 'build' subdirectory, +otherwise it is located in `tempfile.gettempdir()`. On subsequent runs, +the test in `pynexttest` is run, and the next test is written to `pynexttest`. +When the last test has been run, `pynexttest` is deleted. In this way it is possible to single step through the test files. This is useful when doing memory analysis on the Python interpreter, which process tends to consume too many resources to run the full regression test non-stop. --S is used to resume running tests after an interrupted run. It will -maintain the order a standard run (i.e. it assumes -r is not used). +`-S` is used to resume running tests after an interrupted run. It will +maintain the order a standard run (i.e. it assumes `-r` is not used). This is useful after the tests have prematurely stopped for some external reason and you want to resume the run from where you left off rather -than starting from the beginning. Note: this is different from --prioritize. +than starting from the beginning. Note: this is different from `--prioritize`. ---prioritize is used to influence the order of selected tests, such that +`--prioritize` is used to influence the order of selected tests, such that the tests listed as an argument are executed first. This is especially -useful when combined with -j and -r to pin the longest-running tests -to start at the beginning of a test run. Pass --prioritize=test_a,test_b -to make test_a run first, followed by test_b, and then the other tests. -If test_a wasn't selected for execution by regular means, --prioritize will +useful when combined with `-j` and `-r` to pin the longest-running tests +to start at the beginning of a test run. Pass `--prioritize=test_a,test_b` +to make `test_a` run first, followed by `test_b`, and then the other tests. +If test_a wasn't selected for execution by regular means, `--prioritize` will not make it execute. --f reads the names of tests from the file given as f's argument, one +`-f` reads the names of tests from the file given as `f`'s argument, one or more test names per line. Whitespace is ignored. Blank lines and -lines beginning with '#' are ignored. This is especially useful for +lines beginning with `#` are ignored. This is especially useful for whittling down failures involving interactions among tests. --L causes the leaks(1) command to be run just before exit if it exists. -leaks(1) is available on Mac OS X and presumably on some other +`-L` causes the leaks(1) command to be run just before exit if it exists. +leaks(1) is available on macOS and presumably on some other FreeBSD-derived systems. --R runs each test several times and examines sys.gettotalrefcount() to +`-R` runs each test several times and examines `sys.gettotalrefcount()` to see if the test appears to be leaking references. The argument should -be of the form stab:run:fname where 'stab' is the number of times the -test is run to let gettotalrefcount settle down, 'run' is the number -of times further it is run and 'fname' is the name of the file the -reports are written to. These parameters all have defaults (5, 4 and -"reflog.txt" respectively), and the minimal invocation is '-R :'. +be of the form `stab:run:fname` where `stab` is the number of times the +test is run to let gettotalrefcount settle down, `run` is the number +of times further it is run and `fname` is the name of the file the +reports are written to. These parameters all have defaults (`5`, `4` and +`"reflog.txt"` respectively), and the minimal invocation is `-R :`. --M runs tests that require an exorbitant amount of memory. These tests +`-M` runs tests that require an exorbitant amount of memory. These tests typically try to ascertain containers keep working when containing more than 2 billion objects, which only works on 64-bit systems. There are also some tests that try to exhaust the address space of the process, which only makes sense on 32-bit systems with at least 2Gb of memory. The passed-in memlimit, -which is a string in the form of '2.5Gb', determines how much memory the -tests will limit themselves to (but they may go slightly over.) The number +which is a string in the form of `'2.5Gb'`, determines how much memory the +tests will limit themselves to (but they may go slightly over). The number shouldn't be more memory than the machine has (including swap memory). You should also keep in mind that swap memory is generally much, much slower than RAM, and setting memlimit to all available RAM or higher will heavily @@ -90,7 +90,7 @@ to use more than memlimit memory will be skipped. The big-memory tests generally run very, very long. --u is used to specify which special resource intensive tests to run, +`-u` is used to specify which special resource intensive tests to run, such as those requiring large file support or network connectivity. The argument is a comma-separated list of words indicating the resources to test. Currently only the following are defined: @@ -137,16 +137,16 @@ wantobjects - Allows to run Tkinter tests with the specified value of tkinter.wantobjects. -To enable all resources except one, use '-uall,-'. For -example, to run all the tests except for the gui tests, give the -option '-uall,-gui'. +To enable all resources except one, use `-uall,-`. For +example, to run all the tests except for the `gui` tests, give the +option `-uall,-gui`. ---matchfile filters tests using a text file, one pattern per line. +`--matchfile` filters tests using a text file, one pattern per line. Pattern examples: -- test method: test_stat_attributes -- test class: FileTests -- test identifier: test_os.FileTests.test_stat_attributes +- test method: `test_stat_attributes` +- test class: `FileTests` +- test identifier: `test_os.FileTests.test_stat_attributes` """ @@ -245,7 +245,7 @@ def _create_parser(): 'buildbot workers') group.add_argument('--timeout', metavar='TIMEOUT', help='dump the traceback and exit if a test takes ' - 'more than TIMEOUT seconds; disabled if TIMEOUT ' + 'more than `TIMEOUT` seconds; disabled if `TIMEOUT` ' 'is negative or equals to zero') group.add_argument('--wait', action='store_true', help='wait for user input, e.g., allow a debugger ' @@ -261,11 +261,11 @@ def _create_parser(): group = parser.add_argument_group('Verbosity') group.add_argument('-v', '--verbose', action='count', - help='run tests in verbose mode with output to stdout') + help='run tests in verbose mode with output to `stdout`') group.add_argument('-w', '--rerun', action='store_true', help='re-run failed tests in verbose mode') group.add_argument('--verbose2', action='store_true', dest='rerun', - help='deprecated alias to --rerun') + help='deprecated alias to `--rerun`') group.add_argument('-W', '--verbose3', action='store_true', help='display test output on failure') group.add_argument('-q', '--quiet', action='store_true', @@ -295,22 +295,22 @@ def _create_parser(): more_details) group.add_argument('-m', '--match', metavar='PAT', dest='match_tests', action=FilterAction, const=True, - help='match test cases and methods with glob pattern PAT') + help='match test cases and methods with glob pattern `PAT`') group.add_argument('-i', '--ignore', metavar='PAT', dest='match_tests', action=FilterAction, const=False, - help='ignore test cases and methods with glob pattern PAT') + help='ignore test cases and methods with glob pattern `PAT`') group.add_argument('--matchfile', metavar='FILENAME', dest='match_tests', action=FromFileFilterAction, const=True, - help='similar to --match but get patterns from a ' + help='similar to `--match` but get patterns from a ' 'text file, one pattern per line') group.add_argument('--ignorefile', metavar='FILENAME', dest='match_tests', action=FromFileFilterAction, const=False, - help='similar to --matchfile but it receives patterns ' + help='similar to `--matchfile` but it receives patterns ' 'from text file to ignore') group.add_argument('-G', '--failfast', action='store_true', - help='fail as soon as a test fails (only with -v or -W)') + help='fail as soon as a test fails (only with `-v` or `-W`)') group.add_argument('-u', '--use', metavar='RES1,RES2,...', action='extend', type=resources_list, help='specify which special resource intensive tests ' @@ -325,7 +325,7 @@ def _create_parser(): group = parser.add_argument_group('Special runs') group.add_argument('-L', '--runleaks', action='store_true', - help='run the leaks(1) command just before exit.' + + help='run the `leaks(1)` command just before exit.' + more_details) group.add_argument('-R', '--huntrleaks', metavar='RUNCOUNTS', type=huntrleaks, @@ -333,20 +333,20 @@ def _create_parser(): 'very slow).' + more_details) group.add_argument('-j', '--multiprocess', metavar='PROCESSES', dest='use_mp', type=int, - help='run PROCESSES processes at once') + help='run `PROCESSES` processes at once') group.add_argument('--single-process', action='store_true', dest='single_process', help='always run all tests sequentially in ' - 'a single process, ignore -jN option, ' + 'a single process, ignore `-jN` option, ' 'and failed tests are also rerun sequentially ' 'in the same process') group.add_argument('--parallel-threads', metavar='PARALLEL_THREADS', type=int, - help='run copies of each test in PARALLEL_THREADS at ' + help='run copies of each test in `PARALLEL_THREADS` at ' 'once') group.add_argument('-T', '--coverage', action='store_true', dest='trace', - help='turn on code coverage tracing using the trace ' + help='turn on code coverage tracing using the `trace` ' 'module') group.add_argument('-D', '--coverdir', metavar='DIR', type=relative_filename, @@ -356,18 +356,18 @@ def _create_parser(): help='put coverage files alongside modules') group.add_argument('-t', '--threshold', metavar='THRESHOLD', type=int, - help='call gc.set_threshold(THRESHOLD)') + help='call `gc.set_threshold(THRESHOLD)`') group.add_argument('-n', '--nowindows', action='store_true', help='suppress error message boxes on Windows') group.add_argument('-F', '--forever', action='store_true', help='run the specified tests in a loop, until an ' - 'error happens; imply --failfast') + 'error happens; imply `--failfast`') group.add_argument('--list-tests', action='store_true', help="only write the name of tests that will be run, " "don't execute them") group.add_argument('--list-cases', action='store_true', - help='only write the name of test cases that will be run' - ' , don\'t execute them') + help='only write the name of test cases that will be run, ' + 'don\'t execute them') group.add_argument('-P', '--pgo', dest='pgo', action='store_true', help='enable Profile Guided Optimization (PGO) training') group.add_argument('--pgo-extended', action='store_true', @@ -390,9 +390,11 @@ def _create_parser(): group.add_argument('--tempdir', metavar='PATH', help='override the working directory for the test run') group.add_argument('--cleanup', action='store_true', - help='remove old test_python_* directories') + help='remove old `test_python_*` directories') group.add_argument('--bisect', action='store_true', - help='if some tests fail, run test.bisect_cmd on them') + help='if some tests fail, run `test.bisect_cmd` on them') + group.add_argument('--pythoninfo', action='store_true', + help="run `python -m test.pythoninfo` before tests") group.add_argument('--dont-add-python-opts', dest='_add_python_opts', action='store_false', help="internal option, don't use it") diff --git a/Lib/test/libregrtest/findtests.py b/Lib/test/libregrtest/findtests.py index f01c1240774..cd19a7c5c83 100644 --- a/Lib/test/libregrtest/findtests.py +++ b/Lib/test/libregrtest/findtests.py @@ -21,13 +21,16 @@ "test_asyncio", "test_concurrent_futures", "test_doctests", + "test_free_threading", "test_future_stmt", "test_gdb", "test_inspect", - "test_pydoc", + "test_io", "test_multiprocessing_fork", "test_multiprocessing_forkserver", "test_multiprocessing_spawn", + "test_os", + "test_pydoc", } diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py index d8b9605ea49..ac82541059c 100644 --- a/Lib/test/libregrtest/main.py +++ b/Lib/test/libregrtest/main.py @@ -26,7 +26,7 @@ strip_py_suffix, count, format_duration, printlist, get_temp_dir, get_work_dir, exit_timeout, display_header, cleanup_temp_dir, print_warning, - is_cross_compiled, get_host_runner, + is_cross_compiled, get_host_runner, display_title, EXIT_TIMEOUT) @@ -126,6 +126,7 @@ def __init__(self, ns: Namespace, _add_python_opts: bool = False): self.coverage: bool = ns.trace self.coverage_dir: StrPath | None = ns.coverdir self._tmp_dir: StrPath | None = ns.tempdir + self.pythoninfo: bool = ns.pythoninfo # Randomize self.randomize: bool = ns.randomize @@ -322,9 +323,7 @@ def _run_bisect(self, runtests: RunTests, test: str, progress: str) -> bool: title = f"Bisect {test}" if progress: title = f"{title} ({progress})" - print(title) - print("#" * len(title)) - print() + display_title(title) cmd = runtests.create_python_cmd() cmd.extend([ @@ -345,9 +344,7 @@ def _run_bisect(self, runtests: RunTests, test: str, progress: str) -> bool: exitcode = proc.returncode title = f"{title}: exit code {exitcode}" - print(title) - print("#" * len(title)) - print(flush=True) + display_title(title) if exitcode: print(f"Bisect failed with exit code {exitcode}") @@ -752,6 +749,15 @@ def tmp_dir(self) -> StrPath: ) return self._tmp_dir + def run_pythoninfo(self): + from test import pythoninfo + try: + pythoninfo.main() + except SystemExit: + # Ignore non-zero exit code on purpose + pass + print() + def main(self, tests: TestList | None = None) -> NoReturn: if self.want_add_python_opts: self._add_python_opts() @@ -765,6 +771,9 @@ def main(self, tests: TestList | None = None) -> NoReturn: if self.want_wait: input("Press any key to continue...") + if self.pythoninfo: + self.run_pythoninfo() + setup_test_dir(self.test_dir) selected, tests = self.find_tests(tests) diff --git a/Lib/test/libregrtest/refleak.py b/Lib/test/libregrtest/refleak.py index 5c78515506d..e7da17e500e 100644 --- a/Lib/test/libregrtest/refleak.py +++ b/Lib/test/libregrtest/refleak.py @@ -93,6 +93,13 @@ def runtest_refleak(test_name, test_func, for obj in abc.__subclasses__() + [abc]: abcs[obj] = _get_dump(obj)[0] + # `ByteString` is not included in `collections.abc.__all__` + with warnings.catch_warnings(action='ignore', category=DeprecationWarning): + ByteString = collections.abc.ByteString + # Mypy doesn't even think `ByteString` is a class, hence the `type: ignore` + for obj in ByteString.__subclasses__() + [ByteString]: # type: ignore[attr-defined] + abcs[obj] = _get_dump(obj)[0] + # bpo-31217: Integer pool to get a single integer object for the same # value. The pool is used to prevent false alarm when checking for memory # block leaks. Fill the pool with values in -1000..1000 which are the most @@ -254,6 +261,8 @@ def dash_R_cleanup(fs, ps, pic, zdc, abcs, linecache_data): # Clear ABC registries, restoring previously saved ABC registries. abs_classes = [getattr(collections.abc, a) for a in collections.abc.__all__] + with warnings.catch_warnings(action='ignore', category=DeprecationWarning): + abs_classes.append(collections.abc.ByteString) abs_classes = filter(isabstract, abs_classes) for abc in abs_classes: for obj in abc.__subclasses__() + [abc]: diff --git a/Lib/test/libregrtest/run_workers.py b/Lib/test/libregrtest/run_workers.py index 424085a0050..4458e160705 100644 --- a/Lib/test/libregrtest/run_workers.py +++ b/Lib/test/libregrtest/run_workers.py @@ -147,9 +147,12 @@ def _kill(self) -> None: use_killpg = USE_PROCESS_GROUP if use_killpg: - parent_sid = os.getsid(0) - sid = os.getsid(popen.pid) - use_killpg = (sid != parent_sid) + try: + use_killpg = (os.getsid(popen.pid) != os.getsid(0)) + except PermissionError: + # On OpenBSD getsid() is only allowed for a process in the + # same session, so the failure means that it is not. + use_killpg = True if use_killpg: what = f"{self} process group" diff --git a/Lib/test/libregrtest/runtests.py b/Lib/test/libregrtest/runtests.py index e6d34d8e6a3..0a9edce1085 100644 --- a/Lib/test/libregrtest/runtests.py +++ b/Lib/test/libregrtest/runtests.py @@ -159,7 +159,7 @@ def create_python_cmd(self) -> list[str]: if '-u' not in python_opts: cmd.append('-u') # Unbuffered stdout and stderr if self.coverage: - cmd.append("-Xpresite=test.cov") + cmd.append("-Xpresite=test.cov:enable") return cmd def bisect_cmd_args(self) -> list[str]: diff --git a/Lib/test/libregrtest/setup.py b/Lib/test/libregrtest/setup.py index b9b76a44e3b..d62194acd9c 100644 --- a/Lib/test/libregrtest/setup.py +++ b/Lib/test/libregrtest/setup.py @@ -50,6 +50,14 @@ def setup_process() -> None: for signum in signals: faulthandler.register(signum, chain=True, file=stderr_fd) + # Restore the default SIGINT handler if there is no Python-level + # handler. Python inherits the SIG_IGN disposition when the test + # suite runs as a shell background job; then asyncio.Runner does not + # install its own handler and _thread.interrupt_main() is a no-op, + # which makes some tests in test_asyncio and test_threading hang. + if signal.getsignal(signal.SIGINT) in (signal.SIG_IGN, signal.SIG_DFL): + signal.signal(signal.SIGINT, signal.default_int_handler) + adjust_rlimit_nofile() support.record_original_stdout(sys.stdout) diff --git a/Lib/test/libregrtest/tsan.py b/Lib/test/libregrtest/tsan.py index d984a735bdf..c7ec63763bc 100644 --- a/Lib/test/libregrtest/tsan.py +++ b/Lib/test/libregrtest/tsan.py @@ -3,13 +3,12 @@ TSAN_TESTS = [ 'test_asyncio', - # TODO: enable more of test_capi once bugs are fixed (GH-116908, GH-116909). - 'test_capi.test_mem', - 'test_capi.test_pyatomic', + 'test_capi', 'test_code', 'test_ctypes', - # 'test_concurrent_futures', # gh-130605: too many data races + 'test_concurrent_futures', 'test_enum', + 'test_enumerate', 'test_functools', 'test_httpservers', 'test_imaplib', @@ -31,6 +30,7 @@ 'test_threadsignals', 'test_weakref', 'test_free_threading', + 'test_embed', ] # Tests that should be run with `--parallel-threads=N` under TSAN. These tests diff --git a/Lib/test/libregrtest/utils.py b/Lib/test/libregrtest/utils.py index 15caba75acf..99e47cfabf2 100644 --- a/Lib/test/libregrtest/utils.py +++ b/Lib/test/libregrtest/utils.py @@ -118,7 +118,8 @@ def printlist(x, width=70, indent=4, file=None): blanks = ' ' * indent # Print the sorted list: 'x' may be a '--random' list or a set() print(textwrap.fill(' '.join(str(elt) for elt in sorted(x)), width, - initial_indent=blanks, subsequent_indent=blanks), + initial_indent=blanks, subsequent_indent=blanks, + break_long_words=False, break_on_hyphens=False), file=file) @@ -294,6 +295,25 @@ def clear_caches(): else: importlib_metadata.FastPath.__new__.cache_clear() + try: + encodings = sys.modules['encodings'] + except KeyError: + pass + else: + encodings._cache.clear() + + try: + codecs = sys.modules['codecs'] + except KeyError: + pass + else: + # There's no direct API to clear the codecs search cache, but + # `unregister` clears it implicitly. + def noop_search_function(name): + return None + codecs.register(noop_search_function) + codecs.unregister(noop_search_function) + def get_build_info(): # Get most important configure and build options as a list of strings. @@ -727,3 +747,9 @@ def _sanitize_xml_replace(regs): def sanitize_xml(text: str) -> str: return ILLEGAL_XML_CHARS_RE.sub(_sanitize_xml_replace, text) + + +def display_title(title): + print(title) + print("#" * len(title)) + print(flush=True) diff --git a/Lib/test/picklecommon.py b/Lib/test/picklecommon.py index 4c19b6c421f..bb8e41b0149 100644 --- a/Lib/test/picklecommon.py +++ b/Lib/test/picklecommon.py @@ -263,6 +263,17 @@ class MyFrozenSet(frozenset): MyStr, MyUnicode, MyTuple, MyList, MyDict, MySet, MyFrozenSet] +try: + frozendict +except NameError: + # Python 3.14 and older + pass +else: + class MyFrozenDict(dict): + sample = frozendict({"a": 1, "b": 2}) + myclasses.append(MyFrozenDict) + + # For test_newobj_overridden_new class MyIntWithNew(int): def __new__(cls, value): @@ -388,3 +399,48 @@ def pie(self): class Subclass(tuple): class Nested(str): pass + +# For test_private_methods +class PrivateMethods: + def __init__(self, value): + self.value = value + + def __private_method(self): + return self.value + + def get_method(self): + return self.__private_method + + @classmethod + def get_unbound_method(cls): + return cls.__private_method + + @classmethod + def __private_classmethod(cls): + return 43 + + @classmethod + def get_classmethod(cls): + return cls.__private_classmethod + + @staticmethod + def __private_staticmethod(): + return 44 + + @classmethod + def get_staticmethod(cls): + return cls.__private_staticmethod + +# For test_private_nested_classes +class PrivateNestedClasses: + @classmethod + def get_nested(cls): + return cls.__Nested + + class __Nested: + @classmethod + def get_nested2(cls): + return cls.__Nested2 + + class __Nested2: + pass diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index 75e2afa7ae6..83a71b7efed 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -79,6 +79,15 @@ def count_opcode(code, pickle): def identity(x): return x +def itersize(start, stop): + # Produce geometrical increasing sequence from start to stop + # (inclusively) for tests. + size = start + while size < stop: + yield size + size <<= 1 + yield stop + class UnseekableIO(io.BytesIO): def peek(self, *args): @@ -751,9 +760,8 @@ def assert_is_copy(self, obj, objcopy, msg=None): self.assertEqual(getattr(obj, slot, None), getattr(objcopy, slot, None), msg=msg) - def check_unpickling_error(self, errors, data): - with self.subTest(data=data), \ - self.assertRaises(errors): + def check_unpickling_error_strict(self, errors, data): + with self.assertRaises(errors): try: self.loads(data) except BaseException as exc: @@ -762,6 +770,10 @@ def check_unpickling_error(self, errors, data): (data, exc.__class__.__name__, exc)) raise + def check_unpickling_error(self, errors, data): + with self.subTest(data=data): + self.check_unpickling_error_strict(errors, data) + def test_load_from_data0(self): self.assert_is_copy(self._testdata, self.loads(DATA0)) @@ -910,6 +922,16 @@ def test_constants(self): self.assertIs(self.loads(b'I01\n.'), True) self.assertIs(self.loads(b'I00\n.'), False) + def test_issue135241(self): + # C implementation should check for hardcoded values 00 and 01 + # when getting booleans from the INT opcode. Doing a str comparison + # to bypass truthy/falsy comparisons. These payloads should return + # 0, not False. + out1 = self.loads(b'I+0\n.') + self.assertEqual(str(out1), '0') + out2 = self.loads(b'I 0\n.') + self.assertEqual(str(out2), '0') + def test_zero_padded_integers(self): self.assertEqual(self.loads(b'I010\n.'), 10) self.assertEqual(self.loads(b'I-010\n.'), -10) @@ -1038,6 +1060,155 @@ def test_negative_32b_binput(self): dumped = b'\x80\x03X\x01\x00\x00\x00ar\xff\xff\xff\xff.' self.check_unpickling_error(ValueError, dumped) + def test_too_large_put(self): + # Test that PUT with large id does not cause allocation of + # too large memo table. The C implementation uses a dict-based memo + # for sparse indices (when idx > memo_len * 2) instead of allocating + # a massive array. This test verifies large sparse indices work without + # causing memory exhaustion. + # + # The following simple pickle creates an empty list, memoizes it + # using a large index, then loads it back on the stack, builds + # a tuple containing 2 identical empty lists and returns it. + data = lambda n: (b'((lp' + str(n).encode() + b'\n' + + b'g' + str(n).encode() + b'\nt.') + # 0: ( MARK + # 1: ( MARK + # 2: l LIST (MARK at 1) + # 3: p PUT 1000000000000 + # 18: g GET 1000000000000 + # 33: t TUPLE (MARK at 0) + # 34: . STOP + for idx in [10**6, 10**9, 10**12]: + if idx > sys.maxsize: + continue + self.assertEqual(self.loads(data(idx)), ([],)*2) + + def test_too_large_long_binput(self): + # Test that LONG_BINPUT with large id does not cause allocation of + # too large memo table. The C implementation uses a dict-based memo + # for sparse indices (when idx > memo_len * 2) instead of allocating + # a massive array. This test verifies large sparse indices work without + # causing memory exhaustion. + # + # The following simple pickle creates an empty list, memoizes it + # using a large index, then loads it back on the stack, builds + # a tuple containing 2 identical empty lists and returns it. + data = lambda n: (b'(]r' + struct.pack(' sys.maxsize')) + + def test_truncated_large_binunicode8(self): + data = lambda size: b'\x8d' + struct.pack('= 2**63 - 1 and max_memuse >= 2**31: @@ -1327,7 +1415,7 @@ def trace_wrapper(*args, **kwargs): sys.settrace(original_trace) coverage_wrapper = trace_wrapper - if 'test.cov' in sys.modules: # -Xpresite=test.cov used + if 'test.cov' in sys.modules: # -Xpresite=test.cov:enable used cov = sys.monitoring.COVERAGE_ID @functools.wraps(func) def coverage_wrapper(*args, **kwargs): @@ -1351,6 +1439,7 @@ def no_rerun(reason): def deco(func): assert not isinstance(func, type), func _has_run = False + @functools.wraps(func) def wrapper(self): nonlocal _has_run if _has_run: @@ -1390,11 +1479,6 @@ def requires_specialization(test): _opcode.ENABLE_SPECIALIZATION, "requires specialization")(test) -def requires_specialization_ft(test): - return unittest.skipUnless( - _opcode.ENABLE_SPECIALIZATION_FT, "requires specialization")(test) - - def reset_code(f: types.FunctionType) -> types.FunctionType: """Clear all specializations, local instrumentation, and JIT code for the given function.""" f.__code__ = f.__code__.replace() @@ -2749,26 +2833,6 @@ def exceeds_recursion_limit(): return 150_000 -def _has_huge_c_stack(depth): - """Check that *depth* recursive calls cannot exhaust the C stack.""" - try: - from _testinternalcapi import get_c_recursion_remaining - except ImportError: - # Fall back to checking for an unlimited stack size. - if is_emscripten or is_wasi or os.name == "nt": - return False - import resource - soft, hard = resource.getrlimit(resource.RLIMIT_STACK) - return soft == hard and soft in (-1, 0xFFFF_FFFF_FFFF_FFFF) - else: - remaining = get_c_recursion_remaining() - # A negative value means integer overflow in the estimate - # (e.g. with an unlimited RLIMIT_STACK). The estimate is based on - # the size of the interpreter loop frame, so it is only a lower - # bound for recursion with smaller C frames. - return remaining >= depth or remaining < 0 - - def skip_if_huge_c_stack(depth=150_000): """Skip decorator for tests which cannot overflow the C stack. @@ -2776,67 +2840,23 @@ def skip_if_huge_c_stack(depth=150_000): trigger the recursion protection if the C stack is too large (e.g. with a large or unlimited RLIMIT_STACK), and either fail, or run for a very long time, or crash, or consume all memory. - - Prefer run_with_limited_c_stack() for tests recursing to a fixed depth. """ + try: + from _testinternalcapi import get_c_recursion_remaining + except ImportError: + # Fall back to checking for an unlimited stack size. + huge = False + if not (is_emscripten or is_wasi) and os.name != "nt": + import resource + soft, hard = resource.getrlimit(resource.RLIMIT_STACK) + huge = soft == hard and soft in (-1, 0xFFFF_FFFF_FFFF_FFFF) + else: + remaining = get_c_recursion_remaining() + # A negative value means integer overflow in the estimate + # (e.g. with an unlimited RLIMIT_STACK). + huge = remaining >= depth or remaining < 0 return unittest.skipIf( - _has_huge_c_stack(depth), - f"the C stack is large enough for {depth} recursive calls") - - -# Small enough to be exhausted by tens of thousands of recursive calls, -# but not smaller than Py_C_STACK_SIZE (4 MiB) which the interpreter -# assumes if it cannot query the thread stack size. -C_STACK_SIZE = 8 * 1024 * 1024 - - -def run_with_limited_c_stack(depth=150_000, size=C_STACK_SIZE): - """Decorator for tests exhausting the C stack with *depth* recursive calls. - - Run the test in a separate thread with the C stack of *size* bytes, so - that the outcome does not depend on the C stack size of the main thread - (which can be large or unlimited, see RLIMIT_STACK). - - If a thread with the limited C stack cannot be created, run the test in - the current thread, but skip it if the C stack is too large. - """ - reason = f"the C stack is large enough for {depth} recursive calls" - def decorator(test): - @functools.wraps(test) - def wrapper(*args, **kwargs): - def run_test(): - # The C stack can still be too large if limiting it failed. - if _has_huge_c_stack(depth): - raise unittest.SkipTest(reason) - test(*args, **kwargs) - - try: - import threading - old_size = threading.stack_size(size) - except (ImportError, ValueError, RuntimeError): - # Setting the thread stack size is not supported. - return run_test() - - exceptions = [] - def run(): - try: - run_test() - except BaseException as exc: - exceptions.append(exc) - - thread = threading.Thread(target=run) - try: - thread.start() - except RuntimeError: - # Threads are not supported. - return run_test() - finally: - threading.stack_size(old_size) - thread.join() - if exceptions: - raise exceptions[0] - return wrapper - return decorator + huge, f"the C stack is large enough for {depth} recursive calls") # Windows doesn't have os.uname() but it doesn't support s390x. @@ -2916,7 +2936,7 @@ def iter_builtin_types(): # Fall back to making a best-effort guess. if hasattr(object, '__flags__'): # Look for any type object with the Py_TPFLAGS_STATIC_BUILTIN flag set. - import datetime + import datetime # noqa: F401 seen = set() for cls, subs in walk_class_hierarchy(object): if cls in seen: @@ -3052,6 +3072,13 @@ def force_color(color: bool): import _colorize from .os_helper import EnvironmentVarGuard + if color: + try: + import _pyrepl # noqa: F401 + except ModuleNotFoundError: + # Can't force enable color without _pyrepl, so just skip. + raise unittest.SkipTest("_pyrepl is missing") + with ( swap_attr(_colorize, "can_colorize", lambda *, file=None: color), EnvironmentVarGuard() as env, diff --git a/Lib/test/support/channels.py b/Lib/test/support/channels.py index 5352f7d4da3..fab1797659b 100644 --- a/Lib/test/support/channels.py +++ b/Lib/test/support/channels.py @@ -6,8 +6,8 @@ # aliases: from _interpchannels import ( - ChannelError, ChannelNotFoundError, ChannelClosedError, - ChannelEmptyError, ChannelNotEmptyError, + ChannelError, ChannelNotFoundError, ChannelClosedError, # noqa: F401 + ChannelEmptyError, ChannelNotEmptyError, # noqa: F401 ) from concurrent.interpreters._crossinterp import ( UNBOUND_ERROR, UNBOUND_REMOVE, diff --git a/Lib/test/support/isolation.py b/Lib/test/support/isolation.py index bc2189329c0..f449bf44034 100644 --- a/Lib/test/support/isolation.py +++ b/Lib/test/support/isolation.py @@ -163,16 +163,6 @@ def _raise_fixture_outcome(outcome): raise exc from _remote(outcome['detail']) -def _check_returncode(returncode, output, what): - # The subprocess writes its result before exiting, so a non-zero exit code - # means it died afterwards, during finalization, unnoticed by the result. - if returncode: - exc = _SubprocessTestError( - f'the subprocess exited with code {returncode} ' - f'after running the {what}') - raise exc from _remote(output) - - def _isolate_method(func): @functools.wraps(func) def wrapper(self, /, *args, **kwargs): @@ -190,9 +180,7 @@ def wrapper(self, /, *args, **kwargs): raise exc from _remote(output) # The parent measures this method's own duration (the real cost of the # isolated run, subprocess startup included), so nothing to forward here. - # Replay the outcomes first: a failure of the test itself is more useful. _replay_outcomes(self, payload['outcomes']) - _check_returncode(returncode, output, 'test') return wrapper @@ -231,20 +219,13 @@ def setUpClass(cls): by_id.setdefault(outcome['id'], []).append(outcome) cls._isolated_outcomes = by_id cls._isolated_durations = dict(payload.get('durations', ())) - # Report the crash from tearDownClass(), after replaying the outcomes. - cls._isolated_exit = (returncode, output) def tearDownClass(cls): if runningInSubprocess: orig_tearDownClass(cls) - return - cls._isolated_outcomes = None - cls._isolated_durations = None - # Missing if an overriding setUpClass() bypassed the subprocess. - exited = getattr(cls, '_isolated_exit', None) - cls._isolated_exit = None - if exited is not None: - _check_returncode(*exited, 'class') + else: + cls._isolated_outcomes = None + cls._isolated_durations = None def _callSetUp(self): # In the parent the real test does not run, so neither should setUp(). diff --git a/Lib/test/support/strace_helper.py b/Lib/test/support/strace_helper.py index cf95f7bdc7d..bf15283d302 100644 --- a/Lib/test/support/strace_helper.py +++ b/Lib/test/support/strace_helper.py @@ -74,7 +74,7 @@ def sections(self): def _filter_memory_call(call): # mmap can operate on a fd or "MAP_ANONYMOUS" which gives a block of memory. # Ignore "MAP_ANONYMOUS + the "MAP_ANON" alias. - if call.syscall == "mmap" and "MAP_ANON" in call.args[3]: + if call.syscall in ("mmap", "mmap2") and "MAP_ANON" in call.args[3]: return True if call.syscall in ("munmap", "mprotect"): diff --git a/Lib/test/test__colorize.py b/Lib/test/test__colorize.py index 026277267e0..c7bc6914ac1 100644 --- a/Lib/test/test__colorize.py +++ b/Lib/test/test__colorize.py @@ -5,6 +5,7 @@ import unittest import unittest.mock import _colorize +from test.support import cpython_only, import_helper from test.support.os_helper import EnvironmentVarGuard @@ -22,6 +23,15 @@ def supports_virtual_terminal(): return contextlib.nullcontext() +class TestImportTime(unittest.TestCase): + + @cpython_only + def test_lazy_import(self): + import_helper.ensure_lazy_imports( + "_colorize", {"copy", "re", "inspect", "typing"} + ) + + class TestTheme(unittest.TestCase): def test_attributes(self): @@ -40,6 +50,7 @@ def test_copy_with(self): unittest_no_colors = _colorize.Unittest.no_colors() copy = theme.copy_with(unittest=unittest_no_colors) self.assertEqual(copy.argparse, theme.argparse) + self.assertEqual(copy.difflib, theme.difflib) self.assertEqual(copy.syntax, theme.syntax) self.assertEqual(copy.traceback, theme.traceback) self.assertEqual(copy.unittest, unittest_no_colors) diff --git a/Lib/test/test__interpchannels.py b/Lib/test/test__interpchannels.py new file mode 100644 index 00000000000..2b0aba42896 --- /dev/null +++ b/Lib/test/test__interpchannels.py @@ -0,0 +1,1842 @@ +from collections import namedtuple +import contextlib +import sys +from textwrap import dedent +import threading +import time +import unittest + +from test.support import import_helper + +_channels = import_helper.import_module('_interpchannels') +from concurrent.interpreters import _crossinterp +from test.test__interpreters import ( + _interpreters, + _run_output, + clean_up_interpreters, +) + + +REPLACE = _crossinterp._UNBOUND_CONSTANT_TO_FLAG[_crossinterp.UNBOUND] + + +# Additional tests are found in Lib/test/test_interpreters/test_channels.py. +# New tests should be added there. +# XXX The tests here should be moved there. See the note under LowLevelTests. + + +################################## +# helpers + +def recv_wait(cid): + while True: + try: + obj, unboundop = _channels.recv(cid) + except _channels.ChannelEmptyError: + time.sleep(0.1) + else: + assert unboundop is None, repr(unboundop) + return obj + + +def recv_nowait(cid, *args, unbound=False): + obj, unboundop = _channels.recv(cid, *args) + assert (unboundop is None) != unbound, repr(unboundop) + return obj + + +#@contextmanager +#def run_threaded(id, source, **shared): +# def run(): +# run_interp(id, source, **shared) +# t = threading.Thread(target=run) +# t.start() +# yield +# t.join() + + +def run_interp(id, source, **shared): + _run_interp(id, source, shared) + + +def _run_interp(id, source, shared, _mainns={}): + source = dedent(source) + main, *_ = _interpreters.get_main() + if main == id: + cur, *_ = _interpreters.get_current() + if cur != main: + raise RuntimeError + # XXX Run a func? + exec(source, _mainns) + else: + _interpreters.run_string(id, source, shared) + + +class Interpreter(namedtuple('Interpreter', 'name id')): + + @classmethod + def from_raw(cls, raw): + if isinstance(raw, cls): + return raw + elif isinstance(raw, str): + return cls(raw) + else: + raise NotImplementedError + + def __new__(cls, name=None, id=None): + main, *_ = _interpreters.get_main() + if id == main: + if not name: + name = 'main' + elif name != 'main': + raise ValueError( + 'name mismatch (expected "main", got "{}")'.format(name)) + id = main + elif id is not None: + if not name: + name = 'interp' + elif name == 'main': + raise ValueError('name mismatch (unexpected "main")') + assert isinstance(id, int), repr(id) + elif not name or name == 'main': + name = 'main' + id = main + else: + id = _interpreters.create() + self = super().__new__(cls, name, id) + return self + + +# XXX expect_channel_closed() is unnecessary once we improve exc propagation. + +@contextlib.contextmanager +def expect_channel_closed(): + try: + yield + except _channels.ChannelClosedError: + pass + else: + assert False, 'channel not closed' + + +class ChannelAction(namedtuple('ChannelAction', 'action end interp')): + + def __new__(cls, action, end=None, interp=None): + if not end: + end = 'both' + if not interp: + interp = 'main' + self = super().__new__(cls, action, end, interp) + return self + + def __init__(self, *args, **kwargs): + if self.action == 'use': + if self.end not in ('same', 'opposite', 'send', 'recv'): + raise ValueError(self.end) + elif self.action in ('close', 'force-close'): + if self.end not in ('both', 'same', 'opposite', 'send', 'recv'): + raise ValueError(self.end) + else: + raise ValueError(self.action) + if self.interp not in ('main', 'same', 'other', 'extra'): + raise ValueError(self.interp) + + def resolve_end(self, end): + if self.end == 'same': + return end + elif self.end == 'opposite': + return 'recv' if end == 'send' else 'send' + else: + return self.end + + def resolve_interp(self, interp, other, extra): + if self.interp == 'same': + return interp + elif self.interp == 'other': + if other is None: + raise RuntimeError + return other + elif self.interp == 'extra': + if extra is None: + raise RuntimeError + return extra + elif self.interp == 'main': + if interp.name == 'main': + return interp + elif other and other.name == 'main': + return other + else: + raise RuntimeError + # Per __init__(), there aren't any others. + + +class ChannelState(namedtuple('ChannelState', 'pending closed')): + + def __new__(cls, pending=0, *, closed=False): + self = super().__new__(cls, pending, closed) + return self + + def incr(self): + return type(self)(self.pending + 1, closed=self.closed) + + def decr(self): + return type(self)(self.pending - 1, closed=self.closed) + + def close(self, *, force=True): + if self.closed: + if not force or self.pending == 0: + return self + return type(self)(0 if force else self.pending, closed=True) + + +def run_action(cid, action, end, state, *, hideclosed=True): + if state.closed: + if action == 'use' and end == 'recv' and state.pending: + expectfail = False + else: + expectfail = True + else: + expectfail = False + + try: + result = _run_action(cid, action, end, state) + except _channels.ChannelClosedError: + if not hideclosed and not expectfail: + raise + result = state.close() + else: + if expectfail: + raise ... # XXX + return result + + +def _run_action(cid, action, end, state): + if action == 'use': + if end == 'send': + _channels.send(cid, b'spam', blocking=False) + return state.incr() + elif end == 'recv': + if not state.pending: + try: + _channels.recv(cid) + except _channels.ChannelEmptyError: + return state + else: + raise Exception('expected ChannelEmptyError') + else: + recv_nowait(cid) + return state.decr() + else: + raise ValueError(end) + elif action == 'close': + kwargs = {} + if end in ('recv', 'send'): + kwargs[end] = True + _channels.close(cid, **kwargs) + return state.close() + elif action == 'force-close': + kwargs = { + 'force': True, + } + if end in ('recv', 'send'): + kwargs[end] = True + _channels.close(cid, **kwargs) + return state.close(force=True) + else: + raise ValueError(action) + + +def clean_up_channels(): + for cid, _, _ in _channels.list_all(): + try: + _channels.destroy(cid) + except _channels.ChannelNotFoundError: + pass # already destroyed + + +class TestBase(unittest.TestCase): + + def tearDown(self): + clean_up_channels() + clean_up_interpreters() + + +################################## +# channel tests + +class ChannelIDTests(TestBase): + + def test_default_kwargs(self): + cid = _channels._channel_id(10, force=True) + + self.assertEqual(int(cid), 10) + self.assertEqual(cid.end, 'both') + + def test_with_kwargs(self): + cid = _channels._channel_id(10, send=True, force=True) + self.assertEqual(cid.end, 'send') + + cid = _channels._channel_id(10, send=True, recv=False, force=True) + self.assertEqual(cid.end, 'send') + + cid = _channels._channel_id(10, recv=True, force=True) + self.assertEqual(cid.end, 'recv') + + cid = _channels._channel_id(10, recv=True, send=False, force=True) + self.assertEqual(cid.end, 'recv') + + cid = _channels._channel_id(10, send=True, recv=True, force=True) + self.assertEqual(cid.end, 'both') + + def test_coerce_id(self): + class Int(str): + def __index__(self): + return 10 + + cid = _channels._channel_id(Int(), force=True) + self.assertEqual(int(cid), 10) + + def test_bad_id(self): + self.assertRaises(TypeError, _channels._channel_id, object()) + self.assertRaises(TypeError, _channels._channel_id, 10.0) + self.assertRaises(TypeError, _channels._channel_id, '10') + self.assertRaises(TypeError, _channels._channel_id, b'10') + self.assertRaises(ValueError, _channels._channel_id, -1) + self.assertRaises(OverflowError, _channels._channel_id, 2**64) + + def test_bad_kwargs(self): + with self.assertRaises(ValueError): + _channels._channel_id(10, send=False, recv=False) + + def test_does_not_exist(self): + cid = _channels.create(REPLACE) + with self.assertRaises(_channels.ChannelNotFoundError): + _channels._channel_id(int(cid) + 1) # unforced + + def test_str(self): + cid = _channels._channel_id(10, force=True) + self.assertEqual(str(cid), '10') + + def test_repr(self): + cid = _channels._channel_id(10, force=True) + self.assertEqual(repr(cid), 'ChannelID(10)') + + cid = _channels._channel_id(10, send=True, force=True) + self.assertEqual(repr(cid), 'ChannelID(10, send=True)') + + cid = _channels._channel_id(10, recv=True, force=True) + self.assertEqual(repr(cid), 'ChannelID(10, recv=True)') + + cid = _channels._channel_id(10, send=True, recv=True, force=True) + self.assertEqual(repr(cid), 'ChannelID(10)') + + def test_equality(self): + cid1 = _channels.create(REPLACE) + cid2 = _channels._channel_id(int(cid1)) + cid3 = _channels.create(REPLACE) + + self.assertTrue(cid1 == cid1) + self.assertTrue(cid1 == cid2) + self.assertTrue(cid1 == int(cid1)) + self.assertTrue(int(cid1) == cid1) + self.assertTrue(cid1 == float(int(cid1))) + self.assertTrue(float(int(cid1)) == cid1) + self.assertFalse(cid1 == float(int(cid1)) + 0.1) + self.assertFalse(cid1 == str(int(cid1))) + self.assertFalse(cid1 == 2**1000) + self.assertFalse(cid1 == float('inf')) + self.assertFalse(cid1 == 'spam') + self.assertFalse(cid1 == cid3) + + self.assertFalse(cid1 != cid1) + self.assertFalse(cid1 != cid2) + self.assertTrue(cid1 != cid3) + + def test_shareable(self): + chan = _channels.create(REPLACE) + + obj = _channels.create(REPLACE) + _channels.send(chan, obj, blocking=False) + got = recv_nowait(chan) + + self.assertEqual(got, obj) + self.assertIs(type(got), type(obj)) + # XXX Check the following in the channel tests? + #self.assertIsNot(got, obj) + + +class ChannelTests(TestBase): + + def test_create_cid(self): + cid = _channels.create(REPLACE) + self.assertIsInstance(cid, _channels.ChannelID) + + def test_sequential_ids(self): + before = [cid for cid, _, _ in _channels.list_all()] + id1 = _channels.create(REPLACE) + id2 = _channels.create(REPLACE) + id3 = _channels.create(REPLACE) + after = [cid for cid, _, _ in _channels.list_all()] + + self.assertEqual(id2, int(id1) + 1) + self.assertEqual(id3, int(id2) + 1) + self.assertEqual(set(after) - set(before), {id1, id2, id3}) + + def test_channel_list_all_closed(self): + id1 = _channels.create() + id2 = _channels.create() + id3 = _channels.create() + before = _channels.list_all() + expected = [info for info in before if info[0] != id2] + _channels.close(id2, force=True) + after = _channels.list_all() + self.assertEqual(set(after), set(expected)) + self.assertEqual(len(after), len(before) - 1) + + def test_channel_list_all_destroyed(self): + id1 = _channels.create() + id2 = _channels.create() + id3 = _channels.create() + before = _channels.list_all() + expected = [info for info in before if info[0] != id2] + _channels.destroy(id2) + after = _channels.list_all() + self.assertEqual(set(after), set(expected)) + self.assertEqual(len(after), len(before) - 1) + + def test_channel_list_all_released(self): + id1 = _channels.create() + id2 = _channels.create() + id3 = _channels.create() + before = _channels.list_all() + _channels.release(id2, send=True, recv=True) + after = _channels.list_all() + self.assertEqual(set(after), set(before)) + self.assertEqual(len(after), len(before)) + + def test_ids_global(self): + id1 = _interpreters.create() + out = _run_output(id1, dedent(""" + import _interpchannels as _channels + cid = _channels.create(3) + print(cid) + """)) + cid1 = int(out.strip()) + + id2 = _interpreters.create() + out = _run_output(id2, dedent(""" + import _interpchannels as _channels + cid = _channels.create(3) + print(cid) + """)) + cid2 = int(out.strip()) + + self.assertEqual(cid2, int(cid1) + 1) + + def test_channel_list_interpreters_none(self): + """Test listing interpreters for a channel with no associations.""" + # Test for channel with no associated _interpreters. + cid = _channels.create(REPLACE) + send_interps = _channels.list_interpreters(cid, send=True) + recv_interps = _channels.list_interpreters(cid, send=False) + self.assertEqual(send_interps, []) + self.assertEqual(recv_interps, []) + + def test_channel_list_interpreters_basic(self): + """Test basic listing channel _interpreters.""" + interp0, *_ = _interpreters.get_main() + cid = _channels.create(REPLACE) + _channels.send(cid, "send", blocking=False) + # Test for a channel that has one end associated to an interpreter. + send_interps = _channels.list_interpreters(cid, send=True) + recv_interps = _channels.list_interpreters(cid, send=False) + self.assertEqual(send_interps, [interp0]) + self.assertEqual(recv_interps, []) + + interp1 = _interpreters.create() + _run_output(interp1, dedent(f""" + import _interpchannels as _channels + _channels.recv({cid}) + """)) + # Test for channel that has both ends associated to an interpreter. + send_interps = _channels.list_interpreters(cid, send=True) + recv_interps = _channels.list_interpreters(cid, send=False) + self.assertEqual(send_interps, [interp0]) + self.assertEqual(recv_interps, [interp1]) + + def test_channel_list_interpreters_multiple(self): + """Test listing interpreters for a channel with many associations.""" + interp0, *_ = _interpreters.get_main() + interp1 = _interpreters.create() + interp2 = _interpreters.create() + interp3 = _interpreters.create() + cid = _channels.create(REPLACE) + + _channels.send(cid, "send", blocking=False) + _run_output(interp1, dedent(f""" + import _interpchannels as _channels + _channels.send({cid}, "send", blocking=False) + """)) + _run_output(interp2, dedent(f""" + import _interpchannels as _channels + _channels.recv({cid}) + """)) + _run_output(interp3, dedent(f""" + import _interpchannels as _channels + _channels.recv({cid}) + """)) + send_interps = _channels.list_interpreters(cid, send=True) + recv_interps = _channels.list_interpreters(cid, send=False) + self.assertEqual(set(send_interps), {interp0, interp1}) + self.assertEqual(set(recv_interps), {interp2, interp3}) + + def test_channel_list_interpreters_destroyed(self): + """Test listing channel interpreters with a destroyed interpreter.""" + interp0, *_ = _interpreters.get_main() + interp1 = _interpreters.create() + cid = _channels.create(REPLACE) + _channels.send(cid, "send", blocking=False) + _run_output(interp1, dedent(f""" + import _interpchannels as _channels + _channels.recv({cid}) + """)) + # Should be one interpreter associated with each end. + send_interps = _channels.list_interpreters(cid, send=True) + recv_interps = _channels.list_interpreters(cid, send=False) + self.assertEqual(send_interps, [interp0]) + self.assertEqual(recv_interps, [interp1]) + + _interpreters.destroy(interp1) + # Destroyed interpreter should not be listed. + send_interps = _channels.list_interpreters(cid, send=True) + recv_interps = _channels.list_interpreters(cid, send=False) + self.assertEqual(send_interps, [interp0]) + self.assertEqual(recv_interps, []) + + def test_channel_list_interpreters_released(self): + """Test listing channel interpreters with a released channel.""" + # Set up one channel with main interpreter on the send end and two + # subinterpreters on the receive end. + interp0, *_ = _interpreters.get_main() + interp1 = _interpreters.create() + interp2 = _interpreters.create() + cid = _channels.create(REPLACE) + _channels.send(cid, "data", blocking=False) + _run_output(interp1, dedent(f""" + import _interpchannels as _channels + _channels.recv({cid}) + """)) + _channels.send(cid, "data", blocking=False) + _run_output(interp2, dedent(f""" + import _interpchannels as _channels + _channels.recv({cid}) + """)) + # Check the setup. + send_interps = _channels.list_interpreters(cid, send=True) + recv_interps = _channels.list_interpreters(cid, send=False) + self.assertEqual(len(send_interps), 1) + self.assertEqual(len(recv_interps), 2) + + # Release the main interpreter from the send end. + _channels.release(cid, send=True) + # Send end should have no associated _interpreters. + send_interps = _channels.list_interpreters(cid, send=True) + recv_interps = _channels.list_interpreters(cid, send=False) + self.assertEqual(len(send_interps), 0) + self.assertEqual(len(recv_interps), 2) + + # Release one of the subinterpreters from the receive end. + _run_output(interp2, dedent(f""" + import _interpchannels as _channels + _channels.release({cid}) + """)) + # Receive end should have the released interpreter removed. + send_interps = _channels.list_interpreters(cid, send=True) + recv_interps = _channels.list_interpreters(cid, send=False) + self.assertEqual(len(send_interps), 0) + self.assertEqual(recv_interps, [interp1]) + + def test_channel_list_interpreters_closed(self): + """Test listing channel interpreters with a closed channel.""" + interp0, *_ = _interpreters.get_main() + interp1 = _interpreters.create() + cid = _channels.create(REPLACE) + # Put something in the channel so that it's not empty. + _channels.send(cid, "send", blocking=False) + + # Check initial state. + send_interps = _channels.list_interpreters(cid, send=True) + recv_interps = _channels.list_interpreters(cid, send=False) + self.assertEqual(len(send_interps), 1) + self.assertEqual(len(recv_interps), 0) + + # Force close the channel. + _channels.close(cid, force=True) + # Both ends should raise an error. + with self.assertRaises(_channels.ChannelClosedError): + _channels.list_interpreters(cid, send=True) + with self.assertRaises(_channels.ChannelClosedError): + _channels.list_interpreters(cid, send=False) + + def test_channel_list_interpreters_closed_send_end(self): + """Test listing channel interpreters with a channel's send end closed.""" + interp0, *_ = _interpreters.get_main() + interp1 = _interpreters.create() + cid = _channels.create(REPLACE) + # Put something in the channel so that it's not empty. + _channels.send(cid, "send", blocking=False) + + # Check initial state. + send_interps = _channels.list_interpreters(cid, send=True) + recv_interps = _channels.list_interpreters(cid, send=False) + self.assertEqual(len(send_interps), 1) + self.assertEqual(len(recv_interps), 0) + + # Close the send end of the channel. + _channels.close(cid, send=True) + # Send end should raise an error. + with self.assertRaises(_channels.ChannelClosedError): + _channels.list_interpreters(cid, send=True) + # Receive end should not be closed (since channel is not empty). + recv_interps = _channels.list_interpreters(cid, send=False) + self.assertEqual(len(recv_interps), 0) + + # Close the receive end of the channel from a subinterpreter. + _run_output(interp1, dedent(f""" + import _interpchannels as _channels + _channels.close({cid}, force=True) + """)) + return + # Both ends should raise an error. + with self.assertRaises(_channels.ChannelClosedError): + _channels.list_interpreters(cid, send=True) + with self.assertRaises(_channels.ChannelClosedError): + _channels.list_interpreters(cid, send=False) + + def test_allowed_types(self): + cid = _channels.create(REPLACE) + objects = [ + None, + 'spam', + b'spam', + 42, + ] + for obj in objects: + with self.subTest(obj): + _channels.send(cid, obj, blocking=False) + got = recv_nowait(cid) + + self.assertEqual(got, obj) + self.assertIs(type(got), type(obj)) + # XXX Check the following? + #self.assertIsNot(got, obj) + # XXX What about between interpreters? + + def test_run_string_arg_unresolved(self): + cid = _channels.create(REPLACE) + interp = _interpreters.create() + + _interpreters.set___main___attrs(interp, dict(cid=cid.send)) + out = _run_output(interp, dedent(""" + import _interpchannels as _channels + print(cid.end) + _channels.send(cid, b'spam', blocking=False) + """)) + obj = recv_nowait(cid) + + self.assertEqual(obj, b'spam') + self.assertEqual(out.strip(), 'send') + + # XXX For now there is no high-level channel into which the + # sent channel ID can be converted... + # Note: this test caused crashes on some buildbots (bpo-33615). + @unittest.skip('disabled until high-level channels exist') + def test_run_string_arg_resolved(self): + cid = _channels.create(REPLACE) + cid = _channels._channel_id(cid, _resolve=True) + interp = _interpreters.create() + + out = _run_output(interp, dedent(""" + import _interpchannels as _channels + print(chan.id.end) + _channels.send(chan.id, b'spam', blocking=False) + """), + dict(chan=cid.send)) + obj = recv_nowait(cid) + + self.assertEqual(obj, b'spam') + self.assertEqual(out.strip(), 'send') + + #------------------- + # send/recv + + def test_send_recv_main(self): + cid = _channels.create(REPLACE) + orig = b'spam' + _channels.send(cid, orig, blocking=False) + obj = recv_nowait(cid) + + self.assertEqual(obj, orig) + self.assertIsNot(obj, orig) + + def test_send_recv_same_interpreter(self): + id1 = _interpreters.create() + out = _run_output(id1, dedent(""" + import _interpchannels as _channels + cid = _channels.create(REPLACE) + orig = b'spam' + _channels.send(cid, orig, blocking=False) + obj, _ = _channels.recv(cid) + assert obj is not orig + assert obj == orig + """)) + + def test_send_recv_different_interpreters(self): + cid = _channels.create(REPLACE) + id1 = _interpreters.create() + out = _run_output(id1, dedent(f""" + import _interpchannels as _channels + _channels.send({cid}, b'spam', blocking=False) + """)) + obj = recv_nowait(cid) + + self.assertEqual(obj, b'spam') + + def test_send_recv_different_threads(self): + cid = _channels.create(REPLACE) + + def f(): + obj = recv_wait(cid) + _channels.send(cid, obj) + t = threading.Thread(target=f) + t.start() + + _channels.send(cid, b'spam') + obj = recv_wait(cid) + t.join() + + self.assertEqual(obj, b'spam') + + def test_send_recv_different_interpreters_and_threads(self): + cid = _channels.create(REPLACE) + id1 = _interpreters.create() + out = None + + def f(): + nonlocal out + out = _run_output(id1, dedent(f""" + import time + import _interpchannels as _channels + while True: + try: + obj, _ = _channels.recv({cid}) + break + except _channels.ChannelEmptyError: + time.sleep(0.1) + assert(obj == b'spam') + _channels.send({cid}, b'eggs') + """)) + t = threading.Thread(target=f) + t.start() + + _channels.send(cid, b'spam') + obj = recv_wait(cid) + t.join() + + self.assertEqual(obj, b'eggs') + + def test_send_not_found(self): + with self.assertRaises(_channels.ChannelNotFoundError): + _channels.send(10, b'spam') + + def test_recv_not_found(self): + with self.assertRaises(_channels.ChannelNotFoundError): + _channels.recv(10) + + def test_recv_empty(self): + cid = _channels.create(REPLACE) + with self.assertRaises(_channels.ChannelEmptyError): + _channels.recv(cid) + + def test_recv_default(self): + default = object() + cid = _channels.create(REPLACE) + obj1 = recv_nowait(cid, default) + _channels.send(cid, None, blocking=False) + _channels.send(cid, 1, blocking=False) + _channels.send(cid, b'spam', blocking=False) + _channels.send(cid, b'eggs', blocking=False) + obj2 = recv_nowait(cid, default) + obj3 = recv_nowait(cid, default) + obj4 = recv_nowait(cid) + obj5 = recv_nowait(cid, default) + obj6 = recv_nowait(cid, default) + + self.assertIs(obj1, default) + self.assertIs(obj2, None) + self.assertEqual(obj3, 1) + self.assertEqual(obj4, b'spam') + self.assertEqual(obj5, b'eggs') + self.assertIs(obj6, default) + + def test_recv_sending_interp_destroyed(self): + with self.subTest('closed'): + cid1 = _channels.create(REPLACE) + interp = _interpreters.create() + _interpreters.run_string(interp, dedent(f""" + import _interpchannels as _channels + _channels.send({cid1}, b'spam', blocking=False) + """)) + _interpreters.destroy(interp) + + with self.assertRaisesRegex(RuntimeError, + f'channel {cid1} is closed'): + _channels.recv(cid1) + del cid1 + with self.subTest('still open'): + cid2 = _channels.create(REPLACE) + interp = _interpreters.create() + _interpreters.run_string(interp, dedent(f""" + import _interpchannels as _channels + _channels.send({cid2}, b'spam', blocking=False) + """)) + _channels.send(cid2, b'eggs', blocking=False) + _interpreters.destroy(interp) + + recv_nowait(cid2, unbound=True) + recv_nowait(cid2, unbound=False) + with self.assertRaisesRegex(RuntimeError, + f'channel {cid2} is empty'): + _channels.recv(cid2) + del cid2 + + #------------------- + # send_buffer + + def test_send_buffer(self): + buf = bytearray(b'spamspamspam') + cid = _channels.create(REPLACE) + _channels.send_buffer(cid, buf, blocking=False) + obj = recv_nowait(cid) + + self.assertIsNot(obj, buf) + self.assertIsInstance(obj, memoryview) + self.assertEqual(obj, buf) + + buf[4:8] = b'eggs' + self.assertEqual(obj, buf) + obj[4:8] = b'ham.' + self.assertEqual(obj, buf) + + #------------------- + # send with waiting + + def build_send_waiter(self, obj, *, buffer=False): + # We want a long enough sleep that send() actually has to wait. + + if buffer: + send = _channels.send_buffer + else: + send = _channels.send + + cid = _channels.create(REPLACE) + try: + started = time.monotonic() + send(cid, obj, blocking=False) + stopped = time.monotonic() + recv_nowait(cid) + finally: + _channels.destroy(cid) + delay = stopped - started # seconds + delay *= 3 + + def wait(): + time.sleep(delay) + return wait + + def test_send_blocking_waiting(self): + received = None + obj = b'spam' + wait = self.build_send_waiter(obj) + cid = _channels.create(REPLACE) + def f(): + nonlocal received + wait() + received = recv_wait(cid) + t = threading.Thread(target=f) + t.start() + _channels.send(cid, obj, blocking=True) + t.join() + + self.assertEqual(received, obj) + + def test_send_buffer_blocking_waiting(self): + received = None + obj = bytearray(b'spam') + wait = self.build_send_waiter(obj, buffer=True) + cid = _channels.create(REPLACE) + def f(): + nonlocal received + wait() + received = recv_wait(cid) + t = threading.Thread(target=f) + t.start() + _channels.send_buffer(cid, obj, blocking=True) + t.join() + + self.assertEqual(received, obj) + + def test_send_blocking_no_wait(self): + received = None + obj = b'spam' + cid = _channels.create(REPLACE) + def f(): + nonlocal received + received = recv_wait(cid) + t = threading.Thread(target=f) + t.start() + _channels.send(cid, obj, blocking=True) + t.join() + + self.assertEqual(received, obj) + + def test_send_buffer_blocking_no_wait(self): + received = None + obj = bytearray(b'spam') + cid = _channels.create(REPLACE) + def f(): + nonlocal received + received = recv_wait(cid) + t = threading.Thread(target=f) + t.start() + _channels.send_buffer(cid, obj, blocking=True) + t.join() + + self.assertEqual(received, obj) + + def test_send_timeout(self): + obj = b'spam' + + with self.subTest('non-blocking with timeout'): + cid = _channels.create(REPLACE) + with self.assertRaises(ValueError): + _channels.send(cid, obj, blocking=False, timeout=0.1) + + with self.subTest('timeout hit'): + cid = _channels.create(REPLACE) + with self.assertRaises(TimeoutError): + _channels.send(cid, obj, blocking=True, timeout=0.1) + with self.assertRaises(_channels.ChannelEmptyError): + received = recv_nowait(cid) + print(repr(received)) + + with self.subTest('timeout not hit'): + cid = _channels.create(REPLACE) + def f(): + recv_wait(cid) + t = threading.Thread(target=f) + t.start() + _channels.send(cid, obj, blocking=True, timeout=10) + t.join() + + def test_send_buffer_timeout(self): + try: + self._has_run_once_timeout + except AttributeError: + # At the moment, this test leaks a few references. + # It looks like the leak originates with the addition + # of _channels.send_buffer() (gh-110246), whereas the + # tests were added afterward. We want this test even + # if the refleak isn't fixed yet, so we skip here. + raise unittest.SkipTest('temporarily skipped due to refleaks') + else: + self._has_run_once_timeout = True + + obj = bytearray(b'spam') + + with self.subTest('non-blocking with timeout'): + cid = _channels.create(REPLACE) + with self.assertRaises(ValueError): + _channels.send_buffer(cid, obj, blocking=False, timeout=0.1) + + with self.subTest('timeout hit'): + cid = _channels.create(REPLACE) + with self.assertRaises(TimeoutError): + _channels.send_buffer(cid, obj, blocking=True, timeout=0.1) + with self.assertRaises(_channels.ChannelEmptyError): + received = recv_nowait(cid) + print(repr(received)) + + with self.subTest('timeout not hit'): + cid = _channels.create(REPLACE) + def f(): + recv_wait(cid) + t = threading.Thread(target=f) + t.start() + _channels.send_buffer(cid, obj, blocking=True, timeout=10) + t.join() + + def test_send_closed_while_waiting(self): + obj = b'spam' + wait = self.build_send_waiter(obj) + + with self.subTest('without timeout'): + cid = _channels.create(REPLACE) + def f(): + wait() + _channels.close(cid, force=True) + t = threading.Thread(target=f) + t.start() + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, obj, blocking=True) + t.join() + + with self.subTest('with timeout'): + cid = _channels.create(REPLACE) + def f(): + wait() + _channels.close(cid, force=True) + t = threading.Thread(target=f) + t.start() + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, obj, blocking=True, timeout=30) + t.join() + + def test_send_buffer_closed_while_waiting(self): + try: + self._has_run_once_closed + except AttributeError: + # At the moment, this test leaks a few references. + # It looks like the leak originates with the addition + # of _channels.send_buffer() (gh-110246), whereas the + # tests were added afterward. We want this test even + # if the refleak isn't fixed yet, so we skip here. + raise unittest.SkipTest('temporarily skipped due to refleaks') + else: + self._has_run_once_closed = True + + obj = bytearray(b'spam') + wait = self.build_send_waiter(obj, buffer=True) + + with self.subTest('without timeout'): + cid = _channels.create(REPLACE) + def f(): + wait() + _channels.close(cid, force=True) + t = threading.Thread(target=f) + t.start() + with self.assertRaises(_channels.ChannelClosedError): + _channels.send_buffer(cid, obj, blocking=True) + t.join() + + with self.subTest('with timeout'): + cid = _channels.create(REPLACE) + def f(): + wait() + _channels.close(cid, force=True) + t = threading.Thread(target=f) + t.start() + with self.assertRaises(_channels.ChannelClosedError): + _channels.send_buffer(cid, obj, blocking=True, timeout=30) + t.join() + + #------------------- + # close + + def test_close_single_user(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + recv_nowait(cid) + _channels.close(cid) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, b'eggs') + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + def test_close_multiple_users(self): + cid = _channels.create(REPLACE) + id1 = _interpreters.create() + id2 = _interpreters.create() + _interpreters.run_string(id1, dedent(f""" + import _interpchannels as _channels + _channels.send({cid}, b'spam', blocking=False) + """)) + _interpreters.run_string(id2, dedent(f""" + import _interpchannels as _channels + _channels.recv({cid}) + """)) + _channels.close(cid) + + excsnap = _interpreters.run_string(id1, dedent(f""" + _channels.send({cid}, b'spam') + """)) + self.assertEqual(excsnap.type.__name__, 'ChannelClosedError') + + excsnap = _interpreters.run_string(id2, dedent(f""" + _channels.send({cid}, b'spam') + """)) + self.assertEqual(excsnap.type.__name__, 'ChannelClosedError') + + def test_close_multiple_times(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + recv_nowait(cid) + _channels.close(cid) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.close(cid) + + def test_close_empty(self): + tests = [ + (False, False), + (True, False), + (False, True), + (True, True), + ] + for send, recv in tests: + with self.subTest((send, recv)): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + recv_nowait(cid) + _channels.close(cid, send=send, recv=recv) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, b'eggs') + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + def test_close_defaults_with_unused_items(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + _channels.send(cid, b'ham', blocking=False) + + with self.assertRaises(_channels.ChannelNotEmptyError): + _channels.close(cid) + recv_nowait(cid) + _channels.send(cid, b'eggs', blocking=False) + + def test_close_recv_with_unused_items_unforced(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + _channels.send(cid, b'ham', blocking=False) + + with self.assertRaises(_channels.ChannelNotEmptyError): + _channels.close(cid, recv=True) + recv_nowait(cid) + _channels.send(cid, b'eggs', blocking=False) + recv_nowait(cid) + recv_nowait(cid) + _channels.close(cid, recv=True) + + def test_close_send_with_unused_items_unforced(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + _channels.send(cid, b'ham', blocking=False) + _channels.close(cid, send=True) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, b'eggs') + recv_nowait(cid) + recv_nowait(cid) + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + def test_close_both_with_unused_items_unforced(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + _channels.send(cid, b'ham', blocking=False) + + with self.assertRaises(_channels.ChannelNotEmptyError): + _channels.close(cid, recv=True, send=True) + recv_nowait(cid) + _channels.send(cid, b'eggs', blocking=False) + recv_nowait(cid) + recv_nowait(cid) + _channels.close(cid, recv=True) + + def test_close_recv_with_unused_items_forced(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + _channels.send(cid, b'ham', blocking=False) + _channels.close(cid, recv=True, force=True) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, b'eggs') + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + def test_close_send_with_unused_items_forced(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + _channels.send(cid, b'ham', blocking=False) + _channels.close(cid, send=True, force=True) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, b'eggs') + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + def test_close_both_with_unused_items_forced(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + _channels.send(cid, b'ham', blocking=False) + _channels.close(cid, send=True, recv=True, force=True) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, b'eggs') + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + def test_close_never_used(self): + cid = _channels.create(REPLACE) + _channels.close(cid) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, b'spam') + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + def test_close_by_unassociated_interp(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + interp = _interpreters.create() + _interpreters.run_string(interp, dedent(f""" + import _interpchannels as _channels + _channels.close({cid}, force=True) + """)) + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + with self.assertRaises(_channels.ChannelClosedError): + _channels.close(cid) + + def test_close_used_multiple_times_by_single_user(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + _channels.send(cid, b'spam', blocking=False) + _channels.send(cid, b'spam', blocking=False) + recv_nowait(cid) + _channels.close(cid, force=True) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, b'eggs') + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + def test_channel_list_interpreters_invalid_channel(self): + cid = _channels.create(REPLACE) + # Test for invalid channel ID. + with self.assertRaises(_channels.ChannelNotFoundError): + _channels.list_interpreters(1000, send=True) + + _channels.close(cid) + # Test for a channel that has been closed. + with self.assertRaises(_channels.ChannelClosedError): + _channels.list_interpreters(cid, send=True) + + def test_channel_list_interpreters_invalid_args(self): + # Tests for invalid arguments passed to the API. + cid = _channels.create(REPLACE) + with self.assertRaises(TypeError): + _channels.list_interpreters(cid) + + +class ChannelReleaseTests(TestBase): + + # XXX Add more test coverage a la the tests for close(). + + """ + - main / interp / other + - run in: current thread / new thread / other thread / different threads + - end / opposite + - force / no force + - used / not used (associated / not associated) + - empty / emptied / never emptied / partly emptied + - closed / not closed + - released / not released + - creator (interp) / other + - associated interpreter not running + - associated interpreter destroyed + """ + + """ + use + pre-release + release + after + check + """ + + """ + release in: main, interp1 + creator: same, other (incl. interp2) + + use: None,send,recv,send/recv in None,same,other(incl. interp2),same+other(incl. interp2),all + pre-release: None,send,recv,both in None,same,other(incl. interp2),same+other(incl. interp2),all + pre-release forced: None,send,recv,both in None,same,other(incl. interp2),same+other(incl. interp2),all + + release: same + release forced: same + + use after: None,send,recv,send/recv in None,same,other(incl. interp2),same+other(incl. interp2),all + release after: None,send,recv,send/recv in None,same,other(incl. interp2),same+other(incl. interp2),all + check released: send/recv for same/other(incl. interp2) + check closed: send/recv for same/other(incl. interp2) + """ + + def test_single_user(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + recv_nowait(cid) + _channels.release(cid, send=True, recv=True) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, b'eggs') + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + def test_multiple_users(self): + cid = _channels.create(REPLACE) + id1 = _interpreters.create() + id2 = _interpreters.create() + _interpreters.run_string(id1, dedent(f""" + import _interpchannels as _channels + _channels.send({cid}, b'spam', blocking=False) + """)) + out = _run_output(id2, dedent(f""" + import _interpchannels as _channels + obj, _ = _channels.recv({cid}) + _channels.release({cid}) + print(repr(obj)) + """)) + _interpreters.run_string(id1, dedent(f""" + _channels.release({cid}) + """)) + + self.assertEqual(out.strip(), "b'spam'") + + def test_no_kwargs(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + recv_nowait(cid) + _channels.release(cid) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, b'eggs') + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + def test_multiple_times(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + recv_nowait(cid) + _channels.release(cid, send=True, recv=True) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.release(cid, send=True, recv=True) + + def test_with_unused_items(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + _channels.send(cid, b'ham', blocking=False) + _channels.release(cid, send=True, recv=True) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + def test_never_used(self): + cid = _channels.create(REPLACE) + _channels.release(cid) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, b'spam') + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + def test_by_unassociated_interp(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + interp = _interpreters.create() + _interpreters.run_string(interp, dedent(f""" + import _interpchannels as _channels + _channels.release({cid}) + """)) + obj = recv_nowait(cid) + _channels.release(cid) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, b'eggs') + self.assertEqual(obj, b'spam') + + def test_close_if_unassociated(self): + # XXX Something's not right with this test... + cid = _channels.create(REPLACE) + interp = _interpreters.create() + _interpreters.run_string(interp, dedent(f""" + import _interpchannels as _channels + obj = _channels.send({cid}, b'spam', blocking=False) + _channels.release({cid}) + """)) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + def test_partially(self): + # XXX Is partial close too weird/confusing? + cid = _channels.create(REPLACE) + _channels.send(cid, None, blocking=False) + recv_nowait(cid) + _channels.send(cid, b'spam', blocking=False) + _channels.release(cid, send=True) + obj = recv_nowait(cid) + + self.assertEqual(obj, b'spam') + + def test_used_multiple_times_by_single_user(self): + cid = _channels.create(REPLACE) + _channels.send(cid, b'spam', blocking=False) + _channels.send(cid, b'spam', blocking=False) + _channels.send(cid, b'spam', blocking=False) + recv_nowait(cid) + _channels.release(cid, send=True, recv=True) + + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(cid, b'eggs') + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(cid) + + +class ChannelCloseFixture(namedtuple('ChannelCloseFixture', + 'end interp other extra creator')): + + # Set this to True to avoid creating interpreters, e.g. when + # scanning through test permutations without running them. + QUICK = False + + def __new__(cls, end, interp, other, extra, creator): + assert end in ('send', 'recv') + if cls.QUICK: + known = {} + else: + interp = Interpreter.from_raw(interp) + other = Interpreter.from_raw(other) + extra = Interpreter.from_raw(extra) + known = { + interp.name: interp, + other.name: other, + extra.name: extra, + } + if not creator: + creator = 'same' + self = super().__new__(cls, end, interp, other, extra, creator) + self._prepped = set() + self._state = ChannelState() + self._known = known + return self + + @property + def state(self): + return self._state + + @property + def cid(self): + try: + return self._cid + except AttributeError: + creator = self._get_interpreter(self.creator) + self._cid = self._new_channel(creator) + return self._cid + + def get_interpreter(self, interp): + interp = self._get_interpreter(interp) + self._prep_interpreter(interp) + return interp + + def expect_closed_error(self, end=None): + if end is None: + end = self.end + if end == 'recv' and self.state.closed == 'send': + return False + return bool(self.state.closed) + + def prep_interpreter(self, interp): + self._prep_interpreter(interp) + + def record_action(self, action, result): + self._state = result + + def clean_up(self): + clean_up_interpreters() + clean_up_channels() + + # internal methods + + def _new_channel(self, creator): + if creator.name == 'main': + return _channels.create(REPLACE) + else: + ch = _channels.create(REPLACE) + run_interp(creator.id, f""" + import _interpreters + cid = _xxsubchannels.create() + # We purposefully send back an int to avoid tying the + # channel to the other interpreter. + _xxsubchannels.send({ch}, int(cid), blocking=False) + del _interpreters + """) + self._cid = recv_nowait(ch) + return self._cid + + def _get_interpreter(self, interp): + if interp in ('same', 'interp'): + return self.interp + elif interp == 'other': + return self.other + elif interp == 'extra': + return self.extra + else: + name = interp + try: + interp = self._known[name] + except KeyError: + interp = self._known[name] = Interpreter(name) + return interp + + def _prep_interpreter(self, interp): + if interp.id in self._prepped: + return + self._prepped.add(interp.id) + if interp.name == 'main': + return + run_interp(interp.id, f""" + import _interpchannels as channels + import test.test__interpchannels as helpers + ChannelState = helpers.ChannelState + try: + cid + except NameError: + cid = _channels._channel_id({self.cid}) + """) + + +@unittest.skip('these tests take several hours to run') +class ExhaustiveChannelTests(TestBase): + + """ + - main / interp / other + - run in: current thread / new thread / other thread / different threads + - end / opposite + - force / no force + - used / not used (associated / not associated) + - empty / emptied / never emptied / partly emptied + - closed / not closed + - released / not released + - creator (interp) / other + - associated interpreter not running + - associated interpreter destroyed + + - close after unbound + """ + + """ + use + pre-close + close + after + check + """ + + """ + close in: main, interp1 + creator: same, other, extra + + use: None,send,recv,send/recv in None,same,other,same+other,all + pre-close: None,send,recv in None,same,other,same+other,all + pre-close forced: None,send,recv in None,same,other,same+other,all + + close: same + close forced: same + + use after: None,send,recv,send/recv in None,same,other,extra,same+other,all + close after: None,send,recv,send/recv in None,same,other,extra,same+other,all + check closed: send/recv for same/other(incl. interp2) + """ + + def iter_action_sets(self): + # - used / not used (associated / not associated) + # - empty / emptied / never emptied / partly emptied + # - closed / not closed + # - released / not released + + # never used + yield [] + + # only pre-closed (and possible used after) + for closeactions in self._iter_close_action_sets('same', 'other'): + yield closeactions + for postactions in self._iter_post_close_action_sets(): + yield closeactions + postactions + for closeactions in self._iter_close_action_sets('other', 'extra'): + yield closeactions + for postactions in self._iter_post_close_action_sets(): + yield closeactions + postactions + + # used + for useactions in self._iter_use_action_sets('same', 'other'): + yield useactions + for closeactions in self._iter_close_action_sets('same', 'other'): + actions = useactions + closeactions + yield actions + for postactions in self._iter_post_close_action_sets(): + yield actions + postactions + for closeactions in self._iter_close_action_sets('other', 'extra'): + actions = useactions + closeactions + yield actions + for postactions in self._iter_post_close_action_sets(): + yield actions + postactions + for useactions in self._iter_use_action_sets('other', 'extra'): + yield useactions + for closeactions in self._iter_close_action_sets('same', 'other'): + actions = useactions + closeactions + yield actions + for postactions in self._iter_post_close_action_sets(): + yield actions + postactions + for closeactions in self._iter_close_action_sets('other', 'extra'): + actions = useactions + closeactions + yield actions + for postactions in self._iter_post_close_action_sets(): + yield actions + postactions + + def _iter_use_action_sets(self, interp1, interp2): + interps = (interp1, interp2) + + # only recv end used + yield [ + ChannelAction('use', 'recv', interp1), + ] + yield [ + ChannelAction('use', 'recv', interp2), + ] + yield [ + ChannelAction('use', 'recv', interp1), + ChannelAction('use', 'recv', interp2), + ] + + # never emptied + yield [ + ChannelAction('use', 'send', interp1), + ] + yield [ + ChannelAction('use', 'send', interp2), + ] + yield [ + ChannelAction('use', 'send', interp1), + ChannelAction('use', 'send', interp2), + ] + + # partially emptied + for interp1 in interps: + for interp2 in interps: + for interp3 in interps: + yield [ + ChannelAction('use', 'send', interp1), + ChannelAction('use', 'send', interp2), + ChannelAction('use', 'recv', interp3), + ] + + # fully emptied + for interp1 in interps: + for interp2 in interps: + for interp3 in interps: + for interp4 in interps: + yield [ + ChannelAction('use', 'send', interp1), + ChannelAction('use', 'send', interp2), + ChannelAction('use', 'recv', interp3), + ChannelAction('use', 'recv', interp4), + ] + + def _iter_close_action_sets(self, interp1, interp2): + ends = ('recv', 'send') + interps = (interp1, interp2) + for force in (True, False): + op = 'force-close' if force else 'close' + for interp in interps: + for end in ends: + yield [ + ChannelAction(op, end, interp), + ] + for recvop in ('close', 'force-close'): + for sendop in ('close', 'force-close'): + for recv in interps: + for send in interps: + yield [ + ChannelAction(recvop, 'recv', recv), + ChannelAction(sendop, 'send', send), + ] + + def _iter_post_close_action_sets(self): + for interp in ('same', 'extra', 'other'): + yield [ + ChannelAction('use', 'recv', interp), + ] + yield [ + ChannelAction('use', 'send', interp), + ] + + def run_actions(self, fix, actions): + for action in actions: + self.run_action(fix, action) + + def run_action(self, fix, action, *, hideclosed=True): + end = action.resolve_end(fix.end) + interp = action.resolve_interp(fix.interp, fix.other, fix.extra) + fix.prep_interpreter(interp) + if interp.name == 'main': + result = run_action( + fix.cid, + action.action, + end, + fix.state, + hideclosed=hideclosed, + ) + fix.record_action(action, result) + else: + _cid = _channels.create(REPLACE) + run_interp(interp.id, f""" + result = helpers.run_action( + {fix.cid}, + {repr(action.action)}, + {repr(end)}, + {repr(fix.state)}, + hideclosed={hideclosed}, + ) + _channels.send({_cid}, result.pending.to_bytes(1, 'little'), blocking=False) + _channels.send({_cid}, b'X' if result.closed else b'', blocking=False) + """) + result = ChannelState( + pending=int.from_bytes(recv_nowait(_cid), 'little'), + closed=bool(recv_nowait(_cid)), + ) + fix.record_action(action, result) + + def iter_fixtures(self): + # XXX threads? + interpreters = [ + ('main', 'interp', 'extra'), + ('interp', 'main', 'extra'), + ('interp1', 'interp2', 'extra'), + ('interp1', 'interp2', 'main'), + ] + for interp, other, extra in interpreters: + for creator in ('same', 'other', 'creator'): + for end in ('send', 'recv'): + yield ChannelCloseFixture(end, interp, other, extra, creator) + + def _close(self, fix, *, force): + op = 'force-close' if force else 'close' + close = ChannelAction(op, fix.end, 'same') + if not fix.expect_closed_error(): + self.run_action(fix, close, hideclosed=False) + else: + with self.assertRaises(_channels.ChannelClosedError): + self.run_action(fix, close, hideclosed=False) + + def _assert_closed_in_interp(self, fix, interp=None): + if interp is None or interp.name == 'main': + with self.assertRaises(_channels.ChannelClosedError): + _channels.recv(fix.cid) + with self.assertRaises(_channels.ChannelClosedError): + _channels.send(fix.cid, b'spam') + with self.assertRaises(_channels.ChannelClosedError): + _channels.close(fix.cid) + with self.assertRaises(_channels.ChannelClosedError): + _channels.close(fix.cid, force=True) + else: + run_interp(interp.id, """ + with helpers.expect_channel_closed(): + _channels.recv(cid) + """) + run_interp(interp.id, """ + with helpers.expect_channel_closed(): + _channels.send(cid, b'spam', blocking=False) + """) + run_interp(interp.id, """ + with helpers.expect_channel_closed(): + _channels.close(cid) + """) + run_interp(interp.id, """ + with helpers.expect_channel_closed(): + _channels.close(cid, force=True) + """) + + def _assert_closed(self, fix): + self.assertTrue(fix.state.closed) + + for _ in range(fix.state.pending): + recv_nowait(fix.cid) + self._assert_closed_in_interp(fix) + + for interp in ('same', 'other'): + interp = fix.get_interpreter(interp) + if interp.name == 'main': + continue + self._assert_closed_in_interp(fix, interp) + + interp = fix.get_interpreter('fresh') + self._assert_closed_in_interp(fix, interp) + + def _iter_close_tests(self, verbose=False): + i = 0 + for actions in self.iter_action_sets(): + print() + for fix in self.iter_fixtures(): + i += 1 + if i > 1000: + return + if verbose: + if (i - 1) % 6 == 0: + print() + print(i, fix, '({} actions)'.format(len(actions))) + else: + if (i - 1) % 6 == 0: + print(' ', end='') + print('.', end=''); sys.stdout.flush() + yield i, fix, actions + if verbose: + print('---') + print() + + # This is useful for scanning through the possible tests. + def _skim_close_tests(self): + ChannelCloseFixture.QUICK = True + for i, fix, actions in self._iter_close_tests(): + pass + + def test_close(self): + for i, fix, actions in self._iter_close_tests(): + with self.subTest('{} {} {}'.format(i, fix, actions)): + fix.prep_interpreter(fix.interp) + self.run_actions(fix, actions) + + self._close(fix, force=False) + + self._assert_closed(fix) + # XXX Things slow down if we have too many interpreters. + fix.clean_up() + + def test_force_close(self): + for i, fix, actions in self._iter_close_tests(): + with self.subTest('{} {} {}'.format(i, fix, actions)): + fix.prep_interpreter(fix.interp) + self.run_actions(fix, actions) + + self._close(fix, force=True) + + self._assert_closed(fix) + # XXX Things slow down if we have too many interpreters. + fix.clean_up() + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test__interpreters.py b/Lib/test/test__interpreters.py new file mode 100644 index 00000000000..ec5a26bc1c0 --- /dev/null +++ b/Lib/test/test__interpreters.py @@ -0,0 +1,1124 @@ +import contextlib +import os +import pickle +from textwrap import dedent +import threading +import unittest + +from test import support +from test.support import import_helper +from test.support import os_helper +from test.support import script_helper + + +_interpreters = import_helper.import_module('_interpreters') +from _interpreters import InterpreterNotFoundError + + +################################## +# helpers + +def _captured_script(script): + r, w = os.pipe() + indented = script.replace('\n', '\n ') + wrapped = dedent(f""" + import contextlib + with open({w}, 'w', encoding="utf-8") as spipe: + with contextlib.redirect_stdout(spipe): + {indented} + """) + return wrapped, open(r, encoding="utf-8") + + +def _run_output(interp, request): + script, rpipe = _captured_script(request) + with rpipe: + _interpreters.run_string(interp, script) + return rpipe.read() + + +def _wait_for_interp_to_run(interp, timeout=None): + # bpo-37224: Running this test file in multiprocesses will fail randomly. + # The failure reason is that the thread can't acquire the cpu to + # run subinterpreter earlier than the main thread in multiprocess. + if timeout is None: + timeout = support.SHORT_TIMEOUT + for _ in support.sleeping_retry(timeout, error=False): + if _interpreters.is_running(interp): + break + else: + raise RuntimeError('interp is not running') + + +@contextlib.contextmanager +def _running(interp): + r, w = os.pipe() + def run(): + _interpreters.run_string(interp, dedent(f""" + # wait for "signal" + with open({r}, encoding="utf-8") as rpipe: + rpipe.read() + """)) + + t = threading.Thread(target=run) + t.start() + _wait_for_interp_to_run(interp) + + yield + + with open(w, 'w', encoding="utf-8") as spipe: + spipe.write('done') + t.join() + + +def clean_up_interpreters(): + for id, *_ in _interpreters.list_all(): + if id == 0: # main + continue + try: + _interpreters.destroy(id) + except _interpreters.InterpreterError: + pass # already destroyed + + +class TestBase(unittest.TestCase): + + def tearDown(self): + clean_up_interpreters() + + +################################## +# misc. tests + +class IsShareableTests(unittest.TestCase): + + def test_default_shareables(self): + shareables = [ + # singletons + None, + # builtin objects + b'spam', + 'spam', + 10, + -10, + True, + False, + 100.0, + (1, ('spam', 'eggs')), + ] + for obj in shareables: + with self.subTest(obj): + self.assertTrue( + _interpreters.is_shareable(obj)) + + def test_not_shareable(self): + class Cheese: + def __init__(self, name): + self.name = name + def __str__(self): + return self.name + + class SubBytes(bytes): + """A subclass of a shareable type.""" + + not_shareables = [ + # singletons + NotImplemented, + ..., + # builtin types and objects + type, + object, + object(), + Exception(), + # user-defined types and objects + Cheese, + Cheese('Wensleydale'), + SubBytes(b'spam'), + ] + for obj in not_shareables: + with self.subTest(repr(obj)): + self.assertFalse( + _interpreters.is_shareable(obj)) + + +class ModuleTests(TestBase): + + def test_import_in_interpreter(self): + _run_output( + _interpreters.create(), + 'import _interpreters', + ) + + +################################## +# interpreter tests + +class ListAllTests(TestBase): + + def test_initial(self): + main, *_ = _interpreters.get_main() + ids = [id for id, *_ in _interpreters.list_all()] + self.assertEqual(ids, [main]) + + def test_after_creating(self): + main, *_ = _interpreters.get_main() + first = _interpreters.create() + second = _interpreters.create() + ids = [id for id, *_ in _interpreters.list_all()] + self.assertEqual(ids, [main, first, second]) + + def test_after_destroying(self): + main, *_ = _interpreters.get_main() + first = _interpreters.create() + second = _interpreters.create() + _interpreters.destroy(first) + ids = [id for id, *_ in _interpreters.list_all()] + self.assertEqual(ids, [main, second]) + + +class GetCurrentTests(TestBase): + + def test_main(self): + main, *_ = _interpreters.get_main() + cur, *_ = _interpreters.get_current() + self.assertEqual(cur, main) + self.assertIsInstance(cur, int) + + def test_subinterpreter(self): + main, *_ = _interpreters.get_main() + interp = _interpreters.create() + out = _run_output(interp, dedent(""" + import _interpreters + cur, *_ = _interpreters.get_current() + print(cur) + assert isinstance(cur, int) + """)) + cur = int(out.strip()) + _, expected = [id for id, *_ in _interpreters.list_all()] + self.assertEqual(cur, expected) + self.assertNotEqual(cur, main) + + +class GetMainTests(TestBase): + + def test_from_main(self): + [expected] = [id for id, *_ in _interpreters.list_all()] + main, *_ = _interpreters.get_main() + self.assertEqual(main, expected) + self.assertIsInstance(main, int) + + def test_from_subinterpreter(self): + [expected] = [id for id, *_ in _interpreters.list_all()] + interp = _interpreters.create() + out = _run_output(interp, dedent(""" + import _interpreters + main, *_ = _interpreters.get_main() + print(main) + assert isinstance(main, int) + """)) + main = int(out.strip()) + self.assertEqual(main, expected) + + +class IsRunningTests(TestBase): + + def test_main(self): + main, *_ = _interpreters.get_main() + self.assertTrue(_interpreters.is_running(main)) + + @unittest.skip('Fails on FreeBSD') + def test_subinterpreter(self): + interp = _interpreters.create() + self.assertFalse(_interpreters.is_running(interp)) + + with _running(interp): + self.assertTrue(_interpreters.is_running(interp)) + self.assertFalse(_interpreters.is_running(interp)) + + def test_from_subinterpreter(self): + interp = _interpreters.create() + out = _run_output(interp, dedent(f""" + import _interpreters + if _interpreters.is_running({interp}): + print(True) + else: + print(False) + """)) + self.assertEqual(out.strip(), 'True') + + def test_already_destroyed(self): + interp = _interpreters.create() + _interpreters.destroy(interp) + with self.assertRaises(InterpreterNotFoundError): + _interpreters.is_running(interp) + + def test_does_not_exist(self): + with self.assertRaises(InterpreterNotFoundError): + _interpreters.is_running(1_000_000) + + def test_bad_id(self): + with self.assertRaises(ValueError): + _interpreters.is_running(-1) + + +class CreateTests(TestBase): + + def test_in_main(self): + id = _interpreters.create() + self.assertIsInstance(id, int) + + after = [id for id, *_ in _interpreters.list_all()] + self.assertIn(id, after) + + @unittest.skip('enable this test when working on pystate.c') + def test_unique_id(self): + seen = set() + for _ in range(100): + id = _interpreters.create() + _interpreters.destroy(id) + seen.add(id) + + self.assertEqual(len(seen), 100) + + @support.skip_if_sanitizer('gh-129824: race on tp_flags', thread=True) + def test_in_thread(self): + lock = threading.Lock() + id = None + def f(): + nonlocal id + id = _interpreters.create() + lock.acquire() + lock.release() + + t = threading.Thread(target=f) + with lock: + t.start() + t.join() + after = set(id for id, *_ in _interpreters.list_all()) + self.assertIn(id, after) + + def test_in_subinterpreter(self): + main, = [id for id, *_ in _interpreters.list_all()] + id1 = _interpreters.create() + out = _run_output(id1, dedent(""" + import _interpreters + id = _interpreters.create() + print(id) + assert isinstance(id, int) + """)) + id2 = int(out.strip()) + + after = set(id for id, *_ in _interpreters.list_all()) + self.assertEqual(after, {main, id1, id2}) + + def test_in_threaded_subinterpreter(self): + main, = [id for id, *_ in _interpreters.list_all()] + id1 = _interpreters.create() + id2 = None + def f(): + nonlocal id2 + out = _run_output(id1, dedent(""" + import _interpreters + id = _interpreters.create() + print(id) + """)) + id2 = int(out.strip()) + + t = threading.Thread(target=f) + t.start() + t.join() + + after = set(id for id, *_ in _interpreters.list_all()) + self.assertEqual(after, {main, id1, id2}) + + def test_after_destroy_all(self): + before = set(id for id, *_ in _interpreters.list_all()) + # Create 3 subinterpreters. + ids = [] + for _ in range(3): + id = _interpreters.create() + ids.append(id) + # Now destroy them. + for id in ids: + _interpreters.destroy(id) + # Finally, create another. + id = _interpreters.create() + after = set(id for id, *_ in _interpreters.list_all()) + self.assertEqual(after, before | {id}) + + def test_after_destroy_some(self): + before = set(id for id, *_ in _interpreters.list_all()) + # Create 3 subinterpreters. + id1 = _interpreters.create() + id2 = _interpreters.create() + id3 = _interpreters.create() + # Now destroy 2 of them. + _interpreters.destroy(id1) + _interpreters.destroy(id3) + # Finally, create another. + id = _interpreters.create() + after = set(id for id, *_ in _interpreters.list_all()) + self.assertEqual(after, before | {id, id2}) + + +class DestroyTests(TestBase): + + def test_one(self): + id1 = _interpreters.create() + id2 = _interpreters.create() + id3 = _interpreters.create() + before = set(id for id, *_ in _interpreters.list_all()) + self.assertIn(id2, before) + + _interpreters.destroy(id2) + + after = set(id for id, *_ in _interpreters.list_all()) + self.assertNotIn(id2, after) + self.assertIn(id1, after) + self.assertIn(id3, after) + + def test_all(self): + initial = set(id for id, *_ in _interpreters.list_all()) + ids = set() + for _ in range(3): + id = _interpreters.create() + ids.add(id) + before = set(id for id, *_ in _interpreters.list_all()) + self.assertEqual(before, initial | ids) + for id in ids: + _interpreters.destroy(id) + after = set(id for id, *_ in _interpreters.list_all()) + self.assertEqual(after, initial) + + def test_main(self): + main, = [id for id, *_ in _interpreters.list_all()] + with self.assertRaises(_interpreters.InterpreterError): + _interpreters.destroy(main) + + def f(): + with self.assertRaises(_interpreters.InterpreterError): + _interpreters.destroy(main) + + t = threading.Thread(target=f) + t.start() + t.join() + + def test_already_destroyed(self): + id = _interpreters.create() + _interpreters.destroy(id) + with self.assertRaises(InterpreterNotFoundError): + _interpreters.destroy(id) + + def test_does_not_exist(self): + with self.assertRaises(InterpreterNotFoundError): + _interpreters.destroy(1_000_000) + + def test_bad_id(self): + with self.assertRaises(ValueError): + _interpreters.destroy(-1) + + def test_from_current(self): + main, = [id for id, *_ in _interpreters.list_all()] + id = _interpreters.create() + script = dedent(f""" + import _interpreters + try: + _interpreters.destroy({id}) + except _interpreters.InterpreterError: + pass + """) + + _interpreters.run_string(id, script) + after = set(id for id, *_ in _interpreters.list_all()) + self.assertEqual(after, {main, id}) + + def test_from_sibling(self): + main, = [id for id, *_ in _interpreters.list_all()] + id1 = _interpreters.create() + id2 = _interpreters.create() + script = dedent(f""" + import _interpreters + _interpreters.destroy({id2}) + """) + _interpreters.run_string(id1, script) + + after = set(id for id, *_ in _interpreters.list_all()) + self.assertEqual(after, {main, id1}) + + def test_from_other_thread(self): + id = _interpreters.create() + def f(): + _interpreters.destroy(id) + + t = threading.Thread(target=f) + t.start() + t.join() + + def test_still_running(self): + main, = [id for id, *_ in _interpreters.list_all()] + interp = _interpreters.create() + with _running(interp): + self.assertTrue(_interpreters.is_running(interp), + msg=f"Interp {interp} should be running before destruction.") + + with self.assertRaises(_interpreters.InterpreterError, + msg=f"Should not be able to destroy interp {interp} while it's still running."): + _interpreters.destroy(interp) + self.assertTrue(_interpreters.is_running(interp)) + + +class CommonTests(TestBase): + def setUp(self): + super().setUp() + self.id = _interpreters.create() + + def test_signatures(self): + # See https://github.com/python/cpython/issues/126654 + msg = r"exec\(\) argument 'shared' must be dict, not int" + with self.assertRaisesRegex(TypeError, msg): + _interpreters.exec(self.id, 'a', 1) + with self.assertRaisesRegex(TypeError, msg): + _interpreters.exec(self.id, 'a', shared=1) + msg = r"run_string\(\) argument 'shared' must be dict, not int" + with self.assertRaisesRegex(TypeError, msg): + _interpreters.run_string(self.id, 'a', shared=1) + msg = r"run_func\(\) argument 'shared' must be dict, not int" + with self.assertRaisesRegex(TypeError, msg): + _interpreters.run_func(self.id, lambda: None, shared=1) + # See https://github.com/python/cpython/issues/135855 + msg = r"set___main___attrs\(\) argument 'updates' must be dict, not int" + with self.assertRaisesRegex(TypeError, msg): + _interpreters.set___main___attrs(self.id, 1) + + def test_invalid_shared_none(self): + msg = r'must be dict, not None' + with self.assertRaisesRegex(TypeError, msg): + _interpreters.exec(self.id, 'a', shared=None) + with self.assertRaisesRegex(TypeError, msg): + _interpreters.run_string(self.id, 'a', shared=None) + with self.assertRaisesRegex(TypeError, msg): + _interpreters.run_func(self.id, lambda: None, shared=None) + with self.assertRaisesRegex(TypeError, msg): + _interpreters.set___main___attrs(self.id, None) + + def test_invalid_shared_encoding(self): + # See https://github.com/python/cpython/issues/127196 + bad_shared = {"\uD82A": 0} + msg = 'surrogates not allowed' + with self.assertRaisesRegex(UnicodeEncodeError, msg): + _interpreters.exec(self.id, 'a', shared=bad_shared) + with self.assertRaisesRegex(UnicodeEncodeError, msg): + _interpreters.run_string(self.id, 'a', shared=bad_shared) + with self.assertRaisesRegex(UnicodeEncodeError, msg): + _interpreters.run_func(self.id, lambda: None, shared=bad_shared) + + +class RunStringTests(TestBase): + + def setUp(self): + super().setUp() + self.id = _interpreters.create() + + def test_success(self): + script, file = _captured_script('print("it worked!", end="")') + with file: + _interpreters.run_string(self.id, script) + out = file.read() + + self.assertEqual(out, 'it worked!') + + def test_in_thread(self): + script, file = _captured_script('print("it worked!", end="")') + with file: + def f(): + _interpreters.run_string(self.id, script) + + t = threading.Thread(target=f) + t.start() + t.join() + out = file.read() + + self.assertEqual(out, 'it worked!') + + def test_create_thread(self): + subinterp = _interpreters.create() + script, file = _captured_script(""" + import threading + def f(): + print('it worked!', end='') + + t = threading.Thread(target=f) + t.start() + t.join() + """) + with file: + _interpreters.run_string(subinterp, script) + out = file.read() + + self.assertEqual(out, 'it worked!') + + def test_create_daemon_thread(self): + with self.subTest('isolated'): + expected = 'spam spam spam spam spam' + subinterp = _interpreters.create('isolated') + script, file = _captured_script(f""" + import threading + def f(): + print('it worked!', end='') + + try: + t = threading.Thread(target=f, daemon=True) + t.start() + t.join() + except RuntimeError: + print('{expected}', end='') + """) + with file: + _interpreters.run_string(subinterp, script) + out = file.read() + + self.assertEqual(out, expected) + + with self.subTest('not isolated'): + subinterp = _interpreters.create('legacy') + script, file = _captured_script(""" + import threading + def f(): + print('it worked!', end='') + + t = threading.Thread(target=f, daemon=True) + t.start() + t.join() + """) + with file: + _interpreters.run_string(subinterp, script) + out = file.read() + + self.assertEqual(out, 'it worked!') + + def test_shareable_types(self): + interp = _interpreters.create() + objects = [ + None, + 'spam', + b'spam', + 42, + ] + for obj in objects: + with self.subTest(obj): + _interpreters.set___main___attrs(interp, dict(obj=obj)) + _interpreters.run_string( + interp, + f'assert(obj == {obj!r})', + ) + + def test_os_exec(self): + expected = 'spam spam spam spam spam' + subinterp = _interpreters.create() + script, file = _captured_script(f""" + import os, sys + try: + os.execl(sys.executable) + except RuntimeError: + print('{expected}', end='') + """) + with file: + _interpreters.run_string(subinterp, script) + out = file.read() + + self.assertEqual(out, expected) + + @support.requires_fork() + def test_fork(self): + import tempfile + with tempfile.NamedTemporaryFile('w+', encoding="utf-8") as file: + file.write('') + file.flush() + + expected = 'spam spam spam spam spam' + script = dedent(f""" + import os + try: + os.fork() + except RuntimeError: + with open('{file.name}', 'w', encoding='utf-8') as out: + out.write('{expected}') + """) + _interpreters.run_string(self.id, script) + + file.seek(0) + content = file.read() + self.assertEqual(content, expected) + + def test_already_running(self): + with _running(self.id): + with self.assertRaises(_interpreters.InterpreterError): + _interpreters.run_string(self.id, 'print("spam")') + + def test_does_not_exist(self): + id = 0 + while id in set(id for id, *_ in _interpreters.list_all()): + id += 1 + with self.assertRaises(InterpreterNotFoundError): + _interpreters.run_string(id, 'print("spam")') + + def test_error_id(self): + with self.assertRaises(ValueError): + _interpreters.run_string(-1, 'print("spam")') + + def test_bad_id(self): + with self.assertRaises(TypeError): + _interpreters.run_string('spam', 'print("spam")') + + def test_bad_script(self): + with self.assertRaises(TypeError): + _interpreters.run_string(self.id, 10) + + def test_bytes_for_script(self): + with self.assertRaises(TypeError): + _interpreters.run_string(self.id, b'print("spam")') + + def test_str_subclass_string(self): + class StrSubclass(str): pass + + output = _run_output(self.id, StrSubclass('print(1 + 2)')) + self.assertEqual(output, '3\n') + + def test_with_shared(self): + r, w = os.pipe() + + shared = { + 'spam': b'ham', + 'eggs': b'-1', + 'cheddar': None, + } + script = dedent(f""" + eggs = int(eggs) + spam = 42 + result = spam + eggs + + ns = dict(vars()) + del ns['__builtins__'] + import pickle + with open({w}, 'wb') as chan: + pickle.dump(ns, chan) + """) + _interpreters.set___main___attrs(self.id, shared) + _interpreters.run_string(self.id, script) + with open(r, 'rb') as chan: + ns = pickle.load(chan) + + self.assertEqual(ns['spam'], 42) + self.assertEqual(ns['eggs'], -1) + self.assertEqual(ns['result'], 41) + self.assertIsNone(ns['cheddar']) + + def test_shared_overwrites(self): + _interpreters.run_string(self.id, dedent(""" + spam = 'eggs' + ns1 = dict(vars()) + del ns1['__builtins__'] + """)) + + shared = {'spam': b'ham'} + script = dedent(""" + ns2 = dict(vars()) + del ns2['__builtins__'] + """) + _interpreters.set___main___attrs(self.id, shared) + _interpreters.run_string(self.id, script) + + r, w = os.pipe() + script = dedent(f""" + ns = dict(vars()) + del ns['__builtins__'] + import pickle + with open({w}, 'wb') as chan: + pickle.dump(ns, chan) + """) + _interpreters.run_string(self.id, script) + with open(r, 'rb') as chan: + ns = pickle.load(chan) + + self.assertEqual(ns['ns1']['spam'], 'eggs') + self.assertEqual(ns['ns2']['spam'], b'ham') + self.assertEqual(ns['spam'], b'ham') + + def test_shared_overwrites_default_vars(self): + r, w = os.pipe() + + shared = {'__name__': b'not __main__'} + script = dedent(f""" + spam = 42 + + ns = dict(vars()) + del ns['__builtins__'] + import pickle + with open({w}, 'wb') as chan: + pickle.dump(ns, chan) + """) + _interpreters.set___main___attrs(self.id, shared) + _interpreters.run_string(self.id, script) + with open(r, 'rb') as chan: + ns = pickle.load(chan) + + self.assertEqual(ns['__name__'], b'not __main__') + + def test_main_reused(self): + r, w = os.pipe() + _interpreters.run_string(self.id, dedent(f""" + spam = True + + ns = dict(vars()) + del ns['__builtins__'] + import pickle + with open({w}, 'wb') as chan: + pickle.dump(ns, chan) + del ns, pickle, chan + """)) + with open(r, 'rb') as chan: + ns1 = pickle.load(chan) + + r, w = os.pipe() + _interpreters.run_string(self.id, dedent(f""" + eggs = False + + ns = dict(vars()) + del ns['__builtins__'] + import pickle + with open({w}, 'wb') as chan: + pickle.dump(ns, chan) + """)) + with open(r, 'rb') as chan: + ns2 = pickle.load(chan) + + self.assertIn('spam', ns1) + self.assertNotIn('eggs', ns1) + self.assertIn('eggs', ns2) + self.assertIn('spam', ns2) + + def test_execution_namespace_is_main(self): + r, w = os.pipe() + + script = dedent(f""" + spam = 42 + + ns = dict(vars()) + ns['__builtins__'] = str(ns['__builtins__']) + import pickle + with open({w}, 'wb') as chan: + pickle.dump(ns, chan) + """) + _interpreters.run_string(self.id, script) + with open(r, 'rb') as chan: + ns = pickle.load(chan) + + ns.pop('__builtins__') + ns.pop('__loader__') + self.assertEqual(ns, { + '__name__': '__main__', + '__doc__': None, + '__package__': None, + '__spec__': None, + 'spam': 42, + }) + + # XXX Fix this test! + @unittest.skip('blocking forever') + def test_still_running_at_exit(self): + script = dedent(""" + from textwrap import dedent + import threading + import _interpreters + id = _interpreters.create() + def f(): + _interpreters.run_string(id, dedent(''' + import time + # Give plenty of time for the main interpreter to finish. + time.sleep(1_000_000) + ''')) + + t = threading.Thread(target=f) + t.start() + """) + with support.temp_dir() as dirname: + filename = script_helper.make_script(dirname, 'interp', script) + with script_helper.spawn_python(filename) as proc: + retcode = proc.wait() + + self.assertEqual(retcode, 0) + + +class RunFailedTests(TestBase): + + def setUp(self): + super().setUp() + self.id = _interpreters.create() + + def add_module(self, modname, text): + import tempfile + tempdir = tempfile.mkdtemp() + self.addCleanup(lambda: os_helper.rmtree(tempdir)) + _interpreters.run_string(self.id, dedent(f""" + import sys + sys.path.insert(0, {tempdir!r}) + """)) + return script_helper.make_script(tempdir, modname, text) + + def run_script(self, text, *, fails=False): + r, w = os.pipe() + try: + script = dedent(f""" + import os, sys + os.write({w}, b'0') + + # This raises an exception: + {{}} + + # Nothing from here down should ever run. + os.write({w}, b'1') + class NeverError(Exception): pass + raise NeverError # never raised + """).format(dedent(text)) + if fails: + err = _interpreters.run_string(self.id, script) + self.assertIsNot(err, None) + return err + else: + err = _interpreters.run_string(self.id, script) + self.assertIs(err, None) + return None + except: + raise # re-raise + else: + msg = os.read(r, 100) + self.assertEqual(msg, b'0') + finally: + os.close(r) + os.close(w) + + def _assert_run_failed(self, exctype, msg, script): + if isinstance(exctype, str): + exctype_name = exctype + exctype = None + else: + exctype_name = exctype.__name__ + + # Run the script. + excinfo = self.run_script(script, fails=True) + + # Check the wrapper exception. + self.assertEqual(excinfo.type.__name__, exctype_name) + if msg is None: + self.assertEqual(excinfo.formatted.split(':')[0], + exctype_name) + else: + self.assertEqual(excinfo.formatted, + '{}: {}'.format(exctype_name, msg)) + + return excinfo + + def assert_run_failed(self, exctype, script): + self._assert_run_failed(exctype, None, script) + + def assert_run_failed_msg(self, exctype, msg, script): + self._assert_run_failed(exctype, msg, script) + + def test_exit(self): + with self.subTest('sys.exit(0)'): + # XXX Should an unhandled SystemExit(0) be handled as not-an-error? + self.assert_run_failed(SystemExit, """ + sys.exit(0) + """) + + with self.subTest('sys.exit()'): + self.assert_run_failed(SystemExit, """ + import sys + sys.exit() + """) + + with self.subTest('sys.exit(42)'): + self.assert_run_failed_msg(SystemExit, '42', """ + import sys + sys.exit(42) + """) + + with self.subTest('SystemExit'): + self.assert_run_failed_msg(SystemExit, '42', """ + raise SystemExit(42) + """) + + # XXX Also check os._exit() (via a subprocess)? + + def test_plain_exception(self): + self.assert_run_failed_msg(Exception, 'spam', """ + raise Exception("spam") + """) + + def test_invalid_syntax(self): + script = dedent(""" + x = 1 + 2 + y = 2 + 4 + z = 4 + 8 + + # missing close paren + print("spam" + + if x + y + z < 20: + ... + """) + + with self.subTest('script'): + with self.assertRaises(SyntaxError): + _interpreters.run_string(self.id, script) + + with self.subTest('module'): + modname = 'spam_spam_spam' + filename = self.add_module(modname, script) + self.assert_run_failed(SyntaxError, f""" + import {modname} + """) + + def test_NameError(self): + self.assert_run_failed(NameError, """ + res = spam + eggs + """) + # XXX check preserved suggestions + + def test_AttributeError(self): + self.assert_run_failed(AttributeError, """ + object().spam + """) + # XXX check preserved suggestions + + def test_ExceptionGroup(self): + self.assert_run_failed(ExceptionGroup, """ + raise ExceptionGroup('exceptions', [ + Exception('spam'), + ImportError('eggs'), + ]) + """) + + def test_user_defined_exception(self): + self.assert_run_failed_msg('MyError', 'spam', """ + class MyError(Exception): + pass + raise MyError('spam') + """) + + +class RunFuncTests(TestBase): + + def setUp(self): + super().setUp() + self.id = _interpreters.create() + + def test_success(self): + r, w = os.pipe() + def script(): + global w + import contextlib + with open(w, 'w', encoding="utf-8") as spipe: + with contextlib.redirect_stdout(spipe): + print('it worked!', end='') + _interpreters.set___main___attrs(self.id, dict(w=w)) + _interpreters.run_func(self.id, script) + + with open(r, encoding="utf-8") as outfile: + out = outfile.read() + + self.assertEqual(out, 'it worked!') + + def test_in_thread(self): + r, w = os.pipe() + def script(): + global w + import contextlib + with open(w, 'w', encoding="utf-8") as spipe: + with contextlib.redirect_stdout(spipe): + print('it worked!', end='') + failed = None + def f(): + nonlocal failed + try: + _interpreters.set___main___attrs(self.id, dict(w=w)) + _interpreters.run_func(self.id, script) + except Exception as exc: + failed = exc + t = threading.Thread(target=f) + t.start() + t.join() + if failed: + raise Exception from failed + + with open(r, encoding="utf-8") as outfile: + out = outfile.read() + + self.assertEqual(out, 'it worked!') + + def test_code_object(self): + r, w = os.pipe() + + def script(): + global w + import contextlib + with open(w, 'w', encoding="utf-8") as spipe: + with contextlib.redirect_stdout(spipe): + print('it worked!', end='') + code = script.__code__ + _interpreters.set___main___attrs(self.id, dict(w=w)) + _interpreters.run_func(self.id, code) + + with open(r, encoding="utf-8") as outfile: + out = outfile.read() + + self.assertEqual(out, 'it worked!') + + def test_closure(self): + spam = True + def script(): + assert spam + with self.assertRaises(ValueError): + _interpreters.run_func(self.id, script) + + def test_return_value(self): + def script(): + return 'spam' + with self.assertRaises(ValueError): + _interpreters.run_func(self.id, script) + +# @unittest.skip("we're not quite there yet") + def test_args(self): + with self.subTest('args'): + def script(a, b=0): + assert a == b + with self.assertRaises(ValueError): + _interpreters.run_func(self.id, script) + + with self.subTest('*args'): + def script(*args): + assert not args + with self.assertRaises(ValueError): + _interpreters.run_func(self.id, script) + + with self.subTest('**kwargs'): + def script(**kwargs): + assert not kwargs + with self.assertRaises(ValueError): + _interpreters.run_func(self.id, script) + + with self.subTest('kwonly'): + def script(*, spam=True): + assert spam + with self.assertRaises(ValueError): + _interpreters.run_func(self.id, script) + + with self.subTest('posonly'): + def script(spam, /): + assert spam + with self.assertRaises(ValueError): + _interpreters.run_func(self.id, script) + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_android.py b/Lib/test/test_android.py index 31daafbc3d6..edab0fe71a6 100644 --- a/Lib/test/test_android.py +++ b/Lib/test/test_android.py @@ -1,4 +1,5 @@ import io +import platform import queue import re import subprocess @@ -16,6 +17,11 @@ if sys.platform != "android": raise unittest.SkipTest("Android-specific") +# Some API levels filter out consecutive identical lines and replace them with a +# "chatty" marker. +api_level = platform.android_ver().api_level +chatty_issue = (26 <= api_level <= 30) + # (name, level, fileno) STREAM_INFO = [("stdout", "I", 1), ("stderr", "W", 2)] @@ -174,15 +180,13 @@ def write(s, lines=None, *, write_len=None): write("\u0000b", [r"\xc0\x80b"]) write("a\u0000b", [r"a\xc0\x80b"]) - # Multi-line messages. Avoid identical consecutive lines, as - # they may activate "chatty" filtering and break the tests. - # - # Additional spaces will appear in the output where necessary to - # protect leading newlines. + # Multi-line messages. Additional spaces will appear in the output where + # necessary to protect leading newlines. write("\nx", [" "]) write("\na\n", ["x", "a"]) write("\n", [" "]) - write("\n\n", [" ", " "]) + if not chatty_issue: + write("\n\n", [" ", " "]) write("b\n", ["b"]) write("c\n\n", ["c", " "]) write("d\ne", ["d"]) @@ -201,7 +205,8 @@ def write(s, lines=None, *, write_len=None): write("\nx", [" ", "x"]) write("\na\n", [" ", "a"]) write("\n", [" "]) - write("\n\n", [" ", " "]) + if not chatty_issue: + write("\n\n", [" ", " "]) write("b\n", ["b"]) write("c\n\n", ["c", " "]) write("d\ne", ["d", "e"]) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index e376c44faa4..c8954b20740 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -7,7 +7,6 @@ import io import operator import os -import py_compile import shutil import stat import sys @@ -81,6 +80,108 @@ def test_skip_invalid_stdout(self): self.assertRegex(mocked_stderr.getvalue(), r'usage:') +class TestLazyImports(unittest.TestCase): + LAZY_IMPORTS = { + "_colorize", + "copy", + "difflib", + "shutil", + "textwrap", + "warnings", + } + def test_module_import(self): + import_helper.ensure_lazy_imports( + "argparse", + self.LAZY_IMPORTS, + ) + + def test_create_parser(self): + # Test imports are still unused after + # creating a parser + create_parser = "argparse.ArgumentParser()" + imported_modules = {"shutil"} + + import_helper.ensure_lazy_imports( + "argparse", + self.LAZY_IMPORTS - imported_modules, + additional_code=create_parser, + ) + + def test_add_subparser(self): + add_subparser = textwrap.dedent( + """ + parser = argparse.ArgumentParser() + parser.add_subparsers(dest='command', required=False) + """ + ) + imported_modules = {"shutil"} + + import_helper.ensure_lazy_imports( + "argparse", + self.LAZY_IMPORTS - imported_modules, + additional_code=add_subparser, + ) + + def test_parse_args(self): + example_parser = textwrap.dedent( + """ + parser = argparse.ArgumentParser(prog='PROG') + parser.add_argument('-f', '--foo') + parser.add_argument('bar') + parser.parse_args(['BAR']) + parser.parse_args(['BAR', '--foo', 'FOO']) + """ + ) + imported_modules = {"shutil"} + import_helper.ensure_lazy_imports( + "argparse", + self.LAZY_IMPORTS - imported_modules, + additional_code=example_parser + ) + + +class TestArgumentParserCopiable(unittest.TestCase): + def _get_parser(self): + parser = argparse.ArgumentParser(exit_on_error=False) + parser.add_argument('--foo', type=int, default=42) + parser.add_argument('bar', nargs='?', default='baz') + return parser + + @force_not_colorized + def test_copiable(self): + import copy + parser = self._get_parser() + parser2 = copy.copy(parser) + ns = parser2.parse_args(['--foo', '123', 'quux']) + self.assertEqual(ns.foo, 123) + self.assertEqual(ns.bar, 'quux') + ns2 = parser2.parse_args([]) + self.assertEqual(ns2.foo, 42) + self.assertEqual(ns2.bar, 'baz') + + # Test shallow copy also gets new arguments + parser.add_argument("--extra") + ns3 = parser2.parse_args(["--extra", "bar"]) + self.assertEqual(ns3.extra, "bar") + + @force_not_colorized + def test_deepcopiable(self): + import copy + parser = self._get_parser() + parser2 = copy.deepcopy(parser) + ns = parser2.parse_args(['--foo', '123', 'quux']) + self.assertEqual(ns.foo, 123) + self.assertEqual(ns.bar, 'quux') + ns2 = parser2.parse_args([]) + self.assertEqual(ns2.foo, 42) + self.assertEqual(ns2.bar, 'baz') + + # Test deep copy does not get new arguments + parser.add_argument("--extra") + with self.assertRaises(argparse.ArgumentError): + parser2.parse_args(["--extra", "bar"]) + + class TestArgumentParserPickleable(unittest.TestCase): @force_not_colorized @@ -602,13 +703,22 @@ class TestOptionalsShortLong(ParserTestCase): class TestOptionalsDest(ParserTestCase): """Tests various means of setting destination""" - argument_signatures = [Sig('--foo-bar'), Sig('--baz', dest='zabbaz')] + argument_signatures = [ + Sig('-x', '-foobar', '--foo-bar', '-barfoo', '-X'), + Sig('--baz', dest='zabbaz'), + Sig('-y', '-qux', '-Y'), + Sig('-z'), + ] failures = ['a'] successes = [ - ('--foo-bar f', NS(foo_bar='f', zabbaz=None)), - ('--baz g', NS(foo_bar=None, zabbaz='g')), - ('--foo-bar h --baz i', NS(foo_bar='h', zabbaz='i')), - ('--baz j --foo-bar k', NS(foo_bar='k', zabbaz='j')), + ('--foo-bar f', NS(foo_bar='f', zabbaz=None, qux=None, z=None)), + ('-x f', NS(foo_bar='f', zabbaz=None, qux=None, z=None)), + ('--baz g', NS(foo_bar=None, zabbaz='g', qux=None, z=None)), + ('--foo-bar h --baz i', NS(foo_bar='h', zabbaz='i', qux=None, z=None)), + ('--baz j --foo-bar k', NS(foo_bar='k', zabbaz='j', qux=None, z=None)), + ('-qux l', NS(foo_bar=None, zabbaz=None, qux='l', z=None)), + ('-y l', NS(foo_bar=None, zabbaz=None, qux='l', z=None)), + ('-z m', NS(foo_bar=None, zabbaz=None, qux=None, z='m')), ] @@ -817,6 +927,76 @@ def test_invalid_name(self): self.assertEqual(str(cm.exception), "invalid option name '--no-foo' for BooleanOptionalAction") +class TestBooleanOptionalActionSingleDash(ParserTestCase): + """Tests BooleanOptionalAction with single dash""" + + argument_signatures = [ + Sig('-foo', '-x', action=argparse.BooleanOptionalAction), + ] + failures = ['--foo', '--no-foo', '-no-foo', '-no-x', '-nox'] + successes = [ + ('', NS(foo=None)), + ('-foo', NS(foo=True)), + ('-nofoo', NS(foo=False)), + ('-x', NS(foo=True)), + ] + + def test_invalid_name(self): + parser = argparse.ArgumentParser() + with self.assertRaises(ValueError) as cm: + parser.add_argument('-nofoo', action=argparse.BooleanOptionalAction) + self.assertEqual(str(cm.exception), + "invalid option name '-nofoo' for BooleanOptionalAction") + +class TestBooleanOptionalActionAlternatePrefixChars(ParserTestCase): + """Tests BooleanOptionalAction with custom prefixes""" + + parser_signature = Sig(prefix_chars='+-', add_help=False) + argument_signatures = [Sig('++foo', action=argparse.BooleanOptionalAction)] + failures = ['--foo', '--no-foo'] + successes = [ + ('', NS(foo=None)), + ('++foo', NS(foo=True)), + ('++no-foo', NS(foo=False)), + ] + + def test_invalid_name(self): + parser = argparse.ArgumentParser(prefix_chars='+/') + with self.assertRaisesRegex(ValueError, + 'BooleanOptionalAction.*is not valid for positional arguments'): + parser.add_argument('--foo', action=argparse.BooleanOptionalAction) + with self.assertRaises(ValueError) as cm: + parser.add_argument('++no-foo', action=argparse.BooleanOptionalAction) + self.assertEqual(str(cm.exception), + "invalid option name '++no-foo' for BooleanOptionalAction") + +class TestBooleanOptionalActionSingleAlternatePrefixChar(ParserTestCase): + """Tests BooleanOptionalAction with single alternate prefix char""" + + parser_signature = Sig(prefix_chars='+/', add_help=False) + argument_signatures = [ + Sig('+foo', '+x', action=argparse.BooleanOptionalAction), + ] + failures = ['++foo', '++no-foo', '++nofoo', + '-no-foo', '-nofoo', '+no-foo', '-nofoo', + '+no-x', '+nox', '-no-x', '-nox'] + successes = [ + ('', NS(foo=None)), + ('+foo', NS(foo=True)), + ('+nofoo', NS(foo=False)), + ('+x', NS(foo=True)), + ] + + def test_invalid_name(self): + parser = argparse.ArgumentParser(prefix_chars='+/') + with self.assertRaisesRegex(ValueError, + 'BooleanOptionalAction.*is not valid for positional arguments'): + parser.add_argument('-foo', action=argparse.BooleanOptionalAction) + with self.assertRaises(ValueError) as cm: + parser.add_argument('+nofoo', action=argparse.BooleanOptionalAction) + self.assertEqual(str(cm.exception), + "invalid option name '+nofoo' for BooleanOptionalAction") + class TestBooleanOptionalActionRequired(ParserTestCase): """Tests BooleanOptionalAction required""" @@ -1856,7 +2036,7 @@ def test_r_1_replace(self): class StdStreamComparer: def __init__(self, attr): # We try to use the actual stdXXX.buffer attribute as our - # marker, but but under some test environments, + # marker, but under some test environments, # sys.stdout/err are replaced by io.StringIO which won't have .buffer, # so we use a sentinel simply to show that the tests do the right thing # for any buffer supporting object @@ -2304,11 +2484,12 @@ class TestNegativeNumber(ParserTestCase): ('--complex -1e-3j', NS(int=None, float=None, complex=-0.001j)), ] +@force_not_colorized_test_class class TestArgumentAndSubparserSuggestions(TestCase): """Test error handling and suggestion when a user makes a typo""" def test_wrong_argument_error_with_suggestions(self): - parser = ErrorRaisingArgumentParser(suggest_on_error=True) + parser = ErrorRaisingArgumentParser() parser.add_argument('foo', choices=['bar', 'baz']) with self.assertRaises(ArgumentParserError) as excinfo: parser.parse_args(('bazz',)) @@ -2328,7 +2509,7 @@ def test_wrong_argument_error_no_suggestions(self): ) def test_wrong_argument_subparsers_with_suggestions(self): - parser = ErrorRaisingArgumentParser(suggest_on_error=True) + parser = ErrorRaisingArgumentParser() subparsers = parser.add_subparsers(required=True) subparsers.add_parser('foo') subparsers.add_parser('bar') @@ -2352,18 +2533,19 @@ def test_wrong_argument_subparsers_no_suggestions(self): excinfo.exception.stderr, ) - def test_wrong_argument_no_suggestion_implicit(self): - parser = ErrorRaisingArgumentParser() + def test_wrong_argument_with_suggestion_explicit(self): + parser = ErrorRaisingArgumentParser(suggest_on_error=True) parser.add_argument('foo', choices=['bar', 'baz']) with self.assertRaises(ArgumentParserError) as excinfo: parser.parse_args(('bazz',)) self.assertIn( - "error: argument foo: invalid choice: 'bazz' (choose from 'bar', 'baz')", + "error: argument foo: invalid choice: 'bazz', maybe you meant" + " 'baz'? (choose from 'bar', 'baz')", excinfo.exception.stderr, ) def test_suggestions_choices_empty(self): - parser = ErrorRaisingArgumentParser(suggest_on_error=True) + parser = ErrorRaisingArgumentParser() parser.add_argument('foo', choices=[]) with self.assertRaises(ArgumentParserError) as excinfo: parser.parse_args(('bazz',)) @@ -2373,7 +2555,7 @@ def test_suggestions_choices_empty(self): ) def test_suggestions_choices_int(self): - parser = ErrorRaisingArgumentParser(suggest_on_error=True) + parser = ErrorRaisingArgumentParser() parser.add_argument('foo', choices=[1, 2]) with self.assertRaises(ArgumentParserError) as excinfo: parser.parse_args(('3',)) @@ -2383,7 +2565,7 @@ def test_suggestions_choices_int(self): ) def test_suggestions_choices_mixed_types(self): - parser = ErrorRaisingArgumentParser(suggest_on_error=True) + parser = ErrorRaisingArgumentParser() parser.add_argument('foo', choices=[1, '2']) with self.assertRaises(ArgumentParserError) as excinfo: parser.parse_args(('3',)) @@ -2710,6 +2892,16 @@ def test_optional_subparsers(self): ret = parser.parse_args(()) self.assertIsNone(ret.command) + def test_subparser_help_with_parent_required_optional(self): + parser = ErrorRaisingArgumentParser(prog='PROG') + parser.add_argument('--foo', required=True) + parser.add_argument('--bar') + subparsers = parser.add_subparsers() + parser_sub = subparsers.add_parser('sub') + parser_sub.add_argument('arg') + self.assertEqual(parser_sub.format_usage(), + 'usage: PROG --foo FOO sub [-h] arg\n') + def test_help(self): self.assertEqual(self.parser.format_usage(), 'usage: PROG [-h] [--foo] bar {1,2,3} ...\n') @@ -5626,6 +5818,11 @@ def custom_formatter(prog): a-very-long-command command that does something ''')) + def test_direct_formatter_instantiation(self): + formatter = argparse.HelpFormatter(prog="program") + formatter.add_usage(usage=None, actions=[], groups=[]) + help_text = formatter.format_help() + self.assertEqual(help_text, "usage: program\n") # ===================================== # Optional/Positional constructor tests @@ -5660,6 +5857,8 @@ def test_invalid_option_strings(self): self.assertTypeError('-', errmsg='dest= is required') self.assertTypeError('--', errmsg='dest= is required') self.assertTypeError('---', errmsg='dest= is required') + self.assertTypeError('-', '--', '---', + errmsg="dest= is required for options like '-', '--', '---'") def test_invalid_prefix(self): self.assertValueError('--foo', '+foo', @@ -6197,6 +6396,7 @@ def spam(string_to_convert): # Check that deprecated arguments output warning # ============================================== +@force_not_colorized_test_class class TestDeprecatedArguments(TestCase): def test_deprecated_option(self): @@ -6507,6 +6707,20 @@ def test_remainder(self): args = parser.parse_args(['--foo', 'a', '--', 'b', '--', 'c']) self.assertEqual(NS(foo='a', bar=['--', 'b', '--', 'c']), args) + def test_optional_remainder(self): + parser = argparse.ArgumentParser(exit_on_error=False) + parser.add_argument('--foo', nargs='...') + parser.add_argument('bar', nargs='*') + + args = parser.parse_args(['--', '--foo', 'a', 'b']) + self.assertEqual(NS(foo=None, bar=['--foo', 'a', 'b']), args) + args = parser.parse_args(['--foo', '--', 'a', 'b']) + self.assertEqual(NS(foo=['--', 'a', 'b'], bar=[]), args) + args = parser.parse_args(['--foo', 'a', '--', 'b']) + self.assertEqual(NS(foo=['a', '--', 'b'], bar=[]), args) + args = parser.parse_args(['--foo', 'a', 'b', '--']) + self.assertEqual(NS(foo=['a', 'b', '--'], bar=[]), args) + def test_subparser(self): parser = argparse.ArgumentParser(exit_on_error=False) parser.add_argument('foo') @@ -7084,9 +7298,8 @@ def make_script(self, dirname, basename, *, compiled=False): script_name = script_helper.make_script(dirname, basename, self.source) if not compiled: return script_name - py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) return pyc_file def make_zip_script(self, script_name, name_in_zip=None): @@ -7132,6 +7345,9 @@ def test_module_as_main_without_altering_argv(self): basename = 'module' + os_helper.FS_NONASCII modulename = f'{self.dirname}.{basename}' self.make_script(self.dirname, basename) + # The filesystem encoding may be non-UTF-8, + # but the runner runs in UTF-8 mode + modulename = os.fsencode(modulename).decode("utf-8", "surrogateescape") runner_source = textwrap.dedent(f'''\ import runpy runpy._run_module_as_main({modulename!r}, alter_argv=False) @@ -7243,6 +7459,13 @@ def test_argparse_color(self): choices=("Aaaaa", "Bbbbb", "Ccccc", "Ddddd"), help="pick one", ) + parser.add_argument( + "--optional8", + default="A", + metavar="X", + choices=("A", "B", "C"), + help="among %(choices)s, default is %(default)s", + ) parser.add_argument("+f") parser.add_argument("++bar") @@ -7268,6 +7491,8 @@ def test_argparse_color(self): short_b = self.theme.short_option label_b = self.theme.label pos_b = self.theme.action + default = self.theme.default + interp = self.theme.interpolated_value reset = self.theme.reset # Act @@ -7280,8 +7505,8 @@ def test_argparse_color(self): f"""\ {heading}usage: {reset}{prog}PROG{reset} [{short}-h{reset}] [{short}-v{reset} | {short}-q{reset}] [{short}-o{reset}] [{long}--optional2 {label}OPTIONAL2{reset}] [{long}--optional3 {label}{{X,Y,Z}}{reset}] [{long}--optional4 {label}{{X,Y,Z}}{reset}] [{long}--optional5 {label}{{X,Y,Z}}{reset}] [{long}--optional6 {label}{{X,Y,Z}}{reset}] - [{short}-p {label}{{Aaaaa,Bbbbb,Ccccc,Ddddd}}{reset}] [{short}+f {label}F{reset}] [{long}++bar {label}BAR{reset}] [{long}-+baz {label}BAZ{reset}] - [{short}-c {label}COUNT{reset}] + [{short}-p {label}{{Aaaaa,Bbbbb,Ccccc,Ddddd}}{reset}] [{long}--optional8 {label}X{reset}] [{short}+f {label}F{reset}] [{long}++bar {label}BAR{reset}] + [{long}-+baz {label}BAZ{reset}] [{short}-c {label}COUNT{reset}] {pos}x{reset} {pos}y{reset} {pos}this_indeed_is_a_very_long_action_name{reset} {pos}{{sub1,sub2}} ...{reset} Colorful help @@ -7294,17 +7519,18 @@ def test_argparse_color(self): {heading}options:{reset} {short_b}-h{reset}, {long_b}--help{reset} show this help message and exit - {short_b}-v{reset}, {long_b}--verbose{reset} more spam (default: False) - {short_b}-q{reset}, {long_b}--quiet{reset} less spam (default: False) + {short_b}-v{reset}, {long_b}--verbose{reset} more spam {default}(default: {reset}{interp}False{reset}{default}){reset} + {short_b}-q{reset}, {long_b}--quiet{reset} less spam {default}(default: {reset}{interp}False{reset}{default}){reset} {short_b}-o{reset}, {long_b}--optional1{reset} {long_b}--optional2{reset} {label_b}OPTIONAL2{reset} - pick one (default: None) + pick one {default}(default: {reset}{interp}None{reset}{default}){reset} {long_b}--optional3{reset} {label_b}{{X,Y,Z}}{reset} - {long_b}--optional4{reset} {label_b}{{X,Y,Z}}{reset} pick one (default: None) - {long_b}--optional5{reset} {label_b}{{X,Y,Z}}{reset} pick one (default: None) - {long_b}--optional6{reset} {label_b}{{X,Y,Z}}{reset} pick one (default: None) + {long_b}--optional4{reset} {label_b}{{X,Y,Z}}{reset} pick one {default}(default: {reset}{interp}None{reset}{default}){reset} + {long_b}--optional5{reset} {label_b}{{X,Y,Z}}{reset} pick one {default}(default: {reset}{interp}None{reset}{default}){reset} + {long_b}--optional6{reset} {label_b}{{X,Y,Z}}{reset} pick one {default}(default: {reset}{interp}None{reset}{default}){reset} {short_b}-p{reset}, {long_b}--optional7{reset} {label_b}{{Aaaaa,Bbbbb,Ccccc,Ddddd}}{reset} - pick one (default: None) + pick one {default}(default: {reset}{interp}None{reset}{default}){reset} + {long_b}--optional8{reset} {label_b}X{reset} among {interp}A, B, C{reset}, default is {interp}A{reset} {short_b}+f{reset} {label_b}F{reset} {long_b}++bar{reset} {label_b}BAR{reset} {long_b}-+baz{reset} {label_b}BAZ{reset} @@ -7370,6 +7596,62 @@ def test_argparse_color_custom_usage(self): ), ) + def test_argparse_color_wrapping_matches_uncolored(self): + # gh-142035: color codes must not affect where help text wraps. + # Stripping the escapes from colored help must yield exactly the + # same text as the uncolored help across representative widths. + def build(color, path="output.txt"): + parser = argparse.ArgumentParser(prog="PROG", color=color) + parser.add_argument( + "--mode", + default="auto", + choices=("auto", "fast", "slow"), + help="select the operating mode from the available choices " + "%(choices)s and note the default is %(default)s here", + ) + parser.add_argument( + "--path", + default=path, + help="write output to %(default)s and continue processing", + ) + return parser + + env = self.enterContext(os_helper.EnvironmentVarGuard()) + paths = ( + "output.txt", + "/var/lib/application/cache/unusually_long_generated_filename", + "production-read-only-replica", + ) + for path in paths: + for columns in ("80", "60", "45", "30", "20"): + with self.subTest(path=path, columns=columns): + env["COLUMNS"] = columns + colored = build(color=True, path=path).format_help() + plain = build(color=False, path=path).format_help() + self.assertIn( + f"{self.theme.interpolated_value}auto" + f"{self.theme.reset}", + colored, + ) + self.assertEqual(_colorize.decolor(colored), plain) + + def test_argparse_color_preserved_when_wrapping_between_words(self): + parser = argparse.ArgumentParser(prog="PROG", color=True) + parser.add_argument( + "--mode", default="auto", + help="select the %(default)s operating mode from the available " + "options and continue with several more words", + ) + + env = self.enterContext(os_helper.EnvironmentVarGuard()) + env["COLUMNS"] = "40" + help_text = parser.format_help() + + self.assertIn( + f"{self.theme.interpolated_value}auto{self.theme.reset}", + help_text, + ) + def test_custom_formatter_function(self): def custom_formatter(prog): return argparse.RawTextHelpFormatter(prog, indent_increment=5) @@ -7454,6 +7736,311 @@ def test_subparser_prog_is_stored_without_color(self): help_text = demo_parser.format_help() self.assertNotIn('\x1b[', help_text) + def test_error_and_warning_keywords_colorized(self): + parser = argparse.ArgumentParser(prog='PROG') + parser.add_argument('foo') + + with self.assertRaises(SystemExit): + with captured_stderr() as stderr: + parser.parse_args([]) + + err = stderr.getvalue() + error_color = self.theme.error + reset = self.theme.reset + self.assertIn(f'{error_color}error:{reset}', err) + + with captured_stderr() as stderr: + parser._warning('test warning') + + warn = stderr.getvalue() + warning_color = self.theme.warning + self.assertIn(f'{warning_color}warning:{reset}', warn) + + def test_error_and_warning_not_colorized_when_disabled(self): + parser = argparse.ArgumentParser(prog='PROG', color=False) + parser.add_argument('foo') + + with self.assertRaises(SystemExit): + with captured_stderr() as stderr: + parser.parse_args([]) + + err = stderr.getvalue() + self.assertNotIn('\x1b[', err) + self.assertIn('error:', err) + + with captured_stderr() as stderr: + parser._warning('test warning') + + warn = stderr.getvalue() + self.assertNotIn('\x1b[', warn) + self.assertIn('warning:', warn) + + def test_backtick_markup_in_epilog(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='Example: `python -m myapp --verbose`', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'Example: {prog_extra}python -m myapp --verbose{reset}', + help_text) + self.assertNotIn('`', help_text) + + def test_backtick_markup_in_description(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + description='Run `python myapp` or ``python -m myapp`` to start.', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn( + f'Run {prog_extra}python myapp{reset} or ' + f'{prog_extra}python -m myapp{reset} to start.', + help_text, + ) + self.assertNotIn("`", help_text) + + def test_backtick_markup_multiple(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='Try `app run` or ``app test``.', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'{prog_extra}app run{reset}', help_text) + self.assertIn(f'{prog_extra}app test{reset}', help_text) + self.assertNotIn('`', help_text) + + def test_backtick_markup_not_applied_when_color_disabled(self): + # When color is disabled, backticks are preserved as-is + parser = argparse.ArgumentParser( + prog='PROG', + color=False, + epilog='Examples: `python -m myapp` or ``python -m myapp --x``', + ) + + help_text = parser.format_help() + self.assertIn('`python -m myapp`', help_text) + self.assertIn('``python -m myapp --x``', help_text) + self.assertNotIn('\x1b[', help_text) + + def test_backtick_markup_with_format_string(self): + parser = argparse.ArgumentParser( + prog='myapp', + color=True, + epilog='Run `%(prog)s --help` for more info.', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'{prog_extra}myapp --help{reset}', help_text) + + def test_backtick_markup_in_subparser(self): + parser = argparse.ArgumentParser(prog='PROG', color=True) + subparsers = parser.add_subparsers() + sub = subparsers.add_parser( + 'sub', + description='Run `PROG sub --foo` to start.', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = sub.format_help() + self.assertIn(f'{prog_extra}PROG sub --foo{reset}', help_text) + + def test_backtick_markup_special_regex_chars(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='`grep "foo.*bar" | sort`', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'{prog_extra}grep "foo.*bar" | sort{reset}', help_text) + + def test_backtick_markup_in_argument_help(self): + parser = argparse.ArgumentParser(prog="PROG", color=True) + parser.add_argument("--foo", help="set the `foo` value") + parser.add_argument("--bar", help="set the ``bar`` value") + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f"set the {prog_extra}foo{reset} value", help_text) + self.assertIn(f"set the {prog_extra}bar{reset} value", help_text) + self.assertNotIn("`", help_text) + + def test_backtick_markup_in_argument_help_with_format(self): + parser = argparse.ArgumentParser(prog="PROG", color=True) + parser.add_argument( + "--foo", default="bar", help="set `foo` (default: %(default)s)" + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f"set {prog_extra}foo{reset}", help_text) + + def test_backtick_markup_in_argument_help_color_disabled(self): + parser = argparse.ArgumentParser(prog="PROG", color=False) + parser.add_argument("--foo", help="set the `foo` value") + + help_text = parser.format_help() + self.assertIn("set the `foo` value", help_text) + self.assertNotIn("\x1b[", help_text) + + def test_argument_help_interpolation_accepts_string_like_proxy(self): + class LazyStr: + def __init__(self, message): + self._message = message + + def __str__(self): + return self._message + + def __getattr__(self, name): + return getattr(str(self), name) + + def __contains__(self, item): + return item in self._message + + def __mod__(self, other): + return self._message % other + + parser = argparse.ArgumentParser(prog="PROG", color=True) + parser.add_argument( + "--foo", + default="bar", + help=LazyStr("foo (default: %(default)s)"), + ) + parser.add_argument( + "--baz", + help=LazyStr("baz plain text"), + ) + + interp = self.theme.interpolated_value + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f"foo (default: {interp}bar{reset})", help_text) + self.assertIn("baz plain text", help_text) + + def test_help_with_format_specifiers(self): + # GH-142950: format specifiers like %x should work with color=True + parser = argparse.ArgumentParser(prog='PROG', color=True) + parser.add_argument('--hex', type=int, default=255, + help='hex: %(default)x, alt: %(default)#x') + parser.add_argument('--zero', type=int, default=7, + help='zero: %(default)05d') + parser.add_argument('--str', default='test', + help='str: %(default)s') + parser.add_argument('--pct', type=int, default=50, + help='pct: %(default)d%%') + parser.add_argument('--literal', help='literal: 100%%') + parser.add_argument('--prog', help='prog: %(prog)s') + parser.add_argument('--type', type=int, help='type: %(type)s') + parser.add_argument('--choices', choices=['a', 'b'], + help='choices: %(choices)s') + + help_text = parser.format_help() + + interp = self.theme.interpolated_value + reset = self.theme.reset + + self.assertIn(f'hex: {interp}ff{reset}', help_text) + self.assertIn(f'alt: {interp}0xff{reset}', help_text) + self.assertIn(f'zero: {interp}00007{reset}', help_text) + self.assertIn(f'str: {interp}test{reset}', help_text) + self.assertIn(f'pct: {interp}50{reset}%', help_text) + self.assertIn('literal: 100%', help_text) + self.assertIn(f'prog: {interp}PROG{reset}', help_text) + self.assertIn(f'type: {interp}int{reset}', help_text) + self.assertIn(f'choices: {interp}a, b{reset}', help_text) + + def test_print_help_uses_target_file_for_color_decision(self): + parser = argparse.ArgumentParser(prog='PROG', color=True) + parser.add_argument('--opt') + output = io.StringIO() + calls = [] + + def fake_can_colorize(*, file=None): + calls.append(file) + return file is None + + with swap_attr(_colorize, 'can_colorize', fake_can_colorize): + parser.print_help(file=output) + + self.assertIs(calls[-1], output) + self.assertIn(output, calls) + self.assertNotIn('\x1b[', output.getvalue()) + + def test_print_usage_uses_target_file_for_color_decision(self): + parser = argparse.ArgumentParser(prog='PROG', color=True) + parser.add_argument('--opt') + output = io.StringIO() + calls = [] + + def fake_can_colorize(*, file=None): + calls.append(file) + return file is None + + with swap_attr(_colorize, 'can_colorize', fake_can_colorize): + parser.print_usage(file=output) + + self.assertIs(calls[-1], output) + self.assertIn(output, calls) + self.assertNotIn('\x1b[', output.getvalue()) + + def test_fake_color_theme_matches_real(self): + from argparse import _colorless_theme + + # Check the attributes match those of the 'real' theme + _colorize_nocolor = _colorize.get_theme(force_no_color=True).argparse + for k in _colorize_nocolor: + self.assertEqual( + getattr(_colorless_theme, k), getattr(_colorize_nocolor, k) + ) + + def test_fake_color_theme_raises(self): + from argparse import _colorless_theme + + # Make sure the _colorless_theme doesn't return empty strings + # for magic methods or private attributes + with self.assertRaises(AttributeError): + _colorless_theme.__unknown_dunder__ + + with self.assertRaises(AttributeError): + _colorless_theme._private_attribute + + +class TestModule(unittest.TestCase): + def test_deprecated__version__(self): + with self.assertWarnsRegex( + DeprecationWarning, + "'__version__' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(argparse, "__version__") + self.assertEqual(cm.filename, __file__) + def tearDownModule(): # Remove global references to avoid looking like we have refleaks. diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 1b727f3b1fe..01529a9a2c1 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -150,6 +150,29 @@ def test_ipaddr_info_no_inet_pton(self, m_socket): socket.SOCK_STREAM, socket.IPPROTO_TCP)) + def test_interleave_addrinfos(self): + self.maxDiff = None + SIX_A = (socket.AF_INET6, 0, 0, '', ('2001:db8::1', 1)) + SIX_B = (socket.AF_INET6, 0, 0, '', ('2001:db8::2', 2)) + SIX_C = (socket.AF_INET6, 0, 0, '', ('2001:db8::3', 3)) + SIX_D = (socket.AF_INET6, 0, 0, '', ('2001:db8::4', 4)) + FOUR_A = (socket.AF_INET, 0, 0, '', ('192.0.2.1', 5)) + FOUR_B = (socket.AF_INET, 0, 0, '', ('192.0.2.2', 6)) + FOUR_C = (socket.AF_INET, 0, 0, '', ('192.0.2.3', 7)) + FOUR_D = (socket.AF_INET, 0, 0, '', ('192.0.2.4', 8)) + + addrinfos = [SIX_A, SIX_B, SIX_C, FOUR_A, FOUR_B, FOUR_C, FOUR_D, SIX_D] + expected = [SIX_A, FOUR_A, SIX_B, FOUR_B, SIX_C, FOUR_C, SIX_D, FOUR_D] + + self.assertEqual(expected, base_events._interleave_addrinfos(addrinfos)) + + expected_fafc_2 = [SIX_A, SIX_B, FOUR_A, SIX_C, FOUR_B, SIX_D, FOUR_C, FOUR_D] + self.assertEqual( + expected_fafc_2, + base_events._interleave_addrinfos(addrinfos, first_address_family_count=2), + ) + + class BaseEventLoopTests(test_utils.TestCase): @@ -1053,6 +1076,76 @@ def test_asyncgen_finalization_by_gc_in_other_thread(self): test_utils.run_briefly(self.loop) self.assertTrue(status['finalized']) + @unittest.skipUnless(socket_helper.IPV6_ENABLED, 'no IPv6 support') + @patch_socket + def test_create_connection_happy_eyeballs(self, m_socket): + + class MyProto(asyncio.Protocol): + pass + + async def getaddrinfo(*args, **kw): + return [(socket.AF_INET6, 0, 0, '', ('2001:db8::1', 1)), + (socket.AF_INET, 0, 0, '', ('192.0.2.1', 5))] + + async def sock_connect(sock, address): + if address[0] == '2001:db8::1': + await asyncio.sleep(1) + sock.connect(address) + + # gh-151540: use a selector event loop instead of the platform + # default; the Windows proactor loop would register the mocked + # socket with a real IOCP handle instead of the mocked + # _add_reader/_add_writer below. + loop = asyncio.SelectorEventLoop() + loop._add_writer = mock.Mock() + loop._add_writer = mock.Mock() + loop._add_reader = mock.Mock() + loop.getaddrinfo = getaddrinfo + loop.sock_connect = sock_connect + + coro = loop.create_connection(MyProto, 'example.com', 80, happy_eyeballs_delay=0.3) + transport, protocol = loop.run_until_complete(coro) + try: + sock = transport._sock + sock.connect.assert_called_with(('192.0.2.1', 5)) + finally: + transport.close() + test_utils.run_briefly(loop) # allow transport to close + loop.close() + + @patch_socket + def test_create_connection_happy_eyeballs_ipv4_only(self, m_socket): + + class MyProto(asyncio.Protocol): + pass + + async def getaddrinfo(*args, **kw): + return [(socket.AF_INET, 0, 0, '', ('192.0.2.1', 5)), + (socket.AF_INET, 0, 0, '', ('192.0.2.2', 6))] + + async def sock_connect(sock, address): + if address[0] == '192.0.2.1': + await asyncio.sleep(1) + sock.connect(address) + + # gh-151540: see test_create_connection_happy_eyeballs above. + loop = asyncio.SelectorEventLoop() + loop._add_writer = mock.Mock() + loop._add_writer = mock.Mock() + loop._add_reader = mock.Mock() + loop.getaddrinfo = getaddrinfo + loop.sock_connect = sock_connect + + coro = loop.create_connection(MyProto, 'example.com', 80, happy_eyeballs_delay=0.3) + transport, protocol = loop.run_until_complete(coro) + try: + sock = transport._sock + sock.connect.assert_called_with(('192.0.2.2', 6)) + finally: + transport.close() + test_utils.run_briefly(loop) # allow transport to close + loop.close() + class MyProto(asyncio.Protocol): done = None @@ -1194,6 +1287,47 @@ def getaddrinfo(*args, **kw): self.loop.run_until_complete(coro) self.assertTrue(sock.close.called) + def test_create_connection_sock_transport_error_closes_sock(self): + # gh-153133: a user-provided socket is closed if the transport is + # never created. + sock = mock.Mock() + sock.type = socket.SOCK_STREAM + + def factory(): + raise ZeroDivisionError + + coro = self.loop.create_connection(factory, sock=sock) + with self.assertRaises(ZeroDivisionError): + self.loop.run_until_complete(coro) + self.assertTrue(sock.close.called) + + @patch_socket + def test_create_connection_transport_error_closes_sock(self, m_socket): + # gh-153133: an internally created socket is closed if the transport + # is never created. + sock = mock.Mock() + m_socket.socket.return_value = sock + + def getaddrinfo(*args, **kw): + fut = self.loop.create_future() + addr = (socket.AF_INET, socket.SOCK_STREAM, 0, '', + ('127.0.0.1', 80)) + fut.set_result([addr]) + return fut + self.loop.getaddrinfo = getaddrinfo + + async def sock_connect(sock, address): + return None + + def factory(): + raise ZeroDivisionError + + with mock.patch.object(self.loop, 'sock_connect', sock_connect): + coro = self.loop.create_connection(factory, '127.0.0.1', 80) + with self.assertRaises(ZeroDivisionError): + self.loop.run_until_complete(coro) + self.assertTrue(sock.close.called) + @patch_socket def test_create_connection_happy_eyeballs_empty_exceptions(self, m_socket): # See gh-135836: Fix IndexError when Happy Eyeballs algorithm @@ -1608,7 +1742,8 @@ def mock_make_ssl_transport(sock, protocol, sslcontext, waiter, server_side=False, server_hostname='python.org', ssl_handshake_timeout=handshake_timeout, - ssl_shutdown_timeout=shutdown_timeout) + ssl_shutdown_timeout=shutdown_timeout, + context=ANY) # Next try an explicit server_hostname. self.loop._make_ssl_transport.reset_mock() coro = self.loop.create_connection( @@ -1623,7 +1758,8 @@ def mock_make_ssl_transport(sock, protocol, sslcontext, waiter, server_side=False, server_hostname='perl.com', ssl_handshake_timeout=handshake_timeout, - ssl_shutdown_timeout=shutdown_timeout) + ssl_shutdown_timeout=shutdown_timeout, + context=ANY) # Finally try an explicit empty server_hostname. self.loop._make_ssl_transport.reset_mock() coro = self.loop.create_connection( @@ -1638,7 +1774,8 @@ def mock_make_ssl_transport(sock, protocol, sslcontext, waiter, server_side=False, server_hostname='', ssl_handshake_timeout=handshake_timeout, - ssl_shutdown_timeout=shutdown_timeout) + ssl_shutdown_timeout=shutdown_timeout, + context=ANY) def test_create_connection_no_ssl_server_hostname_errors(self): # When not using ssl, server_hostname must be None. @@ -2016,7 +2153,7 @@ def test_accept_connection_exception(self, m_log): constants.ACCEPT_RETRY_DELAY, # self.loop._start_serving mock.ANY, - MyProto, sock, None, None, mock.ANY, mock.ANY, mock.ANY) + MyProto, sock, None, None, mock.ANY, mock.ANY, mock.ANY, mock.ANY) def test_call_coroutine(self): async def simple_coroutine(): diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index 919d543b032..696541d8d00 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -33,6 +33,7 @@ from multiprocessing.util import _cleanup_tests as multiprocessing_cleanup_tests from test.test_asyncio import utils as test_utils from test import support +from test.support import os_helper from test.support import socket_helper from test.support import threading_helper from test.support import ALWAYS_EQ, LARGEST, SMALLEST @@ -559,6 +560,19 @@ def writer(data): r.close() self.assertEqual(read, data) + def test_writelines_empty_chunk(self): + # gh-155888: an empty chunk can never be drained, so never buffer it + rsock, wsock = socket.socketpair() + self.addCleanup(rsock.close) + + async def main(): + reader, writer = await asyncio.open_connection(sock=wsock) + writer.writelines([b'data', b'']) + writer.close() + await asyncio.wait_for(writer.wait_closed(), support.SHORT_TIMEOUT) + + self.loop.run_until_complete(main()) + @unittest.skipUnless(hasattr(signal, 'SIGKILL'), 'No SIGKILL') def test_add_signal_handler(self): caught = 0 @@ -1569,6 +1583,60 @@ def create_socket(): transport_1.close() transport_2.close() + def test_datagram_recvfrom_connection_reset_recovers(self): + # gh-127057: a UDP socket that sent a datagram to an address that + # wasn't listening can raise ConnectionResetError on a later + # receive. The transport must keep working afterwards. + loop = self.loop + + class Protocol(asyncio.DatagramProtocol): + def connection_made(self, transport): + self.transport = transport + self.errors = [] + self.received = [] + self.datagram_received_event = loop.create_future() + + def error_received(self, exc): + self.errors.append(exc) + + def datagram_received(self, data, addr): + self.received.append(data) + if not self.datagram_received_event.done(): + self.datagram_received_event.set_result(None) + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setblocking(False) + sock.bind(('127.0.0.1', 0)) + addr = sock.getsockname() + + # Bind and immediately close a second socket to get an address + # that is guaranteed not to be listening. + closed = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + closed.bind(('127.0.0.1', 0)) + closed_addr = closed.getsockname() + closed.close() + + # Trigger the error before the socket is wrapped in a transport, + # so that the first read raises synchronously. + sock.sendto(b'x', closed_addr) + + transport, protocol = loop.run_until_complete( + loop.create_datagram_endpoint(Protocol, sock=sock)) + + transport.sendto(b'ping', addr) + loop.run_until_complete(asyncio.wait_for( + protocol.datagram_received_event, support.SHORT_TIMEOUT)) + self.assertEqual(protocol.received, [b'ping']) + + if sys.platform == 'win32': + # Other platforms don't report ICMP errors on an + # unconnected UDP socket. + self.assertEqual(len(protocol.errors), 1) + self.assertIsInstance(protocol.errors[0], ConnectionResetError) + + transport.close() + test_utils.run_briefly(loop) + def test_internal_fds(self): loop = self.create_event_loop() if not isinstance(loop, selector_events.BaseSelectorEventLoop): @@ -1724,6 +1792,48 @@ def reader(data): self.loop.run_until_complete(proto.done) self.assertEqual('CLOSED', proto.state) + @unittest.skipUnless(sys.platform != 'win32', + "Don't support pipes for Windows") + @unittest.skipUnless(hasattr(os, 'mkfifo'), 'requires os.mkfifo()') + def test_write_named_fifo_unread_data(self): + # gh-145030: on macOS and Solaris, the write end of a named FIFO + # polls as readable while unread data sits in the FIFO, which made + # the transport misinterpret the event as the reader hanging up + # and close itself. + path = os_helper.TESTFN + os.mkfifo(path) + self.assertNotEqual(os.stat(path).st_nlink, 0) + self.addCleanup(os_helper.unlink, path) + rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK) + self.addCleanup(os.close, rfd) + wfd = os.open(path, os.O_WRONLY | os.O_NONBLOCK) + pipeobj = io.open(wfd, 'wb', 1024) + + proto = MyWritePipeProto(loop=self.loop) + connect = self.loop.connect_write_pipe(lambda: proto, pipeobj) + transport, p = self.loop.run_until_complete(connect) + self.assertIs(p, proto) + self.assertEqual('CONNECTED', proto.state) + + transport.write(b'1') + # Iterate the event loop while the data stays unread in the FIFO; + # the transport must not detect a false disconnection. + for _ in range(10): + test_utils.run_briefly(self.loop) + self.assertEqual('CONNECTED', proto.state) + self.assertFalse(transport.is_closing()) + self.assertEqual(b'1', os.read(rfd, 1024)) + + transport.write(b'2345') + for _ in range(10): + test_utils.run_briefly(self.loop) + self.assertEqual('CONNECTED', proto.state) + self.assertEqual(b'2345', os.read(rfd, 1024)) + + transport.close() + self.loop.run_until_complete(proto.done) + self.assertEqual('CLOSED', proto.state) + @unittest.skipUnless(sys.platform != 'win32', "Don't support pipes for Windows") def test_write_pipe_disconnect_on_close(self): @@ -3172,5 +3282,100 @@ def test_get_loop(self): events.AbstractServer().get_loop() +@unittest.skipIf(sys.platform == 'win32', 'Unix pipe transport semantics') +class UnixPipeObjectSupportTests(unittest.TestCase): + def setUp(self): + super().setUp() + self.loop = asyncio.SelectorEventLoop() + self.addCleanup(self.loop.close) + + def check_accepted(self, connect, pipeobj): + lost = self.loop.create_future() + + class Proto(asyncio.Protocol): + def connection_lost(self, exc): + if not lost.done(): + lost.set_result(exc) + + async def run(): + transport, protocol = await connect(Proto, pipeobj) + self.assertIsInstance(protocol, Proto) + self.assertFalse(pipeobj.closed) + transport.close() + self.assertIsNone(await lost) + + self.loop.run_until_complete(run()) + self.assertTrue(pipeobj.closed) + + def check_rejected(self, connect, pipeobj): + self.addCleanup(pipeobj.close) + with self.assertRaisesRegex(ValueError, 'Pipe transport is'): + self.loop.run_until_complete(connect(asyncio.Protocol, pipeobj)) + + def test_read_pipe(self): + rfd, wfd = os.pipe() + self.addCleanup(os.close, wfd) + self.check_accepted(self.loop.connect_read_pipe, open(rfd, 'rb', 0)) + + def test_write_pipe(self): + rfd, wfd = os.pipe() + self.addCleanup(os.close, rfd) + self.check_accepted(self.loop.connect_write_pipe, open(wfd, 'wb', 0)) + + def test_read_fifo(self): + path = os_helper.TESTFN + os.mkfifo(path) + self.addCleanup(os_helper.unlink, path) + rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK) + wfd = os.open(path, os.O_WRONLY) + self.addCleanup(os.close, wfd) + self.check_accepted(self.loop.connect_read_pipe, open(rfd, 'rb', 0)) + + def test_write_fifo(self): + path = os_helper.TESTFN + os.mkfifo(path) + self.addCleanup(os_helper.unlink, path) + rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK) + self.addCleanup(os.close, rfd) + wfd = os.open(path, os.O_WRONLY) + self.check_accepted(self.loop.connect_write_pipe, open(wfd, 'wb', 0)) + + def test_read_socket(self): + rsock, wsock = socket.socketpair() + self.addCleanup(wsock.close) + self.check_accepted(self.loop.connect_read_pipe, + open(rsock.detach(), 'rb', 0)) + + def test_write_socket(self): + rsock, wsock = socket.socketpair() + self.addCleanup(rsock.close) + self.check_accepted(self.loop.connect_write_pipe, + open(wsock.detach(), 'wb', 0)) + + @unittest.skipUnless(hasattr(os, 'openpty'), 'need os.openpty()') + def test_read_character_device(self): + master, slave = os.openpty() + self.addCleanup(os.close, slave) + self.check_accepted(self.loop.connect_read_pipe, open(master, 'rb', 0)) + + @unittest.skipUnless(hasattr(os, 'openpty'), 'need os.openpty()') + def test_write_character_device(self): + master, slave = os.openpty() + self.addCleanup(os.close, master) + self.check_accepted(self.loop.connect_write_pipe, open(slave, 'wb', 0)) + + def test_read_regular_file(self): + self.addCleanup(os_helper.unlink, os_helper.TESTFN) + with open(os_helper.TESTFN, 'wb') as f: + f.write(b'spam') + self.check_rejected(self.loop.connect_read_pipe, + open(os_helper.TESTFN, 'rb', 0)) + + def test_write_regular_file(self): + self.addCleanup(os_helper.unlink, os_helper.TESTFN) + self.check_rejected(self.loop.connect_write_pipe, + open(os_helper.TESTFN, 'wb', 0)) + + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_asyncio/test_free_threading.py b/Lib/test/test_asyncio/test_free_threading.py index d874ed00bd7..0e149dadd7f 100644 --- a/Lib/test/test_asyncio/test_free_threading.py +++ b/Lib/test/test_asyncio/test_free_threading.py @@ -165,6 +165,45 @@ async def main(): loop.set_task_factory(self.factory) r.run(main()) + def test_all_tasks_from_other_thread_includes_eager_tasks(self): + # gh-152020: all_tasks() called from another thread used to drop + # eager-started tasks on free-threaded builds. + loop = asyncio.new_event_loop() + + async def wait_forever(): + await asyncio.Event().wait() + + def eager_factory(loop, coro, **kwargs): + return self.factory(loop, coro, eager_start=True, **kwargs) + + async def setup(): + loop.set_task_factory(eager_factory) + eager = loop.create_task(wait_forever(), name="EAGER") + loop.set_task_factory(None) + normal = loop.create_task(wait_forever(), name="NORMAL") + return eager, normal + + async def teardown(): + tasks = [t for t in asyncio.all_tasks() + if t is not asyncio.current_task()] + for t in tasks: + t.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + + thread = threading.Thread(target=loop.run_forever) + thread.start() + try: + held = asyncio.run_coroutine_threadsafe(setup(), loop).result() + names = {t.get_name() for t in asyncio.all_tasks(loop)} + self.assertIn("NORMAL", names) + self.assertIn("EAGER", names) + del held + finally: + asyncio.run_coroutine_threadsafe(teardown(), loop).result() + loop.call_soon_threadsafe(loop.stop) + thread.join() + loop.close() + class TestPyFreeThreading(TestFreeThreading, TestCase): diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index 2f22fbccba4..ba02263dff0 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -146,6 +146,33 @@ async def main(): 'async generator CallStackTestBase.test_stack_async_gen..gen()', stack_for_gen_nested_call[1]) + async def test_stack_wait_for_non_positive_timeout(self): + # gh-157058: wait_for(fut, 0) must still record the waiter + cleanup = asyncio.Future() + + async def worker(): + try: + await asyncio.Future() + finally: + await cleanup + + async def probe(t): + await asyncio.wait_for(t, 0) + + t = asyncio.ensure_future(worker()) + p = asyncio.create_task(probe(t), name='probe') + for _ in range(5): + await asyncio.sleep(0) + + stack = capture_test_stack(fut=t) + + cleanup.set_result(None) + await asyncio.gather(p, t, return_exceptions=True) + + self.assertEqual(stack[0][2], [ + ['T', ['a _cancel_and_wait', 'a wait_for', 'a probe'], []], + ]) + async def test_stack_gather(self): stack_for_deep = None @@ -175,6 +202,29 @@ async def main(): ] ]) + async def test_stack_gather_survivor(self): + # gh-157213: a child that outlives gather() must not be shown as awaited + + async def fail(): + raise ValueError + + async def survivor(): + await asyncio.Future() + + t = asyncio.create_task(survivor(), name='survivor') + with self.assertRaises(ValueError): + await asyncio.gather(t, fail()) + + self.assertEqual(capture_test_stack(fut=t)[0], [ + 'T', + ['a survivor'], + [] + ]) + + t.cancel() + with self.assertRaises(asyncio.CancelledError): + await t + async def test_stack_shield(self): stack_for_shield = None @@ -271,6 +321,68 @@ async def main(t1, t2): ] ]) + async def test_stack_as_completed(self): + # gh-156523: as_completed() must record the awaiting task + stack_for_inner = None + + async def inner(): + await asyncio.sleep(0) + nonlocal stack_for_inner + stack_for_inner = capture_test_stack() + + async def main(t): + for f in asyncio.as_completed([t]): + await f + + t = asyncio.create_task(inner(), name='inner') + await main(t) + self.assertFalse(t._asyncio_awaited_by) + + self.assertEqual(stack_for_inner[0], [ + 'T', + ['s capture_test_stack', 'a inner'], + [ + ['T', + ['a get', 'a _wait_for_one', 'a main', + 'a test_stack_as_completed'], + [] + ] + ] + ]) + + async def test_stack_as_completed_timeout(self): + # gh-156523: the awaiting task must be dropped when as_completed() times out + stack_for_inner = None + + async def inner(): + nonlocal stack_for_inner + stack_for_inner = capture_test_stack() + await asyncio.sleep(3600) + + async def main(t): + with self.assertRaises(TimeoutError): + for f in asyncio.as_completed([t], timeout=0.01): + await f + + t = asyncio.create_task(inner(), name='inner') + await main(t) + self.assertFalse(t._asyncio_awaited_by) + t.cancel() + with self.assertRaises(asyncio.CancelledError): + await t + + self.assertEqual(stack_for_inner[0], [ + 'T', + ['s capture_test_stack', 'a inner'], + [ + ['T', + ['a get', 'a _wait_for_one', 'a main', + 'a test_stack_as_completed_timeout'], + [] + ] + ] + ]) + async def test_stack_task(self): stack_for_inner = None @@ -345,6 +457,44 @@ async def main(): self.assertTrue(stack_for_fut[1].startswith('* Future(id=')) + async def test_call_graph_finished_task(self): + # gh-156408: the call graph must not record a finished coroutine's None frame + async def boom(): + raise ValueError + + done = asyncio.create_task(asyncio.sleep(0), name='done') + failed = asyncio.create_task(boom(), name='failed') + cancelled = asyncio.create_task(asyncio.Event().wait(), name='cancelled') + cancelled.cancel() + await asyncio.gather(done, failed, cancelled, return_exceptions=True) + + for task in (done, failed, cancelled): + with self.subTest(task=task.get_name()): + buf = io.StringIO() + asyncio.print_call_graph(task, file=buf) + self.assertEqual(asyncio.capture_call_graph(task).call_stack, ()) + self.assertIn(f"name={task.get_name()!r}", buf.getvalue()) + + async def test_capture_call_graph_generator_keeps_caller_frames(self): + # gh-156988: sync gen should not clear the call chain + stack = None + + def gen(): + nonlocal stack + graph = asyncio.capture_call_graph() + stack = [entry.frame.f_code.co_name for entry in graph.call_stack] + yield + + def middle(): + for _ in gen(): + pass + + async def main(): + middle() + + await main() + self.assertEqual(stack[:3], ['gen', 'middle', 'main']) + @unittest.skipIf( not hasattr(asyncio.futures, "_c_future_add_to_awaited_by"), diff --git a/Lib/test/test_asyncio/test_selector_events.py b/Lib/test/test_asyncio/test_selector_events.py index 4bb5d4fb816..27b3af888b7 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -1122,6 +1122,49 @@ def _resume_writing(): self.assertEqual(transport.get_write_buffer_size(), 0) self.assertTrue(self.protocol.connection_lost.called) + def test_write_ready_resume_writing_closes(self): + # gh-156512: closing from resume_writing() must not lose the connection twice + self.sock.send.return_value = 2 + + def _resume_writing(): + transport.close() + + self.protocol.resume_writing.side_effect = _resume_writing + self.loop.call_exception_handler = mock.Mock() + + transport = self.socket_transport() + transport.set_write_buffer_limits(high=1, low=0) + transport.write(b'data') + + self.loop.writers[7]._run() + test_utils.run_briefly(self.loop) + + self.assertEqual(self.protocol.connection_lost.call_count, 1) + self.loop.call_exception_handler.assert_not_called() + + @unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg') + def test_write_sendmsg_resume_writing_closes(self): + # gh-156512: same as above, for the sendmsg write path + self.sock.send.return_value = 2 + self.sock.sendmsg.return_value = 2 + + def _resume_writing(): + transport.close() + + self.protocol.resume_writing.side_effect = _resume_writing + self.loop.call_exception_handler = mock.Mock() + + transport = self.socket_transport(sendmsg=True) + transport.set_write_buffer_limits(high=1, low=0) + transport.write(b'data') + + self.loop.writers[7]._run() + test_utils.run_briefly(self.loop) + + self.assertEqual(self.protocol.connection_lost.call_count, 1) + self.loop.call_exception_handler.assert_not_called() + + class SelectorSocketTransportBufferedProtocolTests(test_utils.TestCase): def setUp(self): diff --git a/Lib/test/test_asyncio/test_server.py b/Lib/test/test_asyncio/test_server.py index 5bd0f7e2af4..581ea47d2de 100644 --- a/Lib/test/test_asyncio/test_server.py +++ b/Lib/test/test_asyncio/test_server.py @@ -266,6 +266,38 @@ async def serve(rd, wr): await asyncio.sleep(0) self.assertTrue(task.done()) + async def test_close_with_hanging_client(self): + # Synchronize server cancellation only after the socket connection is + # accepted and this event is set + conn_event = asyncio.Event() + class Proto(asyncio.Protocol): + def connection_made(self, transport): + conn_event.set() + + loop = asyncio.get_running_loop() + srv = await loop.create_server(Proto, socket_helper.HOSTv4, 0) + + # Start the server + serve_forever_task = asyncio.create_task(srv.serve_forever()) + await asyncio.sleep(0) + + # Create a connection to server + addr = srv.sockets[0].getsockname() + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(addr) + self.addCleanup(sock.close) + + # Send a CancelledError into the server to emulate a Ctrl+C + # KeyboardInterrupt whilst the server is handling a hanging client + await conn_event.wait() + serve_forever_task.cancel() + + # Ensure the client is closed within a timeout + async with asyncio.timeout(0.5): + await srv.wait_closed() + + self.assertFalse(srv.is_serving()) + # Test the various corner cases of Unix server socket removal class UnixServerCleanupTests(unittest.IsolatedAsyncioTestCase): diff --git a/Lib/test/test_asyncio/test_server_context.py b/Lib/test/test_asyncio/test_server_context.py new file mode 100644 index 00000000000..3f15654a1af --- /dev/null +++ b/Lib/test/test_asyncio/test_server_context.py @@ -0,0 +1,314 @@ +import asyncio +import contextvars +import unittest +import sys + +from unittest import TestCase + +try: + import ssl +except ImportError: + ssl = None + +from test.test_asyncio import utils as test_utils + +def tearDownModule(): + asyncio.events._set_event_loop_policy(None) + +class ServerContextvarsTestCase: + loop_factory = None # To be defined in subclasses + server_ssl_context = None # To be defined in subclasses for SSL tests + client_ssl_context = None # To be defined in subclasses for SSL tests + + def run_coro(self, coro): + return asyncio.run(coro, loop_factory=self.loop_factory) + + def test_start_server1(self): + # Test that asyncio.start_server captures the context at the time of server creation + async def test(): + var = contextvars.ContextVar("var", default="default") + + async def handle_client(reader, writer): + value = var.get() + writer.write(value.encode()) + await writer.drain() + writer.close() + + server = await asyncio.start_server(handle_client, '127.0.0.1', 0, + ssl=self.server_ssl_context) + # change the value + var.set("after_server") + + async def client(addr): + reader, writer = await asyncio.open_connection(*addr, + ssl=self.client_ssl_context) + data = await reader.read(100) + writer.close() + await writer.wait_closed() + return data.decode() + + async with server: + addr = server.sockets[0].getsockname() + self.assertEqual(await client(addr), "default") + + self.assertEqual(var.get(), "after_server") + + self.run_coro(test()) + + def test_start_server2(self): + # Test that mutations to the context in one handler don't affect other handlers or the server's context + async def test(): + var = contextvars.ContextVar("var", default="default") + + async def handle_client(reader, writer): + value = var.get() + writer.write(value.encode()) + var.set("in_handler") + await writer.drain() + writer.close() + + server = await asyncio.start_server(handle_client, '127.0.0.1', 0, + ssl=self.server_ssl_context) + var.set("after_server") + + async def client(addr): + reader, writer = await asyncio.open_connection(*addr, + ssl=self.client_ssl_context) + data = await reader.read(100) + writer.close() + await writer.wait_closed() + return data.decode() + + async with server: + addr = server.sockets[0].getsockname() + self.assertEqual(await client(addr), "default") + self.assertEqual(await client(addr), "default") + self.assertEqual(await client(addr), "default") + + self.assertEqual(var.get(), "after_server") + + self.run_coro(test()) + + def test_start_server3(self): + # Test that mutations to context in concurrent handlers don't affect each other or the server's context + async def test(): + var = contextvars.ContextVar("var", default="default") + var.set("before_server") + + async def handle_client(reader, writer): + writer.write(var.get().encode()) + await writer.drain() + writer.close() + + server = await asyncio.start_server(handle_client, '127.0.0.1', 0, + ssl=self.server_ssl_context) + var.set("after_server") + + async def client(addr): + reader, writer = await asyncio.open_connection(*addr, + ssl=self.client_ssl_context) + data = await reader.read(100) + self.assertEqual(data.decode(), "before_server") + writer.close() + await writer.wait_closed() + + async with server: + addr = server.sockets[0].getsockname() + async with asyncio.TaskGroup() as tg: + for _ in range(100): + tg.create_task(client(addr)) + + self.assertEqual(var.get(), "after_server") + + self.run_coro(test()) + + def test_create_server1(self): + # Test that loop.create_server captures the context at the time of server creation + # and that mutations to the context in protocol callbacks don't affect the server's context + async def test(): + var = contextvars.ContextVar("var", default="default") + + class EchoProtocol(asyncio.Protocol): + def connection_made(self, transport): + self.transport = transport + value = var.get() + var.set("in_handler") + self.transport.write(value.encode()) + self.transport.close() + + server = await asyncio.get_running_loop().create_server( + lambda: EchoProtocol(), '127.0.0.1', 0, + ssl=self.server_ssl_context) + var.set("after_server") + + async def client(addr): + reader, writer = await asyncio.open_connection(*addr, + ssl=self.client_ssl_context) + data = await reader.read(100) + self.assertEqual(data.decode(), "default") + writer.close() + await writer.wait_closed() + + async with server: + addr = server.sockets[0].getsockname() + await client(addr) + + self.assertEqual(var.get(), "after_server") + + self.run_coro(test()) + + def test_create_server2(self): + # Test that mutations to context in one protocol instance don't affect other instances or the server's context + async def test(): + var = contextvars.ContextVar("var", default="default") + + class EchoProtocol(asyncio.Protocol): + def __init__(self): + super().__init__() + assert var.get() == "default", var.get() + def connection_made(self, transport): + self.transport = transport + value = var.get() + var.set("in_handler") + self.transport.write(value.encode()) + self.transport.close() + + server = await asyncio.get_running_loop().create_server( + lambda: EchoProtocol(), '127.0.0.1', 0, + ssl=self.server_ssl_context) + + var.set("after_server") + + async def client(addr, expected): + reader, writer = await asyncio.open_connection(*addr, + ssl=self.client_ssl_context) + data = await reader.read(100) + self.assertEqual(data.decode(), expected) + writer.close() + await writer.wait_closed() + + async with server: + addr = server.sockets[0].getsockname() + await client(addr, "default") + await client(addr, "default") + + self.assertEqual(var.get(), "after_server") + + self.run_coro(test()) + + def test_gh140947(self): + # See https://github.com/python/cpython/issues/140947 + + cvar1 = contextvars.ContextVar("cvar1") + cvar2 = contextvars.ContextVar("cvar2") + cvar3 = contextvars.ContextVar("cvar3") + results = {} + is_ssl = self.server_ssl_context is not None + + def capture_context(meth): + result = [] + for k,v in contextvars.copy_context().items(): + if k.name.startswith("cvar"): + result.append((k.name, v)) + results[meth] = sorted(result) + + class DemoProtocol(asyncio.Protocol): + def __init__(self, on_conn_lost): + self.transport = None + self.on_conn_lost = on_conn_lost + self.tasks = set() + + def connection_made(self, transport): + capture_context("connection_made") + self.transport = transport + + def data_received(self, data): + capture_context("data_received") + + task = asyncio.create_task(self.asgi()) + self.tasks.add(task) + task.add_done_callback(self.tasks.discard) + + self.transport.pause_reading() + + def connection_lost(self, exc): + capture_context("connection_lost") + if not self.on_conn_lost.done(): + self.on_conn_lost.set_result(True) + + async def asgi(self): + capture_context("asgi start") + cvar1.set(True) + # make sure that we only resume after the pause + # otherwise the resume does nothing + if is_ssl: + while not self.transport._ssl_protocol._app_reading_paused: + await asyncio.sleep(0.01) + else: + while not self.transport._paused: + await asyncio.sleep(0.01) + cvar2.set(True) + self.transport.resume_reading() + cvar3.set(True) + capture_context("asgi end") + + async def main(): + loop = asyncio.get_running_loop() + on_conn_lost = loop.create_future() + + server = await loop.create_server( + lambda: DemoProtocol(on_conn_lost), '127.0.0.1', 0, + ssl=self.server_ssl_context) + async with server: + addr = server.sockets[0].getsockname() + reader, writer = await asyncio.open_connection(*addr, + ssl=self.client_ssl_context) + writer.write(b"anything") + await writer.drain() + writer.close() + await writer.wait_closed() + await on_conn_lost + + self.run_coro(main()) + self.assertDictEqual(results, { + "connection_made": [], + "data_received": [], + "asgi start": [], + "asgi end": [("cvar1", True), ("cvar2", True), ("cvar3", True)], + "connection_lost": [], + }) + + +class AsyncioEventLoopTests(TestCase, ServerContextvarsTestCase): + loop_factory = staticmethod(asyncio.new_event_loop) + +@unittest.skipUnless(ssl, "SSL not available") +class AsyncioEventLoopSSLTests(AsyncioEventLoopTests): + def setUp(self): + super().setUp() + self.server_ssl_context = test_utils.simple_server_sslcontext() + self.client_ssl_context = test_utils.simple_client_sslcontext() + +if sys.platform == "win32": + class AsyncioProactorEventLoopTests(TestCase, ServerContextvarsTestCase): + loop_factory = asyncio.ProactorEventLoop + + class AsyncioSelectorEventLoopTests(TestCase, ServerContextvarsTestCase): + loop_factory = asyncio.SelectorEventLoop + + @unittest.skipUnless(ssl, "SSL not available") + class AsyncioProactorEventLoopSSLTests(AsyncioProactorEventLoopTests): + def setUp(self): + super().setUp() + self.server_ssl_context = test_utils.simple_server_sslcontext() + self.client_ssl_context = test_utils.simple_client_sslcontext() + + @unittest.skipUnless(ssl, "SSL not available") + class AsyncioSelectorEventLoopSSLTests(AsyncioSelectorEventLoopTests): + def setUp(self): + super().setUp() + self.server_ssl_context = test_utils.simple_server_sslcontext() + self.client_ssl_context = test_utils.simple_client_sslcontext() + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_asyncio/test_ssl.py b/Lib/test/test_asyncio/test_ssl.py index ca15fc3bdd4..784c784d261 100644 --- a/Lib/test/test_asyncio/test_ssl.py +++ b/Lib/test/test_asyncio/test_ssl.py @@ -1544,6 +1544,9 @@ async def client(addr): # This triggers bug gh-115514, also tested using mocks in # test.test_asyncio.test_selector_events.SelectorSocketTransportTests.test_write_buffer_after_close socket_transport = writer.transport._ssl_protocol._transport + # connection_lost may have already cleared _transport. + if socket_transport is None: + return class SocketWrapper: def __init__(self, sock) -> None: diff --git a/Lib/test/test_asyncio/test_sslproto.py b/Lib/test/test_asyncio/test_sslproto.py index 3e304c16642..45539702410 100644 --- a/Lib/test/test_asyncio/test_sslproto.py +++ b/Lib/test/test_asyncio/test_sslproto.py @@ -70,6 +70,50 @@ def test_handshake_timeout_negative(self): sslproto.SSLProtocol(self.loop, app_proto, sslcontext, waiter, ssl_handshake_timeout=-10) + def test_check_hostname_accepts_server_hostname(self): + # Supplying a server_hostname succeeds with check_hostname enabled. + sslcontext = test_utils.simple_client_sslcontext(disable_verify=False) + sslcontext.check_hostname = True + app_proto = mock.Mock() + waiter = mock.Mock() + + # No ValueError is raised from SSLProtocol with 'server_hostname'. + ssl_proto = sslproto.SSLProtocol(self.loop, app_proto, sslcontext, waiter, + server_hostname='example.org') + self.addCleanup(ssl_proto._app_transport.close) + + @support.subTests("server_hostname", [None, '']) + def test_check_hostname_requires_server_hostname(self, server_hostname): + # A caller-supplied context asking for hostname checking used to be + # taken through wrap_bio() with no name to check against, verifying + # the certificate chain but never the peer's identity. + # loop.start_tls() defaults server_hostname to None, and + # loop.create_connection() turns server_hostname='' into None here, + # so both reached that state. + sslcontext = test_utils.simple_client_sslcontext(disable_verify=False) + sslcontext.check_hostname = True + app_proto = mock.Mock() + waiter = mock.Mock() + + # Supplying an empty server_hostname fails with check_hostname enabled. + with self.assertRaisesRegex( + ValueError, + 'check_hostname requires server_hostname'): + sslproto.SSLProtocol(self.loop, app_proto, sslcontext, + waiter, + server_hostname=server_hostname) + + # Disabling check_hostname allows for an empty or unset server_hostname. + sslcontext.check_hostname = False + + ssl_proto = sslproto.SSLProtocol(self.loop, app_proto, sslcontext, waiter) + self.addCleanup(ssl_proto._app_transport.close) + + ssl_proto = sslproto.SSLProtocol(self.loop, app_proto, sslcontext, + waiter, + server_hostname=server_hostname) + self.addCleanup(ssl_proto._app_transport.close) + def test_eof_received_waiter(self): waiter = self.loop.create_future() ssl_proto = self.ssl_protocol(waiter=waiter) diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py index cae8c7c6f7c..352929327da 100644 --- a/Lib/test/test_asyncio/test_streams.py +++ b/Lib/test/test_asyncio/test_streams.py @@ -861,6 +861,34 @@ async def run_test(): self.loop.run_until_complete(run_test()) self.assertEqual(messages, []) + def test_streamwriter_start_tls_updates_reader_transport(self): + reader = asyncio.StreamReader(loop=self.loop) + protocol = asyncio.StreamReaderProtocol(reader, loop=self.loop) + old_transport = mock.Mock() + old_transport.get_extra_info.return_value = None + old_transport.is_closing.return_value = False + protocol.connection_made(old_transport) + + writer = asyncio.StreamWriter(old_transport, protocol, reader, self.loop) + + ssl_context = mock.sentinel.ssl_context + new_transport = mock.Mock() + new_transport.get_extra_info.return_value = ssl_context + self.loop.start_tls = mock.AsyncMock(return_value=new_transport) + + self.loop.run_until_complete(writer.start_tls(ssl_context)) + + self.loop.start_tls.assert_awaited_once_with( + old_transport, protocol, ssl_context, + server_side=False, server_hostname=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + ) + self.assertIs(writer.transport, new_transport) + self.assertIs(protocol._transport, new_transport) + self.assertIs(reader._transport, new_transport) + self.assertTrue(protocol._over_ssl) + def test_streamreader_constructor_without_loop(self): with self.assertRaisesRegex(RuntimeError, 'no current event loop'): asyncio.StreamReader() diff --git a/Lib/test/test_asyncio/test_subprocess.py b/Lib/test/test_asyncio/test_subprocess.py index 9dd596bbf27..e04f2833b5b 100644 --- a/Lib/test/test_asyncio/test_subprocess.py +++ b/Lib/test/test_asyncio/test_subprocess.py @@ -11,7 +11,7 @@ from asyncio import subprocess from test.test_asyncio import utils as test_utils from test import support -from test.support import os_helper, warnings_helper, gc_collect +from test.support import os_helper, gc_collect if not support.has_subprocess_support: raise unittest.SkipTest("test module requires subprocess") @@ -111,37 +111,6 @@ def test_subprocess_repr(self): ) transport.close() - def test_proc_exited_no_invalid_state_error_on_exit_waiters(self): - # gh-145541: when _connect_pipes hasn't completed (so - # _pipes_connected is False) and the process exits, _try_finish() - # sets the result on exit waiters. Then _call_connection_lost() must - # not call set_result() again on the same waiters. - self.loop.set_exception_handler( - lambda loop, context: self.fail( - f"unexpected exception: {context}") - ) - waiter = self.loop.create_future() - transport, protocol = self.create_transport(waiter) - - # Simulate a waiter registered via _wait() before the process exits. - exit_waiter = self.loop.create_future() - transport._exit_waiters.append(exit_waiter) - - # _connect_pipes hasn't completed, so _pipes_connected is False. - self.assertFalse(transport._pipes_connected) - - # Simulate process exit. _try_finish() will set the result on - # exit_waiter because _pipes_connected is False, and then schedule - # _call_connection_lost() because _pipes is empty (vacuously all - # disconnected). _call_connection_lost() must skip exit_waiter - # because it's already done. - transport._process_exited(6) - self.loop.run_until_complete(waiter) - - self.assertEqual(exit_waiter.result(), 6) - - transport.close() - class SubprocessMixin: @@ -250,7 +219,7 @@ def test_kill_issue43884(self): # kills the process and all its children. creationflags = CREATE_NEW_PROCESS_GROUP proc = self.loop.run_until_complete( - asyncio.create_subprocess_shell(blocking_shell_command, stdout=asyncio.subprocess.PIPE, + asyncio.create_subprocess_shell(blocking_shell_command, creationflags=creationflags) ) self.loop.run_until_complete(asyncio.sleep(1)) @@ -435,6 +404,46 @@ async def len_message(message): self.assertEqual(output.rstrip(), b'3') self.assertEqual(exitcode, 0) + def test_wait_even_if_pipe_is_open(self): + # gh-119710: Process.wait() must return once the process exits even + # if its stdout pipe is inherited by a grandchild that keeps it open, + # so the pipe never reaches EOF. Otherwise wait() hangs forever + # despite the returncode being known. + + async def run(): + # The grandchild inherits the child's stdin and stdout pipes and + # keeps both open after the child is killed. It writes "ready" + # so we know it has started, and exits once its stdin hits EOF. + code = textwrap.dedent("""\ + import subprocess, sys + subprocess.run([sys.executable, "-c", + "import sys; sys.stdout.write('ready');" + " sys.stdout.flush(); sys.stdin.read()"]) + """) + + proc = await asyncio.create_subprocess_exec( + sys.executable, "-c", code, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + try: + wait_proc = asyncio.create_task(proc.wait()) + # Wait until the grandchild holds the inherited pipes; this + # also lets the wait() task register its waiter. + await proc.stdout.readexactly(5) + proc.kill() + returncode = await asyncio.wait_for( + wait_proc, timeout=support.SHORT_TIMEOUT) + if sys.platform == 'win32': + self.assertIsInstance(returncode, int) + else: + self.assertEqual(-signal.SIGKILL, returncode) + finally: + proc.stdin.close() # let the grandchild exit + await proc.stdout.read() + + self.loop.run_until_complete(run()) + def test_empty_input(self): async def empty_input(): @@ -911,7 +920,6 @@ async def main(): self.loop.run_until_complete(main()) - @warnings_helper.ignore_warnings(category=ResourceWarning) def test_subprocess_read_pipe_cancelled(self): async def main(): loop = asyncio.get_running_loop() @@ -922,7 +930,6 @@ async def main(): asyncio.run(main()) gc_collect() - @warnings_helper.ignore_warnings(category=ResourceWarning) def test_subprocess_write_pipe_cancelled(self): async def main(): loop = asyncio.get_running_loop() @@ -933,7 +940,6 @@ async def main(): asyncio.run(main()) gc_collect() - @warnings_helper.ignore_warnings(category=ResourceWarning) def test_subprocess_read_write_pipe_cancelled(self): async def main(): loop = asyncio.get_running_loop() @@ -967,6 +973,45 @@ def test_watcher_implementation(self): else: self.assertIsInstance(watcher, unix_events._ThreadedChildWatcher) + @unittest.skipUnless(hasattr(os, 'waitid'), 'needs os.waitid()') + def test_send_signal_never_targets_reaped_pid(self): + # gh-127049: there must be no window between the child watcher + # reaping the child and asyncio publishing the exit in which + # send_signal() signals the freed (possibly recycled) PID. + reaped_pids = set() + stale_kills = [] + orig_waitpid = os.waitpid + orig_kill = os.kill + + def waitpid(pid, options): + res = orig_waitpid(pid, options) + if res[0] != 0: + reaped_pids.add(res[0]) + return res + + def kill(pid, sig): + if pid in reaped_pids: + stale_kills.append(pid) + return + orig_kill(pid, sig) + + async def run(): + with mock.patch('os.waitpid', waitpid), \ + mock.patch('os.kill', kill): + proc = await asyncio.create_subprocess_exec( + *PROGRAM_BLOCKED) + proc.kill() + deadline = self.loop.time() + support.SHORT_TIMEOUT + while proc.returncode is None: + if self.loop.time() > deadline: + self.fail('child exit was not published in time') + proc.kill() + await asyncio.sleep(0) + await proc.wait() + self.assertEqual(stale_kills, []) + + self.loop.run_until_complete(run()) + class SubprocessThreadedWatcherTests(SubprocessWatcherMixin, test_utils.TestCase): @@ -980,6 +1025,35 @@ def tearDown(self): unix_events.can_use_pidfd = self._original_can_use_pidfd return super().tearDown() + @unittest.skipUnless(hasattr(os, 'waitid'), 'needs os.waitid()') + def test_pid_not_reaped_before_exit_published(self): + # gh-127049: the watcher thread must wait for the child + # without reaping it: the PID must stay reserved until the + # event loop thread reaps it and publishes the exit as one + # atomic step. + async def run(): + proc = await asyncio.create_subprocess_exec( + *PROGRAM_BLOCKED) + thread = self.loop._watcher._threads.get(proc.pid) + self.assertIsNotNone(thread) + proc.kill() + # Wait for the watcher thread to observe the exit while + # the event loop cannot process the notification yet. + thread.join(support.SHORT_TIMEOUT) + self.assertFalse(thread.is_alive()) + # The exit has not been published yet... + self.assertIsNone(proc.returncode) + # ...so the child must still be an unreaped zombie and + # signalling its PID must still be safe. waitid() raises + # ChildProcessError if the PID was already reaped (and + # possibly recycled by the kernel). + os.waitid(os.P_PID, proc.pid, + os.WEXITED | os.WNOWAIT | os.WNOHANG) + proc.kill() + self.assertEqual(await proc.wait(), -signal.SIGKILL) + + self.loop.run_until_complete(run()) + @unittest.skipUnless( unix_events.can_use_pidfd(), "operating system does not support pidfds", diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index 834430f44e6..12438f1b8eb 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -1102,11 +1102,240 @@ async def throw_error(): # cancellation happens here and error is more understandable await asyncio.sleep(0) + async def test_taskgroup_cancel_children(self): + # (asserting that TimeoutError is not raised) + async with asyncio.timeout(1): + async with asyncio.TaskGroup() as tg: + tg.create_task(asyncio.sleep(10)) + tg.create_task(asyncio.sleep(10)) + await asyncio.sleep(0) + tg.cancel() + + async def test_taskgroup_cancel_body(self): + count = 0 + async with asyncio.TaskGroup() as tg: + tg.cancel() + count += 1 + await asyncio.sleep(0) + count += 1 + self.assertEqual(count, 1) + + async def test_taskgroup_cancel_idempotent(self): + count = 0 + async with asyncio.TaskGroup() as tg: + tg.cancel() + tg.cancel() + count += 1 + await asyncio.sleep(0) + count += 1 + self.assertEqual(count, 1) + + async def test_taskgroup_cancel_after_exit(self): + async with asyncio.TaskGroup() as tg: + await asyncio.sleep(0) + # (asserting that exception is not raised) + tg.cancel() + + async def test_taskgroup_cancel_before_enter(self): + tg = asyncio.TaskGroup() + tg.cancel() + count = 0 + async with tg: + count += 1 + await asyncio.sleep(0) + count += 1 + self.assertEqual(count, 1) + + async def test_taskgroup_cancel_before_create_task(self): + async with asyncio.TaskGroup() as tg: + tg.cancel() + # TODO: This behavior is not ideal. We'd rather have no exception + # raised, and the child task run until the first await. + with self.assertRaises(RuntimeError): + tg.create_task(asyncio.sleep(1)) + + async def test_taskgroup_cancel_from_child_before_first_suspension(self): + # gh-155418: an eager task can cancel the group before joining _tasks + async def child(tg): + tg.cancel() + await asyncio.sleep(10) + self.fail("the child was not cancelled") + + async with asyncio.TaskGroup() as tg: + task = tg.create_task(child(tg)) + self.assertTrue(task.cancelled()) + + async def test_taskgroup_cancel_keeps_outer_cancellation(self): + # gh-155433: any cancellation from outside the group must propagate. + cancelling = asyncio.Event() + release = asyncio.Event() + + async def child(): + try: + await asyncio.sleep(10) + finally: + # The group is cancelling: it has cancelled its parent task + # and is waiting for this task to finish. + cancelling.set() + await release.wait() + + async def body(): + async with asyncio.TaskGroup() as tg: + tg.create_task(child()) + await asyncio.sleep(0) + tg.cancel() + + task = asyncio.create_task(body()) + await cancelling.wait() + task.cancel('message') + release.set() + with self.assertRaises(asyncio.CancelledError) as cm: + await task + self.assertEqual('message', cm.exception.args[0]) + + async def test_taskgroup_cancel_before_exception(self): + async def raise_exc(parent_tg: asyncio.TaskGroup): + parent_tg.cancel() + raise RuntimeError + + with self.assertRaises(ExceptionGroup): + async with asyncio.TaskGroup() as tg: + tg.create_task(raise_exc(tg)) + await asyncio.sleep(1) + + async def test_taskgroup_cancel_after_exception(self): + async def raise_exc(parent_tg: asyncio.TaskGroup): + try: + raise RuntimeError + finally: + parent_tg.cancel() + + with self.assertRaises(ExceptionGroup): + async with asyncio.TaskGroup() as tg: + tg.create_task(raise_exc(tg)) + await asyncio.sleep(1) + + async def test_taskgroup_body_cancel_before_exception(self): + with self.assertRaises(ExceptionGroup): + async with asyncio.TaskGroup() as tg: + tg.cancel() + raise RuntimeError + + async def test_taskgroup_body_cancel_after_exception(self): + with self.assertRaises(ExceptionGroup): + async with asyncio.TaskGroup() as tg: + try: + raise RuntimeError + finally: + tg.cancel() + + async def test_taskgroup_cancel_one_winner(self): + async def race(*fns): + outcome = None + async def run(fn): + nonlocal outcome + outcome = await fn() + tg.cancel() + + async with asyncio.TaskGroup() as tg: + for fn in fns: + tg.create_task(run(fn)) + return outcome + + event = asyncio.Event() + record = [] + async def fn_1(): + record.append("1 started") + await event.wait() + record.append("1 finished") + return 1 + + async def fn_2(): + record.append("2 started") + await event.wait() + record.append("2 finished") + return 2 + + async def fn_3(): + record.append("3 started") + event.set() + await asyncio.sleep(10) + record.append("3 finished") + return 3 + + self.assertEqual(await race(fn_1, fn_2, fn_3), 1) + self.assertListEqual(record, ["1 started", "2 started", "3 started", "1 finished"]) + + async def test_taskgroup_generator_exit_01(self): + # GeneratorExit in a TaskGroup should be fine + async def gen(): + yield 1 + + async def fn(): + async with asyncio.TaskGroup() as tg: + async for n in gen(): + yield n + + g = fn() + await g.asend(None) + await g.aclose() + + async def test_taskgroup_generator_exit_02(self): + # A lone GeneratorExit in a task should still give an ExceptionGroup + async def t(): + raise GeneratorExit + + async def fn(): + async with asyncio.TaskGroup() as tg: + tg.create_task(t()) + + with self.assertRaises(BaseExceptionGroup) as cm: + await fn() + self.assertEqual(get_error_types(cm.exception), {GeneratorExit}) + + async def test_taskgroup_generator_exit_03(self): + # A GeneratorExit in one task and an error in another should + # still give an ExceptionGroup + async def t1(): + raise GeneratorExit + + async def t2(): + raise AssertionError('t2 failed') + + async def fn(): + async with asyncio.TaskGroup() as tg: + tg.create_task(t1()) + tg.create_task(t2()) + + with self.assertRaises(BaseExceptionGroup) as cm: + await fn() + + self.assertEqual(get_error_types(cm.exception), {GeneratorExit, AssertionError}) + + async def test_taskgroup_generator_exit_04(self): + event = asyncio.Event() + async def t(): + event.set() + raise AssertionError('t failed') + + async def fn(): + async with asyncio.TaskGroup() as tg: + tg.create_task(t()) + yield 1 + + g = fn() + await g.asend(None) + await event.wait() # wait for t() to run + + with self.assertRaises(BaseExceptionGroup) as cm: + await g.aclose() + + self.assertEqual(get_error_types(cm.exception), {GeneratorExit, AssertionError}) + class TestTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase): loop_factory = asyncio.EventLoop - class TestEagerTaskTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase): @staticmethod def loop_factory(): diff --git a/Lib/test/test_asyncio/test_tools.py b/Lib/test/test_asyncio/test_tools.py index 34e94830204..df934164eb9 100644 --- a/Lib/test/test_asyncio/test_tools.py +++ b/Lib/test/test_asyncio/test_tools.py @@ -2,12 +2,27 @@ from asyncio import tools -from collections import namedtuple +import _remote_debugging -FrameInfo = namedtuple('FrameInfo', ['funcname', 'filename', 'lineno']) -CoroInfo = namedtuple('CoroInfo', ['call_stack', 'task_name']) -TaskInfo = namedtuple('TaskInfo', ['task_id', 'task_name', 'coroutine_stack', 'awaited_by']) -AwaitedInfo = namedtuple('AwaitedInfo', ['thread_id', 'awaited_by']) + +def LocationInfo(lineno, end_lineno=None, col_offset=None, end_col_offset=None): + return _remote_debugging.LocationInfo((lineno, end_lineno, col_offset, end_col_offset)) + + +def FrameInfo(funcname, filename, location, opcode=None): + return _remote_debugging.FrameInfo((filename, location, funcname, opcode)) + + +def CoroInfo(call_stack, task_name): + return _remote_debugging.CoroInfo((call_stack, task_name)) + + +def TaskInfo(task_id, task_name, coroutine_stack, awaited_by): + return _remote_debugging.TaskInfo((task_id, task_name, coroutine_stack, awaited_by)) + + +def AwaitedInfo(thread_id, awaited_by): + return _remote_debugging.AwaitedInfo((thread_id, awaited_by)) # mock output of get_all_awaited_by function. @@ -33,33 +48,33 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("awaiter3", "/path/to/app.py", 130), - FrameInfo("awaiter2", "/path/to/app.py", 120), - FrameInfo("awaiter", "/path/to/app.py", 110) + FrameInfo("awaiter3", "/path/to/app.py", LocationInfo(130)), + FrameInfo("awaiter2", "/path/to/app.py", LocationInfo(120)), + FrameInfo("awaiter", "/path/to/app.py", LocationInfo(110)) ], task_name=4 ), CoroInfo( call_stack=[ - FrameInfo("awaiterB3", "/path/to/app.py", 190), - FrameInfo("awaiterB2", "/path/to/app.py", 180), - FrameInfo("awaiterB", "/path/to/app.py", 170) + FrameInfo("awaiterB3", "/path/to/app.py", LocationInfo(190)), + FrameInfo("awaiterB2", "/path/to/app.py", LocationInfo(180)), + FrameInfo("awaiterB", "/path/to/app.py", LocationInfo(170)) ], task_name=5 ), CoroInfo( call_stack=[ - FrameInfo("awaiterB3", "/path/to/app.py", 190), - FrameInfo("awaiterB2", "/path/to/app.py", 180), - FrameInfo("awaiterB", "/path/to/app.py", 170) + FrameInfo("awaiterB3", "/path/to/app.py", LocationInfo(190)), + FrameInfo("awaiterB2", "/path/to/app.py", LocationInfo(180)), + FrameInfo("awaiterB", "/path/to/app.py", LocationInfo(170)) ], task_name=6 ), CoroInfo( call_stack=[ - FrameInfo("awaiter3", "/path/to/app.py", 130), - FrameInfo("awaiter2", "/path/to/app.py", 120), - FrameInfo("awaiter", "/path/to/app.py", 110) + FrameInfo("awaiter3", "/path/to/app.py", LocationInfo(130)), + FrameInfo("awaiter2", "/path/to/app.py", LocationInfo(120)), + FrameInfo("awaiter", "/path/to/app.py", LocationInfo(110)) ], task_name=7 ) @@ -72,9 +87,9 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("_aexit", "", 0), - FrameInfo("__aexit__", "", 0), - FrameInfo("main", "", 0) + FrameInfo("_aexit", "", LocationInfo(0)), + FrameInfo("__aexit__", "", LocationInfo(0)), + FrameInfo("main", "", LocationInfo(0)) ], task_name=2 ) @@ -87,9 +102,9 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("_aexit", "", 0), - FrameInfo("__aexit__", "", 0), - FrameInfo("main", "", 0) + FrameInfo("_aexit", "", LocationInfo(0)), + FrameInfo("__aexit__", "", LocationInfo(0)), + FrameInfo("main", "", LocationInfo(0)) ], task_name=2 ) @@ -102,10 +117,10 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("_aexit", "", 0), - FrameInfo("__aexit__", "", 0), - FrameInfo("blocho_caller", "", 0), - FrameInfo("bloch", "", 0) + FrameInfo("_aexit", "", LocationInfo(0)), + FrameInfo("__aexit__", "", LocationInfo(0)), + FrameInfo("blocho_caller", "", LocationInfo(0)), + FrameInfo("bloch", "", LocationInfo(0)) ], task_name=8 ) @@ -118,10 +133,10 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("_aexit", "", 0), - FrameInfo("__aexit__", "", 0), - FrameInfo("blocho_caller", "", 0), - FrameInfo("bloch", "", 0) + FrameInfo("_aexit", "", LocationInfo(0)), + FrameInfo("__aexit__", "", LocationInfo(0)), + FrameInfo("blocho_caller", "", LocationInfo(0)), + FrameInfo("bloch", "", LocationInfo(0)) ], task_name=8 ) @@ -134,10 +149,10 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("_aexit", "", 0), - FrameInfo("__aexit__", "", 0), - FrameInfo("blocho_caller", "", 0), - FrameInfo("bloch", "", 0) + FrameInfo("_aexit", "", LocationInfo(0)), + FrameInfo("__aexit__", "", LocationInfo(0)), + FrameInfo("blocho_caller", "", LocationInfo(0)), + FrameInfo("bloch", "", LocationInfo(0)) ], task_name=9 ) @@ -150,10 +165,10 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("_aexit", "", 0), - FrameInfo("__aexit__", "", 0), - FrameInfo("blocho_caller", "", 0), - FrameInfo("bloch", "", 0) + FrameInfo("_aexit", "", LocationInfo(0)), + FrameInfo("__aexit__", "", LocationInfo(0)), + FrameInfo("blocho_caller", "", LocationInfo(0)), + FrameInfo("bloch", "", LocationInfo(0)) ], task_name=9 ) @@ -222,7 +237,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main2", "", 0)], + call_stack=[FrameInfo("main2", "", LocationInfo(0))], task_name=5 ) ] @@ -233,7 +248,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main2", "", 0)], + call_stack=[FrameInfo("main2", "", LocationInfo(0))], task_name=5 ) ] @@ -244,7 +259,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main2", "", 0)], + call_stack=[FrameInfo("main2", "", LocationInfo(0))], task_name=5 ) ] @@ -266,7 +281,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=1 ) ] @@ -277,7 +292,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=1 ) ] @@ -288,7 +303,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=1 ) ] @@ -347,7 +362,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=4 ) ] @@ -358,7 +373,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=4 ) ] @@ -369,7 +384,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=4 ) ] @@ -415,11 +430,11 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("awaiter2", "", 0)], + call_stack=[FrameInfo("awaiter2", "", LocationInfo(0))], task_name=4 ), CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=2 ) ] @@ -430,7 +445,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("awaiter", "", 0)], + call_stack=[FrameInfo("awaiter", "", LocationInfo(0))], task_name=3 ) ] @@ -462,9 +477,9 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("nested", "", 0), - FrameInfo("nested", "", 0), - FrameInfo("task_b", "", 0) + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("task_b", "", LocationInfo(0)) ], task_name=4 ) @@ -477,17 +492,17 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("nested", "", 0), - FrameInfo("nested", "", 0), - FrameInfo("task_c", "", 0) + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("task_c", "", LocationInfo(0)) ], task_name=5 ), CoroInfo( call_stack=[ - FrameInfo("nested", "", 0), - FrameInfo("nested", "", 0), - FrameInfo("task_a", "", 0) + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("task_a", "", LocationInfo(0)) ], task_name=3 ) @@ -500,8 +515,8 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("nested", "", 0), - FrameInfo("nested", "", 0) + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("nested", "", LocationInfo(0)) ], task_name=6 ) @@ -514,9 +529,9 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("nested", "", 0), - FrameInfo("nested", "", 0), - FrameInfo("task_b", "", 0) + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("task_b", "", LocationInfo(0)) ], task_name=4 ) @@ -553,33 +568,33 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("awaiter3", "", 0), - FrameInfo("awaiter2", "", 0), - FrameInfo("awaiter", "", 0) + FrameInfo("awaiter3", "", LocationInfo(0)), + FrameInfo("awaiter2", "", LocationInfo(0)), + FrameInfo("awaiter", "", LocationInfo(0)) ], task_name=4 ), CoroInfo( call_stack=[ - FrameInfo("awaiter1_3", "", 0), - FrameInfo("awaiter1_2", "", 0), - FrameInfo("awaiter1", "", 0) + FrameInfo("awaiter1_3", "", LocationInfo(0)), + FrameInfo("awaiter1_2", "", LocationInfo(0)), + FrameInfo("awaiter1", "", LocationInfo(0)) ], task_name=5 ), CoroInfo( call_stack=[ - FrameInfo("awaiter1_3", "", 0), - FrameInfo("awaiter1_2", "", 0), - FrameInfo("awaiter1", "", 0) + FrameInfo("awaiter1_3", "", LocationInfo(0)), + FrameInfo("awaiter1_2", "", LocationInfo(0)), + FrameInfo("awaiter1", "", LocationInfo(0)) ], task_name=6 ), CoroInfo( call_stack=[ - FrameInfo("awaiter3", "", 0), - FrameInfo("awaiter2", "", 0), - FrameInfo("awaiter", "", 0) + FrameInfo("awaiter3", "", LocationInfo(0)), + FrameInfo("awaiter2", "", LocationInfo(0)), + FrameInfo("awaiter", "", LocationInfo(0)) ], task_name=7 ) @@ -592,9 +607,9 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("_aexit", "", 0), - FrameInfo("__aexit__", "", 0), - FrameInfo("main", "", 0) + FrameInfo("_aexit", "", LocationInfo(0)), + FrameInfo("__aexit__", "", LocationInfo(0)), + FrameInfo("main", "", LocationInfo(0)) ], task_name=2 ) @@ -607,9 +622,9 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("_aexit", "", 0), - FrameInfo("__aexit__", "", 0), - FrameInfo("main", "", 0) + FrameInfo("_aexit", "", LocationInfo(0)), + FrameInfo("__aexit__", "", LocationInfo(0)), + FrameInfo("main", "", LocationInfo(0)) ], task_name=2 ) @@ -622,10 +637,10 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("_aexit", "", 0), - FrameInfo("__aexit__", "", 0), - FrameInfo("blocho_caller", "", 0), - FrameInfo("bloch", "", 0) + FrameInfo("_aexit", "", LocationInfo(0)), + FrameInfo("__aexit__", "", LocationInfo(0)), + FrameInfo("blocho_caller", "", LocationInfo(0)), + FrameInfo("bloch", "", LocationInfo(0)) ], task_name=8 ) @@ -638,10 +653,10 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("_aexit", "", 0), - FrameInfo("__aexit__", "", 0), - FrameInfo("blocho_caller", "", 0), - FrameInfo("bloch", "", 0) + FrameInfo("_aexit", "", LocationInfo(0)), + FrameInfo("__aexit__", "", LocationInfo(0)), + FrameInfo("blocho_caller", "", LocationInfo(0)), + FrameInfo("bloch", "", LocationInfo(0)) ], task_name=8 ) @@ -654,10 +669,10 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("_aexit", "", 0), - FrameInfo("__aexit__", "", 0), - FrameInfo("blocho_caller", "", 0), - FrameInfo("bloch", "", 0) + FrameInfo("_aexit", "", LocationInfo(0)), + FrameInfo("__aexit__", "", LocationInfo(0)), + FrameInfo("blocho_caller", "", LocationInfo(0)), + FrameInfo("bloch", "", LocationInfo(0)) ], task_name=9 ) @@ -670,10 +685,10 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("_aexit", "", 0), - FrameInfo("__aexit__", "", 0), - FrameInfo("blocho_caller", "", 0), - FrameInfo("bloch", "", 0) + FrameInfo("_aexit", "", LocationInfo(0)), + FrameInfo("__aexit__", "", LocationInfo(0)), + FrameInfo("blocho_caller", "", LocationInfo(0)), + FrameInfo("bloch", "", LocationInfo(0)) ], task_name=9 ) @@ -797,7 +812,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main2", "", 0)], + call_stack=[FrameInfo("main2", "", LocationInfo(0))], task_name=5 ) ] @@ -808,7 +823,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main2", "", 0)], + call_stack=[FrameInfo("main2", "", LocationInfo(0))], task_name=5 ) ] @@ -819,7 +834,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main2", "", 0)], + call_stack=[FrameInfo("main2", "", LocationInfo(0))], task_name=5 ) ] @@ -841,7 +856,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=1 ) ] @@ -852,7 +867,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=1 ) ] @@ -863,7 +878,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=1 ) ] @@ -916,7 +931,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=4 ) ] @@ -927,7 +942,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=4 ) ] @@ -938,7 +953,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=4 ) ] @@ -979,11 +994,11 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("awaiter2", "", 0)], + call_stack=[FrameInfo("awaiter2", "", LocationInfo(0))], task_name=4 ), CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=2 ) ] @@ -994,7 +1009,7 @@ coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("awaiter", "", 0)], + call_stack=[FrameInfo("awaiter", "", LocationInfo(0))], task_name=3 ) ] @@ -1033,9 +1048,9 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("nested", "", 0), - FrameInfo("nested", "", 0), - FrameInfo("task_b", "", 0) + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("task_b", "", LocationInfo(0)) ], task_name=4 ) @@ -1048,17 +1063,17 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("nested", "", 0), - FrameInfo("nested", "", 0), - FrameInfo("task_c", "", 0) + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("task_c", "", LocationInfo(0)) ], task_name=5 ), CoroInfo( call_stack=[ - FrameInfo("nested", "", 0), - FrameInfo("nested", "", 0), - FrameInfo("task_a", "", 0) + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("task_a", "", LocationInfo(0)) ], task_name=3 ) @@ -1071,8 +1086,8 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("nested", "", 0), - FrameInfo("nested", "", 0) + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("nested", "", LocationInfo(0)) ], task_name=6 ) @@ -1085,9 +1100,9 @@ awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("nested", "", 0), - FrameInfo("nested", "", 0), - FrameInfo("task_b", "", 0) + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("nested", "", LocationInfo(0)), + FrameInfo("task_b", "", LocationInfo(0)) ], task_name=4 ) @@ -1290,7 +1305,7 @@ def test_cycle_detection(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=3 ) ] @@ -1301,7 +1316,7 @@ def test_cycle_detection(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=2 ) ] @@ -1331,7 +1346,7 @@ def test_complex_tree(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=2 ) ] @@ -1342,7 +1357,7 @@ def test_complex_tree(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=3 ) ] @@ -1379,7 +1394,7 @@ def test_complex_table(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=2 ) ] @@ -1390,7 +1405,7 @@ def test_complex_table(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("main", "", 0)], + call_stack=[FrameInfo("main", "", LocationInfo(0))], task_name=3 ) ] @@ -1417,11 +1432,11 @@ def test_deep_coroutine_chain(self): awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("c1", "", 0), - FrameInfo("c2", "", 0), - FrameInfo("c3", "", 0), - FrameInfo("c4", "", 0), - FrameInfo("c5", "", 0) + FrameInfo("c1", "", LocationInfo(0)), + FrameInfo("c2", "", LocationInfo(0)), + FrameInfo("c3", "", LocationInfo(0)), + FrameInfo("c4", "", LocationInfo(0)), + FrameInfo("c5", "", LocationInfo(0)) ], task_name=11 ) @@ -1461,7 +1476,7 @@ def test_multiple_cycles_same_node(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("call1", "", 0)], + call_stack=[FrameInfo("call1", "", LocationInfo(0))], task_name=2 ) ] @@ -1472,7 +1487,7 @@ def test_multiple_cycles_same_node(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("call2", "", 0)], + call_stack=[FrameInfo("call2", "", LocationInfo(0))], task_name=3 ) ] @@ -1483,11 +1498,11 @@ def test_multiple_cycles_same_node(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("call3", "", 0)], + call_stack=[FrameInfo("call3", "", LocationInfo(0))], task_name=1 ), CoroInfo( - call_stack=[FrameInfo("call4", "", 0)], + call_stack=[FrameInfo("call4", "", LocationInfo(0))], task_name=2 ) ] @@ -1511,7 +1526,7 @@ def test_table_output_format(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("foo", "", 0)], + call_stack=[FrameInfo("foo", "", LocationInfo(0))], task_name=2 ) ] @@ -1555,7 +1570,7 @@ def test_task_awaits_self(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("loopback", "", 0)], + call_stack=[FrameInfo("loopback", "", LocationInfo(0))], task_name=1 ) ] @@ -1579,7 +1594,7 @@ def test_task_with_missing_awaiter_id(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("coro", "", 0)], + call_stack=[FrameInfo("coro", "", LocationInfo(0))], task_name=999 ) ] @@ -1603,11 +1618,11 @@ def test_duplicate_coroutine_frames(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("frameA", "", 0)], + call_stack=[FrameInfo("frameA", "", LocationInfo(0))], task_name=2 ), CoroInfo( - call_stack=[FrameInfo("frameA", "", 0)], + call_stack=[FrameInfo("frameA", "", LocationInfo(0))], task_name=3 ) ] @@ -1651,7 +1666,7 @@ def test_task_with_no_name(self): coroutine_stack=[], awaited_by=[ CoroInfo( - call_stack=[FrameInfo("f1", "", 0)], + call_stack=[FrameInfo("f1", "", LocationInfo(0))], task_name=2 ) ] @@ -1682,8 +1697,8 @@ def test_tree_rendering_with_custom_emojis(self): awaited_by=[ CoroInfo( call_stack=[ - FrameInfo("f1", "", 0), - FrameInfo("f2", "", 0) + FrameInfo("f1", "", LocationInfo(0)), + FrameInfo("f2", "", LocationInfo(0)) ], task_name=2 ) diff --git a/Lib/test/test_asyncio/test_unix_events.py b/Lib/test/test_asyncio/test_unix_events.py index 520f5c733c3..118cb866997 100644 --- a/Lib/test/test_asyncio/test_unix_events.py +++ b/Lib/test/test_asyncio/test_unix_events.py @@ -15,7 +15,7 @@ from unittest import mock from test import support -from test.support import os_helper +from test.support import os_helper, warnings_helper from test.support import socket_helper from test.support import wait_process from test.support import hashlib_helper @@ -1182,6 +1182,7 @@ async def runner(): @support.requires_fork() class TestFork(unittest.TestCase): + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_fork_not_share_current_task(self): loop = object() task = object() @@ -1214,6 +1215,7 @@ def test_fork_not_share_current_task(self): self.assertEqual(result, b'NO TASK') wait_process(pid, exitcode=0) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_fork_not_share_event_loop(self): # The forked process should not share the event loop with the parent loop = object() @@ -1241,6 +1243,7 @@ def test_fork_not_share_event_loop(self): self.assertEqual(result, b'NO LOOP') wait_process(pid, exitcode=0) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @hashlib_helper.requires_hashdigest('md5') @support.skip_if_sanitizer("TSAN doesn't support threads after fork", thread=True) def test_fork_signal_handling(self): @@ -1288,6 +1291,7 @@ async def func(): self.assertFalse(parent_handled.is_set()) self.assertTrue(child_handled.is_set()) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @hashlib_helper.requires_hashdigest('md5') @support.skip_if_sanitizer("TSAN doesn't support threads after fork", thread=True) def test_fork_asyncio_run(self): @@ -1308,6 +1312,7 @@ async def child_main(): self.assertEqual(result.value, 42) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @hashlib_helper.requires_hashdigest('md5') @support.skip_if_sanitizer("TSAN doesn't support threads after fork", thread=True) def test_fork_asyncio_subprocess(self): @@ -1328,5 +1333,45 @@ async def child_main(): self.assertEqual(result.value, 0) + +@unittest.skipUnless( + unix_events.can_use_pidfd(), + "operating system does not support pidfd", +) +class PidfdChildWatcherTests(test_utils.TestCase): + + def setUp(self): + super().setUp() + self.loop = asyncio.new_event_loop() + self.set_event_loop(self.loop) + + def test_pidfd_closed_when_waitpid_raises(self): + # _do_wait() must close the pidfd even when waitpid() + # fails with something other than ChildProcessError, otherwise the + # pidfd is leaked + self.loop.set_exception_handler(lambda loop, context: None) + + async def coro(): + before = os_helper.fd_count() + proc = await asyncio.create_subprocess_exec( + sys.executable, '-c', 'import sys; sys.stdin.read()', + stdin=asyncio.subprocess.PIPE + ) + + with mock.patch.object(os, 'waitpid', + side_effect=OSError('unexpected')) as m: + proc.stdin.close() + while not m.called: + await asyncio.sleep(0) + + os.waitpid(proc.pid, 0) + proc._transport._process_exited(0) + await proc.wait() + + self.assertEqual(os_helper.fd_count(), before) + + self.loop.run_until_complete(coro()) + + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_asyncio/test_windows_events.py b/Lib/test/test_asyncio/test_windows_events.py index 0af3368627a..4a85b1a09ee 100644 --- a/Lib/test/test_asyncio/test_windows_events.py +++ b/Lib/test/test_asyncio/test_windows_events.py @@ -5,6 +5,7 @@ import time import threading import unittest +import warnings from unittest import mock if sys.platform != 'win32': @@ -15,6 +16,9 @@ import asyncio from asyncio import windows_events +from asyncio import windows_utils +from test import support +from test.support import os_helper from test.test_asyncio import utils as test_utils @@ -359,5 +363,110 @@ async def main(): asyncio.events._set_event_loop_policy(old_policy) +class ProactorPipeObjectSupportTests(unittest.TestCase): + + def setUp(self): + super().setUp() + self.loop = asyncio.ProactorEventLoop() + self.addCleanup(self.loop.close) + self.errors = [] + self.loop.set_exception_handler( + lambda loop, context: self.errors.append(context)) + + def check_read_rejected(self, pipe): + self.addCleanup(pipe.close) + lost = self.loop.create_future() + + class Proto(asyncio.Protocol): + def connection_lost(self, exc): + if not lost.done(): + lost.set_result(exc) + + async def run(): + transport, _ = await self.loop.connect_read_pipe(Proto, pipe) + exc = await lost + transport.close() + return exc + + exc = self.loop.run_until_complete(run()) + self.assertIsInstance(exc, OSError) + + def check_write_rejected(self, pipe): + self.addCleanup(pipe.close) + with warnings.catch_warnings(): + warnings.simplefilter('ignore', ResourceWarning) + with self.assertRaises(OSError): + self.loop.run_until_complete( + self.loop.connect_write_pipe(asyncio.BaseProtocol, pipe)) + support.gc_collect() + + def check_accepted(self, rpipe, wpipe): + self.addCleanup(rpipe.close) + self.addCleanup(wpipe.close) + + chunks = [] + lost = self.loop.create_future() + + class ReadProto(asyncio.Protocol): + def data_received(self, data): + chunks.append(data) + + def connection_lost(self, exc): + if not lost.done(): + lost.set_result(exc) + + async def run(): + rtransport, _ = await self.loop.connect_read_pipe(ReadProto, rpipe) + wtransport, _ = await self.loop.connect_write_pipe( + asyncio.BaseProtocol, wpipe) + wtransport.write(b'spam') + wtransport.close() + await lost + rtransport.close() + + self.loop.run_until_complete(run()) + self.assertEqual(b''.join(chunks), b'spam') + self.assertFalse(self.errors) + + def test_overlapped_pipe(self): + rhandle, whandle = windows_utils.pipe(duplex=True, overlapped=(True, True)) + self.check_accepted(windows_utils.PipeHandle(rhandle), + windows_utils.PipeHandle(whandle)) + + def test_socketpair(self): + rsock, wsock = socket.socketpair() + self.check_accepted(rsock, wsock) + + def test_read_non_overlapped_pipe(self): + rhandle, whandle = windows_utils.pipe(overlapped=(False, False)) + self.addCleanup(windows_utils.PipeHandle(whandle).close) + self.check_read_rejected(windows_utils.PipeHandle(rhandle)) + + def test_write_non_overlapped_pipe(self): + rhandle, whandle = windows_utils.pipe(overlapped=(False, False)) + self.addCleanup(windows_utils.PipeHandle(rhandle).close) + self.check_write_rejected(windows_utils.PipeHandle(whandle)) + + def test_read_regular_file(self): + self.addCleanup(os_helper.unlink, os_helper.TESTFN) + with open(os_helper.TESTFN, 'wb') as f: + f.write(b'spam') + self.check_read_rejected(open(os_helper.TESTFN, 'rb', 0)) + + def test_write_regular_file(self): + self.addCleanup(os_helper.unlink, os_helper.TESTFN) + self.check_write_rejected(open(os_helper.TESTFN, 'wb', 0)) + + def test_read_os_pipe(self): + rfd, wfd = os.pipe() + self.addCleanup(os.close, wfd) + self.check_read_rejected(open(rfd, 'rb', 0)) + + def test_write_os_pipe(self): + rfd, wfd = os.pipe() + self.addCleanup(os.close, rfd) + self.check_write_rejected(open(wfd, 'wb', 0)) + + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_asyncio/utils.py b/Lib/test/test_asyncio/utils.py index a480e16e81b..62cfcf8ceb5 100644 --- a/Lib/test/test_asyncio/utils.py +++ b/Lib/test/test_asyncio/utils.py @@ -388,8 +388,8 @@ def close(self): else: # pragma: no cover raise AssertionError("Time generator is not finished") - def _add_reader(self, fd, callback, *args): - self.readers[fd] = events.Handle(callback, args, self, None) + def _add_reader(self, fd, callback, *args, context=None): + self.readers[fd] = events.Handle(callback, args, self, context) def _remove_reader(self, fd): self.remove_reader_count[fd] += 1 @@ -414,8 +414,8 @@ def assert_no_reader(self, fd): if fd in self.readers: raise AssertionError(f'fd {fd} is registered') - def _add_writer(self, fd, callback, *args): - self.writers[fd] = events.Handle(callback, args, self, None) + def _add_writer(self, fd, callback, *args, context=None): + self.writers[fd] = events.Handle(callback, args, self, context) def _remove_writer(self, fd): self.remove_writer_count[fd] += 1 diff --git a/Lib/test/test_calendar.py b/Lib/test/test_calendar.py index 50ccf66b8da..04eb5155a82 100644 --- a/Lib/test/test_calendar.py +++ b/Lib/test/test_calendar.py @@ -8,6 +8,7 @@ import io import locale import os +import platform import sys import time @@ -112,18 +113,25 @@ default_format = dict(year="year", month="month", encoding="ascii") +result_2004_css = """""" + result_2004_html = """\ - - - + + - - + + Calendar for 2004 +{css_styles} + - -
    2004
    +
    +
    2004
    @@ -132,7 +140,7 @@
    January
    MonTueWedThuFriSatSun
       1234
    19202122232425
    262728293031 
    -
    +
    @@ -141,7 +149,7 @@
    February
    MonTueWedThuFriSatSun
          1
    16171819202122
    23242526272829
    -
    +
    @@ -150,7 +158,7 @@
    March
    MonTueWedThuFriSatSun
    1234567
    22232425262728
    293031    
    -
    +
    @@ -159,7 +167,7 @@
    April
    MonTueWedThuFriSatSun
       1234
    19202122232425
    2627282930  
    -
    +
    @@ -169,7 +177,7 @@
    May
    MonTueWedThuFriSatSun
         12
    24252627282930
    31      
    -
    +
    @@ -178,7 +186,7 @@
    June
    MonTueWedThuFriSatSun
     123456
    21222324252627
    282930    
    -
    +
    @@ -187,7 +195,7 @@
    July
    MonTueWedThuFriSatSun
       1234
    19202122232425
    262728293031 
    -
    + - - + + - - - - - - + + + + + + - - + + - - - - - - - - - + + + + + + + + +
    @@ -197,7 +205,7 @@
    August
    MonTueWedThuFriSatSun
          1
    23242526272829
    3031     
    -
    + - - + + - - - - - - + + + + + + - - + + - - - - - - - - - + + + + + + + + +
    @@ -206,7 +214,7 @@
    September
    MonTueWedThuFriSatSun
      12345
    20212223242526
    27282930   
    -
    + - - + + - - - - - - + + + + + + - - + + - - - - - - - - - + + + + + + + + +
    @@ -215,7 +223,7 @@
    October
    MonTueWedThuFriSatSun
        123
    18192021222324
    25262728293031
    -
    + - - - - - + + + + +
    @@ -224,7 +232,7 @@
    November
    MonTueWedThuFriSatSun
    1234567
    22232425262728
    2930     
    -
    + - - - - - + + + + +
    @@ -237,6 +245,34 @@ """ +result_2009_6_html = """\ + + + + + +Calendar for 2009 + + + + +
    December
    MonTueWedThuFriSatSun
      12345
    + + + + + + + +
    June 2009
    MonTueWedThuFriSatSun
    1234567
    891011121314
    15161718192021
    22232425262728
    2930     
    + + +""" + result_2004_days = [ [[[0, 0, 0, 1, 2, 3, 4], [5, 6, 7, 8, 9, 10, 11], @@ -384,10 +420,12 @@ def check_htmlcalendar_encoding(self, req, res): cal = calendar.HTMLCalendar() format_ = default_format.copy() format_["encoding"] = req or 'utf-8' + format_with_css = {**format_, "css_styles": result_2004_css} + formatted_html = result_2004_html.format(**format_with_css) output = cal.formatyearpage(2004, encoding=req) self.assertEqual( output, - result_2004_html.format(**format_).encode(res) + formatted_html.encode(res) ) def test_output(self): @@ -417,7 +455,7 @@ def test_output_htmlcalendar_encoding_utf8(self): self.check_htmlcalendar_encoding('utf-8', 'utf-8') def test_output_htmlcalendar_encoding_default(self): - self.check_htmlcalendar_encoding(None, sys.getdefaultencoding()) + self.check_htmlcalendar_encoding(None, 'utf-8') def test_yeardatescalendar(self): def shrink(cal): @@ -501,6 +539,19 @@ def test_format(self): calendar.format(["1", "2", "3"], colwidth=3, spacing=1) self.assertEqual(out.getvalue().strip(), "1 2 3") + def test_format_html_year_with_month(self): + self.assertEqual( + calendar.HTMLCalendar().formatmonthpage(2009, 6).decode("ascii"), + result_2009_6_html + ) + + def test_formatmonthpage_no_width(self): + # gh-140212: formatmonthpage must not accept a 'width' argument. + cal = calendar.HTMLCalendar() + self.assertIn(b'class="month">June 2009', cal.formatmonthpage(2009, 6)) + self.assertRaises(TypeError, cal.formatmonthpage, 2009, 6, width=3) + + class CalendarTestCase(unittest.TestCase): def test_deprecation_warning(self): @@ -556,7 +607,8 @@ def test_days(self): self.assertEqual(value[::-1], list(reversed(value))) def test_months(self): - for attr in "month_name", "month_abbr": + for attr in ("month_name", "month_abbr", "standalone_month_name", + "standalone_month_abbr"): value = getattr(calendar, attr) self.assertEqual(len(value), 13) self.assertEqual(len(value[:]), 13) @@ -566,6 +618,38 @@ def test_months(self): # verify it "acts like a sequence" in two forms of iteration self.assertEqual(value[::-1], list(reversed(value))) + @support.run_with_locale('LC_ALL', 'pl_PL') + @unittest.skipUnless(sys.platform == 'darwin' or platform.libc_ver()[0] == 'glibc', + "Guaranteed to work with glibc and macOS") + def test_standalone_month_name_and_abbr_pl_locale(self): + expected_standalone_month_names = [ + "", "styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec", + "lipiec", "sierpień", "wrzesień", "październik", "listopad", + "grudzień" + ] + expected_standalone_month_abbr = [ + "", "sty", "lut", "mar", "kwi", "maj", "cze", + "lip", "sie", "wrz", "paź", "lis", "gru" + ] + self.assertEqual( + list(calendar.standalone_month_name), + expected_standalone_month_names + ) + self.assertEqual( + list(calendar.standalone_month_abbr), + expected_standalone_month_abbr + ) + + def test_standalone_month_name_and_abbr_C_locale(self): + # Ensure that the standalone month names and abbreviations are + # equal to the regular month names and abbreviations for + # the "C" locale. + with calendar.different_locale("C"): + self.assertListEqual(list(calendar.month_name), + list(calendar.standalone_month_name)) + self.assertListEqual(list(calendar.month_abbr), + list(calendar.standalone_month_abbr)) + def test_locale_text_calendar(self): try: cal = calendar.LocaleTextCalendar(locale='') @@ -672,6 +756,57 @@ def test_locale_calendar_formatweekday(self): except locale.Error: raise unittest.SkipTest('cannot set the en_US locale') + # These locales have weekday names all shorter than English's longest + # 'Wednesday'. They should not be abbreviated unnecessarily + @support.run_with_locales("LC_ALL", + 'Chinese', 'zh_CN.UTF-8', + 'French', 'fr_FR.UTF-8', + 'Norwegian', 'nb_NO.UTF-8', + 'Malay', 'ms_MY.UTF8' + ) + def test_locale_calendar_short_weekday_names(self): + names = (datetime.date(2001, 1, i+1).strftime('%A') for i in range(7)) + max_length = max(map(len, names)) + if max_length >= 9: + self.skipTest('weekday names are too long') + + def get_weekday_names(width): + return calendar.TextCalendar().formatweekheader(width).split() + + # Weekday names should not be abbreviated if the width is sufficient + self.assertEqual( + get_weekday_names(max_length), + get_weekday_names(max_length + 10) + ) + + # Any width shorter than necessary should produce abbreviations + self.assertNotEqual( + get_weekday_names(max_length), + get_weekday_names(max_length - 1) + ) + + # These locales have a weekday name longer than 'Wednesday' + # They should be properly abbreviated rather than truncated + @support.run_with_locales("LC_ALL", + 'Portuguese', 'pt_PT.UTF-8', + 'German', 'de_DE.UTF-8', + 'Russian', 'ru_RU.UTF-8', + ) + def test_locale_calendar_long_weekday_names(self): + names = (datetime.date(2001, 1, i+1).strftime('%A') for i in range(7)) + max_length = max(map(len, names)) + abbrev_names = (datetime.date(2001, 1, i+1).strftime('%a') for i in range(7)) + abbrev_max_length = max(map(len, abbrev_names)) + + if max_length <= 9: + self.skipTest('weekday names are too short') + if abbrev_max_length >= 9: + self.skipTest('abbreviated weekday names are too long') + + def get_weekday_names(width): + return calendar.TextCalendar().formatweekheader(width).split() + self.assertEqual(get_weekday_names(abbrev_max_length), get_weekday_names(max_length-1)) + def test_locale_calendar_formatmonthname(self): try: # formatmonthname uses the same month names regardless of the width argument. @@ -944,6 +1079,7 @@ def test_several_leapyears_in_range(self): def conv(s): return s.replace('\n', os.linesep).encode() +@support.force_not_colorized_test_class class CommandLineTestCase(unittest.TestCase): def setUp(self): self.runners = [self.run_cli_ok, self.run_cmd_ok] @@ -997,7 +1133,6 @@ def assertFailure(self, *args): self.assertCLIFails(*args) self.assertCmdFails(*args) - @support.force_not_colorized def test_help(self): stdout = self.run_cmd_ok('-h') self.assertIn(b'usage:', stdout) @@ -1018,7 +1153,6 @@ def test_illegal_arguments(self): self.assertFailure('2004', '1', 'spam') self.assertFailure('2004', '1', '1') self.assertFailure('2004', '1', '1', 'spam') - self.assertFailure('-t', 'html', '2004', '1') def test_output_current_year(self): for run in self.runners: @@ -1108,7 +1242,7 @@ def test_option_type(self): output = run('--type', 'text', '2004') self.assertEqual(output, conv(result_2004_text)) output = run('--type', 'html', '2004') - self.assertStartsWith(output, b'') self.assertIn(b'Calendar for 2004', output) def test_html_output_current_year(self): @@ -1121,15 +1255,25 @@ def test_html_output_current_year(self): def test_html_output_year_encoding(self): for run in self.runners: output = run('-t', 'html', '--encoding', 'ascii', '2004') - self.assertEqual(output, result_2004_html.format(**default_format).encode('ascii')) + format_with_css = default_format.copy() + format_with_css["css_styles"] = result_2004_css + self.assertEqual(output, result_2004_html.format(**format_with_css).encode('ascii')) def test_html_output_year_css(self): self.assertFailure('-t', 'html', '-c') self.assertFailure('-t', 'html', '--css') for run in self.runners: output = run('-t', 'html', '--css', 'custom.css', '2004') - self.assertIn(b'', output) + self.assertIn(b'', output) + + +@support.force_colorized_test_class +class ColorTestCase(unittest.TestCase): + def test_formatmonth_color(self): + today = datetime.date(2026, 5, 4) + cal = calendar._CLIDemoCalendar(highlight_day=today) + output = cal.formatmonth(2026, 5) + self.assertIn("\x1b[30m\x1b[43mMay 2026\x1b[0m\n\x1b[36m", output) class MiscTestCase(unittest.TestCase): @@ -1183,7 +1327,7 @@ def test_formatweek_head(self): def test_format_year(self): self.assertIn( - ('' % + ('
    ' % self.cal.cssclass_year), self.cal.formatyear(2017)) def test_format_year_head(self): diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index d36c5231609..7ac3b121236 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -2,7 +2,7 @@ import unittest from test import support -from test.support import cpython_only, import_helper, script_helper +from test.support import cpython_only, import_helper, isolation testmeths = [ @@ -449,7 +449,6 @@ def __delattr__(self, *args): def testHasAttrString(self): import sys - from test.support import import_helper _testlimitedcapi = import_helper.import_module('_testlimitedcapi') class A: @@ -557,6 +556,7 @@ class Custom: self.assertFalse(hasattr(c, "__call__")) @unittest.skip("TODO: RUSTPYTHON; segmentation fault") + @support.skip_if_huge_c_stack() @support.skip_emscripten_stack_overflow() @support.skip_wasi_stack_overflow() def testSFBug532646(self): @@ -653,6 +653,7 @@ class B(A): a = A(hash(A.f)^(-1)) hash(a.f) + @cpython_only def testSetattrWrapperNameIntern(self): # Issue #25794: __setattr__ should intern the attribute name class A: @@ -1014,36 +1015,104 @@ class C: C.a = X() C.a = X() - @cpython_only + @support.nomemtest + @isolation.runInSubprocess() def test_detach_materialized_dict_no_memory(self): - # Skip test if _testcapi is not available: - import_helper.import_module('_testcapi') - - code = """if 1: - import test.support - import _testcapi + import _testcapi - class A: - def __init__(self): - self.a = 1 - self.b = 2 + class A: + def __init__(self): + self.a = 1 + self.b = 2 + + # The failing allocation should be the one which detaches the + # dictionary from the object, but other allocations can happen + # first, so try to fail every one of the first allocations. + raised = False + for n in range(20): a = A() d = a.__dict__ - with test.support.catch_unraisable_exception() as ex: - _testcapi.set_nomemory(0, 1) - del a - assert ex.unraisable.exc_type is MemoryError try: - d["a"] - except KeyError: - pass - else: - assert False, "KeyError not raised" - """ - rc, out, err = script_helper.assert_python_ok("-c", code) - self.assertEqual(rc, 0) - self.assertFalse(out, msg=out.decode('utf-8')) - self.assertFalse(err, msg=err.decode('utf-8')) + with support.catch_unraisable_exception() as ex: + _testcapi.set_nomemory(n, n + 1) + try: + del a + finally: + _testcapi.remove_mem_hooks() + exc_type = ex.unraisable and ex.unraisable.exc_type + except MemoryError: + # The failing allocation was not in the deallocation code. + continue + if exc_type is not MemoryError: + continue + raised = True + if "a" not in d: + # The dictionary was cleared, as expected. + break + else: + if not raised: + self.fail("MemoryError was not raised during deallocation") + self.fail("the dictionary was not cleared") + +class DefinitionOrderTests(unittest.TestCase): + # PEP 520: Preserving Class Attribute Definition Order + + @staticmethod + def defined_names(namespace): + # Skip the names added by the compiler, like __firstlineno__. + return [name for name in namespace if not name.startswith('__')] + + def test_definition_order(self): + class C: + b = 1 + a = 2 + def m(self): pass + @staticmethod + def s(): pass + z = 3 + + self.assertEqual(self.defined_names(C.__dict__), + ['b', 'a', 'm', 's', 'z']) + + def test_definition_order_redefinition(self): + class C: + b = 1 + a = 2 + b = 3 + + self.assertEqual(self.defined_names(C.__dict__), ['b', 'a']) + self.assertEqual(C.b, 3) + + def test_definition_order_after_deletion(self): + class C: + a = 1 + b = 2 + del a + a = 3 + + self.assertEqual(self.defined_names(C.__dict__), ['b', 'a']) + + def test_definition_order_in_namespace(self): + namespaces = [] + class Meta(type): + def __new__(mcls, name, bases, namespace, **kwds): + namespaces.append(list(namespace)) + return super().__new__(mcls, name, bases, namespace, **kwds) + + class C(metaclass=Meta): + b = 1 + a = 2 + def m(self): pass + + self.assertEqual(self.defined_names(namespaces[0]), ['b', 'a', 'm']) + + def test_prepare_preserves_order(self): + namespace = type.__prepare__('C', ()) + namespace['b'] = 1 + namespace['a'] = 2 + namespace['b'] = 3 + self.assertEqual(list(namespace), ['b', 'a']) + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py index a2ea0535389..2efae308032 100644 --- a/Lib/test/test_cmd_line_script.py +++ b/Lib/test/test_cmd_line_script.py @@ -240,9 +240,8 @@ def test_script_abspath(self): def test_script_compiled(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, 'script') - py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) self._check_script(pyc_file, pyc_file, pyc_file, script_dir, None, importlib.machinery.SourcelessFileLoader) @@ -257,9 +256,8 @@ def test_directory(self): def test_directory_compiled(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, '__main__') - py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) self._check_script(script_dir, pyc_file, script_dir, script_dir, '', importlib.machinery.SourcelessFileLoader) @@ -279,8 +277,8 @@ def test_zipfile(self): def test_zipfile_compiled_timestamp(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, '__main__') - compiled_name = py_compile.compile( - script_name, doraise=True, + compiled_name = script_name + 'c' + py_compile.compile(script_name, compiled_name, doraise=True, invalidation_mode=py_compile.PycInvalidationMode.TIMESTAMP) zip_name, run_name = make_zip_script(script_dir, 'test_zip', compiled_name) self._check_script(zip_name, run_name, zip_name, zip_name, '', @@ -289,8 +287,8 @@ def test_zipfile_compiled_timestamp(self): def test_zipfile_compiled_checked_hash(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, '__main__') - compiled_name = py_compile.compile( - script_name, doraise=True, + compiled_name = script_name + 'c' + py_compile.compile(script_name, compiled_name, doraise=True, invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH) zip_name, run_name = make_zip_script(script_dir, 'test_zip', compiled_name) self._check_script(zip_name, run_name, zip_name, zip_name, '', @@ -299,8 +297,8 @@ def test_zipfile_compiled_checked_hash(self): def test_zipfile_compiled_unchecked_hash(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, '__main__') - compiled_name = py_compile.compile( - script_name, doraise=True, + compiled_name = script_name + 'c' + py_compile.compile(script_name, compiled_name, doraise=True, invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH) zip_name, run_name = make_zip_script(script_dir, 'test_zip', compiled_name) self._check_script(zip_name, run_name, zip_name, zip_name, '', @@ -353,9 +351,8 @@ def test_package_compiled(self): pkg_dir = os.path.join(script_dir, 'test_pkg') make_pkg(pkg_dir) script_name = _make_test_script(pkg_dir, '__main__') - compiled_name = py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) self._check_script(["-m", "test_pkg"], pyc_file, pyc_file, script_dir, 'test_pkg', importlib.machinery.SourcelessFileLoader, @@ -808,6 +805,30 @@ def test_script_as_dev_fd(self): out, err = p.communicate() self.assertEqual(out, b"12345678912345678912345\n") + def test_filter_syntax_warnings_by_module(self): + filename = support.findfile('test_import/data/syntax_warnings.py') + rc, out, err = assert_python_ok( + '-Werror', + '-Walways:::__main__', + '-Werror:::test.test_import.data.syntax_warnings', + '-Werror:::syntax_warnings', + filename) + self.assertEqual(err.count(b': SyntaxWarning: '), 6) + + def test_zipfile_run_filter_syntax_warnings_by_module(self): + filename = support.findfile('test_import/data/syntax_warnings.py') + with open(filename, 'rb') as f: + source = f.read() + with os_helper.temp_dir() as script_dir: + zip_name, _ = make_zip_pkg( + script_dir, 'test_zip', 'test_pkg', '__main__', source) + rc, out, err = assert_python_ok( + '-Werror', + '-Walways:::__main__', + '-Werror:::test_pkg.__main__', + os.path.join(zip_name, 'test_pkg') + ) + self.assertEqual(err.count(b': SyntaxWarning: '), 12) def tearDownModule(): diff --git a/Lib/test/test_codeop.py b/Lib/test/test_codeop.py index 5976303ad3c..d57452602ce 100644 --- a/Lib/test/test_codeop.py +++ b/Lib/test/test_codeop.py @@ -373,7 +373,7 @@ def test_syntax_errors(self, compiler): def foo(x,x): pass """) - message = "duplicate argument 'x' in function definition" + message = "duplicate parameter 'x' in function definition" with self.assertRaisesRegex(SyntaxError, message): compiler(code, "", 'exec') diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index c1dadc4e274..b1b2dd2ca5c 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -12,6 +12,7 @@ import string import sys from test import support +from test.support.import_helper import import_fresh_module import types import unittest @@ -26,7 +27,7 @@ from collections.abc import Set, MutableSet from collections.abc import Mapping, MutableMapping, KeysView, ItemsView, ValuesView from collections.abc import Sequence, MutableSequence -from collections.abc import ByteString, Buffer +from collections.abc import Buffer class TestUserObjects(unittest.TestCase): @@ -1935,6 +1936,14 @@ def assert_index_same(seq1, seq2, index_args): nativeseq, seqseq, (letter, start, stop)) def test_ByteString(self): + previous_sys_modules = sys.modules.copy() + self.addCleanup(sys.modules.update, previous_sys_modules) + + for module in "collections", "_collections_abc", "collections.abc": + sys.modules.pop(module, None) + + with self.assertWarns(DeprecationWarning): + from collections.abc import ByteString for sample in [bytes, bytearray]: with self.assertWarns(DeprecationWarning): self.assertIsInstance(sample(), ByteString) @@ -1956,6 +1965,14 @@ class X(ByteString): pass # No metaclass conflict class Z(ByteString, Awaitable): pass + def test_ByteString_attribute_access(self): + collections_abc = import_fresh_module( + "collections.abc", + fresh=("collections", "_collections_abc") + ) + with self.assertWarns(DeprecationWarning): + collections_abc.ByteString + def test_Buffer(self): for sample in [bytes, bytearray, memoryview]: self.assertIsInstance(sample(b"x"), Buffer) @@ -2175,6 +2192,7 @@ def correctly_ordered(seq): self.assertTrue(correctly_ordered(p - q)) self.assertTrue(correctly_ordered(p | q)) self.assertTrue(correctly_ordered(p & q)) + self.assertTrue(correctly_ordered(p ^ q)) p, q = Counter(ps), Counter(qs) p += q @@ -2192,6 +2210,10 @@ def correctly_ordered(seq): p &= q self.assertTrue(correctly_ordered(p)) + p, q = Counter(ps), Counter(qs) + p ^= q + self.assertTrue(correctly_ordered(p)) + p, q = Counter(ps), Counter(qs) p.update(q) self.assertTrue(correctly_ordered(p)) @@ -2274,6 +2296,7 @@ def test_multiset_operations(self): (Counter.__sub__, lambda x, y: max(0, x-y)), (Counter.__or__, lambda x, y: max(0,x,y)), (Counter.__and__, lambda x, y: max(0, min(x,y))), + (Counter.__xor__, lambda x, y: max(0, max(x,y) - min(x,y))), ]: result = counterop(p, q) for x in elements: @@ -2291,6 +2314,7 @@ def test_multiset_operations(self): (Counter.__sub__, set.__sub__), (Counter.__or__, set.__or__), (Counter.__and__, set.__and__), + (Counter.__xor__, set.__xor__), ]: counter_result = counterop(p, q) set_result = setop(set(p.elements()), set(q.elements())) @@ -2309,6 +2333,7 @@ def test_inplace_operations(self): (Counter.__isub__, Counter.__sub__), (Counter.__ior__, Counter.__or__), (Counter.__iand__, Counter.__and__), + (Counter.__ixor__, Counter.__xor__), ]: c = p.copy() c_id = id(c) @@ -2384,6 +2409,7 @@ def test_multiset_operations_equivalent_to_set_operations(self): self.assertEqual(set(cp - cq), sp - sq) self.assertEqual(set(cp | cq), sp | sq) self.assertEqual(set(cp & cq), sp & sq) + self.assertEqual(set(cp ^ cq), sp ^ sq) self.assertEqual(cp == cq, sp == sq) self.assertEqual(cp != cq, sp != sq) self.assertEqual(cp <= cq, sp <= sq) @@ -2397,6 +2423,8 @@ def test_eq(self): def test_le(self): self.assertTrue(Counter(a=3, b=2, c=0) <= Counter('ababa')) + self.assertTrue(Counter() <= Counter(c=1)) + self.assertFalse(Counter() <= Counter(c=-1)) self.assertFalse(Counter(a=3, b=2) <= Counter('babab')) def test_lt(self): @@ -2405,12 +2433,48 @@ def test_lt(self): def test_ge(self): self.assertTrue(Counter(a=2, b=1, c=0) >= Counter('aab')) + self.assertTrue(Counter() >= Counter(c=-1)) + self.assertFalse(Counter() >= Counter(c=1)) self.assertFalse(Counter(a=3, b=2, c=0) >= Counter('aabd')) def test_gt(self): self.assertTrue(Counter(a=3, b=2, c=0) > Counter('aab')) self.assertFalse(Counter(a=2, b=1, c=0) > Counter('aab')) + def test_symmetric_difference(self): + population = (-4, -3, -2, -1, 0, 1, 2, 3, 4) + + for a, b1, b2, c in product(population, repeat=4): + p = Counter(a=a, b=b1) + q = Counter(b=b2, c=c) + r = p ^ q + + # Elementwise invariants + for k in ('a', 'b', 'c'): + self.assertEqual(r[k], max(p[k], q[k]) - min(p[k], q[k])) + self.assertEqual(r[k], abs(p[k] - q[k])) + + # Invariant for all positive, negative, and zero counts + self.assertEqual(r, (p - q) | (q - p)) + + # Invariant for non-negative counts + if a >= 0 and b1 >= 0 and b2 >= 0 and c >= 0: + self.assertEqual(r, (p | q) - (p & q)) + + # Zeros and negatives eliminated + self.assertTrue(all(value > 0 for value in r.values())) + + # Output preserves input order: p first and then q + keys = list(p) + list(q) + indices = [keys.index(k) for k in r] + self.assertEqual(indices, sorted(indices)) + + # Inplace operation matches binary operation + pp = Counter(p) + qq = Counter(q) + pp ^= qq + self.assertEqual(pp, r) + def load_tests(loader, tests, pattern): tests.addTest(doctest.DocTestSuite(collections)) diff --git a/Lib/test/test_compileall.py b/Lib/test/test_compileall.py index be5e7d7feee..a612a729014 100644 --- a/Lib/test/test_compileall.py +++ b/Lib/test/test_compileall.py @@ -28,11 +28,15 @@ from test import support from test.support import os_helper from test.support import script_helper -from test.test_py_compile import without_source_date_epoch -from test.test_py_compile import SourceDateEpochTestMeta +from test.support.os_helper import without_source_date_epoch +from test.support.os_helper import SourceDateEpochTestMeta from test.support.os_helper import FakePath +if sys.implementation.cache_tag is None: + raise unittest.SkipTest('requires sys.implementation.cache_tag is not None') + + def get_pyc(script, opt): if not opt: # Replace None and 0 with '' diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index 91b37a8fb00..da683caff42 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -1,6 +1,8 @@ +import errno import unittest import sys from test import support +from test.support import import_helper from test.support.testcase import ComplexesAreIdenticalMixin from test.support.numbers import ( VALID_UNDERSCORE_LITERALS, @@ -9,6 +11,7 @@ from random import random from math import isnan, copysign +import cmath import operator INF = float("inf") @@ -505,17 +508,25 @@ def check(z, x, y): with self.assertWarnsRegex(DeprecationWarning, "argument 'imag' must be a real number, not complex"): check(complex(0.0, 4.25j), -4.25, 0.0) - with self.assertWarnsRegex(DeprecationWarning, - "argument 'real' must be a real number, not complex"): + with (self.assertWarnsRegex(DeprecationWarning, + "argument 'real' must be a real number, not complex"), + self.assertWarnsRegex(DeprecationWarning, + "argument 'imag' must be a real number, not complex")): check(complex(4.25+0j, 0j), 4.25, 0.0) - with self.assertWarnsRegex(DeprecationWarning, - "argument 'real' must be a real number, not complex"): + with (self.assertWarnsRegex(DeprecationWarning, + "argument 'real' must be a real number, not complex"), + self.assertWarnsRegex(DeprecationWarning, + "argument 'imag' must be a real number, not complex")): check(complex(4.25j, 0j), 0.0, 4.25) - with self.assertWarnsRegex(DeprecationWarning, - "argument 'real' must be a real number, not complex"): + with (self.assertWarnsRegex(DeprecationWarning, + "argument 'real' must be a real number, not complex"), + self.assertWarnsRegex(DeprecationWarning, + "argument 'imag' must be a real number, not complex")): check(complex(0j, 4.25+0j), 0.0, 4.25) - with self.assertWarnsRegex(DeprecationWarning, - "argument 'real' must be a real number, not complex"): + with (self.assertWarnsRegex(DeprecationWarning, + "argument 'real' must be a real number, not complex"), + self.assertWarnsRegex(DeprecationWarning, + "argument 'imag' must be a real number, not complex")): check(complex(0j, 4.25j), -4.25, 0.0) check(complex(real=4.25), 4.25, 0.0) @@ -782,8 +793,30 @@ def test_abs(self): for num in nums: self.assertAlmostEqual((num.real**2 + num.imag**2) ** 0.5, abs(num)) + for x in 0.0, -0.0, INF, -INF, NAN: + for y in 0.0, -0.0, INF, -INF, NAN: + with self.subTest(x=x, y=y): + z = complex(x, y) + r = abs(z) + if cmath.isfinite(z): + self.assertFloatsAreIdentical(r, 0.0) + elif cmath.isinf(z): + self.assertEqual(r, INF) + else: + self.assertTrue(cmath.isnan(z)) + self.assertTrue(isnan(r)) + self.assertRaises(OverflowError, abs, complex(DBL_MAX, DBL_MAX)) + def test_abs_errno_handling(self): + _testcapi = import_helper.import_module('_testcapi') + z = complex('nan') + _testcapi.set_errno(errno.ERANGE) + try: + self.assertTrue(isnan(abs(z))) + finally: + _testcapi.set_errno(0) + def test_repr_str(self): def test(v, expected, test_fn=self.assertEqual): test_fn(repr(v), expected) diff --git a/Lib/test/test_concurrent_futures/executor.py b/Lib/test/test_concurrent_futures/executor.py index 6a26d695b0b..a37c4d45f07 100644 --- a/Lib/test/test_concurrent_futures/executor.py +++ b/Lib/test/test_concurrent_futures/executor.py @@ -1,12 +1,11 @@ import itertools import threading import time -import unittest import weakref from concurrent import futures from operator import add from test import support -from test.support import Py_GIL_DISABLED +from test.support import Py_GIL_DISABLED, warnings_helper def mul(x, y): @@ -41,12 +40,15 @@ def __len__(self): class ExecutorTest: + # Executor.shutdown() and context manager usage is tested by # ExecutorShutdownTest. + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_submit(self): future = self.executor.submit(pow, 2, 8) self.assertEqual(256, future.result()) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_submit_keyword(self): future = self.executor.submit(mul, 2, y=8) self.assertEqual(16, future.result()) @@ -57,6 +59,7 @@ def test_submit_keyword(self): with self.assertRaises(TypeError): self.executor.submit(arg=1) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_map(self): self.assertEqual( list(self.executor.map(pow, range(10), range(10))), @@ -66,12 +69,15 @@ def test_map(self): list(self.executor.map(pow, range(10), range(10), chunksize=3)), list(map(pow, range(10), range(10)))) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_map_exception(self): i = self.executor.map(divmod, [1, 1, 1, 1], [2, 3, 0, 5]) self.assertEqual(i.__next__(), (0, 1)) self.assertEqual(i.__next__(), (0, 1)) - self.assertRaises(ZeroDivisionError, i.__next__) + with self.assertRaises(ZeroDivisionError): + i.__next__() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.requires_resource('walltime') def test_map_timeout(self): results = [] @@ -85,7 +91,9 @@ def test_map_timeout(self): else: self.fail('expected TimeoutError') - self.assertEqual([None, None], results) + # gh-110097: On heavily loaded systems, the launch of the worker may + # take longer than the specified timeout. + self.assertIn(results, ([None, None], [None], [])) def test_map_buffersize_type_validation(self): for buffersize in ("foo", 2.0): @@ -96,7 +104,6 @@ def test_map_buffersize_type_validation(self): ): self.executor.map(str, range(4), buffersize=buffersize) - def test_map_buffersize_value_validation(self): for buffersize in (0, -1): with self.subTest(buffersize=buffersize): @@ -106,7 +113,7 @@ def test_map_buffersize_value_validation(self): ): self.executor.map(str, range(4), buffersize=buffersize) - + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_map_buffersize(self): ints = range(4) for buffersize in (1, 2, len(ints), len(ints) * 2): @@ -114,7 +121,7 @@ def test_map_buffersize(self): res = self.executor.map(str, ints, buffersize=buffersize) self.assertListEqual(list(res), ["0", "1", "2", "3"]) - + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_map_buffersize_on_multiple_iterables(self): ints = range(4) for buffersize in (1, 2, len(ints), len(ints) * 2): @@ -122,19 +129,34 @@ def test_map_buffersize_on_multiple_iterables(self): res = self.executor.map(add, ints, ints, buffersize=buffersize) self.assertListEqual(list(res), [0, 2, 4, 6]) - + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_map_buffersize_on_infinite_iterable(self): res = self.executor.map(str, itertools.count(), buffersize=2) self.assertEqual(next(res, None), "0") self.assertEqual(next(res, None), "1") self.assertEqual(next(res, None), "2") + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() + def test_map_buffersize_on_multiple_infinite_iterables(self): + res = self.executor.map( + add, + itertools.count(), + itertools.count(), + buffersize=2 + ) + self.assertEqual(next(res, None), 0) + self.assertEqual(next(res, None), 2) + self.assertEqual(next(res, None), 4) def test_map_buffersize_on_empty_iterable(self): res = self.executor.map(str, [], buffersize=2) self.assertIsNone(next(res, None)) + def test_map_buffersize_without_iterable(self): + res = self.executor.map(str, buffersize=2) + self.assertIsNone(next(res, None)) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_map_buffersize_when_buffer_is_full(self): ints = iter(range(4)) buffersize = 2 @@ -146,7 +168,7 @@ def test_map_buffersize_when_buffer_is_full(self): msg="should have fetched only `buffersize` elements from `ints`.", ) - + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_shutdown_race_issue12456(self): # Issue #12456: race condition at shutdown where trying to post a # sentinel in the call queue blocks (the queue is full while processes @@ -154,6 +176,7 @@ def test_shutdown_race_issue12456(self): self.executor.map(str, [2] * (self.worker_count + 1)) self.executor.shutdown() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.cpython_only def test_no_stale_references(self): # Issue #16284: check that the executors don't unnecessarily hang onto @@ -198,6 +221,7 @@ def test_max_workers_negative(self): "than 0"): self.executor_type(max_workers=number) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_free_reference(self): # Issue #14406: Result iterator should not keep an internal # reference to result objects. @@ -210,6 +234,7 @@ def test_free_reference(self): if wr() is None: break + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_swallows_falsey_exceptions(self): # see gh-132063: Prevent exceptions that evaluate as falsey # from being ignored. diff --git a/Lib/test/test_concurrent_futures/test_as_completed.py b/Lib/test/test_concurrent_futures/test_as_completed.py index c90b0021d85..31c7bb3ebd8 100644 --- a/Lib/test/test_concurrent_futures/test_as_completed.py +++ b/Lib/test/test_concurrent_futures/test_as_completed.py @@ -7,6 +7,7 @@ CANCELLED_AND_NOTIFIED, FINISHED, Future) from test import support +from test.support import warnings_helper from .util import ( PENDING_FUTURE, RUNNING_FUTURE, @@ -19,6 +20,7 @@ def mul(x, y): class AsCompletedTests: + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_no_timeout(self): future1 = self.executor.submit(mul, 2, 21) future2 = self.executor.submit(mul, 7, 6) @@ -35,6 +37,7 @@ def test_no_timeout(self): future1, future2]), completed) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_future_times_out(self): """Test ``futures.as_completed`` timing out before completing it's final future.""" @@ -62,6 +65,7 @@ def test_future_times_out(self): # Check that ``future`` wasn't completed. self.assertEqual(completed_futures, already_completed) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_duplicate_futures(self): # Issue 20367. Duplicate futures should not raise exceptions or give # duplicate responses. diff --git a/Lib/test/test_concurrent_futures/test_deadlock.py b/Lib/test/test_concurrent_futures/test_deadlock.py index dcc1d68563a..5cd84f7e430 100644 --- a/Lib/test/test_concurrent_futures/test_deadlock.py +++ b/Lib/test/test_concurrent_futures/test_deadlock.py @@ -10,6 +10,7 @@ from concurrent.futures.process import BrokenProcessPool, _ThreadWakeup from test import support +from test.support import warnings_helper from .util import ( create_executor_tests, setup_module, @@ -111,9 +112,9 @@ def _fail_on_deadlock(self, executor): print(f"\nTraceback:\n {tb}", file=sys.__stderr__) self.fail(f"Executor deadlock:\n\n{tb}") - - def _check_crash(self, error, func, *args, ignore_stderr=False): - # test for deadlock caused by crashes in a pool + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() + def _check_error(self, error, func, *args, ignore_stderr=False): + # test for deadlock caused by crashes or exiting in a pool self.executor.shutdown(wait=True) executor = self.executor_type( @@ -138,49 +139,52 @@ def _check_crash(self, error, func, *args, ignore_stderr=False): def test_error_at_task_pickle(self): # Check problem occurring while pickling a task in # the task_handler thread - self._check_crash(PicklingError, id, ErrorAtPickle()) + self._check_error(PicklingError, id, ErrorAtPickle()) def test_exit_at_task_unpickle(self): # Check problem occurring while unpickling a task on workers - self._check_crash(BrokenProcessPool, id, ExitAtUnpickle()) + self._check_error(BrokenProcessPool, id, ExitAtUnpickle()) def test_error_at_task_unpickle(self): # gh-109832: Restore stderr overridden by _raise_error_ignore_stderr() self.addCleanup(setattr, sys, 'stderr', sys.stderr) # Check problem occurring while unpickling a task on workers - self._check_crash(BrokenProcessPool, id, ErrorAtUnpickle()) + self._check_error(BrokenProcessPool, id, ErrorAtUnpickle()) + @support.skip_if_sanitizer("UBSan: explicit SIGSEV not allowed", ub=True) def test_crash_at_task_unpickle(self): # Check problem occurring while unpickling a task on workers - self._check_crash(BrokenProcessPool, id, CrashAtUnpickle()) + self._check_error(BrokenProcessPool, id, CrashAtUnpickle()) + @support.skip_if_sanitizer("UBSan: explicit SIGSEV not allowed", ub=True) def test_crash_during_func_exec_on_worker(self): # Check problem occurring during func execution on workers - self._check_crash(BrokenProcessPool, _crash) + self._check_error(BrokenProcessPool, _crash) def test_exit_during_func_exec_on_worker(self): # Check problem occurring during func execution on workers - self._check_crash(SystemExit, _exit) + self._check_error(SystemExit, _exit) def test_error_during_func_exec_on_worker(self): # Check problem occurring during func execution on workers - self._check_crash(RuntimeError, _raise_error, RuntimeError) + self._check_error(RuntimeError, _raise_error, RuntimeError) + @support.skip_if_sanitizer("UBSan: explicit SIGSEV not allowed", ub=True) def test_crash_during_result_pickle_on_worker(self): # Check problem occurring while pickling a task result # on workers - self._check_crash(BrokenProcessPool, _return_instance, CrashAtPickle) + self._check_error(BrokenProcessPool, _return_instance, CrashAtPickle) def test_exit_during_result_pickle_on_worker(self): # Check problem occurring while pickling a task result # on workers - self._check_crash(SystemExit, _return_instance, ExitAtPickle) + self._check_error(SystemExit, _return_instance, ExitAtPickle) def test_error_during_result_pickle_on_worker(self): # Check problem occurring while pickling a task result # on workers - self._check_crash(PicklingError, _return_instance, ErrorAtPickle) + self._check_error(PicklingError, _return_instance, ErrorAtPickle) def test_error_during_result_unpickle_in_result_handler(self): # gh-109832: Restore stderr overridden by _raise_error_ignore_stderr() @@ -188,15 +192,17 @@ def test_error_during_result_unpickle_in_result_handler(self): # Check problem occurring while unpickling a task in # the result_handler thread - self._check_crash(BrokenProcessPool, + self._check_error(BrokenProcessPool, _return_instance, ErrorAtUnpickle, ignore_stderr=True) def test_exit_during_result_unpickle_in_result_handler(self): # Check problem occurring while unpickling a task in # the result_handler thread - self._check_crash(BrokenProcessPool, _return_instance, ExitAtUnpickle) + self._check_error(BrokenProcessPool, _return_instance, ExitAtUnpickle) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() + @support.skip_if_sanitizer("UBSan: explicit SIGSEV not allowed", ub=True) def test_shutdown_deadlock(self): # Test that the pool calling shutdown do not cause deadlock # if a worker fails after the shutdown call. @@ -209,6 +215,7 @@ def test_shutdown_deadlock(self): with self.assertRaises(BrokenProcessPool): f.result() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_shutdown_deadlock_pickle(self): # Test that the pool calling shutdown with wait=False does not cause # a deadlock if a task fails at pickle after the shutdown call. @@ -235,8 +242,10 @@ def test_shutdown_deadlock_pickle(self): # dangling threads executor_manager.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() + @support.skip_if_sanitizer("UBSan: explicit SIGSEV not allowed", ub=True) def test_crash_big_data(self): - # Test that there is a clean exception instad of a deadlock when a + # Test that there is a clean exception instead of a deadlock when a # child process crashes while some data is being written into the # queue. # https://github.com/python/cpython/issues/94777 @@ -250,6 +259,7 @@ def test_crash_big_data(self): executor.shutdown(wait=True) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_gh105829_should_not_deadlock_if_wakeup_pipe_full(self): # Issue #105829: The _ExecutorManagerThread wakeup pipe could # fill up and block. See: https://github.com/python/cpython/issues/105829 diff --git a/Lib/test/test_concurrent_futures/test_init.py b/Lib/test/test_concurrent_futures/test_init.py index df640929309..c9fedc72132 100644 --- a/Lib/test/test_concurrent_futures/test_init.py +++ b/Lib/test/test_concurrent_futures/test_init.py @@ -1,5 +1,6 @@ import contextlib import logging +import multiprocessing import queue import time import unittest @@ -11,6 +12,7 @@ from logging.handlers import QueueHandler from test import support +from test.support import warnings_helper from .util import ExecutorMixin, create_executor_tests, setup_module @@ -20,6 +22,10 @@ def init(x): global INITIALIZER_STATUS INITIALIZER_STATUS = x + # InterpreterPoolInitializerTest.test_initializer fails + # if we don't have a LOAD_GLOBAL. (It could be any global.) + # We will address this separately. + INITIALIZER_STATUS def get_init_status(): return INITIALIZER_STATUS @@ -44,6 +50,7 @@ def setUp(self): initargs=('initialized',)) super().setUp() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_initializer(self): futures = [self.executor.submit(get_init_status) for _ in range(self.worker_count)] @@ -70,6 +77,7 @@ def setUp(self): self.executor_kwargs = dict(initializer=init_fail) super().setUp() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_initializer(self): with self._assert_logged('ValueError: error in initializer'): try: @@ -140,6 +148,8 @@ def test_spawn(self): self._test(ProcessPoolSpawnFailingInitializerTest) @support.skip_if_sanitizer("TSAN doesn't support threads after fork", thread=True) + @unittest.skipUnless("forkserver" in multiprocessing.get_all_start_methods(), + "forkserver start method is not available") def test_forkserver(self): self._test(ProcessPoolForkserverFailingInitializerTest) diff --git a/Lib/test/test_concurrent_futures/test_interpreter_pool.py b/Lib/test/test_concurrent_futures/test_interpreter_pool.py index b8f1a0f3a29..853a363f19e 100644 --- a/Lib/test/test_concurrent_futures/test_interpreter_pool.py +++ b/Lib/test/test_concurrent_futures/test_interpreter_pool.py @@ -129,12 +129,12 @@ def test_init_script(self): """ os.write(w, b'\0') - executor = self.executor_type(initializer=initscript) - before_init = os.read(r, 100) - fut = executor.submit(script) - after_init = read_msg(r) - fut.result() - after_run = read_msg(r) + with self.executor_type(initializer=initscript) as executor: + before_init = os.read(r, 100) + fut = executor.submit(script) + after_init = read_msg(r) + fut.result() + after_run = read_msg(r) self.assertEqual(before_init, b'\0') self.assertEqual(after_init, msg1) @@ -151,11 +151,11 @@ def test_init_func(self): r, w = self.pipe() os.write(w, b'\0') - executor = self.executor_type( - initializer=write_msg, initargs=(w, msg)) - before = os.read(r, 100) - executor.submit(mul, 10, 10) - after = read_msg(r) + with self.executor_type( + initializer=write_msg, initargs=(w, msg)) as executor: + before = os.read(r, 100) + executor.submit(mul, 10, 10) + after = read_msg(r) self.assertEqual(before, b'\0') self.assertEqual(after, msg) @@ -261,11 +261,10 @@ def test_submit_script(self): import os os.write({w}, __name__.encode('utf-8') + b'\\0') """ - executor = self.executor_type() - - fut = executor.submit(script) - res = fut.result() - after = read_msg(r) + with self.executor_type() as executor: + fut = executor.submit(script) + res = fut.result() + after = read_msg(r) self.assertEqual(after, b'__main__') self.assertIs(res, None) @@ -279,25 +278,24 @@ def task2(): spam += 1 return spam - executor = self.executor_type() - - fut = executor.submit(task1) - with self.assertRaises(_interpreters.NotShareableError): - fut.result() + with self.executor_type() as executor: + fut = executor.submit(task1) + with self.assertRaises(_interpreters.NotShareableError): + fut.result() - fut = executor.submit(task2) - with self.assertRaises(_interpreters.NotShareableError): - fut.result() + fut = executor.submit(task2) + with self.assertRaises(_interpreters.NotShareableError): + fut.result() def test_submit_local_instance(self): class Spam: def __init__(self): self.value = True - executor = self.executor_type() - fut = executor.submit(Spam) - with self.assertRaises(_interpreters.NotShareableError): - fut.result() + with self.executor_type() as executor: + fut = executor.submit(Spam) + with self.assertRaises(_interpreters.NotShareableError): + fut.result() def test_submit_instance_method(self): class Spam: @@ -305,15 +303,15 @@ def run(self): return True spam = Spam() - executor = self.executor_type() - fut = executor.submit(spam.run) - with self.assertRaises(_interpreters.NotShareableError): - fut.result() + with self.executor_type() as executor: + fut = executor.submit(spam.run) + with self.assertRaises(_interpreters.NotShareableError): + fut.result() def test_submit_func_globals(self): - executor = self.executor_type() - fut = executor.submit(get_current_name) - name = fut.result() + with self.executor_type() as executor: + fut = executor.submit(get_current_name) + name = fut.result() self.assertEqual(name, __name__) self.assertNotEqual(name, '__main__') diff --git a/Lib/test/test_concurrent_futures/test_process_pool.py b/Lib/test/test_concurrent_futures/test_process_pool.py index e99abaf6523..c7d489c4f5b 100644 --- a/Lib/test/test_concurrent_futures/test_process_pool.py +++ b/Lib/test/test_concurrent_futures/test_process_pool.py @@ -3,13 +3,15 @@ import sys import threading import time +import traceback import unittest import unittest.mock +import weakref from concurrent import futures from concurrent.futures.process import BrokenProcessPool from test import support -from test.support import hashlib_helper +from test.support import hashlib_helper, threading_helper, warnings_helper from test.test_importlib.metadata.fixtures import parameterize from .executor import ExecutorTest, mul @@ -25,7 +27,6 @@ def __init__(self, mgr): def __del__(self): self.event.set() - TERMINATE_WORKERS = futures.ProcessPoolExecutor.terminate_workers.__name__ KILL_WORKERS = futures.ProcessPoolExecutor.kill_workers.__name__ FORCE_SHUTDOWN_PARAMS = [ @@ -42,6 +43,12 @@ def _put_wait_put(queue, event): queue.put('finished') +def _report_wait_return(queue, event, value): + """ Used as part of _run_stranded_worker_exit_test """ + queue.put(value) + event.wait() + return value + class ProcessPoolExecutorTest(ExecutorTest): @@ -51,6 +58,7 @@ def test_max_workers_too_large(self): "max_workers must be <= 61"): futures.ProcessPoolExecutor(max_workers=62) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_killed_child(self): # When a child process is abruptly terminated, the whole pool gets # "broken". @@ -63,6 +71,33 @@ def test_killed_child(self): # Submitting other jobs fails as well. self.assertRaises(BrokenProcessPool, self.executor.submit, pow, 2, 8) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() + def test_broken_process_pool_traceback(self): + # When a child process is abruptly terminated, the whole pool gets + # "broken", and a BrokenProcessPool exception should be created + # for each future instead of sharing one exception among all futures. + event = self.create_event() + futures = [self.executor.submit(event.wait) for _ in range(3)] + p = next(iter(self.executor._processes.values())) + p.terminate() + for fut in futures: + # Don't use assertRaises(): it clears the traceback off exc. + try: + fut.result() + except BrokenProcessPool as exc: + tb = exc.__traceback__ + else: + self.fail("BrokenProcessPool not raised") + count = sum( + 1 + for frame_summary in traceback.extract_tb(tb) + if frame_summary.filename == __file__ + ) + # This code file should appear exactly once in the traceback. + # A shared exception would accumulate a frame per result() call. + self.assertEqual(count, 1) + + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_map_chunksize(self): def bad_map(): list(self.executor.map(pow, range(40), range(40), chunksize=-1)) @@ -83,6 +118,7 @@ def bad_map(): def _test_traceback(cls): raise RuntimeError(123) # some comment + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_traceback(self): # We want ensure that the traceback from the child process is # contained in the traceback raised in the main process. @@ -105,6 +141,22 @@ def test_traceback(self): self.assertIn('raise RuntimeError(123) # some comment', f1.getvalue()) + def test_traceback_when_child_process_terminates_abruptly(self): + # gh-139462 enhancement - BrokenProcessPool exceptions + # should describe which process terminated. + exit_code = 99 + with self.executor_type(max_workers=1) as executor: + future = executor.submit(os._exit, exit_code) + with self.assertRaises(BrokenProcessPool) as bpe: + future.result() + + cause = bpe.exception.__cause__ + self.assertIsInstance(cause, futures.process._RemoteTraceback) + self.assertIn( + f"terminated abruptly with exit code {exit_code}", cause.tb + ) + + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @hashlib_helper.requires_hashdigest('md5') def test_ressources_gced_in_workers(self): # Ensure that argument for a job are correctly gc-ed after the job @@ -125,6 +177,7 @@ def test_ressources_gced_in_workers(self): mgr.shutdown() mgr.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_saturation(self): executor = self.executor mp_context = self.get_context() @@ -195,6 +248,153 @@ def test_max_tasks_per_child_defaults_to_spawn_context(self): executor = self.executor_type(1, max_tasks_per_child=3) self.assertEqual(executor._mp_context.get_start_method(), "spawn") + def test_max_tasks_per_child_pending_tasks_gh115634(self): + # gh-115634: A worker exiting at its max_tasks_per_child limit left a + # stale token in the idle worker semaphore, so no replacement worker + # was spawned and the remaining queued tasks deadlocked. Submit more + # tasks than the pool can run at once so a backlog is queued while + # workers hit their task limit. + context = self.get_context() + if context.get_start_method(allow_none=False) == "fork": + raise unittest.SkipTest("Incompatible with the fork start method.") + + for max_workers, max_tasks, num_tasks in [(1, 2, 6), (2, 2, 8)]: + with self.subTest(max_workers=max_workers, max_tasks=max_tasks): + executor = self.executor_type( + max_workers, mp_context=context, + max_tasks_per_child=max_tasks) + try: + futures = [executor.submit(mul, i, 2) + for i in range(num_tasks)] + # If the deadlock regresses, the result() calls time out, + # and the shutdown below hangs until the test timeout. + results = [f.result(timeout=support.SHORT_TIMEOUT) + for f in futures] + self.assertEqual(results, + [i * 2 for i in range(num_tasks)]) + finally: + executor.shutdown(wait=True, cancel_futures=True) + + def _run_stranded_worker_exit_test(self, *, shutdown, drop_reference): + # A worker exits upon reaching its max_tasks_per_child limit while + # more submitted work is queued. While the executor object is + # alive a replacement worker must be spawned and the remaining + # work executed; once it has been garbage collected no replacement + # is possible and the remaining futures must fail promptly instead + # of never resolving. + context = self.get_context() + if context.get_start_method(allow_none=False) == "fork": + raise unittest.SkipTest("Incompatible with the fork start method.") + manager = context.Manager() + self.addCleanup(manager.join) + self.addCleanup(manager.shutdown) + started = manager.Queue() + gate = manager.Event() + + executor = self.executor_type( + 1, mp_context=context, max_tasks_per_child=1) + futs = [executor.submit(_report_wait_return, started, gate, i) + for i in range(3)] + self.addCleanup(threading_helper.join_thread, + executor._executor_manager_thread) + # Wait until the worker is inside the first task so that it exits + # at its task limit only after the executor has been shut down + # and/or garbage collected below. + self.assertEqual(started.get(timeout=support.SHORT_TIMEOUT), 0) + if shutdown: + executor.shutdown(wait=False) + if drop_reference: + executor_ref = weakref.ref(executor) + executor = None + for _ in support.sleeping_retry(support.SHORT_TIMEOUT): + support.gc_collect() + if executor_ref() is None: + break + gate.set() + + self.assertEqual(futs[0].result(timeout=support.SHORT_TIMEOUT), 0) + if drop_reference: + for fut in futs[1:]: + with self.assertRaisesRegex(BrokenProcessPool, + "garbage collected"): + fut.result(timeout=support.SHORT_TIMEOUT) + else: + results = [f.result(timeout=support.SHORT_TIMEOUT) + for f in futs[1:]] + self.assertEqual(results, [1, 2]) + + def test_shutdown_no_wait_max_tasks_gh119592(self): + # gh-119592: shutdown(wait=False) used to clear executor state that + # worker replacement relied on. A worker exiting at its + # max_tasks_per_child limit afterwards could not be replaced, so + # the remaining submitted work never ran, and a racing worker exit + # could crash the manager thread on the partially cleared state. + for drop_reference in (False, True): + with self.subTest(drop_reference=drop_reference): + self._run_stranded_worker_exit_test( + shutdown=True, drop_reference=drop_reference) + + def test_gc_during_max_tasks_worker_exit_gh152967(self): + # gh-152967: If the executor was garbage collected without + # shutdown() while its last worker exited at its + # max_tasks_per_child limit, no replacement worker could be + # spawned and the remaining futures were never resolved. + self._run_stranded_worker_exit_test( + shutdown=False, drop_reference=True) + + def _run_unreplaceable_worker_exit_test(self, *, error_regex, + force_shutting_down=False, + failing_spawn=False): + # Drive a max_tasks_per_child worker exit while worker + # replacement is impossible; the queued futures must fail + # promptly with a BrokenProcessPool explaining why. + context = self.get_context() + if context.get_start_method(allow_none=False) == "fork": + raise unittest.SkipTest("Incompatible with the fork start method.") + manager = context.Manager() + self.addCleanup(manager.join) + self.addCleanup(manager.shutdown) + started = manager.Queue() + gate = manager.Event() + + executor = self.executor_type( + 1, mp_context=context, max_tasks_per_child=1) + futs = [executor.submit(_report_wait_return, started, gate, i) + for i in range(3)] + self.addCleanup(threading_helper.join_thread, + executor._executor_manager_thread) + self.assertEqual(started.get(timeout=support.SHORT_TIMEOUT), 0) + if force_shutting_down: + with executor._shutdown_lock: + executor._force_shutting_down = True + if failing_spawn: + spawn_patch = unittest.mock.patch( + "concurrent.futures.process._spawn_worker", + side_effect=OSError("spawn failed")) + spawn_patch.start() + self.addCleanup(spawn_patch.stop) + gate.set() + + self.assertEqual(futs[0].result(timeout=support.SHORT_TIMEOUT), 0) + for fut in futs[1:]: + with self.assertRaisesRegex(BrokenProcessPool, error_regex): + fut.result(timeout=support.SHORT_TIMEOUT) + + def test_force_shutdown_during_max_tasks_worker_exit(self): + # A worker exiting at its max_tasks_per_child limit during + # terminate_workers()/kill_workers() must not be replaced (the + # replacement would escape the kill); queued futures fail instead. + self._run_unreplaceable_worker_exit_test( + force_shutting_down=True, + error_regex="forcefully shut down") + + def test_failed_worker_replacement_breaks_pool(self): + # If no replacement worker can be started and no workers remain, + # the pool must break rather than strand the queued futures. + self._run_unreplaceable_worker_exit_test( + failing_spawn=True, + error_regex="could not be started") + def test_max_tasks_early_shutdown(self): context = self.get_context() if context.get_start_method(allow_none=False) == "fork": @@ -210,6 +410,7 @@ def test_max_tasks_early_shutdown(self): for i, future in enumerate(futures): self.assertEqual(future.result(), mul(i, i)) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_python_finalization_error(self): # gh-109047: Catch RuntimeError on thread creation # during Python finalization. @@ -238,7 +439,6 @@ def mock_start_new_thread(func, *args, **kwargs): list(executor.map(mul, [(2, 3)] * 10)) executor.shutdown() - def test_terminate_workers(self): mock_fn = unittest.mock.Mock() with self.executor_type(max_workers=1) as executor: @@ -261,6 +461,7 @@ def test_force_shutdown_workers_invalid_op(self): executor._force_shutdown, operation='invalid operation'), + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @parameterize(*FORCE_SHUTDOWN_PARAMS) def test_force_shutdown_workers(self, function_name): manager = self.get_context().Manager() @@ -350,5 +551,6 @@ def test_force_shutdown_workers_stops_pool(self, function_name): def setUpModule(): setup_module() + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_concurrent_futures/test_shutdown.py b/Lib/test/test_concurrent_futures/test_shutdown.py index 99b315b47e2..129b5cb5057 100644 --- a/Lib/test/test_concurrent_futures/test_shutdown.py +++ b/Lib/test/test_concurrent_futures/test_shutdown.py @@ -6,6 +6,7 @@ from concurrent import futures from test import support +from test.support import warnings_helper from test.support.script_helper import assert_python_ok from .util import ( @@ -49,6 +50,7 @@ def test_interpreter_shutdown(self): self.assertFalse(err) self.assertEqual(out.strip(), b"apple") + @support.force_not_colorized def test_submit_after_interpreter_shutdown(self): # Test the atexit hook for shutdown of worker threads and processes rc, out, err = assert_python_ok('-c', """if 1: @@ -77,12 +79,14 @@ def run_last(): self.assertIn("RuntimeError: cannot schedule new futures", err.decode()) self.assertEqual(out.strip(), b"runtime-error") + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_hang_issue12364(self): fs = [self.executor.submit(time.sleep, 0.1) for _ in range(50)] self.executor.shutdown() for f in fs: f.result() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_cancel_futures(self): assert self.worker_count <= 5, "test needs few workers" fs = [self.executor.submit(time.sleep, .1) for _ in range(50)] @@ -111,23 +115,25 @@ def test_hang_gh83386(self): See https://github.com/python/cpython/issues/83386. """ - if self.executor_type == futures.ProcessPoolExecutor: - raise unittest.SkipTest( - "Hangs, see https://github.com/python/cpython/issues/83386") - rc, out, err = assert_python_ok('-c', """if True: from concurrent.futures import {executor_type} from test.test_concurrent_futures.test_shutdown import sleep_and_print if __name__ == "__main__": - if {context!r}: multiprocessing.set_start_method({context!r}) - t = {executor_type}(max_workers=3) + context = '{context}' + if context == "": + t = {executor_type}(max_workers=3) + else: + from multiprocessing import get_context + t = {executor_type}(max_workers=3, + mp_context=get_context(context)) t.submit(sleep_and_print, 1.0, "apple") t.shutdown(wait=False) """.format(executor_type=self.executor_type.__name__, - context=getattr(self, 'ctx', None))) + context=getattr(self, 'ctx', ''))) self.assertFalse(err) self.assertEqual(out.strip(), b"apple") + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_hang_gh94440(self): """shutdown(wait=True) doesn't hang when a future was submitted and quickly canceled right before shutdown. @@ -171,6 +177,7 @@ def acquire_lock(lock): for t in self.executor._threads: t.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_context_manager_shutdown(self): with futures.ThreadPoolExecutor(max_workers=5) as e: executor = e @@ -180,6 +187,7 @@ def test_context_manager_shutdown(self): for t in executor._threads: t.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_del_shutdown(self): executor = futures.ThreadPoolExecutor(max_workers=5) res = executor.map(abs, range(-5, 5)) @@ -193,6 +201,7 @@ def test_del_shutdown(self): # executor got shutdown. assert all([r == abs(v) for r, v in zip(res, range(-5, 5))]) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_shutdown_no_wait(self): # Ensure that the executor cleans up the threads when calling # shutdown with wait=False @@ -207,7 +216,7 @@ def test_shutdown_no_wait(self): # executor got shutdown. assert all([r == abs(v) for r, v in zip(res, range(-5, 5))]) - + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_thread_names_assigned(self): executor = futures.ThreadPoolExecutor( max_workers=5, thread_name_prefix='SpecialPool') @@ -220,6 +229,7 @@ def test_thread_names_assigned(self): self.assertRegex(t.name, r'^SpecialPool_[0-4]$') t.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_thread_names_default(self): executor = futures.ThreadPoolExecutor(max_workers=5) executor.map(abs, range(-5, 5)) @@ -234,8 +244,6 @@ def test_thread_names_default(self): t.join() def test_cancel_futures_wait_false(self): - # Can only be reliably tested for TPE, since PPE often hangs with - # `wait=False` (even without *cancel_futures*). rc, out, err = assert_python_ok('-c', """if True: from concurrent.futures import ThreadPoolExecutor from test.test_concurrent_futures.test_shutdown import sleep_and_print @@ -253,6 +261,7 @@ def test_cancel_futures_wait_false(self): class ProcessPoolShutdownTest(ExecutorShutdownTest): + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_processes_terminate(self): def acquire_lock(lock): lock.acquire() @@ -276,6 +285,7 @@ def acquire_lock(lock): for p in processes.values(): p.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_context_manager_shutdown(self): with futures.ProcessPoolExecutor( max_workers=5, mp_context=self.get_context()) as e: @@ -286,6 +296,7 @@ def test_context_manager_shutdown(self): for p in processes.values(): p.join() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_del_shutdown(self): executor = futures.ProcessPoolExecutor( max_workers=5, mp_context=self.get_context()) @@ -308,6 +319,7 @@ def test_del_shutdown(self): # executor got shutdown. assert all([r == abs(v) for r, v in zip(res, range(-5, 5))]) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_shutdown_no_wait(self): # Ensure that the executor cleans up the processes when calling # shutdown with wait=False diff --git a/Lib/test/test_concurrent_futures/test_thread_pool.py b/Lib/test/test_concurrent_futures/test_thread_pool.py index 4324241b374..226b7acb64f 100644 --- a/Lib/test/test_concurrent_futures/test_thread_pool.py +++ b/Lib/test/test_concurrent_futures/test_thread_pool.py @@ -7,6 +7,7 @@ import unittest from concurrent import futures from test import support +from test.support import warnings_helper from .executor import ExecutorTest, mul from .util import BaseTestCase, ThreadPoolMixin, setup_module @@ -53,6 +54,7 @@ def test_idle_thread_reuse(self): @support.requires_fork() @unittest.skipUnless(hasattr(os, 'register_at_fork'), 'need os.register_at_fork') @support.requires_resource('cpu') + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_hang_global_shutdown_lock(self): # bpo-45021: _global_shutdown_lock should be reinitialized in the child # process, otherwise it will never exit @@ -68,6 +70,7 @@ def submit(pool): @support.requires_fork() @unittest.skipUnless(hasattr(os, 'register_at_fork'), 'need os.register_at_fork') + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_process_fork_from_a_threadpool(self): # bpo-43944: clear concurrent.futures.thread._threads_queues after fork, # otherwise child process will try to join parent thread diff --git a/Lib/test/test_concurrent_futures/test_wait.py b/Lib/test/test_concurrent_futures/test_wait.py index 6749a690f6c..b8250cec7ab 100644 --- a/Lib/test/test_concurrent_futures/test_wait.py +++ b/Lib/test/test_concurrent_futures/test_wait.py @@ -3,7 +3,7 @@ import unittest from concurrent import futures from test import support -from test.support import threading_helper +from test.support import threading_helper, warnings_helper from .util import ( CANCELLED_FUTURE, CANCELLED_AND_NOTIFIED_FUTURE, EXCEPTION_FUTURE, @@ -22,6 +22,7 @@ def wait_and_raise(e): class WaitTests: + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_20369(self): # See https://bugs.python.org/issue20369 future = self.executor.submit(mul, 1, 2) @@ -30,7 +31,7 @@ def test_20369(self): self.assertEqual({future}, done) self.assertEqual(set(), not_done) - + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_first_completed(self): event = self.create_event() future1 = self.executor.submit(mul, 21, 2) @@ -47,6 +48,7 @@ def test_first_completed(self): event.set() future2.result() # wait for job to finish + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_first_completed_some_already_completed(self): event = self.create_event() future1 = self.executor.submit(event.wait) @@ -64,6 +66,7 @@ def test_first_completed_some_already_completed(self): event.set() future1.result() # wait for job to finish + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_first_exception(self): event1 = self.create_event() event2 = self.create_event() @@ -93,6 +96,7 @@ def wait_for_future1(): event2.set() future3.result() # wait for job to finish + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_first_exception_some_already_complete(self): event = self.create_event() future1 = self.executor.submit(divmod, 21, 0) @@ -114,6 +118,7 @@ def test_first_exception_some_already_complete(self): event.set() future2.result() # wait for job to finish + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_first_exception_one_already_failed(self): event = self.create_event() future1 = self.executor.submit(event.wait) @@ -129,6 +134,7 @@ def test_first_exception_one_already_failed(self): event.set() future1.result() # wait for job to finish + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_all_completed(self): future1 = self.executor.submit(divmod, 2, 0) future2 = self.executor.submit(mul, 2, 21) @@ -148,6 +154,7 @@ def test_all_completed(self): future2]), finished) self.assertEqual(set(), pending) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_timeout(self): short_timeout = 0.050 @@ -200,5 +207,6 @@ def future_func(): def setUpModule(): setup_module() + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_concurrent_futures/util.py b/Lib/test/test_concurrent_futures/util.py index 1ea5a16d8b9..1dad78f08e7 100644 --- a/Lib/test/test_concurrent_futures/util.py +++ b/Lib/test/test_concurrent_futures/util.py @@ -10,7 +10,7 @@ from concurrent.futures.process import _check_system_limits from test import support -from test.support import threading_helper +from test.support import threading_helper, warnings_helper def create_future(state=PENDING, exception=None, result=None): @@ -51,7 +51,8 @@ def setUp(self): max_workers=self.worker_count, mp_context=self.get_context(), **self.executor_kwargs) - self.manager = self.get_context().Manager() + with warnings_helper.ignore_fork_in_thread_deprecation_warnings(): + self.manager = self.get_context().Manager() else: self.executor = self.executor_type( max_workers=self.worker_count, @@ -138,6 +139,8 @@ def get_context(self): self.skipTest("require unix system") if support.check_sanitizer(thread=True): self.skipTest("TSAN doesn't support threads after fork") + if "forkserver" not in multiprocessing.get_all_start_methods(): + self.skipTest("forkserver start method is not available") return super().get_context() def create_event(self): @@ -146,6 +149,7 @@ def create_event(self): def create_executor_tests(remote_globals, mixin, bases=(BaseTestCase,), executor_mixins=(ThreadPoolMixin, + InterpreterPoolMixin, ProcessPoolForkMixin, ProcessPoolForkserverMixin, ProcessPoolSpawnMixin)): diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index 6a3329fa5aa..e291f814edb 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -680,6 +680,154 @@ def test(x): self.assertEqual(state, [1, 'something else', 999]) + def test_contextmanager_decorate_generator_function(self): + @contextmanager + def woohoo(y): + state.append(y) + yield + state.append(999) + + state = [] + @woohoo(1) + def test(x): + self.assertEqual(state, [1]) + state.append(x) + yield + state.append("second item") + return "result" + + gen = test("something") + for _ in gen: + self.assertEqual(state, [1, "something"]) + self.assertEqual(state, [1, "something", "second item", 999]) + + # The wrapped generator's return value is preserved. + state = [] + gen = test("something") + with self.assertRaises(StopIteration) as cm: + while True: + next(gen) + self.assertEqual(cm.exception.value, "result") + + + def test_contextmanager_decorate_generator_function_exception(self): + @contextmanager + def woohoo(): + state.append("enter") + try: + yield + finally: + state.append("exit") + + state = [] + @woohoo() + def test(): + state.append("body") + yield + raise ZeroDivisionError + + with self.assertRaises(ZeroDivisionError): + for _ in test(): + pass + self.assertEqual(state, ["enter", "body", "exit"]) + + + def test_contextmanager_decorate_generator_function_early_stop(self): + @contextmanager + def woohoo(): + state.append("enter") + try: + yield + finally: + state.append("exit") + + state = [] + @woohoo() + def test(): + try: + yield 1 + yield 2 + finally: + state.append("inner closed") + + gen = test() + self.assertEqual(next(gen), 1) + gen.close() + # The inner generator is closed before the context manager exits. + self.assertEqual(state, ["enter", "inner closed", "exit"]) + + + def test_contextmanager_decorate_generator_function_send_throw(self): + @contextmanager + def woohoo(): + yield + + @woohoo() + def test(): + received = yield "first" + state.append(("received", received)) + try: + yield "second" + except ValueError as exc: + state.append(("caught", type(exc))) + yield "after throw" + + # .send() and .throw() are forwarded to the wrapped generator. + state = [] + gen = test() + self.assertEqual(next(gen), "first") + self.assertEqual(gen.send("VALUE"), "second") + self.assertEqual(gen.throw(ValueError), "after throw") + gen.close() + self.assertEqual( + state, [("received", "VALUE"), ("caught", ValueError)] + ) + + + def test_contextmanager_decorate_coroutine_function(self): + @contextmanager + def woohoo(y): + state.append(y) + yield + state.append(999) + + state = [] + @woohoo(1) + async def test(x): + self.assertEqual(state, [1]) + state.append(x) + + coro = test("something") + with self.assertRaises(StopIteration): + coro.send(None) + + self.assertEqual(state, [1, "something", 999]) + + + def test_contextmanager_decorate_asyncgen_function(self): + @contextmanager + def woohoo(y): + state.append(y) + yield + state.append(999) + + state = [] + @woohoo(1) + async def test(x): + self.assertEqual(state, [1]) + state.append(x) + yield + state.append("second item") + + agen = test("something") + with self.assertRaises(StopIteration): + agen.asend(None).send(None) + with self.assertRaises(StopAsyncIteration): + agen.asend(None).send(None) + + self.assertEqual(state, [1, "something", "second item", 999]) + + class TestBaseExitStack: exit_stack = None @@ -788,6 +936,75 @@ def _exit(): result.append(2) self.assertEqual(result, [1, 2, 3, 4]) + def test_enter_context_classmethod(self): + class TestCM: + @classmethod + def __enter__(cls): + result.append(('enter', cls)) + @classmethod + def __exit__(cls, *exc_details): + result.append(('exit', cls, *exc_details)) + + cm = TestCM() + result = [] + with self.exit_stack() as stack: + stack.enter_context(cm) + self.assertEqual(result, [('enter', TestCM)]) + self.assertEqual(result, [('enter', TestCM), + ('exit', TestCM, None, None, None)]) + + result = [] + with self.exit_stack() as stack: + stack.push(cm) + self.assertEqual(result, []) + self.assertEqual(result, [('exit', TestCM, None, None, None)]) + + def test_enter_context_staticmethod(self): + class TestCM: + @staticmethod + def __enter__(): + result.append('enter') + @staticmethod + def __exit__(*exc_details): + result.append(('exit', *exc_details)) + + cm = TestCM() + result = [] + with self.exit_stack() as stack: + stack.enter_context(cm) + self.assertEqual(result, ['enter']) + self.assertEqual(result, ['enter', ('exit', None, None, None)]) + + result = [] + with self.exit_stack() as stack: + stack.push(cm) + self.assertEqual(result, []) + self.assertEqual(result, [('exit', None, None, None)]) + + def test_enter_context_slots(self): + class TestCM: + __slots__ = ('__enter__', '__exit__') + def __init__(self): + def enter(): + result.append('enter') + def exit(*exc_details): + result.append(('exit', *exc_details)) + self.__enter__ = enter + self.__exit__ = exit + + cm = TestCM() + result = [] + with self.exit_stack() as stack: + stack.enter_context(cm) + self.assertEqual(result, ['enter']) + self.assertEqual(result, ['enter', ('exit', None, None, None)]) + + result = [] + with self.exit_stack() as stack: + stack.push(cm) + self.assertEqual(result, []) + self.assertEqual(result, [('exit', None, None, None)]) + def test_enter_context_errors(self): class LacksEnterAndExit: pass diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index dcd00720379..95bdfdb3d9d 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -402,6 +402,144 @@ async def test(): await test() self.assertFalse(entered) + @_async_test + async def test_decorator_decorate_sync_function(self): + @asynccontextmanager + async def context(): + state.append(1) + yield + state.append(999) + + state = [] + @context() + def test(x): + self.assertEqual(state, [1]) + state.append(x) + + await test("something") + self.assertEqual(state, [1, "something", 999]) + + @_async_test + async def test_decorator_decorate_generator_function(self): + @asynccontextmanager + async def context(): + state.append(1) + yield + state.append(999) + + state = [] + @context() + def test(x): + self.assertEqual(state, [1]) + state.append(x) + yield + state.append("second item") + + async for _ in test("something"): + self.assertEqual(state, [1, "something"]) + self.assertEqual(state, [1, "something", "second item", 999]) + + @_async_test + async def test_decorator_decorate_asyncgen_function(self): + @asynccontextmanager + async def context(): + state.append(1) + yield + state.append(999) + + state = [] + @context() + async def test(x): + self.assertEqual(state, [1]) + state.append(x) + yield + state.append("second item") + + async for _ in test("something"): + self.assertEqual(state, [1, "something"]) + self.assertEqual(state, [1, "something", "second item", 999]) + + @_async_test + async def test_decorator_decorate_asyncgen_function_exception(self): + @asynccontextmanager + async def context(): + state.append("enter") + try: + yield + finally: + state.append("exit") + + state = [] + @context() + async def test(): + state.append("body") + yield + raise ZeroDivisionError + + with self.assertRaises(ZeroDivisionError): + async for _ in test(): + pass + self.assertEqual(state, ["enter", "body", "exit"]) + + @_async_test + async def test_decorator_decorate_asyncgen_function_early_stop(self): + @asynccontextmanager + async def context(): + state.append("enter") + try: + yield + finally: + state.append("exit") + + state = [] + @context() + async def test(): + try: + yield 1 + yield 2 + finally: + state.append("inner closed") + + agen = test() + async for value in agen: + self.assertEqual(value, 1) + break + await agen.aclose() + # The inner async generator is closed before the context + # manager exits. + self.assertEqual(state, ["enter", "inner closed", "exit"]) + + @_async_test + async def test_decorator_decorate_asyncgen_function_asend_athrow(self): + @asynccontextmanager + async def context(): + yield + + @context() + async def test(): + try: + received = yield "first" + state.append(("received", received)) + yield "second" + except ValueError: + state.append("inner saw ValueError") + raise + finally: + state.append("inner closed") + + # asend() values and athrow() exceptions are not forwarded to the + # wrapped generator (a documented limitation). + state = [] + agen = test() + self.assertEqual(await agen.__anext__(), "first") + self.assertEqual(await agen.asend("VALUE"), "second") + # The inner generator received None, not "VALUE". + self.assertEqual(state, [("received", None)]) + with self.assertRaises(ValueError): + await agen.athrow(ValueError) + # The inner generator was closed, not thrown into. + self.assertEqual(state, [("received", None), "inner closed"]) + @_async_test async def test_decorator_with_exception(self): entered = False @@ -641,6 +779,78 @@ async def _exit(): self.assertEqual(result, [1, 2, 3, 4]) + @_async_test + async def test_enter_async_context_classmethod(self): + class TestCM: + @classmethod + async def __aenter__(cls): + result.append(('enter', cls)) + @classmethod + async def __aexit__(cls, *exc_details): + result.append(('exit', cls, *exc_details)) + + cm = TestCM() + result = [] + async with self.exit_stack() as stack: + await stack.enter_async_context(cm) + self.assertEqual(result, [('enter', TestCM)]) + self.assertEqual(result, [('enter', TestCM), + ('exit', TestCM, None, None, None)]) + + result = [] + async with self.exit_stack() as stack: + stack.push_async_exit(cm) + self.assertEqual(result, []) + self.assertEqual(result, [('exit', TestCM, None, None, None)]) + + @_async_test + async def test_enter_async_context_staticmethod(self): + class TestCM: + @staticmethod + async def __aenter__(): + result.append('enter') + @staticmethod + async def __aexit__(*exc_details): + result.append(('exit', *exc_details)) + + cm = TestCM() + result = [] + async with self.exit_stack() as stack: + await stack.enter_async_context(cm) + self.assertEqual(result, ['enter']) + self.assertEqual(result, ['enter', ('exit', None, None, None)]) + + result = [] + async with self.exit_stack() as stack: + stack.push_async_exit(cm) + self.assertEqual(result, []) + self.assertEqual(result, [('exit', None, None, None)]) + + @_async_test + async def test_enter_async_context_slots(self): + class TestCM: + __slots__ = ('__aenter__', '__aexit__') + def __init__(self): + async def enter(): + result.append('enter') + async def exit(*exc_details): + result.append(('exit', *exc_details)) + self.__aenter__ = enter + self.__aexit__ = exit + + cm = TestCM() + result = [] + async with self.exit_stack() as stack: + await stack.enter_async_context(cm) + self.assertEqual(result, ['enter']) + self.assertEqual(result, ['enter', ('exit', None, None, None)]) + + result = [] + async with self.exit_stack() as stack: + stack.push_async_exit(cm) + self.assertEqual(result, []) + self.assertEqual(result, [('exit', None, None, None)]) + @_async_test async def test_enter_async_context_errors(self): class LacksEnterAndExit: diff --git a/Lib/test/test_coroutines.py b/Lib/test/test_coroutines.py index a515e0f5ca9..674273e6cdf 100644 --- a/Lib/test/test_coroutines.py +++ b/Lib/test/test_coroutines.py @@ -1,3 +1,4 @@ +import ast import contextlib import copy import inspect @@ -407,6 +408,118 @@ async def bar(): with self.subTest(code=code), self.assertRaises(SyntaxError): compile(code, "", "exec") + def test_async_comprehension_scope(self): + # List/set/dict comprehensions with await or async for are allowed + # only in async functions, or at module level with top-level await. + allowed = [ + "async def f():\n [await x for x in y]", + "async def f():\n {await x for x in y}", + "async def f():\n {k: await x for k, x in y}", + "async def f():\n [x async for x in y]", + "async def f():\n {x async for x in y}", + "async def f():\n {k: x async for k, x in y}", + "async def f():\n [[await x for x in y] for y in z]", + # Defaults, bases, and genexp iterables are evaluated in the + # enclosing scope. + "async def outer():\n async def f(x=[await y for y in z]): pass", + "async def f():\n class C([await x for x in y]): pass", + "async def f():\n (x for x in [await y for y in z])", + "async def f():\n (x for x in [y async for y in z])", + ] + for code in allowed: + with self.subTest(code=code): + compile(code, "", "exec") + + # Generator expressions with await are async genexps and may appear + # outside async functions. A listcomp nested in a genexp body is also + # allowed (the genexp becomes an async generator). + for code in [ + "(await x for x in y)", + "def f():\n (await x for x in y)", + "class C:\n (await x for x in y)", + "lambda: (await x for x in y)", + "([await x for x in y] for y in z)", + "def f():\n ([await x for x in y] for y in z)", + "class C:\n ([await x for x in y] for y in z)", + "async def f():\n ([await x for x in y] for y in z)", + ]: + with self.subTest(code=code): + compile(code, "", "exec") + + err = "asynchronous comprehension outside of an asynchronous function" + invalid = [ + "[await x for x in y]", + "{await x for x in y}", + "{k: await x for k, x in y}", + "[x async for x in y]", + "{x async for x in y}", + "{k: x async for k, x in y}", + "[[await x for x in y] for y in z]", + "[[x async for x in y] for y in z]", + "def f():\n [await x for x in y]", + "def f():\n [x async for x in y]", + "async def f():\n def g():\n [await x for x in y]", + "class C:\n [await x for x in y]", + "class C:\n {await x for x in y}", + "class C:\n {k: await x for k, x in y}", + "class C:\n [x async for x in y]", + "class C:\n [[await x for x in y] for y in z]", + "async def f():\n class C:\n x = [await y for y in z]", + "async def f():\n class C:\n x = [y async for y in z]", + # Lambdas are never async, even inside async def. + "lambda: [await x for x in y]", + "async def f():\n lambda: [await x for x in y]", + "class C:\n f = lambda: [await x for x in y]", + # Defaults, bases, and genexp iterables run in the enclosing scope. + "(x for x in [await y for y in z])", + "(x for x in [y async for y in z])", + "def f():\n (x for x in [await y for y in z])", + "async def f(x=[await y for y in z]): pass", + "def f(x=[await y for y in z]): pass", + "class C:\n def f(self, x=[await y for y in z]): pass", + "class C([await x for x in y]): pass", + # Type aliases and type-parameter scopes. + "type T = [await x for x in y]", + "type T = [x async for x in y]", + "def f[T=[await x for x in y]](): pass", + "def f[T: [await x for x in y]](): pass", + "async def f[T=[await x for x in y]](): pass", + "async def f(x: [await y for y in z]): pass", + ] + for code in invalid: + with self.subTest(code=code): + support.check_syntax_error(self, code, err) + + support.check_syntax_error( + self, "await x", "'await' outside function") + support.check_syntax_error( + self, "class C:\n await x", "'await' outside function") + support.check_syntax_error( + self, "def f():\n await x", "'await' outside async function") + + flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT + for code in [ + "[await x for x in y]", + "async def f(x=[await y for y in z]): pass", + "class C([await x for x in y]): pass", + "f'{[await x for x in y]}'", + "(x for x in [await y for y in z])", + "(x for x in [y async for y in z])", + ]: + with self.subTest(code=code, tla=True): + compile(code, "", "exec", flags=flags) + still_invalid = [ + "lambda: [await x for x in y]", + "def f():\n (x for x in [await y for y in z])", + "class C:\n def f(self, x=[await y for y in z]): pass", + "type T = [await x for x in y]", + "async def f[T=[await x for x in y]](): pass", + ] + for code in still_invalid: + with self.subTest(code=code, tla=True): + with self.assertRaisesRegex(SyntaxError, err): + compile(code, "", "exec", flags=flags) + def test_badsyntax_2(self): samples = [ """def foo(): @@ -1008,7 +1121,7 @@ async def foo(): return (await Awaitable()) with self.assertRaisesRegex( - TypeError, "__await__.*returned non-iterator of type"): + TypeError, "__await__.*must return an iterator, not"): run_async(foo()) @@ -1106,7 +1219,7 @@ async def foo(): return await Awaitable() with self.assertRaisesRegex( - TypeError, r"__await__\(\) returned a coroutine"): + TypeError, r"__await__\(\) must return an iterator, not coroutine"): run_async(foo()) c.close() @@ -1120,7 +1233,7 @@ async def foo(): return await Awaitable() with self.assertRaisesRegex( - TypeError, "__await__.*returned non-iterator of type"): + TypeError, "__await__.*must return an iterator, not"): run_async(foo()) @@ -2265,36 +2378,20 @@ def c(): # before fixing, visible stack from throw would be shorter than from send. self.assertEqual(len_send, len_throw) - def test_call_aiter_once_in_comprehension(self): - - class AsyncIterator: - - def __init__(self): - self.val = 0 - - async def __anext__(self): - if self.val == 2: - raise StopAsyncIteration - self.val += 1 - return self.val - - # No __aiter__ method - - class C: + def test_call_generator_in_frame_clear(self): + # gh-143939: Running a generator while clearing the coroutine's frame + # should not be misinterpreted as a yield. + class CallGeneratorOnDealloc: + def __del__(self): + next(x for x in [1]) - def __aiter__(self): - return AsyncIterator() - - async def run_listcomp(): - return [i async for i in C()] - - async def run_asyncgen(): - ag = (i async for i in C()) - return [i async for i in ag] - - self.assertEqual(run_async(run_listcomp()), ([], [1, 2])) - self.assertEqual(run_async(run_asyncgen()), ([], [1, 2])) + async def coro(): + obj = CallGeneratorOnDealloc() + return 42 + yielded, result = run_async(coro()) + self.assertEqual(yielded, []) + self.assertEqual(result, 42) @unittest.skipIf( support.is_emscripten or support.is_wasi, @@ -2520,7 +2617,7 @@ async def foo(): return (await future) with self.assertRaisesRegex( - TypeError, "__await__.*returned non-iterator of type 'int'"): + TypeError, "__await__.*must return an iterator, not int"): self.assertEqual(foo().send(None), 1) diff --git a/Lib/test/test_crossinterp.py b/Lib/test/test_crossinterp.py new file mode 100644 index 00000000000..f4bf5a66ad2 --- /dev/null +++ b/Lib/test/test_crossinterp.py @@ -0,0 +1,1550 @@ +import contextlib +import itertools +import sys +import traceback +import types +import unittest +import warnings + +from test import support +from test.support import import_helper + +_testinternalcapi = import_helper.import_module('_testinternalcapi') +_interpreters = import_helper.import_module('_interpreters') +from _interpreters import NotShareableError + +from test import _code_definitions as code_defs +from test import _crossinterp_definitions as defs + + +@contextlib.contextmanager +def ignore_byteswarning(): + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', category=BytesWarning) + yield + + +# builtin types + +BUILTINS_TYPES = [o for _, o in __builtins__.items() if isinstance(o, type)] +EXCEPTION_TYPES = [cls for cls in BUILTINS_TYPES + if issubclass(cls, BaseException)] +OTHER_TYPES = [o for n, o in vars(types).items() + if (isinstance(o, type) and + n not in ('DynamicClassAttribute', '_GeneratorWrapper'))] +BUILTIN_TYPES = [ + *BUILTINS_TYPES, + *OTHER_TYPES, +] + +# builtin exceptions + +try: + raise Exception +except Exception as exc: + CAUGHT = exc +EXCEPTIONS_WITH_SPECIAL_SIG = { + BaseExceptionGroup: (lambda msg: (msg, [CAUGHT])), + ExceptionGroup: (lambda msg: (msg, [CAUGHT])), + UnicodeError: (lambda msg: (None, msg, None, None, None)), + UnicodeEncodeError: (lambda msg: ('utf-8', '', 1, 3, msg)), + UnicodeDecodeError: (lambda msg: ('utf-8', b'', 1, 3, msg)), + UnicodeTranslateError: (lambda msg: ('', 1, 3, msg)), +} +BUILTIN_EXCEPTIONS = [ + *(cls(*sig('error!')) for cls, sig in EXCEPTIONS_WITH_SPECIAL_SIG.items()), + *(cls('error!') for cls in EXCEPTION_TYPES + if cls not in EXCEPTIONS_WITH_SPECIAL_SIG), +] + +# other builtin objects + +METHOD = defs.SpamOkay().okay +BUILTIN_METHOD = [].append +METHOD_DESCRIPTOR_WRAPPER = str.join +METHOD_WRAPPER = object().__str__ +WRAPPER_DESCRIPTOR = object.__init__ +BUILTIN_WRAPPERS = { + METHOD: types.MethodType, + BUILTIN_METHOD: types.BuiltinMethodType, + dict.__dict__['fromkeys']: types.ClassMethodDescriptorType, + types.FunctionType.__code__: types.GetSetDescriptorType, + types.FunctionType.__globals__: types.MemberDescriptorType, + METHOD_DESCRIPTOR_WRAPPER: types.MethodDescriptorType, + METHOD_WRAPPER: types.MethodWrapperType, + WRAPPER_DESCRIPTOR: types.WrapperDescriptorType, + staticmethod(defs.SpamOkay.okay): None, + classmethod(defs.SpamOkay.okay): None, + property(defs.SpamOkay.okay): None, +} +BUILTIN_FUNCTIONS = [ + # types.BuiltinFunctionType + len, + sys.is_finalizing, + sys.exit, + _testinternalcapi.get_crossinterp_data, +] +assert 'emptymod' not in sys.modules +with import_helper.ready_to_import('emptymod', ''): + import emptymod as EMPTYMOD +MODULES = [ + sys, + defs, + unittest, + EMPTYMOD, +] +OBJECT = object() +EXCEPTION = Exception() +LAMBDA = (lambda: None) +BUILTIN_SIMPLE = [ + OBJECT, + # singletons + None, + True, + False, + Ellipsis, + NotImplemented, + # bytes + *(i.to_bytes(2, 'little', signed=True) + for i in range(-1, 258)), + # str + 'hello world', + '你好世界', + '', + # int + sys.maxsize + 1, + sys.maxsize, + -sys.maxsize - 1, + -sys.maxsize - 2, + *range(-1, 258), + 2**1000, + # float + 0.0, + 1.1, + -1.0, + 0.12345678, + -0.12345678, +] +TUPLE_EXCEPTION = (0, 1.0, EXCEPTION) +TUPLE_OBJECT = (0, 1.0, OBJECT) +TUPLE_NESTED_EXCEPTION = (0, 1.0, (EXCEPTION,)) +TUPLE_NESTED_OBJECT = (0, 1.0, (OBJECT,)) +MEMORYVIEW_EMPTY = memoryview(b'') +MEMORYVIEW_NOT_EMPTY = memoryview(b'spam'*42) +MAPPING_PROXY_EMPTY = types.MappingProxyType({}) +BUILTIN_CONTAINERS = [ + # tuple (flat) + (), + (1,), + ("hello", "world", ), + (1, True, "hello"), + TUPLE_EXCEPTION, + TUPLE_OBJECT, + # tuple (nested) + ((1,),), + ((1, 2), (3, 4)), + ((1, 2), (3, 4), (5, 6)), + TUPLE_NESTED_EXCEPTION, + TUPLE_NESTED_OBJECT, + # buffer + MEMORYVIEW_EMPTY, + MEMORYVIEW_NOT_EMPTY, + # list + [], + [1, 2, 3], + [[1], (2,), {3: 4}], + # dict + {}, + {1: 7, 2: 8, 3: 9}, + {1: [1], 2: (2,), 3: {3: 4}}, + # frozendict + frozendict(), + frozendict({1: 7, 2: 8, 3: 9}), + frozendict({1: [1], 2: (2,), 3: {3: 4}, 4: frozendict({5: 6})}), + # set + set(), + {1, 2, 3}, + {frozenset({1}), (2,)}, + # frozenset + frozenset([]), + frozenset({frozenset({1}), (2,)}), + # bytearray + bytearray(b''), + # other + MAPPING_PROXY_EMPTY, + types.SimpleNamespace(), +] +ns = {} +exec(""" +try: + raise Exception +except Exception as exc: + TRACEBACK = exc.__traceback__ + FRAME = TRACEBACK.tb_frame +""", ns, ns) +BUILTIN_OTHER = [ + # types.CellType + types.CellType(), + # types.FrameType + ns['FRAME'], + # types.TracebackType + ns['TRACEBACK'], +] +del ns + +# user-defined objects + +USER_TOP_INSTANCES = [c(*a) for c, a in defs.TOP_CLASSES.items()] +USER_NESTED_INSTANCES = [c(*a) for c, a in defs.NESTED_CLASSES.items()] +USER_INSTANCES = [ + *USER_TOP_INSTANCES, + *USER_NESTED_INSTANCES, +] +USER_EXCEPTIONS = [ + defs.MimimalError('error!'), +] + +# shareable objects + +TUPLES_WITHOUT_EQUALITY = [ + TUPLE_EXCEPTION, + TUPLE_OBJECT, + TUPLE_NESTED_EXCEPTION, + TUPLE_NESTED_OBJECT, +] +_UNSHAREABLE_SIMPLE = [ + Ellipsis, + NotImplemented, + OBJECT, + sys.maxsize + 1, + -sys.maxsize - 2, + 2**1000, +] +with ignore_byteswarning(): + _SHAREABLE_SIMPLE = [o for o in BUILTIN_SIMPLE + if o not in _UNSHAREABLE_SIMPLE] + _SHAREABLE_CONTAINERS = [ + *(o for o in BUILTIN_CONTAINERS if type(o) is memoryview), + *(o for o in BUILTIN_CONTAINERS + if type(o) is tuple and o not in TUPLES_WITHOUT_EQUALITY), + ] + _UNSHAREABLE_CONTAINERS = [o for o in BUILTIN_CONTAINERS + if o not in _SHAREABLE_CONTAINERS] +SHAREABLE = [ + *_SHAREABLE_SIMPLE, + *_SHAREABLE_CONTAINERS, +] +NOT_SHAREABLE = [ + *_UNSHAREABLE_SIMPLE, + *_UNSHAREABLE_CONTAINERS, + *BUILTIN_TYPES, + *BUILTIN_WRAPPERS, + *BUILTIN_EXCEPTIONS, + *BUILTIN_FUNCTIONS, + *MODULES, + *BUILTIN_OTHER, + # types.CodeType + *(f.__code__ for f in defs.FUNCTIONS), + *(f.__code__ for f in defs.FUNCTION_LIKE), + # types.FunctionType + *defs.FUNCTIONS, + defs.SpamOkay.okay, + LAMBDA, + *defs.FUNCTION_LIKE, + # coroutines and generators + *defs.FUNCTION_LIKE_APPLIED, + # user classes + *defs.CLASSES, + *USER_INSTANCES, + # user exceptions + *USER_EXCEPTIONS, +] + +# pickleable objects + +PICKLEABLE = [ + *BUILTIN_SIMPLE, + *(o for o in BUILTIN_CONTAINERS if o not in [ + MEMORYVIEW_EMPTY, + MEMORYVIEW_NOT_EMPTY, + MAPPING_PROXY_EMPTY, + ] or type(o) is dict), + *BUILTINS_TYPES, + *BUILTIN_EXCEPTIONS, + *BUILTIN_FUNCTIONS, + *defs.TOP_FUNCTIONS, + defs.SpamOkay.okay, + *defs.FUNCTION_LIKE, + *defs.TOP_CLASSES, + *USER_TOP_INSTANCES, + *USER_EXCEPTIONS, + # from OTHER_TYPES + types.NoneType, + types.EllipsisType, + types.NotImplementedType, + types.GenericAlias, + types.UnionType, + types.SimpleNamespace, + # from BUILTIN_WRAPPERS + METHOD, + BUILTIN_METHOD, + METHOD_DESCRIPTOR_WRAPPER, + METHOD_WRAPPER, + WRAPPER_DESCRIPTOR, +] +assert not any(isinstance(o, types.MappingProxyType) for o in PICKLEABLE) + + +# helpers + +DEFS = defs +with open(code_defs.__file__) as infile: + _code_defs_text = infile.read() +with open(DEFS.__file__) as infile: + _defs_text = infile.read() + _defs_text = _defs_text.replace('from ', '# from ') +DEFS_TEXT = f""" +####################################### +# from {code_defs.__file__} + +{_code_defs_text} + +####################################### +# from {defs.__file__} + +{_defs_text} +""" +del infile, _code_defs_text, _defs_text + + +def load_defs(module=None): + """Return a new copy of the test._crossinterp_definitions module. + + The module's __name__ matches the "module" arg, which is either + a str or a module. + + If the "module" arg is a module then the just-loaded defs are also + copied into that module. + + Note that the new module is not added to sys.modules. + """ + if module is None: + modname = DEFS.__name__ + elif isinstance(module, str): + modname = module + module = None + else: + modname = module.__name__ + # Create the new module and populate it. + defs = import_helper.create_module(modname) + defs.__file__ = DEFS.__file__ + exec(DEFS_TEXT, defs.__dict__) + # Copy the defs into the module arg, if any. + if module is not None: + for name, value in defs.__dict__.items(): + if name.startswith('_'): + continue + assert not hasattr(module, name), (name, getattr(module, name)) + setattr(module, name, value) + return defs + + +@contextlib.contextmanager +def using___main__(): + """Make sure __main__ module exists (and clean up after).""" + modname = '__main__' + if modname not in sys.modules: + with import_helper.isolated_modules(): + yield import_helper.add_module(modname) + else: + with import_helper.module_restored(modname) as mod: + yield mod + + +@contextlib.contextmanager +def temp_module(modname): + """Create the module and add to sys.modules, then remove it after.""" + assert modname not in sys.modules, (modname,) + with import_helper.isolated_modules(): + yield import_helper.add_module(modname) + + +@contextlib.contextmanager +def missing_defs_module(modname, *, prep=False): + assert modname not in sys.modules, (modname,) + if prep: + with import_helper.ready_to_import(modname, DEFS_TEXT): + yield modname + else: + with import_helper.isolated_modules(): + yield modname + + +class _GetXIDataTests(unittest.TestCase): + + MODE = None + + def assert_functions_equal(self, func1, func2): + assert type(func1) is types.FunctionType, repr(func1) + assert type(func2) is types.FunctionType, repr(func2) + self.assertEqual(func1.__name__, func2.__name__) + self.assertEqual(func1.__code__, func2.__code__) + self.assertEqual(func1.__defaults__, func2.__defaults__) + self.assertEqual(func1.__kwdefaults__, func2.__kwdefaults__) + # We don't worry about __globals__ for now. + + def assert_exc_args_equal(self, exc1, exc2): + args1 = exc1.args + args2 = exc2.args + if isinstance(exc1, ExceptionGroup): + self.assertIs(type(args1), type(args2)) + self.assertEqual(len(args1), 2) + self.assertEqual(len(args1), len(args2)) + self.assertEqual(args1[0], args2[0]) + group1 = args1[1] + group2 = args2[1] + self.assertEqual(len(group1), len(group2)) + for grouped1, grouped2 in zip(group1, group2): + # Currently the "extra" attrs are not preserved + # (via __reduce__). + self.assertIs(type(exc1), type(exc2)) + self.assert_exc_equal(grouped1, grouped2) + else: + self.assertEqual(args1, args2) + + def assert_exc_equal(self, exc1, exc2): + self.assertIs(type(exc1), type(exc2)) + + if type(exc1).__eq__ is not object.__eq__: + self.assertEqual(exc1, exc2) + + self.assert_exc_args_equal(exc1, exc2) + # XXX For now we do not preserve tracebacks. + if exc1.__traceback__ is not None: + self.assertEqual(exc1.__traceback__, exc2.__traceback__) + self.assertEqual( + getattr(exc1, '__notes__', None), + getattr(exc2, '__notes__', None), + ) + # We assume there are no cycles. + if exc1.__cause__ is None: + self.assertIs(exc1.__cause__, exc2.__cause__) + else: + self.assert_exc_equal(exc1.__cause__, exc2.__cause__) + if exc1.__context__ is None: + self.assertIs(exc1.__context__, exc2.__context__) + else: + self.assert_exc_equal(exc1.__context__, exc2.__context__) + + def assert_equal_or_equalish(self, obj, expected): + cls = type(expected) + if cls.__eq__ is not object.__eq__: + self.assertEqual(obj, expected) + elif cls is types.FunctionType: + self.assert_functions_equal(obj, expected) + elif isinstance(expected, BaseException): + self.assert_exc_equal(obj, expected) + elif cls is types.MethodType: + raise NotImplementedError(cls) + elif cls is types.BuiltinMethodType: + raise NotImplementedError(cls) + elif cls is types.MethodWrapperType: + raise NotImplementedError(cls) + elif cls.__bases__ == (object,): + self.assertEqual(obj.__dict__, expected.__dict__) + else: + raise NotImplementedError(cls) + + def get_xidata(self, obj, *, mode=None): + mode = self._resolve_mode(mode) + return _testinternalcapi.get_crossinterp_data(obj, mode) + + def get_roundtrip(self, obj, *, mode=None): + mode = self._resolve_mode(mode) + return self._get_roundtrip(obj, mode) + + def _get_roundtrip(self, obj, mode): + xid = _testinternalcapi.get_crossinterp_data(obj, mode) + return _testinternalcapi.restore_crossinterp_data(xid) + + def assert_roundtrip_identical(self, values, *, mode=None): + mode = self._resolve_mode(mode) + for obj in values: + with self.subTest(repr(obj)): + got = self._get_roundtrip(obj, mode) + self.assertIs(got, obj) + + def assert_roundtrip_equal(self, values, *, mode=None, expecttype=None): + mode = self._resolve_mode(mode) + for obj in values: + with self.subTest(repr(obj)): + got = self._get_roundtrip(obj, mode) + if got is obj: + continue + self.assertIs(type(got), + type(obj) if expecttype is None else expecttype) + self.assert_equal_or_equalish(got, obj) + + def assert_roundtrip_equal_not_identical(self, values, *, + mode=None, expecttype=None): + mode = self._resolve_mode(mode) + for obj in values: + with self.subTest(repr(obj)): + got = self._get_roundtrip(obj, mode) + self.assertIsNot(got, obj) + self.assertIs(type(got), + type(obj) if expecttype is None else expecttype) + self.assert_equal_or_equalish(got, obj) + + def assert_roundtrip_not_equal(self, values, *, + mode=None, expecttype=None): + mode = self._resolve_mode(mode) + for obj in values: + with self.subTest(repr(obj)): + got = self._get_roundtrip(obj, mode) + self.assertIsNot(got, obj) + self.assertIs(type(got), + type(obj) if expecttype is None else expecttype) + self.assertNotEqual(got, obj) + + def assert_not_shareable(self, values, exctype=None, *, mode=None): + mode = self._resolve_mode(mode) + for obj in values: + with self.subTest(repr(obj)): + with self.assertRaises(NotShareableError) as cm: + _testinternalcapi.get_crossinterp_data(obj, mode) + if exctype is not None: + self.assertIsInstance(cm.exception.__cause__, exctype) + + def _resolve_mode(self, mode): + if mode is None: + mode = self.MODE + assert mode + return mode + + +class PickleTests(_GetXIDataTests): + + MODE = 'pickle' + + def test_shareable(self): + with ignore_byteswarning(): + for obj in SHAREABLE: + if obj in PICKLEABLE: + self.assert_roundtrip_equal([obj]) + else: + self.assert_not_shareable([obj]) + + def test_not_shareable(self): + with ignore_byteswarning(): + for obj in NOT_SHAREABLE: + if type(obj) is types.MappingProxyType: + self.assert_not_shareable([obj]) + elif obj in PICKLEABLE: + with self.subTest(repr(obj)): + # We don't worry about checking the actual value. + # The other tests should cover that well enough. + got = self.get_roundtrip(obj) + self.assertIs(type(got), type(obj)) + else: + self.assert_not_shareable([obj]) + + def test_list(self): + self.assert_roundtrip_equal_not_identical([ + [], + [1, 2, 3], + [[1], (2,), {3: 4}], + ]) + + def test_dict(self): + self.assert_roundtrip_equal_not_identical([ + {}, + {1: 7, 2: 8, 3: 9}, + {1: [1], 2: (2,), 3: {3: 4}}, + ]) + + def test_set(self): + self.assert_roundtrip_equal_not_identical([ + set(), + {1, 2, 3}, + {frozenset({1}), (2,)}, + ]) + + # classes + + def assert_class_defs_same(self, defs): + # Unpickle relative to the unchanged original module. + self.assert_roundtrip_identical(defs.TOP_CLASSES) + + instances = [] + for cls, args in defs.TOP_CLASSES.items(): + if cls in defs.CLASSES_WITHOUT_EQUALITY: + continue + instances.append(cls(*args)) + self.assert_roundtrip_equal_not_identical(instances) + + # these don't compare equal + instances = [] + for cls, args in defs.TOP_CLASSES.items(): + if cls not in defs.CLASSES_WITHOUT_EQUALITY: + continue + instances.append(cls(*args)) + self.assert_roundtrip_equal(instances) + + def assert_class_defs_other_pickle(self, defs, mod): + # Pickle relative to a different module than the original. + for cls in defs.TOP_CLASSES: + assert not hasattr(mod, cls.__name__), (cls, getattr(mod, cls.__name__)) + self.assert_not_shareable(defs.TOP_CLASSES) + + instances = [] + for cls, args in defs.TOP_CLASSES.items(): + instances.append(cls(*args)) + self.assert_not_shareable(instances) + + def assert_class_defs_other_unpickle(self, defs, mod, *, fail=False): + # Unpickle relative to a different module than the original. + for cls in defs.TOP_CLASSES: + assert not hasattr(mod, cls.__name__), (cls, getattr(mod, cls.__name__)) + + instances = [] + for cls, args in defs.TOP_CLASSES.items(): + with self.subTest(repr(cls)): + setattr(mod, cls.__name__, cls) + xid = self.get_xidata(cls) + inst = cls(*args) + instxid = self.get_xidata(inst) + instances.append( + (cls, xid, inst, instxid)) + + for cls, xid, inst, instxid in instances: + with self.subTest(repr(cls)): + delattr(mod, cls.__name__) + if fail: + with self.assertRaises(NotShareableError): + _testinternalcapi.restore_crossinterp_data(xid) + continue + got = _testinternalcapi.restore_crossinterp_data(xid) + self.assertIsNot(got, cls) + self.assertNotEqual(got, cls) + + gotcls = got + got = _testinternalcapi.restore_crossinterp_data(instxid) + self.assertIsNot(got, inst) + self.assertIs(type(got), gotcls) + if cls in defs.CLASSES_WITHOUT_EQUALITY: + self.assertNotEqual(got, inst) + elif cls in defs.BUILTIN_SUBCLASSES: + self.assertEqual(got, inst) + else: + self.assertNotEqual(got, inst) + + def assert_class_defs_not_shareable(self, defs): + self.assert_not_shareable(defs.TOP_CLASSES) + + instances = [] + for cls, args in defs.TOP_CLASSES.items(): + instances.append(cls(*args)) + self.assert_not_shareable(instances) + + def test_user_class_normal(self): + self.assert_class_defs_same(defs) + + def test_user_class_in___main__(self): + with using___main__() as mod: + defs = load_defs(mod) + self.assert_class_defs_same(defs) + + def test_user_class_not_in___main___with_filename(self): + with using___main__() as mod: + defs = load_defs('__main__') + assert defs.__file__ + mod.__file__ = defs.__file__ + self.assert_class_defs_not_shareable(defs) + + def test_user_class_not_in___main___without_filename(self): + with using___main__() as mod: + defs = load_defs('__main__') + defs.__file__ = None + mod.__file__ = None + self.assert_class_defs_not_shareable(defs) + + def test_user_class_not_in___main___unpickle_with_filename(self): + with using___main__() as mod: + defs = load_defs('__main__') + assert defs.__file__ + mod.__file__ = defs.__file__ + self.assert_class_defs_other_unpickle(defs, mod) + + def test_user_class_not_in___main___unpickle_without_filename(self): + with using___main__() as mod: + defs = load_defs('__main__') + defs.__file__ = None + mod.__file__ = None + self.assert_class_defs_other_unpickle(defs, mod, fail=True) + + def test_user_class_in_module(self): + with temp_module('__spam__') as mod: + defs = load_defs(mod) + self.assert_class_defs_same(defs) + + def test_user_class_not_in_module_with_filename(self): + with temp_module('__spam__') as mod: + defs = load_defs(mod.__name__) + assert defs.__file__ + # For now, we only address this case for __main__. + self.assert_class_defs_not_shareable(defs) + + def test_user_class_not_in_module_without_filename(self): + with temp_module('__spam__') as mod: + defs = load_defs(mod.__name__) + defs.__file__ = None + self.assert_class_defs_not_shareable(defs) + + def test_user_class_module_missing_then_imported(self): + with missing_defs_module('__spam__', prep=True) as modname: + defs = load_defs(modname) + # For now, we only address this case for __main__. + self.assert_class_defs_not_shareable(defs) + + def test_user_class_module_missing_not_available(self): + with missing_defs_module('__spam__') as modname: + defs = load_defs(modname) + self.assert_class_defs_not_shareable(defs) + + def test_nested_class(self): + eggs = defs.EggsNested() + with self.assertRaises(NotShareableError): + self.get_roundtrip(eggs) + + # functions + + def assert_func_defs_same(self, defs): + # Unpickle relative to the unchanged original module. + self.assert_roundtrip_identical(defs.TOP_FUNCTIONS) + + def assert_func_defs_other_pickle(self, defs, mod): + # Pickle relative to a different module than the original. + for func in defs.TOP_FUNCTIONS: + assert not hasattr(mod, func.__name__), (getattr(mod, func.__name__),) + self.assert_not_shareable(defs.TOP_FUNCTIONS) + + def assert_func_defs_other_unpickle(self, defs, mod, *, fail=False): + # Unpickle relative to a different module than the original. + for func in defs.TOP_FUNCTIONS: + assert not hasattr(mod, func.__name__), (getattr(mod, func.__name__),) + + captured = [] + for func in defs.TOP_FUNCTIONS: + with self.subTest(func): + setattr(mod, func.__name__, func) + xid = self.get_xidata(func) + captured.append( + (func, xid)) + + for func, xid in captured: + with self.subTest(func): + delattr(mod, func.__name__) + if fail: + with self.assertRaises(NotShareableError): + _testinternalcapi.restore_crossinterp_data(xid) + continue + got = _testinternalcapi.restore_crossinterp_data(xid) + self.assertIsNot(got, func) + self.assertNotEqual(got, func) + + def assert_func_defs_not_shareable(self, defs): + self.assert_not_shareable(defs.TOP_FUNCTIONS) + + def test_user_function_normal(self): + self.assert_roundtrip_equal(defs.TOP_FUNCTIONS) + self.assert_func_defs_same(defs) + + def test_user_func_in___main__(self): + with using___main__() as mod: + defs = load_defs(mod) + self.assert_func_defs_same(defs) + + def test_user_func_not_in___main___with_filename(self): + with using___main__() as mod: + defs = load_defs('__main__') + assert defs.__file__ + mod.__file__ = defs.__file__ + self.assert_func_defs_not_shareable(defs) + + def test_user_func_not_in___main___without_filename(self): + with using___main__() as mod: + defs = load_defs('__main__') + defs.__file__ = None + mod.__file__ = None + self.assert_func_defs_not_shareable(defs) + + def test_user_func_not_in___main___unpickle_with_filename(self): + with using___main__() as mod: + defs = load_defs('__main__') + assert defs.__file__ + mod.__file__ = defs.__file__ + self.assert_func_defs_other_unpickle(defs, mod) + + def test_user_func_not_in___main___unpickle_without_filename(self): + with using___main__() as mod: + defs = load_defs('__main__') + defs.__file__ = None + mod.__file__ = None + self.assert_func_defs_other_unpickle(defs, mod, fail=True) + + def test_user_func_in_module(self): + with temp_module('__spam__') as mod: + defs = load_defs(mod) + self.assert_func_defs_same(defs) + + def test_user_func_not_in_module_with_filename(self): + with temp_module('__spam__') as mod: + defs = load_defs(mod.__name__) + assert defs.__file__ + # For now, we only address this case for __main__. + self.assert_func_defs_not_shareable(defs) + + def test_user_func_not_in_module_without_filename(self): + with temp_module('__spam__') as mod: + defs = load_defs(mod.__name__) + defs.__file__ = None + self.assert_func_defs_not_shareable(defs) + + def test_user_func_module_missing_then_imported(self): + with missing_defs_module('__spam__', prep=True) as modname: + defs = load_defs(modname) + # For now, we only address this case for __main__. + self.assert_func_defs_not_shareable(defs) + + def test_user_func_module_missing_not_available(self): + with missing_defs_module('__spam__') as modname: + defs = load_defs(modname) + self.assert_func_defs_not_shareable(defs) + + def test_nested_function(self): + self.assert_not_shareable(defs.NESTED_FUNCTIONS) + + # exceptions + + def test_user_exception_normal(self): + self.assert_roundtrip_equal([ + defs.MimimalError('error!'), + ]) + self.assert_roundtrip_equal_not_identical([ + defs.RichError('error!', 42), + ]) + + def test_builtin_exception(self): + msg = 'error!' + try: + raise Exception + except Exception as exc: + caught = exc + special = { + BaseExceptionGroup: (msg, [caught]), + ExceptionGroup: (msg, [caught]), + UnicodeError: (None, msg, None, None, None), + UnicodeEncodeError: ('utf-8', '', 1, 3, msg), + UnicodeDecodeError: ('utf-8', b'', 1, 3, msg), + UnicodeTranslateError: ('', 1, 3, msg), + } + exceptions = [] + for cls in EXCEPTION_TYPES: + args = special.get(cls) or (msg,) + exceptions.append(cls(*args)) + + self.assert_roundtrip_equal(exceptions) + + +class MarshalTests(_GetXIDataTests): + + MODE = 'marshal' + + def test_simple_builtin_singletons(self): + self.assert_roundtrip_identical([ + True, + False, + None, + Ellipsis, + ]) + self.assert_not_shareable([ + NotImplemented, + ]) + + def test_simple_builtin_objects(self): + self.assert_roundtrip_equal([ + # int + *range(-1, 258), + sys.maxsize + 1, + sys.maxsize, + -sys.maxsize - 1, + -sys.maxsize - 2, + 2**1000, + # complex + 1+2j, + # float + 0.0, + 1.1, + -1.0, + 0.12345678, + -0.12345678, + # bytes + *(i.to_bytes(2, 'little', signed=True) + for i in range(-1, 258)), + b'hello world', + # str + 'hello world', + '你好世界', + '', + ]) + self.assert_not_shareable([ + OBJECT, + types.SimpleNamespace(), + ]) + + def test_bytearray(self): + # bytearray is special because it unmarshals to bytes, not bytearray. + self.assert_roundtrip_equal([ + bytearray(), + bytearray(b'hello world'), + ], expecttype=bytes) + + def test_compound_immutable_builtin_objects(self): + self.assert_roundtrip_equal([ + # tuple + (), + (1,), + ("hello", "world"), + (1, True, "hello"), + # frozenset + frozenset([1, 2, 3]), + ]) + # nested + self.assert_roundtrip_equal([ + # tuple + ((1,),), + ((1, 2), (3, 4)), + ((1, 2), (3, 4), (5, 6)), + # frozenset + frozenset([frozenset([1]), frozenset([2]), frozenset([3])]), + ]) + + def test_compound_mutable_builtin_objects(self): + self.assert_roundtrip_equal([ + # list + [], + [1, 2, 3], + # dict + {}, + {1: 7, 2: 8, 3: 9}, + # set + set(), + {1, 2, 3}, + ]) + # nested + self.assert_roundtrip_equal([ + [[1], [2], [3]], + {1: {'a': True}, 2: {'b': False}}, + {(1, 2, 3,)}, + ]) + + def test_compound_builtin_objects_with_bad_items(self): + bogus = object() + self.assert_not_shareable([ + (bogus,), + frozenset([bogus]), + [bogus], + {bogus: True}, + {True: bogus}, + {bogus}, + ]) + + def test_builtin_code(self): + self.assert_roundtrip_equal([ + *(f.__code__ for f in defs.FUNCTIONS), + *(f.__code__ for f in defs.FUNCTION_LIKE), + ]) + + def test_builtin_type(self): + shareable = [ + StopIteration, + ] + types = BUILTIN_TYPES + self.assert_not_shareable(cls for cls in types + if cls not in shareable) + self.assert_roundtrip_identical(cls for cls in types + if cls in shareable) + + def test_builtin_function(self): + functions = [ + len, + sys.is_finalizing, + sys.exit, + _testinternalcapi.get_crossinterp_data, + ] + for func in functions: + assert type(func) is types.BuiltinFunctionType, func + + self.assert_not_shareable(functions) + + def test_builtin_exception(self): + msg = 'error!' + try: + raise Exception + except Exception as exc: + caught = exc + special = { + BaseExceptionGroup: (msg, [caught]), + ExceptionGroup: (msg, [caught]), +# UnicodeError: (None, msg, None, None, None), + UnicodeEncodeError: ('utf-8', '', 1, 3, msg), + UnicodeDecodeError: ('utf-8', b'', 1, 3, msg), + UnicodeTranslateError: ('', 1, 3, msg), + } + exceptions = [] + for cls in EXCEPTION_TYPES: + args = special.get(cls) or (msg,) + exceptions.append(cls(*args)) + + self.assert_not_shareable(exceptions) + # Note that StopIteration (the type) can be marshalled, + # but its instances cannot. + + def test_module(self): + assert type(sys) is types.ModuleType, type(sys) + assert type(defs) is types.ModuleType, type(defs) + assert type(unittest) is types.ModuleType, type(defs) + + assert 'emptymod' not in sys.modules + with import_helper.ready_to_import('emptymod', ''): + import emptymod + + self.assert_not_shareable([ + sys, + defs, + unittest, + emptymod, + ]) + + def test_user_class(self): + self.assert_not_shareable(defs.TOP_CLASSES) + + instances = [] + for cls, args in defs.TOP_CLASSES.items(): + instances.append(cls(*args)) + self.assert_not_shareable(instances) + + def test_user_function(self): + self.assert_not_shareable(defs.TOP_FUNCTIONS) + + def test_user_exception(self): + self.assert_not_shareable([ + defs.MimimalError('error!'), + defs.RichError('error!', 42), + ]) + + +class CodeTests(_GetXIDataTests): + + MODE = 'code' + + def test_function_code(self): + self.assert_roundtrip_equal_not_identical([ + *(f.__code__ for f in defs.FUNCTIONS), + *(f.__code__ for f in defs.FUNCTION_LIKE), + ]) + + def test_functions(self): + self.assert_not_shareable([ + *defs.FUNCTIONS, + *defs.FUNCTION_LIKE, + ]) + + def test_other_objects(self): + self.assert_not_shareable([ + None, + True, + False, + Ellipsis, + NotImplemented, + 9999, + 'spam', + b'spam', + (), + [], + {}, + object(), + ]) + + +class ShareableFuncTests(_GetXIDataTests): + + MODE = 'func' + + def test_stateless(self): + self.assert_roundtrip_equal([ + *defs.STATELESS_FUNCTIONS, + # Generators can be stateless too. + *defs.FUNCTION_LIKE, + ]) + + def test_not_stateless(self): + self.assert_not_shareable([ + *(f for f in defs.FUNCTIONS + if f not in defs.STATELESS_FUNCTIONS), + ]) + + def test_other_objects(self): + self.assert_not_shareable([ + None, + True, + False, + Ellipsis, + NotImplemented, + 9999, + 'spam', + b'spam', + (), + [], + {}, + object(), + ]) + + +class PureShareableScriptTests(_GetXIDataTests): + + MODE = 'script-pure' + + VALID_SCRIPTS = [ + '', + 'spam', + '# a comment', + 'print("spam")', + 'raise Exception("spam")', + """if True: + do_something() + """, + """if True: + def spam(x): + return x + class Spam: + def eggs(self): + return 42 + x = Spam().eggs() + raise ValueError(spam(x)) + """, + ] + INVALID_SCRIPTS = [ + ' pass', # IndentationError + '----', # SyntaxError + """if True: + def spam(): + # no body + spam() + """, # IndentationError + ] + + def test_valid_str(self): + self.assert_roundtrip_not_equal([ + *self.VALID_SCRIPTS, + ], expecttype=types.CodeType) + + def test_invalid_str(self): + self.assert_not_shareable([ + *self.INVALID_SCRIPTS, + ]) + + def test_valid_bytes(self): + self.assert_roundtrip_not_equal([ + *(s.encode('utf8') for s in self.VALID_SCRIPTS), + ], expecttype=types.CodeType) + + def test_invalid_bytes(self): + self.assert_not_shareable([ + *(s.encode('utf8') for s in self.INVALID_SCRIPTS), + ]) + + def test_pure_script_code(self): + self.assert_roundtrip_equal_not_identical([ + *(f.__code__ for f in defs.PURE_SCRIPT_FUNCTIONS), + ]) + + def test_impure_script_code(self): + self.assert_not_shareable([ + *(f.__code__ for f in defs.SCRIPT_FUNCTIONS + if f not in defs.PURE_SCRIPT_FUNCTIONS), + ]) + + def test_other_code(self): + self.assert_not_shareable([ + *(f.__code__ for f in defs.FUNCTIONS + if f not in defs.SCRIPT_FUNCTIONS), + *(f.__code__ for f in defs.FUNCTION_LIKE), + ]) + + def test_pure_script_function(self): + self.assert_roundtrip_not_equal([ + *defs.PURE_SCRIPT_FUNCTIONS, + ], expecttype=types.CodeType) + + def test_impure_script_function(self): + self.assert_not_shareable([ + *(f for f in defs.SCRIPT_FUNCTIONS + if f not in defs.PURE_SCRIPT_FUNCTIONS), + ]) + + def test_other_function(self): + self.assert_not_shareable([ + *(f for f in defs.FUNCTIONS + if f not in defs.SCRIPT_FUNCTIONS), + *defs.FUNCTION_LIKE, + ]) + + def test_other_objects(self): + self.assert_not_shareable([ + None, + True, + False, + Ellipsis, + NotImplemented, + (), + [], + {}, + object(), + ]) + + +class ShareableScriptTests(PureShareableScriptTests): + + MODE = 'script' + + def test_impure_script_code(self): + self.assert_roundtrip_equal_not_identical([ + *(f.__code__ for f in defs.SCRIPT_FUNCTIONS + if f not in defs.PURE_SCRIPT_FUNCTIONS), + ]) + + def test_impure_script_function(self): + self.assert_roundtrip_not_equal([ + *(f for f in defs.SCRIPT_FUNCTIONS + if f not in defs.PURE_SCRIPT_FUNCTIONS), + ], expecttype=types.CodeType) + + +class ShareableFallbackTests(_GetXIDataTests): + + MODE = 'fallback' + + def test_shareable(self): + self.assert_roundtrip_equal(SHAREABLE) + + def test_not_shareable(self): + okay = [ + *PICKLEABLE, + *defs.STATELESS_FUNCTIONS, + LAMBDA, + ] + ignored = [ + *TUPLES_WITHOUT_EQUALITY, + OBJECT, + METHOD, + BUILTIN_METHOD, + METHOD_WRAPPER, + ] + with ignore_byteswarning(): + self.assert_roundtrip_equal([ + *(o for o in NOT_SHAREABLE + if o in okay and o not in ignored + and o is not MAPPING_PROXY_EMPTY), + ]) + self.assert_roundtrip_not_equal([ + *(o for o in NOT_SHAREABLE + if o in ignored and o is not MAPPING_PROXY_EMPTY), + ]) + self.assert_not_shareable([ + *(o for o in NOT_SHAREABLE if o not in okay), + MAPPING_PROXY_EMPTY, + ]) + + +class ShareableTypeTests(_GetXIDataTests): + + MODE = 'xidata' + + def test_shareable(self): + self.assert_roundtrip_equal(SHAREABLE) + + def test_singletons(self): + self.assert_roundtrip_identical([ + None, + True, + False, + ]) + self.assert_not_shareable([ + Ellipsis, + NotImplemented, + ]) + + def test_types(self): + self.assert_roundtrip_equal([ + b'spam', + 9999, + ]) + + def test_bytes(self): + values = (i.to_bytes(2, 'little', signed=True) + for i in range(-1, 258)) + self.assert_roundtrip_equal(values) + + def test_strs(self): + self.assert_roundtrip_equal([ + 'hello world', + '你好世界', + '', + ]) + + def test_int(self): + bounds = [sys.maxsize, -sys.maxsize - 1] + values = itertools.chain(range(-1, 258), bounds) + self.assert_roundtrip_equal(values) + + def test_non_shareable_int(self): + ints = [ + sys.maxsize + 1, + -sys.maxsize - 2, + 2**1000, + ] + self.assert_not_shareable(ints, OverflowError) + + def test_float(self): + self.assert_roundtrip_equal([ + 0.0, + 1.1, + -1.0, + 0.12345678, + -0.12345678, + ]) + + def test_tuple(self): + self.assert_roundtrip_equal([ + (), + (1,), + ("hello", "world", ), + (1, True, "hello"), + ]) + # Test nesting + self.assert_roundtrip_equal([ + ((1,),), + ((1, 2), (3, 4)), + ((1, 2), (3, 4), (5, 6)), + ]) + + def test_tuples_containing_non_shareable_types(self): + non_shareables = [ + EXCEPTION, + OBJECT, + ] + for s in non_shareables: + value = tuple([0, 1.0, s]) + with self.subTest(repr(value)): + with self.assertRaises(NotShareableError): + self.get_xidata(value) + # Check nested as well + value = tuple([0, 1., (s,)]) + with self.subTest("nested " + repr(value)): + with self.assertRaises(NotShareableError): + self.get_xidata(value) + + # The rest are not shareable. + + def test_not_shareable(self): + self.assert_not_shareable(NOT_SHAREABLE) + + def test_object(self): + self.assert_not_shareable([ + object(), + ]) + + def test_code(self): + # types.CodeType + self.assert_not_shareable([ + *(f.__code__ for f in defs.FUNCTIONS), + *(f.__code__ for f in defs.FUNCTION_LIKE), + ]) + + def test_function_object(self): + for func in defs.FUNCTIONS: + assert type(func) is types.FunctionType, func + assert type(defs.SpamOkay.okay) is types.FunctionType, func + assert type(LAMBDA) is types.LambdaType + + self.assert_not_shareable([ + *defs.FUNCTIONS, + defs.SpamOkay.okay, + LAMBDA, + ]) + + def test_builtin_function(self): + functions = [ + len, + sys.is_finalizing, + sys.exit, + _testinternalcapi.get_crossinterp_data, + ] + for func in functions: + assert type(func) is types.BuiltinFunctionType, func + + self.assert_not_shareable(functions) + + def test_function_like(self): + self.assert_not_shareable(defs.FUNCTION_LIKE) + self.assert_not_shareable(defs.FUNCTION_LIKE_APPLIED) + + def test_builtin_wrapper(self): + _wrappers = { + defs.SpamOkay().okay: types.MethodType, + [].append: types.BuiltinMethodType, + dict.__dict__['fromkeys']: types.ClassMethodDescriptorType, + types.FunctionType.__code__: types.GetSetDescriptorType, + types.FunctionType.__globals__: types.MemberDescriptorType, + str.join: types.MethodDescriptorType, + object().__str__: types.MethodWrapperType, + object.__init__: types.WrapperDescriptorType, + } + for obj, expected in _wrappers.items(): + assert type(obj) is expected, (obj, expected) + + self.assert_not_shareable([ + *_wrappers, + staticmethod(defs.SpamOkay.okay), + classmethod(defs.SpamOkay.okay), + property(defs.SpamOkay.okay), + ]) + + def test_module(self): + assert type(sys) is types.ModuleType, type(sys) + assert type(defs) is types.ModuleType, type(defs) + assert type(unittest) is types.ModuleType, type(defs) + + assert 'emptymod' not in sys.modules + with import_helper.ready_to_import('emptymod', ''): + import emptymod + + self.assert_not_shareable([ + sys, + defs, + unittest, + emptymod, + ]) + + def test_class(self): + self.assert_not_shareable(defs.CLASSES) + + instances = [] + for cls, args in defs.CLASSES.items(): + instances.append(cls(*args)) + self.assert_not_shareable(instances) + + def test_builtin_type(self): + self.assert_not_shareable(BUILTIN_TYPES) + + def test_exception(self): + self.assert_not_shareable([ + defs.MimimalError('error!'), + ]) + + def test_builtin_exception(self): + msg = 'error!' + try: + raise Exception + except Exception as exc: + caught = exc + special = { + BaseExceptionGroup: (msg, [caught]), + ExceptionGroup: (msg, [caught]), +# UnicodeError: (None, msg, None, None, None), + UnicodeEncodeError: ('utf-8', '', 1, 3, msg), + UnicodeDecodeError: ('utf-8', b'', 1, 3, msg), + UnicodeTranslateError: ('', 1, 3, msg), + } + exceptions = [] + for cls in EXCEPTION_TYPES: + args = special.get(cls) or (msg,) + exceptions.append(cls(*args)) + + self.assert_not_shareable(exceptions) + + def test_builtin_objects(self): + ns = {} + exec("""if True: + try: + raise Exception + except Exception as exc: + TRACEBACK = exc.__traceback__ + FRAME = TRACEBACK.tb_frame + """, ns, ns) + + self.assert_not_shareable([ + MAPPING_PROXY_EMPTY, + types.SimpleNamespace(), + # types.CellType + types.CellType(), + # types.FrameType + ns['FRAME'], + # types.TracebackType + ns['TRACEBACK'], + ]) + + +class CaptureExceptionTests(unittest.TestCase): + + # Prevent crashes with incompatible TracebackException.format(). + # Regression test for https://github.com/python/cpython/issues/143377. + + def capture_with_formatter(self, exc, formatter): + with support.swap_attr(traceback.TracebackException, "format", formatter): + return _interpreters.capture_exception(exc) + + def test_capture_exception(self): + captured = _interpreters.capture_exception(ValueError("hello")) + + self.assertEqual(captured.type.__name__, "ValueError") + self.assertEqual(captured.type.__qualname__, "ValueError") + self.assertEqual(captured.type.__module__, "builtins") + + self.assertEqual(captured.msg, "hello") + self.assertEqual(captured.formatted, "ValueError: hello") + + def test_capture_exception_custom_format(self): + exc = ValueError("good bye!") + formatter = lambda self: ["hello\n", "world\n"] + captured = self.capture_with_formatter(exc, formatter) + self.assertEqual(captured.msg, "good bye!") + self.assertEqual(captured.formatted, "ValueError: good bye!") + self.assertEqual(captured.errdisplay, "hello\nworld") + + @support.subTests("exc_lines", ([], ["x-no-nl"], ["x-no-nl", "y-no-nl"])) + def test_capture_exception_invalid_format(self, exc_lines): + formatter = lambda self: exc_lines + captured = self.capture_with_formatter(ValueError(), formatter) + self.assertEqual(captured.msg, "") + self.assertEqual(captured.formatted, "ValueError: ") + self.assertEqual(captured.errdisplay, "".join(exc_lines)) + + @unittest.skipUnless( + support.Py_DEBUG, + "printing subinterpreter unraisable exceptions requires DEBUG build", + ) + def test_capture_exception_unraisable_exception(self): + formatter = lambda self: 1 + with support.catch_unraisable_exception() as cm: + captured = self.capture_with_formatter(ValueError(), formatter) + self.assertNotHasAttr(captured, "errdisplay") + self.assertEqual(cm.unraisable.exc_type, TypeError) + self.assertEqual(str(cm.unraisable.exc_value), + "can only join an iterable") + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_ctypes/__init__.py b/Lib/test/test_ctypes/__init__.py index eb9126cbe18..d848beb1ff1 100644 --- a/Lib/test/test_ctypes/__init__.py +++ b/Lib/test/test_ctypes/__init__.py @@ -1,4 +1,5 @@ import os +import unittest from test import support from test.support import import_helper @@ -6,5 +7,21 @@ # skip tests if the _ctypes extension was not built import_helper.import_module('ctypes') + +class TestModule(unittest.TestCase): + def test_deprecated__version__(self): + import ctypes + import _ctypes + + for mod in (ctypes, _ctypes): + with self.subTest(mod=mod): + with self.assertWarnsRegex( + DeprecationWarning, + "'__version__' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(mod, "__version__") + self.assertEqual(cm.filename, __file__) + + def load_tests(*args): return support.load_package_tests(os.path.dirname(__file__), *args) diff --git a/Lib/test/test_ctypes/_support.py b/Lib/test/test_ctypes/_support.py index 946d654a19a..700657a4e41 100644 --- a/Lib/test/test_ctypes/_support.py +++ b/Lib/test/test_ctypes/_support.py @@ -3,7 +3,6 @@ import ctypes from _ctypes import Structure, Union, _Pointer, Array, _SimpleCData, CFuncPtr import sys -from test import support _CData = Structure.__base__ diff --git a/Lib/test/test_ctypes/test_as_parameter.py b/Lib/test/test_ctypes/test_as_parameter.py index 2da1acfcf29..abbfb6efdce 100644 --- a/Lib/test/test_ctypes/test_as_parameter.py +++ b/Lib/test/test_ctypes/test_as_parameter.py @@ -5,7 +5,8 @@ c_short, c_int, c_long, c_longlong, c_byte, c_wchar, c_float, c_double, ArgumentError) -from test.support import import_helper, skip_if_sanitizer +from test.support import (import_helper, skip_if_sanitizer, + skip_emscripten_stack_overflow, skip_if_huge_c_stack) _ctypes_test = import_helper.import_module("_ctypes_test") @@ -193,6 +194,8 @@ class S8I(Structure): (9*2, 8*3, 7*4, 6*5, 5*6, 4*7, 3*8, 2*9)) @skip_if_sanitizer('requires deep stack', thread=True) + @skip_if_huge_c_stack() + @skip_emscripten_stack_overflow() def test_recursive_as_param(self): class A: pass diff --git a/Lib/test/test_ctypes/test_c_simple_type_meta.py b/Lib/test/test_ctypes/test_c_simple_type_meta.py index fd261acf497..3b4fcf9c42a 100644 --- a/Lib/test/test_ctypes/test_c_simple_type_meta.py +++ b/Lib/test/test_ctypes/test_c_simple_type_meta.py @@ -213,14 +213,12 @@ def test_bad_type_message(self): class F(metaclass=PyCSimpleType): _type_ = "\0" message = str(cm.exception) - expected_type_chars = list('cbBhHiIlLdDFGfuzZqQPXOv?g') - if not hasattr(ctypes, 'c_float_complex'): - expected_type_chars.remove('F') - expected_type_chars.remove('D') - expected_type_chars.remove('G') + expected_type_chars = list('cbBhHiIlLdfuzZqQPXOv?g') if not MS_WINDOWS: expected_type_chars.remove('X') self.assertIn("'" + ''.join(expected_type_chars) + "'", message) + if hasattr(ctypes, 'c_float_complex'): + self.assertIn("'Zf', 'Zd', 'Zg'", message) def test_creating_pointer_in_dunder_init_3(self): """Check if interfcase subclasses properly creates according internal diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index 6c7c2e52707..d1ac62816eb 100644 --- a/Lib/test/test_ctypes/test_callbacks.py +++ b/Lib/test/test_ctypes/test_callbacks.py @@ -328,6 +328,21 @@ def func(): f"of ctypes callback function {func!r}") self.assertIsNone(cm.unraisable.object) + def test_narrow_int_return_widened(self): + # gh-156933: Narrow integers were not widened on s390x + CALLBACK = CFUNCTYPE(c_int) + + dll = CDLL(_ctypes_test.__file__) + _testfunc_callback_int_to_longlong = dll._testfunc_callback_int_to_longlong + _testfunc_callback_int_to_longlong.argtypes = [CALLBACK] + _testfunc_callback_int_to_longlong.restype = c_longlong + + @CALLBACK + def cb(): + return -1 + + self.assertEqual(_testfunc_callback_int_to_longlong(cb), -1) + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_ctypes/test_dlerror.py b/Lib/test/test_ctypes/test_dlerror.py index 5658234f9ec..bb0aec0b3c5 100644 --- a/Lib/test/test_ctypes/test_dlerror.py +++ b/Lib/test/test_ctypes/test_dlerror.py @@ -30,8 +30,9 @@ """ -@unittest.skipUnless(sys.platform.startswith('linux'), - 'test requires GNU IFUNC support') +@unittest.skipIf(not sys.platform.startswith('linux') + or platform.machine().startswith('parisc'), + 'test requires GNU IFUNC support') @unittest.skipIf(test.support.linked_to_musl(), "Requires glibc") class TestNullDlsym(unittest.TestCase): """GH-126554: Ensure that we catch NULL dlsym return values diff --git a/Lib/test/test_ctypes/test_find.py b/Lib/test/test_ctypes/test_find.py index 8bc84c3d2ef..92175cc38ee 100644 --- a/Lib/test/test_ctypes/test_find.py +++ b/Lib/test/test_ctypes/test_find.py @@ -1,4 +1,5 @@ import os.path +import subprocess import sys import test.support import unittest @@ -78,9 +79,24 @@ def test_shell_injection(self): @unittest.skipUnless(sys.platform.startswith('linux'), 'Test only valid for Linux') class FindLibraryLinux(unittest.TestCase): + @classmethod + def setUpClass(cls): + try: + p = subprocess.run(['ld', '--version'], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True) + except OSError: + pass + else: + if p.stdout.startswith('mold '): + # The mold linker is known to be incompatible with + # the soft-deprecated ctypes.util.find_library + # (which uses `ld -t`). + raise unittest.SkipTest('Fails when ld is mold') + @thread_unsafe('uses setenv') def test_find_on_libpath(self): - import subprocess import tempfile try: diff --git a/Lib/test/test_ctypes/test_generated_structs.py b/Lib/test/test_ctypes/test_generated_structs.py index aa448fad5bb..1cb46a82701 100644 --- a/Lib/test/test_ctypes/test_generated_structs.py +++ b/Lib/test/test_ctypes/test_generated_structs.py @@ -10,7 +10,7 @@ """ import unittest -from test.support import import_helper, verbose +from test.support import import_helper import re from dataclasses import dataclass from functools import cached_property diff --git a/Lib/test/test_ctypes/test_incomplete.py b/Lib/test/test_ctypes/test_incomplete.py index fefdfe9102e..8f6316d6fba 100644 --- a/Lib/test/test_ctypes/test_incomplete.py +++ b/Lib/test/test_ctypes/test_incomplete.py @@ -1,7 +1,6 @@ import ctypes import unittest -import warnings -from ctypes import Structure, POINTER, pointer, c_char_p +from ctypes import Structure, POINTER, pointer, c_char_p, c_int # String-based "incomplete pointers" were implemented in ctypes 0.6.3 (2003, when # ctypes was an external project). They made obsolete by the current @@ -21,9 +20,7 @@ class cell(Structure): _fields_ = [("name", c_char_p), ("next", lpcell)] - with warnings.catch_warnings(): - warnings.simplefilter('ignore', DeprecationWarning) - ctypes.SetPointerType(lpcell, cell) + lpcell.set_type(cell) self.assertIs(POINTER(cell), lpcell) @@ -50,10 +47,32 @@ class cell(Structure): _fields_ = [("name", c_char_p), ("next", lpcell)] + lpcell.set_type(cell) + self.assertIs(POINTER(cell), lpcell) + + def test_set_type_updates_format(self): + # gh-142966: set_type should update StgInfo.format + # to match the element type's format with self.assertWarns(DeprecationWarning): - ctypes.SetPointerType(lpcell, cell) + lp = POINTER("node") + + class node(Structure): + _fields_ = [("value", c_int)] + + # Get the expected format before set_type + node_format = memoryview(node()).format + expected_format = "&" + node_format + + lp.set_type(node) + + # Create instance to check format via memoryview + n = node(42) + p = lp(n) + actual_format = memoryview(p).format + + # After set_type, the pointer's format should be "&" + self.assertEqual(actual_format, expected_format) - self.assertIs(POINTER(cell), lpcell) if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_ctypes/test_macholib.py b/Lib/test/test_ctypes/test_macholib.py index 9d906179956..9a5e18ed589 100644 --- a/Lib/test/test_ctypes/test_macholib.py +++ b/Lib/test/test_ctypes/test_macholib.py @@ -108,5 +108,17 @@ def test_framework_info(self): d('P', 'F.framework/Versions/A/F_debug', 'F', 'A', 'debug')) +class TestModule(unittest.TestCase): + def test_deprecated__version__(self): + import ctypes.macholib + + with self.assertWarnsRegex( + DeprecationWarning, + "'__version__' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(ctypes.macholib, "__version__") + self.assertEqual(cm.filename, __file__) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_ctypes/test_numbers.py b/Lib/test/test_ctypes/test_numbers.py index c57c58eb002..b7df08f6079 100644 --- a/Lib/test/test_ctypes/test_numbers.py +++ b/Lib/test/test_ctypes/test_numbers.py @@ -119,8 +119,11 @@ def test_floats(self): @unittest.skipUnless(hasattr(ctypes, "c_double_complex"), "requires C11 complex type") def test_complex(self): - for t in [ctypes.c_double_complex, ctypes.c_float_complex, - ctypes.c_longdouble_complex]: + for format, t in [ + ('Zd', ctypes.c_double_complex), + ('Zf', ctypes.c_float_complex), + ('Zg', ctypes.c_longdouble_complex), + ]: self.assertEqual(t(1).value, 1+0j) self.assertEqual(t(1.0).value, 1+0j) self.assertEqual(t(1+0.125j).value, 1+0.125j) @@ -128,6 +131,12 @@ def test_complex(self): self.assertEqual(t(FloatLike()).value, 2+0j) self.assertEqual(t(ComplexLike()).value, 1+1j) + prefix = '>' if sys.byteorder == 'big' else '<' + num = t(1.0) + self.assertEqual(memoryview(num).format, prefix + format) + array = (t * 3)() + self.assertEqual(memoryview(array).format, prefix + format) + @unittest.skipUnless(hasattr(ctypes, "c_double_complex"), "requires C11 complex type") def test_complex_round_trip(self): diff --git a/Lib/test/test_ctypes/test_parameters.py b/Lib/test/test_ctypes/test_parameters.py index 46f8ff93efa..6dadb7b410d 100644 --- a/Lib/test/test_ctypes/test_parameters.py +++ b/Lib/test/test_ctypes/test_parameters.py @@ -1,6 +1,7 @@ import sys import unittest import test.support +import ctypes from ctypes import (CDLL, PyDLL, ArgumentError, Structure, Array, Union, _Pointer, _SimpleCData, _CFuncPtr, @@ -247,6 +248,13 @@ def test_parameter_repr(self): self.assertRegex(repr(c_char_p.from_param(b'hihi')), r"^$") self.assertRegex(repr(c_wchar_p.from_param('hihi')), r"^$") self.assertRegex(repr(c_void_p.from_param(0x12)), r"^$") + if hasattr(ctypes, 'c_double_complex'): + self.assertRegex(repr(ctypes.c_double_complex.from_param(0)), + r"^$") + self.assertRegex(repr(ctypes.c_float_complex.from_param(0)), + r"^$") + self.assertRegex(repr(ctypes.c_longdouble_complex.from_param(0)), + r"^$") @test.support.cpython_only def test_from_param_result_refcount(self): diff --git a/Lib/test/test_ctypes/test_structures.py b/Lib/test/test_ctypes/test_structures.py index 92d4851d739..65ccc12625f 100644 --- a/Lib/test/test_ctypes/test_structures.py +++ b/Lib/test/test_ctypes/test_structures.py @@ -299,9 +299,16 @@ class X(Structure): self.assertEqual(s.first, got.first) self.assertEqual(s.second, got.second) + @unittest.skipIf(support.is_wasm32, "wasm ABI is incompatible with test expectations") def _test_issue18060(self, Vector): # Regression tests for gh-62260 + # This test passes a struct of two doubles by value to atan2(), whose C + # signature is atan2(double, double), so it only works on platforms + # where the abi of a function that takes a struct with two doubles + # matches the abi of a function that takes two doubles. The wasm32 ABI + # does not satisfy this condition and the test breaks. + # The call to atan2() should succeed if the # class fields were correctly cloned in the # subclasses. Otherwise, it will segfault. diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py new file mode 100644 index 00000000000..ae3d7cdc5ea --- /dev/null +++ b/Lib/test/test_curses.py @@ -0,0 +1,2046 @@ +import functools +import inspect +import os +import string +import sys +import tempfile +import unittest +from unittest.mock import MagicMock + +from test.support import (requires, verbose, SaveSignals, cpython_only, + check_disallow_instantiation, MISSING_C_DOCSTRINGS, + gc_collect) +from test.support.import_helper import import_module + +# Optionally test curses module. This currently requires that the +# 'curses' resource be given on the regrtest command line using the -u +# option. If not available, nothing after this line will be executed. +requires('curses') + +# If either of these don't exist, skip the tests. +curses = import_module('curses') +import_module('curses.ascii') +import_module('curses.textpad') +try: + import curses.panel +except ImportError: + pass + +def requires_curses_func(name): + return unittest.skipUnless(hasattr(curses, name), + 'requires curses.%s' % name) + +def requires_curses_window_meth(name): + def deco(test): + @functools.wraps(test) + def wrapped(self, *args, **kwargs): + if not hasattr(self.stdscr, name): + raise unittest.SkipTest('requires curses.window.%s' % name) + test(self, *args, **kwargs) + return wrapped + return deco + + +def requires_colors(test): + @functools.wraps(test) + def wrapped(self, *args, **kwargs): + if not curses.has_colors(): + self.skipTest('requires colors support') + curses.start_color() + test(self, *args, **kwargs) + return wrapped + +term = os.environ.get('TERM') +SHORT_MAX = 0x7fff + +# If newterm was supported we could use it instead of initscr and not exit +@unittest.skipIf(not term or term == 'unknown', + "$TERM=%r, calling initscr() may cause exit" % term) +@unittest.skipIf(sys.platform == "cygwin", + "cygwin's curses mostly just hangs") +class TestCurses(unittest.TestCase): + + @classmethod + def setUpClass(cls): + if verbose: + print(f'TERM={term}', file=sys.stderr, flush=True) + # testing setupterm() inside initscr/endwin + # causes terminal breakage + stdout_fd = sys.__stdout__.fileno() + curses.setupterm(fd=stdout_fd) + + def setUp(self): + self.isatty = True + self.output = sys.__stdout__ + stdout_fd = sys.__stdout__.fileno() + if not sys.__stdout__.isatty(): + # initstr() unconditionally uses C stdout. + # If it is redirected to file or pipe, try to attach it + # to terminal. + # First, save a copy of the file descriptor of stdout, so it + # can be restored after finishing the test. + dup_fd = os.dup(stdout_fd) + self.addCleanup(os.close, dup_fd) + self.addCleanup(os.dup2, dup_fd, stdout_fd) + + if sys.__stderr__.isatty(): + # If stderr is connected to terminal, use it. + tmp = sys.__stderr__ + self.output = sys.__stderr__ + else: + try: + # Try to open the terminal device. + tmp = open('/dev/tty', 'wb', buffering=0) + except OSError: + # As a fallback, use regular file to write control codes. + # Some functions (like savetty) will not work, but at + # least the garbage control sequences will not be mixed + # with the testing report. + tmp = tempfile.TemporaryFile(mode='wb', buffering=0) + self.isatty = False + self.addCleanup(tmp.close) + self.output = None + os.dup2(tmp.fileno(), stdout_fd) + + self.save_signals = SaveSignals() + self.save_signals.save() + self.addCleanup(self.save_signals.restore) + if verbose and self.output is not None: + # just to make the test output a little more readable + sys.stderr.flush() + sys.stdout.flush() + print(file=self.output, flush=True) + self.stdscr = curses.initscr() + if self.isatty: + curses.savetty() + self.addCleanup(curses.endwin) + self.addCleanup(curses.resetty) + self.stdscr.erase() + + @requires_curses_func('filter') + def test_filter(self): + # TODO: Should be called before initscr() or newterm() are called. + # TODO: nofilter() + curses.filter() + + @requires_curses_func('use_env') + def test_use_env(self): + # TODO: Should be called before initscr() or newterm() are called. + # TODO: use_tioctl() + curses.use_env(False) + curses.use_env(True) + + def test_error(self): + self.assertIsSubclass(curses.error, Exception) + + def test_create_windows(self): + win = curses.newwin(5, 10) + self.assertEqual(win.getbegyx(), (0, 0)) + self.assertEqual(win.getparyx(), (-1, -1)) + self.assertEqual(win.getmaxyx(), (5, 10)) + + win = curses.newwin(10, 15, 2, 5) + self.assertEqual(win.getbegyx(), (2, 5)) + self.assertEqual(win.getparyx(), (-1, -1)) + self.assertEqual(win.getmaxyx(), (10, 15)) + + win2 = win.subwin(3, 7) + self.assertEqual(win2.getbegyx(), (3, 7)) + self.assertEqual(win2.getparyx(), (1, 2)) + self.assertEqual(win2.getmaxyx(), (9, 13)) + + win2 = win.subwin(5, 10, 3, 7) + self.assertEqual(win2.getbegyx(), (3, 7)) + self.assertEqual(win2.getparyx(), (1, 2)) + self.assertEqual(win2.getmaxyx(), (5, 10)) + + win3 = win.derwin(2, 3) + self.assertEqual(win3.getbegyx(), (4, 8)) + self.assertEqual(win3.getparyx(), (2, 3)) + self.assertEqual(win3.getmaxyx(), (8, 12)) + + win3 = win.derwin(6, 11, 2, 3) + self.assertEqual(win3.getbegyx(), (4, 8)) + self.assertEqual(win3.getparyx(), (2, 3)) + self.assertEqual(win3.getmaxyx(), (6, 11)) + + win.mvwin(0, 1) + self.assertEqual(win.getbegyx(), (0, 1)) + self.assertEqual(win.getparyx(), (-1, -1)) + self.assertEqual(win.getmaxyx(), (10, 15)) + self.assertEqual(win2.getbegyx(), (3, 7)) + self.assertEqual(win2.getparyx(), (1, 2)) + self.assertEqual(win2.getmaxyx(), (5, 10)) + self.assertEqual(win3.getbegyx(), (4, 8)) + self.assertEqual(win3.getparyx(), (2, 3)) + self.assertEqual(win3.getmaxyx(), (6, 11)) + + win2.mvderwin(2, 1) + self.assertEqual(win2.getbegyx(), (3, 7)) + self.assertEqual(win2.getparyx(), (2, 1)) + self.assertEqual(win2.getmaxyx(), (5, 10)) + + win3.mvderwin(2, 1) + self.assertEqual(win3.getbegyx(), (4, 8)) + self.assertEqual(win3.getparyx(), (2, 1)) + self.assertEqual(win3.getmaxyx(), (6, 11)) + + def test_subwindows_references(self): + win = curses.newwin(5, 10) + win2 = win.subwin(3, 7) + del win + gc_collect() + del win2 + gc_collect() + + def test_move_cursor(self): + stdscr = self.stdscr + win = stdscr.subwin(10, 15, 2, 5) + stdscr.move(1, 2) + win.move(2, 4) + self.assertEqual(stdscr.getyx(), (1, 2)) + self.assertEqual(win.getyx(), (2, 4)) + + win.cursyncup() + self.assertEqual(stdscr.getyx(), (4, 9)) + + def test_refresh_control(self): + stdscr = self.stdscr + # touchwin()/untouchwin()/is_wintouched() + stdscr.refresh() + self.assertIs(stdscr.is_wintouched(), False) + stdscr.touchwin() + self.assertIs(stdscr.is_wintouched(), True) + stdscr.refresh() + self.assertIs(stdscr.is_wintouched(), False) + stdscr.touchwin() + self.assertIs(stdscr.is_wintouched(), True) + stdscr.untouchwin() + self.assertIs(stdscr.is_wintouched(), False) + + # touchline()/untouchline()/is_linetouched() + stdscr.touchline(5, 2) + self.assertIs(stdscr.is_linetouched(5), True) + self.assertIs(stdscr.is_linetouched(6), True) + self.assertIs(stdscr.is_wintouched(), True) + stdscr.touchline(5, 1, False) + self.assertIs(stdscr.is_linetouched(5), False) + + # syncup() + win = stdscr.subwin(10, 15, 2, 5) + win2 = win.subwin(5, 10, 3, 7) + win2.touchwin() + stdscr.untouchwin() + win2.syncup() + self.assertIs(win.is_wintouched(), True) + self.assertIs(stdscr.is_wintouched(), True) + + # syncdown() + stdscr.touchwin() + win.untouchwin() + win2.untouchwin() + win2.syncdown() + self.assertIs(win2.is_wintouched(), True) + + # syncok() + if hasattr(stdscr, 'syncok') and not sys.platform.startswith("sunos"): + win.untouchwin() + stdscr.untouchwin() + for syncok in [False, True]: + win2.syncok(syncok) + win2.addch('a') + self.assertIs(win.is_wintouched(), syncok) + self.assertIs(stdscr.is_wintouched(), syncok) + + # Many tests below use a common set of non-ASCII cases, each applied only + # when the window encoding can represent it -- so the whole suite is meant to + # be run under several locales (e.g. ISO-8859-1, ISO-8859-15, KOI8-U): + # 'A'/'a' ASCII + # 'é' common to the Latin encodings + # '¤'/'€'/'є' byte 0xA4 in ISO-8859-1 / ISO-8859-15 / KOI8-U + # Precomposed characters are used so a round-trip does not depend on the form. + + def _encodable(self, s): + # Wide characters are only supported in a locale that can encode them. + try: + s.encode(self.stdscr.encoding) + except UnicodeEncodeError: + return False + return True + + def _read_char(self, y, x): + # The character written to a cell, read back for output checks. inch() + # is unusable here: on a wide build it returns the low 8 bits of the + # character's code point rather than its locale-encoded byte, mangling + # anything outside Latin-1. in_wch() reads the wide cell directly; + # without it, instr() re-encodes the cell to the window encoding. + stdscr = self.stdscr + if hasattr(stdscr, 'in_wch'): + return str(stdscr.in_wch(y, x)) + return stdscr.instr(y, x, 1).decode(stdscr.encoding) + + def test_output_character(self): + stdscr = self.stdscr + encoding = stdscr.encoding + # addch() + stdscr.refresh() + stdscr.move(0, 0) + stdscr.addch('A') + stdscr.addch(b'A') + stdscr.addch(65) + # See _encodable for the character set. Each is either written (mapped + # to a single byte), or raises UnicodeEncodeError (not in the encoding) + # or OverflowError (a multibyte sequence, e.g. in UTF-8). + for c in ('A', '\u00e9', '\u00a4', '\u20ac', '\u0454'): + try: + stdscr.addch(c) + except UnicodeEncodeError: + self.assertRaises(UnicodeEncodeError, c.encode, encoding) + except OverflowError: + encoded = c.encode(encoding) + self.assertNotEqual(len(encoded), 1, repr(encoded)) + stdscr.addch('A', curses.A_BOLD) + stdscr.addch(1, 2, 'A') + stdscr.addch(2, 3, 'A', curses.A_BOLD) + self.assertIs(stdscr.is_wintouched(), True) + + # The same characters supplied as an int chtype (a byte > 127). The + # cell is read back with _read_char(), not inch(): on a wide build the + # int is stored through the locale as a wide character that inch() + # cannot represent for a character outside Latin-1. + for c in ('é', '¤', '€', 'є'): + try: + b = c.encode(encoding) + except UnicodeEncodeError: + continue + if len(b) != 1: + continue + # A wide build stores a character outside Latin-1 as a wide cell, + # not as its encoded byte, so it cannot round-trip here. + if ord(c) > 0xff and hasattr(stdscr, 'get_wch'): + continue + v = b[0] + with self.subTest(c=c): + stdscr.addch(0, 0, v) + self.assertEqual(self._read_char(0, 0), c) + stdscr.addch(0, 1, v, curses.A_BOLD) + self.assertEqual(self._read_char(0, 1), c) + self.assertTrue(stdscr.inch(0, 1) & curses.A_BOLD) + stdscr.move(2, 0) + stdscr.echochar(v) + self.assertEqual(self._read_char(2, 0), c) + # insch() decodes the byte through the locale like addch(), so + # it round-trips the same character. + stdscr.insch(1, 0, v) + self.assertEqual(self._read_char(1, 0), c) + + # The same characters supplied as a str. Unlike the int path above, a + # str is stored as a wide-character cell on a wide build, so every + # encodable character round-trips, insch() included. A multibyte + # character does not fit a cell on a narrow build and is skipped. + wide = hasattr(stdscr, 'in_wch') + for c in ('é', '¤', '€', 'є'): + if not self._encodable(c): + continue + if not wide and len(c.encode(encoding)) != 1: + continue + # A wide build stores a character outside Latin-1 as a wide cell, + # not as its encoded byte, so it cannot round-trip here. + if ord(c) > 0xff and hasattr(stdscr, 'get_wch'): + continue + with self.subTest(c=c): + stdscr.addch(0, 0, c) + self.assertEqual(self._read_char(0, 0), c) + stdscr.addch(0, 1, c, curses.A_BOLD) + self.assertEqual(self._read_char(0, 1), c) + self.assertTrue(stdscr.inch(0, 1) & curses.A_BOLD) + stdscr.insch(1, 0, c) + self.assertEqual(self._read_char(1, 0), c) + stdscr.move(2, 0) + stdscr.echochar(c) + self.assertEqual(self._read_char(2, 0), c) + + # echochar() + stdscr.refresh() + stdscr.move(0, 0) + stdscr.echochar('A') + stdscr.echochar(b'A') + stdscr.echochar(65) + # See _encodable for the character set; as in the addch() loop above. + for c in ('A', '\u00e9', '\u00a4', '\u20ac', '\u0454'): + try: + stdscr.echochar(c) + except UnicodeEncodeError: + # The character is not encodable with the current encoding. + self.assertRaises(UnicodeEncodeError, c.encode, encoding) + except OverflowError: + # The character is encoded to a multibyte sequence. + encoded = c.encode(encoding) + self.assertNotEqual(len(encoded), 1, repr(encoded)) + stdscr.echochar('A', curses.A_BOLD) + self.assertIs(stdscr.is_wintouched(), False) + + def test_output_string(self): + stdscr = self.stdscr + encoding = stdscr.encoding + # addstr()/insstr() + for func in [stdscr.addstr, stdscr.insstr]: + with self.subTest(func.__qualname__): + func('abcd') + func(b'abcd') + # Common and encoding-distinctive strings (see _encodable for the + # 0xA4 set); 'àßçđ' is UTF-8-only. Each is written if the + # encoding allows, else raises UnicodeEncodeError. + for s in ('soupçon', 'àßçđ', 'soupçon ¤', 'soupçon €', 'дякую'): + stdscr.move(0, 0) + try: + func(s) + except UnicodeEncodeError: + self.assertRaises(UnicodeEncodeError, s.encode, encoding) + stdscr.move(0, 0) + func('abcd', curses.A_BOLD) + func(1, 2, 'abcd') + func(2, 3, 'abcd', curses.A_BOLD) + + # addnstr()/insnstr() + for func in [stdscr.addnstr, stdscr.insnstr]: + with self.subTest(func.__qualname__): + stdscr.move(0, 0) + func('1234', 3) + func(b'1234', 3) + # As above (see _encodable); Arabic-Indic digits are UTF-8-only. + for s in ('caf\u00e9', '\u0661\u0662\u0663\u0664', 'caf\u00e9 \u00a4', 'caf\u00e9 \u20ac', '\u0434\u044f\u043a\u0443\u044e'): + stdscr.move(0, 0) + try: + func(s, 3) + except UnicodeEncodeError: + self.assertRaises(UnicodeEncodeError, s.encode, encoding) + stdscr.move(0, 0) + func('1234', 5) + func('1234', 3, curses.A_BOLD) + func(1, 2, '1234', 3) + func(2, 3, '1234', 3, curses.A_BOLD) + + def test_output_string_embedded_null_chars(self): + # reject embedded null bytes and characters + stdscr = self.stdscr + for arg in ['a\0', b'a\0']: + with self.subTest(arg=arg): + self.assertRaises(ValueError, stdscr.addstr, arg) + self.assertRaises(ValueError, stdscr.addnstr, arg, 1) + self.assertRaises(ValueError, stdscr.insstr, arg) + self.assertRaises(ValueError, stdscr.insnstr, arg, 1) + + def test_output_string_attr_restored(self): + # A write with an attr restores the window rendition afterwards, + # whether it succeeded or failed. + win = curses.newwin(2, 10, 0, 0) + def current_attrs(): + # Write a cell with the window's current rendition and read it + # back, so that the rendition itself is checked. + win.addstr(1, 0, ' ') + return win.inch(1, 0) & curses.A_ATTRIBUTES + for func, args in [(win.addstr, ('x',)), (win.addnstr, ('x', 1)), + (win.insstr, ('x',)), (win.insnstr, ('x', 1))]: + with self.subTest(func.__qualname__): + win.attrset(curses.A_UNDERLINE) + # y=100 is outside the window, so the write fails. + self.assertRaises(curses.error, func, 100, 0, *args, + curses.A_BOLD) + self.assertEqual(current_attrs(), curses.A_UNDERLINE) + func(0, 0, *args, curses.A_BOLD) + self.assertEqual(current_attrs(), curses.A_UNDERLINE) + + def test_add_string_behavior(self): + # addstr() advances the cursor past the written text; addnstr() + # writes at most n characters. + win = curses.newwin(1, 10, 0, 0) + win.addstr(0, 0, 'abc') + self.assertEqual(win.getyx(), (0, 3)) + win.erase() + win.addnstr(0, 0, 'abcdef', 3) + self.assertEqual(win.instr(0, 0), b'abc ') + + def test_insert_string_behavior(self): + # insstr()/insnstr() insert at the cursor, shift the rest of the + # line right (losing characters off the edge), and leave the cursor + # where it was. + win = curses.newwin(1, 10, 0, 0) + win.addstr(0, 0, 'abcde') + win.move(0, 1) + win.insstr('XY') + self.assertEqual(win.getyx(), (0, 1)) # cursor did not advance + self.assertEqual(win.instr(0, 0), b'aXYbcde ') + + win.erase() + win.addstr(0, 0, 'ZZZZZ') + win.move(0, 0) + win.insnstr('abcdef', 3) # at most 3 characters + self.assertEqual(win.instr(0, 0), b'abcZZZZZ ') + + def test_insch(self): + # insch() inserts a single character at the cursor (or at y, x), + # shifting the rest of the line right. + win = curses.newwin(2, 10, 0, 0) + win.addstr(0, 0, 'abc') + win.move(0, 1) + win.insch(ord('X')) + self.assertEqual(win.instr(0, 0), b'aXbc ') + win.insch(1, 0, 'Y', curses.A_BOLD) + self.assertEqual(win.inch(1, 0), b'Y'[0] | curses.A_BOLD) + + def test_pad(self): + pad = curses.newpad(10, 20) + pad.addstr(0, 0, 'PADTEXT') + self.assertEqual(pad.instr(0, 0, 7), b'PADTEXT') + + # subpad() creates a pad within the parent pad. Cell sharing with + # the parent is implementation-defined, so write to the subpad itself. + sub = pad.subpad(3, 5, 0, 0) + self.assertEqual(sub.getmaxyx(), (3, 5)) + sub.addstr(1, 0, 'sub') + self.assertEqual(sub.instr(1, 0, 3), b'sub') + + # A pad is refreshed onto an explicit screen rectangle; the + # 6-argument form is required (and rejected for ordinary windows). + pad.refresh(0, 0, 0, 0, 4, 10) + pad.noutrefresh(0, 0, 0, 0, 4, 10) + curses.doupdate() + self.assertRaises(TypeError, pad.refresh) + win = curses.newwin(5, 5, 0, 0) + self.assertRaises(TypeError, win.refresh, 0, 0, 0, 0, 4, 4) + + def test_read_from_window(self): + stdscr = self.stdscr + stdscr.addstr(0, 1, 'ABCD', curses.A_BOLD) + # inch() + stdscr.move(0, 1) + self.assertEqual(stdscr.inch(), 65 | curses.A_BOLD) + self.assertEqual(stdscr.inch(0, 3), 67 | curses.A_BOLD) + stdscr.move(0, 0) + # instr() + self.assertEqual(stdscr.instr()[:6], b' ABCD ') + self.assertEqual(stdscr.instr(3)[:6], b' AB') + self.assertEqual(stdscr.instr(0, 2)[:4], b'BCD ') + self.assertEqual(stdscr.instr(0, 2, 4), b'BCD ') + self.assertRaises(ValueError, stdscr.instr, -2) + self.assertRaises(ValueError, stdscr.instr, 0, 2, -2) + # A non-ASCII character of an 8-bit locale reads back as its encoded + # byte (see _encodable for the set). Both instr() and inch() return the + # locale byte for any character that fits the locale's single-byte + # encoding. + encoding = stdscr.encoding + for ch in ('A', 'é', '¤', '€', 'є'): + try: + b = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(b) != 1: + continue + with self.subTest(ch=ch): + stdscr.addstr(2, 0, ch) + self.assertEqual(stdscr.instr(2, 0, 1), b) + self.assertEqual(stdscr.inch(2, 0), b[0]) + + def test_coordinate_errors(self): + # Addressing a cell outside the window raises curses.error. + win = curses.newwin(5, 10, 0, 0) + self.assertRaises(curses.error, win.move, 100, 100) + self.assertRaises(curses.error, win.move, -1, -1) + self.assertRaises(curses.error, win.addch, 100, 100, ord('x')) + self.assertRaises(curses.error, win.inch, 100, 100) + if hasattr(win, 'chgat'): # chgat() requires wchgat() + self.assertRaises(curses.error, win.chgat, 100, 0, curses.A_BOLD) + + def test_argument_errors(self): + win = curses.newwin(5, 10, 0, 0) + # A character argument must be an int, a byte or a one-element string. + self.assertRaises(TypeError, win.addch, []) + self.assertRaises(OverflowError, win.addch, 2**64) + # A string method rejects a non-string, non-bytes argument. + self.assertRaises(TypeError, win.addstr, 5) + self.assertRaises(TypeError, win.addstr) + # Wrong number of positional arguments. + self.assertRaises(TypeError, win.instr, 0, 0, 0, 0) + + def test_getch(self): + win = curses.newwin(5, 12, 5, 2) + + # TODO: Test with real input by writing to master fd. + for c in 'spam\n'[::-1]: + curses.ungetch(c) + self.assertEqual(win.getch(3, 1), b's'[0]) + self.assertEqual(win.getyx(), (3, 1)) + self.assertEqual(win.getch(3, 4), b'p'[0]) + self.assertEqual(win.getyx(), (3, 4)) + self.assertEqual(win.getch(), b'a'[0]) + self.assertEqual(win.getyx(), (3, 4)) + self.assertEqual(win.getch(), b'm'[0]) + self.assertEqual(win.getch(), b'\n'[0]) + + # A key value > 127 is delivered unchanged (it is not locale text). + curses.ungetch(0xE9) + self.assertEqual(win.getch(), 0xE9) + + def test_getstr(self): + win = curses.newwin(5, 12, 5, 2) + curses.echo() + self.addCleanup(curses.noecho) + + self.assertRaises(ValueError, win.getstr, -400) + self.assertRaises(ValueError, win.getstr, 2, 3, -400) + + # TODO: Test with real input by writing to master fd. + for c in 'Lorem\nipsum\ndolor\nsit\namet\n'[::-1]: + curses.ungetch(c) + self.assertEqual(win.getstr(3, 1, 2), b'Lo') + self.assertEqual(win.instr(3, 0), b' Lo ') + self.assertEqual(win.getstr(3, 5, 10), b'ipsum') + self.assertEqual(win.instr(3, 0), b' Lo ipsum ') + self.assertEqual(win.getstr(1, 5), b'dolor') + self.assertEqual(win.instr(1, 0), b' dolor ') + self.assertEqual(win.getstr(2), b'si') + self.assertEqual(win.instr(1, 0), b'si dolor ') + self.assertEqual(win.getstr(), b'amet') + self.assertEqual(win.instr(1, 0), b'amet dolor ') + + def test_clear(self): + win = curses.newwin(5, 15, 5, 2) + lorem_ipsum(win) + + win.move(0, 8) + win.clrtoeol() + self.assertEqual(win.instr(0, 0).rstrip(), b'Lorem ip') + self.assertEqual(win.instr(1, 0).rstrip(), b'dolor sit amet,') + + win.move(0, 3) + win.clrtobot() + self.assertEqual(win.instr(0, 0).rstrip(), b'Lor') + self.assertEqual(win.instr(1, 0).rstrip(), b'') + + for func in [win.erase, win.clear]: + lorem_ipsum(win) + func() + self.assertEqual(win.instr(0, 0).rstrip(), b'') + self.assertEqual(win.instr(1, 0).rstrip(), b'') + + def test_insert_delete(self): + win = curses.newwin(5, 15, 5, 2) + lorem_ipsum(win) + + win.move(0, 2) + win.delch() + self.assertEqual(win.instr(0, 0), b'Loem ipsum ') + win.delch(0, 7) + self.assertEqual(win.instr(0, 0), b'Loem ipum ') + + win.move(1, 5) + win.deleteln() + self.assertEqual(win.instr(0, 0), b'Loem ipum ') + self.assertEqual(win.instr(1, 0), b'consectetur ') + self.assertEqual(win.instr(2, 0), b'adipiscing elit') + self.assertEqual(win.instr(3, 0), b'sed do eiusmod ') + self.assertEqual(win.instr(4, 0), b' ') + + win.move(1, 5) + win.insertln() + self.assertEqual(win.instr(0, 0), b'Loem ipum ') + self.assertEqual(win.instr(1, 0), b' ') + self.assertEqual(win.instr(2, 0), b'consectetur ') + + win.clear() + lorem_ipsum(win) + win.move(1, 5) + win.insdelln(2) + self.assertEqual(win.instr(0, 0), b'Lorem ipsum ') + self.assertEqual(win.instr(1, 0), b' ') + self.assertEqual(win.instr(2, 0), b' ') + self.assertEqual(win.instr(3, 0), b'dolor sit amet,') + + win.clear() + lorem_ipsum(win) + win.move(1, 5) + win.insdelln(-2) + self.assertEqual(win.instr(0, 0), b'Lorem ipsum ') + self.assertEqual(win.instr(1, 0), b'adipiscing elit') + self.assertEqual(win.instr(2, 0), b'sed do eiusmod ') + self.assertEqual(win.instr(3, 0), b' ') + + def test_scroll(self): + win = curses.newwin(5, 15, 5, 2) + lorem_ipsum(win) + win.scrollok(True) + win.scroll() + self.assertEqual(win.instr(0, 0), b'dolor sit amet,') + win.scroll(2) + self.assertEqual(win.instr(0, 0), b'adipiscing elit') + win.scroll(-3) + self.assertEqual(win.instr(0, 0), b' ') + self.assertEqual(win.instr(2, 0), b' ') + self.assertEqual(win.instr(3, 0), b'adipiscing elit') + win.scrollok(False) + + def test_attributes(self): + # TODO: attr_get(), attr_set(), ... + win = curses.newwin(5, 15, 5, 2) + win.attron(curses.A_BOLD) + win.attroff(curses.A_BOLD) + win.attrset(curses.A_BOLD) + + win.standout() + win.standend() + + @requires_curses_window_meth('chgat') + def test_chgat(self): + win = curses.newwin(5, 15, 5, 2) + win.addstr(2, 0, 'Lorem ipsum') + win.addstr(3, 0, 'dolor sit amet') + + win.move(2, 8) + win.chgat(curses.A_BLINK) + self.assertEqual(win.inch(2, 7), b'p'[0]) + self.assertEqual(win.inch(2, 8), b's'[0] | curses.A_BLINK) + self.assertEqual(win.inch(2, 14), b' '[0] | curses.A_BLINK) + + win.move(2, 1) + win.chgat(3, curses.A_BOLD) + self.assertEqual(win.inch(2, 0), b'L'[0]) + self.assertEqual(win.inch(2, 1), b'o'[0] | curses.A_BOLD) + self.assertEqual(win.inch(2, 3), b'e'[0] | curses.A_BOLD) + self.assertEqual(win.inch(2, 4), b'm'[0]) + + win.chgat(3, 2, curses.A_UNDERLINE) + self.assertEqual(win.inch(3, 1), b'o'[0]) + self.assertEqual(win.inch(3, 2), b'l'[0] | curses.A_UNDERLINE) + self.assertEqual(win.inch(3, 14), b' '[0] | curses.A_UNDERLINE) + + win.chgat(3, 4, 7, curses.A_BLINK) + self.assertEqual(win.inch(3, 3), b'o'[0] | curses.A_UNDERLINE) + self.assertEqual(win.inch(3, 4), b'r'[0] | curses.A_BLINK) + self.assertEqual(win.inch(3, 10), b'a'[0] | curses.A_BLINK) + self.assertEqual(win.inch(3, 11), b'm'[0] | curses.A_UNDERLINE) + self.assertEqual(win.inch(3, 14), b' '[0] | curses.A_UNDERLINE) + + def test_background(self): + win = curses.newwin(5, 15, 5, 2) + win.addstr(0, 0, 'Lorem ipsum') + + self.assertIn(win.getbkgd(), (0, 32)) + + # bkgdset() + win.bkgdset('_') + self.assertEqual(win.getbkgd(), b'_'[0]) + win.bkgdset(b'#') + self.assertEqual(win.getbkgd(), b'#'[0]) + win.bkgdset(65) + self.assertEqual(win.getbkgd(), 65) + win.bkgdset(0) + self.assertEqual(win.getbkgd(), 32) + + win.bkgdset('#', curses.A_REVERSE) + self.assertEqual(win.getbkgd(), b'#'[0] | curses.A_REVERSE) + self.assertEqual(win.inch(0, 0), b'L'[0]) + self.assertEqual(win.inch(0, 5), b' '[0]) + win.bkgdset(0) + + # bkgd() + win.bkgd('_') + self.assertEqual(win.getbkgd(), b'_'[0]) + self.assertEqual(win.inch(0, 0), b'L'[0]) + self.assertEqual(win.inch(0, 5), b'_'[0]) + + win.bkgd('#', curses.A_REVERSE) + self.assertEqual(win.getbkgd(), b'#'[0] | curses.A_REVERSE) + self.assertEqual(win.inch(0, 0), b'L'[0] | curses.A_REVERSE) + self.assertEqual(win.inch(0, 5), b'#'[0] | curses.A_REVERSE) + + # A non-ASCII background character of an 8-bit locale reads back as its + # encoded byte. See _encodable for the character set. + win.bkgd(' ') + encoding = win.encoding + for ch in ('é', '¤', '€', 'є'): + try: + b = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(b) != 1: + continue + # A wide build stores a character outside Latin-1 as a wide cell, + # not as its encoded byte, so it cannot round-trip here. + if ord(ch) > 0xff and hasattr(win, 'get_wch'): + continue + with self.subTest(ch=ch): + win.bkgd(ch) + self.assertEqual(win.getbkgd(), b[0]) + if ord(ch) < 0x100: + # The same byte given as an int. A wide build stores it + # through the locale, so only a Latin-1 byte round-trips. + win.bkgd(' ') + win.bkgdset(b[0]) + self.assertEqual(win.getbkgd(), b[0]) + win.bkgd(b[0]) + self.assertEqual(win.getbkgd(), b[0]) + + def test_overlay(self): + srcwin = curses.newwin(5, 18, 3, 4) + lorem_ipsum(srcwin) + dstwin = curses.newwin(7, 17, 5, 7) + for i in range(6): + dstwin.addstr(i, 0, '_'*17) + + srcwin.overlay(dstwin) + self.assertEqual(dstwin.instr(0, 0), b'sectetur_________') + self.assertEqual(dstwin.instr(1, 0), b'piscing_elit,____') + self.assertEqual(dstwin.instr(2, 0), b'_do_eiusmod______') + self.assertEqual(dstwin.instr(3, 0), b'_________________') + + srcwin.overwrite(dstwin) + self.assertEqual(dstwin.instr(0, 0), b'sectetur __') + self.assertEqual(dstwin.instr(1, 0), b'piscing elit, __') + self.assertEqual(dstwin.instr(2, 0), b' do eiusmod __') + self.assertEqual(dstwin.instr(3, 0), b'_________________') + + srcwin.overlay(dstwin, 1, 4, 3, 2, 4, 11) + self.assertEqual(dstwin.instr(3, 0), b'__r_sit_amet_____') + self.assertEqual(dstwin.instr(4, 0), b'__ectetur________') + self.assertEqual(dstwin.instr(5, 0), b'_________________') + + srcwin.overwrite(dstwin, 1, 4, 3, 2, 4, 11) + self.assertEqual(dstwin.instr(3, 0), b'__r sit amet_____') + self.assertEqual(dstwin.instr(4, 0), b'__ectetur _____') + self.assertEqual(dstwin.instr(5, 0), b'_________________') + + def test_refresh(self): + win = curses.newwin(5, 15, 2, 5) + win.noutrefresh() + win.redrawln(1, 2) + win.redrawwin() + win.refresh() + curses.doupdate() + + @requires_curses_window_meth('resize') + def test_resize(self): + win = curses.newwin(5, 15, 2, 5) + win.resize(4, 20) + self.assertEqual(win.getmaxyx(), (4, 20)) + win.resize(5, 15) + self.assertEqual(win.getmaxyx(), (5, 15)) + + @requires_curses_window_meth('enclose') + def test_enclose(self): + win = curses.newwin(5, 15, 2, 5) + self.assertIs(win.enclose(2, 5), True) + self.assertIs(win.enclose(1, 5), False) + self.assertIs(win.enclose(2, 4), False) + self.assertIs(win.enclose(6, 19), True) + self.assertIs(win.enclose(7, 19), False) + self.assertIs(win.enclose(6, 20), False) + + def test_putwin(self): + win = curses.newwin(5, 12, 1, 2) + win.addstr(2, 1, 'Lorem ipsum') + with tempfile.TemporaryFile() as f: + win.putwin(f) + del win + f.seek(0) + win = curses.getwin(f) + self.assertEqual(win.getbegyx(), (1, 2)) + self.assertEqual(win.getmaxyx(), (5, 12)) + self.assertEqual(win.instr(2, 0), b' Lorem ipsum') + + def test_borders_and_lines(self): + win = curses.newwin(5, 10, 5, 2) + win.border('|', '!', '-', '_', + '+', '\\', '#', '/') + self.assertEqual(win.instr(0, 0), b'+--------\\') + self.assertEqual(win.instr(1, 0), b'| !') + self.assertEqual(win.instr(4, 0), b'#________/') + win.border(b'|', b'!', b'-', b'_', + b'+', b'\\', b'#', b'/') + win.border(65, 66, 67, 68, + 69, 70, 71, 72) + self.assertRaises(TypeError, win.border, + 65, 66, 67, 68, 69, [], 71, 72) + self.assertRaises(TypeError, win.border, + 65, 66, 67, 68, 69, 70, 71, 72, 73) + self.assertRaises(TypeError, win.border, + 65, 66, 67, 68, 69, 70, 71, 72, 73) + win.border(65, 66, 67, 68, 69, 70, 71) + win.border(65, 66, 67, 68, 69, 70) + win.border(65, 66, 67, 68, 69) + win.border(65, 66, 67, 68) + win.border(65, 66, 67) + win.border(65, 66) + win.border(65) + win.border() + # With no arguments, border() fills the edges with ACS line and corner + # characters. + chartext = curses.A_CHARTEXT + maxy, maxx = win.getmaxyx() + self.assertEqual(win.inch(0, 0) & chartext, curses.ACS_ULCORNER & chartext) + self.assertEqual(win.inch(0, maxx-1) & chartext, curses.ACS_URCORNER & chartext) + self.assertEqual(win.inch(maxy-1, 0) & chartext, curses.ACS_LLCORNER & chartext) + self.assertEqual(win.inch(maxy-1, maxx-1) & chartext, curses.ACS_LRCORNER & chartext) + self.assertEqual(win.inch(0, 1) & chartext, curses.ACS_HLINE & chartext) + self.assertEqual(win.inch(1, 0) & chartext, curses.ACS_VLINE & chartext) + + win.box(':', '~') + self.assertEqual(win.instr(0, 1, 8), b'~~~~~~~~') + self.assertEqual(win.instr(1, 0), b': :') + self.assertEqual(win.instr(4, 1, 8), b'~~~~~~~~') + win.box(b':', b'~') + win.box(65, 67) + self.assertRaises(TypeError, win.box, 65, 66, 67) + self.assertRaises(TypeError, win.box, 65) + win.box() + # With no arguments, box() likewise draws ACS corners and lines. + self.assertEqual(win.inch(0, 0) & chartext, curses.ACS_ULCORNER & chartext) + self.assertEqual(win.inch(0, maxx-1) & chartext, curses.ACS_URCORNER & chartext) + self.assertEqual(win.inch(0, 1) & chartext, curses.ACS_HLINE & chartext) + self.assertEqual(win.inch(1, 0) & chartext, curses.ACS_VLINE & chartext) + + win.move(1, 2) + win.hline('-', 5) + self.assertEqual(win.instr(1, 1, 7), b' ----- ') + win.hline(b'-', 5) + win.hline(45, 5) + win.hline('-', 5, curses.A_BOLD) + win.hline(1, 1, '-', 5) + win.hline(1, 1, '-', 5, curses.A_BOLD) + + win.move(1, 2) + win.vline('a', 3) + win.vline(b'a', 3) + win.vline(97, 3) + win.vline('a', 3, curses.A_STANDOUT) + win.vline(1, 1, 'a', 3) + win.vline(1, 1, ';', 2, curses.A_STANDOUT) + self.assertEqual(win.inch(1, 1), b';'[0] | curses.A_STANDOUT) + self.assertEqual(win.inch(2, 1), b';'[0] | curses.A_STANDOUT) + self.assertEqual(win.inch(3, 1), b'a'[0]) + + # A border or line character of an 8-bit locale round-trips as its + # encoded byte. See _encodable for the character set. + encoding = win.encoding + for ch in ('é', '¤', '€', 'є'): + try: + b = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(b) != 1: + continue + # A wide build stores a character outside Latin-1 as a wide cell, + # not as its encoded byte, so it cannot round-trip here. + if ord(ch) > 0xff and hasattr(win, 'get_wch'): + continue + with self.subTest(ch=ch): + win.erase() + win.hline(2, 0, ch, 5) + self.assertEqual(win.instr(2, 0, 5), b * 5) + win.vline(0, 0, ch, 3) + self.assertEqual(win.instr(0, 0, 1), b) + self.assertEqual(win.instr(1, 0, 1), b) + win.border(ch, ch, ch, ch, ch, ch, ch, ch) + self.assertEqual(win.instr(0, 0), b * maxx) + if ord(ch) < 0x100: + # The same byte given as an int. A wide build stores it + # through the locale, so only a Latin-1 byte round-trips. + v = b[0] + win.erase() + win.hline(2, 0, v, 5) + self.assertEqual(win.instr(2, 0, 5), b * 5) + win.vline(0, 0, v, 3) + self.assertEqual(win.instr(1, 0, 1), b) + win.border(v, v, v, v, v, v, v, v) + self.assertEqual(win.instr(0, 0), b * maxx) + win.box(v, v) + self.assertEqual(win.instr(0, 1, 1), b) + + def test_unctrl(self): + # TODO: wunctrl() + self.assertEqual(curses.unctrl(b'A'), b'A') + self.assertEqual(curses.unctrl('A'), b'A') + self.assertEqual(curses.unctrl(65), b'A') + self.assertEqual(curses.unctrl(b'\n'), b'^J') + self.assertEqual(curses.unctrl('\n'), b'^J') + self.assertEqual(curses.unctrl(10), b'^J') + # A printable non-ASCII byte of an 8-bit locale is returned unchanged. + # See _encodable for the character set. + encoding = self.stdscr.encoding + for ch in ('é', '¤', '€', 'є'): + try: + b = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(b) != 1: + continue + with self.subTest(ch=ch): + self.assertEqual(curses.unctrl(ch), b) + self.assertEqual(curses.unctrl(b[0]), b) # the byte as an int + self.assertRaises(TypeError, curses.unctrl, b'') + self.assertRaises(TypeError, curses.unctrl, b'AB') + self.assertRaises(TypeError, curses.unctrl, '') + self.assertRaises(TypeError, curses.unctrl, 'AB') + self.assertRaises(OverflowError, curses.unctrl, 2**64) + + def test_endwin(self): + if not self.isatty: + self.skipTest('requires terminal') + self.assertIs(curses.isendwin(), False) + curses.endwin() + self.assertIs(curses.isendwin(), True) + curses.doupdate() + self.assertIs(curses.isendwin(), False) + + def test_terminfo(self): + self.assertIsInstance(curses.tigetflag('hc'), int) + self.assertEqual(curses.tigetflag('cols'), -1) + self.assertEqual(curses.tigetflag('cr'), -1) + + self.assertIsInstance(curses.tigetnum('cols'), int) + self.assertEqual(curses.tigetnum('hc'), -2) + self.assertEqual(curses.tigetnum('cr'), -2) + + self.assertIsInstance(curses.tigetstr('cr'), (bytes, type(None))) + self.assertIsNone(curses.tigetstr('hc')) + self.assertIsNone(curses.tigetstr('cols')) + + cud = curses.tigetstr('cud') + if cud is not None: + # See issue10570. + self.assertIsInstance(cud, bytes) + curses.tparm(cud, 2) + cud_2 = curses.tparm(cud, 2) + self.assertIsInstance(cud_2, bytes) + curses.putp(cud_2) + + curses.putp(b'abc\n') + + def test_misc_module_funcs(self): + curses.delay_output(1) + curses.flushinp() + + curses.doupdate() + self.assertIs(curses.isendwin(), False) + + curses.napms(100) + + curses.newpad(50, 50) + + def test_env_queries(self): + # TODO: term_attrs(), erasewchar(), killwchar() + self.assertIsInstance(curses.termname(), bytes) + self.assertIsInstance(curses.longname(), bytes) + self.assertIsInstance(curses.baudrate(), int) + self.assertIsInstance(curses.has_ic(), bool) + self.assertIsInstance(curses.has_il(), bool) + self.assertIsInstance(curses.termattrs(), int) + + c = curses.killchar() + self.assertIsInstance(c, bytes) + self.assertEqual(len(c), 1) + c = curses.erasechar() + self.assertIsInstance(c, bytes) + self.assertEqual(len(c), 1) + + def test_output_options(self): + stdscr = self.stdscr + + stdscr.clearok(True) + stdscr.clearok(False) + + stdscr.idcok(True) + stdscr.idcok(False) + + stdscr.idlok(False) + stdscr.idlok(True) + + if hasattr(stdscr, 'immedok'): + stdscr.immedok(True) + stdscr.immedok(False) + + stdscr.leaveok(True) + stdscr.leaveok(False) + + stdscr.scrollok(True) + stdscr.scrollok(False) + + stdscr.setscrreg(5, 10) + + curses.nonl() + curses.nl(True) + curses.nl(False) + curses.nl() + + def test_input_options(self): + stdscr = self.stdscr + + if self.isatty: + curses.nocbreak() + curses.cbreak() + curses.cbreak(False) + curses.cbreak(True) + + curses.intrflush(True) + curses.intrflush(False) + + curses.raw() + curses.raw(False) + curses.raw(True) + curses.noraw() + + curses.noecho() + curses.echo() + curses.echo(False) + curses.echo(True) + + curses.halfdelay(255) + curses.halfdelay(1) + + stdscr.keypad(True) + stdscr.keypad(False) + + curses.meta(True) + curses.meta(False) + + stdscr.nodelay(True) + stdscr.nodelay(False) + + curses.noqiflush() + curses.qiflush(True) + curses.qiflush(False) + curses.qiflush() + + stdscr.notimeout(True) + stdscr.notimeout(False) + + stdscr.timeout(-1) + stdscr.timeout(0) + stdscr.timeout(5) + + @requires_curses_func('typeahead') + def test_typeahead(self): + curses.typeahead(sys.__stdin__.fileno()) + curses.typeahead(-1) + + def test_prog_mode(self): + if not self.isatty: + self.skipTest('requires terminal') + curses.def_prog_mode() + curses.reset_prog_mode() + # def_shell_mode()/reset_shell_mode() are intentionally not exercised + # here: they capture and restore curses' "shell mode" terminal state, + # which is only meaningful before initscr(). Calling them mid-suite + # corrupts the modes that endwin() restores and breaks later tests. + + def test_beep(self): + if (curses.tigetstr("bel") is not None + or curses.tigetstr("flash") is not None): + curses.beep() + else: + try: + curses.beep() + except curses.error: + self.skipTest('beep() failed') + + def test_flash(self): + if (curses.tigetstr("bel") is not None + or curses.tigetstr("flash") is not None): + curses.flash() + else: + try: + curses.flash() + except curses.error: + self.skipTest('flash() failed') + + def test_curs_set(self): + for vis, cap in [(0, 'civis'), (2, 'cvvis'), (1, 'cnorm')]: + if curses.tigetstr(cap) is not None: + curses.curs_set(vis) + else: + try: + curses.curs_set(vis) + except curses.error: + pass + + @requires_curses_func('get_escdelay') + def test_escdelay(self): + escdelay = curses.get_escdelay() + self.assertIsInstance(escdelay, int) + curses.set_escdelay(25) + self.assertEqual(curses.get_escdelay(), 25) + curses.set_escdelay(escdelay) + + @requires_curses_func('get_tabsize') + def test_tabsize(self): + tabsize = curses.get_tabsize() + self.assertIsInstance(tabsize, int) + curses.set_tabsize(4) + self.assertEqual(curses.get_tabsize(), 4) + curses.set_tabsize(tabsize) + + @requires_curses_func('getsyx') + def test_getsyx(self): + y, x = curses.getsyx() + self.assertIsInstance(y, int) + self.assertIsInstance(x, int) + curses.setsyx(4, 5) + self.assertEqual(curses.getsyx(), (4, 5)) + + def bad_colors(self): + return (-1, curses.COLORS, -2**31 - 1, 2**31, -2**63 - 1, 2**63, 2**64) + + def bad_colors2(self): + return (curses.COLORS, 2**31, 2**63, 2**64) + + def bad_pairs(self): + return (-1, -2**31 - 1, 2**31, -2**63 - 1, 2**63, 2**64) + + def test_has_colors(self): + self.assertIsInstance(curses.has_colors(), bool) + self.assertIsInstance(curses.can_change_color(), bool) + + def test_start_color(self): + if not curses.has_colors(): + self.skipTest('requires colors support') + curses.start_color() + if verbose: + print(f'COLORS = {curses.COLORS}', file=sys.stderr) + print(f'COLOR_PAIRS = {curses.COLOR_PAIRS}', file=sys.stderr) + + @requires_colors + def test_color_content(self): + self.assertEqual(curses.color_content(curses.COLOR_BLACK), (0, 0, 0)) + curses.color_content(0) + maxcolor = curses.COLORS - 1 + curses.color_content(maxcolor) + + for color in self.bad_colors(): + self.assertRaises(ValueError, curses.color_content, color) + + @requires_colors + def test_init_color(self): + if not curses.can_change_color(): + self.skipTest('cannot change color') + + old = curses.color_content(0) + try: + curses.init_color(0, *old) + except curses.error: + self.skipTest('cannot change color (init_color() failed)') + self.addCleanup(curses.init_color, 0, *old) + curses.init_color(0, 0, 0, 0) + self.assertEqual(curses.color_content(0), (0, 0, 0)) + curses.init_color(0, 1000, 1000, 1000) + self.assertEqual(curses.color_content(0), (1000, 1000, 1000)) + + maxcolor = curses.COLORS - 1 + old = curses.color_content(maxcolor) + curses.init_color(maxcolor, *old) + self.addCleanup(curses.init_color, maxcolor, *old) + curses.init_color(maxcolor, 0, 500, 1000) + self.assertEqual(curses.color_content(maxcolor), (0, 500, 1000)) + + for color in self.bad_colors(): + self.assertRaises(ValueError, curses.init_color, color, 0, 0, 0) + for comp in (-1, 1001): + self.assertRaises(ValueError, curses.init_color, 0, comp, 0, 0) + self.assertRaises(ValueError, curses.init_color, 0, 0, comp, 0) + self.assertRaises(ValueError, curses.init_color, 0, 0, 0, comp) + + def get_pair_limit(self): + pair_limit = curses.COLOR_PAIRS + if hasattr(curses, 'ncurses_version'): + if curses.has_extended_color_support(): + pair_limit += 2*curses.COLORS + 1 + if (not curses.has_extended_color_support() + or (6, 1) <= curses.ncurses_version < (6, 2)): + pair_limit = min(pair_limit, SHORT_MAX) + # If use_default_colors() is called, the upper limit of the extended + # range may be restricted, so we need to check if the limit is still + # correct + try: + curses.init_pair(pair_limit - 1, 0, 0) + except ValueError: + pair_limit = curses.COLOR_PAIRS + return pair_limit + + @requires_colors + def test_pair_content(self): + curses.pair_content(0) + maxpair = self.get_pair_limit() - 1 + if maxpair > 0: + curses.pair_content(maxpair) + + for pair in self.bad_pairs(): + self.assertRaises(ValueError, curses.pair_content, pair) + + @requires_colors + def test_init_pair(self): + old = curses.pair_content(1) + curses.init_pair(1, *old) + self.addCleanup(curses.init_pair, 1, *old) + + curses.init_pair(1, 0, 0) + self.assertEqual(curses.pair_content(1), (0, 0)) + maxcolor = curses.COLORS - 1 + curses.init_pair(1, maxcolor, 0) + self.assertEqual(curses.pair_content(1), (maxcolor, 0)) + curses.init_pair(1, 0, maxcolor) + self.assertEqual(curses.pair_content(1), (0, maxcolor)) + maxpair = self.get_pair_limit() - 1 + if maxpair > 1: + curses.init_pair(maxpair, 0, 0) + self.assertEqual(curses.pair_content(maxpair), (0, 0)) + + for pair in self.bad_pairs(): + self.assertRaises(ValueError, curses.init_pair, pair, 0, 0) + for color in self.bad_colors2(): + self.assertRaises(ValueError, curses.init_pair, 1, color, 0) + self.assertRaises(ValueError, curses.init_pair, 1, 0, color) + + @requires_colors + def test_color_attrs(self): + for pair in 0, 1, 255: + attr = curses.color_pair(pair) + self.assertEqual(curses.pair_number(attr), pair, attr) + self.assertEqual(curses.pair_number(attr | curses.A_BOLD), pair) + self.assertEqual(curses.color_pair(0), 0) + self.assertEqual(curses.pair_number(0), 0) + + @requires_curses_func('use_default_colors') + @requires_colors + def test_use_default_colors(self): + try: + curses.use_default_colors() + except curses.error: + self.skipTest('cannot change color (use_default_colors() failed)') + self.assertEqual(curses.pair_content(0), (-1, -1)) + + @requires_curses_func('assume_default_colors') + @requires_colors + def test_assume_default_colors(self): + try: + curses.assume_default_colors(-1, -1) + except curses.error: + self.skipTest('cannot change color (assume_default_colors() failed)') + self.assertEqual(curses.pair_content(0), (-1, -1)) + curses.assume_default_colors(curses.COLOR_YELLOW, curses.COLOR_BLUE) + self.assertEqual(curses.pair_content(0), (curses.COLOR_YELLOW, curses.COLOR_BLUE)) + curses.assume_default_colors(curses.COLOR_RED, -1) + self.assertEqual(curses.pair_content(0), (curses.COLOR_RED, -1)) + curses.assume_default_colors(-1, curses.COLOR_GREEN) + self.assertEqual(curses.pair_content(0), (-1, curses.COLOR_GREEN)) + curses.assume_default_colors(-1, -1) + + def test_keyname(self): + # TODO: key_name() + self.assertEqual(curses.keyname(65), b'A') + self.assertEqual(curses.keyname(13), b'^M') + self.assertEqual(curses.keyname(127), b'^?') + self.assertEqual(curses.keyname(0), b'^@') + self.assertRaises(ValueError, curses.keyname, -1) + self.assertIsInstance(curses.keyname(256), bytes) + + @requires_curses_func('has_key') + def test_has_key(self): + self.assertIsInstance(curses.has_key(13), bool) + self.assertIsInstance(curses.has_key(curses.KEY_LEFT), bool) + + @requires_curses_func('getmouse') + def test_getmouse(self): + (availmask, oldmask) = curses.mousemask(curses.BUTTON1_PRESSED) + if availmask == 0: + self.skipTest('mouse stuff not available') + curses.mouseinterval(10) + # just verify these don't cause errors + curses.ungetmouse(0, 0, 0, 0, curses.BUTTON1_PRESSED) + m = curses.getmouse() + + @requires_curses_func('panel') + def test_userptr_without_set(self): + w = curses.newwin(10, 10) + p = curses.panel.new_panel(w) + # try to access userptr() before calling set_userptr() -- segfaults + with self.assertRaises(curses.panel.error, + msg='userptr should fail since not set'): + p.userptr() + + @requires_curses_func('panel') + def test_userptr_memory_leak(self): + w = curses.newwin(10, 10) + p = curses.panel.new_panel(w) + obj = object() + nrefs = sys.getrefcount(obj) + for i in range(100): + p.set_userptr(obj) + + p.set_userptr(None) + self.assertEqual(sys.getrefcount(obj), nrefs, + "set_userptr leaked references") + + @requires_curses_func('panel') + def test_userptr_segfault(self): + w = curses.newwin(10, 10) + panel = curses.panel.new_panel(w) + class A: + def __del__(self): + panel.set_userptr(None) + panel.set_userptr(A()) + panel.set_userptr(None) + + @cpython_only + @requires_curses_func('panel') + def test_disallow_instantiation(self): + # Ensure that the type disallows instantiation (bpo-43916) + w = curses.newwin(10, 10) + panel = curses.panel.new_panel(w) + check_disallow_instantiation(self, type(panel)) + + @requires_curses_func('panel') + def test_panel_stack(self): + panel = curses.panel + # new_panel() puts the panel on top of the stack, so the three + # panels end up ordered bottom -> top as p1, p2, p3. + p1 = panel.new_panel(curses.newwin(3, 6, 0, 0)) + p2 = panel.new_panel(curses.newwin(3, 6, 1, 1)) + p3 = panel.new_panel(curses.newwin(3, 6, 2, 2)) + self.addCleanup(self._delete_panels, p1, p2, p3) + + # The most recently created panel is on top. + self.assertIs(panel.top_panel(), p3) + # window() returns the wrapped window. + self.assertEqual(p2.window().getbegyx(), (1, 1)) + + # above()/below() walk the stack one step at a time. + self.assertIs(p1.above(), p2) + self.assertIs(p2.above(), p3) + self.assertIsNone(p3.above()) # nothing above the top panel + self.assertIs(p3.below(), p2) + self.assertIs(p2.below(), p1) + + # top() raises a panel to the top, bottom() lowers it to the bottom. + p1.top() + self.assertIs(panel.top_panel(), p1) + self.assertIsNone(p1.above()) + p1.bottom() + self.assertIs(panel.bottom_panel(), p1) + self.assertIsNone(p1.below()) + + # update_panels() refreshes the virtual screen from the stack. + panel.update_panels() + + @requires_curses_func('panel') + def test_panel_hide_show(self): + p = curses.panel.new_panel(curses.newwin(3, 6, 0, 0)) + self.addCleanup(self._delete_panels, p) + self.assertIs(p.hidden(), False) + p.hide() + self.assertIs(p.hidden(), True) + p.show() + self.assertIs(p.hidden(), False) + + @requires_curses_func('panel') + def test_panel_move(self): + win = curses.newwin(3, 6, 1, 2) + p = curses.panel.new_panel(win) + self.addCleanup(self._delete_panels, p) + self.assertEqual(win.getbegyx(), (1, 2)) + p.move(4, 5) + self.assertEqual(win.getbegyx(), (4, 5)) + + @requires_curses_func('panel') + def test_panel_replace(self): + win1 = curses.newwin(3, 6, 0, 0) + win2 = curses.newwin(4, 8, 1, 1) + p = curses.panel.new_panel(win1) + self.addCleanup(self._delete_panels, p) + self.assertIs(p.window(), win1) + p.replace(win2) + self.assertIs(p.window(), win2) + + @requires_curses_func('panel') + def test_panel_userptr(self): + p = curses.panel.new_panel(curses.newwin(3, 6, 0, 0)) + self.addCleanup(self._delete_panels, p) + obj = ['userptr'] + p.set_userptr(obj) + self.assertIs(p.userptr(), obj) + + def _delete_panels(self, *panels): + # Drop the panels from the global stack so they do not leak into + # later tests that inspect top_panel()/bottom_panel(). + for p in panels: + try: + p.bottom() + except curses.panel.error: + pass + del panels + gc_collect() + + def _make_textbox(self, nlines, ncols, *, insert_mode=False, stripspaces=1): + win = curses.newwin(nlines, ncols, 0, 0) + box = curses.textpad.Textbox(win, insert_mode=insert_mode) + box.stripspaces = stripspaces + return box, win + + def _type(self, box, text): + for ch in text: + box.do_command(ch if isinstance(ch, int) else ord(ch)) + + def test_textbox_gather(self): + # Typed text is read back by gather(). With stripspaces on (the + # default) gather() keeps a single trailing blank on a line and + # drops trailing empty lines. + box, win = self._make_textbox(3, 10) + self._type(box, 'Hello') + self.assertEqual(box.gather(), 'Hello \n') + + def test_textbox_gather_multiline(self): + box, win = self._make_textbox(3, 10) + self._type(box, 'ab') + box.do_command(curses.ascii.NL) # ^j -> start of next line + self._type(box, 'cd') + self.assertEqual(box.gather(), 'ab \ncd \n') + + def test_textbox_stripspaces(self): + box, win = self._make_textbox(1, 8, stripspaces=1) + self._type(box, 'hi') + self.assertEqual(box.gather(), 'hi ') + + box, win = self._make_textbox(1, 8, stripspaces=0) + self._type(box, 'hi') + self.assertEqual(box.gather(), 'hi ') + + def test_textbox_insert_mode(self): + # In insert mode a typed character shifts the rest of the line right. + box, win = self._make_textbox(1, 10, insert_mode=True) + self._type(box, 'aXc') + win.move(0, 1) + self._type(box, 'b') + self.assertEqual(box.gather(), 'abXc ') + + def test_textbox_fill_last_cell(self): + # The lower-right cell can be written, even though addch() there + # cannot advance the cursor past the end of the window. + box, win = self._make_textbox(1, 4, stripspaces=0) + self._type(box, 'abcd') + self.assertEqual(box.gather(), 'abcd') + + def test_textbox_fill_last_cell_multiline(self): + box, win = self._make_textbox(2, 3, stripspaces=0) + self._type(box, 'abc') + box.do_command(curses.ascii.NL) # ^j -> start of next line + self._type(box, 'def') # 'f' lands in the lower-right cell + self.assertEqual(box.gather(), 'abc\ndef\n') + + def test_textbox_fill_last_cell_insert_mode(self): + box, win = self._make_textbox(1, 4, insert_mode=True, stripspaces=0) + self._type(box, 'abcd') + self.assertEqual(box.gather(), 'abcd') + + def test_textbox_fill_last_cell_scrollok(self): + # Writing the lower-right cell must not scroll the window even if it + # has scrolling enabled. + box, win = self._make_textbox(2, 3, stripspaces=0) + win.scrollok(True) + self._type(box, 'abc') + box.do_command(curses.ascii.NL) + self._type(box, 'def') + self.assertEqual(box.gather(), 'abc\ndef\n') + + def test_textbox_8bit(self): + # A character of an 8-bit locale encoding is entered and read back + # through the byte API. The byte path also runs on a wide build, so the + # test is not skipped there. Run the suite under an 8-bit locale + # (ISO-8859-1, ISO-8859-15 or KOI8-U) to reach the non-ASCII cases; each + # string is used only if the encoding maps it to single bytes. 'abc' is + # ASCII, 'café' is common to the Latin encodings, and the rest are + # distinctive (byte 0xA4 is '¤'/'€'/'є' in ISO-8859-1/-15/KOI8-U). + encoding = self.stdscr.encoding + for text in ['abc', 'café', 'naïve ¤¦', 'café €Šž', 'дякую єі']: + try: + data = text.encode(encoding) + except UnicodeEncodeError: + continue + if len(data) != len(text): + continue # a multibyte encoding is not the 8-bit byte path + with self.subTest(text=text): + box, win = self._make_textbox(1, 16) + for byte in data: + box.do_command(byte) + self.assertEqual(box.gather(), text + ' ') + + def test_textbox_8bit_insert(self): + # Insert mode shifts the rest of the line right by reading each cell back + # and rewriting it; a non-ASCII 8-bit-locale character must survive the + # shift, even on a wide build where inch() mangles it. See + # test_textbox_8bit for the character choices. + encoding = self.stdscr.encoding + for ch in ['é', '¤', '€', 'є']: + try: + data = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(data) != 1: + continue + with self.subTest(ch=ch): + box, win = self._make_textbox(1, 10, insert_mode=True) + for byte in ('a' + ch + 'c').encode(encoding): + box.do_command(byte) + win.move(0, 1) + box.do_command(ord('b')) # insert 'b', shifting ch and 'c' right + self.assertEqual(box.gather(), 'ab' + ch + 'c ') + + def test_textbox_8bit_fill_last_cell(self): + # A non-ASCII 8-bit-locale character must survive being written to the + # lower-right cell, which uses insch() rather than addch(). See + # test_textbox_8bit for the character choices. + encoding = self.stdscr.encoding + for ch in ['é', '¤', '€', 'є']: + try: + data = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(data) != 1: + continue + with self.subTest(ch=ch): + text = 'ab' + ch # the last character fills the corner + box, win = self._make_textbox(1, len(text), stripspaces=0) + for byte in text.encode(encoding): + box.do_command(byte) + self.assertEqual(box.gather(), text) + + def test_textbox_movement(self): + box, win = self._make_textbox(3, 10) + self._type(box, 'abc') + box.do_command(curses.ascii.SOH) # ^a -> left edge + self.assertEqual(win.getyx(), (0, 0)) + box.do_command(curses.ascii.ENQ) # ^e -> end of line + self.assertEqual(win.getyx(), (0, 3)) + + def test_textbox_kill_to_eol(self): + box, win = self._make_textbox(1, 10) + self._type(box, 'abcdef') + win.move(0, 3) + box.do_command(curses.ascii.VT) # ^k -> clear to end of line + self.assertEqual(box.gather(), 'abc ') + + def test_textbox_backspace(self): + box, win = self._make_textbox(1, 10) + self._type(box, 'abc') + box.do_command(curses.ascii.BS) # ^h -> delete backward + self.assertEqual(box.gather(), 'ab ') + + def test_textbox_edit(self): + # edit() reads characters until Ctrl-G and returns the contents. + box, win = self._make_textbox(1, 10) + for ch in reversed('Hi' + chr(curses.ascii.BEL)): + curses.ungetch(ch) + self.assertEqual(box.edit(), 'Hi ') + + def test_textbox_edit_validate(self): + # The validate hook can rewrite an incoming keystroke. + box, win = self._make_textbox(1, 10) + for ch in reversed('abc' + chr(curses.ascii.BEL)): + curses.ungetch(ch) + box.edit(lambda ch: ord('X') if ch == ord('b') else ch) + self.assertEqual(box.gather(), 'aXc ') + + def test_textpad_rectangle(self): + # rectangle() draws a box with ACS line/corner characters. + win = curses.newwin(6, 12, 0, 0) + curses.textpad.rectangle(win, 0, 0, 4, 8) + chartext = curses.A_CHARTEXT + self.assertEqual(win.inch(0, 0) & chartext, + curses.ACS_ULCORNER & chartext) + self.assertEqual(win.inch(0, 8) & chartext, + curses.ACS_URCORNER & chartext) + self.assertEqual(win.inch(4, 0) & chartext, + curses.ACS_LLCORNER & chartext) + self.assertEqual(win.inch(4, 8) & chartext, + curses.ACS_LRCORNER & chartext) + self.assertEqual(win.inch(0, 1) & chartext, + curses.ACS_HLINE & chartext) + self.assertEqual(win.inch(1, 0) & chartext, + curses.ACS_VLINE & chartext) + + def test_wrapper(self): + # wrapper() sets up curses, passes the screen to the callable along + # with extra arguments, returns its result and restores the terminal. + if not self.isatty: + self.skipTest('requires terminal') + + def body(stdscr, a, b): + self.assertIsInstance(stdscr, type(self.stdscr)) + self.assertIs(curses.isendwin(), False) + return a + b + + self.assertEqual(curses.wrapper(body, 2, 3), 5) + self.assertIs(curses.isendwin(), True) + # wrapper() left the screen ended; revive it so the per-test + # endwin() cleanup does not fail with ERR. + curses.doupdate() + + @requires_curses_func('is_term_resized') + def test_is_term_resized(self): + lines, cols = curses.LINES, curses.COLS + self.assertIs(curses.is_term_resized(lines, cols), False) + self.assertIs(curses.is_term_resized(lines-1, cols-1), True) + + @requires_curses_func('resize_term') + def test_resize_term(self): + curses.update_lines_cols() + lines, cols = curses.LINES, curses.COLS + new_lines = lines - 1 + new_cols = cols + 1 + curses.resize_term(new_lines, new_cols) + self.assertEqual(curses.LINES, new_lines) + self.assertEqual(curses.COLS, new_cols) + + curses.resize_term(lines, cols) + self.assertEqual(curses.LINES, lines) + self.assertEqual(curses.COLS, cols) + + with self.assertRaises(OverflowError): + curses.resize_term(35000, 1) + with self.assertRaises(OverflowError): + curses.resize_term(1, 35000) + # GH-120378: Overflow failure in resize_term() causes refresh to fail + tmp = curses.initscr() + tmp.erase() + + @requires_curses_func('resizeterm') + def test_resizeterm(self): + curses.update_lines_cols() + lines, cols = curses.LINES, curses.COLS + new_lines = lines - 1 + new_cols = cols + 1 + curses.resizeterm(new_lines, new_cols) + self.assertEqual(curses.LINES, new_lines) + self.assertEqual(curses.COLS, new_cols) + + curses.resizeterm(lines, cols) + self.assertEqual(curses.LINES, lines) + self.assertEqual(curses.COLS, cols) + + with self.assertRaises(OverflowError): + curses.resizeterm(35000, 1) + with self.assertRaises(OverflowError): + curses.resizeterm(1, 35000) + # GH-120378: Overflow failure in resizeterm() causes refresh to fail + tmp = curses.initscr() + tmp.erase() + + def test_ungetch(self): + curses.ungetch(b'A') + self.assertEqual(self.stdscr.getkey(), 'A') + curses.ungetch('B') + self.assertEqual(self.stdscr.getkey(), 'B') + curses.ungetch(67) + self.assertEqual(self.stdscr.getkey(), 'C') + + def test_issue6243(self): + curses.ungetch(1025) + self.stdscr.getkey() + + @requires_curses_func('unget_wch') + @unittest.skipIf(getattr(curses, 'ncurses_version', (99,)) < (5, 8), + "unget_wch is broken in ncurses 5.7 and earlier") + def test_unget_wch(self): + stdscr = self.stdscr + encoding = stdscr.encoding + # See _encodable for the character set, plus a non-BMP character. + for ch in ('a', '\xe9', '\xa4', '\u20ac', '\u0454', '\U0010FFFF'): + try: + ch.encode(encoding) + except UnicodeEncodeError: + continue + try: + curses.unget_wch(ch) + except Exception as err: + self.fail("unget_wch(%a) failed with encoding %s: %s" + % (ch, stdscr.encoding, err)) + read = stdscr.get_wch() + self.assertEqual(read, ch) + + code = ord(ch) + curses.unget_wch(code) + read = stdscr.get_wch() + self.assertEqual(read, ch) + + def test_encoding(self): + stdscr = self.stdscr + import codecs + encoding = stdscr.encoding + codecs.lookup(encoding) + with self.assertRaises(TypeError): + stdscr.encoding = 10 + stdscr.encoding = encoding + with self.assertRaises(TypeError): + del stdscr.encoding + + @unittest.skipIf(MISSING_C_DOCSTRINGS, + "Signature information for builtins requires docstrings") + def test_issue21088(self): + stdscr = self.stdscr + # + # http://bugs.python.org/issue21088 + # + # the bug: + # when converting curses.window.addch to Argument Clinic + # the first two parameters were switched. + + # if someday we can represent the signature of addch + # we will need to rewrite this test. + try: + signature = inspect.signature(stdscr.addch) + self.assertFalse(signature) + except ValueError: + # not generating a signature is fine. + pass + + # So. No signature for addch. + # But Argument Clinic gave us a human-readable equivalent + # as the first line of the docstring. So we parse that, + # and ensure that the parameters appear in the correct order. + # Since this is parsing output from Argument Clinic, we can + # be reasonably certain the generated parsing code will be + # correct too. + human_readable_signature = stdscr.addch.__doc__.split("\n")[0] + self.assertIn("[y, x,]", human_readable_signature) + + @requires_curses_window_meth('resize') + def test_issue13051(self): + win = curses.newwin(5, 15, 2, 5) + box = curses.textpad.Textbox(win, insert_mode=True) + lines, cols = win.getmaxyx() + win.resize(lines-2, cols-2) + # this may cause infinite recursion, leading to a RuntimeError + box._insert_printable_char('a') + + +class MiscTests(unittest.TestCase): + + @requires_curses_func('update_lines_cols') + def test_update_lines_cols(self): + curses.update_lines_cols() + lines, cols = curses.LINES, curses.COLS + curses.LINES = curses.COLS = 0 + curses.update_lines_cols() + self.assertEqual(curses.LINES, lines) + self.assertEqual(curses.COLS, cols) + + @requires_curses_func('ncurses_version') + def test_ncurses_version(self): + v = curses.ncurses_version + if verbose: + print(f'ncurses_version = {curses.ncurses_version}', flush=True) + self.assertIsInstance(v[:], tuple) + self.assertEqual(len(v), 3) + self.assertIsInstance(v[0], int) + self.assertIsInstance(v[1], int) + self.assertIsInstance(v[2], int) + self.assertIsInstance(v.major, int) + self.assertIsInstance(v.minor, int) + self.assertIsInstance(v.patch, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertEqual(v[2], v.patch) + self.assertGreaterEqual(v.major, 0) + self.assertGreaterEqual(v.minor, 0) + self.assertGreaterEqual(v.patch, 0) + + def test_has_extended_color_support(self): + r = curses.has_extended_color_support() + self.assertIsInstance(r, bool) + + +class TestAscii(unittest.TestCase): + + def test_controlnames(self): + for name in curses.ascii.controlnames: + self.assertHasAttr(curses.ascii, name) + + def test_ctypes(self): + def check(func, expected): + with self.subTest(ch=c, func=func): + self.assertEqual(func(i), expected) + self.assertEqual(func(c), expected) + + for i in range(256): + c = chr(i) + b = bytes([i]) + check(curses.ascii.isalnum, b.isalnum()) + check(curses.ascii.isalpha, b.isalpha()) + check(curses.ascii.isdigit, b.isdigit()) + check(curses.ascii.islower, b.islower()) + check(curses.ascii.isspace, b.isspace()) + check(curses.ascii.isupper, b.isupper()) + + check(curses.ascii.isascii, i < 128) + check(curses.ascii.ismeta, i >= 128) + check(curses.ascii.isctrl, i < 32) + check(curses.ascii.iscntrl, i < 32 or i == 127) + check(curses.ascii.isblank, c in ' \t') + check(curses.ascii.isgraph, 32 < i <= 126) + check(curses.ascii.isprint, 32 <= i <= 126) + check(curses.ascii.ispunct, c in string.punctuation) + check(curses.ascii.isxdigit, c in string.hexdigits) + + for i in (-2, -1, 256, sys.maxunicode, sys.maxunicode+1): + self.assertFalse(curses.ascii.isalnum(i)) + self.assertFalse(curses.ascii.isalpha(i)) + self.assertFalse(curses.ascii.isdigit(i)) + self.assertFalse(curses.ascii.islower(i)) + self.assertFalse(curses.ascii.isspace(i)) + self.assertFalse(curses.ascii.isupper(i)) + + self.assertFalse(curses.ascii.isascii(i)) + self.assertFalse(curses.ascii.isctrl(i)) + self.assertFalse(curses.ascii.iscntrl(i)) + self.assertFalse(curses.ascii.isblank(i)) + self.assertFalse(curses.ascii.isgraph(i)) + self.assertFalse(curses.ascii.isprint(i)) + self.assertFalse(curses.ascii.ispunct(i)) + self.assertFalse(curses.ascii.isxdigit(i)) + + self.assertFalse(curses.ascii.ismeta(-1)) + + def test_ascii(self): + ascii = curses.ascii.ascii + self.assertEqual(ascii('\xc1'), 'A') + self.assertEqual(ascii('A'), 'A') + self.assertEqual(ascii(ord('\xc1')), ord('A')) + + def test_ctrl(self): + ctrl = curses.ascii.ctrl + self.assertEqual(ctrl('J'), '\n') + self.assertEqual(ctrl('\n'), '\n') + self.assertEqual(ctrl('@'), '\0') + self.assertEqual(ctrl(ord('J')), ord('\n')) + + def test_alt(self): + alt = curses.ascii.alt + self.assertEqual(alt('\n'), '\x8a') + self.assertEqual(alt('A'), '\xc1') + self.assertEqual(alt(ord('A')), 0xc1) + + def test_unctrl(self): + unctrl = curses.ascii.unctrl + self.assertEqual(unctrl('a'), 'a') + self.assertEqual(unctrl('A'), 'A') + self.assertEqual(unctrl(';'), ';') + self.assertEqual(unctrl(' '), ' ') + self.assertEqual(unctrl('\x7f'), '^?') + self.assertEqual(unctrl('\n'), '^J') + self.assertEqual(unctrl('\0'), '^@') + self.assertEqual(unctrl(ord('A')), 'A') + self.assertEqual(unctrl(ord('\n')), '^J') + # Meta-bit characters + self.assertEqual(unctrl('\x8a'), '!^J') + self.assertEqual(unctrl('\xc1'), '!A') + self.assertEqual(unctrl(ord('\x8a')), '!^J') + self.assertEqual(unctrl(ord('\xc1')), '!A') + + +def lorem_ipsum(win): + text = [ + 'Lorem ipsum', + 'dolor sit amet,', + 'consectetur', + 'adipiscing elit,', + 'sed do eiusmod', + 'tempor incididunt', + 'ut labore et', + 'dolore magna', + 'aliqua.', + ] + maxy, maxx = win.getmaxyx() + for y, line in enumerate(text[:maxy]): + win.addstr(y, 0, line[:maxx - (y == maxy - 1)]) + + +class TextboxTest(unittest.TestCase): + def setUp(self): + self.mock_win = MagicMock(spec=curses.window) + self.mock_win.getyx.return_value = (1, 1) + self.mock_win.getmaxyx.return_value = (10, 20) + self.mock_win.encoding = 'utf-8' + # A non-blank cell so that _end_of_line() reports a full line: instr() + # backs the text reads, inch() the insert-mode shift. + self.mock_win.instr.return_value = b'x' + self.mock_win.inch.return_value = ord('x') + self.textbox = curses.textpad.Textbox(self.mock_win) + + def test_init(self): + """Test textbox initialization.""" + self.mock_win.reset_mock() + tb = curses.textpad.Textbox(self.mock_win) + self.mock_win.getmaxyx.assert_called_once_with() + self.mock_win.keypad.assert_called_once_with(1) + self.assertEqual(tb.insert_mode, False) + self.assertEqual(tb.stripspaces, 1) + self.assertIsNone(tb.lastcmd) + self.mock_win.reset_mock() + + def test_insert(self): + """Test inserting a printable character.""" + self.mock_win.reset_mock() + self.textbox.do_command(ord('a')) + self.mock_win.addch.assert_called_with(ord('a')) + self.textbox.do_command(ord('b')) + self.mock_win.addch.assert_called_with(ord('b')) + self.textbox.do_command(ord('c')) + self.mock_win.addch.assert_called_with(ord('c')) + self.mock_win.reset_mock() + + def test_delete(self): + """Test deleting a character.""" + self.mock_win.reset_mock() + self.textbox.do_command(curses.ascii.BS) + self.textbox.do_command(curses.KEY_BACKSPACE) + self.textbox.do_command(curses.ascii.DEL) + assert self.mock_win.delch.call_count == 3 + self.mock_win.reset_mock() + + def test_move_left(self): + """Test moving the cursor left.""" + self.mock_win.reset_mock() + self.textbox.do_command(curses.KEY_LEFT) + self.mock_win.move.assert_called_with(1, 0) + self.mock_win.reset_mock() + + def test_move_right(self): + """Test moving the cursor right.""" + self.mock_win.reset_mock() + self.textbox.do_command(curses.KEY_RIGHT) + self.mock_win.move.assert_called_with(1, 2) + self.mock_win.reset_mock() + + def test_move_left_and_right(self): + """Test moving the cursor left and then right.""" + self.mock_win.reset_mock() + self.textbox.do_command(curses.KEY_LEFT) + self.mock_win.move.assert_called_with(1, 0) + self.textbox.do_command(curses.KEY_RIGHT) + self.mock_win.move.assert_called_with(1, 2) + self.mock_win.reset_mock() + + def test_move_up(self): + """Test moving the cursor up.""" + self.mock_win.reset_mock() + self.textbox.do_command(curses.KEY_UP) + self.mock_win.move.assert_called_with(0, 1) + self.mock_win.reset_mock() + + def test_move_down(self): + """Test moving the cursor down.""" + self.mock_win.reset_mock() + self.textbox.do_command(curses.KEY_DOWN) + self.mock_win.move.assert_called_with(2, 1) + self.mock_win.reset_mock() + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index acbbf60ef37..17dee875991 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -27,11 +27,21 @@ import dataclasses # Needed for the string "dataclasses.InitVar[int]" to work as an annotation. from test import support -from test.support import import_helper +from test.support import cpython_only, import_helper # Just any custom exception we can catch. class CustomError(Exception): pass + +class TestImportTime(unittest.TestCase): + + @cpython_only + def test_lazy_import(self): + import_helper.ensure_lazy_imports( + "dataclasses", {"inspect", "re", "copy"} + ) + + class TestCase(unittest.TestCase): def test_no_fields(self): @dataclass @@ -71,7 +81,7 @@ def test_field_repr(self): expected_output = "Field(name='id',type=None," \ f"default=1,default_factory={MISSING!r}," \ "init=True,repr=False,hash=None," \ - "compare=True,metadata=mappingproxy({})," \ + "compare=True,metadata=frozendict()," \ f"kw_only={MISSING!r}," \ "doc='Docstring'," \ "_field_type=None)" @@ -985,7 +995,7 @@ class D: self.assertNotIn('x', D.__dict__) def test_missing_repr(self): - self.assertIn('MISSING_TYPE object', repr(MISSING)) + self.assertEqual(repr(MISSING), 'MISSING') def test_dont_include_other_annotations(self): @dataclass @@ -1693,17 +1703,24 @@ class GroupTuple: class GroupDict: id: int users: Dict[str, User] + @dataclass + class GroupFrozenDict: + id: int + users: frozendict[str, User] a = User('Alice', 1) b = User('Bob', 2) gl = GroupList(0, [a, b]) gt = GroupTuple(0, (a, b)) gd = GroupDict(0, {'first': a, 'second': b}) + gfd = GroupFrozenDict(0, frozendict({'first': a, 'second': b})) self.assertEqual(asdict(gl), {'id': 0, 'users': [{'name': 'Alice', 'id': 1}, {'name': 'Bob', 'id': 2}]}) self.assertEqual(asdict(gt), {'id': 0, 'users': ({'name': 'Alice', 'id': 1}, {'name': 'Bob', 'id': 2})}) - self.assertEqual(asdict(gd), {'id': 0, 'users': {'first': {'name': 'Alice', 'id': 1}, - 'second': {'name': 'Bob', 'id': 2}}}) + expected_dict = {'id': 0, 'users': {'first': {'name': 'Alice', 'id': 1}, + 'second': {'name': 'Bob', 'id': 2}}} + self.assertEqual(asdict(gd), expected_dict) + self.assertEqual(asdict(gfd), expected_dict) def test_helper_asdict_builtin_object_containers(self): @dataclass @@ -1884,14 +1901,21 @@ class GroupTuple: class GroupDict: id: int users: Dict[str, User] + @dataclass + class GroupFrozenDict: + id: int + users: frozendict[str, User] a = User('Alice', 1) b = User('Bob', 2) gl = GroupList(0, [a, b]) gt = GroupTuple(0, (a, b)) gd = GroupDict(0, {'first': a, 'second': b}) + gfd = GroupFrozenDict(0, frozendict({'first': a, 'second': b})) self.assertEqual(astuple(gl), (0, [('Alice', 1), ('Bob', 2)])) self.assertEqual(astuple(gt), (0, (('Alice', 1), ('Bob', 2)))) - self.assertEqual(astuple(gd), (0, {'first': ('Alice', 1), 'second': ('Bob', 2)})) + d = {'first': ('Alice', 1), 'second': ('Bob', 2)} + self.assertEqual(astuple(gd), (0, d)) + self.assertEqual(astuple(gfd), (0, frozendict(d))) def test_helper_astuple_builtin_object_containers(self): @dataclass @@ -2295,6 +2319,20 @@ class C: self.assertDocStrEqual(C.__doc__, "C()") + def test_docstring_slotted(self): + @dataclass(slots=True) + class C: + x: int + + self.assertDocStrEqual(C.__doc__, "C(x:int)") + + def test_docstring_recursive(self): + @dataclass() + class C: + x: list[C] + + self.assertDocStrEqual(C.__doc__, "C(x:list[test.test_dataclasses.TestDocString.test_docstring_recursive..C])") + def test_docstring_one_field(self): @dataclass class C: @@ -4444,10 +4482,17 @@ def test_classvar_module_level_import(self): from test.test_dataclasses import dataclass_module_1_str from test.test_dataclasses import dataclass_module_2 from test.test_dataclasses import dataclass_module_2_str - - for m in (dataclass_module_1, dataclass_module_1_str, - dataclass_module_2, dataclass_module_2_str, - ): + from test.test_dataclasses import dataclass_module_3 + from test.test_dataclasses import dataclass_module_3_str + from test.test_dataclasses import dataclass_module_4 + from test.test_dataclasses import dataclass_module_4_str + + for m in ( + dataclass_module_1, dataclass_module_1_str, + dataclass_module_2, dataclass_module_2_str, + dataclass_module_3, dataclass_module_3_str, + dataclass_module_4, dataclass_module_4_str, + ): with self.subTest(m=m): # There's a difference in how the ClassVars are # interpreted when using string annotations or @@ -4751,6 +4796,14 @@ def custom_dataclass(cls, *args, **kwargs): self.assertEqual(c.x, 10) self.assertEqual(c.__custom__, True) + def test_empty_annotation_string(self): + @dataclass + class DataclassWithEmptyTypeAnnotation: + x: "" + + c = DataclassWithEmptyTypeAnnotation(10) + self.assertEqual(c.x, 10) + class TestReplace(unittest.TestCase): def test(self): diff --git a/Lib/test/test_dataclasses/_types_proxy.py b/Lib/test/test_dataclasses/_types_proxy.py new file mode 100644 index 00000000000..f4aaeef7aec --- /dev/null +++ b/Lib/test/test_dataclasses/_types_proxy.py @@ -0,0 +1,8 @@ +# We need this to test a case when a type +# is imported via some other package, +# like ClassVar from typing_extensions instead of typing. +# https://github.com/python/cpython/issues/133956 +from typing import ClassVar +from dataclasses import InitVar + +__all__ = ["ClassVar", "InitVar"] diff --git a/Lib/test/test_dataclasses/dataclass_module_3.py b/Lib/test/test_dataclasses/dataclass_module_3.py new file mode 100644 index 00000000000..74abc091f35 --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_module_3.py @@ -0,0 +1,32 @@ +#from __future__ import annotations +USING_STRINGS = False + +# dataclass_module_3.py and dataclass_module_3_str.py are identical +# except only the latter uses string annotations. + +from dataclasses import dataclass +import test.test_dataclasses._types_proxy as tp + +T_CV2 = tp.ClassVar[int] +T_CV3 = tp.ClassVar + +T_IV2 = tp.InitVar[int] +T_IV3 = tp.InitVar + +@dataclass +class CV: + T_CV4 = tp.ClassVar + cv0: tp.ClassVar[int] = 20 + cv1: tp.ClassVar = 30 + cv2: T_CV2 + cv3: T_CV3 + not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar. + +@dataclass +class IV: + T_IV4 = tp.InitVar + iv0: tp.InitVar[int] + iv1: tp.InitVar + iv2: T_IV2 + iv3: T_IV3 + not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar. diff --git a/Lib/test/test_dataclasses/dataclass_module_3_str.py b/Lib/test/test_dataclasses/dataclass_module_3_str.py new file mode 100644 index 00000000000..49e5fca6183 --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_module_3_str.py @@ -0,0 +1,32 @@ +from __future__ import annotations +USING_STRINGS = True + +# dataclass_module_3.py and dataclass_module_3_str.py are identical +# except only the latter uses string annotations. + +from dataclasses import dataclass +import test.test_dataclasses._types_proxy as tp + +T_CV2 = tp.ClassVar[int] +T_CV3 = tp.ClassVar + +T_IV2 = tp.InitVar[int] +T_IV3 = tp.InitVar + +@dataclass +class CV: + T_CV4 = tp.ClassVar + cv0: tp.ClassVar[int] = 20 + cv1: tp.ClassVar = 30 + cv2: T_CV2 + cv3: T_CV3 + not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar. + +@dataclass +class IV: + T_IV4 = tp.InitVar + iv0: tp.InitVar[int] + iv1: tp.InitVar + iv2: T_IV2 + iv3: T_IV3 + not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar. diff --git a/Lib/test/test_dataclasses/dataclass_module_4.py b/Lib/test/test_dataclasses/dataclass_module_4.py new file mode 100644 index 00000000000..7e0c8a18356 --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_module_4.py @@ -0,0 +1,38 @@ +#from __future__ import annotations +USING_STRINGS = False + +# dataclass_module_4.py and dataclass_module_4_str.py are identical +# except only the latter uses string annotations. + +from dataclasses import dataclass +import dataclasses +import typing + +class TypingProxy: + class Nested: + ClassVar = typing.ClassVar + InitVar = dataclasses.InitVar + +T_CV2 = TypingProxy.Nested.ClassVar[int] +T_CV3 = TypingProxy.Nested.ClassVar + +T_IV2 = TypingProxy.Nested.InitVar[int] +T_IV3 = TypingProxy.Nested.InitVar + +@dataclass +class CV: + T_CV4 = TypingProxy.Nested.ClassVar + cv0: TypingProxy.Nested.ClassVar[int] = 20 + cv1: TypingProxy.Nested.ClassVar = 30 + cv2: T_CV2 + cv3: T_CV3 + not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar. + +@dataclass +class IV: + T_IV4 = TypingProxy.Nested.InitVar + iv0: TypingProxy.Nested.InitVar[int] + iv1: TypingProxy.Nested.InitVar + iv2: T_IV2 + iv3: T_IV3 + not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar. diff --git a/Lib/test/test_dataclasses/dataclass_module_4_str.py b/Lib/test/test_dataclasses/dataclass_module_4_str.py new file mode 100644 index 00000000000..876f3dcf7c8 --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_module_4_str.py @@ -0,0 +1,38 @@ +from __future__ import annotations +USING_STRINGS = True + +# dataclass_module_4.py and dataclass_module_4_str.py are identical +# except only the latter uses string annotations. + +from dataclasses import dataclass +import dataclasses +import typing + +class TypingProxy: + class Nested: + ClassVar = typing.ClassVar + InitVar = dataclasses.InitVar + +T_CV2 = TypingProxy.Nested.ClassVar[int] +T_CV3 = TypingProxy.Nested.ClassVar + +T_IV2 = TypingProxy.Nested.InitVar[int] +T_IV3 = TypingProxy.Nested.InitVar + +@dataclass +class CV: + T_CV4 = TypingProxy.Nested.ClassVar + cv0: TypingProxy.Nested.ClassVar[int] = 20 + cv1: TypingProxy.Nested.ClassVar = 30 + cv2: T_CV2 + cv3: T_CV3 + not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar. + +@dataclass +class IV: + T_IV4 = TypingProxy.Nested.InitVar + iv0: TypingProxy.Nested.InitVar[int] + iv1: TypingProxy.Nested.InitVar + iv2: T_IV2 + iv3: T_IV3 + not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar. diff --git a/Lib/test/test_dbm_dumb.py b/Lib/test/test_dbm_dumb.py index 672f9092207..d977a81876d 100644 --- a/Lib/test/test_dbm_dumb.py +++ b/Lib/test/test_dbm_dumb.py @@ -114,6 +114,16 @@ def test_write_write_read(self): with contextlib.closing(dumbdbm.open(_fname)) as f: self.assertEqual(f[b'1'], b'hello2') + def test_reorganize_persists_changed_offsets(self): + with dumbdbm.open(_fname, 'n') as f: + f[b'deleted'] = b'x' + f[b'retained'] = b'value' + del f[b'deleted'] + f.reorganize() + + with dumbdbm.open(_fname, 'r') as f: + self.assertEqual(f[b'retained'], b'value') + def test_str_read(self): self.init_db() with contextlib.closing(dumbdbm.open(_fname, 'r')) as f: diff --git a/Lib/test/test_dbm_gnu.py b/Lib/test/test_dbm_gnu.py new file mode 100644 index 00000000000..66268c42a30 --- /dev/null +++ b/Lib/test/test_dbm_gnu.py @@ -0,0 +1,222 @@ +import os +import unittest +from test import support +from test.support import cpython_only, import_helper +from test.support.os_helper import (TESTFN, TESTFN_NONASCII, FakePath, + create_empty_file, temp_dir, unlink) + +gdbm = import_helper.import_module("dbm.gnu") # skip if not supported + +filename = TESTFN + +class TestGdbm(unittest.TestCase): + @staticmethod + def setUpClass(): + if support.verbose: + try: + from _gdbm import _GDBM_VERSION as version + except ImportError: + pass + else: + print(f"gdbm version: {version}") + + def setUp(self): + self.g = None + + def tearDown(self): + if self.g is not None: + self.g.close() + unlink(filename) + + @cpython_only + def test_disallow_instantiation(self): + # Ensure that the type disallows instantiation (bpo-43916) + self.g = gdbm.open(filename, 'c') + support.check_disallow_instantiation(self, type(self.g)) + + def test_key_methods(self): + self.g = gdbm.open(filename, 'c') + self.assertEqual(self.g.keys(), []) + self.g['a'] = 'b' + self.g['12345678910'] = '019237410982340912840198242' + self.g[b'bytes'] = b'data' + key_set = set(self.g.keys()) + self.assertEqual(key_set, set([b'a', b'bytes', b'12345678910'])) + self.assertIn('a', self.g) + self.assertIn(b'a', self.g) + self.assertEqual(self.g[b'bytes'], b'data') + key = self.g.firstkey() + while key: + self.assertIn(key, key_set) + key_set.remove(key) + key = self.g.nextkey(key) + # get() and setdefault() work as in the dict interface + self.assertEqual(self.g.get(b'a'), b'b') + self.assertIsNone(self.g.get(b'xxx')) + self.assertEqual(self.g.get(b'xxx', b'foo'), b'foo') + with self.assertRaises(KeyError): + self.g['xxx'] + self.assertEqual(self.g.setdefault(b'xxx', b'foo'), b'foo') + self.assertEqual(self.g[b'xxx'], b'foo') + + def test_error_conditions(self): + # Try to open a non-existent database. + unlink(filename) + self.assertRaises(gdbm.error, gdbm.open, filename, 'r') + # Try to access a closed database. + self.g = gdbm.open(filename, 'c') + self.g.close() + self.assertRaises(gdbm.error, lambda: self.g['a']) + # try pass an invalid open flag + self.assertRaises(gdbm.error, lambda: gdbm.open(filename, 'rx').close()) + + def test_flags(self): + # Test the flag parameter open() by trying all supported flag modes. + all = set(gdbm.open_flags) + # Test standard flags (presumably "crwn"). + modes = all - set('fsu') + for mode in sorted(modes): # put "c" mode first + self.g = gdbm.open(filename, mode) + self.g.close() + + # Test additional flags (presumably "fsu"). + flags = all - set('crwn') + for mode in modes: + for flag in flags: + self.g = gdbm.open(filename, mode + flag) + self.g.close() + + def test_reorganize(self): + self.g = gdbm.open(filename, 'c') + size0 = os.path.getsize(filename) + + # bpo-33901: on macOS with gdbm 1.15, an empty database uses 16 MiB + # and adding an entry of 10,000 B has no effect on the file size. + # Add size0 bytes to make sure that the file size changes. + value_size = max(size0, 10000) + self.g['x'] = 'x' * value_size + size1 = os.path.getsize(filename) + self.assertGreater(size1, size0) + + del self.g['x'] + # 'size' is supposed to be the same even after deleting an entry. + self.assertEqual(os.path.getsize(filename), size1) + + self.g.reorganize() + size2 = os.path.getsize(filename) + self.assertLess(size2, size1) + self.assertGreaterEqual(size2, size0) + + def test_context_manager(self): + with gdbm.open(filename, 'c') as db: + db["gdbm context manager"] = "context manager" + + with gdbm.open(filename, 'r') as db: + self.assertEqual(list(db.keys()), [b"gdbm context manager"]) + + with self.assertRaises(gdbm.error) as cm: + db.keys() + self.assertEqual(str(cm.exception), + "GDBM object has already been closed") + + def test_bool_empty(self): + with gdbm.open(filename, 'c') as db: + self.assertFalse(bool(db)) + + def test_bool_not_empty(self): + with gdbm.open(filename, 'c') as db: + db['a'] = 'b' + self.assertTrue(bool(db)) + + def test_bool_on_closed_db_raises(self): + with gdbm.open(filename, 'c') as db: + db['a'] = 'b' + self.assertRaises(gdbm.error, bool, db) + + def test_bytes(self): + with gdbm.open(filename, 'c') as db: + db[b'bytes key \xbd'] = b'bytes value \xbd' + with gdbm.open(filename, 'r') as db: + self.assertEqual(list(db.keys()), [b'bytes key \xbd']) + self.assertTrue(b'bytes key \xbd' in db) + self.assertEqual(db[b'bytes key \xbd'], b'bytes value \xbd') + + def test_unicode(self): + with gdbm.open(filename, 'c') as db: + db['Unicode key \U0001f40d'] = 'Unicode value \U0001f40d' + with gdbm.open(filename, 'r') as db: + self.assertEqual(list(db.keys()), ['Unicode key \U0001f40d'.encode()]) + self.assertTrue('Unicode key \U0001f40d'.encode() in db) + self.assertTrue('Unicode key \U0001f40d' in db) + self.assertEqual(db['Unicode key \U0001f40d'.encode()], + 'Unicode value \U0001f40d'.encode()) + self.assertEqual(db['Unicode key \U0001f40d'], + 'Unicode value \U0001f40d'.encode()) + + def test_write_readonly_file(self): + with gdbm.open(filename, 'c') as db: + db[b'bytes key'] = b'bytes value' + with gdbm.open(filename, 'r') as db: + with self.assertRaises(gdbm.error): + del db[b'not exist key'] + with self.assertRaises(gdbm.error): + del db[b'bytes key'] + with self.assertRaises(gdbm.error): + db[b'not exist key'] = b'not exist value' + + @unittest.skipUnless(TESTFN_NONASCII, + 'requires OS support of non-ASCII encodings') + def test_nonascii_filename(self): + filename = TESTFN_NONASCII + self.addCleanup(unlink, filename) + with gdbm.open(filename, 'c') as db: + db[b'key'] = b'value' + self.assertTrue(os.path.exists(filename)) + with gdbm.open(filename, 'r') as db: + self.assertEqual(list(db.keys()), [b'key']) + self.assertTrue(b'key' in db) + self.assertEqual(db[b'key'], b'value') + + def test_nonexisting_file(self): + nonexisting_file = 'nonexisting-file' + with self.assertRaises(gdbm.error) as cm: + gdbm.open(nonexisting_file) + self.assertIn(nonexisting_file, str(cm.exception)) + self.assertEqual(cm.exception.filename, nonexisting_file) + + def test_open_with_pathlib_path(self): + gdbm.open(FakePath(filename), "c").close() + + def test_open_with_bytes_path(self): + gdbm.open(os.fsencode(filename), "c").close() + + def test_open_with_pathlib_bytes_path(self): + gdbm.open(FakePath(os.fsencode(filename)), "c").close() + + def test_clear(self): + kvs = [('foo', 'bar'), ('1234', '5678')] + with gdbm.open(filename, 'c') as db: + for k, v in kvs: + db[k] = v + self.assertIn(k, db) + self.assertEqual(len(db), len(kvs)) + + db.clear() + for k, v in kvs: + self.assertNotIn(k, db) + self.assertEqual(len(db), 0) + + @support.run_with_locale( + 'LC_ALL', + 'fr_FR.iso88591', 'ja_JP.sjis', 'zh_CN.gbk', + 'fr_FR.utf8', 'en_US.utf8', + '', + ) + def test_localized_error(self): + with temp_dir() as d: + create_empty_file(os.path.join(d, 'test')) + self.assertRaises(gdbm.error, gdbm.open, filename, 'r') + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_dbm_ndbm.py b/Lib/test/test_dbm_ndbm.py new file mode 100644 index 00000000000..e0f31c9a9a3 --- /dev/null +++ b/Lib/test/test_dbm_ndbm.py @@ -0,0 +1,165 @@ +from test.support import import_helper +from test.support import os_helper +import_helper.import_module("dbm.ndbm") #skip if not supported +import os +import unittest +import dbm.ndbm +from dbm.ndbm import error + +class DbmTestCase(unittest.TestCase): + + def setUp(self): + self.filename = os_helper.TESTFN + self.d = dbm.ndbm.open(self.filename, 'c') + self.d.close() + + def tearDown(self): + for suffix in ['', '.pag', '.dir', '.db']: + os_helper.unlink(self.filename + suffix) + + def test_keys(self): + self.d = dbm.ndbm.open(self.filename, 'c') + self.assertEqual(self.d.keys(), []) + self.d['a'] = 'b' + self.d[b'bytes'] = b'data' + self.d['12345678910'] = '019237410982340912840198242' + self.d.keys() + self.assertIn('a', self.d) + self.assertIn(b'a', self.d) + self.assertEqual(self.d[b'bytes'], b'data') + # get() and setdefault() work as in the dict interface + self.assertEqual(self.d.get(b'a'), b'b') + self.assertIsNone(self.d.get(b'xxx')) + self.assertEqual(self.d.get(b'xxx', b'foo'), b'foo') + with self.assertRaises(KeyError): + self.d['xxx'] + self.assertEqual(self.d.setdefault(b'xxx', b'foo'), b'foo') + self.assertEqual(self.d[b'xxx'], b'foo') + self.d.close() + + def test_empty_value(self): + if dbm.ndbm.library == 'Berkeley DB': + self.skipTest("Berkeley DB doesn't distinguish the empty value " + "from the absent one") + self.d = dbm.ndbm.open(self.filename, 'c') + self.assertEqual(self.d.keys(), []) + self.d['empty'] = '' + self.assertEqual(self.d.keys(), [b'empty']) + self.assertIn(b'empty', self.d) + self.assertEqual(self.d[b'empty'], b'') + self.assertEqual(self.d.get(b'empty'), b'') + self.assertEqual(self.d.setdefault(b'empty'), b'') + self.d.close() + + def test_modes(self): + for mode in ['r', 'rw', 'w', 'n']: + try: + self.d = dbm.ndbm.open(self.filename, mode) + self.d.close() + except error: + self.fail() + + def test_context_manager(self): + with dbm.ndbm.open(self.filename, 'c') as db: + db["ndbm context manager"] = "context manager" + + with dbm.ndbm.open(self.filename, 'r') as db: + self.assertEqual(list(db.keys()), [b"ndbm context manager"]) + + with self.assertRaises(dbm.ndbm.error) as cm: + db.keys() + self.assertEqual(str(cm.exception), + "DBM object has already been closed") + + def test_bytes(self): + with dbm.ndbm.open(self.filename, 'c') as db: + db[b'bytes key \xbd'] = b'bytes value \xbd' + with dbm.ndbm.open(self.filename, 'r') as db: + self.assertEqual(list(db.keys()), [b'bytes key \xbd']) + self.assertTrue(b'bytes key \xbd' in db) + self.assertEqual(db[b'bytes key \xbd'], b'bytes value \xbd') + + def test_unicode(self): + with dbm.ndbm.open(self.filename, 'c') as db: + db['Unicode key \U0001f40d'] = 'Unicode value \U0001f40d' + with dbm.ndbm.open(self.filename, 'r') as db: + self.assertEqual(list(db.keys()), ['Unicode key \U0001f40d'.encode()]) + self.assertTrue('Unicode key \U0001f40d'.encode() in db) + self.assertTrue('Unicode key \U0001f40d' in db) + self.assertEqual(db['Unicode key \U0001f40d'.encode()], + 'Unicode value \U0001f40d'.encode()) + self.assertEqual(db['Unicode key \U0001f40d'], + 'Unicode value \U0001f40d'.encode()) + + def test_write_readonly_file(self): + with dbm.ndbm.open(self.filename, 'c') as db: + db[b'bytes key'] = b'bytes value' + with dbm.ndbm.open(self.filename, 'r') as db: + with self.assertRaises(error): + del db[b'not exist key'] + with self.assertRaises(error): + del db[b'bytes key'] + with self.assertRaises(error): + db[b'not exist key'] = b'not exist value' + + @unittest.skipUnless(os_helper.TESTFN_NONASCII, + 'requires OS support of non-ASCII encodings') + def test_nonascii_filename(self): + filename = os_helper.TESTFN_NONASCII + for suffix in ['', '.pag', '.dir', '.db']: + self.addCleanup(os_helper.unlink, filename + suffix) + with dbm.ndbm.open(filename, 'c') as db: + db[b'key'] = b'value' + self.assertTrue(any(os.path.exists(filename + suffix) + for suffix in ['', '.pag', '.dir', '.db'])) + with dbm.ndbm.open(filename, 'r') as db: + self.assertEqual(list(db.keys()), [b'key']) + self.assertTrue(b'key' in db) + self.assertEqual(db[b'key'], b'value') + + def test_nonexisting_file(self): + nonexisting_file = 'nonexisting-file' + with self.assertRaises(dbm.ndbm.error) as cm: + dbm.ndbm.open(nonexisting_file) + self.assertIn(nonexisting_file, str(cm.exception)) + self.assertEqual(cm.exception.filename, nonexisting_file) + + def test_open_with_pathlib_path(self): + dbm.ndbm.open(os_helper.FakePath(self.filename), "c").close() + + def test_open_with_bytes_path(self): + dbm.ndbm.open(os.fsencode(self.filename), "c").close() + + def test_open_with_pathlib_bytes_path(self): + dbm.ndbm.open(os_helper.FakePath(os.fsencode(self.filename)), "c").close() + + def test_bool_empty(self): + with dbm.ndbm.open(self.filename, 'c') as db: + self.assertFalse(bool(db)) + + def test_bool_not_empty(self): + with dbm.ndbm.open(self.filename, 'c') as db: + db['a'] = 'b' + self.assertTrue(bool(db)) + + def test_bool_on_closed_db_raises(self): + with dbm.ndbm.open(self.filename, 'c') as db: + db['a'] = 'b' + self.assertRaises(dbm.ndbm.error, bool, db) + + def test_clear(self): + kvs = [('foo', 'bar'), ('1234', '5678')] + with dbm.ndbm.open(self.filename, 'c') as db: + for k, v in kvs: + db[k] = v + self.assertIn(k, db) + self.assertEqual(len(db), len(kvs)) + + db.clear() + for k, v in kvs: + self.assertNotIn(k, db) + self.assertEqual(len(db), 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_defaultdict.py b/Lib/test/test_defaultdict.py index a193eb10f16..cc78f01e3e2 100644 --- a/Lib/test/test_defaultdict.py +++ b/Lib/test/test_defaultdict.py @@ -186,6 +186,18 @@ def test_union(self): with self.assertRaises(TypeError): i |= None + # frozendict + i_fd = i | frozendict(s) + self.assertIs(type(i_fd), defaultdict) + self.assertIs(i_fd.default_factory, int) + self.assertDictEqual(i_fd, {1: "one", 2: 2, 0: "zero"}) + self.assertEqual(list(i_fd), [1, 2, 0]) + + fd_i = frozendict(s) | i + self.assertIs(type(fd_i), frozendict) + self.assertEqual(fd_i, {1: "one", 2: 2, 0: "zero"}) + self.assertEqual(list(fd_i), [0, 1, 2]) + def test_factory_conflict_with_set_value(self): key = "conflict_test" count = 0 diff --git a/Lib/test/test_dictviews.py b/Lib/test/test_dictviews.py index 024d20a63a1..b95840f8546 100644 --- a/Lib/test/test_dictviews.py +++ b/Lib/test/test_dictviews.py @@ -3,7 +3,7 @@ import pickle import unittest from test.support import (skip_emscripten_stack_overflow, - skip_wasi_stack_overflow, run_with_limited_c_stack, + skip_wasi_stack_overflow, skip_if_huge_c_stack, exceeds_recursion_limit) class DictSetTest(unittest.TestCase): @@ -280,7 +280,7 @@ def test_recursive_repr(self): self.assertIsInstance(r, str) @unittest.skip("TODO: RUSTPYTHON; segfault") - @run_with_limited_c_stack() + @skip_if_huge_c_stack() @skip_wasi_stack_overflow() @skip_emscripten_stack_overflow() def test_deeply_nested_repr(self): diff --git a/Lib/test/test_difflib.py b/Lib/test/test_difflib.py index 518b8c5278e..4f99b7c91c6 100644 --- a/Lib/test/test_difflib.py +++ b/Lib/test/test_difflib.py @@ -1,5 +1,7 @@ import difflib -from test.support import findfile +from test import support +from test.support import findfile, force_colorized +from test.support.import_helper import ensure_lazy_imports import unittest import doctest import sys @@ -265,21 +267,21 @@ def test_make_file_default_charset(self): html_diff = difflib.HtmlDiff() output = html_diff.make_file(patch914575_from1.splitlines(), patch914575_to1.splitlines()) - self.assertIn('content="text/html; charset=utf-8"', output) + self.assertIn('charset="utf-8"', output) def test_make_file_iso88591_charset(self): html_diff = difflib.HtmlDiff() output = html_diff.make_file(patch914575_from1.splitlines(), patch914575_to1.splitlines(), charset='iso-8859-1') - self.assertIn('content="text/html; charset=iso-8859-1"', output) + self.assertIn('charset="iso-8859-1"', output) def test_make_file_usascii_charset_with_nonascii_input(self): html_diff = difflib.HtmlDiff() output = html_diff.make_file(patch914575_nonascii_from1.splitlines(), patch914575_nonascii_to1.splitlines(), charset='us-ascii') - self.assertIn('content="text/html; charset=us-ascii"', output) + self.assertIn('charset="us-ascii"', output) self.assertIn('ımplıcıt', output) def test_strip_trailing_newlines_before_diff(self): @@ -397,6 +399,22 @@ def test_range_format_context(self): self.assertEqual(fmt(3,6), '4,6') self.assertEqual(fmt(0,0), '0') + @force_colorized + def test_unified_diff_colored_output(self): + args = [['one', 'three'], ['two', 'three'], 'Original', 'Current', + '2005-01-26 23:30:50', '2010-04-02 10:20:52'] + actual = list(difflib.unified_diff(*args, lineterm='', color=True)) + + expect = [ + "\033[1m--- Original\t2005-01-26 23:30:50\033[0m", + "\033[1m+++ Current\t2010-04-02 10:20:52\033[0m", + "\033[36m@@ -1,2 +1,2 @@\033[0m", + "\033[31m-one\033[0m", + "\033[32m+two\033[0m", + "\033[0m three\033[0m", + ] + self.assertEqual(expect, actual) + class TestBytes(unittest.TestCase): # don't really care about the content of the output, just the fact @@ -651,6 +669,12 @@ def setUpModule(): difflib.HtmlDiff._default_prefix = 0 +class LazyImportTest(unittest.TestCase): + @support.cpython_only + def test_lazy_import(self): + ensure_lazy_imports("difflib", {"_colorize"}) + + def load_tests(loader, tests, pattern): tests.addTest(doctest.DocTestSuite(difflib)) return tests diff --git a/Lib/test/test_difflib_expect.html b/Lib/test/test_difflib_expect.html index 9f33a9e9c9c..240e2c33368 100644 --- a/Lib/test/test_difflib_expect.html +++ b/Lib/test/test_difflib_expect.html @@ -1,22 +1,42 @@ - - - - + + - - - @@ -37,11 +59,11 @@ - - - - - + + + + + @@ -53,11 +75,11 @@ - - - - - + + + + + @@ -69,11 +91,11 @@ - - - - - + + + + + @@ -111,11 +133,11 @@

    Context (first diff within numlines=5(default))

    - - - - - + + + + + @@ -128,11 +150,11 @@

    Context (first diff within numlines=5(default))

    - - - - - + + + + + @@ -145,11 +167,11 @@

    Context (first diff within numlines=5(default))

    - - - - - + + + + + @@ -170,11 +192,11 @@

    Context (first diff after numlines=5(default))

    - - - - - + + + + + @@ -187,11 +209,11 @@

    Context (first diff after numlines=5(default))

    - - - - - + + + + + @@ -204,11 +226,11 @@

    Context (first diff after numlines=5(default))

    - - - - - + + + + + @@ -225,11 +247,11 @@

    Context (numlines=6)

    - - - - - + + + + + @@ -241,11 +263,11 @@

    Context (numlines=6)

    - - - - - + + + + + @@ -257,11 +279,11 @@

    Context (numlines=6)

    - - - - - + + + + + @@ -278,25 +300,25 @@

    Context (numlines=0)

    - - - - - + + + + + - - - - - + + + + + - - - - - + + + + +

    from
    to
    f1f1
    n2   1. Beautiful is beTTer than ugly.n2   1. Beautiful is better than ugly.
    3   2. Explicit is better than implicit.
    4   3. Simple is better than complex.3   3.   Simple is better than complex.
    5   4. Complex is better than complicated.4   4. Complicated is better than complex.
    5   5. Flat is better than nested.
    n2   1. Beautiful is beTTer than ugly.n2   1. Beautiful is better than ugly.
    3   2. Explicit is better than implicit.
    4   3. Simple is better than complex.3   3.   Simple is better than complex.
    5   4. Complex is better than complicated.4   4. Complicated is better than complex.
    5   5. Flat is better than nested.
    61236123
    71237123
    81238123
    1412314123
    1512315123
    1616
    n17   1. Beautiful is beTTer than ugly.n17   1. Beautiful is better than ugly.
    18   2. Explicit is better than implicit.
    19   3. Simple is better than complex.18   3.   Simple is better than complex.
    20   4. Complex is better than complicated.19   4. Complicated is better than complex.
    20   5. Flat is better than nested.
    n17   1. Beautiful is beTTer than ugly.n17   1. Beautiful is better than ugly.
    18   2. Explicit is better than implicit.
    19   3. Simple is better than complex.18   3.   Simple is better than complex.
    20   4. Complex is better than complicated.19   4. Complicated is better than complex.
    20   5. Flat is better than nested.
    2112321123
    2212322123
    2312323123
    2912329123
    3012330123
    3131
    t32   1. Beautiful is beTTer than ugly.t32   1. Beautiful is better than ugly.
    33   2. Explicit is better than implicit.
    34   3. Simple is better than complex.33   3.   Simple is better than complex.
    35   4. Complex is better than complicated.34   4. Complicated is better than complex.
    35   5. Flat is better than nested.
    t32   1. Beautiful is beTTer than ugly.t32   1. Beautiful is better than ugly.
    33   2. Explicit is better than implicit.
    34   3. Simple is better than complex.33   3.   Simple is better than complex.
    35   4. Complex is better than complicated.34   4. Complicated is better than complex.
    35   5. Flat is better than nested.
    3612336123
    3712337123
    3812338123

    from
    to
    f1f1
    n2   1. Beautiful is beTTer than ugly.n2   1. Beautiful is better than ugly.
    3   2. Explicit is better than implicit.
    4   3. Simple is better than complex.3   3.   Simple is better than complex.
    5   4. Complex is better than complicated.4   4. Complicated is better than complex.
    5   5. Flat is better than nested.
    n2   1. Beautiful is beTTer than ugly.n2   1. Beautiful is better than ugly.
    3   2. Explicit is better than implicit.
    4   3. Simple is better than complex.3   3.   Simple is better than complex.
    5   4. Complex is better than complicated.4   4. Complicated is better than complex.
    5   5. Flat is better than nested.
    61236123
    71237123
    81238123
    1412314123
    1512315123
    1616
    n17   1. Beautiful is beTTer than ugly.n17   1. Beautiful is better than ugly.
    18   2. Explicit is better than implicit.
    19   3. Simple is better than complex.18   3.   Simple is better than complex.
    20   4. Complex is better than complicated.19   4. Complicated is better than complex.
    20   5. Flat is better than nested.
    n17   1. Beautiful is beTTer than ugly.n17   1. Beautiful is better than ugly.
    18   2. Explicit is better than implicit.
    19   3. Simple is better than complex.18   3.   Simple is better than complex.
    20   4. Complex is better than complicated.19   4. Complicated is better than complex.
    20   5. Flat is better than nested.
    2112321123
    2212322123
    2312323123
    2912329123
    3012330123
    3131
    t32   1. Beautiful is beTTer than ugly.t32   1. Beautiful is better than ugly.
    33   2. Explicit is better than implicit.
    34   3. Simple is better than complex.33   3.   Simple is better than complex.
    35   4. Complex is better than complicated.34   4. Complicated is better than complex.
    35   5. Flat is better than nested.
    t32   1. Beautiful is beTTer than ugly.t32   1. Beautiful is better than ugly.
    33   2. Explicit is better than implicit.
    34   3. Simple is better than complex.33   3.   Simple is better than complex.
    35   4. Complex is better than complicated.34   4. Complicated is better than complex.
    35   5. Flat is better than nested.
    3612336123
    3712337123
    3812338123
    94569456
    1045610456
    1111
    n12   1. Beautiful is beTTer than ugly.n12   1. Beautiful is better than ugly.
    13   2. Explicit is better than implicit.
    14   3. Simple is better than complex.13   3.   Simple is better than complex.
    15   4. Complex is better than complicated.14   4. Complicated is better than complex.
    15   5. Flat is better than nested.
    n12   1. Beautiful is beTTer than ugly.n12   1. Beautiful is better than ugly.
    13   2. Explicit is better than implicit.
    14   3. Simple is better than complex.13   3.   Simple is better than complex.
    15   4. Complex is better than complicated.14   4. Complicated is better than complex.
    15   5. Flat is better than nested.
    1612316123
    1712317123
    1812318123
    2412324123
    2512325123
    2626
    n27   1. Beautiful is beTTer than ugly.n27   1. Beautiful is better than ugly.
    28   2. Explicit is better than implicit.
    29   3. Simple is better than complex.28   3.   Simple is better than complex.
    30   4. Complex is better than complicated.29   4. Complicated is better than complex.
    30   5. Flat is better than nested.
    n27   1. Beautiful is beTTer than ugly.n27   1. Beautiful is better than ugly.
    28   2. Explicit is better than implicit.
    29   3. Simple is better than complex.28   3.   Simple is better than complex.
    30   4. Complex is better than complicated.29   4. Complicated is better than complex.
    30   5. Flat is better than nested.
    3112331123
    3212332123
    3312333123
    3912339123
    4012340123
    4141
    t42   1. Beautiful is beTTer than ugly.t42   1. Beautiful is better than ugly.
    43   2. Explicit is better than implicit.
    44   3. Simple is better than complex.43   3.   Simple is better than complex.
    45   4. Complex is better than complicated.44   4. Complicated is better than complex.
    45   5. Flat is better than nested.
    t42   1. Beautiful is beTTer than ugly.t42   1. Beautiful is better than ugly.
    43   2. Explicit is better than implicit.
    44   3. Simple is better than complex.43   3.   Simple is better than complex.
    45   4. Complex is better than complicated.44   4. Complicated is better than complex.
    45   5. Flat is better than nested.
    4612346123
    4712347123
    4812348123

    from
    to
    f1f1
    n2   1. Beautiful is beTTer than ugly.n2   1. Beautiful is better than ugly.
    3   2. Explicit is better than implicit.
    4   3. Simple is better than complex.3   3.   Simple is better than complex.
    5   4. Complex is better than complicated.4   4. Complicated is better than complex.
    5   5. Flat is better than nested.
    n2   1. Beautiful is beTTer than ugly.n2   1. Beautiful is better than ugly.
    3   2. Explicit is better than implicit.
    4   3. Simple is better than complex.3   3.   Simple is better than complex.
    5   4. Complex is better than complicated.4   4. Complicated is better than complex.
    5   5. Flat is better than nested.
    61236123
    71237123
    81238123
    1412314123
    1512315123
    1616
    n17   1. Beautiful is beTTer than ugly.n17   1. Beautiful is better than ugly.
    18   2. Explicit is better than implicit.
    19   3. Simple is better than complex.18   3.   Simple is better than complex.
    20   4. Complex is better than complicated.19   4. Complicated is better than complex.
    20   5. Flat is better than nested.
    n17   1. Beautiful is beTTer than ugly.n17   1. Beautiful is better than ugly.
    18   2. Explicit is better than implicit.
    19   3. Simple is better than complex.18   3.   Simple is better than complex.
    20   4. Complex is better than complicated.19   4. Complicated is better than complex.
    20   5. Flat is better than nested.
    2112321123
    2212322123
    2312323123
    2912329123
    3012330123
    3131
    t32   1. Beautiful is beTTer than ugly.t32   1. Beautiful is better than ugly.
    33   2. Explicit is better than implicit.
    34   3. Simple is better than complex.33   3.   Simple is better than complex.
    35   4. Complex is better than complicated.34   4. Complicated is better than complex.
    35   5. Flat is better than nested.
    t32   1. Beautiful is beTTer than ugly.t32   1. Beautiful is better than ugly.
    33   2. Explicit is better than implicit.
    34   3. Simple is better than complex.33   3.   Simple is better than complex.
    35   4. Complex is better than complicated.34   4. Complicated is better than complex.
    35   5. Flat is better than nested.
    3612336123
    3712337123
    3812338123

    from
    to
    n2   1. Beautiful is beTTer than ugly.n2   1. Beautiful is better than ugly.
    3   2. Explicit is better than implicit.
    4   3. Simple is better than complex.3   3.   Simple is better than complex.
    5   4. Complex is better than complicated.4   4. Complicated is better than complex.
    5   5. Flat is better than nested.
    n2   1. Beautiful is beTTer than ugly.n2   1. Beautiful is better than ugly.
    3   2. Explicit is better than implicit.
    4   3. Simple is better than complex.3   3.   Simple is better than complex.
    5   4. Complex is better than complicated.4   4. Complicated is better than complex.
    5   5. Flat is better than nested.
    n17   1. Beautiful is beTTer than ugly.n17   1. Beautiful is better than ugly.
    18   2. Explicit is better than implicit.
    19   3. Simple is better than complex.18   3.   Simple is better than complex.
    20   4. Complex is better than complicated.19   4. Complicated is better than complex.
    20   5. Flat is better than nested.
    n17   1. Beautiful is beTTer than ugly.n17   1. Beautiful is better than ugly.
    18   2. Explicit is better than implicit.
    19   3. Simple is better than complex.18   3.   Simple is better than complex.
    20   4. Complex is better than complicated.19   4. Complicated is better than complex.
    20   5. Flat is better than nested.
    t32   1. Beautiful is beTTer than ugly.t32   1. Beautiful is better than ugly.
    33   2. Explicit is better than implicit.
    34   3. Simple is better than complex.33   3.   Simple is better than complex.
    35   4. Complex is better than complicated.34   4. Complicated is better than complex.
    35   5. Flat is better than nested.
    t32   1. Beautiful is beTTer than ugly.t32   1. Beautiful is better than ugly.
    33   2. Explicit is better than implicit.
    34   3. Simple is better than complex.33   3.   Simple is better than complex.
    35   4. Complex is better than complicated.34   4. Complicated is better than complex.
    35   5. Flat is better than nested.

    Same Context

    @@ -396,11 +418,11 @@

    tabsize=2

    f1f1
    t2    Line 1: preceded by from:[tt] to:[ssss]t2    Line 1: preceded by from:[tt] to:[ssss]
    3      Line 2: preceded by from:[sstt] to:[sssst]3      Line 2: preceded by from:[sstt] to:[sssst]
    4      Line 3: preceded by from:[sstst] to:[ssssss]4      Line 3: preceded by from:[sstst] to:[ssssss]
    5Line 4:   has from:[sst] to:[sss] after :5Line 4:   has from:[sst] to:[sss] after :
    6Line 5: has from:[t] to:[ss] at end 6Line 5: has from:[t] to:[ss] at end
    t2    Line 1: preceded by from:[tt] to:[ssss]t2    Line 1: preceded by from:[tt] to:[ssss]
    3      Line 2: preceded by from:[sstt] to:[sssst]3      Line 2: preceded by from:[sstt] to:[sssst]
    4      Line 3: preceded by from:[sstst] to:[ssssss]4      Line 3: preceded by from:[sstst] to:[ssssss]
    5Line 4:   has from:[sst] to:[sss] after :5Line 4:   has from:[sst] to:[sss] after :
    6Line 5: has from:[t] to:[ss] at end 6Line 5: has from:[t] to:[ss] at end

    tabsize=default

    @@ -412,11 +434,11 @@

    tabsize=default

    f1f1
    t2                Line 1: preceded by from:[tt] to:[ssss]t2    Line 1: preceded by from:[tt] to:[ssss]
    3                Line 2: preceded by from:[sstt] to:[sssst]3        Line 2: preceded by from:[sstt] to:[sssst]
    4                Line 3: preceded by from:[sstst] to:[ssssss]4      Line 3: preceded by from:[sstst] to:[ssssss]
    5Line 4:         has from:[sst] to:[sss] after :5Line 4:   has from:[sst] to:[sss] after :
    6Line 5: has from:[t] to:[ss] at end     6Line 5: has from:[t] to:[ss] at end
    t2                Line 1: preceded by from:[tt] to:[ssss]t2    Line 1: preceded by from:[tt] to:[ssss]
    3                Line 2: preceded by from:[sstt] to:[sssst]3        Line 2: preceded by from:[sstt] to:[sssst]
    4                Line 3: preceded by from:[sstst] to:[ssssss]4      Line 3: preceded by from:[sstst] to:[ssssss]
    5Line 4:         has from:[sst] to:[sss] after :5Line 4:   has from:[sst] to:[sss] after :
    6Line 5: has from:[t] to:[ss] at end     6Line 5: has from:[t] to:[ss] at end

    Context (wrapcolumn=14,numlines=0)

    @@ -427,31 +449,31 @@

    Context (wrapcolumn=14,numlines=0)

    n4line 2n4line 2    adde
     >d
    n4line 2n4line 2    adde
     >d
    n6line 4   changn6line 4   chanG
    >ed>Ed
    7line 5   chang7line 5a  chanG
    >ed>ed
    8line 6   chang8line 6a  chang
    >ed>Ed
    n6line 4   changn6line 4   chanG
    >ed>Ed
    7line 5   chang7line 5a  chanG
    >ed>ed
    8line 6   chang8line 6a  chang
    >ed>Ed
    n10line 8  subtran10line 8
    >cted 
    n10line 8  subtran10line 8
    >cted 
    t1212345678901234t121234567890
    >56789012345689 
    >012345 
    13short line13another long l
     >ine that needs
     > to be wrapped
    14just fits in!!14just fitS in!!
    15just fits in t15just fits in t
    >wo lines yup!!>wo lineS yup!!
    t1212345678901234t121234567890
    >56789012345689 
    >012345 
    13short line13another long l
     >ine that needs
     > to be wrapped
    14just fits in!!14just fitS in!!
    15just fits in t15just fits in t
    >wo lines yup!!>wo lineS yup!!

    wrapcolumn=14,splitlines()

    @@ -467,28 +489,28 @@

    wrapcolumn=14,splitlines()

    >56789012345689>56789012345689
    >012345>012345
    3line 13line 1
    n4line 2n4line 2    adde
     >d
    n4line 2n4line 2    adde
     >d
    5line 35line 3
    n6line 4   changn6line 4   chanG
    >ed>Ed
    7line 5   chang7line 5a  chanG
    >ed>ed
    8line 6   chang8line 6a  chang
    >ed>Ed
    n6line 4   changn6line 4   chanG
    >ed>Ed
    7line 5   chang7line 5a  chanG
    >ed>ed
    8line 6   chang8line 6a  chang
    >ed>Ed
    9line 79line 7
    n10line 8  subtran10line 8
    >cted 
    n10line 8  subtran10line 8
    >cted 
    11line 911line 9
    t1212345678901234t121234567890
    >56789012345689 
    >012345 
    13short line13another long l
     >ine that needs
     > to be wrapped
    14just fits in!!14just fitS in!!
    15just fits in t15just fits in t
    >wo lines yup!!>wo lineS yup!!
    t1212345678901234t121234567890
    >56789012345689 
    >012345 
    13short line13another long l
     >ine that needs
     > to be wrapped
    14just fits in!!14just fitS in!!
    15just fits in t15just fits in t
    >wo lines yup!!>wo lineS yup!!
    16the end16the end
    @@ -505,28 +527,28 @@

    wrapcolumn=14,splitlines(True)

    >56789012345689>56789012345689
    >012345>012345
    3line 13line 1
    n4line 2n4line 2    adde
     >d
    n4line 2n4line 2    adde
     >d
    5line 35line 3
    n6line 4   changn6line 4   chanG
    >ed>Ed
    7line 5   chang7line 5a  chanG
    >ed>ed
    8line 6   chang8line 6a  chang
    >ed>Ed
    n6line 4   changn6line 4   chanG
    >ed>Ed
    7line 5   chang7line 5a  chanG
    >ed>ed
    8line 6   chang8line 6a  chang
    >ed>Ed
    9line 79line 7
    n10line 8  subtran10line 8
    >cted 
    n10line 8  subtran10line 8
    >cted 
    11line 911line 9
    t1212345678901234t121234567890
    >56789012345689 
    >012345 
    13short line13another long l
     >ine that needs
     > to be wrapped
    14just fits in!!14just fitS in!!
    15just fits in t15just fits in t
    >wo lines yup!!>wo lineS yup!!
    t1212345678901234t121234567890
    >56789012345689 
    >012345 
    13short line13another long l
     >ine that needs
     > to be wrapped
    14just fits in!!14just fitS in!!
    15just fits in t15just fits in t
    >wo lines yup!!>wo lineS yup!!
    16the end16the end
    diff --git a/Lib/test/test_dtrace.py b/Lib/test/test_dtrace.py index ba2fa99707c..41b52662bd0 100644 --- a/Lib/test/test_dtrace.py +++ b/Lib/test/test_dtrace.py @@ -1,24 +1,57 @@ import dis import os.path import re +import shlex +import signal import subprocess import sys import sysconfig +import tempfile import types import unittest from test import support from test.support import findfile, MS_WINDOWS +from test.support import os_helper if not support.has_subprocess_support: raise unittest.SkipTest("test module requires subprocess") +if not sysconfig.get_config_var('WITH_DTRACE'): + raise unittest.SkipTest( + "CPython must be configured with the --with-dtrace option." + ) def abspath(filename): return os.path.abspath(findfile(filename, subdir="dtracedata")) +def get_probe_binary(): + binary = sys.executable + if sysconfig.get_config_var("Py_ENABLE_SHARED"): + lib_dir = sysconfig.get_config_var("LIBDIR") + if not lib_dir or sysconfig.is_python_build(): + lib_dir = os.path.abspath(os.path.dirname(sys.executable)) + + lib_names = [] + for name in ( + sysconfig.get_config_var("INSTSONAME"), + sysconfig.get_config_var("LDLIBRARY"), + ): + if name and name not in lib_names: + lib_names.append(name) + + if lib_dir: + for name in lib_names: + libpython_path = os.path.join(lib_dir, name) + if os.path.exists(libpython_path): + binary = libpython_path + break + + return binary + + def normalize_trace_output(output): """Normalize DTrace output for comparison. @@ -33,27 +66,93 @@ def normalize_trace_output(output): result = [ row.split("\t") for row in output.splitlines() - if row and not row.startswith('#') + if row and not row.startswith('#') and not row.startswith('@') ] result.sort(key=lambda row: int(row[0])) result = [row[1] for row in result] - return "\n".join(result) + # Normalize paths to basenames (bpftrace outputs full paths) + normalized = [] + for line in result: + # Replace full paths with just the filename + line = re.sub(r'/[^:]+/([^/:]+\.py)', r'\1', line) + normalized.append(line) + return "\n".join(normalized) except (IndexError, ValueError): raise AssertionError( "tracer produced unparsable output:\n{}".format(output) ) +USE_PROCESS_GROUP = (hasattr(os, "setsid") and hasattr(os, "killpg")) +TERMINATE_TIMEOUT = 10 + +def create_process_group(*args, **kwargs): + if USE_PROCESS_GROUP: + kwargs['start_new_session'] = True + return subprocess.Popen(*args, **kwargs) + +def terminate_process_group(proc): + if USE_PROCESS_GROUP: + try: + os.killpg(proc.pid, signal.SIGTERM) + except ProcessLookupError: + pass + else: + proc.terminate() + + try: + proc.communicate(timeout=TERMINATE_TIMEOUT) + except subprocess.TimeoutExpired: + if USE_PROCESS_GROUP: + try: + os.killpg(proc.pid, signal.SIGKILL) + except ProcessLookupError: + pass + else: + proc.kill() + proc.communicate(timeout=TERMINATE_TIMEOUT) # Clean up + + +def run_readelf(cmd): + # Force the C locale to disable localization. + env = dict(os.environ, LC_ALL="C") + try: + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=env, + ) + except OSError: + raise unittest.SkipTest("Couldn't find readelf on the path") + + with proc: + stdout, stderr = proc.communicate() + + if proc.returncode: + raise AssertionError( + f"Command {shlex.join(cmd)!r} failed " + f"with exit code {proc.returncode}: " + f"stdout={stdout!r} stderr={stderr!r}" + ) + + return stdout + + class TraceBackend: EXTENSION = None COMMAND = None COMMAND_ARGS = [] def run_case(self, name, optimize_python=None): - actual_output = normalize_trace_output(self.trace_python( - script_file=abspath(name + self.EXTENSION), - python_file=abspath(name + ".py"), - optimize_python=optimize_python)) + try: + actual_output = normalize_trace_output(self.trace_python( + script_file=abspath(name + self.EXTENSION), + python_file=abspath(name + ".py"), + optimize_python=optimize_python)) + except subprocess.TimeoutExpired: + raise AssertionError(f"{self.COMMAND[0]} timed out") with open(abspath(name + self.EXTENSION + ".expected")) as f: expected_output = f.read().rstrip() @@ -66,25 +165,42 @@ def generate_trace_command(self, script_file, subcommand=None): command += ["-c", subcommand] return command - def trace(self, script_file, subcommand=None): + def trace(self, script_file, subcommand=None, *, timeout=None, + check_returncode=False): command = self.generate_trace_command(script_file, subcommand) - stdout, _ = subprocess.Popen(command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True).communicate() + proc = create_process_group(command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True) + try: + stdout, _ = proc.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + terminate_process_group(proc) + raise + if check_returncode and proc.returncode: + raise AssertionError( + f"Command {shlex.join(command)!r} failed " + f"with exit code {proc.returncode}: output={stdout!r}" + ) return stdout def trace_python(self, script_file, python_file, optimize_python=None): python_flags = [] if optimize_python: python_flags.extend(["-O"] * optimize_python) - subcommand = " ".join([sys.executable] + python_flags + [python_file]) - return self.trace(script_file, subcommand) + subcommand = shlex.join([sys.executable] + python_flags + [python_file]) + return self.trace(script_file, subcommand, timeout=60, + check_returncode=True) def assert_usable(self): try: - output = self.trace(abspath("assert_usable" + self.EXTENSION)) + output = self.trace(abspath("assert_usable" + self.EXTENSION), + timeout=10) output = output.strip() + except subprocess.TimeoutExpired: + raise unittest.SkipTest( + f"{self.COMMAND[0]} timed out during usability check" + ) except (FileNotFoundError, NotADirectoryError, PermissionError) as fnfe: output = str(fnfe) if output != "probe: success": @@ -96,11 +212,223 @@ def assert_usable(self): class DTraceBackend(TraceBackend): EXTENSION = ".d" COMMAND = ["dtrace", "-q", "-s"] + if sys.platform == "sunos5": + COMMAND.insert(2, "-Z") class SystemTapBackend(TraceBackend): EXTENSION = ".stp" COMMAND = ["stap", "-g"] + PROBE_PLACEHOLDER = "@PYTHON_SYSTEMTAP_PROBE@" + + @staticmethod + def quote_systemtap_string(value): + return value.replace("\\", "\\\\").replace('"', '\\"') + + def python_probe(self): + executable = self.quote_systemtap_string(sys.executable) + probe_binary = get_probe_binary() + if probe_binary == sys.executable: + return f'process("{executable}").mark' + + # Python built with --enable-shared + probe_binary = self.quote_systemtap_string(probe_binary) + return f'process("{executable}").library("{probe_binary}").mark' + + def render_script(self, filename): + with open(filename) as fp: + script = fp.read() + + return script.replace(self.PROBE_PLACEHOLDER, self.python_probe()) + + def trace(self, script_file, subcommand=None, *, timeout=None, + check_returncode=False): + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", suffix=self.EXTENSION, delete=False + ) as script: + script.write(self.render_script(script_file)) + generated_script_file = script.name + + try: + return super().trace( + generated_script_file, + subcommand, + timeout=timeout, + check_returncode=check_returncode, + ) + finally: + os_helper.unlink(generated_script_file) + + +class BPFTraceBackend(TraceBackend): + EXTENSION = ".bt" + COMMAND = ["bpftrace"] + + # Inline bpftrace programs for each test case + PROGRAMS = { + "call_stack": """ + usdt:{python}:python:function__entry {{ + printf("%lld\\tfunction__entry:%s:%s:%d\\n", + nsecs, str(arg0), str(arg1), arg2); + }} + usdt:{python}:python:function__return {{ + printf("%lld\\tfunction__return:%s:%s:%d\\n", + nsecs, str(arg0), str(arg1), arg2); + }} + """, + "gc": """ + usdt:{python}:python:function__entry {{ + if (str(arg1) == "start") {{ @tracing = 1; }} + }} + usdt:{python}:python:function__return {{ + if (str(arg1) == "start") {{ @tracing = 0; }} + }} + usdt:{python}:python:gc__start {{ + if (@tracing) {{ + printf("%lld\\tgc__start:%d\\n", nsecs, arg0); + }} + }} + usdt:{python}:python:gc__done {{ + if (@tracing) {{ + printf("%lld\\tgc__done:%lld\\n", nsecs, arg0); + }} + }} + END {{ clear(@tracing); }} + """, + } + + # Which test scripts to filter by filename (None = use @tracing flag) + FILTER_BY_FILENAME = {"call_stack": "call_stack.py"} + + @staticmethod + def _filter_probe_rows(output): + return "\n".join( + line for line in output.splitlines() + if line.partition("\t")[0].isdigit() + ) + + # Expected outputs for each test case + # Note: bpftrace captures entry/return and may have slight timing + # differences compared to SystemTap due to probe firing order + EXPECTED = { + "call_stack": """function__entry:call_stack.py::0 +function__entry:call_stack.py:start:23 +function__entry:call_stack.py:function_1:1 +function__entry:call_stack.py:function_3:9 +function__return:call_stack.py:function_3:10 +function__return:call_stack.py:function_1:2 +function__entry:call_stack.py:function_2:5 +function__entry:call_stack.py:function_1:1 +function__entry:call_stack.py:function_3:9 +function__return:call_stack.py:function_3:10 +function__return:call_stack.py:function_1:2 +function__return:call_stack.py:function_2:6 +function__entry:call_stack.py:function_3:9 +function__return:call_stack.py:function_3:10 +function__entry:call_stack.py:function_4:13 +function__return:call_stack.py:function_4:14 +function__entry:call_stack.py:function_5:18 +function__return:call_stack.py:function_5:21 +function__return:call_stack.py:start:28 +function__return:call_stack.py::30""", + "gc": """gc__start:0 +gc__done:0 +gc__start:1 +gc__done:0 +gc__start:2 +gc__done:0 +gc__start:2 +gc__done:1""", + } + + def run_case(self, name, optimize_python=None): + if name not in self.PROGRAMS: + raise unittest.SkipTest(f"No bpftrace program for {name}") + + python_file = abspath(name + ".py") + python_flags = [] + if optimize_python: + python_flags.extend(["-O"] * optimize_python) + + subcommand = [sys.executable] + python_flags + [python_file] + program = self.PROGRAMS[name].format(python=get_probe_binary()) + + try: + proc = create_process_group( + ["bpftrace", "-e", program, "-c", shlex.join(subcommand)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + stdout, stderr = proc.communicate(timeout=60) + except subprocess.TimeoutExpired: + terminate_process_group(proc) + raise AssertionError("bpftrace timed out") + except (FileNotFoundError, PermissionError) as e: + raise unittest.SkipTest(f"bpftrace not available: {e}") + + if proc.returncode != 0: + raise AssertionError( + f"bpftrace failed with code {proc.returncode}:\n{stderr}" + ) + + stdout = self._filter_probe_rows(stdout) + + # Filter output by filename if specified (bpftrace captures everything) + if name in self.FILTER_BY_FILENAME: + filter_filename = self.FILTER_BY_FILENAME[name] + filtered_lines = [ + line for line in stdout.splitlines() + if filter_filename in line + ] + stdout = "\n".join(filtered_lines) + + actual_output = normalize_trace_output(stdout) + expected_output = self.EXPECTED[name].strip() + + return (expected_output, actual_output) + + def assert_usable(self): + # Check if bpftrace is available and can attach to USDT probes + program = f'usdt:{get_probe_binary()}:python:function__entry {{ printf("probe: success\\n"); exit(); }}' + try: + proc = create_process_group( + ["bpftrace", "-e", program, "-c", + shlex.join([sys.executable, "-c", "pass"])], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + stdout, stderr = proc.communicate(timeout=10) + except subprocess.TimeoutExpired: + terminate_process_group(proc) + raise unittest.SkipTest("bpftrace timed out during usability check") + except OSError as e: + raise unittest.SkipTest(f"bpftrace not available: {e}") + + # Check for permission errors (bpftrace usually requires root) + if proc.returncode != 0: + raise unittest.SkipTest( + f"bpftrace(1) failed with code {proc.returncode}: {stderr}" + ) + + if "probe: success" not in stdout: + raise unittest.SkipTest( + f"bpftrace(1) failed: stdout={stdout!r} stderr={stderr!r}" + ) + + +class BPFTraceOutputTests(unittest.TestCase): + def test_filter_probe_rows_ignores_warnings(self): + output = """stdin:1-19: WARNING: found external warnings +HINT: include/vmlinux.h:1439:3: warning: declaration does not declare anything +4623214882928\tgc__start:0 +4623214885575\tgc__done:0 +""" + self.assertEqual( + BPFTraceBackend._filter_probe_rows(output), + "4623214882928\tgc__start:0\n4623214885575\tgc__done:0", + ) @unittest.skipIf(MS_WINDOWS, "Tests not compliant with trace on Windows.") @@ -127,7 +455,8 @@ def test_function_entry_return(self): def test_verify_call_opcodes(self): """Ensure our call stack test hits all function call opcodes""" - opcodes = set(["CALL_FUNCTION", "CALL_FUNCTION_EX", "CALL_FUNCTION_KW"]) + # Modern Python uses CALL, CALL_KW, and CALL_FUNCTION_EX + opcodes = set(["CALL", "CALL_FUNCTION_EX", "CALL_KW"]) with open(abspath("call_stack.py")) as f: code_string = f.read() @@ -152,9 +481,6 @@ def get_function_instructions(funcname): def test_gc(self): self.run_case("gc") - def test_line(self): - self.run_case("line") - class DTraceNormalTests(TraceTests, unittest.TestCase): backend = DTraceBackend() @@ -175,38 +501,28 @@ class SystemTapOptimizedTests(TraceTests, unittest.TestCase): backend = SystemTapBackend() optimize_python = 2 + +class BPFTraceNormalTests(TraceTests, unittest.TestCase): + backend = BPFTraceBackend() + optimize_python = 0 + + +class BPFTraceOptimizedTests(TraceTests, unittest.TestCase): + backend = BPFTraceBackend() + optimize_python = 2 + + class CheckDtraceProbes(unittest.TestCase): @classmethod def setUpClass(cls): - if sysconfig.get_config_var('WITH_DTRACE'): - readelf_major_version, readelf_minor_version = cls.get_readelf_version() - if support.verbose: - print(f"readelf version: {readelf_major_version}.{readelf_minor_version}") - else: - raise unittest.SkipTest("CPython must be configured with the --with-dtrace option.") + readelf_major_version, readelf_minor_version = cls.get_readelf_version() + if support.verbose: + print(f"readelf version: {readelf_major_version}.{readelf_minor_version}") @staticmethod def get_readelf_version(): - try: - cmd = ["readelf", "--version"] - proc = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - ) - with proc: - version, stderr = proc.communicate() - - if proc.returncode: - raise Exception( - f"Command {' '.join(cmd)!r} failed " - f"with exit code {proc.returncode}: " - f"stdout={version!r} stderr={stderr!r}" - ) - except OSError: - raise unittest.SkipTest("Couldn't find readelf on the path") + version = run_readelf(["readelf", "--version"]) # Regex to parse: # 'GNU readelf (GNU Binutils) 2.40.0\n' -> 2.40 @@ -217,14 +533,7 @@ def get_readelf_version(): return int(match.group(1)), int(match.group(2)) def get_readelf_output(self): - command = ["readelf", "-n", sys.executable] - stdout, _ = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True, - ).communicate() - return stdout + return run_readelf(["readelf", "-n", get_probe_binary()]) def test_check_probes(self): readelf_output = self.get_readelf_output() @@ -235,6 +544,8 @@ def test_check_probes(self): "Name: audit", "Name: gc__start", "Name: gc__done", + "Name: function__entry", + "Name: function__return", ] for probe_name in available_probe_names: @@ -247,8 +558,6 @@ def test_missing_probes(self): # Missing probes will be added in the future. missing_probe_names = [ - "Name: function__entry", - "Name: function__return", "Name: line", ] diff --git a/Lib/test/test_ensurepip.py b/Lib/test/test_ensurepip.py index a8bfbcd4abc..20a56ed715d 100644 --- a/Lib/test/test_ensurepip.py +++ b/Lib/test/test_ensurepip.py @@ -13,6 +13,12 @@ import ensurepip._uninstall +if sys.implementation.cache_tag is None: + COMPILE_OPT = ["--no-compile"] +else: + COMPILE_OPT = [] + + class TestPackages(unittest.TestCase): def touch(self, directory, filename): fullname = os.path.join(directory, filename) @@ -70,6 +76,11 @@ def setUp(self): self.run_pip.return_value = 0 self.addCleanup(run_pip_patch.stop) + # Allow testing on zlib-less platforms by avoiding the check for zlib in _bootstrap() + zlib_patch = unittest.mock.patch.dict('sys.modules', {'zlib': unittest.mock.MagicMock()}) + zlib_patch.start() + self.addCleanup(zlib_patch.stop) + # Avoid side effects on the actual os module real_devnull = os.devnull os_patch = unittest.mock.patch("ensurepip.os") @@ -90,7 +101,7 @@ def test_basic_bootstrapping(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "pip", + unittest.mock.ANY, *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) @@ -104,7 +115,7 @@ def test_bootstrapping_with_root(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "--root", "/foo/bar/", + unittest.mock.ANY, "--root", "/foo/bar/", *COMPILE_OPT, "pip", ], unittest.mock.ANY, @@ -116,7 +127,7 @@ def test_bootstrapping_with_user(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "--user", "pip", + unittest.mock.ANY, "--user", *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) @@ -127,7 +138,7 @@ def test_bootstrapping_with_upgrade(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "--upgrade", "pip", + unittest.mock.ANY, "--upgrade", *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) @@ -138,7 +149,7 @@ def test_bootstrapping_with_verbosity_1(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "-v", "pip", + unittest.mock.ANY, "-v", *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) @@ -149,7 +160,7 @@ def test_bootstrapping_with_verbosity_2(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "-vv", "pip", + unittest.mock.ANY, "-vv", *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) @@ -160,7 +171,7 @@ def test_bootstrapping_with_verbosity_3(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "-vvv", "pip", + unittest.mock.ANY, "-vvv", *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) @@ -195,6 +206,16 @@ def test_pip_config_file_disabled(self): ensurepip.bootstrap() self.assertEqual(self.os_environ["PIP_CONFIG_FILE"], os.devnull) + def test_missing_zlib(self): + with unittest.mock.patch.dict('sys.modules', {'zlib': None}): + with self.assertRaises(ModuleNotFoundError) as cm: + ensurepip.bootstrap() + + error_msg = str(cm.exception) + self.assertIn("ensurepip requires the standard library module 'zlib'", error_msg) + + self.assertFalse(self.run_pip.called) + @contextlib.contextmanager def fake_pip(version=ensurepip.version()): if version is None: @@ -307,7 +328,7 @@ def test_basic_bootstrapping(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "pip", + unittest.mock.ANY, *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index edea5bc05e1..bfa4653f9cb 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -232,6 +232,33 @@ def test_error_offset_continuation_characters(self): check = self.check check('"\\\n"(1 for c in I,\\\n\\', 2, 2) + def testSyntaxErrorRange(self): + # gh-156894: the position was reported in bytes, not in characters, + # for the errors which cover a range + for source, offset, end_offset in [ + ('abcd = 00010', 8, 11), + ('\u03b1\u03b2\u03b3\u03b4 = 00010', 8, 11), + ('a\u0301b\u0308c\u20d7d\u1ab0 = 00010', 12, 15), + ("abcd = ub'a'", 8, 10), + ("\u03b1\u03b2\u03b3\u03b4 = ub'a'", 8, 10), + ("a\u0301b\u0308c\u20d7d\u1ab0 = ub'a'", 12, 14), + ]: + with self.subTest(source=source): + with self.assertRaises(SyntaxError) as cm: + compile(source, '', 'exec') + self.assertEqual(cm.exception.offset, offset) + self.assertEqual(cm.exception.end_offset, end_offset) + + def testSyntaxErrorNonUTF8Offset(self): + # gh-157378: the position was reported one column short for each + # multi-byte character preceding the invalid byte on the same line + check = self.check + check(b'X\x80', 1, 2, 1, 2) + check(b'\xc3\xa9X\x80', 1, 3, 1, 3) + check(b'\t\xc3\xa9X\x80', 1, 4, 1, 4) + check(b'a\xc3\xa9b\x80c', 1, 4, 1, 4) + check(b'a\n\xc3\xa9X\x80', 2, 3, 2, 3) + def testSyntaxErrorOffset(self): check = self.check check('def fact(x):\n\treturn x!\n', 2, 10) @@ -252,7 +279,16 @@ def testSyntaxErrorOffset(self): check('[\nfile\nfor str(file)\nin\n[]\n]', 3, 5) check('[file for\n str(file) in []]', 2, 2) check("ages = {'Alice'=22, 'Bob'=23}", 1, 9) - check('match ...:\n case {**rest, "key": value}:\n ...', 2, 19) + check(dedent("""\ + match ...: + case {**rest1, "after": after}: + ... + """), 2, 11) + check(dedent("""\ + match ...: + case {"before": before, **rest2, "after": after}: + ... + """), 2, 29) check("[a b c d e f]", 1, 2) check("for x yfff:", 1, 7) check("f(a for a in b, c)", 1, 3, 1, 15) @@ -328,7 +364,6 @@ def baz(): check('x=1\nfrom __future__ import division', 2, 1) check('foo(1=2)', 1, 5) check('def f():\n x, y: int', 2, 3) - check('[*x for x in xs]', 1, 2) check('foo(x for x in range(10), 100)', 1, 5) check('for 1 in []: pass', 1, 5) check('(yield i) = 2', 1, 2) @@ -1510,6 +1545,7 @@ def gen(): @cpython_only @unittest.skipIf(_testcapi is None, "requires _testcapi") @force_not_colorized + @support.skip_if_huge_c_stack() def test_recursion_normalizing_infinite_exception(self): # Issue #30697. Test that a RecursionError is raised when # maximum recursion depth has been exceeded when creating @@ -1576,11 +1612,7 @@ def recurse_in_body_and_except(): sys.setrecursionlimit(recursionlimit) - @cpython_only - # Python built with Py_TRACE_REFS fail with a fatal error in - # _PyRefchain_Trace() on memory allocation error. - @unittest.skipIf(support.Py_TRACE_REFS, 'cannot test Py_TRACE_REFS build') - @unittest.skipIf(_testcapi is None, "requires _testcapi") + @support.nomemtest def test_recursion_normalizing_with_no_memory(self): # Issue #30697. Test that in the abort that occurs when there is no # memory left and the size of the Python frames stack is greater than @@ -1707,6 +1739,20 @@ def inner(): gc_collect() # For PyPy or other GCs. self.assertEqual(wr(), None) + def test_oserror_reinit_leak(self): + # gh-150988: Check for memory leak when re-initializing OSError. + # Previously, setting OSError attributes in a subclass + # before calling super().__init__() leaked memory. + class LeakingOSError(OSError): + def __init__(self, code, message, filename, filename2): + self.strerror = message + self.filename = filename + self.filename2 = filename2 + super().__init__(code, message, filename, None, filename2) + + exc = LeakingOSError(1, "some message", "filename.py", "filename2.py") + exc.__init__(2, "another message", "filename3.py", "filename4.py") + def test_errno_ENOTDIR(self): # Issue #12802: "not a directory" errors are ENOTDIR even on Windows with self.assertRaises(OSError) as cm: @@ -1753,11 +1799,7 @@ def test_unhandled(self): self.assertIn("test message", report) self.assertEndsWith(report, "\n") - @cpython_only - # Python built with Py_TRACE_REFS fail with a fatal error in - # _PyRefchain_Trace() on memory allocation error. - @unittest.skipIf(support.Py_TRACE_REFS, 'cannot test Py_TRACE_REFS build') - @unittest.skipIf(_testcapi is None, "requires _testcapi") + @support.nomemtest def test_memory_error_in_PyErr_PrintEx(self): code = """if 1: import _testcapi @@ -1915,12 +1957,8 @@ def test_keyerror_context(self): exc2 = None - @cpython_only - # Python built with Py_TRACE_REFS fail with a fatal error in - # _PyRefchain_Trace() on memory allocation error. - @unittest.skipIf(support.Py_TRACE_REFS, 'cannot test Py_TRACE_REFS build') + @support.nomemtest def test_exec_set_nomemory_hang(self): - import_module("_testcapi") # gh-134163: A MemoryError inside code that was wrapped by a try/except # block would lead to an infinite loop. @@ -2117,6 +2155,50 @@ def test_copy_pickle(self): self.assertEqual(exc.name, orig.name) self.assertEqual(exc.path, orig.path) + def test_repr(self): + exc = ImportError() + self.assertEqual(repr(exc), "ImportError()") + + exc = ImportError('test') + self.assertEqual(repr(exc), "ImportError('test')") + + exc = ImportError('test', 'case') + self.assertEqual(repr(exc), "ImportError('test', 'case')") + + exc = ImportError(name='somemodule') + self.assertEqual(repr(exc), "ImportError(name='somemodule')") + + exc = ImportError('test', name='somemodule') + self.assertEqual(repr(exc), "ImportError('test', name='somemodule')") + + exc = ImportError(path='somepath') + self.assertEqual(repr(exc), "ImportError(path='somepath')") + + exc = ImportError('test', path='somepath') + self.assertEqual(repr(exc), "ImportError('test', path='somepath')") + + exc = ImportError(name='somename', path='somepath') + self.assertEqual(repr(exc), + "ImportError(name='somename', path='somepath')") + + exc = ImportError('test', name='somename', path='somepath') + self.assertEqual(repr(exc), + "ImportError('test', name='somename', path='somepath')") + + exc = ModuleNotFoundError('test', name='somename', path='somepath') + self.assertEqual(repr(exc), + "ModuleNotFoundError('test', name='somename', path='somepath')") + + def test_ModuleNotFoundError_repr_with_failed_import(self): + with self.assertRaises(ModuleNotFoundError) as cm: + import does_not_exist # type: ignore[import] # noqa: F401 + + self.assertEqual(cm.exception.name, "does_not_exist") + self.assertIsNone(cm.exception.path) + + self.assertEqual(repr(cm.exception), + "ModuleNotFoundError(\"No module named 'does_not_exist'\", name='does_not_exist')") + def run_script(source): if isinstance(source, str): @@ -2382,7 +2464,8 @@ def test_encodings(self): ) err = run_script(source.encode('cp437')) self.assertEqual(err[-3], ' "┬ó┬ó┬ó┬ó┬ó┬ó" + f(4, x for x in range(1))') - self.assertEqual(err[-2], ' ^^^') + self.assertEqual(err[-2], ' ^^^^^^^^^^^^^^^^^^^') + self.assertEqual(err[-1], 'SyntaxError: Generator expression must be parenthesized') # Check backwards tokenizer errors source = '# -*- coding: ascii -*-\n\n(\n' diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 08779bdb008..797b13af59f 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -1,11 +1,14 @@ +import asyncio import unittest import os import textwrap +import contextlib import importlib import sys import socket import threading import time +from contextlib import contextmanager from asyncio import staggered, taskgroups, base_events, tasks from unittest.mock import ANY from test.support import ( @@ -13,6 +16,7 @@ SHORT_TIMEOUT, busy_retry, requires_gil_enabled, + requires_remote_subprocess_debugging, ) from test.support.import_helper import import_module from test.support.script_helper import make_script @@ -20,6 +24,31 @@ import subprocess +# Profiling mode constants +PROFILING_MODE_WALL = 0 +PROFILING_MODE_CPU = 1 +PROFILING_MODE_GIL = 2 +PROFILING_MODE_ALL = 3 +PROFILING_MODE_EXCEPTION = 4 + +# Thread status flags +THREAD_STATUS_HAS_GIL = 1 << 0 +THREAD_STATUS_ON_CPU = 1 << 1 +THREAD_STATUS_UNKNOWN = 1 << 2 +THREAD_STATUS_HAS_EXCEPTION = 1 << 4 + +# Maximum number of retry attempts for operations that may fail transiently +MAX_TRIES = 10 +RETRY_DELAY = 0.1 + +# Exceptions that can occur transiently when reading from a live process +TRANSIENT_ERRORS = (OSError, RuntimeError, UnicodeDecodeError) + +try: + from concurrent import interpreters +except ImportError: + interpreters = None + PROCESS_VM_READV_SUPPORTED = False try: @@ -32,12 +61,149 @@ ) +# ============================================================================ +# Module-level helper functions +# ============================================================================ + + def _make_test_script(script_dir, script_basename, source): to_return = make_script(script_dir, script_basename, source) importlib.invalidate_caches() return to_return +def _create_server_socket(port, backlog=1): + """Create and configure a server socket for test communication.""" + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(("localhost", port)) + server_socket.settimeout(SHORT_TIMEOUT) + server_socket.listen(backlog) + return server_socket + + +def _wait_for_signal(sock, expected_signals, timeout=SHORT_TIMEOUT): + """ + Wait for expected signal(s) from a socket with proper timeout and EOF handling. + + Args: + sock: Connected socket to read from + expected_signals: Single bytes object or list of bytes objects to wait for + timeout: Socket timeout in seconds + + Returns: + bytes: Complete accumulated response buffer + + Raises: + RuntimeError: If connection closed before signal received or timeout + """ + if isinstance(expected_signals, bytes): + expected_signals = [expected_signals] + + sock.settimeout(timeout) + buffer = b"" + + while True: + # Check if all expected signals are in buffer + if all(sig in buffer for sig in expected_signals): + return buffer + + try: + chunk = sock.recv(4096) + if not chunk: + # EOF - connection closed + raise RuntimeError( + f"Connection closed before receiving expected signals. " + f"Expected: {expected_signals}, Got: {buffer[-200:]!r}" + ) + buffer += chunk + except socket.timeout: + raise RuntimeError( + f"Timeout waiting for signals. " + f"Expected: {expected_signals}, Got: {buffer[-200:]!r}" + ) + + +def _wait_for_n_signals(sock, signal_pattern, count, timeout=SHORT_TIMEOUT): + """ + Wait for N occurrences of a signal pattern. + + Args: + sock: Connected socket to read from + signal_pattern: bytes pattern to count (e.g., b"ready") + count: Number of occurrences expected + timeout: Socket timeout in seconds + + Returns: + bytes: Complete accumulated response buffer + + Raises: + RuntimeError: If connection closed or timeout before receiving all signals + """ + sock.settimeout(timeout) + buffer = b"" + found_count = 0 + + while found_count < count: + try: + chunk = sock.recv(4096) + if not chunk: + raise RuntimeError( + f"Connection closed after {found_count}/{count} signals. " + f"Last 200 bytes: {buffer[-200:]!r}" + ) + buffer += chunk + # Count occurrences in entire buffer + found_count = buffer.count(signal_pattern) + except socket.timeout: + raise RuntimeError( + f"Timeout waiting for {count} signals (found {found_count}). " + f"Last 200 bytes: {buffer[-200:]!r}" + ) + + return buffer + + +@contextmanager +def _managed_subprocess(args, timeout=SHORT_TIMEOUT): + """ + Context manager for subprocess lifecycle management. + + Ensures process is properly terminated and cleaned up even on exceptions. + Uses graceful termination first, then forceful kill if needed. + """ + p = subprocess.Popen(args) + try: + yield p + finally: + try: + p.terminate() + try: + p.wait(timeout=timeout) + except subprocess.TimeoutExpired: + p.kill() + try: + p.wait(timeout=timeout) + except subprocess.TimeoutExpired: + pass # Process refuses to die, nothing more we can do + except OSError: + pass # Process already dead + + +def _cleanup_sockets(*sockets): + """Safely close multiple sockets, ignoring errors.""" + for sock in sockets: + if sock is not None: + try: + sock.close() + except OSError: + pass + + +# ============================================================================ +# Decorators and skip conditions +# ============================================================================ + skip_if_not_supported = unittest.skipIf( ( sys.platform != "darwin" @@ -48,36 +214,311 @@ def _make_test_script(script_dir, script_basename, source): ) +def requires_subinterpreters(meth): + """Decorator to skip a test if subinterpreters are not supported.""" + return unittest.skipIf(interpreters is None, "subinterpreters required")( + meth + ) + + +# ============================================================================ +# Simple wrapper functions for RemoteUnwinder +# ============================================================================ + def get_stack_trace(pid): - unwinder = RemoteUnwinder(pid, all_threads=True, debug=True) - return unwinder.get_stack_trace() + for _ in busy_retry(SHORT_TIMEOUT): + try: + unwinder = RemoteUnwinder(pid, all_threads=True, debug=True) + return unwinder.get_stack_trace() + except RuntimeError as e: + continue + raise RuntimeError("Failed to get stack trace after retries") def get_async_stack_trace(pid): - unwinder = RemoteUnwinder(pid, debug=True) - return unwinder.get_async_stack_trace() + for _ in busy_retry(SHORT_TIMEOUT): + try: + unwinder = RemoteUnwinder(pid, debug=True) + return unwinder.get_async_stack_trace() + except RuntimeError as e: + continue + raise RuntimeError("Failed to get async stack trace after retries") def get_all_awaited_by(pid): - unwinder = RemoteUnwinder(pid, debug=True) - return unwinder.get_all_awaited_by() + for _ in busy_retry(SHORT_TIMEOUT): + try: + unwinder = RemoteUnwinder(pid, debug=True) + return unwinder.get_all_awaited_by() + except RuntimeError as e: + continue + raise RuntimeError("Failed to get all awaited_by after retries") + + +def _get_stack_trace_with_retry(unwinder, timeout=SHORT_TIMEOUT, condition=None): + """Get stack trace from an existing unwinder with retry for transient errors. + + This handles the case where we want to reuse an existing RemoteUnwinder + instance but still handle transient failures like "Failed to parse initial + frame in chain" that can occur when sampling at an inopportune moment. + If condition is provided, keeps retrying until condition(traces) is True. + """ + last_error = None + for _ in busy_retry(timeout): + try: + traces = unwinder.get_stack_trace() + if condition is None or condition(traces): + return traces + # Condition not met yet, keep retrying + except TRANSIENT_ERRORS as e: + last_error = e + continue + if last_error: + raise RuntimeError( + f"Failed to get stack trace after retries: {last_error}" + ) + raise RuntimeError("Condition never satisfied within timeout") + + +# ============================================================================ +# Base test class with shared infrastructure +# ============================================================================ + +class RemoteInspectionTestBase(unittest.TestCase): + """Base class for remote inspection tests with common helpers.""" -class TestGetStackTrace(unittest.TestCase): maxDiff = None + def _run_script_and_get_trace( + self, + script, + trace_func, + wait_for_signals=None, + port=None, + backlog=1, + ): + """ + Common pattern: run a script, wait for signals, get trace. + + Args: + script: Script content (will be formatted with port if {port} present) + trace_func: Function to call with pid to get trace (e.g., get_stack_trace) + wait_for_signals: Signal(s) to wait for before getting trace + port: Port to use (auto-selected if None) + backlog: Socket listen backlog + + Returns: + tuple: (trace_result, script_name) + """ + if port is None: + port = find_unused_port() + + # Format script with port if needed + if "{port}" in script or "{{port}}" in script: + script = script.replace("{{port}}", "{port}").format(port=port) + + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + + server_socket = _create_server_socket(port, backlog) + script_name = _make_test_script(script_dir, "script", script) + client_socket = None + + try: + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None + + if wait_for_signals: + _wait_for_signal(client_socket, wait_for_signals) + + trace = trace_func(p.pid) + return trace, script_name + finally: + _cleanup_sockets(client_socket, server_socket) + + def _find_frame_in_trace(self, stack_trace, predicate): + """ + Find a frame matching predicate in stack trace. + + Args: + stack_trace: List of InterpreterInfo objects + predicate: Function(frame) -> bool + + Returns: + FrameInfo or None + """ + for interpreter_info in stack_trace: + for thread_info in interpreter_info.threads: + for frame in thread_info.frame_info: + if predicate(frame): + return frame + return None + + def _find_thread_by_id(self, stack_trace, thread_id): + """Find a thread by its native thread ID.""" + for interpreter_info in stack_trace: + for thread_info in interpreter_info.threads: + if thread_info.thread_id == thread_id: + return thread_info + return None + + def _find_thread_with_frame(self, stack_trace, frame_predicate): + """Find a thread containing a frame matching predicate.""" + for interpreter_info in stack_trace: + for thread_info in interpreter_info.threads: + for frame in thread_info.frame_info: + if frame_predicate(frame): + return thread_info + return None + + def _get_thread_statuses(self, stack_trace): + """Extract thread_id -> status mapping from stack trace.""" + statuses = {} + for interpreter_info in stack_trace: + for thread_info in interpreter_info.threads: + statuses[thread_info.thread_id] = thread_info.status + return statuses + + def _get_task_id_map(self, stack_trace): + """Create task_id -> task mapping from async stack trace.""" + return {task.task_id: task for task in stack_trace[0].awaited_by} + + def _get_awaited_by_relationships(self, stack_trace): + """Extract task name to awaited_by set mapping.""" + id_to_task = self._get_task_id_map(stack_trace) + return { + task.task_name: set( + id_to_task[awaited.task_name].task_name + for awaited in task.awaited_by + ) + for task in stack_trace[0].awaited_by + } + + def _extract_coroutine_stacks(self, stack_trace): + """Extract and format coroutine stacks from tasks.""" + return { + task.task_name: sorted( + tuple(tuple(frame) for frame in coro.call_stack) + for coro in task.coroutine_stack + ) + for task in stack_trace[0].awaited_by + } + + @staticmethod + def _frame_to_lineno_tuple(frame): + """Convert frame to (filename, lineno, funcname, opcode) tuple. + + This extracts just the line number from the location, ignoring column + offsets which can vary due to sampling timing (e.g., when two statements + are on the same line, the sample might catch either one). + """ + filename, location, funcname, opcode = frame + return (filename, location.lineno, funcname, opcode) + + def _extract_coroutine_stacks_lineno_only(self, stack_trace): + """Extract coroutine stacks with line numbers only (no column offsets). + + Use this for tests where sampling timing can cause column offset + variations (e.g., 'expr1; expr2' on the same line). + """ + return { + task.task_name: sorted( + tuple(self._frame_to_lineno_tuple(frame) for frame in coro.call_stack) + for coro in task.coroutine_stack + ) + for task in stack_trace[0].awaited_by + } + + +# ============================================================================ +# Test classes +# ============================================================================ + + +@requires_remote_subprocess_debugging() +class TestSelfStackTrace(RemoteInspectionTestBase): + @skip_if_not_supported + def test_long_task_name_is_truncated(self): + # gh-157788 + async def main(): + asyncio.create_task(asyncio.sleep(10_000), name="x" * 300) + await asyncio.sleep(0) + names = [ + task.task_name + for info in RemoteUnwinder(os.getpid()).get_all_awaited_by() + for task in info.awaited_by + ] + return asyncio.current_task().get_name(), names + + main_name, names = asyncio.run(main()) + self.assertIn(main_name, names) + self.assertEqual([len(n) for n in names if n.startswith("x")], [255]) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_self_trace_with_large_linetable(self): + script = textwrap.dedent("""\ + import os + import _remote_debugging + + assignments = "\\n".join( + f"value_{i} = {i}" for i in range(1000) + ) + expected_lineno = len(assignments.splitlines()) + 1 + source = ( + f"{assignments}\\n" + "stack_trace = " + "_remote_debugging.RemoteUnwinder(os.getpid()).get_stack_trace()\\n" + ) + code = compile(source, "large_linetable.py", "exec") + assert len(code.co_linetable) > 4096, len(code.co_linetable) + namespace = {"os": os, "_remote_debugging": _remote_debugging} + exec(code, namespace) + large_linetable_frames = [ + frame + for interpreter in namespace["stack_trace"] + for thread in interpreter.threads + for frame in thread.frame_info + if frame.filename == "large_linetable.py" + ] + assert len(large_linetable_frames) == 1, large_linetable_frames + assert large_linetable_frames[0].location.lineno == expected_lineno, ( + large_linetable_frames[0] + ) + """) + + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + timeout=SHORT_TIMEOUT, + ) + self.assertEqual( + result.returncode, 0, + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) + + +@requires_remote_subprocess_debugging() +class TestGetStackTrace(RemoteInspectionTestBase): @skip_if_not_supported @unittest.skipIf( sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, "Test only runs on Linux with process_vm_readv support", ) def test_remote_stack_trace(self): - # Spawn a process with some realistic Python code port = find_unused_port() script = textwrap.dedent( f"""\ import time, sys, socket, threading - # Connect to the test process + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('localhost', {port})) @@ -90,66 +531,59 @@ def baz(): foo() def foo(): - sock.sendall(b"ready:thread\\n"); time.sleep(10_000) # same line number + sock.sendall(b"ready:thread\\n"); time.sleep(10_000) t = threading.Thread(target=bar) t.start() - sock.sendall(b"ready:main\\n"); t.join() # same line number + sock.sendall(b"ready:main\\n"); t.join() """ ) - stack_trace = None + with os_helper.temp_dir() as work_dir: script_dir = os.path.join(work_dir, "script_pkg") os.mkdir(script_dir) - # Create a socket server to communicate with the target process - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.bind(("localhost", port)) - server_socket.settimeout(SHORT_TIMEOUT) - server_socket.listen(1) - + server_socket = _create_server_socket(port) script_name = _make_test_script(script_dir, "script", script) client_socket = None + try: - p = subprocess.Popen([sys.executable, script_name]) - client_socket, _ = server_socket.accept() - server_socket.close() - response = b"" - while ( - b"ready:main" not in response - or b"ready:thread" not in response - ): - response += client_socket.recv(1024) - stack_trace = get_stack_trace(p.pid) - except PermissionError: - self.skipTest( - "Insufficient permissions to read the stack trace" - ) - finally: - if client_socket is not None: - client_socket.close() - p.kill() - p.terminate() - p.wait(timeout=SHORT_TIMEOUT) + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None - thread_expected_stack_trace = [ - FrameInfo([script_name, 15, "foo"]), - FrameInfo([script_name, 12, "baz"]), - FrameInfo([script_name, 9, "bar"]), - FrameInfo([threading.__file__, ANY, "Thread.run"]), - ] - # Is possible that there are more threads, so we check that the - # expected stack traces are in the result (looking at you Windows!) - self.assertIn((ANY, thread_expected_stack_trace), stack_trace) - - # Check that the main thread stack trace is in the result - frame = FrameInfo([script_name, 19, ""]) - for _, stack in stack_trace: - if frame in stack: - break - else: - self.fail("Main thread stack trace not found in result") + _wait_for_signal( + client_socket, [b"ready:main", b"ready:thread"] + ) + + stack_trace = get_stack_trace(p.pid) + + # Find expected thread stack by funcname + found_thread = self._find_thread_with_frame( + stack_trace, + lambda f: f.funcname == "foo" and f.location.lineno == 15, + ) + self.assertIsNotNone( + found_thread, "Expected thread stack trace not found" + ) + # Check the funcnames in order + funcnames = [f.funcname for f in found_thread.frame_info] + self.assertEqual( + funcnames[:6], + ["foo", "baz", "bar", "Thread.run", "Thread._bootstrap_inner", "Thread._bootstrap"] + ) + + # Check main thread + found_main = self._find_frame_in_trace( + stack_trace, + lambda f: f.funcname == "" and f.location.lineno == 19, + ) + self.assertIsNotNone( + found_main, "Main thread stack trace not found" + ) + finally: + _cleanup_sockets(client_socket, server_socket) @skip_if_not_supported @unittest.skipIf( @@ -193,13 +627,81 @@ def test_self_trace_after_ctypes_import(self): f"stdout: {result.stdout}\nstderr: {result.stderr}" ) + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_remote_stack_trace_non_ascii_names(self): + # Exercise each PyUnicode kind (1-byte non-ASCII, 2-byte BMP, + # 4-byte non-BMP) for both the filename and the function name + # reported in the stack trace. + latin1 = "zażółć" # 1-byte non-ASCII (forces non-ASCII path) + bmp = "λάμβδα" # 2-byte BMP + astral = "𐌀𐌁𐌂𐌃" # 4-byte non-BMP (Old Italic; XID, no NFKC fold) + func_name = f"{latin1}_{bmp}_{astral}" + script_basename = f"mod_{latin1}_{bmp}_{astral}" + + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import socket + import time + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def {func_name}(): + sock.sendall(b"ready") + time.sleep(10_000) + + {func_name}() + """ + ) + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + script_name = _make_test_script(script_dir, script_basename, script) + + server_socket = _create_server_socket(port) + client_socket = None + try: + p = subprocess.Popen([sys.executable, script_name]) + client_socket, _ = server_socket.accept() + server_socket.close() + _wait_for_signal(client_socket, b"ready") + + stack_trace = get_stack_trace(p.pid) + except PermissionError: + self.skipTest("Insufficient permissions to read the stack trace") + finally: + if client_socket is not None: + client_socket.close() + p.kill() + p.wait(timeout=SHORT_TIMEOUT) + + frames = [ + frame + for interp in stack_trace + for thread in interp.threads + for frame in thread.frame_info + ] + target = next( + (f for f in frames if f.funcname == func_name), None + ) + self.assertIsNotNone( + target, + f"Frame for {func_name!r} missing; got " + f"{[(f.filename, f.funcname) for f in frames]}", + ) + self.assertEqual(target.filename, script_name) + @skip_if_not_supported @unittest.skipIf( sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, "Test only runs on Linux with process_vm_readv support", ) def test_async_remote_stack_trace(self): - # Spawn a process with some realistic Python code port = find_unused_port() script = textwrap.dedent( f"""\ @@ -207,12 +709,12 @@ def test_async_remote_stack_trace(self): import time import sys import socket - # Connect to the test process + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('localhost', {port})) def c5(): - sock.sendall(b"ready"); time.sleep(10_000) # same line number + sock.sendall(b"ready"); time.sleep(10_000) async def c4(): await asyncio.sleep(0) @@ -243,7 +745,7 @@ def new_eager_loop(): asyncio.run(main(), loop_factory={{TASK_FACTORY}}) """ ) - stack_trace = None + for task_factory_variant in "asyncio.new_event_loop", "new_eager_loop": with ( self.subTest(task_factory_variant=task_factory_variant), @@ -251,203 +753,159 @@ def new_eager_loop(): ): script_dir = os.path.join(work_dir, "script_pkg") os.mkdir(script_dir) - server_socket = socket.socket( - socket.AF_INET, socket.SOCK_STREAM - ) - server_socket.setsockopt( - socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 - ) - server_socket.bind(("localhost", port)) - server_socket.settimeout(SHORT_TIMEOUT) - server_socket.listen(1) + + server_socket = _create_server_socket(port) script_name = _make_test_script( script_dir, "script", script.format(TASK_FACTORY=task_factory_variant), ) client_socket = None - try: - p = subprocess.Popen([sys.executable, script_name]) - client_socket, _ = server_socket.accept() - server_socket.close() - response = client_socket.recv(1024) - self.assertEqual(response, b"ready") - stack_trace = get_async_stack_trace(p.pid) - except PermissionError: - self.skipTest( - "Insufficient permissions to read the stack trace" - ) - finally: - if client_socket is not None: - client_socket.close() - p.kill() - p.terminate() - p.wait(timeout=SHORT_TIMEOUT) - - # First check all the tasks are present - tasks_names = [ - task.task_name for task in stack_trace[0].awaited_by - ] - for task_name in ["c2_root", "sub_main_1", "sub_main_2"]: - self.assertIn(task_name, tasks_names) - - # Now ensure that the awaited_by_relationships are correct - id_to_task = { - task.task_id: task for task in stack_trace[0].awaited_by - } - task_name_to_awaited_by = { - task.task_name: set( - id_to_task[awaited.task_name].task_name - for awaited in task.awaited_by - ) - for task in stack_trace[0].awaited_by - } - self.assertEqual( - task_name_to_awaited_by, - { - "c2_root": {"Task-1", "sub_main_1", "sub_main_2"}, - "Task-1": set(), - "sub_main_1": {"Task-1"}, - "sub_main_2": {"Task-1"}, - }, - ) - # Now ensure that the coroutine stacks are correct - coroutine_stacks = { - task.task_name: sorted( - tuple(tuple(frame) for frame in coro.call_stack) - for coro in task.coroutine_stack - ) - for task in stack_trace[0].awaited_by - } - self.assertEqual( - coroutine_stacks, - { - "Task-1": [ - ( - tuple( - [ - taskgroups.__file__, - ANY, - "TaskGroup._aexit", - ] - ), - tuple( - [ - taskgroups.__file__, - ANY, - "TaskGroup.__aexit__", - ] - ), - tuple([script_name, 26, "main"]), - ) - ], - "c2_root": [ - ( - tuple([script_name, 10, "c5"]), - tuple([script_name, 14, "c4"]), - tuple([script_name, 17, "c3"]), - tuple([script_name, 20, "c2"]), - ) - ], - "sub_main_1": [(tuple([script_name, 23, "c1"]),)], - "sub_main_2": [(tuple([script_name, 23, "c1"]),)], - }, - ) + try: + with _managed_subprocess( + [sys.executable, script_name] + ) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None + + response = _wait_for_signal(client_socket, b"ready") + self.assertIn(b"ready", response) + + stack_trace = get_async_stack_trace(p.pid) + + # Check all tasks are present + tasks_names = [ + task.task_name + for task in stack_trace[0].awaited_by + ] + for task_name in [ + "c2_root", + "sub_main_1", + "sub_main_2", + ]: + self.assertIn(task_name, tasks_names) + + # Check awaited_by relationships + relationships = self._get_awaited_by_relationships( + stack_trace + ) + self.assertEqual( + relationships, + { + "c2_root": { + "Task-1", + "sub_main_1", + "sub_main_2", + }, + "Task-1": set(), + "sub_main_1": {"Task-1"}, + "sub_main_2": {"Task-1"}, + }, + ) - # Now ensure the coroutine stacks for the awaited_by relationships are correct. - awaited_by_coroutine_stacks = { - task.task_name: sorted( - ( - id_to_task[coro.task_name].task_name, - tuple(tuple(frame) for frame in coro.call_stack), + # Check coroutine stacks (using line numbers only to avoid + # flakiness from column offset variations when sampling + # catches different statements on the same line) + coroutine_stacks = self._extract_coroutine_stacks_lineno_only( + stack_trace ) - for coro in task.awaited_by - ) - for task in stack_trace[0].awaited_by - } - self.assertEqual( - awaited_by_coroutine_stacks, - { - "Task-1": [], - "c2_root": [ - ( - "Task-1", - ( - tuple( - [ - taskgroups.__file__, - ANY, - "TaskGroup._aexit", - ] - ), - tuple( - [ - taskgroups.__file__, - ANY, - "TaskGroup.__aexit__", - ] - ), - tuple([script_name, 26, "main"]), - ), - ), - ("sub_main_1", (tuple([script_name, 23, "c1"]),)), - ("sub_main_2", (tuple([script_name, 23, "c1"]),)), - ], - "sub_main_1": [ - ( - "Task-1", + self.assertEqual( + coroutine_stacks, + { + "Task-1": [ + ( + (taskgroups.__file__, ANY, "TaskGroup._aexit", None), + (taskgroups.__file__, ANY, "TaskGroup.__aexit__", None), + (script_name, 26, "main", None), + ) + ], + "c2_root": [ + ( + (script_name, 10, "c5", None), + (script_name, 14, "c4", None), + (script_name, 17, "c3", None), + (script_name, 20, "c2", None), + ) + ], + "sub_main_1": [ + ((script_name, 23, "c1", None),) + ], + "sub_main_2": [ + ((script_name, 23, "c1", None),) + ], + }, + ) + + # Check awaited_by coroutine stacks (line numbers only) + id_to_task = self._get_task_id_map(stack_trace) + awaited_by_coroutine_stacks = { + task.task_name: sorted( ( + id_to_task[coro.task_name].task_name, tuple( - [ - taskgroups.__file__, - ANY, - "TaskGroup._aexit", - ] - ), - tuple( - [ - taskgroups.__file__, - ANY, - "TaskGroup.__aexit__", - ] + self._frame_to_lineno_tuple(frame) + for frame in coro.call_stack ), - tuple([script_name, 26, "main"]), - ), + ) + for coro in task.awaited_by ) - ], - "sub_main_2": [ - ( - "Task-1", - ( - tuple( - [ - taskgroups.__file__, - ANY, - "TaskGroup._aexit", - ] + for task in stack_trace[0].awaited_by + } + self.assertEqual( + awaited_by_coroutine_stacks, + { + "Task-1": [], + "c2_root": [ + ( + "Task-1", + ( + (taskgroups.__file__, ANY, "TaskGroup._aexit", None), + (taskgroups.__file__, ANY, "TaskGroup.__aexit__", None), + (script_name, 26, "main", None), + ), ), - tuple( - [ - taskgroups.__file__, - ANY, - "TaskGroup.__aexit__", - ] + ( + "sub_main_1", + ((script_name, 23, "c1", None),), ), - tuple([script_name, 26, "main"]), - ), - ) - ], - }, - ) - - @skip_if_not_supported + ( + "sub_main_2", + ((script_name, 23, "c1", None),), + ), + ], + "sub_main_1": [ + ( + "Task-1", + ( + (taskgroups.__file__, ANY, "TaskGroup._aexit", None), + (taskgroups.__file__, ANY, "TaskGroup.__aexit__", None), + (script_name, 26, "main", None), + ), + ) + ], + "sub_main_2": [ + ( + "Task-1", + ( + (taskgroups.__file__, ANY, "TaskGroup._aexit", None), + (taskgroups.__file__, ANY, "TaskGroup.__aexit__", None), + (script_name, 26, "main", None), + ), + ) + ], + }, + ) + finally: + _cleanup_sockets(client_socket, server_socket) + + @skip_if_not_supported @unittest.skipIf( sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, "Test only runs on Linux with process_vm_readv support", ) def test_asyncgen_remote_stack_trace(self): - # Spawn a process with some realistic Python code port = find_unused_port() script = textwrap.dedent( f"""\ @@ -455,12 +913,12 @@ def test_asyncgen_remote_stack_trace(self): import time import sys import socket - # Connect to the test process + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('localhost', {port})) async def gen_nested_call(): - sock.sendall(b"ready"); time.sleep(10_000) # same line number + sock.sendall(b"ready"); time.sleep(10_000) async def gen(): for num in range(2): @@ -475,59 +933,53 @@ async def main(): asyncio.run(main()) """ ) - stack_trace = None + with os_helper.temp_dir() as work_dir: script_dir = os.path.join(work_dir, "script_pkg") os.mkdir(script_dir) - # Create a socket server to communicate with the target process - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.bind(("localhost", port)) - server_socket.settimeout(SHORT_TIMEOUT) - server_socket.listen(1) + + server_socket = _create_server_socket(port) script_name = _make_test_script(script_dir, "script", script) client_socket = None + try: - p = subprocess.Popen([sys.executable, script_name]) - client_socket, _ = server_socket.accept() - server_socket.close() - response = client_socket.recv(1024) - self.assertEqual(response, b"ready") - stack_trace = get_async_stack_trace(p.pid) - except PermissionError: - self.skipTest( - "Insufficient permissions to read the stack trace" - ) - finally: - if client_socket is not None: - client_socket.close() - p.kill() - p.terminate() - p.wait(timeout=SHORT_TIMEOUT) + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None - # For this simple asyncgen test, we only expect one task with the full coroutine stack - self.assertEqual(len(stack_trace[0].awaited_by), 1) - task = stack_trace[0].awaited_by[0] - self.assertEqual(task.task_name, "Task-1") + response = _wait_for_signal(client_socket, b"ready") + self.assertIn(b"ready", response) - # Check the coroutine stack - based on actual output, only shows main - coroutine_stack = sorted( - tuple(tuple(frame) for frame in coro.call_stack) - for coro in task.coroutine_stack - ) - self.assertEqual( - coroutine_stack, - [ - ( - tuple([script_name, 10, "gen_nested_call"]), - tuple([script_name, 16, "gen"]), - tuple([script_name, 19, "main"]), + stack_trace = get_async_stack_trace(p.pid) + + # For this simple asyncgen test, we only expect one task + self.assertEqual(len(stack_trace[0].awaited_by), 1) + task = stack_trace[0].awaited_by[0] + self.assertEqual(task.task_name, "Task-1") + + # Check the coroutine stack (using line numbers only to avoid + # flakiness from column offset variations when sampling + # catches different statements on the same line) + coroutine_stack = sorted( + tuple(self._frame_to_lineno_tuple(frame) for frame in coro.call_stack) + for coro in task.coroutine_stack + ) + self.assertEqual( + coroutine_stack, + [ + ( + (script_name, 10, "gen_nested_call", None), + (script_name, 16, "gen", None), + (script_name, 19, "main", None), + ) + ], ) - ], - ) - # No awaited_by relationships expected for this simple case - self.assertEqual(task.awaited_by, []) + # No awaited_by relationships expected + self.assertEqual(task.awaited_by, []) + finally: + _cleanup_sockets(client_socket, server_socket) @skip_if_not_supported @unittest.skipIf( @@ -535,7 +987,6 @@ async def main(): "Test only runs on Linux with process_vm_readv support", ) def test_async_gather_remote_stack_trace(self): - # Spawn a process with some realistic Python code port = find_unused_port() script = textwrap.dedent( f"""\ @@ -543,13 +994,13 @@ def test_async_gather_remote_stack_trace(self): import time import sys import socket - # Connect to the test process + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('localhost', {port})) async def deep(): await asyncio.sleep(0) - sock.sendall(b"ready"); time.sleep(10_000) # same line number + sock.sendall(b"ready"); time.sleep(10_000) async def c1(): await asyncio.sleep(0) @@ -564,103 +1015,89 @@ async def main(): asyncio.run(main()) """ ) - stack_trace = None + with os_helper.temp_dir() as work_dir: script_dir = os.path.join(work_dir, "script_pkg") os.mkdir(script_dir) - # Create a socket server to communicate with the target process - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.bind(("localhost", port)) - server_socket.settimeout(SHORT_TIMEOUT) - server_socket.listen(1) + + server_socket = _create_server_socket(port) script_name = _make_test_script(script_dir, "script", script) client_socket = None + try: - p = subprocess.Popen([sys.executable, script_name]) - client_socket, _ = server_socket.accept() - server_socket.close() - response = client_socket.recv(1024) - self.assertEqual(response, b"ready") - stack_trace = get_async_stack_trace(p.pid) - except PermissionError: - self.skipTest( - "Insufficient permissions to read the stack trace" - ) - finally: - if client_socket is not None: - client_socket.close() - p.kill() - p.terminate() - p.wait(timeout=SHORT_TIMEOUT) + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None - # First check all the tasks are present - tasks_names = [ - task.task_name for task in stack_trace[0].awaited_by - ] - for task_name in ["Task-1", "Task-2"]: - self.assertIn(task_name, tasks_names) + response = _wait_for_signal(client_socket, b"ready") + self.assertIn(b"ready", response) - # Now ensure that the awaited_by_relationships are correct - id_to_task = { - task.task_id: task for task in stack_trace[0].awaited_by - } - task_name_to_awaited_by = { - task.task_name: set( - id_to_task[awaited.task_name].task_name - for awaited in task.awaited_by - ) - for task in stack_trace[0].awaited_by - } - self.assertEqual( - task_name_to_awaited_by, - { - "Task-1": set(), - "Task-2": {"Task-1"}, - }, - ) + stack_trace = get_async_stack_trace(p.pid) - # Now ensure that the coroutine stacks are correct - coroutine_stacks = { - task.task_name: sorted( - tuple(tuple(frame) for frame in coro.call_stack) - for coro in task.coroutine_stack - ) - for task in stack_trace[0].awaited_by - } - self.assertEqual( - coroutine_stacks, - { - "Task-1": [(tuple([script_name, 21, "main"]),)], - "Task-2": [ - ( - tuple([script_name, 11, "deep"]), - tuple([script_name, 15, "c1"]), - ) - ], - }, - ) + # Check all tasks are present + tasks_names = [ + task.task_name for task in stack_trace[0].awaited_by + ] + for task_name in ["Task-1", "Task-2"]: + self.assertIn(task_name, tasks_names) - # Now ensure the coroutine stacks for the awaited_by relationships are correct. - awaited_by_coroutine_stacks = { - task.task_name: sorted( - ( - id_to_task[coro.task_name].task_name, - tuple(tuple(frame) for frame in coro.call_stack), + # Check awaited_by relationships + relationships = self._get_awaited_by_relationships( + stack_trace ) - for coro in task.awaited_by - ) - for task in stack_trace[0].awaited_by - } - self.assertEqual( - awaited_by_coroutine_stacks, - { - "Task-1": [], - "Task-2": [ - ("Task-1", (tuple([script_name, 21, "main"]),)) - ], - }, - ) + self.assertEqual( + relationships, + { + "Task-1": set(), + "Task-2": {"Task-1"}, + }, + ) + + # Check coroutine stacks (using line numbers only to avoid + # flakiness from column offset variations when sampling + # catches different statements on the same line) + coroutine_stacks = self._extract_coroutine_stacks_lineno_only( + stack_trace + ) + self.assertEqual( + coroutine_stacks, + { + "Task-1": [((script_name, 21, "main", None),)], + "Task-2": [ + ( + (script_name, 11, "deep", None), + (script_name, 15, "c1", None), + ) + ], + }, + ) + + # Check awaited_by coroutine stacks (line numbers only) + id_to_task = self._get_task_id_map(stack_trace) + awaited_by_coroutine_stacks = { + task.task_name: sorted( + ( + id_to_task[coro.task_name].task_name, + tuple( + self._frame_to_lineno_tuple(frame) for frame in coro.call_stack + ), + ) + for coro in task.awaited_by + ) + for task in stack_trace[0].awaited_by + } + self.assertEqual( + awaited_by_coroutine_stacks, + { + "Task-1": [], + "Task-2": [ + ("Task-1", ((script_name, 21, "main", None),)) + ], + }, + ) + finally: + _cleanup_sockets(client_socket, server_socket) @skip_if_not_supported @unittest.skipIf( @@ -668,7 +1105,6 @@ async def main(): "Test only runs on Linux with process_vm_readv support", ) def test_async_staggered_race_remote_stack_trace(self): - # Spawn a process with some realistic Python code port = find_unused_port() script = textwrap.dedent( f"""\ @@ -676,13 +1112,13 @@ def test_async_staggered_race_remote_stack_trace(self): import time import sys import socket - # Connect to the test process + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('localhost', {port})) async def deep(): await asyncio.sleep(0) - sock.sendall(b"ready"); time.sleep(10_000) # same line number + sock.sendall(b"ready"); time.sleep(10_000) async def c1(): await asyncio.sleep(0) @@ -700,123 +1136,101 @@ async def main(): asyncio.run(main()) """ ) - stack_trace = None + with os_helper.temp_dir() as work_dir: script_dir = os.path.join(work_dir, "script_pkg") os.mkdir(script_dir) - # Create a socket server to communicate with the target process - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.bind(("localhost", port)) - server_socket.settimeout(SHORT_TIMEOUT) - server_socket.listen(1) + + server_socket = _create_server_socket(port) script_name = _make_test_script(script_dir, "script", script) client_socket = None + try: - p = subprocess.Popen([sys.executable, script_name]) - client_socket, _ = server_socket.accept() - server_socket.close() - response = client_socket.recv(1024) - self.assertEqual(response, b"ready") - stack_trace = get_async_stack_trace(p.pid) - except PermissionError: - self.skipTest( - "Insufficient permissions to read the stack trace" - ) - finally: - if client_socket is not None: - client_socket.close() - p.kill() - p.terminate() - p.wait(timeout=SHORT_TIMEOUT) + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None - # First check all the tasks are present - tasks_names = [ - task.task_name for task in stack_trace[0].awaited_by - ] - for task_name in ["Task-1", "Task-2"]: - self.assertIn(task_name, tasks_names) + response = _wait_for_signal(client_socket, b"ready") + self.assertIn(b"ready", response) - # Now ensure that the awaited_by_relationships are correct - id_to_task = { - task.task_id: task for task in stack_trace[0].awaited_by - } - task_name_to_awaited_by = { - task.task_name: set( - id_to_task[awaited.task_name].task_name - for awaited in task.awaited_by - ) - for task in stack_trace[0].awaited_by - } - self.assertEqual( - task_name_to_awaited_by, - { - "Task-1": set(), - "Task-2": {"Task-1"}, - }, - ) + stack_trace = get_async_stack_trace(p.pid) - # Now ensure that the coroutine stacks are correct - coroutine_stacks = { - task.task_name: sorted( - tuple(tuple(frame) for frame in coro.call_stack) - for coro in task.coroutine_stack - ) - for task in stack_trace[0].awaited_by - } - self.assertEqual( - coroutine_stacks, - { - "Task-1": [ - ( - tuple([staggered.__file__, ANY, "staggered_race"]), - tuple([script_name, 21, "main"]), - ) - ], - "Task-2": [ - ( - tuple([script_name, 11, "deep"]), - tuple([script_name, 15, "c1"]), - tuple( - [ - staggered.__file__, - ANY, - "staggered_race..run_one_coro", - ] - ), - ) - ], - }, - ) + # Check all tasks are present + tasks_names = [ + task.task_name for task in stack_trace[0].awaited_by + ] + for task_name in ["Task-1", "Task-2"]: + self.assertIn(task_name, tasks_names) - # Now ensure the coroutine stacks for the awaited_by relationships are correct. - awaited_by_coroutine_stacks = { - task.task_name: sorted( - ( - id_to_task[coro.task_name].task_name, - tuple(tuple(frame) for frame in coro.call_stack), + # Check awaited_by relationships + relationships = self._get_awaited_by_relationships( + stack_trace ) - for coro in task.awaited_by - ) - for task in stack_trace[0].awaited_by - } - self.assertEqual( - awaited_by_coroutine_stacks, - { - "Task-1": [], - "Task-2": [ - ( - "Task-1", + self.assertEqual( + relationships, + { + "Task-1": set(), + "Task-2": {"Task-1"}, + }, + ) + + # Check coroutine stacks (using line numbers only to avoid + # flakiness from column offset variations when sampling + # catches different statements on the same line) + coroutine_stacks = self._extract_coroutine_stacks_lineno_only( + stack_trace + ) + self.assertEqual( + coroutine_stacks, + { + "Task-1": [ + ( + (staggered.__file__, ANY, "staggered_race", None), + (script_name, 21, "main", None), + ) + ], + "Task-2": [ + ( + (script_name, 11, "deep", None), + (script_name, 15, "c1", None), + (staggered.__file__, ANY, "staggered_race..run_one_coro", None), + ) + ], + }, + ) + + # Check awaited_by coroutine stacks (line numbers only) + id_to_task = self._get_task_id_map(stack_trace) + awaited_by_coroutine_stacks = { + task.task_name: sorted( ( + id_to_task[coro.task_name].task_name, tuple( - [staggered.__file__, ANY, "staggered_race"] + self._frame_to_lineno_tuple(frame) for frame in coro.call_stack ), - tuple([script_name, 21, "main"]), - ), + ) + for coro in task.awaited_by ) - ], - }, - ) + for task in stack_trace[0].awaited_by + } + self.assertEqual( + awaited_by_coroutine_stacks, + { + "Task-1": [], + "Task-2": [ + ( + "Task-1", + ( + (staggered.__file__, ANY, "staggered_race", None), + (script_name, 21, "main", None), + ), + ) + ], + }, + ) + finally: + _cleanup_sockets(client_socket, server_socket) @skip_if_not_supported @unittest.skipIf( @@ -824,6 +1238,10 @@ async def main(): "Test only runs on Linux with process_vm_readv support", ) def test_async_global_awaited_by(self): + # Reduced from 1000 to 100 to avoid file descriptor exhaustion + # when running tests in parallel (e.g., -j 20) + NUM_TASKS = 100 + port = find_unused_port() script = textwrap.dedent( f"""\ @@ -839,7 +1257,6 @@ def test_async_global_awaited_by(self): PORT = socket_helper.find_unused_port() connections = 0 - # Connect to the test process sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('localhost', {port})) @@ -862,23 +1279,16 @@ async def echo_client(message): assert message == data.decode() writer.close() await writer.wait_closed() - # Signal we are ready to sleep sock.sendall(b"ready") await asyncio.sleep(SHORT_TIMEOUT) async def echo_client_spam(server): async with asyncio.TaskGroup() as tg: - while connections < 1000: + while connections < {NUM_TASKS}: msg = list(ascii_lowercase + digits) random.shuffle(msg) tg.create_task(echo_client("".join(msg))) await asyncio.sleep(0) - # at least a 1000 tasks created. Each task will signal - # when is ready to avoid the race caused by the fact that - # tasks are waited on tg.__exit__ and we cannot signal when - # that happens otherwise - # at this point all client tasks completed without assertion errors - # let's wrap up the test server.close() await server.wait_closed() @@ -893,323 +1303,243 @@ async def main(): asyncio.run(main()) """ ) - stack_trace = None + with os_helper.temp_dir() as work_dir: script_dir = os.path.join(work_dir, "script_pkg") os.mkdir(script_dir) - # Create a socket server to communicate with the target process - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.bind(("localhost", port)) - server_socket.settimeout(SHORT_TIMEOUT) - server_socket.listen(1) + + server_socket = _create_server_socket(port) script_name = _make_test_script(script_dir, "script", script) client_socket = None + try: - p = subprocess.Popen([sys.executable, script_name]) - client_socket, _ = server_socket.accept() - server_socket.close() - for _ in range(1000): - expected_response = b"ready" - response = client_socket.recv(len(expected_response)) - self.assertEqual(response, expected_response) - for _ in busy_retry(SHORT_TIMEOUT): + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None + + # Wait for NUM_TASKS "ready" signals try: - all_awaited_by = get_all_awaited_by(p.pid) - except RuntimeError as re: - # This call reads a linked list in another process with - # no synchronization. That occasionally leads to invalid - # reads. Here we avoid making the test flaky. - msg = str(re) - if msg.startswith("Task list appears corrupted"): - continue - elif msg.startswith( - "Invalid linked list structure reading remote memory" - ): - continue - elif msg.startswith("Unknown error reading memory"): - continue - elif msg.startswith("Unhandled frame owner"): - continue - raise # Unrecognized exception, safest not to ignore it - else: - break - # expected: a list of two elements: 1 thread, 1 interp - self.assertEqual(len(all_awaited_by), 2) - # expected: a tuple with the thread ID and the awaited_by list - self.assertEqual(len(all_awaited_by[0]), 2) - # expected: no tasks in the fallback per-interp task list - self.assertEqual(all_awaited_by[1], (0, [])) - entries = all_awaited_by[0][1] - # expected: at least 1000 pending tasks - self.assertGreaterEqual(len(entries), 1000) - # the first three tasks stem from the code structure - main_stack = [ - FrameInfo([taskgroups.__file__, ANY, "TaskGroup._aexit"]), - FrameInfo( - [taskgroups.__file__, ANY, "TaskGroup.__aexit__"] - ), - FrameInfo([script_name, 60, "main"]), - ] - self.assertIn( - TaskInfo( - [ANY, "Task-1", [CoroInfo([main_stack, ANY])], []] - ), - entries, - ) - self.assertIn( - TaskInfo( - [ - ANY, - "server task", + _wait_for_n_signals(client_socket, b"ready", NUM_TASKS) + except RuntimeError as e: + self.fail(str(e)) + + all_awaited_by = get_all_awaited_by(p.pid) + + # Expected: a list of two elements: 1 thread, 1 interp + self.assertEqual(len(all_awaited_by), 2) + # Expected: a tuple with the thread ID and the awaited_by list + self.assertEqual(len(all_awaited_by[0]), 2) + # Expected: no tasks in the fallback per-interp task list + self.assertEqual(all_awaited_by[1], (0, [])) + + entries = all_awaited_by[0][1] + # Expected: at least NUM_TASKS pending tasks + self.assertGreaterEqual(len(entries), NUM_TASKS) + + # Check the main task structure + main_stack = [ + FrameInfo( + [taskgroups.__file__, ANY, "TaskGroup._aexit", ANY] + ), + FrameInfo( + [taskgroups.__file__, ANY, "TaskGroup.__aexit__", ANY] + ), + FrameInfo([script_name, ANY, "main", ANY]), + ] + self.assertIn( + TaskInfo( + [ANY, "Task-1", [CoroInfo([main_stack, ANY])], []] + ), + entries, + ) + self.assertIn( + TaskInfo( [ - CoroInfo( - [ + ANY, + "server task", + [ + CoroInfo( [ - FrameInfo( - [ - base_events.__file__, - ANY, - "Server.serve_forever", - ] - ) - ], - ANY, - ] - ) - ], - [ - CoroInfo( - [ + [ + FrameInfo( + [ + base_events.__file__, + ANY, + "Server.serve_forever", + ANY, + ] + ) + ], + ANY, + ] + ) + ], + [ + CoroInfo( [ - FrameInfo( - [ - taskgroups.__file__, - ANY, - "TaskGroup._aexit", - ] - ), - FrameInfo( - [ - taskgroups.__file__, - ANY, - "TaskGroup.__aexit__", - ] - ), - FrameInfo( - [script_name, ANY, "main"] - ), - ], - ANY, - ] - ) - ], - ] - ), - entries, - ) - self.assertIn( - TaskInfo( - [ - ANY, - "Task-4", + [ + FrameInfo( + [ + taskgroups.__file__, + ANY, + "TaskGroup._aexit", + ANY, + ] + ), + FrameInfo( + [ + taskgroups.__file__, + ANY, + "TaskGroup.__aexit__", + ANY, + ] + ), + FrameInfo( + [script_name, ANY, "main", ANY] + ), + ], + ANY, + ] + ) + ], + ] + ), + entries, + ) + self.assertIn( + TaskInfo( [ - CoroInfo( - [ + ANY, + "Task-4", + [ + CoroInfo( [ - FrameInfo( - [tasks.__file__, ANY, "sleep"] - ), - FrameInfo( - [ - script_name, - 38, - "echo_client", - ] - ), - ], - ANY, - ] - ) - ], - [ - CoroInfo( - [ + [ + FrameInfo( + [ + tasks.__file__, + ANY, + "sleep", + ANY, + ] + ), + FrameInfo( + [ + script_name, + ANY, + "echo_client", + ANY, + ] + ), + ], + ANY, + ] + ) + ], + [ + CoroInfo( [ - FrameInfo( - [ - taskgroups.__file__, - ANY, - "TaskGroup._aexit", - ] - ), - FrameInfo( - [ - taskgroups.__file__, - ANY, - "TaskGroup.__aexit__", - ] - ), - FrameInfo( - [ - script_name, - 41, - "echo_client_spam", - ] - ), - ], - ANY, - ] - ) - ], - ] - ), - entries, - ) + [ + FrameInfo( + [ + taskgroups.__file__, + ANY, + "TaskGroup._aexit", + ANY, + ] + ), + FrameInfo( + [ + taskgroups.__file__, + ANY, + "TaskGroup.__aexit__", + ANY, + ] + ), + FrameInfo( + [ + script_name, + ANY, + "echo_client_spam", + ANY, + ] + ), + ], + ANY, + ] + ) + ], + ] + ), + entries, + ) - expected_awaited_by = [ - CoroInfo( - [ - [ - FrameInfo( - [ - taskgroups.__file__, - ANY, - "TaskGroup._aexit", - ] - ), - FrameInfo( - [ - taskgroups.__file__, - ANY, - "TaskGroup.__aexit__", - ] - ), - FrameInfo( - [script_name, 41, "echo_client_spam"] - ), - ], - ANY, + # Find tasks awaited by echo_client_spam via TaskGroup + def matches_awaited_by_pattern(task): + if len(task.awaited_by) != 1: + return False + coro = task.awaited_by[0] + if len(coro.call_stack) != 3: + return False + funcnames = [f.funcname for f in coro.call_stack] + return funcnames == [ + "TaskGroup._aexit", + "TaskGroup.__aexit__", + "echo_client_spam", ] - ) - ] - tasks_with_awaited = [ - task - for task in entries - if task.awaited_by == expected_awaited_by - ] - self.assertGreaterEqual(len(tasks_with_awaited), 1000) - - # the final task will have some random number, but it should for - # sure be one of the echo client spam horde (In windows this is not true - # for some reason) - if sys.platform != "win32": - self.assertEqual( - tasks_with_awaited[-1].awaited_by, - entries[-1].awaited_by, - ) - except PermissionError: - self.skipTest( - "Insufficient permissions to read the stack trace" - ) - finally: - if client_socket is not None: - client_socket.close() - p.kill() - p.terminate() - p.wait(timeout=SHORT_TIMEOUT) - @skip_if_not_supported - @unittest.skipIf( - sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, - "Test only runs on Linux with process_vm_readv support", - ) - def test_self_trace(self): - stack_trace = get_stack_trace(os.getpid()) - # Is possible that there are more threads, so we check that the - # expected stack traces are in the result (looking at you Windows!) - this_tread_stack = None - for thread_id, stack in stack_trace: - if thread_id == threading.get_native_id(): - this_tread_stack = stack - break - self.assertIsNotNone(this_tread_stack) - self.assertEqual( - stack[:2], - [ - FrameInfo( - [ - __file__, - get_stack_trace.__code__.co_firstlineno + 2, - "get_stack_trace", - ] - ), - FrameInfo( - [ - __file__, - self.test_self_trace.__code__.co_firstlineno + 6, - "TestGetStackTrace.test_self_trace", + tasks_with_awaited = [ + task + for task in entries + if matches_awaited_by_pattern(task) ] - ), - ], - ) + self.assertGreaterEqual(len(tasks_with_awaited), NUM_TASKS) + + # Final task should be from echo client spam (not on Windows) + if sys.platform != "win32": + self.assertEqual( + tasks_with_awaited[-1].awaited_by, + entries[-1].awaited_by, + ) + finally: + _cleanup_sockets(client_socket, server_socket) @skip_if_not_supported @unittest.skipIf( sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, "Test only runs on Linux with process_vm_readv support", ) - @requires_gil_enabled("Free threaded builds don't have an 'active thread'") - def test_only_active_thread(self): - # Test that only_active_thread parameter works correctly + def test_async_global_awaited_by_from_non_main_thread(self): port = find_unused_port() script = textwrap.dedent( f"""\ - import time, sys, socket, threading + import asyncio + import socket + import threading + import time - # Connect to the test process sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('localhost', {port})) - def worker_thread(name, barrier, ready_event): - barrier.wait() # Synchronize thread start - ready_event.wait() # Wait for main thread signal - # Sleep to keep thread alive - time.sleep(10_000) - - def main_work(): - # Do busy work to hold the GIL - sock.sendall(b"working\\n") - count = 0 - while count < 100000000: - count += 1 - if count % 10000000 == 0: - pass # Keep main thread busy - sock.sendall(b"done\\n") - - # Create synchronization primitives - num_threads = 3 - barrier = threading.Barrier(num_threads + 1) # +1 for main thread - ready_event = threading.Event() - - # Start worker threads - threads = [] - for i in range(num_threads): - t = threading.Thread(target=worker_thread, args=(f"Worker-{{i}}", barrier, ready_event)) - t.start() - threads.append(t) - - # Wait for all threads to be ready - barrier.wait() - - # Signal ready to parent process - sock.sendall(b"ready\\n") + async def worker_main(): + task = asyncio.create_task( + asyncio.sleep(10_000), + name="worker task", + ) + await asyncio.sleep(0) + sock.sendall(f"ready:{{threading.get_native_id()}}\\n".encode()) + await task - # Signal threads to start waiting - ready_event.set() + def run_worker_loop(): + asyncio.run(worker_main()) - # Now do busy work to hold the GIL - main_work() + threading.Thread( + target=run_worker_loop, + name="async-worker", + daemon=True, + ).start() + time.sleep(10_000) """ ) @@ -1217,88 +1547,2463 @@ def main_work(): script_dir = os.path.join(work_dir, "script_pkg") os.mkdir(script_dir) - # Create a socket server to communicate with the target process - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.bind(("localhost", port)) - server_socket.settimeout(SHORT_TIMEOUT) - server_socket.listen(1) - + server_socket = _create_server_socket(port) script_name = _make_test_script(script_dir, "script", script) client_socket = None + try: - p = subprocess.Popen([sys.executable, script_name]) - client_socket, _ = server_socket.accept() - server_socket.close() + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None + + response = _wait_for_signal(client_socket, b"ready:") + worker_thread_id = int( + response.split(b"ready:", 1)[1].splitlines()[0] + ) + + for _ in busy_retry(SHORT_TIMEOUT): + all_awaited_by = get_all_awaited_by(p.pid) + if any( + task.task_name == "worker task" + for info in all_awaited_by + if info.thread_id == worker_thread_id + for task in info.awaited_by + ): + break + else: + self.fail( + "get_all_awaited_by() did not report " + "the asyncio task from the non-main thread" + ) + finally: + _cleanup_sockets(client_socket, server_socket) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_async_remote_stack_trace_from_non_main_thread(self): + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import asyncio + import socket + import threading + import time + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def blocking_call(): + sock.sendall(f"ready:{{threading.get_native_id()}}\\n".encode()) + time.sleep(10_000) + + async def worker_task(): + await asyncio.sleep(0) + blocking_call() + + async def worker_main(): + task = asyncio.create_task( + worker_task(), + name="worker task", + ) + await task + + def run_worker_loop(): + asyncio.run(worker_main()) + + threading.Thread( + target=run_worker_loop, + name="async-worker", + daemon=True, + ).start() + time.sleep(10_000) + """ + ) + + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + + server_socket = _create_server_socket(port) + script_name = _make_test_script(script_dir, "script", script) + client_socket = None + + try: + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None + + response = _wait_for_signal(client_socket, b"ready:") + worker_thread_id = int( + response.split(b"ready:", 1)[1].splitlines()[0] + ) + + for _ in busy_retry(SHORT_TIMEOUT): + stack_trace = get_async_stack_trace(p.pid) + if any( + task.task_name == "worker task" + for info in stack_trace + if info.thread_id == worker_thread_id + for task in info.awaited_by + ): + break + else: + self.fail( + "get_async_stack_trace() did not report " + "the running asyncio task from the non-main thread" + ) + finally: + _cleanup_sockets(client_socket, server_socket) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_self_trace(self): + stack_trace = get_stack_trace(os.getpid()) + + this_thread_stack = None + for interpreter_info in stack_trace: + for thread_info in interpreter_info.threads: + if thread_info.thread_id == threading.get_native_id(): + this_thread_stack = thread_info.frame_info + break + if this_thread_stack: + break + + self.assertIsNotNone(this_thread_stack) + # Check the top two frames + self.assertGreaterEqual(len(this_thread_stack), 2) + self.assertEqual(this_thread_stack[0].funcname, "get_stack_trace") + self.assertTrue(this_thread_stack[0].filename.endswith("test_external_inspection.py")) + self.assertEqual(this_thread_stack[1].funcname, "TestGetStackTrace.test_self_trace") + self.assertTrue(this_thread_stack[1].filename.endswith("test_external_inspection.py")) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + @requires_subinterpreters + def test_subinterpreter_stack_trace(self): + port = find_unused_port() + + import pickle + + subinterp_code = textwrap.dedent(f""" + import socket + import time + + def sub_worker(): + def nested_func(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + sock.sendall(b"ready:sub\\n") + time.sleep(10_000) + nested_func() + + sub_worker() + """).strip() + + pickled_code = pickle.dumps(subinterp_code) + + script = textwrap.dedent( + f""" + from concurrent import interpreters + import time + import sys + import socket + import threading + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def main_worker(): + sock.sendall(b"ready:main\\n") + time.sleep(10_000) + + def run_subinterp(): + subinterp = interpreters.create() + import pickle + pickled_code = {pickled_code!r} + subinterp_code = pickle.loads(pickled_code) + subinterp.exec(subinterp_code) + + sub_thread = threading.Thread(target=run_subinterp) + sub_thread.start() + + main_thread = threading.Thread(target=main_worker) + main_thread.start() + + main_thread.join() + sub_thread.join() + """ + ) + + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + + server_socket = _create_server_socket(port) + script_name = _make_test_script(script_dir, "script", script) + client_sockets = [] + + try: + with _managed_subprocess([sys.executable, script_name]) as p: + # Accept connections from both main and subinterpreter + responses = set() + while len(responses) < 2: + try: + client_socket, _ = server_socket.accept() + client_sockets.append(client_socket) + response = client_socket.recv(1024) + if b"ready:main" in response: + responses.add("main") + if b"ready:sub" in response: + responses.add("sub") + except socket.timeout: + break + + server_socket.close() + server_socket = None + + stack_trace = get_stack_trace(p.pid) + + # Verify we have at least one interpreter + self.assertGreaterEqual(len(stack_trace), 1) + + # Look for main interpreter (ID 0) and subinterpreter (ID > 0) + main_interp = None + sub_interp = None + for interpreter_info in stack_trace: + if interpreter_info.interpreter_id == 0: + main_interp = interpreter_info + elif interpreter_info.interpreter_id > 0: + sub_interp = interpreter_info + + self.assertIsNotNone( + main_interp, "Main interpreter should be present" + ) + + # Check main interpreter has expected stack trace + main_found = self._find_frame_in_trace( + [main_interp], lambda f: f.funcname == "main_worker" + ) + self.assertIsNotNone( + main_found, + "Main interpreter should have main_worker in stack", + ) + + # If subinterpreter is present, check its stack trace + if sub_interp: + sub_found = self._find_frame_in_trace( + [sub_interp], + lambda f: f.funcname + in ("sub_worker", "nested_func"), + ) + self.assertIsNotNone( + sub_found, + "Subinterpreter should have sub_worker or nested_func in stack", + ) + finally: + _cleanup_sockets(*client_sockets, server_socket) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + @requires_subinterpreters + def test_multiple_subinterpreters_with_threads(self): + port = find_unused_port() + + import pickle + + subinterp1_code = textwrap.dedent(f""" + import socket + import time + import threading + + def worker1(): + def nested_func(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + sock.sendall(b"ready:sub1-t1\\n") + time.sleep(10_000) + nested_func() + + def worker2(): + def nested_func(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + sock.sendall(b"ready:sub1-t2\\n") + time.sleep(10_000) + nested_func() + + t1 = threading.Thread(target=worker1) + t2 = threading.Thread(target=worker2) + t1.start() + t2.start() + t1.join() + t2.join() + """).strip() + + subinterp2_code = textwrap.dedent(f""" + import socket + import time + import threading + + def worker1(): + def nested_func(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + sock.sendall(b"ready:sub2-t1\\n") + time.sleep(10_000) + nested_func() + + def worker2(): + def nested_func(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + sock.sendall(b"ready:sub2-t2\\n") + time.sleep(10_000) + nested_func() + + t1 = threading.Thread(target=worker1) + t2 = threading.Thread(target=worker2) + t1.start() + t2.start() + t1.join() + t2.join() + """).strip() + + pickled_code1 = pickle.dumps(subinterp1_code) + pickled_code2 = pickle.dumps(subinterp2_code) + + script = textwrap.dedent( + f""" + from concurrent import interpreters + import time + import sys + import socket + import threading + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def main_worker(): + sock.sendall(b"ready:main\\n") + time.sleep(10_000) + + def run_subinterp1(): + subinterp = interpreters.create() + import pickle + pickled_code = {pickled_code1!r} + subinterp_code = pickle.loads(pickled_code) + subinterp.exec(subinterp_code) + + def run_subinterp2(): + subinterp = interpreters.create() + import pickle + pickled_code = {pickled_code2!r} + subinterp_code = pickle.loads(pickled_code) + subinterp.exec(subinterp_code) + + sub1_thread = threading.Thread(target=run_subinterp1) + sub2_thread = threading.Thread(target=run_subinterp2) + sub1_thread.start() + sub2_thread.start() + + main_thread = threading.Thread(target=main_worker) + main_thread.start() + + main_thread.join() + sub1_thread.join() + sub2_thread.join() + """ + ) + + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + + server_socket = _create_server_socket(port, backlog=5) + script_name = _make_test_script(script_dir, "script", script) + client_sockets = [] + + try: + with _managed_subprocess([sys.executable, script_name]) as p: + # Accept connections from main and all subinterpreter threads + expected_responses = { + "ready:main", + "ready:sub1-t1", + "ready:sub1-t2", + "ready:sub2-t1", + "ready:sub2-t2", + } + responses = set() + + while len(responses) < 5: + try: + client_socket, _ = server_socket.accept() + client_sockets.append(client_socket) + response = client_socket.recv(1024) + response_str = response.decode().strip() + if response_str in expected_responses: + responses.add(response_str) + except socket.timeout: + break + + server_socket.close() + server_socket = None + + stack_trace = get_stack_trace(p.pid) + + # Verify we have multiple interpreters + self.assertGreaterEqual(len(stack_trace), 2) + + # Count interpreters by ID + interpreter_ids = { + interp.interpreter_id for interp in stack_trace + } + self.assertIn( + 0, + interpreter_ids, + "Main interpreter should be present", + ) + self.assertGreaterEqual(len(interpreter_ids), 3) + + # Count total threads + total_threads = sum( + len(interp.threads) for interp in stack_trace + ) + self.assertGreaterEqual(total_threads, 5) + + # Look for expected function names + all_funcnames = set() + for interpreter_info in stack_trace: + for thread_info in interpreter_info.threads: + for frame in thread_info.frame_info: + all_funcnames.add(frame.funcname) + + expected_funcs = { + "main_worker", + "worker1", + "worker2", + "nested_func", + } + found_funcs = expected_funcs.intersection(all_funcnames) + self.assertGreater(len(found_funcs), 0) + finally: + _cleanup_sockets(*client_sockets, server_socket) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + @requires_gil_enabled("Free threaded builds don't have an 'active thread'") + def test_only_active_thread(self): + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import time, sys, socket, threading + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def worker_thread(name, barrier, ready_event): + barrier.wait() + ready_event.wait() + time.sleep(10_000) + + def main_work(): + sock.sendall(b"working\\n") + count = 0 + while count < 100000000: + count += 1 + if count % 10000000 == 0: + pass + sock.sendall(b"done\\n") + + num_threads = 3 + barrier = threading.Barrier(num_threads + 1) + ready_event = threading.Event() + + threads = [] + for i in range(num_threads): + t = threading.Thread(target=worker_thread, args=(f"Worker-{{i}}", barrier, ready_event)) + t.start() + threads.append(t) + + barrier.wait() + sock.sendall(b"ready\\n") + ready_event.set() + main_work() + """ + ) + + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + + server_socket = _create_server_socket(port) + script_name = _make_test_script(script_dir, "script", script) + client_socket = None + + try: + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None + + # Wait for ready and working signals + _wait_for_signal(client_socket, [b"ready", b"working"]) + + # Get stack trace with all threads + unwinder_all = RemoteUnwinder(p.pid, all_threads=True) + for _ in busy_retry(SHORT_TIMEOUT): + with contextlib.suppress(*TRANSIENT_ERRORS): + all_traces = unwinder_all.get_stack_trace() + found = self._find_frame_in_trace( + all_traces, + lambda f: f.funcname == "main_work" + and f.location.lineno > 12, + ) + if found: + break + else: + self.fail( + "Main thread did not start its busy work on time" + ) + + # Get stack trace with only GIL holder + unwinder_gil = RemoteUnwinder( + p.pid, only_active_thread=True + ) + # Use condition to retry until we capture a thread holding the GIL + # (sampling may catch moments with no GIL holder on slow CI) + gil_traces = _get_stack_trace_with_retry( + unwinder_gil, + condition=lambda t: sum(len(i.threads) for i in t) >= 1, + ) + + # Count threads + total_threads = sum( + len(interp.threads) for interp in all_traces + ) + self.assertGreater(total_threads, 1) + + total_gil_threads = sum( + len(interp.threads) for interp in gil_traces + ) + self.assertEqual(total_gil_threads, 1) + + # Get the GIL holder thread ID + gil_thread_id = None + for interpreter_info in gil_traces: + if interpreter_info.threads: + gil_thread_id = interpreter_info.threads[ + 0 + ].thread_id + break + + # Get all thread IDs + all_thread_ids = [] + for interpreter_info in all_traces: + for thread_info in interpreter_info.threads: + all_thread_ids.append(thread_info.thread_id) + + self.assertIn(gil_thread_id, all_thread_ids) + finally: + _cleanup_sockets(client_socket, server_socket) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_opcodes_collection(self): + """Test that opcodes are collected when the opcodes flag is set.""" + script = textwrap.dedent( + """\ + import time, sys, socket + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def foo(): + sock.sendall(b"ready") + time.sleep(10_000) + + foo() + """ + ) + + def get_trace_with_opcodes(pid): + return RemoteUnwinder(pid, opcodes=True).get_stack_trace() + + stack_trace, _ = self._run_script_and_get_trace( + script, get_trace_with_opcodes, wait_for_signals=b"ready" + ) + + # Find our foo frame and verify it has an opcode + foo_frame = self._find_frame_in_trace( + stack_trace, lambda f: f.funcname == "foo" + ) + self.assertIsNotNone(foo_frame, "Could not find foo frame") + self.assertIsInstance(foo_frame.opcode, int) + self.assertGreaterEqual(foo_frame.opcode, 0) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_location_tuple_format(self): + """Test that location is a 4-tuple (lineno, end_lineno, col_offset, end_col_offset).""" + script = textwrap.dedent( + """\ + import time, sys, socket + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def foo(): + sock.sendall(b"ready") + time.sleep(10_000) + + foo() + """ + ) + + def get_trace_with_opcodes(pid): + return RemoteUnwinder(pid, opcodes=True).get_stack_trace() + + stack_trace, _ = self._run_script_and_get_trace( + script, get_trace_with_opcodes, wait_for_signals=b"ready" + ) + + # Find our foo frame + foo_frame = self._find_frame_in_trace( + stack_trace, lambda f: f.funcname == "foo" + ) + self.assertIsNotNone(foo_frame, "Could not find foo frame") + + # Check location is a 4-tuple with valid values + location = foo_frame.location + self.assertIsInstance(location, tuple) + self.assertEqual(len(location), 4) + lineno, end_lineno, col_offset, end_col_offset = location + self.assertIsInstance(lineno, int) + self.assertGreater(lineno, 0) + self.assertIsInstance(end_lineno, int) + self.assertGreaterEqual(end_lineno, lineno) + self.assertIsInstance(col_offset, int) + self.assertGreaterEqual(col_offset, 0) + self.assertIsInstance(end_col_offset, int) + self.assertGreaterEqual(end_col_offset, col_offset) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_location_tuple_exact_values(self): + """Test exact values of location tuple including column offsets.""" + script = textwrap.dedent( + """\ + import time, sys, socket + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def foo(): + sock.sendall(b"ready") + time.sleep(10_000) + + foo() + """ + ) + + def get_trace_with_opcodes(pid): + return RemoteUnwinder(pid, opcodes=True).get_stack_trace() + + stack_trace, _ = self._run_script_and_get_trace( + script, get_trace_with_opcodes, wait_for_signals=b"ready" + ) + + foo_frame = self._find_frame_in_trace( + stack_trace, lambda f: f.funcname == "foo" + ) + self.assertIsNotNone(foo_frame, "Could not find foo frame") + + # Can catch either sock.sendall (line 7) or time.sleep (line 8) + location = foo_frame.location + valid_locations = [ + (7, 7, 4, 26), # sock.sendall(b"ready") + (8, 8, 4, 22), # time.sleep(10_000) + ] + actual = (location.lineno, location.end_lineno, + location.col_offset, location.end_col_offset) + self.assertIn(actual, valid_locations) + + @skip_if_not_supported + @unittest.skipIf(sys._is_gil_enabled(), "Requires free-threading") + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Requires process_vm_readv", + ) + def test_tlbc_cache_refresh_after_growth(self): + # Reproducer from gh-157660. + script = textwrap.dedent("""\ + import os, threading + from _remote_debugging import RemoteUnwinder + from _queue import SimpleQueue + from test import support + + go = threading.Lock() + stop = threading.Lock() + go.acquire() + stop.acquire() + ready = SimpleQueue() + + def leaf(): + ready.put(None) + stop.acquire() + + def start_leaf(): + ready.put(None) + go.acquire() + leaf() + + def park(): + ready.put(None) + stop.acquire() + + def leaf_count(u): + return sum( + f.funcname == "leaf" + for i in u.get_stack_trace() + for t in i.threads for f in t.frame_info + ) + + # SimpleQueue.put() and Lock.acquire() do not push Python frames. + # Once notified, the worker's stack stays stable until go is released. + threading.Thread(target=leaf, daemon=True).start() + ready.get(timeout=support.SHORT_TIMEOUT) + for _ in range(16): + threading.Thread(target=park, daemon=True).start() + ready.get(timeout=support.SHORT_TIMEOUT) + threading.Thread(target=start_leaf, daemon=True).start() + ready.get(timeout=support.SHORT_TIMEOUT) + + u = RemoteUnwinder(os.getpid(), all_threads=True, cache_frames=False) + assert leaf_count(u) == 1 + go.release() + ready.get(timeout=support.SHORT_TIMEOUT) + assert leaf_count(u) == 2 + """) + result = subprocess.run( + [sys.executable, "-X", "gil=0", "-X", "tlbc=1", "-c", script], + capture_output=True, + text=True, + timeout=SHORT_TIMEOUT, + ) + self.assertEqual( + result.returncode, 0, + f"stdout: {result.stdout}\nstderr: {result.stderr}", + ) + + @skip_if_not_supported + @unittest.skipIf(sys._is_gil_enabled(), "Requires free-threading") + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Requires process_vm_readv", + ) + def test_tlbc_cache_refresh_after_slot_fill(self): + # Reproducer from gh-157660. + script = textwrap.dedent("""\ + import os, threading + from _remote_debugging import RemoteUnwinder + from _queue import SimpleQueue + + go = threading.Lock() + stop = threading.Lock() + go.acquire() + stop.acquire() + ready = SimpleQueue() + + def leaf(): + ready.put(None) + stop.acquire() + + def start_leaf(): + ready.put(None) + go.acquire() + leaf() + + from test import support + + def lines(u): + return sorted( + f.location.lineno + for i in u.get_stack_trace() + for t in i.threads for f in t.frame_info + if f.funcname == "leaf" + ) + + # SimpleQueue.put() and Lock.acquire() do not push Python frames. + # Once notified, the worker's stack stays stable until go is released. + threading.Thread(target=leaf, daemon=True).start() + ready.get(timeout=support.SHORT_TIMEOUT) + threading.Thread(target=start_leaf, daemon=True).start() + ready.get(timeout=support.SHORT_TIMEOUT) + u = RemoteUnwinder(os.getpid(), all_threads=True, cache_frames=False) + before = lines(u) + # The notification can be observed before put() returns, so either + # line in leaf() is a valid sample. + assert before in ([12], [13]), before + go.release() + ready.get(timeout=support.SHORT_TIMEOUT) + cached = lines(u) + assert len(cached) == 2, cached + assert all(line in (12, 13) for line in cached), cached + """) + result = subprocess.run( + [sys.executable, "-X", "gil=0", "-X", "tlbc=1", "-c", script], + capture_output=True, + text=True, + timeout=SHORT_TIMEOUT, + ) + self.assertEqual( + result.returncode, 0, + f"stdout: {result.stdout}\nstderr: {result.stderr}", + ) + + +class TestUnsupportedPlatformHandling(unittest.TestCase): + @unittest.skipIf( + sys.platform in ("linux", "darwin", "win32"), + "Test only runs on unsupported platforms (not Linux, macOS, or Windows)", + ) + @unittest.skipIf( + sys.platform == "android", "Android raises Linux-specific exception" + ) + def test_unsupported_platform_error(self): + with self.assertRaises(RuntimeError) as cm: + RemoteUnwinder(os.getpid()) + + self.assertIn( + "Reading the PyRuntime section is not supported on this platform", + str(cm.exception), + ) + + +@requires_remote_subprocess_debugging() +class TestDetectionOfThreadStatus(RemoteInspectionTestBase): + def _run_thread_status_test(self, mode, check_condition): + """ + Common pattern for thread status detection tests. + + Args: + mode: Profiling mode (PROFILING_MODE_CPU, PROFILING_MODE_GIL, etc.) + check_condition: Function(statuses, sleeper_tid, busy_tid) -> bool + """ + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import time, sys, socket, threading + import os + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def sleeper(): + tid = threading.get_native_id() + sock.sendall(f'ready:sleeper:{{tid}}\\n'.encode()) + time.sleep(10000) + + def busy(): + tid = threading.get_native_id() + sock.sendall(f'ready:busy:{{tid}}\\n'.encode()) + x = 0 + while True: + x = x + 1 + time.sleep(0.5) + + t1 = threading.Thread(target=sleeper) + t2 = threading.Thread(target=busy) + t1.start() + t2.start() + sock.sendall(b'ready:main\\n') + t1.join() + t2.join() + sock.close() + """ + ) + + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + + server_socket = _create_server_socket(port) + script_name = _make_test_script( + script_dir, "thread_status_script", script + ) + client_socket = None + + try: + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None + + # Wait for all ready signals and parse TIDs + response = _wait_for_signal( + client_socket, + [b"ready:main", b"ready:sleeper", b"ready:busy"], + ) + + sleeper_tid = None + busy_tid = None + for line in response.split(b"\n"): + if line.startswith(b"ready:sleeper:"): + try: + sleeper_tid = int(line.split(b":")[-1]) + except (ValueError, IndexError): + pass + elif line.startswith(b"ready:busy:"): + try: + busy_tid = int(line.split(b":")[-1]) + except (ValueError, IndexError): + pass + + self.assertIsNotNone( + sleeper_tid, "Sleeper thread id not received" + ) + self.assertIsNotNone( + busy_tid, "Busy thread id not received" + ) + + # Sample until we see expected thread states + statuses = {} + unwinder = RemoteUnwinder( + p.pid, + all_threads=True, + mode=mode, + skip_non_matching_threads=False, + ) + for _ in busy_retry(SHORT_TIMEOUT): + with contextlib.suppress(*TRANSIENT_ERRORS): + traces = unwinder.get_stack_trace() + statuses = self._get_thread_statuses(traces) + + if check_condition( + statuses, sleeper_tid, busy_tid + ): + break + + return statuses, sleeper_tid, busy_tid + finally: + _cleanup_sockets(client_socket, server_socket) + + @unittest.skipIf( + sys.platform not in ("linux", "darwin", "win32"), + "Test only runs on supported platforms (Linux, macOS, or Windows)", + ) + @unittest.skipIf( + sys.platform == "android", "Android raises Linux-specific exception" + ) + def test_thread_status_detection(self): + def check_cpu_status(statuses, sleeper_tid, busy_tid): + return ( + sleeper_tid in statuses + and busy_tid in statuses + and not (statuses[sleeper_tid] & THREAD_STATUS_ON_CPU) + and (statuses[busy_tid] & THREAD_STATUS_ON_CPU) + ) + + statuses, sleeper_tid, busy_tid = self._run_thread_status_test( + PROFILING_MODE_CPU, check_cpu_status + ) + + self.assertIn(sleeper_tid, statuses) + self.assertIn(busy_tid, statuses) + self.assertFalse( + statuses[sleeper_tid] & THREAD_STATUS_ON_CPU, + "Sleeper thread should be off CPU", + ) + self.assertTrue( + statuses[busy_tid] & THREAD_STATUS_ON_CPU, + "Busy thread should be on CPU", + ) + + @unittest.skipIf( + sys.platform not in ("linux", "darwin", "win32"), + "Test only runs on supported platforms (Linux, macOS, or Windows)", + ) + @unittest.skipIf( + sys.platform == "android", "Android raises Linux-specific exception" + ) + def test_thread_status_gil_detection(self): + def check_gil_status(statuses, sleeper_tid, busy_tid): + return ( + sleeper_tid in statuses + and busy_tid in statuses + and not (statuses[sleeper_tid] & THREAD_STATUS_HAS_GIL) + and (statuses[busy_tid] & THREAD_STATUS_HAS_GIL) + ) + + statuses, sleeper_tid, busy_tid = self._run_thread_status_test( + PROFILING_MODE_GIL, check_gil_status + ) + + self.assertIn(sleeper_tid, statuses) + self.assertIn(busy_tid, statuses) + self.assertFalse( + statuses[sleeper_tid] & THREAD_STATUS_HAS_GIL, + "Sleeper thread should not have GIL", + ) + self.assertTrue( + statuses[busy_tid] & THREAD_STATUS_HAS_GIL, + "Busy thread should have GIL", + ) + + @unittest.skipIf( + sys.platform not in ("linux", "darwin", "win32"), + "Test only runs on supported platforms (Linux, macOS, or Windows)", + ) + @unittest.skipIf( + sys.platform == "android", "Android raises Linux-specific exception" + ) + def test_thread_status_all_mode_detection(self): + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import socket + import threading + import time + import sys + + def sleeper_thread(): + conn = socket.create_connection(("localhost", {port})) + conn.sendall(b"sleeper:" + str(threading.get_native_id()).encode()) + while True: + time.sleep(1) + + def busy_thread(): + conn = socket.create_connection(("localhost", {port})) + conn.sendall(b"busy:" + str(threading.get_native_id()).encode()) + while True: + sum(range(100000)) + + t1 = threading.Thread(target=sleeper_thread) + t2 = threading.Thread(target=busy_thread) + t1.start() + t2.start() + t1.join() + t2.join() + """ + ) + + with os_helper.temp_dir() as tmp_dir: + script_file = make_script(tmp_dir, "script", script) + server_socket = _create_server_socket(port, backlog=2) + client_sockets = [] + + try: + with _managed_subprocess( + [sys.executable, script_file], + ) as p: + sleeper_tid = None + busy_tid = None + + # Receive thread IDs from the child process + for _ in range(2): + client_socket, _ = server_socket.accept() + client_sockets.append(client_socket) + line = client_socket.recv(1024) + if line: + if line.startswith(b"sleeper:"): + try: + sleeper_tid = int(line.split(b":")[-1]) + except (ValueError, IndexError): + pass + elif line.startswith(b"busy:"): + try: + busy_tid = int(line.split(b":")[-1]) + except (ValueError, IndexError): + pass + + server_socket.close() + server_socket = None + + statuses = {} + unwinder = RemoteUnwinder( + p.pid, + all_threads=True, + mode=PROFILING_MODE_ALL, + skip_non_matching_threads=False, + ) + for _ in busy_retry(SHORT_TIMEOUT): + with contextlib.suppress(*TRANSIENT_ERRORS): + traces = unwinder.get_stack_trace() + statuses = self._get_thread_statuses(traces) + + # Check ALL mode provides both GIL and CPU info + if ( + sleeper_tid in statuses + and busy_tid in statuses + and not ( + statuses[sleeper_tid] + & THREAD_STATUS_ON_CPU + ) + and not ( + statuses[sleeper_tid] + & THREAD_STATUS_HAS_GIL + ) + and (statuses[busy_tid] & THREAD_STATUS_ON_CPU) + and ( + statuses[busy_tid] & THREAD_STATUS_HAS_GIL + ) + ): + break + + self.assertIsNotNone( + sleeper_tid, "Sleeper thread id not received" + ) + self.assertIsNotNone( + busy_tid, "Busy thread id not received" + ) + self.assertIn(sleeper_tid, statuses) + self.assertIn(busy_tid, statuses) + + # Sleeper: off CPU, no GIL + self.assertFalse( + statuses[sleeper_tid] & THREAD_STATUS_ON_CPU, + "Sleeper should be off CPU", + ) + self.assertFalse( + statuses[sleeper_tid] & THREAD_STATUS_HAS_GIL, + "Sleeper should not have GIL", + ) + + # Busy: on CPU, has GIL + self.assertTrue( + statuses[busy_tid] & THREAD_STATUS_ON_CPU, + "Busy should be on CPU", + ) + self.assertTrue( + statuses[busy_tid] & THREAD_STATUS_HAS_GIL, + "Busy should have GIL", + ) + finally: + _cleanup_sockets(*client_sockets, server_socket) + + def _make_exception_test_script(self, port): + """Create script with exception and normal threads for testing.""" + return textwrap.dedent( + f"""\ + import socket + import threading + import time + + def exception_thread(): + conn = socket.create_connection(("localhost", {port})) + conn.sendall(b"exception:" + str(threading.get_native_id()).encode()) + try: + raise ValueError("test exception") + except ValueError: + while True: + time.sleep(0.01) + + def normal_thread(): + conn = socket.create_connection(("localhost", {port})) + conn.sendall(b"normal:" + str(threading.get_native_id()).encode()) + while True: + sum(range(1000)) + + t1 = threading.Thread(target=exception_thread) + t2 = threading.Thread(target=normal_thread) + t1.start() + t2.start() + t1.join() + t2.join() + """ + ) + + @contextmanager + def _run_exception_test_process(self): + """Context manager to run exception test script and yield thread IDs and process.""" + port = find_unused_port() + script = self._make_exception_test_script(port) + + with os_helper.temp_dir() as tmp_dir: + script_file = make_script(tmp_dir, "script", script) + server_socket = _create_server_socket(port, backlog=2) + client_sockets = [] + + try: + with _managed_subprocess([sys.executable, script_file]) as p: + exception_tid = None + normal_tid = None + + for _ in range(2): + client_socket, _ = server_socket.accept() + client_sockets.append(client_socket) + line = client_socket.recv(1024) + if line: + if line.startswith(b"exception:"): + try: + exception_tid = int(line.split(b":")[-1]) + except (ValueError, IndexError): + pass + elif line.startswith(b"normal:"): + try: + normal_tid = int(line.split(b":")[-1]) + except (ValueError, IndexError): + pass + + server_socket.close() + server_socket = None + + yield p, exception_tid, normal_tid + finally: + _cleanup_sockets(*client_sockets, server_socket) + + @unittest.skipIf( + sys.platform not in ("linux", "darwin", "win32"), + "Test only runs on supported platforms (Linux, macOS, or Windows)", + ) + @unittest.skipIf( + sys.platform == "android", "Android raises Linux-specific exception" + ) + def test_thread_status_exception_detection(self): + """Test that THREAD_STATUS_HAS_EXCEPTION is set when thread has an active exception.""" + with self._run_exception_test_process() as (p, exception_tid, normal_tid): + self.assertIsNotNone(exception_tid, "Exception thread id not received") + self.assertIsNotNone(normal_tid, "Normal thread id not received") + + statuses = {} + unwinder = RemoteUnwinder( + p.pid, + all_threads=True, + mode=PROFILING_MODE_ALL, + skip_non_matching_threads=False, + ) + for _ in busy_retry(SHORT_TIMEOUT): + with contextlib.suppress(*TRANSIENT_ERRORS): + traces = unwinder.get_stack_trace() + statuses = self._get_thread_statuses(traces) + + if ( + exception_tid in statuses + and normal_tid in statuses + and (statuses[exception_tid] & THREAD_STATUS_HAS_EXCEPTION) + and not (statuses[normal_tid] & THREAD_STATUS_HAS_EXCEPTION) + ): + break + + self.assertIn(exception_tid, statuses) + self.assertIn(normal_tid, statuses) + self.assertTrue( + statuses[exception_tid] & THREAD_STATUS_HAS_EXCEPTION, + "Exception thread should have HAS_EXCEPTION flag", + ) + self.assertFalse( + statuses[normal_tid] & THREAD_STATUS_HAS_EXCEPTION, + "Normal thread should not have HAS_EXCEPTION flag", + ) + + @unittest.skipIf( + sys.platform not in ("linux", "darwin", "win32"), + "Test only runs on supported platforms (Linux, macOS, or Windows)", + ) + @unittest.skipIf( + sys.platform == "android", "Android raises Linux-specific exception" + ) + def test_thread_status_exception_mode_filtering(self): + """Test that PROFILING_MODE_EXCEPTION correctly filters threads.""" + with self._run_exception_test_process() as (p, exception_tid, normal_tid): + self.assertIsNotNone(exception_tid, "Exception thread id not received") + self.assertIsNotNone(normal_tid, "Normal thread id not received") + + unwinder = RemoteUnwinder( + p.pid, + all_threads=True, + mode=PROFILING_MODE_EXCEPTION, + skip_non_matching_threads=True, + ) + for _ in busy_retry(SHORT_TIMEOUT): + with contextlib.suppress(*TRANSIENT_ERRORS): + traces = unwinder.get_stack_trace() + statuses = self._get_thread_statuses(traces) + + if exception_tid in statuses: + self.assertNotIn( + normal_tid, + statuses, + "Normal thread should be filtered out in exception mode", + ) + return + + self.fail("Never found exception thread in exception mode") + +@requires_remote_subprocess_debugging() +class TestExceptionDetectionScenarios(RemoteInspectionTestBase): + """Test exception detection across all scenarios. + + This class verifies the exact conditions under which THREAD_STATUS_HAS_EXCEPTION + is set. Each test covers a specific scenario: + + 1. except_block: Thread inside except block + -> SHOULD have HAS_EXCEPTION (exc_info->exc_value is set) + + 2. finally_propagating: Exception propagating through finally block + -> SHOULD have HAS_EXCEPTION (current_exception is set) + + 3. finally_after_except: Finally block after except handled exception + -> Should NOT have HAS_EXCEPTION (exc_info cleared after except) + + 4. finally_no_exception: Finally block with no exception raised + -> Should NOT have HAS_EXCEPTION (no exception state) + """ + + def _make_single_scenario_script(self, port, scenario): + """Create script for a single exception scenario.""" + scenarios = { + "except_block": f"""\ +import socket +import threading +import time + +def target_thread(): + '''Inside except block - exception info is present''' + conn = socket.create_connection(("localhost", {port})) + conn.sendall(b"ready:" + str(threading.get_native_id()).encode()) + try: + raise ValueError("test") + except ValueError: + while True: + time.sleep(0.01) + +t = threading.Thread(target=target_thread) +t.start() +t.join() +""", + "finally_propagating": f"""\ +import socket +import threading +import time + +def target_thread(): + '''Exception propagating through finally - current_exception is set''' + conn = socket.create_connection(("localhost", {port})) + conn.sendall(b"ready:" + str(threading.get_native_id()).encode()) + try: + try: + raise ValueError("propagating") + finally: + # Exception is propagating through here + while True: + time.sleep(0.01) + except: + pass # Never reached due to infinite loop + +t = threading.Thread(target=target_thread) +t.start() +t.join() +""", + "finally_after_except": f"""\ +import socket +import threading +import time + +def target_thread(): + '''Finally runs after except handled - exc_info is cleared''' + conn = socket.create_connection(("localhost", {port})) + conn.sendall(b"ready:" + str(threading.get_native_id()).encode()) + try: + raise ValueError("test") + except ValueError: + pass # Exception caught and handled + finally: + while True: + time.sleep(0.01) + +t = threading.Thread(target=target_thread) +t.start() +t.join() +""", + "finally_no_exception": f"""\ +import socket +import threading +import time + +def target_thread(): + '''Finally with no exception at all''' + conn = socket.create_connection(("localhost", {port})) + conn.sendall(b"ready:" + str(threading.get_native_id()).encode()) + try: + pass # No exception + finally: + while True: + time.sleep(0.01) + +t = threading.Thread(target=target_thread) +t.start() +t.join() +""", + } + + return scenarios[scenario] + + @contextmanager + def _run_scenario_process(self, scenario): + """Context manager to run a single scenario and yield thread ID and process.""" + port = find_unused_port() + script = self._make_single_scenario_script(port, scenario) + + with os_helper.temp_dir() as tmp_dir: + script_file = make_script(tmp_dir, "script", script) + server_socket = _create_server_socket(port, backlog=1) + client_socket = None + + try: + with _managed_subprocess([sys.executable, script_file]) as p: + thread_tid = None + + client_socket, _ = server_socket.accept() + line = client_socket.recv(1024) + if line and line.startswith(b"ready:"): + try: + thread_tid = int(line.split(b":")[-1]) + except (ValueError, IndexError): + pass + + server_socket.close() + server_socket = None + + yield p, thread_tid + finally: + _cleanup_sockets(client_socket, server_socket) + + def _check_thread_status( + self, p, thread_tid, condition, condition_name="condition" + ): + """Helper to check thread status with a custom condition. + + This waits until we see 3 consecutive samples where the condition + returns True, which confirms the thread has reached and is stable + in the expected state. Samples that don't match are ignored (the + thread may not have reached the expected state yet). + + Args: + p: Process object with pid attribute + thread_tid: Thread ID to check + condition: Callable(statuses, thread_tid) -> bool that returns + True when the thread is in the expected state + condition_name: Description of condition for error messages + """ + unwinder = RemoteUnwinder( + p.pid, + all_threads=True, + mode=PROFILING_MODE_ALL, + skip_non_matching_threads=False, + ) + + # Wait for 3 consecutive samples matching expected state + matching_samples = 0 + for _ in busy_retry(SHORT_TIMEOUT): + with contextlib.suppress(*TRANSIENT_ERRORS): + traces = unwinder.get_stack_trace() + statuses = self._get_thread_statuses(traces) + + if thread_tid in statuses: + if condition(statuses, thread_tid): + matching_samples += 1 + if matching_samples >= 3: + return # Success - confirmed stable in expected state + else: + # Thread not yet in expected state, reset counter + matching_samples = 0 + + self.fail( + f"Thread did not stabilize in expected state " + f"({condition_name}) within timeout" + ) + + def _check_exception_status(self, p, thread_tid, expect_exception): + """Helper to check if thread has expected exception status.""" + def condition(statuses, tid): + has_exc = bool(statuses[tid] & THREAD_STATUS_HAS_EXCEPTION) + return has_exc == expect_exception + + self._check_thread_status( + p, thread_tid, condition, + condition_name=f"expect_exception={expect_exception}" + ) + + @unittest.skipIf( + sys.platform not in ("linux", "darwin", "win32"), + "Test only runs on supported platforms (Linux, macOS, or Windows)", + ) + @unittest.skipIf( + sys.platform == "android", "Android raises Linux-specific exception" + ) + def test_except_block_has_exception(self): + """Test that thread inside except block has HAS_EXCEPTION flag. - # Wait for ready signal - response = b"" - while b"ready" not in response: - response += client_socket.recv(1024) - - # Wait for the main thread to start its busy work - while b"working" not in response: - response += client_socket.recv(1024) - - # Get stack trace with all threads - unwinder_all = RemoteUnwinder(p.pid, all_threads=True) - for _ in range(10): - # Wait for the main thread to start its busy work - all_traces = unwinder_all.get_stack_trace() - found = False - for thread_id, stack in all_traces: - if not stack: - continue - current_frame = stack[0] - if ( - current_frame.funcname == "main_work" - and current_frame.lineno > 15 - ): - found = True + When a thread is executing inside an except block, exc_info->exc_value + is set, so THREAD_STATUS_HAS_EXCEPTION should be True. + """ + with self._run_scenario_process("except_block") as (p, thread_tid): + self.assertIsNotNone(thread_tid, "Thread ID not received") + self._check_exception_status(p, thread_tid, expect_exception=True) - if found: - break - # Give a bit of time to take the next sample - time.sleep(0.1) + @unittest.skipIf( + sys.platform not in ("linux", "darwin", "win32"), + "Test only runs on supported platforms (Linux, macOS, or Windows)", + ) + @unittest.skipIf( + sys.platform == "android", "Android raises Linux-specific exception" + ) + def test_finally_propagating_has_exception(self): + """Test that finally block with propagating exception has HAS_EXCEPTION flag. + + When an exception is propagating through a finally block (not yet caught), + current_exception is set, so THREAD_STATUS_HAS_EXCEPTION should be True. + """ + with self._run_scenario_process("finally_propagating") as (p, thread_tid): + self.assertIsNotNone(thread_tid, "Thread ID not received") + self._check_exception_status(p, thread_tid, expect_exception=True) + + @unittest.skipIf( + sys.platform not in ("linux", "darwin", "win32"), + "Test only runs on supported platforms (Linux, macOS, or Windows)", + ) + @unittest.skipIf( + sys.platform == "android", "Android raises Linux-specific exception" + ) + def test_finally_after_except_no_exception(self): + """Test that finally block after except has NO HAS_EXCEPTION flag. + + When a finally block runs after an except block has handled the exception, + Python clears exc_info before entering finally, so THREAD_STATUS_HAS_EXCEPTION + should be False. + """ + with self._run_scenario_process("finally_after_except") as (p, thread_tid): + self.assertIsNotNone(thread_tid, "Thread ID not received") + self._check_exception_status(p, thread_tid, expect_exception=False) + + @unittest.skipIf( + sys.platform not in ("linux", "darwin", "win32"), + "Test only runs on supported platforms (Linux, macOS, or Windows)", + ) + @unittest.skipIf( + sys.platform == "android", "Android raises Linux-specific exception" + ) + def test_finally_no_exception_no_flag(self): + """Test that finally block with no exception has NO HAS_EXCEPTION flag. + + When a finally block runs during normal execution (no exception raised), + there is no exception state, so THREAD_STATUS_HAS_EXCEPTION should be False. + """ + with self._run_scenario_process("finally_no_exception") as (p, thread_tid): + self.assertIsNotNone(thread_tid, "Thread ID not received") + self._check_exception_status(p, thread_tid, expect_exception=False) + + +@requires_remote_subprocess_debugging() +class TestFrameCaching(RemoteInspectionTestBase): + """Test that frame caching produces correct results. + + Uses socket-based synchronization for deterministic testing. + All tests verify cache reuse via object identity checks (assertIs). + """ + + @contextmanager + def _target_process(self, script_body): + """Context manager for running a target process with socket sync.""" + port = find_unused_port() + script = f"""\ +import socket +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.connect(('localhost', {port})) +{textwrap.dedent(script_body)} +""" + + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + + server_socket = _create_server_socket(port) + script_name = _make_test_script(script_dir, "script", script) + client_socket = None + + try: + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None + + def make_unwinder(cache_frames=True): + return RemoteUnwinder( + p.pid, all_threads=True, cache_frames=cache_frames + ) + + yield p, client_socket, make_unwinder + finally: + _cleanup_sockets(client_socket, server_socket) + + def _get_frames_with_retry(self, unwinder, required_funcs): + """Get frames containing required_funcs, with retry for transient errors.""" + for _ in range(MAX_TRIES): + with contextlib.suppress(*TRANSIENT_ERRORS): + traces = unwinder.get_stack_trace() + for interp in traces: + for thread in interp.threads: + funcs = {f.funcname for f in thread.frame_info} + if required_funcs.issubset(funcs): + return thread.frame_info + time.sleep(RETRY_DELAY) + return None + + def _sample_frames( + self, + client_socket, + unwinder, + wait_signal, + send_ack, + required_funcs, + expected_frames=1, + ): + """Wait for signal, sample frames with retry until required funcs present, send ack.""" + _wait_for_signal(client_socket, wait_signal) + frames = None + for _ in range(MAX_TRIES): + frames = self._get_frames_with_retry(unwinder, required_funcs) + if frames and len(frames) >= expected_frames: + break + time.sleep(RETRY_DELAY) + client_socket.sendall(send_ack) + return frames + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_cache_hit_same_stack(self): + """Test that consecutive samples reuse cached parent frame objects. + + The current frame (index 0) is always re-read from memory to get + updated line numbers, so it may be a different object. Parent frames + (index 1+) should be identical objects from cache. + """ + script_body = """\ + def level3(): + sock.sendall(b"sync1") + sock.recv(16) + sock.sendall(b"sync2") + sock.recv(16) + sock.sendall(b"sync3") + sock.recv(16) + + def level2(): + level3() + + def level1(): + level2() + + level1() + """ + + with self._target_process(script_body) as ( + p, + client_socket, + make_unwinder, + ): + unwinder = make_unwinder(cache_frames=True) + expected = {"level1", "level2", "level3"} + + frames1 = self._sample_frames( + client_socket, unwinder, b"sync1", b"ack", expected + ) + frames2 = self._sample_frames( + client_socket, unwinder, b"sync2", b"ack", expected + ) + frames3 = self._sample_frames( + client_socket, unwinder, b"sync3", b"done", expected + ) + + self.assertIsNotNone(frames1) + self.assertIsNotNone(frames2) + self.assertIsNotNone(frames3) + self.assertEqual(len(frames1), len(frames2)) + self.assertEqual(len(frames2), len(frames3)) + + # Current frame (index 0) is always re-read, so check value equality + self.assertEqual(frames1[0].funcname, frames2[0].funcname) + self.assertEqual(frames2[0].funcname, frames3[0].funcname) + + # Parent frames (index 1+) must be identical objects (cache reuse) + for i in range(1, len(frames1)): + f1, f2, f3 = frames1[i], frames2[i], frames3[i] + self.assertIs( + f1, f2, f"Frame {i}: samples 1-2 must be same object" + ) + self.assertIs( + f2, f3, f"Frame {i}: samples 2-3 must be same object" + ) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_line_number_updates_in_same_frame(self): + """Test that line numbers are correctly updated when execution moves within a function. + + When the profiler samples at different points within the same function, + it must report the correct line number for each sample, not stale cached values. + """ + script_body = """\ + def outer(): + inner() + + def inner(): + sock.sendall(b"line_a"); sock.recv(16) + sock.sendall(b"line_b"); sock.recv(16) + sock.sendall(b"line_c"); sock.recv(16) + sock.sendall(b"line_d"); sock.recv(16) + + outer() + """ + + with self._target_process(script_body) as ( + p, + client_socket, + make_unwinder, + ): + unwinder = make_unwinder(cache_frames=True) + + frames_a = self._sample_frames( + client_socket, unwinder, b"line_a", b"ack", {"inner"} + ) + frames_b = self._sample_frames( + client_socket, unwinder, b"line_b", b"ack", {"inner"} + ) + frames_c = self._sample_frames( + client_socket, unwinder, b"line_c", b"ack", {"inner"} + ) + frames_d = self._sample_frames( + client_socket, unwinder, b"line_d", b"done", {"inner"} + ) + + self.assertIsNotNone(frames_a) + self.assertIsNotNone(frames_b) + self.assertIsNotNone(frames_c) + self.assertIsNotNone(frames_d) + + # Get the 'inner' frame from each sample (should be index 0) + inner_a = frames_a[0] + inner_b = frames_b[0] + inner_c = frames_c[0] + inner_d = frames_d[0] + + self.assertEqual(inner_a.funcname, "inner") + self.assertEqual(inner_b.funcname, "inner") + self.assertEqual(inner_c.funcname, "inner") + self.assertEqual(inner_d.funcname, "inner") + + # Line numbers must be different and increasing (execution moves forward) + self.assertLess( + inner_a.location.lineno, inner_b.location.lineno, "Line B should be after line A" + ) + self.assertLess( + inner_b.location.lineno, inner_c.location.lineno, "Line C should be after line B" + ) + self.assertLess( + inner_c.location.lineno, inner_d.location.lineno, "Line D should be after line C" + ) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_cache_invalidation_on_return(self): + """Test cache invalidation when stack shrinks (function returns).""" + script_body = """\ + def inner(): + sock.sendall(b"at_inner") + sock.recv(16) + + def outer(): + inner() + sock.sendall(b"at_outer") + sock.recv(16) + + outer() + """ + + with self._target_process(script_body) as ( + p, + client_socket, + make_unwinder, + ): + unwinder = make_unwinder(cache_frames=True) + + frames_deep = self._sample_frames( + client_socket, + unwinder, + b"at_inner", + b"ack", + {"inner", "outer"}, + ) + frames_shallow = self._sample_frames( + client_socket, unwinder, b"at_outer", b"done", {"outer"} + ) + + self.assertIsNotNone(frames_deep) + self.assertIsNotNone(frames_shallow) + + funcs_deep = [f.funcname for f in frames_deep] + funcs_shallow = [f.funcname for f in frames_shallow] + + self.assertIn("inner", funcs_deep) + self.assertIn("outer", funcs_deep) + self.assertNotIn("inner", funcs_shallow) + self.assertIn("outer", funcs_shallow) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_cache_invalidation_on_call(self): + """Test cache invalidation when stack grows (new function called).""" + script_body = """\ + def deeper(): + sock.sendall(b"at_deeper") + sock.recv(16) + + def middle(): + sock.sendall(b"at_middle") + sock.recv(16) + deeper() + + def top(): + middle() + + top() + """ + + with self._target_process(script_body) as ( + p, + client_socket, + make_unwinder, + ): + unwinder = make_unwinder(cache_frames=True) + + frames_before = self._sample_frames( + client_socket, + unwinder, + b"at_middle", + b"ack", + {"middle", "top"}, + ) + frames_after = self._sample_frames( + client_socket, + unwinder, + b"at_deeper", + b"done", + {"deeper", "middle", "top"}, + ) + + self.assertIsNotNone(frames_before) + self.assertIsNotNone(frames_after) + + funcs_before = [f.funcname for f in frames_before] + funcs_after = [f.funcname for f in frames_after] + + self.assertIn("middle", funcs_before) + self.assertIn("top", funcs_before) + self.assertNotIn("deeper", funcs_before) + + self.assertIn("deeper", funcs_after) + self.assertIn("middle", funcs_after) + self.assertIn("top", funcs_after) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_partial_stack_reuse(self): + """Test that unchanged parent frames are reused from cache when top frame moves.""" + script_body = """\ + def level4(): + sock.sendall(b"sync1") + sock.recv(16) + sock.sendall(b"sync2") + sock.recv(16) + + def level3(): + level4() + + def level2(): + level3() + + def level1(): + level2() + + level1() + """ + + with self._target_process(script_body) as ( + p, + client_socket, + make_unwinder, + ): + unwinder = make_unwinder(cache_frames=True) + + # Sample 1: level4 at first sendall + frames1 = self._sample_frames( + client_socket, + unwinder, + b"sync1", + b"ack", + {"level1", "level2", "level3", "level4"}, + ) + # Sample 2: level4 at second sendall (same stack, different line) + frames2 = self._sample_frames( + client_socket, + unwinder, + b"sync2", + b"done", + {"level1", "level2", "level3", "level4"}, + ) + + self.assertIsNotNone(frames1) + self.assertIsNotNone(frames2) + + def find_frame(frames, funcname): + for f in frames: + if f.funcname == funcname: + return f + return None + + # level4 should have different line numbers (it moved) + l4_1 = find_frame(frames1, "level4") + l4_2 = find_frame(frames2, "level4") + self.assertIsNotNone(l4_1) + self.assertIsNotNone(l4_2) + self.assertNotEqual( + l4_1.location.lineno, + l4_2.location.lineno, + "level4 should be at different lines", + ) + + # Parent frames (level1, level2, level3) should be reused from cache + for name in ["level1", "level2", "level3"]: + f1 = find_frame(frames1, name) + f2 = find_frame(frames2, name) + self.assertIsNotNone(f1, f"{name} missing from sample 1") + self.assertIsNotNone(f2, f"{name} missing from sample 2") + self.assertIs(f1, f2, f"{name} should be reused from cache") + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_recursive_frames(self): + """Test caching with same function appearing multiple times (recursion).""" + script_body = """\ + def recurse(n): + if n <= 0: + sock.sendall(b"sync1") + sock.recv(16) + sock.sendall(b"sync2") + sock.recv(16) else: - self.fail( - "Main thread did not start its busy work on time" - ) + recurse(n - 1) - # Get stack trace with only GIL holder - unwinder_gil = RemoteUnwinder(p.pid, only_active_thread=True) - gil_traces = unwinder_gil.get_stack_trace() + recurse(5) + """ - except PermissionError: - self.skipTest( - "Insufficient permissions to read the stack trace" + with self._target_process(script_body) as ( + p, + client_socket, + make_unwinder, + ): + unwinder = make_unwinder(cache_frames=True) + + frames1 = self._sample_frames( + client_socket, unwinder, b"sync1", b"ack", {"recurse"} + ) + frames2 = self._sample_frames( + client_socket, unwinder, b"sync2", b"done", {"recurse"} + ) + + self.assertIsNotNone(frames1) + self.assertIsNotNone(frames2) + + # Should have multiple "recurse" frames (6 total: recurse(5) down to recurse(0)) + recurse_count = sum(1 for f in frames1 if f.funcname == "recurse") + self.assertEqual(recurse_count, 6, "Should have 6 recursive frames") + + self.assertEqual(len(frames1), len(frames2)) + + # Current frame (index 0) is re-read, check value equality + self.assertEqual(frames1[0].funcname, frames2[0].funcname) + + # Parent frames (index 1+) should be identical objects (cache reuse) + for i in range(1, len(frames1)): + self.assertIs( + frames1[i], + frames2[i], + f"Frame {i}: recursive frames must be same object", + ) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_cache_vs_no_cache_equivalence(self): + """Test that cache_frames=True and cache_frames=False produce equivalent results.""" + script_body = """\ + def level3(): + sock.sendall(b"ready"); sock.recv(16) + + def level2(): + level3() + + def level1(): + level2() + + level1() + """ + + with self._target_process(script_body) as ( + p, + client_socket, + make_unwinder, + ): + _wait_for_signal(client_socket, b"ready") + + # Sample with cache + unwinder_cache = make_unwinder(cache_frames=True) + frames_cached = self._get_frames_with_retry( + unwinder_cache, {"level1", "level2", "level3"} + ) + + # Sample without cache + unwinder_no_cache = make_unwinder(cache_frames=False) + frames_no_cache = self._get_frames_with_retry( + unwinder_no_cache, {"level1", "level2", "level3"} + ) + + client_socket.sendall(b"done") + + self.assertIsNotNone(frames_cached) + self.assertIsNotNone(frames_no_cache) + + # Same number of frames + self.assertEqual(len(frames_cached), len(frames_no_cache)) + + # Same function names in same order + funcs_cached = [f.funcname for f in frames_cached] + funcs_no_cache = [f.funcname for f in frames_no_cache] + self.assertEqual(funcs_cached, funcs_no_cache) + + # For level3 (leaf frame), due to timing races we can be at either + # sock.sendall() or sock.recv() - both are valid. For parent frames, + # the locations should match exactly. + # Valid locations for level3: line 3 has two statements + # sock.sendall(b"ready") -> col 4-26 + # sock.recv(16) -> col 28-41 + level3_valid_cols = {(4, 26), (28, 41)} + + for i in range(len(frames_cached)): + loc_cached = frames_cached[i].location + loc_no_cache = frames_no_cache[i].location + + if frames_cached[i].funcname == "level3": + # Leaf frame: can be at either statement + self.assertIn( + (loc_cached.col_offset, loc_cached.end_col_offset), + level3_valid_cols, ) - finally: - if client_socket is not None: - client_socket.close() - p.kill() - p.terminate() - p.wait(timeout=SHORT_TIMEOUT) + self.assertIn( + (loc_no_cache.col_offset, loc_no_cache.end_col_offset), + level3_valid_cols, + ) + else: + # Parent frames: must match exactly + self.assertEqual(loc_cached, loc_no_cache) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_cache_per_thread_isolation(self): + """Test that frame cache is per-thread and cache invalidation works independently.""" + script_body = """\ + import threading + + lock = threading.Lock() + + def sync(msg): + with lock: + sock.sendall(msg + b"\\n") + sock.recv(1) + + # Thread 1 functions + def baz1(): + sync(b"t1:baz1") + + def bar1(): + baz1() + + def blech1(): + sync(b"t1:blech1") + + def foo1(): + bar1() # Goes down to baz1, syncs + blech1() # Returns up, goes down to blech1, syncs + + # Thread 2 functions + def baz2(): + sync(b"t2:baz2") - # Verify we got multiple threads in all_traces - self.assertGreater( - len(all_traces), 1, "Should have multiple threads" + def bar2(): + baz2() + + def blech2(): + sync(b"t2:blech2") + + def foo2(): + bar2() # Goes down to baz2, syncs + blech2() # Returns up, goes down to blech2, syncs + + t1 = threading.Thread(target=foo1) + t2 = threading.Thread(target=foo2) + t1.start() + t2.start() + t1.join() + t2.join() + """ + + with self._target_process(script_body) as ( + p, + client_socket, + make_unwinder, + ): + unwinder = make_unwinder(cache_frames=True) + + # Message dispatch table: signal -> required functions for that thread + dispatch = { + b"t1:baz1": {"baz1", "bar1", "foo1"}, + b"t2:baz2": {"baz2", "bar2", "foo2"}, + b"t1:blech1": {"blech1", "foo1"}, + b"t2:blech2": {"blech2", "foo2"}, + } + + # Track results for each sync point + results = {} + + # Process 4 sync points (order depends on thread scheduling) + buffer = _wait_for_signal(client_socket, b"\n") + for i in range(4): + # Extract first message from buffer + msg, sep, buffer = buffer.partition(b"\n") + self.assertIn(msg, dispatch, f"Unexpected message: {msg!r}") + + # Sample frames for the thread at this sync point + required_funcs = dispatch[msg] + frames = self._get_frames_with_retry(unwinder, required_funcs) + self.assertIsNotNone(frames, f"Thread not found for {msg!r}") + results[msg] = [f.funcname for f in frames] + + # Release thread and wait for next message (if not last) + client_socket.sendall(b"k") + if i < 3: + buffer += _wait_for_signal(client_socket, b"\n") + + # Validate Phase 1: baz snapshots + t1_baz = results.get(b"t1:baz1") + t2_baz = results.get(b"t2:baz2") + self.assertIsNotNone(t1_baz, "Missing t1:baz1 snapshot") + self.assertIsNotNone(t2_baz, "Missing t2:baz2 snapshot") + + # Thread 1 at baz1: should have foo1->bar1->baz1 + self.assertIn("baz1", t1_baz) + self.assertIn("bar1", t1_baz) + self.assertIn("foo1", t1_baz) + self.assertNotIn("blech1", t1_baz) + # No cross-contamination + self.assertNotIn("baz2", t1_baz) + self.assertNotIn("bar2", t1_baz) + self.assertNotIn("foo2", t1_baz) + + # Thread 2 at baz2: should have foo2->bar2->baz2 + self.assertIn("baz2", t2_baz) + self.assertIn("bar2", t2_baz) + self.assertIn("foo2", t2_baz) + self.assertNotIn("blech2", t2_baz) + # No cross-contamination + self.assertNotIn("baz1", t2_baz) + self.assertNotIn("bar1", t2_baz) + self.assertNotIn("foo1", t2_baz) + + # Validate Phase 2: blech snapshots (cache invalidation test) + t1_blech = results.get(b"t1:blech1") + t2_blech = results.get(b"t2:blech2") + self.assertIsNotNone(t1_blech, "Missing t1:blech1 snapshot") + self.assertIsNotNone(t2_blech, "Missing t2:blech2 snapshot") + + # Thread 1 at blech1: bar1/baz1 should be GONE (cache invalidated) + self.assertIn("blech1", t1_blech) + self.assertIn("foo1", t1_blech) + self.assertNotIn( + "bar1", t1_blech, "Cache not invalidated: bar1 still present" + ) + self.assertNotIn( + "baz1", t1_blech, "Cache not invalidated: baz1 still present" + ) + # No cross-contamination + self.assertNotIn("blech2", t1_blech) + + # Thread 2 at blech2: bar2/baz2 should be GONE (cache invalidated) + self.assertIn("blech2", t2_blech) + self.assertIn("foo2", t2_blech) + self.assertNotIn( + "bar2", t2_blech, "Cache not invalidated: bar2 still present" + ) + self.assertNotIn( + "baz2", t2_blech, "Cache not invalidated: baz2 still present" + ) + # No cross-contamination + self.assertNotIn("blech1", t2_blech) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_new_unwinder_with_stale_last_profiled_frame(self): + """Test that a new unwinder returns complete stack when cache lookup misses.""" + script_body = """\ + def level4(): + sock.sendall(b"sync1") + sock.recv(16) + sock.sendall(b"sync2") + sock.recv(16) + + def level3(): + level4() + + def level2(): + level3() + + def level1(): + level2() + + level1() + """ + + with self._target_process(script_body) as ( + p, + client_socket, + make_unwinder, + ): + expected = {"level1", "level2", "level3", "level4"} + + # First unwinder samples - this sets last_profiled_frame in target + unwinder1 = make_unwinder(cache_frames=True) + frames1 = self._sample_frames( + client_socket, unwinder1, b"sync1", b"ack", expected ) - # Verify we got exactly one thread in gil_traces - self.assertEqual( - len(gil_traces), 1, "Should have exactly one GIL holder" + # Create NEW unwinder (empty cache) and sample + # The target still has last_profiled_frame set from unwinder1 + unwinder2 = make_unwinder(cache_frames=True) + frames2 = self._sample_frames( + client_socket, unwinder2, b"sync2", b"done", expected ) - # The GIL holder should be in the all_traces list - gil_thread_id = gil_traces[0][0] - all_thread_ids = [trace[0] for trace in all_traces] - self.assertIn( - gil_thread_id, - all_thread_ids, - "GIL holder should be among all threads", + self.assertIsNotNone(frames1) + self.assertIsNotNone(frames2) + + funcs1 = [f.funcname for f in frames1] + funcs2 = [f.funcname for f in frames2] + + # Both should have all levels + for level in ["level1", "level2", "level3", "level4"]: + self.assertIn(level, funcs1, f"{level} missing from first sample") + self.assertIn(level, funcs2, f"{level} missing from second sample") + + # Should have same stack depth + self.assertEqual( + len(frames1), + len(frames2), + "New unwinder should return complete stack despite stale last_profiled_frame", + ) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_cache_exhaustion(self): + """Test cache works when frame limit (1024) is exceeded. + + FRAME_CACHE_MAX_FRAMES=1024. With 1100 recursive frames, + the cache can't store all of them but should still work. + """ + # Use 1100 to exceed FRAME_CACHE_MAX_FRAMES=1024 + depth = 1100 + script_body = f"""\ +import sys +sys.setrecursionlimit(2000) + +def recurse(n): + if n <= 0: + sock.sendall(b"ready") + sock.recv(16) # wait for ack + sock.sendall(b"ready2") + sock.recv(16) # wait for done + return + recurse(n - 1) + +recurse({depth}) +""" + + with self._target_process(script_body) as ( + p, + client_socket, + make_unwinder, + ): + unwinder_cache = make_unwinder(cache_frames=True) + unwinder_no_cache = make_unwinder(cache_frames=False) + + frames_cached = self._sample_frames( + client_socket, + unwinder_cache, + b"ready", + b"ack", + {"recurse"}, + expected_frames=1102, + ) + # Sample again with no cache for comparison + frames_no_cache = self._sample_frames( + client_socket, + unwinder_no_cache, + b"ready2", + b"done", + {"recurse"}, + expected_frames=1102, ) + self.assertIsNotNone(frames_cached) + self.assertIsNotNone(frames_no_cache) + + # Both should have many recurse frames (> 1024 limit) + cached_count = [f.funcname for f in frames_cached].count("recurse") + no_cache_count = [f.funcname for f in frames_no_cache].count("recurse") + + self.assertGreater( + cached_count, 1000, "Should have >1000 recurse frames" + ) + self.assertGreater( + no_cache_count, 1000, "Should have >1000 recurse frames" + ) + + # Both modes should produce same frame count + self.assertEqual( + len(frames_cached), + len(frames_no_cache), + "Cache exhaustion should not affect stack completeness", + ) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_get_stats(self): + """Test that get_stats() returns statistics when stats=True.""" + script_body = """\ + sock.sendall(b"ready") + sock.recv(16) + """ + + with self._target_process(script_body) as (p, client_socket, _): + unwinder = RemoteUnwinder(p.pid, all_threads=True, stats=True) + _wait_for_signal(client_socket, b"ready") + + # Take a sample + _get_stack_trace_with_retry(unwinder) + + stats = unwinder.get_stats() + client_socket.sendall(b"done") + + # Verify expected keys exist + expected_keys = [ + "total_samples", + "frame_cache_hits", + "frame_cache_misses", + "frame_cache_partial_hits", + "frames_read_from_cache", + "frames_read_from_memory", + "frame_cache_hit_rate", + "batched_read_attempts", + "batched_read_successes", + "batched_read_misses", + "batched_read_segments_requested", + "batched_read_segments_completed", + "batched_read_success_rate", + "batched_read_segment_completion_rate", + ] + for key in expected_keys: + self.assertIn(key, stats) + + self.assertEqual(stats["total_samples"], 1) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_get_stats_disabled_raises(self): + """Test that get_stats() raises RuntimeError when stats=False.""" + script_body = """\ + sock.sendall(b"ready") + sock.recv(16) + """ + + with self._target_process(script_body) as (p, client_socket, _): + unwinder = RemoteUnwinder( + p.pid, all_threads=True + ) # stats=False by default + _wait_for_signal(client_socket, b"ready") + + with self.assertRaises(RuntimeError): + unwinder.get_stats() + + client_socket.sendall(b"done") + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index a4bf797aab1..4f253e01c0b 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -57,10 +57,6 @@ def testcommon(formatstr, args, output=None, limit=None, overflowok=False): else: b_format = formatstr ba_format = bytearray(b_format) - b_args = [] - if not isinstance(args, tuple): - args = (args, ) - b_args = tuple(args) if output is None: b_output = ba_output = None else: @@ -69,33 +65,23 @@ def testcommon(formatstr, args, output=None, limit=None, overflowok=False): else: b_output = output ba_output = bytearray(b_output) - testformat(b_format, b_args, b_output, limit, overflowok) - testformat(ba_format, b_args, ba_output, limit, overflowok) + testformat(b_format, args, b_output, limit, overflowok) + testformat(ba_format, args, ba_output, limit, overflowok) -def test_exc(formatstr, args, exception, excmsg): - try: - testformat(formatstr, args) - except exception as exc: - if str(exc) == excmsg: - if verbose: - print("yes") - else: - if verbose: print('no') - print('Unexpected ', exception, ':', repr(str(exc))) - except: - if verbose: print('no') - print('Unexpected exception') - raise - else: - raise TestFailed('did not get expected exception: %s' % excmsg) - -def test_exc_common(formatstr, args, exception, excmsg): - # test str and bytes - test_exc(formatstr, args, exception, excmsg) - test_exc(formatstr.encode('ascii'), args, exception, excmsg) class FormatTest(unittest.TestCase): + def check_exc(self, formatstr, args, exception, excmsg): + with self.assertRaisesRegex(exception, re.escape(excmsg)): + testformat(formatstr, args) + + def check_exc_common(self, formatstr, args, exception, excmsg): + # test str and bytes + self.check_exc(formatstr, args, exception, excmsg) + if isinstance(args, dict): + args = {k.encode('ascii'): v for k, v in args.items()} + self.check_exc(formatstr.encode('ascii'), args, exception, excmsg) + def test_common_format(self): # test the format identifiers that work the same across # str, bytes, and bytearrays (integer, float, oct, hex) @@ -272,45 +258,156 @@ def test_common_format(self): if verbose: print('Testing exceptions') - test_exc_common('%', (), ValueError, "incomplete format") - test_exc_common('% %s', 1, ValueError, - "unsupported format character '%' (0x25) at index 2") + test_exc_common = self.check_exc_common + test_exc_common('abc %', (), ValueError, "stray % at position 4") + test_exc_common('abc % %s', 1, ValueError, + "stray % at position 4 or unexpected format character '%' at position 6") + test_exc_common('abc %z', 1, ValueError, + "unsupported format %z at position 4") + test_exc_common("abc %Id", 1, ValueError, + "unsupported format %I at position 4") + test_exc_common("abc %'d", 1, ValueError, + "stray % at position 4 or unexpected format character \"'\" at position 5") + test_exc_common("abc %1 d", 1, ValueError, + "stray % at position 4 or unexpected format character ' ' at position 6") + test_exc_common('abc % (x)r', {}, ValueError, + "stray % at position 4 or unexpected format character '(' at position 6") + test_exc_common('abc %((x)r', {}, ValueError, + "stray % or incomplete format key at position 4") + test_exc_common('%r %r', 1, TypeError, + "not enough arguments for format string (got 1)") + test_exc_common('%r %r', (1,), TypeError, + "not enough arguments for format string (got 1)") + test_exc_common('%r', (), TypeError, + "not enough arguments for format string (got 0)") + test_exc_common('abc %' + '9'*50 + 'r', 1, ValueError, + "width too big at position 4") + test_exc_common('abc %.' + '9'*50 + 'r', 1, ValueError, + "precision too big at position 4") + test_exc_common('%r %*r', 1, TypeError, + "not enough arguments for format string (got 1)") + test_exc_common('%r %*r', (1,), TypeError, + "not enough arguments for format string (got 1)") + test_exc_common('%*r', (1,), TypeError, + "not enough arguments for format string (got 1)") + test_exc_common('%*r', (), TypeError, + "not enough arguments for format string (got 0)") + test_exc_common('%r %.*r', 1, TypeError, + "not enough arguments for format string (got 1)") + test_exc_common('%r %.*r', (1,), TypeError, + "not enough arguments for format string (got 1)") + test_exc_common('%.*r', (1,), TypeError, + "not enough arguments for format string (got 1)") + test_exc_common('%.*r', (), TypeError, + "not enough arguments for format string (got 0)") + test_exc_common('%(x)r', 1, TypeError, + "format requires a mapping, not int") + test_exc_common('%*r', 1, TypeError, + "not enough arguments for format string (got 1)") + test_exc_common('%*r', 3.14, TypeError, + "not enough arguments for format string (got 1)") + test_exc_common('%*r', (3.14, 1), TypeError, + "format argument 1: * requires int, not float") + test_exc_common('%.*r', 1, TypeError, + "not enough arguments for format string (got 1)") + test_exc_common('%.*r', 3.14, TypeError, + "not enough arguments for format string (got 1)") + test_exc_common('%.*r', (3.14, 1), TypeError, + "format argument 1: * requires int, not float") + test_exc_common('%*r', (2**1000, 1), OverflowError, + "format argument 1: too big for width") + test_exc_common('%*r', (-2**1000, 1), OverflowError, + "format argument 1: too big for width") + test_exc_common('%.*r', (2**1000, 1), OverflowError, + "format argument 1: too big for precision") + test_exc_common('%.*r', (-2**1000, 1), OverflowError, + "format argument 1: too big for precision") test_exc_common('%d', '1', TypeError, - "%d format: a real number is required, not str") + "format argument: %d requires a real number, not str") test_exc_common('%d', b'1', TypeError, - "%d format: a real number is required, not bytes") + "format argument: %d requires a real number, not bytes") + test_exc_common('%d', ('1',), TypeError, + "format argument 1: %d requires a real number, not str") test_exc_common('%x', '1', TypeError, - "%x format: an integer is required, not str") + "format argument: %x requires an integer, not str") test_exc_common('%x', 3.14, TypeError, - "%x format: an integer is required, not float") + "format argument: %x requires an integer, not float") + test_exc_common('%x', ('1',), TypeError, + "format argument 1: %x requires an integer, not str") test_exc_common('%i', '1', TypeError, - "%i format: a real number is required, not str") + "format argument: %i requires a real number, not str") test_exc_common('%i', b'1', TypeError, - "%i format: a real number is required, not bytes") + "format argument: %i requires a real number, not bytes") + test_exc_common('%g', '1', TypeError, + "format argument: %g requires a real number, not str") + test_exc_common('%g', ('1',), TypeError, + "format argument 1: %g requires a real number, not str") def test_str_format(self): testformat("%r", "\u0378", "'\\u0378'") # non printable testformat("%a", "\u0378", "'\\u0378'") # non printable testformat("%r", "\u0374", "'\u0374'") # printable testformat("%a", "\u0374", "'\\u0374'") # printable + testformat('%(x)r', {'x': 1}, '1') # Test exception for unknown format characters, etc. if verbose: print('Testing exceptions') + test_exc = self.check_exc test_exc('abc %b', 1, ValueError, - "unsupported format character 'b' (0x62) at index 5") - #test_exc(unicode('abc %\u3000','raw-unicode-escape'), 1, ValueError, - # "unsupported format character '?' (0x3000) at index 5") - test_exc('%g', '1', TypeError, "must be real number, not str") + "unsupported format %b at position 4") + test_exc("abc %\nd", 1, ValueError, + "stray % at position 4 or unexpected format character U+000A at position 5") + test_exc("abc %\x1fd", 1, ValueError, + "stray % at position 4 or unexpected format character U+001F at position 5") + test_exc("abc %\x7fd", 1, ValueError, + "stray % at position 4 or unexpected format character U+007F at position 5") + test_exc("abc %\x80d", 1, ValueError, + "stray % at position 4 or unexpected format character U+0080 at position 5") + test_exc('abc %äd', 1, ValueError, + "stray % at position 4 or unexpected format character 'ä' (U+00E4) at position 5") + test_exc('abc %€d', 1, ValueError, + "stray % at position 4 or unexpected format character '€' (U+20AC) at position 5") test_exc('no format', '1', TypeError, - "not all arguments converted during string formatting") - test_exc('%c', -1, OverflowError, "%c arg not in range(0x110000)") + "not all arguments converted during string formatting (required 0, got 1)") + test_exc('%r', (1, 2), TypeError, + "not all arguments converted during string formatting (required 1, got 2)") + test_exc('%(x)r %r', {'x': 1}, ValueError, + "format requires a parenthesised mapping key at position 6") + test_exc('%(x)*r', {'x': 1}, ValueError, + "* cannot be used with a parenthesised mapping key at position 0") + test_exc('%(x).*r', {'x': 1}, ValueError, + "* cannot be used with a parenthesised mapping key at position 0") + test_exc('%(x)d', {'x': '1'}, TypeError, + "format argument 'x': %d requires a real number, not str") + test_exc('%(x)x', {'x': '1'}, TypeError, + "format argument 'x': %x requires an integer, not str") + test_exc('%(x)g', {'x': '1'}, TypeError, + "format argument 'x': %g requires a real number, not str") + test_exc('%c', -1, OverflowError, + "format argument: %c argument not in range(0x110000)") + test_exc('%c', (-1,), OverflowError, + "format argument 1: %c argument not in range(0x110000)") + test_exc('%(x)c', {'x': -1}, OverflowError, + "format argument 'x': %c argument not in range(0x110000)") test_exc('%c', sys.maxunicode+1, OverflowError, - "%c arg not in range(0x110000)") - #test_exc('%c', 2**128, OverflowError, "%c arg not in range(0x110000)") - test_exc('%c', 3.14, TypeError, "%c requires an int or a unicode character, not float") - test_exc('%c', 'ab', TypeError, "%c requires an int or a unicode character, not a string of length 2") - test_exc('%c', b'x', TypeError, "%c requires an int or a unicode character, not bytes") + "format argument: %c argument not in range(0x110000)") + test_exc('%c', 2**128, OverflowError, + "format argument: %c argument not in range(0x110000)") + test_exc('%c', 3.14, TypeError, + "format argument: %c requires an integer or a unicode character, not float") + test_exc('%c', (3.14,), TypeError, + "format argument 1: %c requires an integer or a unicode character, not float") + test_exc('%(x)c', {'x': 3.14}, TypeError, + "format argument 'x': %c requires an integer or a unicode character, not float") + test_exc('%c', 'ab', TypeError, + "format argument: %c requires an integer or a unicode character, not a string of length 2") + test_exc('%c', ('ab',), TypeError, + "format argument 1: %c requires an integer or a unicode character, not a string of length 2") + test_exc('%(x)c', {'x': 'ab'}, TypeError, + "format argument 'x': %c requires an integer or a unicode character, not a string of length 2") + test_exc('%c', b'x', TypeError, + "format argument: %c requires an integer or a unicode character, not bytes") if maxsize == 2**31-1: # crashes 2.2.1 and earlier: @@ -355,36 +452,84 @@ def __bytes__(self): testcommon(b"%r", b"ghi", b"b'ghi'") testcommon(b"%r", "jkl", b"'jkl'") testcommon(b"%r", "\u0544", b"'\\u0544'") + testcommon(b'%(x)r', {b'x': 1}, b'1') # Test exception for unknown format characters, etc. if verbose: print('Testing exceptions') - test_exc(b'%g', '1', TypeError, "float argument required, not str") - test_exc(b'%g', b'1', TypeError, "float argument required, not bytes") + test_exc = self.check_exc + test_exc(b"abc %\nd", 1, ValueError, + "stray % at position 4 or unexpected format character with code 0x0a at position 5") + test_exc(b"abc %'d", 1, ValueError, + "stray % at position 4 or unexpected format character \"'\" at position 5") + test_exc(b"abc %\x1fd", 1, ValueError, + "stray % at position 4 or unexpected format character with code 0x1f at position 5") + test_exc(b"abc %\x7fd", 1, ValueError, + "stray % at position 4 or unexpected format character with code 0x7f at position 5") + test_exc(b"abc %\x80d", 1, ValueError, + "stray % at position 4 or unexpected format character with code 0x80 at position 5") test_exc(b'no format', 7, TypeError, - "not all arguments converted during bytes formatting") + "not all arguments converted during bytes formatting (required 0, got 1)") test_exc(b'no format', b'1', TypeError, - "not all arguments converted during bytes formatting") + "not all arguments converted during bytes formatting (required 0, got 1)") test_exc(b'no format', bytearray(b'1'), TypeError, - "not all arguments converted during bytes formatting") + "not all arguments converted during bytes formatting (required 0, got 1)") + test_exc(b'%r', (1, 2), TypeError, + "not all arguments converted during bytes formatting (required 1, got 2)") + test_exc(b'%(x)r %r', {b'x': 1}, ValueError, + "format requires a parenthesised mapping key at position 6") + test_exc(b'%(x)*r', {b'x': 1}, ValueError, + "* cannot be used with a parenthesised mapping key at position 0") + test_exc(b'%(x).*r', {b'x': 1}, ValueError, + "* cannot be used with a parenthesised mapping key at position 0") + test_exc(b'%(x)d', {b'x': '1'}, TypeError, + "format argument b'x': %d requires a real number, not str") + test_exc(b'%(x)x', {b'x': '1'}, TypeError, + "format argument b'x': %x requires an integer, not str") + test_exc(b'%(x)g', {b'x': '1'}, TypeError, + "format argument b'x': %g requires a real number, not str") test_exc(b"%c", -1, OverflowError, - "%c arg not in range(256)") + "format argument: %c argument not in range(256)") + test_exc(b"%c", (-1,), OverflowError, + "format argument 1: %c argument not in range(256)") + test_exc(b"%(x)c", {b'x': -1}, OverflowError, + "format argument b'x': %c argument not in range(256)") test_exc(b"%c", 256, OverflowError, - "%c arg not in range(256)") + "format argument: %c argument not in range(256)") test_exc(b"%c", 2**128, OverflowError, - "%c arg not in range(256)") + "format argument: %c argument not in range(256)") test_exc(b"%c", b"Za", TypeError, - "%c requires an integer in range(256) or a single byte, not a bytes object of length 2") + "format argument: %c requires an integer in range(256) or a single byte, not a bytes object of length 2") + test_exc(b"%c", (b"Za",), TypeError, + "format argument 1: %c requires an integer in range(256) or a single byte, not a bytes object of length 2") + test_exc(b"%(x)c", {b'x': b"Za"}, TypeError, + "format argument b'x': %c requires an integer in range(256) or a single byte, not a bytes object of length 2") + test_exc(b"%c", bytearray(b"Za"), TypeError, + "format argument: %c requires an integer in range(256) or a single byte, not a bytearray object of length 2") + test_exc(b"%c", (bytearray(b"Za"),), TypeError, + "format argument 1: %c requires an integer in range(256) or a single byte, not a bytearray object of length 2") + test_exc(b"%(x)c", {b'x': bytearray(b"Za")}, TypeError, + "format argument b'x': %c requires an integer in range(256) or a single byte, not a bytearray object of length 2") test_exc(b"%c", "Y", TypeError, - "%c requires an integer in range(256) or a single byte, not str") + "format argument: %c requires an integer in range(256) or a single byte, not str") test_exc(b"%c", 3.14, TypeError, - "%c requires an integer in range(256) or a single byte, not float") + "format argument: %c requires an integer in range(256) or a single byte, not float") + test_exc(b"%c", (3.14,), TypeError, + "format argument 1: %c requires an integer in range(256) or a single byte, not float") + test_exc(b"%(x)c", {b'x': 3.14}, TypeError, + "format argument b'x': %c requires an integer in range(256) or a single byte, not float") test_exc(b"%b", "Xc", TypeError, - "%b requires a bytes-like object, " - "or an object that implements __bytes__, not 'str'") + "format argument: %b requires a bytes-like object, " + "or an object that implements __bytes__, not str") + test_exc(b"%b", ("Xc",), TypeError, + "format argument 1: %b requires a bytes-like object, " + "or an object that implements __bytes__, not str") + test_exc(b"%(x)b", {b'x': "Xc"}, TypeError, + "format argument b'x': %b requires a bytes-like object, " + "or an object that implements __bytes__, not str") test_exc(b"%s", "Wd", TypeError, - "%b requires a bytes-like object, " - "or an object that implements __bytes__, not 'str'") + "format argument: %b requires a bytes-like object, " + "or an object that implements __bytes__, not str") if maxsize == 2**31-1: # crashes 2.2.1 and earlier: @@ -494,6 +639,28 @@ def test_precision_c_limits(self): with self.assertRaises(ValueError) as cm: format(c, ".%sf" % (INT_MAX + 1)) + @support.cpython_only + def test_precision_near_int_max(self): + # gh-158446: Precisions just below INT_MAX are rejected before any + # output buffer size is computed from them. + _testcapi = import_module("_testcapi") + INT_MAX = _testcapi.INT_MAX + + f = 1e300 + c = complex(f) + for prec in (INT_MAX, INT_MAX - 1023): + for code in "feg": + spec = ".%d%s" % (prec, code) + with self.subTest(spec=spec): + with self.assertRaises(ValueError): + format(f, spec) + with self.assertRaises(ValueError): + format(c, spec) + with self.assertRaises(ValueError): + ("%" + spec) % f + with self.assertRaises(ValueError): + ("%" + spec).encode() % f + def test_g_format_has_no_trailing_zeros(self): # regression test for bugs.python.org/issue40780 self.assertEqual("%.3g" % 1505.0, "1.5e+03") diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index baa638e07fa..b14e2e4ff2d 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -14,11 +14,13 @@ import re import types import decimal +import subprocess import unittest import warnings from test import support from test.support.os_helper import temp_cwd -from test.support.script_helper import assert_python_failure, assert_python_ok +from test.support.script_helper import ( + assert_python_failure, assert_python_ok, spawn_python) a_global = 'global variable' @@ -593,6 +595,17 @@ def test_compile_time_concat_errors(self): r"""b'' f''""", ]) + def test_concat_decode_failure_does_not_crash(self): + script = r''' +import builtins +builtins.__import__ = builtins # Breaks warning machinery so _get_resized_exprs returns NULL +try: + compile('"x"f"\]"b""', '', 'exec') +except Exception: + pass +''' + assert_python_ok('-c', script) + def test_literal(self): self.assertEqual(f'', '') self.assertEqual(f'a', 'a') @@ -606,6 +619,34 @@ def test_unterminated_string(self): r"""f'{("x}'""", ]) + def test_unclosed_multiline_replacement_field(self): + for prefix in ('f', 't', 'rf', 'rt'): + for quote in ('"', "'"): + triple = quote * 3 + cases = ( + # The apparent closing quotes open a string in the field. + ('var = "abc"\na = PREFIXQUOTE {var} extern "C" { QUOTE\n' + 'b = QUOTE string QUOTE', 2), + # Parentheses and dictionaries inside the field must not + # change which opening brace the diagnostic identifies. + ('a = PREFIXQUOTE{\n(QUOTE', 1), + ('a = PREFIXQUOTE{\n{0: QUOTE', 1), + # Use the innermost format field or formatted string. + ('a = PREFIXQUOTE{0:\n{1\nQUOTE', 2), + ('a = f"{\nPREFIXQUOTE{1\nQUOTE', 2), + # Account for parentheses outside the formatted string. + ('a = (PREFIXQUOTE{1\nQUOTE', 1), + ) + for source, lineno in cases: + source = source.replace('PREFIX', prefix).replace('QUOTE', triple) + with self.subTest(source=source): + with self.assertRaises(SyntaxError) as cm: + compile(source, '', 'exec') + self.assertEqual( + cm.exception.msg, + f"{prefix[-1]}-string: expecting '}}' to close '{{' " + f"on line {lineno}") + @unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI") def test_mismatched_parens(self): self.assertAllRaise(SyntaxError, r"closing parenthesis '\}' " @@ -811,6 +852,18 @@ def build_fstr(n, extra=''): s = "f'{1}' 'x' 'y'" * 1024 self.assertEqual(eval(s), '1xy' * 1024) + @support.requires_resource('cpu') + def test_many_fstrings_in_module(self): + fields = ''.join(f'{{x{i}}}' for i in range(100)) + source = ''.join( + f"value_{i} = f'{fields}'\n" for i in range(1_000) + ) + namespace = {f'x{i}': str(i) for i in range(100)} + expected = ''.join(str(i) for i in range(100)) + exec(source, namespace) + self.assertEqual(namespace['value_0'], expected) + self.assertEqual(namespace['value_999'], expected) + def test_format_specifier_expressions(self): width = 10 precision = 4 @@ -1242,7 +1295,7 @@ def __format__(self, spec): self.assertEqual(f'{3:}', '3') self.assertEqual(f'{3!s:}', '3') - @unittest.expectedFailureIf(unittest.__spec__.origin == "frozen", "TODO: RUSTPYTHON; frozen module repr has no 'from' path") + @unittest.expectedFailureIf(unittest.__spec__.origin == 'frozen', "TODO: RUSTPYTHON; frozen module repr has no 'from' path") def test_global(self): self.assertEqual(f'g:{a_global}', 'g:global variable') self.assertEqual(f'g:{a_global!r}', "g:'global variable'") @@ -1328,6 +1381,9 @@ def test_not_equal(self): self.assertEqual(f'{3!=4:}', 'True') self.assertEqual(f'{3!=4!s}', 'True') self.assertEqual(f'{3!=4!s:.3}', 'Tru') + a = 3 + b = 4 + self.assertEqual(f'{a!=b=:>10}', 'a!=b= 1') def test_equal_equal(self): # Because an expression ending in = has special meaning, @@ -1658,6 +1714,14 @@ def __repr__(self): self.assertEqual(f'{" # nooo "=}', '" # nooo "=\' # nooo \'') self.assertEqual(f'{" \" # nooo \" "=}', '" \\" # nooo \\" "=\' " # nooo " \'') + result = f'''{( + 1, # Force lexer metadata reconstruction. + "\"#")=}''' + self.assertEqual( + result, + '(\n 1, \n "\\"#")=(1, \'"#\')', + ) + self.assertEqual(f'{ # some comment goes here """hello"""=}', ' \n """hello"""=\'hello\'') self.assertEqual(f'{"""# this is not a comment @@ -1696,6 +1760,9 @@ def test_invalid_syntax_error_message(self): with self.assertRaisesRegex(SyntaxError, "f-string: expecting '=', or '!', or ':', or '}'"): compile("f'{a $ b}'", "?", "exec") + with self.assertRaisesRegex(SyntaxError, + "f-string: expecting '!', or ':', or '}'"): + compile("f'{a=b}'", "?", "exec") def test_with_two_commas_in_format_specifier(self): error_msg = re.escape("Cannot specify ',' with ','.") @@ -1744,7 +1811,6 @@ def test_not_closing_quotes(self): except SyntaxError as e: self.assertEqual(e.text, 'z = f"""') self.assertEqual(e.lineno, 3) - def test_syntax_error_after_debug(self): self.assertAllRaise(SyntaxError, "f-string: expecting a valid expression after '{'", [ @@ -1772,6 +1838,32 @@ def test_debug_in_file(self): self.assertEqual(stdout.decode('utf-8').strip().replace('\r\n', '\n').replace('\r', '\n'), "3\n=3") + @support.requires_subprocess() + def test_expression_in_interactive_after_buffer_resize(self): + expression = "(\n" + (" " * 64 + "\n") * 256 + "1\n)" + source = ( + f"result = f'''{{{expression}=}}'''\n" + "print(repr(result))\n" + ) + with spawn_python('-i', '-q', stderr=subprocess.PIPE) as process: + stdout, stderr = process.communicate( + source.encode(), timeout=support.SHORT_TIMEOUT) + self.assertEqual(process.returncode, 0, stderr) + self.assertEqual(stdout.decode().strip(), repr(expression + "=1")) + + def test_debug_in_file_after_buffer_resize(self): + expression = "(\n" + (" " * 64 + "\n") * 256 + "1\n)" + expected = expression + "=1" + with temp_cwd(): + script = 'script.py' + source = ( + f"result = f'''{{{expression}=}}'''\n" + f"assert result == {expected!r}\n" + ) + with open(script, 'w') as f: + f.write(source) + assert_python_ok(script) + def test_syntax_warning_infinite_recursion_in_file(self): with temp_cwd(): script = 'script.py' diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 9c3def2c3be..272414a6204 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -88,6 +88,147 @@ def test_trims_trailing_newline(self): input = StringIO('test\n') self.assertEqual('test', getpass._raw_input(input=input)) + def check_raw_input(self, inputs, expect_result, prompt='Password: '): + mock_input = StringIO(inputs) + mock_output = StringIO() + result = getpass._raw_input(prompt, mock_output, mock_input, '*') + self.assertEqual(result, expect_result) + return mock_output.getvalue() + + def test_null_char(self): + self.check_raw_input('pass\x00word\n', 'password') + + def test_raw_input_with_echo_char(self): + output = self.check_raw_input('my1pa$$word!\n', 'my1pa$$word!') + self.assertEqual('Password: ************', output) + + def test_control_chars_with_echo_char(self): + output = self.check_raw_input('pass\twd\b\n', 'pass\tw') + # After backspace: refresh rewrites prompt + 6 echo chars + self.assertEqual( + 'Password: *******' # initial prompt + 7 echo chars + '\r' + ' ' * 17 + '\r' # clear line (10 prompt + 7 prev) + 'Password: ******', # rewrite prompt + 6 echo chars + output + ) + + def test_kill_ctrl_u_with_echo_char(self): + # Ctrl+U (KILL) should clear the entire line + output = self.check_raw_input('foo\x15bar\n', 'bar') + # Should show "***" then refresh to clear, then show "***" for "bar" + self.assertIn('***', output) + # Display refresh uses \r to rewrite the line including prompt + self.assertIn('\r', output) + + def test_werase_ctrl_w_with_echo_char(self): + # Ctrl+W (WERASE) should delete the previous word + self.check_raw_input('hello world\x17end\n', 'hello end') + + def test_ctrl_w_display_preserves_prompt(self): + # Reproducer from gh-138577: type "hello world", Ctrl+W + # Display must show "Password: ******" not "******rd: ***********" + output = self.check_raw_input('hello world\x17\n', 'hello ') + # The final visible state should be "Password: ******" + # Verify prompt is rewritten during refresh, not overwritten by stars + self.assertEndsWith(output, 'Password: ******') + + def test_ctrl_a_insert_display_preserves_prompt(self): + # Reproducer from gh-138577: type "abc", Ctrl+A, type "x" + # Display must show "Password: ****" not "****word: ***" + output = self.check_raw_input('abc\x01x\n', 'xabc') + # The final visible state should be "Password: ****" + self.assertEndsWith(output, 'Password: ****\x08\x08\x08') + + def test_lnext_ctrl_v_with_echo_char(self): + # Ctrl+V (LNEXT) should insert the next character literally + self.check_raw_input('test\x16\x15more\n', 'test\x15more') + + def test_ctrl_a_move_to_start_with_echo_char(self): + # Ctrl+A should move cursor to start + self.check_raw_input('end\x01start\n', 'startend') + + def test_ctrl_a_cursor_position(self): + # After Ctrl+A, cursor is at position 0. + # Refresh writes backspaces to move cursor from end to start. + output = self.check_raw_input('abc\x01\n', 'abc') + self.assertEndsWith(output, 'Password: ***\x08\x08\x08') + + def test_ctrl_a_on_empty(self): + # Ctrl+A on empty line should be a no-op + self.check_raw_input('\x01hello\n', 'hello') + + def test_ctrl_a_already_at_start(self): + # Double Ctrl+A should be same as single Ctrl+A + self.check_raw_input('abc\x01\x01start\n', 'startabc') + + def test_ctrl_a_then_backspace(self): + # Backspace after Ctrl+A should do nothing (cursor at 0) + self.check_raw_input('abc\x01\x7f\n', 'abc') + + def test_ctrl_e_move_to_end_with_echo_char(self): + # Ctrl+E should move cursor to end + self.check_raw_input('start\x01X\x05end\n', 'Xstartend') + + def test_ctrl_e_cursor_position(self): + # After Ctrl+A then Ctrl+E, cursor is back at end. + # Refresh has no backspaces since cursor is at end. + output = self.check_raw_input('abc\x01\x05\n', 'abc') + self.assertEndsWith(output, 'Password: ***') + + def test_ctrl_e_on_empty(self): + # Ctrl+E on empty line should be a no-op + self.check_raw_input('\x05hello\n', 'hello') + + def test_ctrl_e_already_at_end(self): + # Ctrl+E when already at end should be a no-op + self.check_raw_input('abc\x05more\n', 'abcmore') + + def test_ctrl_a_then_ctrl_e(self): + # Ctrl+A then Ctrl+E should return cursor to end, typing appends + self.check_raw_input('abc\x01\x05def\n', 'abcdef') + + def test_ctrl_k_kill_forward_with_echo_char(self): + # Ctrl+K should kill from cursor to end + self.check_raw_input('delete\x01\x0bkeep\n', 'keep') + + def test_ctrl_c_interrupt_with_echo_char(self): + # Ctrl+C should raise KeyboardInterrupt + with self.assertRaises(KeyboardInterrupt): + self.check_raw_input('test\x03more', '') + + def test_ctrl_d_eof_with_echo_char(self): + # Ctrl+D twice should cause EOF + self.check_raw_input('test\x04\x04', 'test') + + def test_backspace_at_start_with_echo_char(self): + # Backspace at start should do nothing + self.check_raw_input('\x7fhello\n', 'hello') + + def test_ctrl_k_at_end_with_echo_char(self): + # Ctrl+K at end should do nothing + self.check_raw_input('hello\x0b\n', 'hello') + + def test_ctrl_w_on_empty_with_echo_char(self): + # Ctrl+W on empty line should do nothing + self.check_raw_input('\x17hello\n', 'hello') + + def test_ctrl_u_on_empty_with_echo_char(self): + # Ctrl+U on empty line should do nothing + self.check_raw_input('\x15hello\n', 'hello') + + def test_multiple_ctrl_operations_with_echo_char(self): + # Test combination: type, move, insert, delete + # "world", Ctrl+A, "hello ", Ctrl+E, "!", Ctrl+A, Ctrl+K, "start" + self.check_raw_input('world\x01hello \x05!\x01\x0bstart\n', 'start') + + def test_ctrl_w_multiple_words_with_echo_char(self): + # Ctrl+W should delete only the last word + self.check_raw_input('one two three\x17\n', 'one two ') + + def test_ctrl_v_then_ctrl_c_with_echo_char(self): + # Ctrl+V should make Ctrl+C literal (not interrupt) + self.check_raw_input('test\x16\x03end\n', 'test\x03end') + # Some of these tests are a bit white-box. The functional requirement is that # the password input be taken directly from the tty, and that it not be echoed @@ -174,33 +315,10 @@ def test_echo_char_replaces_input_with_asterisks(self): result = getpass.unix_getpass(echo_char='*') mock_input.assert_called_once_with('Password: ', textio(), - input=textio(), echo_char='*') + input=textio(), echo_char='*', + term_ctrl_chars=mock.ANY) self.assertEqual(result, mock_result) - def test_raw_input_with_echo_char(self): - passwd = 'my1pa$$word!' - mock_input = StringIO(f'{passwd}\n') - mock_output = StringIO() - with mock.patch('sys.stdin', mock_input), \ - mock.patch('sys.stdout', mock_output): - result = getpass._raw_input('Password: ', mock_output, mock_input, - '*') - self.assertEqual(result, passwd) - self.assertEqual('Password: ************', mock_output.getvalue()) - - def test_control_chars_with_echo_char(self): - passwd = 'pass\twd\b' - expect_result = 'pass\tw' - mock_input = StringIO(f'{passwd}\n') - mock_output = StringIO() - with mock.patch('sys.stdin', mock_input), \ - mock.patch('sys.stdout', mock_output): - result = getpass._raw_input('Password: ', mock_output, mock_input, - '*') - self.assertEqual(result, expect_result) - self.assertEqual('Password: *******\x08 \x08', mock_output.getvalue()) - - class GetpassEchoCharTest(unittest.TestCase): def test_accept_none(self): diff --git a/Lib/test/test_glob.py b/Lib/test/test_glob.py index d0ed5129253..9e4e82b2542 100644 --- a/Lib/test/test_glob.py +++ b/Lib/test/test_glob.py @@ -4,7 +4,6 @@ import shutil import sys import unittest -import warnings from test.support import is_wasi, Py_DEBUG from test.support.os_helper import (TESTFN, skip_unless_symlink, @@ -393,36 +392,6 @@ def test_glob_many_open_files(self): for it in iters: self.assertEqual(next(it), p) - def test_glob0(self): - with self.assertWarns(DeprecationWarning): - glob.glob0(self.tempdir, 'a') - - with warnings.catch_warnings(): - warnings.simplefilter('ignore') - eq = self.assertSequencesEqual_noorder - eq(glob.glob0(self.tempdir, 'a'), ['a']) - eq(glob.glob0(self.tempdir, '.bb'), ['.bb']) - eq(glob.glob0(self.tempdir, '.b*'), []) - eq(glob.glob0(self.tempdir, 'b'), []) - eq(glob.glob0(self.tempdir, '?'), []) - eq(glob.glob0(self.tempdir, '*a'), []) - eq(glob.glob0(self.tempdir, 'a*'), []) - - def test_glob1(self): - with self.assertWarns(DeprecationWarning): - glob.glob1(self.tempdir, 'a') - - with warnings.catch_warnings(): - warnings.simplefilter('ignore') - eq = self.assertSequencesEqual_noorder - eq(glob.glob1(self.tempdir, 'a'), ['a']) - eq(glob.glob1(self.tempdir, '.bb'), ['.bb']) - eq(glob.glob1(self.tempdir, '.b*'), ['.bb']) - eq(glob.glob1(self.tempdir, 'b'), []) - eq(glob.glob1(self.tempdir, '?'), ['a']) - eq(glob.glob1(self.tempdir, '*a'), ['a', 'aaa']) - eq(glob.glob1(self.tempdir, 'a*'), ['a', 'aaa', 'aab']) - def test_translate_matching(self): match = re.compile(glob.translate('*')).match self.assertIsNotNone(match('foo')) diff --git a/Lib/test/test_grammar.py b/Lib/test/test_grammar.py index cfb24a5c457..ebcd98a0a37 100644 --- a/Lib/test/test_grammar.py +++ b/Lib/test/test_grammar.py @@ -1,8 +1,6 @@ # Python test set -- part 1, grammar. # This just tests whether the parser accepts them all. -from test.support import check_syntax_error, skip_wasi_stack_overflow -from test.support import import_helper import annotationlib import inspect import unittest @@ -18,6 +16,12 @@ import typing from test.typinganndata import ann_module2 import test +from test.support import ( + check_syntax_error, + import_helper, + skip_emscripten_stack_overflow, + skip_wasi_stack_overflow, +) from test.support.numbers import ( VALID_UNDERSCORE_LITERALS, INVALID_UNDERSCORE_LITERALS, @@ -250,6 +254,7 @@ def test_eof_error(self): self.assertIn("was never closed", str(cm.exception)) @skip_wasi_stack_overflow() + @skip_emscripten_stack_overflow() def test_max_level(self): # Macro defined in Parser/lexer/state.h MAXLEVEL = 200 diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index ab06f3cf054..44c36013528 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -431,6 +431,81 @@ def test_headers_debuglevel(self): self.assertEqual(lines[2], "header: Second: val1") self.assertEqual(lines[3], "header: Second: val2") + def test_max_response_headers(self): + max_headers = client._MAXHEADERS + 20 + headers = [f"Name{i}: Value{i}".encode() for i in range(max_headers)] + body = b"HTTP/1.1 200 OK\r\n" + b"\r\n".join(headers) + + with self.subTest(max_headers=None): + sock = FakeSocket(body) + resp = client.HTTPResponse(sock) + with self.assertRaisesRegex( + client.HTTPException, f"got more than 100 headers" + ): + resp.begin() + + with self.subTest(max_headers=max_headers): + sock = FakeSocket(body) + resp = client.HTTPResponse(sock) + resp.begin(_max_headers=max_headers) + + def test_max_connection_headers(self): + max_headers = client._MAXHEADERS + 20 + headers = ( + f"Name{i}: Value{i}".encode() for i in range(max_headers - 1) + ) + body = ( + b"HTTP/1.1 200 OK\r\n" + + b"\r\n".join(headers) + + b"\r\nContent-Length: 12\r\n\r\nDummy body\r\n" + ) + + with self.subTest(max_headers=None): + conn = client.HTTPConnection("example.com") + conn.sock = FakeSocket(body) + conn.request("GET", "/") + with self.assertRaisesRegex( + client.HTTPException, f"got more than {client._MAXHEADERS} headers" + ): + response = conn.getresponse() + + with self.subTest(max_headers=None): + conn = client.HTTPConnection( + "example.com", max_response_headers=max_headers + ) + conn.sock = FakeSocket(body) + conn.request("GET", "/") + response = conn.getresponse() + response.read() + + def test_max_connection_trailers(self): + # max_response_headers also limits trailer lines of a chunked + # response, which are read and discarded by read(). + max_trailers = client._MAXHEADERS + 20 + trailer_lines = "".join( + f"X-Trailer{i}: {i}\r\n" for i in range(max_trailers - 1) + ) + body = chunked_start + last_chunk + trailer_lines + chunked_end + + with self.subTest(max_response_headers=None): + conn = client.HTTPConnection("example.com") + conn.sock = FakeSocket(body) + conn.request("GET", "/") + response = conn.getresponse() + with self.assertRaisesRegex( + client.HTTPException, + f"got more than {client._MAXHEADERS} trailers", + ): + response.read() + + with self.subTest(max_response_headers=max_trailers): + conn = client.HTTPConnection( + "example.com", max_response_headers=max_trailers + ) + conn.sock = FakeSocket(body) + conn.request("GET", "/") + response = conn.getresponse() + self.assertEqual(response.read(), chunked_expected) class HttpMethodTests(TestCase): def test_invalid_method_names(self): @@ -1332,6 +1407,35 @@ def test_overflowing_header_limit_after_100(self): self.assertIn('got more than ', str(cm.exception)) self.assertIn('headers', str(cm.exception)) + def test_too_many_interim_responses(self): + # A server streaming "100 Continue" responses forever must not + # hang getresponse(). + body = ( + 'HTTP/1.1 100 Continue\r\n\r\n' + * (client._MAXINTERIMRESPONSES + 1) + ) + resp = client.HTTPResponse(FakeSocket(body)) + with self.assertRaises(client.HTTPException) as cm: + resp.begin() + self.assertIn('got more than ', str(cm.exception)) + self.assertIn('interim responses', str(cm.exception)) + + def test_multiple_interim_responses(self): + # A reasonable number of interim responses before the final + # response is skipped as before. + body = ( + 'HTTP/1.1 100 Continue\r\n\r\n' * 3 + + 'HTTP/1.1 200 OK\r\n' + 'Content-Length: 5\r\n' + '\r\n' + 'hello' + ) + resp = client.HTTPResponse(FakeSocket(body), method="GET") + resp.begin() + self.assertEqual(resp.status, 200) + self.assertEqual(resp.read(), b'hello') + resp.close() + def test_overflowing_chunked_line(self): body = ( 'HTTP/1.1 200 OK\r\n' @@ -1403,6 +1507,35 @@ def test_chunked_trailers(self): self.assertEqual(sock.file.read(), b"") #we read to the end resp.close() + def test_chunked_too_many_trailers(self): + """A response streaming endless trailer lines must raise, not hang""" + too_many_trailers = "".join( + f"X-Trailer{i}: {i}\r\n" for i in range(client._MAXHEADERS + 1) + ) + # An unbounded read() reaches the trailers via the final 0 chunk. + sock = FakeSocket( + chunked_start + last_chunk + too_many_trailers + chunked_end) + resp = client.HTTPResponse(sock, method="GET") + resp.begin() + with self.assertRaisesRegex( + client.HTTPException, + f"got more than {client._MAXHEADERS} trailers", + ): + resp.read() + resp.close() + + # A bounded read(amt) larger than the body hits the same limit. + sock = FakeSocket( + chunked_start + last_chunk + too_many_trailers + chunked_end) + resp = client.HTTPResponse(sock, method="GET") + resp.begin() + with self.assertRaisesRegex( + client.HTTPException, + f"got more than {client._MAXHEADERS} trailers", + ): + resp.read(len(chunked_expected) + 1) + resp.close() + def test_chunked_sync(self): """Check that we don't read past the end of the chunked-encoding stream""" expected = chunked_expected diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index 7bf4e8279c3..70349c6c886 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -3,9 +3,9 @@ Written by Cody A.W. Somerville , Josip Dzolonga, and Michael Otteneder for the 2007/08 GHOP contest. """ -from collections import OrderedDict + from http.server import BaseHTTPRequestHandler, HTTPServer, HTTPSServer, \ - SimpleHTTPRequestHandler, CGIHTTPRequestHandler + SimpleHTTPRequestHandler from http import server, HTTPStatus import contextlib @@ -13,7 +13,6 @@ import socket import sys import re -import base64 import ntpath import pathlib import shutil @@ -22,18 +21,23 @@ import html import http, http.client import urllib.parse +import urllib.request import tempfile import time import datetime import threading from unittest import mock from io import BytesIO, StringIO +from _colorize import get_theme import unittest from test import support from test.support import ( - is_apple, import_helper, os_helper, requires_subprocess, threading_helper + force_not_colorized, + is_apple, import_helper, os_helper, threading_helper ) +from test.support.script_helper import kill_python, spawn_python +from test.support.socket_helper import find_unused_port try: import ssl @@ -382,6 +386,8 @@ def test_simple_get(self): def test_invalid_request(self): self.sock.send(b'POST /index.html\r\n') res = self.sock.recv(1024) + # The error response is not sent in the bare HTTP/0.9 style. + self.assertStartsWith(res, b'HTTP/1.0 400 ') self.assertIn(b"Bad HTTP/0.9 request type ('POST')", res) def test_single_request(self): @@ -478,6 +484,7 @@ def do_GET(self): def do_ERROR(self): self.send_error(HTTPStatus.NOT_FOUND, 'File not found') + @force_not_colorized def test_get(self): self.con = http.client.HTTPConnection(self.HOST, self.PORT) self.con.connect() @@ -488,6 +495,7 @@ def test_get(self): self.assertEndsWith(err.getvalue(), '"GET / HTTP/1.1" 200 -\n') + @force_not_colorized def test_err(self): self.con = http.client.HTTPConnection(self.HOST, self.PORT) self.con.connect() @@ -501,8 +509,49 @@ def test_err(self): self.assertEndsWith(lines[1], '"ERROR / HTTP/1.1" 404 -') +@support.force_colorized_test_class +class RequestHandlerColorizedLoggingTestCase(RequestHandlerLoggingTestCase): + + def test_get(self): + t = get_theme(force_color=True).http_server + self.con = http.client.HTTPConnection(self.HOST, self.PORT) + self.con.connect() + + with support.captured_stderr() as err: + self.con.request("GET", "/") + self.con.getresponse() + + output = err.getvalue() + self.assertIn(f"{t.path}/{t.reset}", output) + self.assertIn(f"{t.status_ok}200", output) + self.assertIn(t.reset, output) + + def test_err(self): + t = get_theme(force_color=True).http_server + self.con = http.client.HTTPConnection(self.HOST, self.PORT) + self.con.connect() + + with support.captured_stderr() as err: + self.con.request("ERROR", "/") + self.con.getresponse() + + lines = err.getvalue().split("\n") + self.assertIn( + f"{t.error}code 404, message File not found{t.reset}", lines[0] + ) + self.assertIn(f"{t.status_client_error}404", lines[1]) + + +class CustomHeaderSimpleHTTPRequestHandler(SimpleHTTPRequestHandler): + extra_response_headers = None + + def __init__(self, *args, **kwargs): + kwargs.setdefault('extra_response_headers', self.extra_response_headers) + super().__init__(*args, **kwargs) + + class SimpleHTTPServerTestCase(BaseTestCase): - class request_handler(NoLogRequestHandler, SimpleHTTPRequestHandler): + class request_handler(NoLogRequestHandler, CustomHeaderSimpleHTTPRequestHandler): pass def setUp(self): @@ -529,12 +578,12 @@ def setUp(self): def tearDown(self): try: os.chdir(self.cwd) - try: - shutil.rmtree(self.tempdir) - except: - pass finally: super().tearDown() + try: + shutil.rmtree(self.tempdir) + except: + pass def check_status_and_reason(self, response, status, data=None): def close_conn(): @@ -561,6 +610,15 @@ def close_conn(): reader.close() return body + def check_status_and_headers(self, response, status, headers=None): + # Drain the body so the server-side handler can close the file + # before tearDown removes the tempdir (matters on Windows). + response.read() + self.assertEqual(response.status, status) + if headers: + for name, value in headers.items(): + self.assertEqual(response.getheader(name), value) + def check_list_dir_dirname(self, dirname, quotedname=None): fullpath = os.path.join(self.tempdir, dirname) try: @@ -859,368 +917,66 @@ def test_path_without_leading_slash(self): self.assertEqual(response.getheader("Location"), self.tempdir_name + "/?hi=1") - -cgi_file1 = """\ -#!%s - -print("Content-type: text/html") -print() -print("Hello World") -""" - -cgi_file2 = """\ -#!%s -import os -import sys -import urllib.parse - -print("Content-type: text/html") -print() - -content_length = int(os.environ["CONTENT_LENGTH"]) -query_string = sys.stdin.buffer.read(content_length) -params = {key.decode("utf-8"): val.decode("utf-8") - for key, val in urllib.parse.parse_qsl(query_string)} - -print("%%s, %%s, %%s" %% (params["spam"], params["eggs"], params["bacon"])) -""" - -cgi_file4 = """\ -#!%s -import os - -print("Content-type: text/html") -print() - -print(os.environ["%s"]) -""" - -cgi_file6 = """\ -#!%s -import os - -print("X-ambv: was here") -print("Content-type: text/html") -print() -print("
    ")
    -for k, v in os.environ.items():
    -    try:
    -        k.encode('ascii')
    -        v.encode('ascii')
    -    except UnicodeEncodeError:
    -        continue  # see: BPO-44647
    -    print(f"{k}={v}")
    -print("
    ") -""" - -cgi_file7 = """\ -#!%s -import os -import sys - -print("Content-type: text/plain") -print() - -content_length = int(os.environ["CONTENT_LENGTH"]) -body = sys.stdin.buffer.read(content_length) - -print(f"{content_length} {len(body)}") -""" - - -@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0, - "This test can't be run reliably as root (issue #13308).") -@requires_subprocess() -class CGIHTTPServerTestCase(BaseTestCase): - class request_handler(NoLogRequestHandler, CGIHTTPRequestHandler): - _test_case_self = None # populated by each setUp() method call. - - def __init__(self, *args, **kwargs): - with self._test_case_self.assertWarnsRegex( - DeprecationWarning, - r'http\.server\.CGIHTTPRequestHandler'): - # This context also happens to catch and silence the - # threading DeprecationWarning from os.fork(). - super().__init__(*args, **kwargs) - - linesep = os.linesep.encode('ascii') - - def setUp(self): - self.request_handler._test_case_self = self # practical, but yuck. - BaseTestCase.setUp(self) - self.cwd = os.getcwd() - self.parent_dir = tempfile.mkdtemp() - self.cgi_dir = os.path.join(self.parent_dir, 'cgi-bin') - self.cgi_child_dir = os.path.join(self.cgi_dir, 'child-dir') - self.sub_dir_1 = os.path.join(self.parent_dir, 'sub') - self.sub_dir_2 = os.path.join(self.sub_dir_1, 'dir') - self.cgi_dir_in_sub_dir = os.path.join(self.sub_dir_2, 'cgi-bin') - os.mkdir(self.cgi_dir) - os.mkdir(self.cgi_child_dir) - os.mkdir(self.sub_dir_1) - os.mkdir(self.sub_dir_2) - os.mkdir(self.cgi_dir_in_sub_dir) - self.nocgi_path = None - self.file1_path = None - self.file2_path = None - self.file3_path = None - self.file4_path = None - self.file5_path = None - self.file6_path = None - self.file7_path = None - - # The shebang line should be pure ASCII: use symlink if possible. - # See issue #7668. - self._pythonexe_symlink = None - if os_helper.can_symlink(): - self.pythonexe = os.path.join(self.parent_dir, 'python') - self._pythonexe_symlink = support.PythonSymlink(self.pythonexe).__enter__() - else: - self.pythonexe = sys.executable - - try: - # The python executable path is written as the first line of the - # CGI Python script. The encoding cookie cannot be used, and so the - # path should be encodable to the default script encoding (utf-8) - self.pythonexe.encode('utf-8') - except UnicodeEncodeError: - self.tearDown() - self.skipTest("Python executable path is not encodable to utf-8") - - self.nocgi_path = os.path.join(self.parent_dir, 'nocgi.py') - with open(self.nocgi_path, 'w', encoding='utf-8') as fp: - fp.write(cgi_file1 % self.pythonexe) - os.chmod(self.nocgi_path, 0o777) - - self.file1_path = os.path.join(self.cgi_dir, 'file1.py') - with open(self.file1_path, 'w', encoding='utf-8') as file1: - file1.write(cgi_file1 % self.pythonexe) - os.chmod(self.file1_path, 0o777) - - self.file2_path = os.path.join(self.cgi_dir, 'file2.py') - with open(self.file2_path, 'w', encoding='utf-8') as file2: - file2.write(cgi_file2 % self.pythonexe) - os.chmod(self.file2_path, 0o777) - - self.file3_path = os.path.join(self.cgi_child_dir, 'file3.py') - with open(self.file3_path, 'w', encoding='utf-8') as file3: - file3.write(cgi_file1 % self.pythonexe) - os.chmod(self.file3_path, 0o777) - - self.file4_path = os.path.join(self.cgi_dir, 'file4.py') - with open(self.file4_path, 'w', encoding='utf-8') as file4: - file4.write(cgi_file4 % (self.pythonexe, 'QUERY_STRING')) - os.chmod(self.file4_path, 0o777) - - self.file5_path = os.path.join(self.cgi_dir_in_sub_dir, 'file5.py') - with open(self.file5_path, 'w', encoding='utf-8') as file5: - file5.write(cgi_file1 % self.pythonexe) - os.chmod(self.file5_path, 0o777) - - self.file6_path = os.path.join(self.cgi_dir, 'file6.py') - with open(self.file6_path, 'w', encoding='utf-8') as file6: - file6.write(cgi_file6 % self.pythonexe) - os.chmod(self.file6_path, 0o777) - - self.file7_path = os.path.join(self.cgi_dir, 'file7.py') - with open(self.file7_path, 'w', encoding='utf-8') as file7: - file7.write(cgi_file7 % self.pythonexe) - os.chmod(self.file7_path, 0o777) - - os.chdir(self.parent_dir) - - def tearDown(self): - self.request_handler._test_case_self = None - try: - os.chdir(self.cwd) - if self._pythonexe_symlink: - self._pythonexe_symlink.__exit__(None, None, None) - if self.nocgi_path: - os.remove(self.nocgi_path) - if self.file1_path: - os.remove(self.file1_path) - if self.file2_path: - os.remove(self.file2_path) - if self.file3_path: - os.remove(self.file3_path) - if self.file4_path: - os.remove(self.file4_path) - if self.file5_path: - os.remove(self.file5_path) - if self.file6_path: - os.remove(self.file6_path) - if self.file7_path: - os.remove(self.file7_path) - os.rmdir(self.cgi_child_dir) - os.rmdir(self.cgi_dir) - os.rmdir(self.cgi_dir_in_sub_dir) - os.rmdir(self.sub_dir_2) - os.rmdir(self.sub_dir_1) - # The 'gmon.out' file can be written in the current working - # directory if C-level code profiling with gprof is enabled. - os_helper.unlink(os.path.join(self.parent_dir, 'gmon.out')) - os.rmdir(self.parent_dir) - finally: - BaseTestCase.tearDown(self) - - def test_url_collapse_path(self): - # verify tail is the last portion and head is the rest on proper urls - test_vectors = { - '': '//', - '..': IndexError, - '/.//..': IndexError, - '/': '//', - '//': '//', - '/\\': '//\\', - '/.//': '//', - 'cgi-bin/file1.py': '/cgi-bin/file1.py', - '/cgi-bin/file1.py': '/cgi-bin/file1.py', - 'a': '//a', - '/a': '//a', - '//a': '//a', - './a': '//a', - './C:/': '/C:/', - '/a/b': '/a/b', - '/a/b/': '/a/b/', - '/a/b/.': '/a/b/', - '/a/b/c/..': '/a/b/', - '/a/b/c/../d': '/a/b/d', - '/a/b/c/../d/e/../f': '/a/b/d/f', - '/a/b/c/../d/e/../../f': '/a/b/f', - '/a/b/c/../d/e/.././././..//f': '/a/b/f', - '../a/b/c/../d/e/.././././..//f': IndexError, - '/a/b/c/../d/e/../../../f': '/a/f', - '/a/b/c/../d/e/../../../../f': '//f', - '/a/b/c/../d/e/../../../../../f': IndexError, - '/a/b/c/../d/e/../../../../f/..': '//', - '/a/b/c/../d/e/../../../../f/../.': '//', - } - for path, expected in test_vectors.items(): - if isinstance(expected, type) and issubclass(expected, Exception): - self.assertRaises(expected, - server._url_collapse_path, path) - else: - actual = server._url_collapse_path(path) - self.assertEqual(expected, actual, - msg='path = %r\nGot: %r\nWanted: %r' % - (path, actual, expected)) - - def test_headers_and_content(self): - res = self.request('/cgi-bin/file1.py') - self.assertEqual( - (res.read(), res.getheader('Content-type'), res.status), - (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK)) - - def test_issue19435(self): - res = self.request('///////////nocgi.py/../cgi-bin/nothere.sh') - self.assertEqual(res.status, HTTPStatus.NOT_FOUND) - - def test_post(self): - params = urllib.parse.urlencode( - {'spam' : 1, 'eggs' : 'python', 'bacon' : 123456}) - headers = {'Content-type' : 'application/x-www-form-urlencoded'} - res = self.request('/cgi-bin/file2.py', 'POST', params, headers) - - self.assertEqual(res.read(), b'1, python, 123456' + self.linesep) - - def test_large_content_length(self): - for w in range(15, 25): - size = 1 << w - body = b'X' * size - headers = {'Content-Length' : str(size)} - res = self.request('/cgi-bin/file7.py', 'POST', body, headers) - self.assertEqual(res.read(), b'%d %d' % (size, size) + self.linesep) - - @unittest.skipIf(sys.platform == "darwin", "TODO: RUSTPYTHON; flaky: the 1ms handler timeout also bounds reading the request line") - def test_large_content_length_truncated(self): - with support.swap_attr(self.request_handler, 'timeout', 0.001): - for w in range(18, 65): - size = 1 << w - headers = {'Content-Length' : str(size)} - res = self.request('/cgi-bin/file1.py', 'POST', b'x', headers) - self.assertEqual(res.read(), b'Hello World' + self.linesep) - - def test_invaliduri(self): - res = self.request('/cgi-bin/invalid') - res.read() - self.assertEqual(res.status, HTTPStatus.NOT_FOUND) - - def test_authorization(self): - headers = {b'Authorization' : b'Basic ' + - base64.b64encode(b'username:pass')} - res = self.request('/cgi-bin/file1.py', 'GET', headers=headers) - self.assertEqual( - (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK), - (res.read(), res.getheader('Content-type'), res.status)) - - def test_no_leading_slash(self): - # http://bugs.python.org/issue2254 - res = self.request('cgi-bin/file1.py') - self.assertEqual( - (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK), - (res.read(), res.getheader('Content-type'), res.status)) - - def test_os_environ_is_not_altered(self): - signature = "Test CGI Server" - os.environ['SERVER_SOFTWARE'] = signature - res = self.request('/cgi-bin/file1.py') - self.assertEqual( - (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK), - (res.read(), res.getheader('Content-type'), res.status)) - self.assertEqual(os.environ['SERVER_SOFTWARE'], signature) - - def test_urlquote_decoding_in_cgi_check(self): - res = self.request('/cgi-bin%2ffile1.py') - self.assertEqual( - (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK), - (res.read(), res.getheader('Content-type'), res.status)) - - def test_nested_cgi_path_issue21323(self): - res = self.request('/cgi-bin/child-dir/file3.py') - self.assertEqual( - (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK), - (res.read(), res.getheader('Content-type'), res.status)) - - def test_query_with_multiple_question_mark(self): - res = self.request('/cgi-bin/file4.py?a=b?c=d') - self.assertEqual( - (b'a=b?c=d' + self.linesep, 'text/html', HTTPStatus.OK), - (res.read(), res.getheader('Content-type'), res.status)) - - def test_query_with_continuous_slashes(self): - res = self.request('/cgi-bin/file4.py?k=aa%2F%2Fbb&//q//p//=//a//b//') - self.assertEqual( - (b'k=aa%2F%2Fbb&//q//p//=//a//b//' + self.linesep, - 'text/html', HTTPStatus.OK), - (res.read(), res.getheader('Content-type'), res.status)) - - def test_cgi_path_in_sub_directories(self): - try: - CGIHTTPRequestHandler.cgi_directories.append('/sub/dir/cgi-bin') - res = self.request('/sub/dir/cgi-bin/file5.py') - self.assertEqual( - (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK), - (res.read(), res.getheader('Content-type'), res.status)) - finally: - CGIHTTPRequestHandler.cgi_directories.remove('/sub/dir/cgi-bin') - - def test_accept(self): - browser_accept = \ - 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' - tests = ( - ((('Accept', browser_accept),), browser_accept), - ((), ''), - # Hack case to get two values for the one header - ((('Accept', 'text/html'), ('ACCEPT', 'text/plain')), - 'text/html,text/plain'), - ) - for headers, expected in tests: - headers = OrderedDict(headers) - with self.subTest(headers): - res = self.request('/cgi-bin/file6.py', 'GET', headers=headers) - self.assertEqual(http.HTTPStatus.OK, res.status) - expected = f"HTTP_ACCEPT={expected}".encode('ascii') - self.assertIn(expected, res.read()) + def test_extra_response_headers_list_dir(self): + with mock.patch.object(self.request_handler, 'extra_response_headers', [ + ('X-Test1', 'test1'), + ('X-Test2', 'test2'), + ]): + response = self.request(self.base_url + '/') + self.check_status_and_headers(response, HTTPStatus.OK, { + "X-Test1": "test1", + "X-Test2": "test2", + }) + + def test_extra_response_headers_get_file(self): + with mock.patch.object(self.request_handler, 'extra_response_headers', [ + ('Set-Cookie', 'test1=value1'), + ('Set-Cookie', 'test2=value2'), + ('X-Test1', 'value3'), + ]): + data = b"Dummy index file\r\n" + with open(os.path.join(self.tempdir_name, 'index.html'), 'wb') as f: + f.write(data) + response = self.request(self.base_url + '/') + self.check_status_and_headers(response, HTTPStatus.OK, { + "Set-Cookie": "test1=value1, test2=value2", + "X-Test1": "value3", + }) + + def test_extra_response_headers_missing_on_404(self): + with mock.patch.object(self.request_handler, 'extra_response_headers', [ + ('X-Test1', 'value'), + ]): + response = self.request(self.base_url + '/missing.html') + self.check_status_and_headers(response, HTTPStatus.NOT_FOUND, { + "X-Test1": None, + }) + + def test_extra_response_headers_dont_overwrite_default_headers(self): + with mock.patch.object(self.request_handler, 'extra_response_headers', [ + ('Content-Type', 'test/not_allowed'), + ('Server', 'not_allowed'), + ('Set-Cookie', 'test=allowed'), + ]): + # The Content-Type header should not be overwritten by the extra_response_headers + # But cookies in the extra_allowed_duplicate_headers are allowed, + # including Set-Cookie + response = self.request(self.base_url + '/') + self.check_status_and_headers(response, HTTPStatus.OK, { + "Set-Cookie": "test=allowed", + }) + self.assertNotEqual(response.getheader("Content-Type"), 'test/not_allowed') + self.assertNotEqual(response.getheader("Server"), 'not_allowed') + + def test_multiple_requests_dont_duplicate_extra_response_headers(self): + with mock.patch.object(self.request_handler, 'extra_response_headers', [ + ('x-test', 'test-value'), + ]): + for _ in range(2): + response = self.request(self.base_url + '/') + self.check_status_and_headers(response, HTTPStatus.OK, { + "x-test": "test-value", + }) class SocketlessRequestHandler(SimpleHTTPRequestHandler): @@ -1242,6 +998,7 @@ def do_GET(self): def log_message(self, format, *args): pass + class RejectingSocketlessRequestHandler(SocketlessRequestHandler): def handle_expect_100(self): self.send_error(HTTPStatus.EXPECTATION_FAILED) @@ -1295,6 +1052,7 @@ def verify_http_server_response(self, response): match = self.HTTPResponseMatch.search(response) self.assertIsNotNone(match) + @force_not_colorized def test_unprintable_not_logged(self): # We call the method from the class directly as our Socketless # Handler subclass overrode it... nice for everything BUT this test. @@ -1346,6 +1104,19 @@ def test_http_0_9(self): self.assertEqual(result[0], b'Data\r\n') self.verify_get_called() + @support.subTests('request,code', [ + (b'GET / FUBAR\r\n\r\n', 400), # bad version + (b'GET / HTTP/2.0\r\n\r\n', 505), # unsupported version + (b'GET\r\n', 400), # bad syntax + (b'POST /\r\n', 400), # bad HTTP/0.9 request type + ]) + def test_request_line_error_has_status_line(self, request, code): + self.handler = SocketlessRequestHandler() + result = self.send_typical_request(request) + self.assertStartsWith(result[0], b'HTTP/1.1 %d ' % code) + self.verify_expected_headers(result[1:result.index(b'\r\n')]) + self.assertFalse(self.handler.get_called) + def test_extra_space(self): result = self.send_typical_request( b'GET /spaced out HTTP/1.1\r\n' @@ -1683,6 +1454,303 @@ def test_server_test_ipv4(self, _): self.assertEqual(mock_server.address_family, socket.AF_INET) +class CommandLineTestCase(unittest.TestCase): + default_port = 8000 + default_bind = None + default_protocol = 'HTTP/1.0' + default_handler = SimpleHTTPRequestHandler + default_server = unittest.mock.ANY + tls_cert = certdata_file('ssl_cert.pem') + tls_key = certdata_file('ssl_key.pem') + tls_password = 'somepass' + tls_cert_options = ['--tls-cert'] + tls_key_options = ['--tls-key'] + tls_password_options = ['--tls-password-file'] + args = { + 'HandlerClass': default_handler, + 'ServerClass': default_server, + 'protocol': default_protocol, + 'port': default_port, + 'bind': default_bind, + 'content_type': 'application/octet-stream', + 'tls_cert': None, + 'tls_key': None, + 'tls_password': None, + } + + def setUp(self): + super().setUp() + self.tls_password_file = tempfile.mktemp() + with open(self.tls_password_file, 'wb') as f: + f.write(self.tls_password.encode()) + self.addCleanup(os_helper.unlink, self.tls_password_file) + + def invoke_httpd(self, *args, stdout=None, stderr=None): + stdout = StringIO() if stdout is None else stdout + stderr = StringIO() if stderr is None else stderr + with contextlib.redirect_stdout(stdout), \ + contextlib.redirect_stderr(stderr): + server._main(args) + return stdout.getvalue(), stderr.getvalue() + + @mock.patch('http.server.test') + def test_port_flag(self, mock_func): + ports = [8000, 65535] + for port in ports: + with self.subTest(port=port): + self.invoke_httpd(str(port)) + call_args = self.args | dict(port=port) + mock_func.assert_called_once_with(**call_args) + mock_func.reset_mock() + + @mock.patch('http.server.test') + def test_directory_flag(self, mock_func): + options = ['-d', '--directory'] + directories = ['.', '/foo', '\\bar', '/', + 'C:\\', 'C:\\foo', 'C:\\bar', + '/home/user', './foo/foo2', 'D:\\foo\\bar'] + for flag in options: + for directory in directories: + with self.subTest(flag=flag, directory=directory): + self.invoke_httpd(flag, directory) + mock_func.assert_called_once_with(**self.args) + mock_func.reset_mock() + + @mock.patch('http.server.test') + def test_bind_flag(self, mock_func): + options = ['-b', '--bind'] + bind_addresses = ['localhost', '127.0.0.1', '::1', + '0.0.0.0', '8.8.8.8'] + for flag in options: + for bind_address in bind_addresses: + with self.subTest(flag=flag, bind_address=bind_address): + self.invoke_httpd(flag, bind_address) + call_args = self.args | dict(bind=bind_address) + mock_func.assert_called_once_with(**call_args) + mock_func.reset_mock() + + @mock.patch('http.server.test') + def test_protocol_flag(self, mock_func): + options = ['-p', '--protocol'] + protocols = ['HTTP/1.0', 'HTTP/1.1', 'HTTP/2.0', 'HTTP/3.0'] + for flag in options: + for protocol in protocols: + with self.subTest(flag=flag, protocol=protocol): + self.invoke_httpd(flag, protocol) + call_args = self.args | dict(protocol=protocol) + mock_func.assert_called_once_with(**call_args) + mock_func.reset_mock() + + @mock.patch('http.server.test') + def test_content_type_flag(self, mock_func): + content_types = ['text/html', 'text/plain', 'application/json'] + for content_type in content_types: + with self.subTest(content_type=content_type): + self.invoke_httpd('--content-type', content_type) + call_args = self.args | dict(content_type=content_type) + mock_func.assert_called_once_with(**call_args) + mock_func.reset_mock() + + @mock.patch('http.server.test') + def test_header_flag(self, mock_func): + call_args = self.args + self.invoke_httpd('--header', 'h1', 'v1', '-H', 'h2', 'v2') + mock_func.assert_called_once_with(**call_args) + mock_func.reset_mock() + + def test_extra_header_flag_too_few_args(self): + with self.assertRaises(SystemExit): + self.invoke_httpd('--header', 'h1') + + def test_extra_header_flag_too_many_args(self): + with self.assertRaises(SystemExit): + self.invoke_httpd('--header', 'h1', 'v1', 'h2') + + @unittest.skipIf(ssl is None, "requires ssl") + @mock.patch('http.server.test') + def test_tls_cert_and_key_flags(self, mock_func): + for tls_cert_option in self.tls_cert_options: + for tls_key_option in self.tls_key_options: + self.invoke_httpd(tls_cert_option, self.tls_cert, + tls_key_option, self.tls_key) + call_args = self.args | { + 'tls_cert': self.tls_cert, + 'tls_key': self.tls_key, + } + mock_func.assert_called_once_with(**call_args) + mock_func.reset_mock() + + @unittest.skipIf(ssl is None, "requires ssl") + @mock.patch('http.server.test') + def test_tls_cert_and_key_and_password_flags(self, mock_func): + for tls_cert_option in self.tls_cert_options: + for tls_key_option in self.tls_key_options: + for tls_password_option in self.tls_password_options: + self.invoke_httpd(tls_cert_option, + self.tls_cert, + tls_key_option, + self.tls_key, + tls_password_option, + self.tls_password_file) + call_args = self.args | { + 'tls_cert': self.tls_cert, + 'tls_key': self.tls_key, + 'tls_password': self.tls_password, + } + mock_func.assert_called_once_with(**call_args) + mock_func.reset_mock() + + @unittest.skipIf(ssl is None, "requires ssl") + @mock.patch('http.server.test') + def test_missing_tls_cert_flag(self, mock_func): + for tls_key_option in self.tls_key_options: + with self.assertRaises(SystemExit): + self.invoke_httpd(tls_key_option, self.tls_key) + mock_func.reset_mock() + + for tls_password_option in self.tls_password_options: + with self.assertRaises(SystemExit): + self.invoke_httpd(tls_password_option, self.tls_password) + mock_func.reset_mock() + + @unittest.skipIf(ssl is None, "requires ssl") + @mock.patch('http.server.test') + def test_invalid_password_file(self, mock_func): + non_existent_file = 'non_existent_file' + for tls_password_option in self.tls_password_options: + for tls_cert_option in self.tls_cert_options: + with self.assertRaises(SystemExit): + self.invoke_httpd(tls_cert_option, + self.tls_cert, + tls_password_option, + non_existent_file) + + @mock.patch('http.server.test') + def test_no_arguments(self, mock_func): + self.invoke_httpd() + mock_func.assert_called_once_with(**self.args) + mock_func.reset_mock() + + @mock.patch('http.server.test') + def test_help_flag(self, _): + options = ['-h', '--help'] + for option in options: + stdout, stderr = StringIO(), StringIO() + with self.assertRaises(SystemExit): + self.invoke_httpd(option, stdout=stdout, stderr=stderr) + self.assertIn('usage', stdout.getvalue()) + self.assertEqual(stderr.getvalue(), '') + + @mock.patch('http.server.test') + def test_unknown_flag(self, _): + stdout, stderr = StringIO(), StringIO() + with self.assertRaises(SystemExit): + self.invoke_httpd('--unknown-flag', stdout=stdout, stderr=stderr) + self.assertEqual(stdout.getvalue(), '') + self.assertIn('error', stderr.getvalue()) + + @mock.patch('http.server.test') + def test_extra_response_headers_arg(self, mock_test): + # Call the main function with extra response headers cli args + server._main( + ['-H', 'Set-Cookie', 'k=v', '-H', 'Set-Cookie', 'k2=v2:v3 v4', '8080'] + ) + # Get the ServerClass (DualStackServerMixin subclass) that _main() + # passed to test(), and verify its finish_request passes + # extra_response_headers to the handler. + _, kwargs = mock_test.call_args + server_class = kwargs['ServerClass'] + + mock_handler_class = mock.MagicMock() + mock_server = mock.Mock() + mock_server.RequestHandlerClass = mock_handler_class + server_class.finish_request(mock_server, mock.Mock(), '127.0.0.1') + mock_handler_class.assert_called_once_with( + mock.ANY, mock.ANY, mock_server, + directory=mock.ANY, + extra_response_headers=[ + ['Set-Cookie', 'k=v'], ['Set-Cookie', 'k2=v2:v3 v4'] + ] + ) + + +class CommandLineRunTimeTestCase(unittest.TestCase): + served_data = os.urandom(32) + served_filename = 'served_filename' + tls_cert = certdata_file('ssl_cert.pem') + tls_key = certdata_file('ssl_key.pem') + tls_password = b'somepass' + tls_password_file = 'ssl_key_password' + + def setUp(self): + super().setUp() + server_dir_context = os_helper.temp_cwd() + server_dir = self.enterContext(server_dir_context) + with open(self.served_filename, 'wb') as f: + f.write(self.served_data) + with open(self.tls_password_file, 'wb') as f: + f.write(self.tls_password) + + def fetch_file(self, path, context=None): + req = urllib.request.Request(path, method='GET') + with urllib.request.urlopen(req, context=context) as res: + return res.read() + + def parse_cli_output(self, output): + match = re.search(r'Serving (HTTP|HTTPS) on (.+) port (\d+)', output) + if match is None: + return None, None, None + return match.group(1).lower(), match.group(2), int(match.group(3)) + + def wait_for_server(self, proc, protocol, bind, port): + """Check that the server has been successfully started.""" + line = proc.stdout.readline().strip() + if support.verbose: + print() + print('python -m http.server: ', line) + return self.parse_cli_output(line) == (protocol, bind, port) + + def test_http_client(self): + bind, port = '127.0.0.1', find_unused_port() + proc = spawn_python('-u', '-m', 'http.server', str(port), '-b', bind, + bufsize=1, text=True) + self.addCleanup(kill_python, proc) + self.addCleanup(proc.terminate) + self.assertTrue(self.wait_for_server(proc, 'http', bind, port)) + res = self.fetch_file(f'http://{bind}:{port}/{self.served_filename}') + self.assertEqual(res, self.served_data) + + @unittest.skipIf(ssl is None, "requires ssl") + def test_https_client(self): + context = ssl.create_default_context() + # allow self-signed certificates + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + bind, port = '127.0.0.1', find_unused_port() + proc = spawn_python('-u', '-m', 'http.server', str(port), '-b', bind, + '--tls-cert', self.tls_cert, + '--tls-key', self.tls_key, + '--tls-password-file', self.tls_password_file, + bufsize=1, text=True) + self.addCleanup(kill_python, proc) + self.addCleanup(proc.terminate) + self.assertTrue(self.wait_for_server(proc, 'https', bind, port)) + url = f'https://{bind}:{port}/{self.served_filename}' + res = self.fetch_file(url, context=context) + self.assertEqual(res, self.served_data) + + +class TestModule(unittest.TestCase): + def test_deprecated__version__(self): + with self.assertWarnsRegex( + DeprecationWarning, + "'__version__' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(http.server, "__version__") + self.assertEqual(cm.filename, __file__) + + def setUpModule(): unittest.addModuleCleanup(os.chdir, os.getcwd()) diff --git a/Lib/test/test_interpreters/__init__.py b/Lib/test/test_interpreters/__init__.py new file mode 100644 index 00000000000..e3d189c4efc --- /dev/null +++ b/Lib/test/test_interpreters/__init__.py @@ -0,0 +1,9 @@ +import os +from test.support import load_package_tests, Py_GIL_DISABLED +import unittest + +if Py_GIL_DISABLED: + raise unittest.SkipTest("GIL disabled") + +def load_tests(*args): + return load_package_tests(os.path.dirname(__file__), *args) diff --git a/Lib/test/test_interpreters/__main__.py b/Lib/test/test_interpreters/__main__.py new file mode 100644 index 00000000000..40a23a297ec --- /dev/null +++ b/Lib/test/test_interpreters/__main__.py @@ -0,0 +1,4 @@ +from . import load_tests +import unittest + +unittest.main() diff --git a/Lib/test/test_interpreters/test_api.py b/Lib/test/test_interpreters/test_api.py new file mode 100644 index 00000000000..13d23af5ace --- /dev/null +++ b/Lib/test/test_interpreters/test_api.py @@ -0,0 +1,2489 @@ +import contextlib +import os +import pickle +import signal +import sys +from textwrap import dedent +import threading +import types +import unittest + +from test import support +from test.support import os_helper +from test.support import script_helper +from test.support import import_helper +from test.support.script_helper import assert_python_ok, spawn_python +# Raise SkipTest if subinterpreters not supported. +_interpreters = import_helper.import_module('_interpreters') +from concurrent import interpreters +from test.support import Py_GIL_DISABLED +from test.support import force_not_colorized +import test._crossinterp_definitions as defs +from concurrent.interpreters import ( + InterpreterError, InterpreterNotFoundError, ExecutionFailed, +) +from .utils import ( + _captured_script, _run_output, _running, TestBase, + requires_test_modules, _testinternalcapi, +) + + +WHENCE_STR_UNKNOWN = 'unknown' +WHENCE_STR_RUNTIME = 'runtime init' +WHENCE_STR_LEGACY_CAPI = 'legacy C-API' +WHENCE_STR_CAPI = 'C-API' +WHENCE_STR_XI = 'cross-interpreter C-API' +WHENCE_STR_STDLIB = '_interpreters module' + + +def is_pickleable(obj): + try: + pickle.dumps(obj) + except Exception: + return False + return True + + +@contextlib.contextmanager +def defined_in___main__(name, script, *, remove=False): + import __main__ as mainmod + mainns = vars(mainmod) + assert name not in mainns + exec(script, mainns, mainns) + if remove: + yield mainns.pop(name) + else: + try: + yield mainns[name] + finally: + mainns.pop(name, None) + + +def build_excinfo(exctype, msg=None, formatted=None, errdisplay=None): + if isinstance(exctype, type): + assert issubclass(exctype, BaseException), exctype + exctype = types.SimpleNamespace( + __name__=exctype.__name__, + __qualname__=exctype.__qualname__, + __module__=exctype.__module__, + ) + elif isinstance(exctype, str): + module, _, name = exctype.rpartition(exctype) + if not module and name in __builtins__: + module = 'builtins' + exctype = types.SimpleNamespace( + __name__=name, + __qualname__=exctype, + __module__=module or None, + ) + else: + assert isinstance(exctype, types.SimpleNamespace) + assert msg is None or isinstance(msg, str), msg + assert formatted is None or isinstance(formatted, str), formatted + assert errdisplay is None or isinstance(errdisplay, str), errdisplay + return types.SimpleNamespace( + type=exctype, + msg=msg, + formatted=formatted, + errdisplay=errdisplay, + ) + + +class ModuleTests(TestBase): + + def test_queue_aliases(self): + first = [ + interpreters.create_queue, + interpreters.Queue, + interpreters.QueueEmpty, + interpreters.QueueFull, + ] + second = [ + interpreters.create_queue, + interpreters.Queue, + interpreters.QueueEmpty, + interpreters.QueueFull, + ] + self.assertEqual(second, first) + + +class CreateTests(TestBase): + + def test_in_main(self): + interp = interpreters.create() + self.assertIsInstance(interp, interpreters.Interpreter) + self.assertIn(interp, interpreters.list_all()) + + # GH-126221: Passing an invalid Unicode character used to cause a SystemError + self.assertRaises(UnicodeEncodeError, _interpreters.create, '\udc80') + + def test_in_thread(self): + lock = threading.Lock() + interp = None + def f(): + nonlocal interp + interp = interpreters.create() + lock.acquire() + lock.release() + t = threading.Thread(target=f) + with lock: + t.start() + t.join() + self.assertIn(interp, interpreters.list_all()) + + def test_in_subinterpreter(self): + main, = interpreters.list_all() + interp = interpreters.create() + out = _run_output(interp, dedent(""" + from concurrent import interpreters + interp = interpreters.create() + print(interp.id) + """)) + interp2 = interpreters.Interpreter(int(out)) + self.assertEqual(interpreters.list_all(), [main, interp, interp2]) + + def test_after_destroy_all(self): + before = set(interpreters.list_all()) + # Create 3 subinterpreters. + interp_lst = [] + for _ in range(3): + interps = interpreters.create() + interp_lst.append(interps) + # Now destroy them. + for interp in interp_lst: + interp.close() + # Finally, create another. + interp = interpreters.create() + self.assertEqual(set(interpreters.list_all()), before | {interp}) + + def test_after_destroy_some(self): + before = set(interpreters.list_all()) + # Create 3 subinterpreters. + interp1 = interpreters.create() + interp2 = interpreters.create() + interp3 = interpreters.create() + # Now destroy 2 of them. + interp1.close() + interp2.close() + # Finally, create another. + interp = interpreters.create() + self.assertEqual(set(interpreters.list_all()), before | {interp3, interp}) + + +class GetMainTests(TestBase): + + def test_id(self): + main = interpreters.get_main() + self.assertEqual(main.id, 0) + + def test_current(self): + main = interpreters.get_main() + current = interpreters.get_current() + self.assertIs(main, current) + + def test_idempotent(self): + main1 = interpreters.get_main() + main2 = interpreters.get_main() + self.assertIs(main1, main2) + + +class GetCurrentTests(TestBase): + + def test_main(self): + main = interpreters.get_main() + current = interpreters.get_current() + self.assertEqual(current, main) + + def test_subinterpreter(self): + main = interpreters.get_main() + interp = interpreters.create() + out = _run_output(interp, dedent(""" + from concurrent import interpreters + cur = interpreters.get_current() + print(cur.id) + """)) + current = interpreters.Interpreter(int(out)) + self.assertEqual(current, interp) + self.assertNotEqual(current, main) + + def test_idempotent(self): + with self.subTest('main'): + cur1 = interpreters.get_current() + cur2 = interpreters.get_current() + self.assertIs(cur1, cur2) + + with self.subTest('subinterpreter'): + interp = interpreters.create() + out = _run_output(interp, dedent(""" + from concurrent import interpreters + cur = interpreters.get_current() + print(id(cur)) + cur = interpreters.get_current() + print(id(cur)) + """)) + objid1, objid2 = (int(v) for v in out.splitlines()) + self.assertEqual(objid1, objid2) + + with self.subTest('per-interpreter'): + interp = interpreters.create() + out = _run_output(interp, dedent(""" + from concurrent import interpreters + cur = interpreters.get_current() + print(id(cur)) + """)) + id1 = int(out) + id2 = id(interp) + self.assertNotEqual(id1, id2) + + @requires_test_modules + def test_created_with_capi(self): + expected = _testinternalcapi.next_interpreter_id() + text = self.run_temp_from_capi(f""" + import {interpreters.__name__} as interpreters + interp = interpreters.get_current() + print((interp.id, interp.whence)) + """) + interpid, whence = eval(text) + self.assertEqual(interpid, expected) + self.assertEqual(whence, WHENCE_STR_CAPI) + + +class ListAllTests(TestBase): + + def test_initial(self): + interps = interpreters.list_all() + self.assertEqual(1, len(interps)) + + def test_after_creating(self): + main = interpreters.get_current() + first = interpreters.create() + second = interpreters.create() + + ids = [] + for interp in interpreters.list_all(): + ids.append(interp.id) + + self.assertEqual(ids, [main.id, first.id, second.id]) + + def test_after_destroying(self): + main = interpreters.get_current() + first = interpreters.create() + second = interpreters.create() + first.close() + + ids = [] + for interp in interpreters.list_all(): + ids.append(interp.id) + + self.assertEqual(ids, [main.id, second.id]) + + def test_idempotent(self): + main = interpreters.get_current() + first = interpreters.create() + second = interpreters.create() + expected = [main, first, second] + + actual = interpreters.list_all() + + self.assertEqual(actual, expected) + for interp1, interp2 in zip(actual, expected): + self.assertIs(interp1, interp2) + + def test_created_with_capi(self): + mainid, *_ = _interpreters.get_main() + interpid1 = _interpreters.create() + interpid2 = _interpreters.create() + interpid3 = _interpreters.create() + interpid4 = interpid3 + 1 + interpid5 = interpid4 + 1 + expected = [ + (mainid, WHENCE_STR_RUNTIME), + (interpid1, WHENCE_STR_STDLIB), + (interpid2, WHENCE_STR_STDLIB), + (interpid3, WHENCE_STR_STDLIB), + (interpid4, WHENCE_STR_CAPI), + (interpid5, WHENCE_STR_STDLIB), + ] + expected2 = expected[:-2] + text = self.run_temp_from_capi(f""" + import {interpreters.__name__} as interpreters + interp = interpreters.create() + print( + [(i.id, i.whence) for i in interpreters.list_all()]) + """) + res = eval(text) + res2 = [(i.id, i.whence) for i in interpreters.list_all()] + self.assertEqual(res, expected) + self.assertEqual(res2, expected2) + + +class InterpreterObjectTests(TestBase): + + def test_init_int(self): + interpid = interpreters.get_current().id + interp = interpreters.Interpreter(interpid) + self.assertEqual(interp.id, interpid) + + def test_init_interpreter_id(self): + interpid = interpreters.get_current()._id + interp = interpreters.Interpreter(interpid) + self.assertEqual(interp._id, interpid) + + def test_init_unsupported(self): + actualid = interpreters.get_current().id + for interpid in [ + str(actualid), + float(actualid), + object(), + None, + '', + ]: + with self.subTest(repr(interpid)): + with self.assertRaises(TypeError): + interpreters.Interpreter(interpid) + + def test_idempotent(self): + main = interpreters.get_main() + interp = interpreters.Interpreter(main.id) + self.assertIs(interp, main) + + def test_init_does_not_exist(self): + with self.assertRaises(InterpreterNotFoundError): + interpreters.Interpreter(1_000_000) + + def test_init_bad_id(self): + with self.assertRaises(ValueError): + interpreters.Interpreter(-1) + + def test_id_type(self): + main = interpreters.get_main() + current = interpreters.get_current() + interp = interpreters.create() + self.assertIsInstance(main.id, int) + self.assertIsInstance(current.id, int) + self.assertIsInstance(interp.id, int) + + def test_id_readonly(self): + interp = interpreters.create() + with self.assertRaises(AttributeError): + interp.id = 1_000_000 + + def test_whence(self): + main = interpreters.get_main() + interp = interpreters.create() + + with self.subTest('main'): + self.assertEqual(main.whence, WHENCE_STR_RUNTIME) + + with self.subTest('from _interpreters'): + self.assertEqual(interp.whence, WHENCE_STR_STDLIB) + + with self.subTest('from C-API'): + text = self.run_temp_from_capi(f""" + import {interpreters.__name__} as interpreters + interp = interpreters.get_current() + print(repr(interp.whence)) + """) + whence = eval(text) + self.assertEqual(whence, WHENCE_STR_CAPI) + + with self.subTest('readonly'): + for value in [ + None, + WHENCE_STR_UNKNOWN, + WHENCE_STR_RUNTIME, + WHENCE_STR_STDLIB, + WHENCE_STR_CAPI, + ]: + with self.assertRaises(AttributeError): + interp.whence = value + with self.assertRaises(AttributeError): + main.whence = value + + def test_hashable(self): + interp = interpreters.create() + expected = hash(interp.id) + actual = hash(interp) + self.assertEqual(actual, expected) + + def test_equality(self): + interp1 = interpreters.create() + interp2 = interpreters.create() + self.assertEqual(interp1, interp1) + self.assertNotEqual(interp1, interp2) + + def test_pickle(self): + interp = interpreters.create() + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(protocol=protocol): + data = pickle.dumps(interp, protocol) + unpickled = pickle.loads(data) + self.assertEqual(unpickled, interp) + + @support.requires_subprocess() + @force_not_colorized + def test_cleanup_in_repl(self): + # GH-135729: Using a subinterpreter in the REPL would lead to an unraisable + # exception during finalization + repl = script_helper.spawn_python("-i") + script = b"""if True: + from concurrent import interpreters + interpreters.create() + exit()""" + stdout, stderr = repl.communicate(script) + self.assertIsNone(stderr) + self.assertIn(b"Interpreter.close()", stdout) + self.assertNotIn(b"Traceback", stdout) + + @support.requires_subprocess() + @unittest.skipIf(os.name == 'nt', "signals don't work well on windows") + def test_keyboard_interrupt_in_thread_running_interp(self): + import subprocess + source = f"""if True: + from concurrent import interpreters + from threading import Thread + + def test(): + import time + print('a', flush=True, end='') + time.sleep(10) + + interp = interpreters.create() + interp.call_in_thread(test) + """ + + with spawn_python("-c", source, stderr=subprocess.PIPE) as proc: + self.assertEqual(proc.stdout.read(1), b'a') + proc.send_signal(signal.SIGINT) + proc.stderr.flush() + error = proc.stderr.read() + self.assertIn(b"KeyboardInterrupt", error) + retcode = proc.wait() + # Sometimes we send the SIGINT after the subthread yields the GIL to + # the main thread, which results in the main thread getting the + # KeyboardInterrupt before finalization is reached. There's not + # any great way to protect against that, so we just allow a -2 + # return code as well. + self.assertIn(retcode, (0, -signal.SIGINT)) + + +class TestInterpreterIsRunning(TestBase): + + def test_main(self): + main = interpreters.get_main() + self.assertTrue(main.is_running()) + + # XXX Is this still true? + @unittest.skip('Fails on FreeBSD') + def test_subinterpreter(self): + interp = interpreters.create() + self.assertFalse(interp.is_running()) + + with _running(interp): + self.assertTrue(interp.is_running()) + self.assertFalse(interp.is_running()) + + def test_finished(self): + r, w = self.pipe() + interp = interpreters.create() + interp.exec(f"""if True: + import os + os.write({w}, b'x') + """) + self.assertFalse(interp.is_running()) + self.assertEqual(os.read(r, 1), b'x') + + def test_from_subinterpreter(self): + interp = interpreters.create() + out = _run_output(interp, dedent(f""" + import _interpreters + if _interpreters.is_running({interp.id}): + print(True) + else: + print(False) + """)) + self.assertEqual(out.strip(), 'True') + + def test_already_destroyed(self): + interp = interpreters.create() + interp.close() + with self.assertRaises(InterpreterNotFoundError): + interp.is_running() + + def test_with_only_background_threads(self): + r_interp, w_interp = self.pipe() + r_thread, w_thread = self.pipe() + + DONE = b'D' + FINISHED = b'F' + + interp = interpreters.create() + interp.exec(f"""if True: + import os + import threading + + def task(): + v = os.read({r_thread}, 1) + assert v == {DONE!r} + os.write({w_interp}, {FINISHED!r}) + t = threading.Thread(target=task) + t.start() + """) + self.assertFalse(interp.is_running()) + + os.write(w_thread, DONE) + interp.exec('t.join()') + self.assertEqual(os.read(r_interp, 1), FINISHED) + + def test_created_with_capi(self): + script = dedent(f""" + import {interpreters.__name__} as interpreters + interp = interpreters.get_current() + print(interp.is_running()) + """) + def parse_results(text): + self.assertNotEqual(text, "") + try: + return eval(text) + except Exception: + raise Exception(repr(text)) + + with self.subTest('running __main__ (from self)'): + with self.interpreter_from_capi() as interpid: + text = self.run_from_capi(interpid, script, main=True) + running = parse_results(text) + self.assertTrue(running) + + with self.subTest('running, but not __main__ (from self)'): + text = self.run_temp_from_capi(script) + running = parse_results(text) + self.assertFalse(running) + + with self.subTest('running __main__ (from other)'): + with self.interpreter_obj_from_capi() as (interp, interpid): + before = interp.is_running() + with self.running_from_capi(interpid, main=True): + during = interp.is_running() + after = interp.is_running() + self.assertFalse(before) + self.assertTrue(during) + self.assertFalse(after) + + with self.subTest('running, but not __main__ (from other)'): + with self.interpreter_obj_from_capi() as (interp, interpid): + before = interp.is_running() + with self.running_from_capi(interpid, main=False): + during = interp.is_running() + after = interp.is_running() + self.assertFalse(before) + self.assertFalse(during) + self.assertFalse(after) + + with self.subTest('not running (from other)'): + with self.interpreter_obj_from_capi() as (interp, _): + running = interp.is_running() + self.assertFalse(running) + + +class TestInterpreterClose(TestBase): + + def test_basic(self): + main = interpreters.get_main() + interp1 = interpreters.create() + interp2 = interpreters.create() + interp3 = interpreters.create() + self.assertEqual(set(interpreters.list_all()), + {main, interp1, interp2, interp3}) + interp2.close() + self.assertEqual(set(interpreters.list_all()), + {main, interp1, interp3}) + + def test_all(self): + before = set(interpreters.list_all()) + interps = set() + for _ in range(3): + interp = interpreters.create() + interps.add(interp) + self.assertEqual(set(interpreters.list_all()), before | interps) + for interp in interps: + interp.close() + self.assertEqual(set(interpreters.list_all()), before) + + def test_main(self): + main, = interpreters.list_all() + with self.assertRaises(InterpreterError): + main.close() + + def f(): + with self.assertRaises(InterpreterError): + main.close() + + t = threading.Thread(target=f) + t.start() + t.join() + + def test_already_destroyed(self): + interp = interpreters.create() + interp.close() + with self.assertRaises(InterpreterNotFoundError): + interp.close() + + def test_from_current(self): + main, = interpreters.list_all() + interp = interpreters.create() + out = _run_output(interp, dedent(f""" + from concurrent import interpreters + interp = interpreters.Interpreter({interp.id}) + try: + interp.close() + except interpreters.InterpreterError: + print('failed') + """)) + self.assertEqual(out.strip(), 'failed') + self.assertEqual(set(interpreters.list_all()), {main, interp}) + + def test_from_sibling(self): + main, = interpreters.list_all() + interp1 = interpreters.create() + interp2 = interpreters.create() + self.assertEqual(set(interpreters.list_all()), + {main, interp1, interp2}) + interp1.exec(dedent(f""" + from concurrent import interpreters + interp2 = interpreters.Interpreter({interp2.id}) + interp2.close() + interp3 = interpreters.create() + interp3.close() + """)) + self.assertEqual(set(interpreters.list_all()), {main, interp1}) + + def test_from_other_thread(self): + interp = interpreters.create() + def f(): + interp.close() + + t = threading.Thread(target=f) + t.start() + t.join() + + # XXX Is this still true? + @unittest.skip('Fails on FreeBSD') + def test_still_running(self): + main, = interpreters.list_all() + interp = interpreters.create() + with _running(interp): + with self.assertRaises(InterpreterError): + interp.close() + self.assertTrue(interp.is_running()) + + def test_subthreads_still_running(self): + r_interp, w_interp = self.pipe() + r_thread, w_thread = self.pipe() + + FINISHED = b'F' + + interp = interpreters.create() + interp.exec(f"""if True: + import os + import threading + import time + + done = False + + def notify_fini(): + global done + done = True + t.join() + threading._register_atexit(notify_fini) + + def task(): + while not done: + time.sleep(0.1) + os.write({w_interp}, {FINISHED!r}) + t = threading.Thread(target=task) + t.start() + """) + interp.close() + + self.assertEqual(os.read(r_interp, 1), FINISHED) + + def test_created_with_capi(self): + script = dedent(f""" + import {interpreters.__name__} as interpreters + interp = interpreters.get_current() + interp.close() + """) + + with self.subTest('running __main__ (from self)'): + with self.interpreter_from_capi() as interpid: + with self.assertRaisesRegex(ExecutionFailed, + 'InterpreterError.*unrecognized'): + self.run_from_capi(interpid, script, main=True) + + with self.subTest('running, but not __main__ (from self)'): + with self.assertRaisesRegex(ExecutionFailed, + 'InterpreterError.*unrecognized'): + self.run_temp_from_capi(script) + + with self.subTest('running __main__ (from other)'): + with self.interpreter_obj_from_capi() as (interp, interpid): + with self.running_from_capi(interpid, main=True): + with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + interp.close() + # Make sure it wssn't closed. + self.assertTrue( + self.interp_exists(interpid)) + + # The rest would be skipped until we deal with running threads when + # interp.close() is called. However, the "whence" restrictions + # trigger first. + + with self.subTest('running, but not __main__ (from other)'): + with self.interpreter_obj_from_capi() as (interp, interpid): + with self.running_from_capi(interpid, main=False): + with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + interp.close() + # Make sure it wssn't closed. + self.assertTrue( + self.interp_exists(interpid)) + + with self.subTest('not running (from other)'): + with self.interpreter_obj_from_capi() as (interp, interpid): + with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + interp.close() + self.assertTrue( + self.interp_exists(interpid)) + + + def test_remaining_threads(self): + r_interp, w_interp = self.pipe() + + FINISHED = b'F' + + # It's unlikely, but technically speaking, it's possible + # that the thread could've finished before interp.close() is + # reached, so this test might not properly exercise the case. + # However, it's quite unlikely and probably not worth bothering about. + interp = interpreters.create() + interp.exec(f"""if True: + import os + import threading + import time + + def task(): + time.sleep(1) + os.write({w_interp}, {FINISHED!r}) + + threads = (threading.Thread(target=task) for _ in range(3)) + for t in threads: + t.start() + """) + interp.close() + + self.assertEqual(os.read(r_interp, 1), FINISHED) + + def test_remaining_daemon_threads(self): + # Daemon threads leak reference by nature, because they hang threads + # without allowing them to do cleanup (i.e., release refs). + # To prevent that from messing up the refleak hunter and whatnot, we + # run this in a subprocess. + code = '''if True: + import _interpreters + import types + interp = _interpreters.create( + types.SimpleNamespace( + use_main_obmalloc=False, + allow_fork=False, + allow_exec=False, + allow_threads=True, + allow_daemon_threads=True, + check_multi_interp_extensions=True, + gil='own', + ) + ) + _interpreters.exec(interp, f"""if True: + import threading + import time + + def task(): + time.sleep(3) + + threads = (threading.Thread(target=task, daemon=True) for _ in range(3)) + for t in threads: + t.start() + """) + _interpreters.destroy(interp) + ''' + assert_python_ok('-c', code) + + +class TestInterpreterPrepareMain(TestBase): + + def test_empty(self): + interp = interpreters.create() + with self.assertRaises(ValueError): + interp.prepare_main() + + def test_dict(self): + values = {'spam': 42, 'eggs': 'ham'} + interp = interpreters.create() + interp.prepare_main(values) + out = _run_output(interp, dedent(""" + print(spam, eggs) + """)) + self.assertEqual(out.strip(), '42 ham') + + def test_tuple(self): + values = {'spam': 42, 'eggs': 'ham'} + values = tuple(values.items()) + interp = interpreters.create() + interp.prepare_main(values) + out = _run_output(interp, dedent(""" + print(spam, eggs) + """)) + self.assertEqual(out.strip(), '42 ham') + + def test_kwargs(self): + values = {'spam': 42, 'eggs': 'ham'} + interp = interpreters.create() + interp.prepare_main(**values) + out = _run_output(interp, dedent(""" + print(spam, eggs) + """)) + self.assertEqual(out.strip(), '42 ham') + + def test_dict_and_kwargs(self): + values = {'spam': 42, 'eggs': 'ham'} + interp = interpreters.create() + interp.prepare_main(values, foo='bar') + out = _run_output(interp, dedent(""" + print(spam, eggs, foo) + """)) + self.assertEqual(out.strip(), '42 ham bar') + + def test_not_shareable(self): + interp = interpreters.create() + with self.assertRaises(interpreters.NotShareableError): + interp.prepare_main(spam={'spam': 'eggs', 'foo': 'bar'}) + + # Make sure neither was actually bound. + with self.assertRaises(ExecutionFailed): + interp.exec('print(foo)') + with self.assertRaises(ExecutionFailed): + interp.exec('print(spam)') + + def test_running(self): + interp = interpreters.create() + interp.prepare_main({'spam': True}) + with self.running(interp): + with self.assertRaisesRegex(InterpreterError, 'running'): + interp.prepare_main({'spam': False}) + interp.exec('assert spam is True') + + @requires_test_modules + def test_created_with_capi(self): + with self.interpreter_obj_from_capi() as (interp, interpid): + with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + interp.prepare_main({'spam': True}) + with self.assertRaisesRegex(ExecutionFailed, 'NameError'): + self.run_from_capi(interpid, 'spam') + + +class TestInterpreterExec(TestBase): + + def test_success(self): + interp = interpreters.create() + script, results = _captured_script('print("it worked!", end="")') + with results: + interp.exec(script) + results = results.final() + results.raise_if_failed() + out = results.stdout + + self.assertEqual(out, 'it worked!') + + def test_failure(self): + interp = interpreters.create() + with self.assertRaises(ExecutionFailed): + interp.exec('raise Exception') + + @force_not_colorized + def test_display_preserved_exception(self): + tempdir = self.temp_dir() + modfile = self.make_module('spam', tempdir, text=""" + def ham(): + raise RuntimeError('uh-oh!') + + def eggs(): + ham() + """) + scriptfile = self.make_script('script.py', tempdir, text=""" + from concurrent import interpreters + + def script(): + import spam + spam.eggs() + + interp = interpreters.create() + try: + interp.exec(script) + finally: + interp.close() + """) + + stdout, stderr = self.assert_python_failure(scriptfile) + self.maxDiff = None + interpmod_line, = (l for l in stderr.splitlines() if ' exec' in l) + # File "{interpreters.__file__}", line 179, in exec + self.assertEqual(stderr, dedent(f"""\ + Traceback (most recent call last): + File "{scriptfile}", line 10, in + interp.exec(script) + ~~~~~~~~~~~^^^^^^^^ + {interpmod_line.strip()} + raise ExecutionFailed(excinfo) + concurrent.interpreters.ExecutionFailed: RuntimeError: uh-oh! + + Uncaught in the interpreter: + + Traceback (most recent call last): + File "{scriptfile}", line 6, in script + spam.eggs() + ~~~~~~~~~^^ + File "{modfile}", line 6, in eggs + ham() + ~~~^^ + File "{modfile}", line 3, in ham + raise RuntimeError('uh-oh!') + RuntimeError: uh-oh! + """)) + self.assertEqual(stdout, '') + + def test_in_thread(self): + interp = interpreters.create() + script, results = _captured_script('print("it worked!", end="")') + with results: + def f(): + interp.exec(script) + + t = threading.Thread(target=f) + t.start() + t.join() + results = results.final() + results.raise_if_failed() + out = results.stdout + + self.assertEqual(out, 'it worked!') + + @support.requires_fork() + def test_fork(self): + interp = interpreters.create() + import tempfile + with tempfile.NamedTemporaryFile('w+', encoding='utf-8') as file: + file.write('') + file.flush() + + expected = 'spam spam spam spam spam' + script = dedent(f""" + import os + try: + os.fork() + except RuntimeError: + with open('{file.name}', 'w', encoding='utf-8') as out: + out.write('{expected}') + """) + interp.exec(script) + + file.seek(0) + content = file.read() + self.assertEqual(content, expected) + + # XXX Is this still true? + @unittest.skip('Fails on FreeBSD') + def test_already_running(self): + interp = interpreters.create() + with _running(interp): + with self.assertRaises(RuntimeError): + interp.exec('print("spam")') + + def test_bad_script(self): + interp = interpreters.create() + with self.assertRaises(TypeError): + interp.exec(10) + + def test_bytes_for_script(self): + r, w = self.pipe() + RAN = b'R' + DONE = b'D' + interp = interpreters.create() + interp.exec(f"""if True: + import os + os.write({w}, {RAN!r}) + """) + os.write(w, DONE) + self.assertEqual(os.read(r, 1), RAN) + + def test_with_background_threads_still_running(self): + r_interp, w_interp = self.pipe() + r_thread, w_thread = self.pipe() + + RAN = b'R' + DONE = b'D' + FINISHED = b'F' + + interp = interpreters.create() + interp.exec(f"""if True: + import os + import threading + + def task(): + v = os.read({r_thread}, 1) + assert v == {DONE!r} + os.write({w_interp}, {FINISHED!r}) + t = threading.Thread(target=task) + t.start() + os.write({w_interp}, {RAN!r}) + """) + interp.exec(f"""if True: + os.write({w_interp}, {RAN!r}) + """) + + os.write(w_thread, DONE) + interp.exec('t.join()') + self.assertEqual(os.read(r_interp, 1), RAN) + self.assertEqual(os.read(r_interp, 1), RAN) + self.assertEqual(os.read(r_interp, 1), FINISHED) + + def test_created_with_capi(self): + with self.interpreter_obj_from_capi() as (interp, _): + with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + interp.exec('raise Exception("it worked!")') + + def test_list_comprehension(self): + # gh-135450: List comprehensions caused an assertion failure + # in _PyCode_CheckNoExternalState() + import string + r_interp, w_interp = self.pipe() + + interp = interpreters.create() + interp.exec(f"""if True: + import os + comp = [str(i) for i in range(10)] + os.write({w_interp}, ''.join(comp).encode()) + """) + self.assertEqual(os.read(r_interp, 10).decode(), string.digits) + interp.close() + + + # test__interpreters covers the remaining + # Interpreter.exec() behavior. + + +call_func_noop = defs.spam_minimal +call_func_ident = defs.spam_returns_arg +call_func_failure = defs.spam_raises + + +def call_func_return_shareable(): + return (1, None) + + +def call_func_return_stateless_func(): + return (lambda x: x) + + +def call_func_return_pickleable(): + return [1, 2, 3] + + +def call_func_return_unpickleable(): + x = 42 + return (lambda: x) + + +def get_call_func_closure(value): + def call_func_closure(): + return value + return call_func_closure + + +def call_func_exec_wrapper(script, ns): + res = exec(script, ns, ns) + return res, ns, id(ns) + + +class Spam: + + @staticmethod + def noop(): + pass + + @classmethod + def from_values(cls, *values): + return cls(values) + + def __init__(self, value): + self.value = value + + def __call__(self, *args, **kwargs): + return (self.value, args, kwargs) + + def __eq__(self, other): + if not isinstance(other, Spam): + return NotImplemented + return self.value == other.value + + def run(self, *args, **kwargs): + return (self.value, args, kwargs) + + +def call_func_complex(op, /, value=None, *args, exc=None, **kwargs): + if exc is not None: + raise exc + if op == '': + raise ValueError('missing op') + elif op == 'ident': + if args or kwargs: + raise Exception((args, kwargs)) + return value + elif op == 'full-ident': + return (value, args, kwargs) + elif op == 'globals': + if value is not None or args or kwargs: + raise Exception((value, args, kwargs)) + return __name__ + elif op == 'interpid': + if value is not None or args or kwargs: + raise Exception((value, args, kwargs)) + return interpreters.get_current().id + elif op == 'closure': + if args or kwargs: + raise Exception((args, kwargs)) + return get_call_func_closure(value) + elif op == 'custom': + if args or kwargs: + raise Exception((args, kwargs)) + return Spam(value) + elif op == 'custom-inner': + if args or kwargs: + raise Exception((args, kwargs)) + class Eggs(Spam): + pass + return Eggs(value) + elif not isinstance(op, str): + raise TypeError(op) + else: + raise NotImplementedError(op) + + +class TestInterpreterCall(TestBase): + + # signature + # - blank + # - args + # - kwargs + # - args, kwargs + # return + # - nothing (None) + # - simple + # - closure + # - custom + # ops: + # - do nothing + # - fail + # - echo + # - do complex, relative to interpreter + # scope + # - global func + # - local closure + # - returned closure + # - callable type instance + # - type + # - classmethod + # - staticmethod + # - instance method + # exception + # - builtin + # - custom + # - preserves info (e.g. SyntaxError) + # - matching error display + + @contextlib.contextmanager + def assert_fails(self, expected): + with self.assertRaises(ExecutionFailed) as cm: + yield cm + uncaught = cm.exception.excinfo + self.assertEqual(uncaught.type.__name__, expected.__name__) + + def assert_fails_not_shareable(self): + return self.assert_fails(interpreters.NotShareableError) + + def assert_code_equal(self, code1, code2): + if code1 == code2: + return + self.assertEqual(code1.co_name, code2.co_name) + self.assertEqual(code1.co_flags, code2.co_flags) + self.assertEqual(code1.co_consts, code2.co_consts) + self.assertEqual(code1.co_varnames, code2.co_varnames) + self.assertEqual(code1.co_cellvars, code2.co_cellvars) + self.assertEqual(code1.co_freevars, code2.co_freevars) + self.assertEqual(code1.co_names, code2.co_names) + self.assertEqual( + _testinternalcapi.get_code_var_counts(code1), + _testinternalcapi.get_code_var_counts(code2), + ) + self.assertEqual(code1.co_code, code2.co_code) + + def assert_funcs_equal(self, func1, func2): + if func1 == func2: + return + self.assertIs(type(func1), type(func2)) + self.assertEqual(func1.__name__, func2.__name__) + self.assertEqual(func1.__defaults__, func2.__defaults__) + self.assertEqual(func1.__kwdefaults__, func2.__kwdefaults__) + self.assertEqual(func1.__closure__, func2.__closure__) + self.assert_code_equal(func1.__code__, func2.__code__) + self.assertEqual( + _testinternalcapi.get_code_var_counts(func1), + _testinternalcapi.get_code_var_counts(func2), + ) + + def assert_exceptions_equal(self, exc1, exc2): + assert isinstance(exc1, Exception) + assert isinstance(exc2, Exception) + if exc1 == exc2: + return + self.assertIs(type(exc1), type(exc2)) + self.assertEqual(exc1.args, exc2.args) + + def test_stateless_funcs(self): + interp = interpreters.create() + + func = call_func_noop + with self.subTest('no args, no return'): + res = interp.call(func) + self.assertIsNone(res) + + func = call_func_return_shareable + with self.subTest('no args, returns shareable'): + res = interp.call(func) + self.assertEqual(res, (1, None)) + + func = call_func_return_stateless_func + expected = (lambda x: x) + with self.subTest('no args, returns stateless func'): + res = interp.call(func) + self.assert_funcs_equal(res, expected) + + func = call_func_return_pickleable + with self.subTest('no args, returns pickleable'): + res = interp.call(func) + self.assertEqual(res, [1, 2, 3]) + + func = call_func_return_unpickleable + with self.subTest('no args, returns unpickleable'): + with self.assertRaises(interpreters.NotShareableError): + interp.call(func) + + def test_stateless_func_returns_arg(self): + interp = interpreters.create() + + for arg in [ + None, + 10, + 'spam!', + b'spam!', + (1, 2, 'spam!'), + memoryview(b'spam!'), + ]: + with self.subTest(f'shareable {arg!r}'): + assert _interpreters.is_shareable(arg) + res = interp.call(defs.spam_returns_arg, arg) + self.assertEqual(res, arg) + + for arg in defs.STATELESS_FUNCTIONS: + with self.subTest(f'stateless func {arg!r}'): + res = interp.call(defs.spam_returns_arg, arg) + self.assert_funcs_equal(res, arg) + + for arg in defs.TOP_FUNCTIONS: + if arg in defs.STATELESS_FUNCTIONS: + continue + with self.subTest(f'stateful func {arg!r}'): + res = interp.call(defs.spam_returns_arg, arg) + self.assert_funcs_equal(res, arg) + assert is_pickleable(arg) + + for arg in [ + Ellipsis, + NotImplemented, + object(), + 2**1000, + [1, 2, 3], + {'a': 1, 'b': 2}, + types.SimpleNamespace(x=42), + # builtin types + object, + type, + Exception, + ModuleNotFoundError, + # builtin exceptions + Exception('uh-oh!'), + ModuleNotFoundError('mymodule'), + # builtin fnctions + len, + sys.exit, + # user classes + *defs.TOP_CLASSES, + *(c(*a) for c, a in defs.TOP_CLASSES.items() + if c not in defs.CLASSES_WITHOUT_EQUALITY), + ]: + with self.subTest(f'pickleable {arg!r}'): + res = interp.call(defs.spam_returns_arg, arg) + if type(arg) is object: + self.assertIs(type(res), object) + elif isinstance(arg, BaseException): + self.assert_exceptions_equal(res, arg) + else: + self.assertEqual(res, arg) + assert is_pickleable(arg) + + for arg in [ + types.MappingProxyType({}), + *(f for f in defs.NESTED_FUNCTIONS + if f not in defs.STATELESS_FUNCTIONS), + ]: + with self.subTest(f'unpickleable {arg!r}'): + assert not _interpreters.is_shareable(arg) + assert not is_pickleable(arg) + with self.assertRaises(interpreters.NotShareableError): + interp.call(defs.spam_returns_arg, arg) + + def test_full_args(self): + interp = interpreters.create() + expected = (1, 2, 3, 4, 5, 6, ('?',), {'g': 7, 'h': 8}) + func = defs.spam_full_args + res = interp.call(func, 1, 2, 3, 4, '?', e=5, f=6, g=7, h=8) + self.assertEqual(res, expected) + + def test_full_defaults(self): + # pickleable, but not stateless + interp = interpreters.create() + expected = (-1, -2, -3, -4, -5, -6, (), {'g': 8, 'h': 9}) + res = interp.call(defs.spam_full_args_with_defaults, g=8, h=9) + self.assertEqual(res, expected) + + def test_modified_arg(self): + interp = interpreters.create() + script = dedent(""" + a = 7 + b = 2 + c = a ** b + """) + ns = {} + expected = {'a': 7, 'b': 2, 'c': 49} + res = interp.call(call_func_exec_wrapper, script, ns) + obj, resns, resid = res + del resns['__builtins__'] + self.assertIsNone(obj) + self.assertEqual(ns, {}) + self.assertEqual(resns, expected) + self.assertNotEqual(resid, id(ns)) + self.assertNotEqual(resid, id(resns)) + + def test_func_in___main___valid(self): + # pickleable, already there' + + with os_helper.temp_dir() as tempdir: + def new_mod(name, text): + script_helper.make_script(tempdir, name, dedent(text)) + + def run(text): + name = 'myscript' + text = dedent(f""" + import sys + sys.path.insert(0, {tempdir!r}) + + """) + dedent(text) + filename = script_helper.make_script(tempdir, name, text) + res = script_helper.assert_python_ok(filename) + return res.out.decode('utf-8').strip() + + # no module indirection + with self.subTest('no indirection'): + text = run(f""" + from concurrent import interpreters + + def spam(): + # This a global var... + return __name__ + + if __name__ == '__main__': + interp = interpreters.create() + res = interp.call(spam) + print(res) + """) + self.assertEqual(text, '') + + # indirect as func, direct interp + new_mod('mymod', f""" + def run(interp, func): + return interp.call(func) + """) + with self.subTest('indirect as func, direct interp'): + text = run(f""" + from concurrent import interpreters + import mymod + + def spam(): + # This a global var... + return __name__ + + if __name__ == '__main__': + interp = interpreters.create() + res = mymod.run(interp, spam) + print(res) + """) + self.assertEqual(text, '') + + # indirect as func, indirect interp + new_mod('mymod', f""" + from concurrent import interpreters + def run(func): + interp = interpreters.create() + return interp.call(func) + """) + with self.subTest('indirect as func, indirect interp'): + text = run(f""" + import mymod + + def spam(): + # This a global var... + return __name__ + + if __name__ == '__main__': + res = mymod.run(spam) + print(res) + """) + self.assertEqual(text, '') + + def test_func_in___main___invalid(self): + interp = interpreters.create() + + funcname = f'{__name__.replace(".", "_")}_spam_okay' + script = dedent(f""" + def {funcname}(): + # This a global var... + return __name__ + """) + + with self.subTest('pickleable, added dynamically'): + with defined_in___main__(funcname, script) as arg: + with self.assertRaises(interpreters.NotShareableError): + interp.call(defs.spam_returns_arg, arg) + + with self.subTest('lying about __main__'): + with defined_in___main__(funcname, script, remove=True) as arg: + with self.assertRaises(interpreters.NotShareableError): + interp.call(defs.spam_returns_arg, arg) + + def test_func_in___main___hidden(self): + # When a top-level function that uses global variables is called + # through Interpreter.call(), it will be pickled, sent over, + # and unpickled. That requires that it be found in the other + # interpreter's __main__ module. However, the original script + # that defined the function is only run in the main interpreter, + # so pickle.loads() would normally fail. + # + # We work around this by running the script in the other + # interpreter. However, this is a one-off solution for the sake + # of unpickling, so we avoid modifying that interpreter's + # __main__ module by running the script in a hidden module. + # + # In this test we verify that the function runs with the hidden + # module as its __globals__ when called in the other interpreter, + # and that the interpreter's __main__ module is unaffected. + text = dedent(""" + eggs = True + + def spam(*, explicit=False): + if explicit: + import __main__ + ns = __main__.__dict__ + else: + # For now we have to have a LOAD_GLOBAL in the + # function in order for globals() to actually return + # spam.__globals__. Maybe it doesn't go through pickle? + # XXX We will fix this later. + spam + ns = globals() + + func = ns.get('spam') + return [ + id(ns), + ns.get('__name__'), + ns.get('__file__'), + id(func), + None if func is None else repr(func), + ns.get('eggs'), + ns.get('ham'), + ] + + if __name__ == "__main__": + from concurrent import interpreters + interp = interpreters.create() + + ham = True + print([ + [ + spam(explicit=True), + spam(), + ], + [ + interp.call(spam, explicit=True), + interp.call(spam), + ], + ]) + """) + with os_helper.temp_dir() as tempdir: + filename = script_helper.make_script(tempdir, 'my-script', text) + res = script_helper.assert_python_ok(filename) + stdout = res.out.decode('utf-8').strip() + local, remote = eval(stdout) + + # In the main interpreter. + main, unpickled = local + nsid, _, _, funcid, func, _, _ = main + self.assertEqual(main, [ + nsid, + '__main__', + filename, + funcid, + func, + True, + True, + ]) + self.assertIsNot(func, None) + self.assertRegex(func, '^$') + self.assertEqual(unpickled, main) + + # In the subinterpreter. + main, unpickled = remote + nsid1, _, _, funcid1, _, _, _ = main + self.assertEqual(main, [ + nsid1, + '__main__', + None, + funcid1, + None, + None, + None, + ]) + nsid2, _, _, funcid2, func, _, _ = unpickled + self.assertEqual(unpickled, [ + nsid2, + '', + filename, + funcid2, + func, + True, + None, + ]) + self.assertIsNot(func, None) + self.assertRegex(func, '^$') + self.assertNotEqual(nsid2, nsid1) + self.assertNotEqual(funcid2, funcid1) + + def test_func_in___main___uses_globals(self): + # See the note in test_func_in___main___hidden about pickle + # and the __main__ module. + # + # Additionally, the solution to that problem must provide + # for global variables on which a pickled function might rely. + # + # To check that, we run a script that has two global functions + # and a global variable in the __main__ module. One of the + # functions sets the global variable and the other returns + # the value. + # + # The script calls those functions multiple times in another + # interpreter, to verify the following: + # + # * the global variable is properly initialized + # * the global variable retains state between calls + # * the setter modifies that persistent variable + # * the getter uses the variable + # * the calls in the other interpreter do not modify + # the main interpreter + # * those calls don't modify the interpreter's __main__ module + # * the functions and variable do not actually show up in the + # other interpreter's __main__ module + text = dedent(""" + count = 0 + + def inc(x=1): + global count + count += x + + def get_count(): + return count + + if __name__ == "__main__": + counts = [] + results = [count, counts] + + from concurrent import interpreters + interp = interpreters.create() + + val = interp.call(get_count) + counts.append(val) + + interp.call(inc) + val = interp.call(get_count) + counts.append(val) + + interp.call(inc, 3) + val = interp.call(get_count) + counts.append(val) + + results.append(count) + + modified = {name: interp.call(eval, f'{name!r} in vars()') + for name in ('count', 'inc', 'get_count')} + results.append(modified) + + print(results) + """) + with os_helper.temp_dir() as tempdir: + filename = script_helper.make_script(tempdir, 'my-script', text) + res = script_helper.assert_python_ok(filename) + stdout = res.out.decode('utf-8').strip() + before, counts, after, modified = eval(stdout) + self.assertEqual(modified, { + 'count': False, + 'inc': False, + 'get_count': False, + }) + self.assertEqual(before, 0) + self.assertEqual(after, 0) + self.assertEqual(counts, [0, 1, 4]) + + def test_raises(self): + interp = interpreters.create() + with self.assertRaises(ExecutionFailed): + interp.call(call_func_failure) + + with self.assert_fails(ValueError): + interp.call(call_func_complex, '???', exc=ValueError('spam')) + + def test_call_valid(self): + interp = interpreters.create() + + for i, (callable, args, kwargs, expected) in enumerate([ + (call_func_noop, (), {}, None), + (call_func_ident, ('spamspamspam',), {}, 'spamspamspam'), + (call_func_return_shareable, (), {}, (1, None)), + (call_func_return_pickleable, (), {}, [1, 2, 3]), + (Spam.noop, (), {}, None), + (Spam.from_values, (), {}, Spam(())), + (Spam.from_values, (1, 2, 3), {}, Spam((1, 2, 3))), + (Spam, ('???',), {}, Spam('???')), + (Spam(101), (), {}, (101, (), {})), + (Spam(10101).run, (), {}, (10101, (), {})), + (call_func_complex, ('ident', 'spam'), {}, 'spam'), + (call_func_complex, ('full-ident', 'spam'), {}, ('spam', (), {})), + (call_func_complex, ('full-ident', 'spam', 'ham'), {'eggs': '!!!'}, + ('spam', ('ham',), {'eggs': '!!!'})), + (call_func_complex, ('globals',), {}, __name__), + (call_func_complex, ('interpid',), {}, interp.id), + (call_func_complex, ('custom', 'spam!'), {}, Spam('spam!')), + ]): + with self.subTest(f'success case #{i+1}'): + res = interp.call(callable, *args, **kwargs) + self.assertEqual(res, expected) + + def test_call_invalid(self): + interp = interpreters.create() + + func = get_call_func_closure + with self.subTest(func): + with self.assertRaises(interpreters.NotShareableError): + interp.call(func, 42) + + func = get_call_func_closure(42) + with self.subTest(func): + with self.assertRaises(interpreters.NotShareableError): + interp.call(func) + + func = call_func_complex + op = 'closure' + with self.subTest(f'{func} ({op})'): + with self.assertRaises(interpreters.NotShareableError): + interp.call(func, op, value='~~~') + + op = 'custom-inner' + with self.subTest(f'{func} ({op})'): + with self.assertRaises(interpreters.NotShareableError): + interp.call(func, op, 'eggs!') + + def test_callable_requires_frame(self): + # There are various functions that require a current frame. + interp = interpreters.create() + for call, expected in [ + ((eval, '[1, 2, 3]'), + [1, 2, 3]), + ((eval, 'sum([1, 2, 3])'), + 6), + ((exec, '...'), + None), + ]: + with self.subTest(str(call)): + res = interp.call(*call) + self.assertEqual(res, expected) + + result_not_pickleable = [ + globals, + locals, + vars, + ] + for func, expectedtype in { + globals: dict, + locals: dict, + vars: dict, + dir: list, + }.items(): + with self.subTest(str(func)): + if func in result_not_pickleable: + with self.assertRaises(interpreters.NotShareableError): + interp.call(func) + else: + res = interp.call(func) + self.assertIsInstance(res, expectedtype) + self.assertIn('__builtins__', res) + + def test_globals_from_builtins(self): + # The builtins exec(), eval(), globals(), locals(), vars(), + # and dir() each runs relative to the target interpreter's + # __main__ module, when called directly. However, + # globals(), locals(), and vars() don't work when called + # directly so we don't check them. + from _frozen_importlib import BuiltinImporter + interp = interpreters.create() + + names = interp.call(dir) + self.assertEqual(names, [ + '__builtins__', + '__doc__', + '__loader__', + '__name__', + '__package__', + '__spec__', + ]) + + values = {name: interp.call(eval, name) + for name in names if name != '__builtins__'} + self.assertEqual(values, { + '__name__': '__main__', + '__doc__': None, + '__spec__': None, # It wasn't imported, so no module spec? + '__package__': None, + '__loader__': BuiltinImporter, + }) + with self.assertRaises(ExecutionFailed): + interp.call(eval, 'spam'), + + interp.call(exec, f'assert dir() == {names}') + + # Update the interpreter's __main__. + interp.prepare_main(spam=42) + expected = names + ['spam'] + + names = interp.call(dir) + self.assertEqual(names, expected) + + value = interp.call(eval, 'spam') + self.assertEqual(value, 42) + + interp.call(exec, f'assert dir() == {expected}, dir()') + + def test_globals_from_stateless_func(self): + # A stateless func, which doesn't depend on any globals, + # doesn't go through pickle, so it runs in __main__. + def set_global(name, value): + globals()[name] = value + + def get_global(name): + return globals().get(name) + + interp = interpreters.create() + + modname = interp.call(get_global, '__name__') + self.assertEqual(modname, '__main__') + + res = interp.call(get_global, 'spam') + self.assertIsNone(res) + + interp.exec('spam = True') + res = interp.call(get_global, 'spam') + self.assertTrue(res) + + interp.call(set_global, 'spam', 42) + res = interp.call(get_global, 'spam') + self.assertEqual(res, 42) + + interp.exec('assert spam == 42, repr(spam)') + + def test_call_in_thread(self): + interp = interpreters.create() + + for i, (callable, args, kwargs) in enumerate([ + (call_func_noop, (), {}), + (call_func_return_shareable, (), {}), + (call_func_return_pickleable, (), {}), + (Spam.from_values, (), {}), + (Spam.from_values, (1, 2, 3), {}), + (Spam(101), (), {}), + (Spam(10101).run, (), {}), + (Spam.noop, (), {}), + (call_func_complex, ('ident', 'spam'), {}), + (call_func_complex, ('full-ident', 'spam'), {}), + (call_func_complex, ('full-ident', 'spam', 'ham'), {'eggs': '!!!'}), + (call_func_complex, ('globals',), {}), + (call_func_complex, ('interpid',), {}), + (call_func_complex, ('custom', 'spam!'), {}), + ]): + with self.subTest(f'success case #{i+1}'): + with self.captured_thread_exception() as ctx: + t = interp.call_in_thread(callable, *args, **kwargs) + t.join() + self.assertIsNone(ctx.caught) + + for i, (callable, args, kwargs) in enumerate([ + (get_call_func_closure, (42,), {}), + (get_call_func_closure(42), (), {}), + ]): + with self.subTest(f'invalid case #{i+1}'): + with self.captured_thread_exception() as ctx: + t = interp.call_in_thread(callable, *args, **kwargs) + t.join() + self.assertIsNotNone(ctx.caught) + + with self.captured_thread_exception() as ctx: + t = interp.call_in_thread(call_func_failure) + t.join() + self.assertIsNotNone(ctx.caught) + + +class TestIsShareable(TestBase): + + def test_default_shareables(self): + shareables = [ + # singletons + None, + # builtin objects + b'spam', + 'spam', + 10, + -10, + True, + False, + 100.0, + (), + (1, ('spam', 'eggs'), True), + ] + for obj in shareables: + with self.subTest(obj): + shareable = interpreters.is_shareable(obj) + self.assertTrue(shareable) + + def test_not_shareable(self): + class Cheese: + def __init__(self, name): + self.name = name + def __str__(self): + return self.name + + class SubBytes(bytes): + """A subclass of a shareable type.""" + + not_shareables = [ + # singletons + NotImplemented, + ..., + # builtin types and objects + type, + object, + object(), + Exception(), + # user-defined types and objects + Cheese, + Cheese('Wensleydale'), + SubBytes(b'spam'), + ] + for obj in not_shareables: + with self.subTest(repr(obj)): + self.assertFalse( + interpreters.is_shareable(obj)) + + +class LowLevelTests(TestBase): + + # The behaviors in the low-level module are important in as much + # as they are exercised by the high-level module. Therefore the + # most important testing happens in the high-level tests. + # These low-level tests cover corner cases that are not + # encountered by the high-level module, thus they + # mostly shouldn't matter as much. + + def test_new_config(self): + # This test overlaps with + # test.test_capi.test_misc.InterpreterConfigTests. + + default = _interpreters.new_config('isolated') + with self.subTest('no arg'): + config = _interpreters.new_config() + self.assert_ns_equal(config, default) + self.assertIsNot(config, default) + + with self.subTest('default'): + config1 = _interpreters.new_config('default') + self.assert_ns_equal(config1, default) + self.assertIsNot(config1, default) + + config2 = _interpreters.new_config('default') + self.assert_ns_equal(config2, config1) + self.assertIsNot(config2, config1) + + for arg in ['', 'default']: + with self.subTest(f'default ({arg!r})'): + config = _interpreters.new_config(arg) + self.assert_ns_equal(config, default) + self.assertIsNot(config, default) + + supported = { + 'isolated': types.SimpleNamespace( + use_main_obmalloc=False, + allow_fork=False, + allow_exec=False, + allow_threads=True, + allow_daemon_threads=False, + check_multi_interp_extensions=True, + gil='own', + ), + 'legacy': types.SimpleNamespace( + use_main_obmalloc=True, + allow_fork=True, + allow_exec=True, + allow_threads=True, + allow_daemon_threads=True, + check_multi_interp_extensions=bool(Py_GIL_DISABLED), + gil='shared', + ), + 'empty': types.SimpleNamespace( + use_main_obmalloc=False, + allow_fork=False, + allow_exec=False, + allow_threads=False, + allow_daemon_threads=False, + check_multi_interp_extensions=False, + gil='default', + ), + } + gil_supported = ['default', 'shared', 'own'] + + for name, vanilla in supported.items(): + with self.subTest(f'supported ({name})'): + expected = vanilla + config1 = _interpreters.new_config(name) + self.assert_ns_equal(config1, expected) + self.assertIsNot(config1, expected) + + config2 = _interpreters.new_config(name) + self.assert_ns_equal(config2, config1) + self.assertIsNot(config2, config1) + + with self.subTest(f'noop override ({name})'): + expected = vanilla + overrides = vars(vanilla) + config = _interpreters.new_config(name, **overrides) + self.assert_ns_equal(config, expected) + + with self.subTest(f'override all ({name})'): + overrides = {k: not v for k, v in vars(vanilla).items()} + for gil in gil_supported: + if vanilla.gil == gil: + continue + overrides['gil'] = gil + expected = types.SimpleNamespace(**overrides) + config = _interpreters.new_config(name, **overrides) + self.assert_ns_equal(config, expected) + + # Override individual fields. + for field, old in vars(vanilla).items(): + if field == 'gil': + values = [v for v in gil_supported if v != old] + else: + values = [not old] + for val in values: + with self.subTest(f'{name}.{field} ({old!r} -> {val!r})'): + overrides = {field: val} + expected = types.SimpleNamespace( + **dict(vars(vanilla), **overrides), + ) + config = _interpreters.new_config(name, **overrides) + self.assert_ns_equal(config, expected) + + with self.subTest('extra override'): + with self.assertRaises(ValueError): + _interpreters.new_config(spam=True) + + # Bad values for bool fields. + for field, value in vars(supported['empty']).items(): + if field == 'gil': + continue + assert isinstance(value, bool) + for value in [1, '', 'spam', 1.0, None, object()]: + with self.subTest(f'bad override ({field}={value!r})'): + with self.assertRaises(TypeError): + _interpreters.new_config(**{field: value}) + + # Bad values for .gil. + for value in [True, 1, 1.0, None, object()]: + with self.subTest(f'bad override (gil={value!r})'): + with self.assertRaises(TypeError): + _interpreters.new_config(gil=value) + for value in ['', 'spam']: + with self.subTest(f'bad override (gil={value!r})'): + with self.assertRaises(ValueError): + _interpreters.new_config(gil=value) + + def test_get_main(self): + interpid, whence = _interpreters.get_main() + self.assertEqual(interpid, 0) + self.assertEqual(whence, _interpreters.WHENCE_RUNTIME) + self.assertEqual( + _interpreters.whence(interpid), + _interpreters.WHENCE_RUNTIME) + + def test_get_current(self): + with self.subTest('main'): + main, *_ = _interpreters.get_main() + interpid, whence = _interpreters.get_current() + self.assertEqual(interpid, main) + self.assertEqual(whence, _interpreters.WHENCE_RUNTIME) + + script = f""" + import _interpreters + interpid, whence = _interpreters.get_current() + print((interpid, whence)) + """ + def parse_stdout(text): + interpid, whence = eval(text) + return interpid, whence + + with self.subTest('from _interpreters'): + orig = _interpreters.create() + text = self.run_and_capture(orig, script) + interpid, whence = parse_stdout(text) + self.assertEqual(interpid, orig) + self.assertEqual(whence, _interpreters.WHENCE_STDLIB) + + with self.subTest('from C-API'): + last = 0 + for id, *_ in _interpreters.list_all(): + last = max(last, id) + expected = last + 1 + text = self.run_temp_from_capi(script) + interpid, whence = parse_stdout(text) + self.assertEqual(interpid, expected) + self.assertEqual(whence, _interpreters.WHENCE_CAPI) + + def test_list_all(self): + mainid, *_ = _interpreters.get_main() + interpid1 = _interpreters.create() + interpid2 = _interpreters.create() + interpid3 = _interpreters.create() + expected = [ + (mainid, _interpreters.WHENCE_RUNTIME), + (interpid1, _interpreters.WHENCE_STDLIB), + (interpid2, _interpreters.WHENCE_STDLIB), + (interpid3, _interpreters.WHENCE_STDLIB), + ] + + with self.subTest('main'): + res = _interpreters.list_all() + self.assertEqual(res, expected) + + with self.subTest('via interp from _interpreters'): + text = self.run_and_capture(interpid2, f""" + import _interpreters + print( + _interpreters.list_all()) + """) + + res = eval(text) + self.assertEqual(res, expected) + + with self.subTest('via interp from C-API'): + interpid4 = interpid3 + 1 + interpid5 = interpid4 + 1 + expected2 = expected + [ + (interpid4, _interpreters.WHENCE_CAPI), + (interpid5, _interpreters.WHENCE_STDLIB), + ] + expected3 = expected + [ + (interpid5, _interpreters.WHENCE_STDLIB), + ] + text = self.run_temp_from_capi(f""" + import _interpreters + _interpreters.create() + print( + _interpreters.list_all()) + """) + res2 = eval(text) + res3 = _interpreters.list_all() + self.assertEqual(res2, expected2) + self.assertEqual(res3, expected3) + + def test_create(self): + isolated = _interpreters.new_config('isolated') + legacy = _interpreters.new_config('legacy') + default = isolated + + with self.subTest('no args'): + interpid = _interpreters.create() + config = _interpreters.get_config(interpid) + self.assert_ns_equal(config, default) + + with self.subTest('config: None'): + interpid = _interpreters.create(None) + config = _interpreters.get_config(interpid) + self.assert_ns_equal(config, default) + + with self.subTest('config: \'empty\''): + with self.assertRaises(InterpreterError): + # The "empty" config isn't viable on its own. + _interpreters.create('empty') + + for arg, expected in { + '': default, + 'default': default, + 'isolated': isolated, + 'legacy': legacy, + }.items(): + with self.subTest(f'str arg: {arg!r}'): + interpid = _interpreters.create(arg) + config = _interpreters.get_config(interpid) + self.assert_ns_equal(config, expected) + + with self.subTest('custom'): + orig = _interpreters.new_config('empty') + orig.use_main_obmalloc = True + orig.check_multi_interp_extensions = bool(Py_GIL_DISABLED) + orig.gil = 'shared' + interpid = _interpreters.create(orig) + config = _interpreters.get_config(interpid) + self.assert_ns_equal(config, orig) + + with self.subTest('missing fields'): + orig = _interpreters.new_config() + del orig.gil + with self.assertRaises(ValueError): + _interpreters.create(orig) + + with self.subTest('extra fields'): + orig = _interpreters.new_config() + orig.spam = True + with self.assertRaises(ValueError): + _interpreters.create(orig) + + with self.subTest('whence'): + interpid = _interpreters.create() + self.assertEqual( + _interpreters.whence(interpid), + _interpreters.WHENCE_STDLIB) + + @requires_test_modules + def test_destroy(self): + with self.subTest('from _interpreters'): + interpid = _interpreters.create() + before = [id for id, *_ in _interpreters.list_all()] + _interpreters.destroy(interpid) + after = [id for id, *_ in _interpreters.list_all()] + + self.assertIn(interpid, before) + self.assertNotIn(interpid, after) + self.assertFalse( + self.interp_exists(interpid)) + + with self.subTest('main'): + interpid, *_ = _interpreters.get_main() + with self.assertRaises(InterpreterError): + # It is the current interpreter. + _interpreters.destroy(interpid) + + with self.subTest('from C-API'): + interpid = _testinternalcapi.create_interpreter() + with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + _interpreters.destroy(interpid, restrict=True) + self.assertTrue( + self.interp_exists(interpid)) + _interpreters.destroy(interpid) + self.assertFalse( + self.interp_exists(interpid)) + + with self.subTest('basic C-API'): + interpid = _testinternalcapi.create_interpreter() + self.assertTrue( + self.interp_exists(interpid)) + _testinternalcapi.destroy_interpreter(interpid, basic=True) + self.assertFalse( + self.interp_exists(interpid)) + + def test_get_config(self): + # This test overlaps with + # test.test_capi.test_misc.InterpreterConfigTests. + + with self.subTest('main'): + expected = _interpreters.new_config('legacy') + expected.gil = 'own' + if Py_GIL_DISABLED: + expected.check_multi_interp_extensions = False + interpid, *_ = _interpreters.get_main() + config = _interpreters.get_config(interpid) + self.assert_ns_equal(config, expected) + + with self.subTest('isolated'): + expected = _interpreters.new_config('isolated') + interpid = _interpreters.create('isolated') + config = _interpreters.get_config(interpid) + self.assert_ns_equal(config, expected) + + with self.subTest('legacy'): + expected = _interpreters.new_config('legacy') + interpid = _interpreters.create('legacy') + config = _interpreters.get_config(interpid) + self.assert_ns_equal(config, expected) + + with self.subTest('from C-API'): + orig = _interpreters.new_config('isolated') + with self.interpreter_from_capi(orig) as interpid: + with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + _interpreters.get_config(interpid, restrict=True) + config = _interpreters.get_config(interpid) + self.assert_ns_equal(config, orig) + + @requires_test_modules + def test_whence(self): + with self.subTest('main'): + interpid, *_ = _interpreters.get_main() + whence = _interpreters.whence(interpid) + self.assertEqual(whence, _interpreters.WHENCE_RUNTIME) + + with self.subTest('stdlib'): + interpid = _interpreters.create() + whence = _interpreters.whence(interpid) + self.assertEqual(whence, _interpreters.WHENCE_STDLIB) + + for orig, name in { + _interpreters.WHENCE_UNKNOWN: 'not ready', + _interpreters.WHENCE_LEGACY_CAPI: 'legacy C-API', + _interpreters.WHENCE_CAPI: 'C-API', + _interpreters.WHENCE_XI: 'cross-interpreter C-API', + }.items(): + with self.subTest(f'from C-API ({orig}: {name})'): + with self.interpreter_from_capi(whence=orig) as interpid: + whence = _interpreters.whence(interpid) + self.assertEqual(whence, orig) + + with self.subTest('from C-API, running'): + text = self.run_temp_from_capi(dedent(f""" + import _interpreters + interpid, *_ = _interpreters.get_current() + print(_interpreters.whence(interpid)) + """), + config=True) + whence = eval(text) + self.assertEqual(whence, _interpreters.WHENCE_CAPI) + + with self.subTest('from legacy C-API, running'): + ... + text = self.run_temp_from_capi(dedent(f""" + import _interpreters + interpid, *_ = _interpreters.get_current() + print(_interpreters.whence(interpid)) + """), + config=False) + whence = eval(text) + self.assertEqual(whence, _interpreters.WHENCE_LEGACY_CAPI) + + def test_contextvars_missing(self): + script = f""" + import contextvars + print(getattr(contextvars.Token, "MISSING", "'doesn't exist'")) + """ + + orig = _interpreters.create() + text = self.run_and_capture(orig, script) + self.assertEqual(text.strip(), "") + + def test_is_running(self): + def check(interpid, expected): + with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + _interpreters.is_running(interpid, restrict=True) + running = _interpreters.is_running(interpid) + self.assertIs(running, expected) + + with self.subTest('from _interpreters (running)'): + interpid = _interpreters.create() + with self.running(interpid): + running = _interpreters.is_running(interpid) + self.assertTrue(running) + + with self.subTest('from _interpreters (not running)'): + interpid = _interpreters.create() + running = _interpreters.is_running(interpid) + self.assertFalse(running) + + with self.subTest('main'): + interpid, *_ = _interpreters.get_main() + check(interpid, True) + + with self.subTest('from C-API (running __main__)'): + with self.interpreter_from_capi() as interpid: + with self.running_from_capi(interpid, main=True): + check(interpid, True) + + with self.subTest('from C-API (running, but not __main__)'): + with self.interpreter_from_capi() as interpid: + with self.running_from_capi(interpid, main=False): + check(interpid, False) + + with self.subTest('from C-API (not running)'): + with self.interpreter_from_capi() as interpid: + check(interpid, False) + + def test_exec(self): + with self.subTest('run script'): + interpid = _interpreters.create() + script, results = _captured_script('print("it worked!", end="")') + with results: + exc = _interpreters.exec(interpid, script) + results = results.final() + results.raise_if_failed() + out = results.stdout + self.assertEqual(out, 'it worked!') + + with self.subTest('uncaught exception'): + interpid = _interpreters.create() + script, results = _captured_script(""" + raise Exception('uh-oh!') + print("it worked!", end="") + """) + with results: + exc = _interpreters.exec(interpid, script) + out = results.stdout() + expected = build_excinfo( + Exception, 'uh-oh!', + # We check these in other tests. + formatted=exc.formatted, + errdisplay=exc.errdisplay, + ) + self.assertEqual(out, '') + self.assert_ns_equal(exc, expected) + + with self.subTest('from C-API'): + with self.interpreter_from_capi() as interpid: + with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + _interpreters.exec(interpid, 'raise Exception("it worked!")', + restrict=True) + exc = _interpreters.exec(interpid, 'raise Exception("it worked!")') + self.assertIsNot(exc, None) + self.assertEqual(exc.msg, 'it worked!') + + def test_call(self): + interpid = _interpreters.create() + + # Here we focus on basic args and return values. + # See TestInterpreterCall for full operational coverage, + # including supported callables. + + with self.subTest('no args, return None'): + func = defs.spam_minimal + res, exc = _interpreters.call(interpid, func) + self.assertIsNone(exc) + self.assertIsNone(res) + + with self.subTest('empty args, return None'): + func = defs.spam_minimal + res, exc = _interpreters.call(interpid, func, (), {}) + self.assertIsNone(exc) + self.assertIsNone(res) + + with self.subTest('no args, return non-None'): + func = defs.script_with_return + res, exc = _interpreters.call(interpid, func) + self.assertIsNone(exc) + self.assertIs(res, True) + + with self.subTest('full args, return non-None'): + expected = (1, 2, 3, 4, 5, 6, (7, 8), {'g': 9, 'h': 0}) + func = defs.spam_full_args + args = (1, 2, 3, 4, 7, 8) + kwargs = dict(e=5, f=6, g=9, h=0) + res, exc = _interpreters.call(interpid, func, args, kwargs) + self.assertIsNone(exc) + self.assertEqual(res, expected) + + with self.subTest('uncaught exception'): + func = defs.spam_raises + res, exc = _interpreters.call(interpid, func) + expected = build_excinfo( + Exception, 'spam!', + # We check these in other tests. + formatted=exc.formatted, + errdisplay=exc.errdisplay, + ) + self.assertIsNone(res) + self.assertEqual(exc, expected) + + @requires_test_modules + def test_set___main___attrs(self): + with self.subTest('from _interpreters'): + interpid = _interpreters.create() + before1 = _interpreters.exec(interpid, 'assert spam == \'eggs\'') + before2 = _interpreters.exec(interpid, 'assert ham == 42') + self.assertEqual(before1.type.__name__, 'NameError') + self.assertEqual(before2.type.__name__, 'NameError') + + _interpreters.set___main___attrs(interpid, dict( + spam='eggs', + ham=42, + )) + after1 = _interpreters.exec(interpid, 'assert spam == \'eggs\'') + after2 = _interpreters.exec(interpid, 'assert ham == 42') + after3 = _interpreters.exec(interpid, 'assert spam == 42') + self.assertIs(after1, None) + self.assertIs(after2, None) + self.assertEqual(after3.type.__name__, 'AssertionError') + + with self.assertRaises(ValueError): + # GH-127165: Embedded NULL characters broke the lookup + _interpreters.set___main___attrs(interpid, {"\x00": 1}) + + with self.subTest('from C-API'): + with self.interpreter_from_capi() as interpid: + with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + _interpreters.set___main___attrs(interpid, {'spam': True}, + restrict=True) + _interpreters.set___main___attrs(interpid, {'spam': True}) + rc = _testinternalcapi.exec_interpreter( + interpid, + 'assert spam is True', + ) + self.assertEqual(rc, 0) + + +if __name__ == '__main__': + # Test needs to be a package, so we can do relative imports. + unittest.main() diff --git a/Lib/test/test_interpreters/test_channels.py b/Lib/test/test_interpreters/test_channels.py new file mode 100644 index 00000000000..5437792b5a7 --- /dev/null +++ b/Lib/test/test_interpreters/test_channels.py @@ -0,0 +1,610 @@ +import importlib +import pickle +import threading +from textwrap import dedent +import unittest +import time + +from test.support import import_helper +# Raise SkipTest if subinterpreters not supported. +_channels = import_helper.import_module('_interpchannels') +from concurrent import interpreters +from test.support import channels +from .utils import _run_output, TestBase + + +class LowLevelTests(TestBase): + + # The behaviors in the low-level module is important in as much + # as it is exercised by the high-level module. Therefore the + # most # important testing happens in the high-level tests. + # These low-level tests cover corner cases that are not + # encountered by the high-level module, thus they + # mostly shouldn't matter as much. + + # Additional tests are found in Lib/test/test__interpchannels.py. + # XXX Those should be either moved to LowLevelTests or eliminated + # in favor of high-level tests in this file. + + def test_highlevel_reloaded(self): + # See gh-115490 (https://github.com/python/cpython/issues/115490). + importlib.reload(channels) + + +class TestChannels(TestBase): + + def test_create(self): + r, s = channels.create() + self.assertIsInstance(r, channels.RecvChannel) + self.assertIsInstance(s, channels.SendChannel) + + def test_list_all(self): + self.assertEqual(channels.list_all(), []) + created = set() + for _ in range(3): + ch = channels.create() + created.add(ch) + after = set(channels.list_all()) + self.assertEqual(after, created) + + def test_list_all_closed(self): + created = [channels.create() for _ in range(3)] + rch, sch = created.pop(1) + rch.close() + self.assertEqual(set(channels.list_all()), set(created)) + + def test_shareable(self): + interp = interpreters.create() + rch, sch = channels.create() + + self.assertTrue( + interpreters.is_shareable(rch)) + self.assertTrue( + interpreters.is_shareable(sch)) + + sch.send_nowait(rch) + sch.send_nowait(sch) + rch2 = rch.recv() + sch2 = rch.recv() + + interp.prepare_main(rch=rch, sch=sch) + sch.send_nowait(rch) + sch.send_nowait(sch) + interp.exec(dedent(""" + rch2 = rch.recv() + sch2 = rch.recv() + assert rch2 == rch + assert sch2 == sch + + sch.send_nowait(rch2) + sch.send_nowait(sch2) + """)) + rch3 = rch.recv() + sch3 = rch.recv() + + self.assertEqual(rch2, rch) + self.assertEqual(sch2, sch) + self.assertEqual(rch3, rch) + self.assertEqual(sch3, sch) + + def test_is_closed(self): + rch, sch = channels.create() + rbefore = rch.is_closed + sbefore = sch.is_closed + rch.close() + rafter = rch.is_closed + safter = sch.is_closed + + self.assertFalse(rbefore) + self.assertFalse(sbefore) + self.assertTrue(rafter) + self.assertTrue(safter) + + +class TestRecvChannelAttrs(TestBase): + + def test_id_type(self): + rch, _ = channels.create() + self.assertIsInstance(rch.id, _channels.ChannelID) + + def test_custom_id(self): + rch = channels.RecvChannel(1) + self.assertEqual(rch.id, 1) + + with self.assertRaises(TypeError): + channels.RecvChannel('1') + + def test_id_readonly(self): + rch = channels.RecvChannel(1) + with self.assertRaises(AttributeError): + rch.id = 2 + + def test_equality(self): + ch1, _ = channels.create() + ch2, _ = channels.create() + self.assertEqual(ch1, ch1) + self.assertNotEqual(ch1, ch2) + + def test_pickle(self): + ch, _ = channels.create() + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(protocol=protocol): + data = pickle.dumps(ch, protocol) + unpickled = pickle.loads(data) + self.assertEqual(unpickled, ch) + + +class TestSendChannelAttrs(TestBase): + + def test_id_type(self): + _, sch = channels.create() + self.assertIsInstance(sch.id, _channels.ChannelID) + + def test_custom_id(self): + sch = channels.SendChannel(1) + self.assertEqual(sch.id, 1) + + with self.assertRaises(TypeError): + channels.SendChannel('1') + + def test_id_readonly(self): + sch = channels.SendChannel(1) + with self.assertRaises(AttributeError): + sch.id = 2 + + def test_equality(self): + _, ch1 = channels.create() + _, ch2 = channels.create() + self.assertEqual(ch1, ch1) + self.assertNotEqual(ch1, ch2) + + def test_pickle(self): + _, ch = channels.create() + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(protocol=protocol): + data = pickle.dumps(ch, protocol) + unpickled = pickle.loads(data) + self.assertEqual(unpickled, ch) + + +class TestSendRecv(TestBase): + + def test_send_recv_main(self): + r, s = channels.create() + orig = b'spam' + s.send_nowait(orig) + obj = r.recv() + + self.assertEqual(obj, orig) + self.assertIsNot(obj, orig) + + def test_send_recv_same_interpreter(self): + interp = interpreters.create() + interp.exec(dedent(""" + from test.support import channels + r, s = channels.create() + orig = b'spam' + s.send_nowait(orig) + obj = r.recv() + assert obj == orig, 'expected: obj == orig' + assert obj is not orig, 'expected: obj is not orig' + """)) + + @unittest.skip('broken (see BPO-...)') + def test_send_recv_different_interpreters(self): + r1, s1 = channels.create() + r2, s2 = channels.create() + orig1 = b'spam' + s1.send_nowait(orig1) + out = _run_output( + interpreters.create(), + dedent(f""" + obj1 = r.recv() + assert obj1 == b'spam', 'expected: obj1 == orig1' + # When going to another interpreter we get a copy. + assert id(obj1) != {id(orig1)}, 'expected: obj1 is not orig1' + orig2 = b'eggs' + print(id(orig2)) + s.send_nowait(orig2) + """), + channels=dict(r=r1, s=s2), + ) + obj2 = r2.recv() + + self.assertEqual(obj2, b'eggs') + self.assertNotEqual(id(obj2), int(out)) + + def test_send_recv_different_threads(self): + r, s = channels.create() + + def f(): + while True: + try: + obj = r.recv() + break + except channels.ChannelEmptyError: + time.sleep(0.1) + s.send(obj) + t = threading.Thread(target=f) + t.start() + + orig = b'spam' + s.send(orig) + obj = r.recv() + t.join() + + self.assertEqual(obj, orig) + self.assertIsNot(obj, orig) + + def test_send_recv_nowait_main(self): + r, s = channels.create() + orig = b'spam' + s.send_nowait(orig) + obj = r.recv_nowait() + + self.assertEqual(obj, orig) + self.assertIsNot(obj, orig) + + def test_send_recv_nowait_main_with_default(self): + r, _ = channels.create() + obj = r.recv_nowait(None) + + self.assertIsNone(obj) + + def test_send_recv_nowait_same_interpreter(self): + interp = interpreters.create() + interp.exec(dedent(""" + from test.support import channels + r, s = channels.create() + orig = b'spam' + s.send_nowait(orig) + obj = r.recv_nowait() + assert obj == orig, 'expected: obj == orig' + # When going back to the same interpreter we get the same object. + assert obj is not orig, 'expected: obj is not orig' + """)) + + @unittest.skip('broken (see BPO-...)') + def test_send_recv_nowait_different_interpreters(self): + r1, s1 = channels.create() + r2, s2 = channels.create() + orig1 = b'spam' + s1.send_nowait(orig1) + out = _run_output( + interpreters.create(), + dedent(f""" + obj1 = r.recv_nowait() + assert obj1 == b'spam', 'expected: obj1 == orig1' + # When going to another interpreter we get a copy. + assert id(obj1) != {id(orig1)}, 'expected: obj1 is not orig1' + orig2 = b'eggs' + print(id(orig2)) + s.send_nowait(orig2) + """), + channels=dict(r=r1, s=s2), + ) + obj2 = r2.recv_nowait() + + self.assertEqual(obj2, b'eggs') + self.assertNotEqual(id(obj2), int(out)) + + def test_recv_timeout(self): + r, _ = channels.create() + with self.assertRaises(TimeoutError): + r.recv(timeout=1) + + def test_recv_channel_does_not_exist(self): + ch = channels.RecvChannel(1_000_000) + with self.assertRaises(channels.ChannelNotFoundError): + ch.recv() + + def test_send_channel_does_not_exist(self): + ch = channels.SendChannel(1_000_000) + with self.assertRaises(channels.ChannelNotFoundError): + ch.send(b'spam') + + def test_recv_nowait_channel_does_not_exist(self): + ch = channels.RecvChannel(1_000_000) + with self.assertRaises(channels.ChannelNotFoundError): + ch.recv_nowait() + + def test_send_nowait_channel_does_not_exist(self): + ch = channels.SendChannel(1_000_000) + with self.assertRaises(channels.ChannelNotFoundError): + ch.send_nowait(b'spam') + + def test_recv_nowait_empty(self): + ch, _ = channels.create() + with self.assertRaises(channels.ChannelEmptyError): + ch.recv_nowait() + + def test_recv_nowait_default(self): + default = object() + rch, sch = channels.create() + obj1 = rch.recv_nowait(default) + sch.send_nowait(None) + sch.send_nowait(1) + sch.send_nowait(b'spam') + sch.send_nowait(b'eggs') + obj2 = rch.recv_nowait(default) + obj3 = rch.recv_nowait(default) + obj4 = rch.recv_nowait() + obj5 = rch.recv_nowait(default) + obj6 = rch.recv_nowait(default) + + self.assertIs(obj1, default) + self.assertIs(obj2, None) + self.assertEqual(obj3, 1) + self.assertEqual(obj4, b'spam') + self.assertEqual(obj5, b'eggs') + self.assertIs(obj6, default) + + def test_send_buffer(self): + buf = bytearray(b'spamspamspam') + obj = None + rch, sch = channels.create() + + def f(): + nonlocal obj + while True: + try: + obj = rch.recv() + break + except channels.ChannelEmptyError: + time.sleep(0.1) + t = threading.Thread(target=f) + t.start() + + sch.send_buffer(buf) + t.join() + + self.assertIsNot(obj, buf) + self.assertIsInstance(obj, memoryview) + self.assertEqual(obj, buf) + + buf[4:8] = b'eggs' + self.assertEqual(obj, buf) + obj[4:8] = b'ham.' + self.assertEqual(obj, buf) + + def test_send_buffer_nowait(self): + buf = bytearray(b'spamspamspam') + rch, sch = channels.create() + sch.send_buffer_nowait(buf) + obj = rch.recv() + + self.assertIsNot(obj, buf) + self.assertIsInstance(obj, memoryview) + self.assertEqual(obj, buf) + + buf[4:8] = b'eggs' + self.assertEqual(obj, buf) + obj[4:8] = b'ham.' + self.assertEqual(obj, buf) + + def test_send_cleared_with_subinterpreter(self): + def common(rch, sch, unbound=None, presize=0): + if not unbound: + extraargs = '' + elif unbound is channels.UNBOUND: + extraargs = ', unbounditems=channels.UNBOUND' + elif unbound is channels.UNBOUND_ERROR: + extraargs = ', unbounditems=channels.UNBOUND_ERROR' + elif unbound is channels.UNBOUND_REMOVE: + extraargs = ', unbounditems=channels.UNBOUND_REMOVE' + else: + raise NotImplementedError(repr(unbound)) + interp = interpreters.create() + + _run_output(interp, dedent(f""" + from test.support import channels + sch = channels.SendChannel({sch.id}) + obj1 = b'spam' + obj2 = b'eggs' + sch.send_nowait(obj1{extraargs}) + sch.send_nowait(obj2{extraargs}) + """)) + self.assertEqual( + _channels.get_count(rch.id), + presize + 2, + ) + + if presize == 0: + obj1 = rch.recv() + self.assertEqual(obj1, b'spam') + self.assertEqual( + _channels.get_count(rch.id), + presize + 1, + ) + + return interp + + with self.subTest('default'): # UNBOUND + rch, sch = channels.create() + interp = common(rch, sch) + del interp + self.assertEqual(_channels.get_count(rch.id), 1) + obj1 = rch.recv() + self.assertEqual(_channels.get_count(rch.id), 0) + self.assertIs(obj1, channels.UNBOUND) + self.assertEqual(_channels.get_count(rch.id), 0) + with self.assertRaises(channels.ChannelEmptyError): + rch.recv_nowait() + + with self.subTest('UNBOUND'): + rch, sch = channels.create() + interp = common(rch, sch, channels.UNBOUND) + del interp + self.assertEqual(_channels.get_count(rch.id), 1) + obj1 = rch.recv() + self.assertIs(obj1, channels.UNBOUND) + self.assertEqual(_channels.get_count(rch.id), 0) + with self.assertRaises(channels.ChannelEmptyError): + rch.recv_nowait() + + with self.subTest('UNBOUND_ERROR'): + rch, sch = channels.create() + interp = common(rch, sch, channels.UNBOUND_ERROR) + + del interp + self.assertEqual(_channels.get_count(rch.id), 1) + with self.assertRaises(channels.ItemInterpreterDestroyed): + rch.recv() + + self.assertEqual(_channels.get_count(rch.id), 0) + with self.assertRaises(channels.ChannelEmptyError): + rch.recv_nowait() + + with self.subTest('UNBOUND_REMOVE'): + rch, sch = channels.create() + + interp = common(rch, sch, channels.UNBOUND_REMOVE) + del interp + self.assertEqual(_channels.get_count(rch.id), 0) + with self.assertRaises(channels.ChannelEmptyError): + rch.recv_nowait() + + sch.send_nowait(b'ham', unbounditems=channels.UNBOUND_REMOVE) + self.assertEqual(_channels.get_count(rch.id), 1) + interp = common(rch, sch, channels.UNBOUND_REMOVE, 1) + self.assertEqual(_channels.get_count(rch.id), 3) + sch.send_nowait(42, unbounditems=channels.UNBOUND_REMOVE) + self.assertEqual(_channels.get_count(rch.id), 4) + del interp + self.assertEqual(_channels.get_count(rch.id), 2) + obj1 = rch.recv() + obj2 = rch.recv() + self.assertEqual(obj1, b'ham') + self.assertEqual(obj2, 42) + self.assertEqual(_channels.get_count(rch.id), 0) + with self.assertRaises(channels.ChannelEmptyError): + rch.recv_nowait() + + def test_send_cleared_with_subinterpreter_mixed(self): + rch, sch = channels.create() + interp = interpreters.create() + + # If we don't associate the main interpreter with the channel + # then the channel will be automatically closed when interp + # is destroyed. + sch.send_nowait(None) + rch.recv() + self.assertEqual(_channels.get_count(rch.id), 0) + + _run_output(interp, dedent(f""" + from test.support import channels + sch = channels.SendChannel({sch.id}) + sch.send_nowait(1, unbounditems=channels.UNBOUND) + sch.send_nowait(2, unbounditems=channels.UNBOUND_ERROR) + sch.send_nowait(3) + sch.send_nowait(4, unbounditems=channels.UNBOUND_REMOVE) + sch.send_nowait(5, unbounditems=channels.UNBOUND) + """)) + self.assertEqual(_channels.get_count(rch.id), 5) + + del interp + self.assertEqual(_channels.get_count(rch.id), 4) + + obj1 = rch.recv() + self.assertIs(obj1, channels.UNBOUND) + self.assertEqual(_channels.get_count(rch.id), 3) + + with self.assertRaises(channels.ItemInterpreterDestroyed): + rch.recv() + self.assertEqual(_channels.get_count(rch.id), 2) + + obj2 = rch.recv() + self.assertIs(obj2, channels.UNBOUND) + self.assertEqual(_channels.get_count(rch.id), 1) + + obj3 = rch.recv() + self.assertIs(obj3, channels.UNBOUND) + self.assertEqual(_channels.get_count(rch.id), 0) + + def test_send_cleared_with_subinterpreter_multiple(self): + rch, sch = channels.create() + interp1 = interpreters.create() + interp2 = interpreters.create() + + sch.send_nowait(1) + _run_output(interp1, dedent(f""" + from test.support import channels + rch = channels.RecvChannel({rch.id}) + sch = channels.SendChannel({sch.id}) + obj1 = rch.recv() + sch.send_nowait(2, unbounditems=channels.UNBOUND) + sch.send_nowait(obj1, unbounditems=channels.UNBOUND_REMOVE) + """)) + _run_output(interp2, dedent(f""" + from test.support import channels + rch = channels.RecvChannel({rch.id}) + sch = channels.SendChannel({sch.id}) + obj2 = rch.recv() + obj1 = rch.recv() + """)) + self.assertEqual(_channels.get_count(rch.id), 0) + sch.send_nowait(3) + _run_output(interp1, dedent(""" + sch.send_nowait(4, unbounditems=channels.UNBOUND) + # interp closed here + sch.send_nowait(5, unbounditems=channels.UNBOUND_REMOVE) + sch.send_nowait(6, unbounditems=channels.UNBOUND) + """)) + _run_output(interp2, dedent(""" + sch.send_nowait(7, unbounditems=channels.UNBOUND_ERROR) + # interp closed here + sch.send_nowait(obj1, unbounditems=channels.UNBOUND_ERROR) + sch.send_nowait(obj2, unbounditems=channels.UNBOUND_REMOVE) + sch.send_nowait(8, unbounditems=channels.UNBOUND) + """)) + _run_output(interp1, dedent(""" + sch.send_nowait(9, unbounditems=channels.UNBOUND_REMOVE) + sch.send_nowait(10, unbounditems=channels.UNBOUND) + """)) + self.assertEqual(_channels.get_count(rch.id), 10) + + obj3 = rch.recv() + self.assertEqual(obj3, 3) + self.assertEqual(_channels.get_count(rch.id), 9) + + obj4 = rch.recv() + self.assertEqual(obj4, 4) + self.assertEqual(_channels.get_count(rch.id), 8) + + del interp1 + self.assertEqual(_channels.get_count(rch.id), 6) + + # obj5 was removed + + obj6 = rch.recv() + self.assertIs(obj6, channels.UNBOUND) + self.assertEqual(_channels.get_count(rch.id), 5) + + obj7 = rch.recv() + self.assertEqual(obj7, 7) + self.assertEqual(_channels.get_count(rch.id), 4) + + del interp2 + self.assertEqual(_channels.get_count(rch.id), 3) + + # obj1 + with self.assertRaises(channels.ItemInterpreterDestroyed): + rch.recv() + self.assertEqual(_channels.get_count(rch.id), 2) + + # obj2 was removed + + obj8 = rch.recv() + self.assertIs(obj8, channels.UNBOUND) + self.assertEqual(_channels.get_count(rch.id), 1) + + # obj9 was removed + + obj10 = rch.recv() + self.assertIs(obj10, channels.UNBOUND) + self.assertEqual(_channels.get_count(rch.id), 0) + + +if __name__ == '__main__': + # Test needs to be a package, so we can do relative imports. + unittest.main() diff --git a/Lib/test/test_interpreters/test_lifecycle.py b/Lib/test/test_interpreters/test_lifecycle.py new file mode 100644 index 00000000000..39c1441b67c --- /dev/null +++ b/Lib/test/test_interpreters/test_lifecycle.py @@ -0,0 +1,194 @@ +import contextlib +import json +import os +import os.path +import sys +from textwrap import dedent +import unittest + +from test import support +from test.support import import_helper +from test.support import os_helper +# Raise SkipTest if subinterpreters not supported. +import_helper.import_module('_interpreters') +from .utils import TestBase + + +class StartupTests(TestBase): + + # We want to ensure the initial state of subinterpreters + # matches expectations. + + _subtest_count = 0 + + @contextlib.contextmanager + def subTest(self, *args): + with super().subTest(*args) as ctx: + self._subtest_count += 1 + try: + yield ctx + finally: + if self._debugged_in_subtest: + if self._subtest_count == 1: + # The first subtest adds a leading newline, so we + # compensate here by not printing a trailing newline. + print('### end subtest debug ###', end='') + else: + print('### end subtest debug ###') + self._debugged_in_subtest = False + + def debug(self, msg, *, header=None): + if header: + self._debug(f'--- {header} ---') + if msg: + if msg.endswith(os.linesep): + self._debug(msg[:-len(os.linesep)]) + else: + self._debug(msg) + self._debug('') + self._debug('------') + else: + self._debug(msg) + + _debugged = False + _debugged_in_subtest = False + def _debug(self, msg): + if not self._debugged: + print() + self._debugged = True + if self._subtest is not None: + if True: + if not self._debugged_in_subtest: + self._debugged_in_subtest = True + print('### start subtest debug ###') + print(msg) + else: + print(msg) + + def create_temp_dir(self): + import tempfile + tmp = tempfile.mkdtemp(prefix='test_interpreters_') + tmp = os.path.realpath(tmp) + self.addCleanup(os_helper.rmtree, tmp) + return tmp + + def write_script(self, *path, text): + filename = os.path.join(*path) + dirname = os.path.dirname(filename) + if dirname: + os.makedirs(dirname, exist_ok=True) + with open(filename, 'w', encoding='utf-8') as outfile: + outfile.write(dedent(text)) + return filename + + @support.requires_subprocess() + def run_python(self, argv, *, cwd=None): + # This method is inspired by + # EmbeddingTestsMixin.run_embedded_interpreter() in test_embed.py. + import shlex + import subprocess + if isinstance(argv, str): + argv = shlex.split(argv) + argv = [sys.executable, *argv] + try: + proc = subprocess.run( + argv, + cwd=cwd, + capture_output=True, + text=True, + ) + except Exception as exc: + self.debug(f'# cmd: {shlex.join(argv)}') + if isinstance(exc, FileNotFoundError) and not exc.filename: + if os.path.exists(argv[0]): + exists = 'exists' + else: + exists = 'does not exist' + self.debug(f'{argv[0]} {exists}') + raise # re-raise + assert proc.stderr == '' or proc.returncode != 0, proc.stderr + if proc.returncode != 0 and support.verbose: + self.debug(f'# python3 {shlex.join(argv[1:])} failed:') + self.debug(proc.stdout, header='stdout') + self.debug(proc.stderr, header='stderr') + self.assertEqual(proc.returncode, 0) + self.assertEqual(proc.stderr, '') + return proc.stdout + + def test_sys_path_0(self): + # The main interpreter's sys.path[0] should be used by subinterpreters. + script = ''' + import sys + from concurrent import interpreters + + orig = sys.path[0] + + interp = interpreters.create() + interp.exec(f"""if True: + import json + import sys + print(json.dumps({{ + 'main': {orig!r}, + 'sub': sys.path[0], + }}, indent=4), flush=True) + """) + interp.close() + ''' + # / + # pkg/ + # __init__.py + # __main__.py + # script.py + # script.py + cwd = self.create_temp_dir() + self.write_script(cwd, 'pkg', '__init__.py', text='') + self.write_script(cwd, 'pkg', '__main__.py', text=script) + self.write_script(cwd, 'pkg', 'script.py', text=script) + self.write_script(cwd, 'script.py', text=script) + + cases = [ + ('script.py', cwd), + ('-m script', cwd), + ('-m pkg', cwd), + ('-m pkg.script', cwd), + ('-c "import script"', ''), + ] + for argv, expected in cases: + with self.subTest(f'python3 {argv}'): + out = self.run_python(argv, cwd=cwd) + data = json.loads(out) + sp0_main, sp0_sub = data['main'], data['sub'] + self.assertEqual(sp0_sub, sp0_main) + self.assertEqual(sp0_sub, expected) + # XXX Also check them all with the -P cmdline flag? + + +class FinalizationTests(TestBase): + + @support.requires_subprocess() + def test_gh_109793(self): + # Make sure finalization finishes and the correct error code + # is reported, even when subinterpreters get cleaned up at the end. + import subprocess + argv = [sys.executable, '-c', '''if True: + from concurrent import interpreters + interp = interpreters.create() + try: + raise Exception + finally: + interp.close() + '''] + proc = subprocess.run(argv, capture_output=True, text=True) + self.assertIn('Traceback', proc.stderr) + if proc.returncode == 0 and support.verbose: + print() + print("--- cmd unexpected succeeded ---") + print(f"stdout:\n{proc.stdout}") + print(f"stderr:\n{proc.stderr}") + print("------") + self.assertEqual(proc.returncode, 1) + + +if __name__ == '__main__': + # Test needs to be a package, so we can do relative imports. + unittest.main() diff --git a/Lib/test/test_interpreters/test_queues.py b/Lib/test/test_interpreters/test_queues.py new file mode 100644 index 00000000000..77334aea383 --- /dev/null +++ b/Lib/test/test_interpreters/test_queues.py @@ -0,0 +1,667 @@ +import importlib +import pickle +import threading +from textwrap import dedent +import unittest + +from test.support import import_helper, Py_DEBUG +# Raise SkipTest if subinterpreters not supported. +_queues = import_helper.import_module('_interpqueues') +from concurrent import interpreters +from concurrent.interpreters import _queues as queues, _crossinterp +from .utils import _run_output, TestBase as _TestBase + +HUGE_TIMEOUT = 3600 +REPLACE = _crossinterp._UNBOUND_CONSTANT_TO_FLAG[_crossinterp.UNBOUND] + + +def get_num_queues(): + return len(_queues.list_all()) + + +class TestBase(_TestBase): + def tearDown(self): + for qid, _, _ in _queues.list_all(): + try: + _queues.destroy(qid) + except Exception: + pass + + +class LowLevelTests(TestBase): + + # The behaviors in the low-level module are important in as much + # as they are exercised by the high-level module. Therefore the + # most important testing happens in the high-level tests. + # These low-level tests cover corner cases that are not + # encountered by the high-level module, thus they + # mostly shouldn't matter as much. + + def test_highlevel_reloaded(self): + # See gh-115490 (https://github.com/python/cpython/issues/115490). + importlib.reload(queues) + + def test_create_destroy(self): + qid = _queues.create(2, REPLACE, -1) + _queues.destroy(qid) + self.assertEqual(get_num_queues(), 0) + with self.assertRaises(queues.QueueNotFoundError): + _queues.get(qid) + with self.assertRaises(queues.QueueNotFoundError): + _queues.destroy(qid) + + def test_not_destroyed(self): + # It should have cleaned up any remaining queues. + stdout, stderr = self.assert_python_ok( + '-c', + dedent(f""" + import {_queues.__name__} as _queues + _queues.create(2, {REPLACE}, -1) + """), + ) + self.assertEqual(stdout, '') + if Py_DEBUG: + self.assertNotEqual(stderr, '') + else: + self.assertEqual(stderr, '') + + def test_bind_release(self): + with self.subTest('typical'): + qid = _queues.create(2, REPLACE, -1) + _queues.bind(qid) + _queues.release(qid) + self.assertEqual(get_num_queues(), 0) + + with self.subTest('bind too much'): + qid = _queues.create(2, REPLACE, -1) + _queues.bind(qid) + _queues.bind(qid) + _queues.release(qid) + _queues.destroy(qid) + self.assertEqual(get_num_queues(), 0) + + with self.subTest('nested'): + qid = _queues.create(2, REPLACE, -1) + _queues.bind(qid) + _queues.bind(qid) + _queues.release(qid) + _queues.release(qid) + self.assertEqual(get_num_queues(), 0) + + with self.subTest('release without binding'): + qid = _queues.create(2, REPLACE, -1) + with self.assertRaises(queues.QueueError): + _queues.release(qid) + + +class QueueTests(TestBase): + + def test_create(self): + with self.subTest('vanilla'): + queue = queues.create() + self.assertEqual(queue.maxsize, 0) + + with self.subTest('small maxsize'): + queue = queues.create(3) + self.assertEqual(queue.maxsize, 3) + + with self.subTest('big maxsize'): + queue = queues.create(100) + self.assertEqual(queue.maxsize, 100) + + with self.subTest('no maxsize'): + queue = queues.create(0) + self.assertEqual(queue.maxsize, 0) + + with self.subTest('negative maxsize'): + queue = queues.create(-10) + self.assertEqual(queue.maxsize, -10) + + with self.subTest('bad maxsize'): + with self.assertRaises(TypeError): + queues.create('1') + + def test_shareable(self): + queue1 = queues.create() + + interp = interpreters.create() + interp.exec(dedent(f""" + from concurrent.interpreters import _queues as queues + queue1 = queues.Queue({queue1.id}) + """)); + + with self.subTest('same interpreter'): + queue2 = queues.create() + queue1.put(queue2) + queue3 = queue1.get() + self.assertIs(queue3, queue2) + + with self.subTest('from current interpreter'): + queue4 = queues.create() + queue1.put(queue4) + out = _run_output(interp, dedent(""" + queue4 = queue1.get() + print(queue4.id) + """)) + qid = int(out) + self.assertEqual(qid, queue4.id) + + with self.subTest('from subinterpreter'): + out = _run_output(interp, dedent(""" + queue5 = queues.create() + queue1.put(queue5) + print(queue5.id) + """)) + qid = int(out) + queue5 = queue1.get() + self.assertEqual(queue5.id, qid) + + def test_id_type(self): + queue = queues.create() + self.assertIsInstance(queue.id, int) + + def test_custom_id(self): + with self.assertRaises(queues.QueueNotFoundError): + queues.Queue(1_000_000) + + def test_id_readonly(self): + queue = queues.create() + with self.assertRaises(AttributeError): + queue.id = 1_000_000 + + def test_maxsize_readonly(self): + queue = queues.create(10) + with self.assertRaises(AttributeError): + queue.maxsize = 1_000_000 + + def test_hashable(self): + queue = queues.create() + expected = hash(queue.id) + actual = hash(queue) + self.assertEqual(actual, expected) + + def test_equality(self): + queue1 = queues.create() + queue2 = queues.create() + self.assertEqual(queue1, queue1) + self.assertNotEqual(queue1, queue2) + + def test_pickle(self): + queue = queues.create() + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(protocol=protocol): + data = pickle.dumps(queue, protocol) + unpickled = pickle.loads(data) + self.assertEqual(unpickled, queue) + + +class TestQueueOps(TestBase): + + def test_empty(self): + queue = queues.create() + before = queue.empty() + queue.put(None) + during = queue.empty() + queue.get() + after = queue.empty() + + self.assertIs(before, True) + self.assertIs(during, False) + self.assertIs(after, True) + + def test_full(self): + for maxsize in [1, 3, 11]: + with self.subTest(f'maxsize={maxsize}'): + num_to_add = maxsize + expected = [False] * (num_to_add * 2 + 3) + expected[maxsize] = True + expected[maxsize + 1] = True + + queue = queues.create(maxsize) + actual = [] + empty = [queue.empty()] + + for _ in range(num_to_add): + actual.append(queue.full()) + queue.put_nowait(None) + actual.append(queue.full()) + with self.assertRaises(queues.QueueFull): + queue.put_nowait(None) + empty.append(queue.empty()) + + for _ in range(num_to_add): + actual.append(queue.full()) + queue.get_nowait() + actual.append(queue.full()) + with self.assertRaises(queues.QueueEmpty): + queue.get_nowait() + actual.append(queue.full()) + empty.append(queue.empty()) + + self.assertEqual(actual, expected) + self.assertEqual(empty, [True, False, True]) + + # no max size + for args in [(), (0,), (-1,), (-10,)]: + with self.subTest(f'maxsize={args[0]}' if args else ''): + num_to_add = 13 + expected = [False] * (num_to_add * 2 + 3) + + queue = queues.create(*args) + actual = [] + empty = [queue.empty()] + + for _ in range(num_to_add): + actual.append(queue.full()) + queue.put_nowait(None) + actual.append(queue.full()) + empty.append(queue.empty()) + + for _ in range(num_to_add): + actual.append(queue.full()) + queue.get_nowait() + actual.append(queue.full()) + with self.assertRaises(queues.QueueEmpty): + queue.get_nowait() + actual.append(queue.full()) + empty.append(queue.empty()) + + self.assertEqual(actual, expected) + self.assertEqual(empty, [True, False, True]) + + def test_qsize(self): + expected = [0, 1, 2, 3, 2, 3, 2, 1, 0, 1, 0] + actual = [] + queue = queues.create() + for _ in range(3): + actual.append(queue.qsize()) + queue.put(None) + actual.append(queue.qsize()) + queue.get() + actual.append(queue.qsize()) + queue.put(None) + actual.append(queue.qsize()) + for _ in range(3): + queue.get() + actual.append(queue.qsize()) + queue.put(None) + actual.append(queue.qsize()) + queue.get() + actual.append(queue.qsize()) + + self.assertEqual(actual, expected) + + def test_put_get_main(self): + expected = list(range(20)) + queue = queues.create() + for i in range(20): + queue.put(i) + actual = [queue.get() for _ in range(20)] + + self.assertEqual(actual, expected) + + def test_put_timeout(self): + queue = queues.create(2) + queue.put(None) + queue.put(None) + with self.assertRaises(queues.QueueFull): + queue.put(None, timeout=0.1) + with self.assertRaises(queues.QueueFull): + queue.put(None, HUGE_TIMEOUT, 0.1) + queue.get() + queue.put(None) + + def test_put_nowait(self): + queue = queues.create(2) + queue.put_nowait(None) + queue.put_nowait(None) + with self.assertRaises(queues.QueueFull): + queue.put_nowait(None) + with self.assertRaises(queues.QueueFull): + queue.put(None, False) + with self.assertRaises(queues.QueueFull): + queue.put(None, False, timeout=HUGE_TIMEOUT) + queue.get() + queue.put_nowait(None) + + def test_put_full_fallback(self): + for obj in [ + None, + True, + 10, + 'spam', + b'spam', + (0, 'a'), + # not shareable + [1, 2, 3], + {'a': 13, 'b': 17}, + ]: + with self.subTest(repr(obj)): + queue = queues.create() + + queue.put(obj) + obj2 = queue.get() + self.assertEqual(obj2, obj) + + queue.put(obj) + obj2 = queue.get_nowait() + self.assertEqual(obj2, obj) + + def test_get_timeout(self): + queue = queues.create() + with self.assertRaises(queues.QueueEmpty): + queue.get(timeout=0.1) + with self.assertRaises(queues.QueueEmpty): + queue.get(HUGE_TIMEOUT, 0.1) + + def test_get_nowait(self): + queue = queues.create() + with self.assertRaises(queues.QueueEmpty): + queue.get_nowait() + with self.assertRaises(queues.QueueEmpty): + queue.get(False) + with self.assertRaises(queues.QueueEmpty): + queue.get(False, timeout=HUGE_TIMEOUT) + + def test_put_get_full_fallback(self): + expected = list(range(20)) + queue = queues.create() + for methname in ('get', 'get_nowait'): + with self.subTest(f'{methname}()'): + get = getattr(queue, methname) + + for i in range(20): + queue.put(i) + actual = [get() for _ in range(20)] + self.assertEqual(actual, expected) + + obj = [1, 2, 3] # lists are not shareable + queue.put(obj) + obj2 = get() + self.assertEqual(obj, obj2) + self.assertIsNot(obj, obj2) + + def test_put_get_same_interpreter(self): + interp = interpreters.create() + interp.exec(dedent(""" + from concurrent.interpreters import _queues as queues + queue = queues.create() + """)) + for methname in ('get', 'get_nowait'): + with self.subTest(f'{methname}()'): + interp.exec(dedent(f""" + orig = b'spam' + queue.put(orig) + obj = queue.{methname}() + assert obj == orig, 'expected: obj == orig' + assert obj is not orig, 'expected: obj is not orig' + """)) + + def test_put_get_different_interpreters(self): + interp = interpreters.create() + queue1 = queues.create() + queue2 = queues.create() + self.assertEqual(len(queues.list_all()), 2) + + for methname in ('get', 'get_nowait'): + with self.subTest(f'{methname}()'): + obj1 = b'spam' + queue1.put(obj1) + + out = _run_output( + interp, + dedent(f""" + from concurrent.interpreters import _queues as queues + queue1 = queues.Queue({queue1.id}) + queue2 = queues.Queue({queue2.id}) + assert queue1.qsize() == 1, 'expected: queue1.qsize() == 1' + obj = queue1.{methname}() + assert queue1.qsize() == 0, 'expected: queue1.qsize() == 0' + assert obj == b'spam', 'expected: obj == obj1' + # When going to another interpreter we get a copy. + assert id(obj) != {id(obj1)}, 'expected: obj is not obj1' + obj2 = b'eggs' + print(id(obj2)) + assert queue2.qsize() == 0, 'expected: queue2.qsize() == 0' + queue2.put(obj2) + assert queue2.qsize() == 1, 'expected: queue2.qsize() == 1' + """)) + self.assertEqual(len(queues.list_all()), 2) + self.assertEqual(queue1.qsize(), 0) + self.assertEqual(queue2.qsize(), 1) + + get = getattr(queue2, methname) + obj2 = get() + self.assertEqual(obj2, b'eggs') + self.assertNotEqual(id(obj2), int(out)) + + def test_put_cleared_with_subinterpreter(self): + def common(queue, unbound=None, presize=0): + if not unbound: + extraargs = '' + elif unbound is queues.UNBOUND: + extraargs = ', unbounditems=queues.UNBOUND' + elif unbound is queues.UNBOUND_ERROR: + extraargs = ', unbounditems=queues.UNBOUND_ERROR' + elif unbound is queues.UNBOUND_REMOVE: + extraargs = ', unbounditems=queues.UNBOUND_REMOVE' + else: + raise NotImplementedError(repr(unbound)) + interp = interpreters.create() + + _run_output(interp, dedent(f""" + from concurrent.interpreters import _queues as queues + queue = queues.Queue({queue.id}) + obj1 = b'spam' + obj2 = b'eggs' + queue.put(obj1{extraargs}) + queue.put(obj2{extraargs}) + """)) + self.assertEqual(queue.qsize(), presize + 2) + + if presize == 0: + obj1 = queue.get() + self.assertEqual(obj1, b'spam') + self.assertEqual(queue.qsize(), presize + 1) + + return interp + + with self.subTest('default'): # UNBOUND + queue = queues.create() + interp = common(queue) + del interp + obj1 = queue.get() + self.assertIs(obj1, queues.UNBOUND) + self.assertEqual(queue.qsize(), 0) + with self.assertRaises(queues.QueueEmpty): + queue.get_nowait() + + with self.subTest('UNBOUND'): + queue = queues.create() + interp = common(queue, queues.UNBOUND) + del interp + obj1 = queue.get() + self.assertIs(obj1, queues.UNBOUND) + self.assertEqual(queue.qsize(), 0) + with self.assertRaises(queues.QueueEmpty): + queue.get_nowait() + + with self.subTest('UNBOUND_ERROR'): + queue = queues.create() + interp = common(queue, queues.UNBOUND_ERROR) + + del interp + self.assertEqual(queue.qsize(), 1) + with self.assertRaises(queues.ItemInterpreterDestroyed): + queue.get() + + self.assertEqual(queue.qsize(), 0) + with self.assertRaises(queues.QueueEmpty): + queue.get_nowait() + + with self.subTest('UNBOUND_REMOVE'): + queue = queues.create() + + interp = common(queue, queues.UNBOUND_REMOVE) + del interp + self.assertEqual(queue.qsize(), 0) + with self.assertRaises(queues.QueueEmpty): + queue.get_nowait() + + queue.put(b'ham', unbounditems=queues.UNBOUND_REMOVE) + self.assertEqual(queue.qsize(), 1) + interp = common(queue, queues.UNBOUND_REMOVE, 1) + self.assertEqual(queue.qsize(), 3) + queue.put(42, unbounditems=queues.UNBOUND_REMOVE) + self.assertEqual(queue.qsize(), 4) + del interp + self.assertEqual(queue.qsize(), 2) + obj1 = queue.get() + obj2 = queue.get() + self.assertEqual(obj1, b'ham') + self.assertEqual(obj2, 42) + self.assertEqual(queue.qsize(), 0) + with self.assertRaises(queues.QueueEmpty): + queue.get_nowait() + + def test_put_cleared_with_subinterpreter_mixed(self): + queue = queues.create() + interp = interpreters.create() + _run_output(interp, dedent(f""" + from concurrent.interpreters import _queues as queues + queue = queues.Queue({queue.id}) + queue.put(1, unbounditems=queues.UNBOUND) + queue.put(2, unbounditems=queues.UNBOUND_ERROR) + queue.put(3) + queue.put(4, unbounditems=queues.UNBOUND_REMOVE) + queue.put(5, unbounditems=queues.UNBOUND) + """)) + self.assertEqual(queue.qsize(), 5) + + del interp + self.assertEqual(queue.qsize(), 4) + + obj1 = queue.get() + self.assertIs(obj1, queues.UNBOUND) + self.assertEqual(queue.qsize(), 3) + + with self.assertRaises(queues.ItemInterpreterDestroyed): + queue.get() + self.assertEqual(queue.qsize(), 2) + + obj2 = queue.get() + self.assertIs(obj2, queues.UNBOUND) + self.assertEqual(queue.qsize(), 1) + + obj3 = queue.get() + self.assertIs(obj3, queues.UNBOUND) + self.assertEqual(queue.qsize(), 0) + + def test_put_cleared_with_subinterpreter_multiple(self): + queue = queues.create() + interp1 = interpreters.create() + interp2 = interpreters.create() + + queue.put(1) + _run_output(interp1, dedent(f""" + from concurrent.interpreters import _queues as queues + queue = queues.Queue({queue.id}) + obj1 = queue.get() + queue.put(2, unbounditems=queues.UNBOUND) + queue.put(obj1, unbounditems=queues.UNBOUND_REMOVE) + """)) + _run_output(interp2, dedent(f""" + from concurrent.interpreters import _queues as queues + queue = queues.Queue({queue.id}) + obj2 = queue.get() + obj1 = queue.get() + """)) + self.assertEqual(queue.qsize(), 0) + queue.put(3) + _run_output(interp1, dedent(""" + queue.put(4, unbounditems=queues.UNBOUND) + # interp closed here + queue.put(5, unbounditems=queues.UNBOUND_REMOVE) + queue.put(6, unbounditems=queues.UNBOUND) + """)) + _run_output(interp2, dedent(""" + queue.put(7, unbounditems=queues.UNBOUND_ERROR) + # interp closed here + queue.put(obj1, unbounditems=queues.UNBOUND_ERROR) + queue.put(obj2, unbounditems=queues.UNBOUND_REMOVE) + queue.put(8, unbounditems=queues.UNBOUND) + """)) + _run_output(interp1, dedent(""" + queue.put(9, unbounditems=queues.UNBOUND_REMOVE) + queue.put(10, unbounditems=queues.UNBOUND) + """)) + self.assertEqual(queue.qsize(), 10) + + obj3 = queue.get() + self.assertEqual(obj3, 3) + self.assertEqual(queue.qsize(), 9) + + obj4 = queue.get() + self.assertEqual(obj4, 4) + self.assertEqual(queue.qsize(), 8) + + del interp1 + self.assertEqual(queue.qsize(), 6) + + # obj5 was removed + + obj6 = queue.get() + self.assertIs(obj6, queues.UNBOUND) + self.assertEqual(queue.qsize(), 5) + + obj7 = queue.get() + self.assertEqual(obj7, 7) + self.assertEqual(queue.qsize(), 4) + + del interp2 + self.assertEqual(queue.qsize(), 3) + + # obj1 + with self.assertRaises(queues.ItemInterpreterDestroyed): + queue.get() + self.assertEqual(queue.qsize(), 2) + + # obj2 was removed + + obj8 = queue.get() + self.assertIs(obj8, queues.UNBOUND) + self.assertEqual(queue.qsize(), 1) + + # obj9 was removed + + obj10 = queue.get() + self.assertIs(obj10, queues.UNBOUND) + self.assertEqual(queue.qsize(), 0) + + def test_put_get_different_threads(self): + queue1 = queues.create() + queue2 = queues.create() + + def f(): + while True: + try: + obj = queue1.get(timeout=0.1) + break + except queues.QueueEmpty: + continue + queue2.put(obj) + t = threading.Thread(target=f) + t.start() + + orig = b'spam' + queue1.put(orig) + obj = queue2.get() + t.join() + + self.assertEqual(obj, orig) + self.assertIsNot(obj, orig) + + +if __name__ == '__main__': + # Test needs to be a package, so we can do relative imports. + unittest.main() diff --git a/Lib/test/test_interpreters/test_stress.py b/Lib/test/test_interpreters/test_stress.py new file mode 100644 index 00000000000..50d2444a4c7 --- /dev/null +++ b/Lib/test/test_interpreters/test_stress.py @@ -0,0 +1,94 @@ +import threading +import unittest + +from test import support +from test.support import import_helper +from test.support import threading_helper +# Raise SkipTest if subinterpreters not supported. +_interpreters = import_helper.import_module('_interpreters') +from concurrent import interpreters +from concurrent.interpreters import InterpreterError +from .utils import TestBase + + +class StressTests(TestBase): + + # In these tests we generally want a lot of interpreters, + # but not so many that any test takes too long. + + @support.requires_resource('cpu') + def test_create_many_sequential(self): + alive = [] + for _ in range(100): + interp = interpreters.create() + alive.append(interp) + del alive + support.gc_collect() + + @threading_helper.requires_working_threading() + @support.bigmemtest(size=200, memuse=32*2**20, dry_run=False) + def test_create_many_threaded(self, size): + alive = [] + start = threading.Event() + def task(): + # try to create all interpreters simultaneously + if not start.wait(support.SHORT_TIMEOUT): + raise TimeoutError + interp = interpreters.create() + alive.append(interp) + threads = [threading.Thread(target=task) for _ in range(size)] + with threading_helper.start_threads(threads): + start.set() + del alive + support.gc_collect() + + @threading_helper.requires_working_threading() + @support.bigmemtest(size=200, memuse=34*2**20, dry_run=False) + def test_many_threads_running_interp_in_other_interp(self, size): + start = threading.Event() + interp = interpreters.create() + + script = f"""if True: + import _interpreters + _interpreters.run_string({interp.id}, '1') + """ + + def run(): + interp = interpreters.create() + alreadyrunning = (f'{interpreters.InterpreterError}: ' + 'interpreter already running') + # try to run all interpreters simultaneously + if not start.wait(support.SHORT_TIMEOUT): + raise TimeoutError + success = False + while not success: + try: + interp.exec(script) + except interpreters.ExecutionFailed as exc: + if exc.excinfo.msg != 'interpreter already running': + raise # re-raise + assert exc.excinfo.type.__name__ == 'InterpreterError' + else: + success = True + + threads = [threading.Thread(target=run) for _ in range(size)] + with threading_helper.start_threads(threads): + start.set() + support.gc_collect() + + @support.nomemtest + def test_create_interpreter_no_memory(self): + import _testcapi + + assertion = self.assertRaises(InterpreterError) + try: + _testcapi.set_nomemory(0, 1) + with assertion: + _interpreters.create() + finally: + _testcapi.remove_mem_hooks() + + +if __name__ == '__main__': + # Test needs to be a package, so we can do relative imports. + unittest.main() diff --git a/Lib/test/test_interpreters/utils.py b/Lib/test/test_interpreters/utils.py new file mode 100644 index 00000000000..bb6da52727c --- /dev/null +++ b/Lib/test/test_interpreters/utils.py @@ -0,0 +1,687 @@ +from collections import namedtuple +import contextlib +import errno +import json +import logging +import os +import os.path +#import select +import subprocess +import sys +import tempfile +from textwrap import dedent +import threading +import types +import unittest + +from test import support + +# We would use test.support.import_helper.import_module(), +# but the indirect import of test.support.os_helper causes refleaks. +try: + import _interpreters +except ImportError as exc: + raise unittest.SkipTest(str(exc)) +from concurrent import interpreters + + +try: + import _testinternalcapi + import _testcapi +except ImportError: + _testinternalcapi = None + _testcapi = None + +def requires_test_modules(func): + return unittest.skipIf(_testinternalcapi is None, "test requires _testinternalcapi module")(func) + + +def _dump_script(text): + lines = text.splitlines() + print() + print('-' * 20) + for i, line in enumerate(lines, 1): + print(f' {i:>{len(str(len(lines)))}} {line}') + print('-' * 20) + + +def _close_file(file): + try: + if hasattr(file, 'close'): + file.close() + else: + os.close(file) + except OSError as exc: + if exc.errno != errno.EBADF: + raise # re-raise + # It was closed already. + + +def pack_exception(exc=None): + captured = _interpreters.capture_exception(exc) + data = dict(captured.__dict__) + data['type'] = dict(captured.type.__dict__) + return json.dumps(data) + + +def unpack_exception(packed): + try: + data = json.loads(packed) + except json.decoder.JSONDecodeError as e: + logging.getLogger(__name__).warning('incomplete exception data', exc_info=e) + print(packed if isinstance(packed, str) else packed.decode('utf-8')) + return None + exc = types.SimpleNamespace(**data) + exc.type = types.SimpleNamespace(**exc.type) + return exc; + + +class CapturingResults: + + STDIO = dedent("""\ + with open({w_pipe}, 'wb', buffering=0) as _spipe_{stream}: + _captured_std{stream} = io.StringIO() + with contextlib.redirect_std{stream}(_captured_std{stream}): + ######################### + # begin wrapped script + + {indented} + + # end wrapped script + ######################### + text = _captured_std{stream}.getvalue() + _spipe_{stream}.write(text.encode('utf-8')) + """)[:-1] + EXC = dedent("""\ + with open({w_pipe}, 'wb', buffering=0) as _spipe_exc: + try: + ######################### + # begin wrapped script + + {indented} + + # end wrapped script + ######################### + except Exception as exc: + text = _interp_utils.pack_exception(exc) + _spipe_exc.write(text.encode('utf-8')) + """)[:-1] + + @classmethod + def wrap_script(cls, script, *, stdout=True, stderr=False, exc=False): + script = dedent(script).strip(os.linesep) + imports = [ + f'import {__name__} as _interp_utils', + ] + wrapped = script + + # Handle exc. + if exc: + exc = os.pipe() + r_exc, w_exc = exc + indented = wrapped.replace('\n', '\n ') + wrapped = cls.EXC.format( + w_pipe=w_exc, + indented=indented, + ) + else: + exc = None + + # Handle stdout. + if stdout: + imports.extend([ + 'import contextlib, io', + ]) + stdout = os.pipe() + r_out, w_out = stdout + indented = wrapped.replace('\n', '\n ') + wrapped = cls.STDIO.format( + w_pipe=w_out, + indented=indented, + stream='out', + ) + else: + stdout = None + + # Handle stderr. + if stderr == 'stdout': + stderr = None + elif stderr: + if not stdout: + imports.extend([ + 'import contextlib, io', + ]) + stderr = os.pipe() + r_err, w_err = stderr + indented = wrapped.replace('\n', '\n ') + wrapped = cls.STDIO.format( + w_pipe=w_err, + indented=indented, + stream='err', + ) + else: + stderr = None + + if wrapped == script: + raise NotImplementedError + else: + for line in imports: + wrapped = f'{line}{os.linesep}{wrapped}' + + results = cls(stdout, stderr, exc) + return wrapped, results + + def __init__(self, out, err, exc): + self._rf_out = None + self._rf_err = None + self._rf_exc = None + self._w_out = None + self._w_err = None + self._w_exc = None + + if out is not None: + r_out, w_out = out + self._rf_out = open(r_out, 'rb', buffering=0) + self._w_out = w_out + + if err is not None: + r_err, w_err = err + self._rf_err = open(r_err, 'rb', buffering=0) + self._w_err = w_err + + if exc is not None: + r_exc, w_exc = exc + self._rf_exc = open(r_exc, 'rb', buffering=0) + self._w_exc = w_exc + + self._buf_out = b'' + self._buf_err = b'' + self._buf_exc = b'' + self._exc = None + + self._closed = False + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + @property + def closed(self): + return self._closed + + def close(self): + if self._closed: + return + self._closed = True + + if self._w_out is not None: + _close_file(self._w_out) + self._w_out = None + if self._w_err is not None: + _close_file(self._w_err) + self._w_err = None + if self._w_exc is not None: + _close_file(self._w_exc) + self._w_exc = None + + self._capture() + + if self._rf_out is not None: + _close_file(self._rf_out) + self._rf_out = None + if self._rf_err is not None: + _close_file(self._rf_err) + self._rf_err = None + if self._rf_exc is not None: + _close_file(self._rf_exc) + self._rf_exc = None + + def _capture(self): + # Ideally this is called only after the script finishes + # (and thus has closed the write end of the pipe. + if self._rf_out is not None: + chunk = self._rf_out.read(100) + while chunk: + self._buf_out += chunk + chunk = self._rf_out.read(100) + if self._rf_err is not None: + chunk = self._rf_err.read(100) + while chunk: + self._buf_err += chunk + chunk = self._rf_err.read(100) + if self._rf_exc is not None: + chunk = self._rf_exc.read(100) + while chunk: + self._buf_exc += chunk + chunk = self._rf_exc.read(100) + + def _unpack_stdout(self): + return self._buf_out.decode('utf-8') + + def _unpack_stderr(self): + return self._buf_err.decode('utf-8') + + def _unpack_exc(self): + if self._exc is not None: + return self._exc + if not self._buf_exc: + return None + self._exc = unpack_exception(self._buf_exc) + return self._exc + + def stdout(self): + if self.closed: + return self.final().stdout + self._capture() + return self._unpack_stdout() + + def stderr(self): + if self.closed: + return self.final().stderr + self._capture() + return self._unpack_stderr() + + def exc(self): + if self.closed: + return self.final().exc + self._capture() + return self._unpack_exc() + + def final(self, *, force=False): + try: + return self._final + except AttributeError: + if not self._closed: + if not force: + raise Exception('no final results available yet') + else: + return CapturedResults.Proxy(self) + self._final = CapturedResults( + self._unpack_stdout(), + self._unpack_stderr(), + self._unpack_exc(), + ) + return self._final + + +class CapturedResults(namedtuple('CapturedResults', 'stdout stderr exc')): + + class Proxy: + def __init__(self, capturing): + self._capturing = capturing + def _finish(self): + if self._capturing is None: + return + self._final = self._capturing.final() + self._capturing = None + def __iter__(self): + self._finish() + yield from self._final + def __len__(self): + self._finish() + return len(self._final) + def __getattr__(self, name): + self._finish() + if name.startswith('_'): + raise AttributeError(name) + return getattr(self._final, name) + + def raise_if_failed(self): + if self.exc is not None: + raise interpreters.ExecutionFailed(self.exc) + + +def _captured_script(script, *, stdout=True, stderr=False, exc=False): + return CapturingResults.wrap_script( + script, + stdout=stdout, + stderr=stderr, + exc=exc, + ) + + +def clean_up_interpreters(): + for interp in interpreters.list_all(): + if interp.id == 0: # main + continue + try: + interp.close() + except _interpreters.InterpreterError: + pass # already destroyed + + +def _run_output(interp, request, init=None): + script, results = _captured_script(request) + with results: + if init: + interp.prepare_main(init) + interp.exec(script) + return results.stdout() + + +@contextlib.contextmanager +def _running(interp): + r, w = os.pipe() + def run(): + interp.exec(dedent(f""" + # wait for "signal" + with open({r}) as rpipe: + rpipe.read() + """)) + + t = threading.Thread(target=run) + t.start() + + yield + + with open(w, 'w') as spipe: + spipe.write('done') + t.join() + + +class TestBase(unittest.TestCase): + + def tearDown(self): + clean_up_interpreters() + + def pipe(self): + def ensure_closed(fd): + try: + os.close(fd) + except OSError: + pass + r, w = os.pipe() + self.addCleanup(lambda: ensure_closed(r)) + self.addCleanup(lambda: ensure_closed(w)) + return r, w + + def temp_dir(self): + tempdir = tempfile.mkdtemp() + tempdir = os.path.realpath(tempdir) + from test.support import os_helper + self.addCleanup(lambda: os_helper.rmtree(tempdir)) + return tempdir + + @contextlib.contextmanager + def captured_thread_exception(self): + ctx = types.SimpleNamespace(caught=None) + def excepthook(args): + ctx.caught = args + orig_excepthook = threading.excepthook + threading.excepthook = excepthook + try: + yield ctx + finally: + threading.excepthook = orig_excepthook + + def make_script(self, filename, dirname=None, text=None): + if text: + text = dedent(text) + if dirname is None: + dirname = self.temp_dir() + filename = os.path.join(dirname, filename) + + os.makedirs(os.path.dirname(filename), exist_ok=True) + with open(filename, 'w', encoding='utf-8') as outfile: + outfile.write(text or '') + return filename + + def make_module(self, name, pathentry=None, text=None): + if text: + text = dedent(text) + if pathentry is None: + pathentry = self.temp_dir() + else: + os.makedirs(pathentry, exist_ok=True) + *subnames, basename = name.split('.') + + dirname = pathentry + for subname in subnames: + dirname = os.path.join(dirname, subname) + if os.path.isdir(dirname): + pass + elif os.path.exists(dirname): + raise Exception(dirname) + else: + os.mkdir(dirname) + initfile = os.path.join(dirname, '__init__.py') + if not os.path.exists(initfile): + with open(initfile, 'w'): + pass + filename = os.path.join(dirname, basename + '.py') + + with open(filename, 'w', encoding='utf-8') as outfile: + outfile.write(text or '') + return filename + + @support.requires_subprocess() + def run_python(self, *argv): + proc = subprocess.run( + [sys.executable, *argv], + capture_output=True, + text=True, + ) + return proc.returncode, proc.stdout, proc.stderr + + def assert_python_ok(self, *argv): + exitcode, stdout, stderr = self.run_python(*argv) + self.assertNotEqual(exitcode, 1) + return stdout, stderr + + def assert_python_failure(self, *argv): + exitcode, stdout, stderr = self.run_python(*argv) + self.assertNotEqual(exitcode, 0) + return stdout, stderr + + def assert_ns_equal(self, ns1, ns2, msg=None): + # This is mostly copied from TestCase.assertDictEqual. + self.assertEqual(type(ns1), type(ns2)) + if ns1 == ns2: + return + + import difflib + import pprint + from unittest.util import _common_shorten_repr + standardMsg = '%s != %s' % _common_shorten_repr(ns1, ns2) + diff = ('\n' + '\n'.join(difflib.ndiff( + pprint.pformat(vars(ns1)).splitlines(), + pprint.pformat(vars(ns2)).splitlines()))) + diff = f'namespace({diff})' + standardMsg = self._truncateMessage(standardMsg, diff) + self.fail(self._formatMessage(msg, standardMsg)) + + def _run_string(self, interp, script): + wrapped, results = _captured_script(script, exc=False) + #_dump_script(wrapped) + with results: + if isinstance(interp, interpreters.Interpreter): + interp.exec(script) + else: + err = _interpreters.run_string(interp, wrapped) + if err is not None: + return None, err + return results.stdout(), None + + def run_and_capture(self, interp, script): + text, err = self._run_string(interp, script) + if err is not None: + raise interpreters.ExecutionFailed(err) + else: + return text + + def interp_exists(self, interpid): + try: + _interpreters.whence(interpid) + except _interpreters.InterpreterNotFoundError: + return False + else: + return True + + @requires_test_modules + @contextlib.contextmanager + def interpreter_from_capi(self, config=None, whence=None): + if config is False: + if whence is None: + whence = _interpreters.WHENCE_LEGACY_CAPI + else: + assert whence in (_interpreters.WHENCE_LEGACY_CAPI, + _interpreters.WHENCE_UNKNOWN), repr(whence) + config = None + elif config is True: + config = _interpreters.new_config('default') + elif config is None: + if whence not in ( + _interpreters.WHENCE_LEGACY_CAPI, + _interpreters.WHENCE_UNKNOWN, + ): + config = _interpreters.new_config('legacy') + elif isinstance(config, str): + config = _interpreters.new_config(config) + + if whence is None: + whence = _interpreters.WHENCE_XI + + interpid = _testinternalcapi.create_interpreter(config, whence=whence) + try: + yield interpid + finally: + try: + _testinternalcapi.destroy_interpreter(interpid) + except _interpreters.InterpreterNotFoundError: + pass + + @contextlib.contextmanager + def interpreter_obj_from_capi(self, config='legacy'): + with self.interpreter_from_capi(config) as interpid: + interp = interpreters.Interpreter( + interpid, + _whence=_interpreters.WHENCE_CAPI, + _ownsref=False, + ) + yield interp, interpid + + @contextlib.contextmanager + def capturing(self, script): + wrapped, capturing = _captured_script(script, stdout=True, exc=True) + #_dump_script(wrapped) + with capturing: + yield wrapped, capturing.final(force=True) + + @requires_test_modules + def run_from_capi(self, interpid, script, *, main=False): + with self.capturing(script) as (wrapped, results): + rc = _testinternalcapi.exec_interpreter(interpid, wrapped, main=main) + assert rc == 0, rc + results.raise_if_failed() + return results.stdout + + @contextlib.contextmanager + def _running(self, run_interp, exec_interp): + token = b'\0' + r_in, w_in = self.pipe() + r_out, w_out = self.pipe() + + def close(): + _close_file(r_in) + _close_file(w_in) + _close_file(r_out) + _close_file(w_out) + + # Start running (and wait). + script = dedent(f""" + import os + try: + # handshake + token = os.read({r_in}, 1) + os.write({w_out}, token) + # Wait for the "done" message. + os.read({r_in}, 1) + except BrokenPipeError: + pass + except OSError as exc: + if exc.errno != 9: + raise # re-raise + # It was closed already. + """) + failed = None + def run(): + nonlocal failed + try: + run_interp(script) + except Exception as exc: + failed = exc + close() + t = threading.Thread(target=run) + t.start() + + # handshake + try: + os.write(w_in, token) + token2 = os.read(r_out, 1) + assert token2 == token, (token2, token) + except OSError: + t.join() + if failed is not None: + raise failed + + # CM __exit__() + try: + try: + yield + finally: + # Send "done". + os.write(w_in, b'\0') + finally: + close() + t.join() + if failed is not None: + raise failed + + @contextlib.contextmanager + def running(self, interp): + if isinstance(interp, int): + interpid = interp + def exec_interp(script): + exc = _interpreters.exec(interpid, script) + assert exc is None, exc + run_interp = exec_interp + else: + def run_interp(script): + text = self.run_and_capture(interp, script) + assert text == '', repr(text) + def exec_interp(script): + interp.exec(script) + with self._running(run_interp, exec_interp): + yield + + @requires_test_modules + @contextlib.contextmanager + def running_from_capi(self, interpid, *, main=False): + def run_interp(script): + text = self.run_from_capi(interpid, script, main=main) + assert text == '', repr(text) + def exec_interp(script): + rc = _testinternalcapi.exec_interpreter(interpid, script) + assert rc == 0, rc + with self._running(run_interp, exec_interp): + yield + + @requires_test_modules + def run_temp_from_capi(self, script, config='legacy'): + if config is False: + # Force using Py_NewInterpreter(). + run_in_interp = (lambda s, c: _testcapi.run_in_subinterp(s)) + config = None + else: + run_in_interp = _testinternalcapi.run_in_subinterp_with_config + if config is True: + config = 'default' + if isinstance(config, str): + config = _interpreters.new_config(config) + with self.capturing(script) as (wrapped, results): + rc = run_in_interp(wrapped, config) + assert rc == 0, rc + results.raise_if_failed() + return results.stdout diff --git a/Lib/test/test_mailbox.py b/Lib/test/test_mailbox.py index 0169948e453..019c699bff5 100644 --- a/Lib/test/test_mailbox.py +++ b/Lib/test/test_mailbox.py @@ -8,9 +8,10 @@ import io import tempfile from test import support -from test.support import import_helper +from test.support import import_helper, warnings_helper from test.support import os_helper from test.support import refleak_helper +from test.support import requires_root_user from test.support import socket_helper import unittest import textwrap @@ -542,6 +543,11 @@ def _test_flush_or_close(self, method, should_call_close): self.assertIn(self._box.get_string(key), contents) oldbox.close() + def test_use_context_manager(self): + # Mailboxes are usable as a context manager + with self._box as box: + self.assertIs(self._box, box) + def test_dump_message(self): # Write message representations to disk for input in (email.message_from_string(_sample_message), @@ -1081,6 +1087,7 @@ def test_permissions_after_flush(self): self.assertEqual(os.stat(self._path).st_mode, mode) + @requires_root_user @unittest.skipUnless(hasattr(os, 'chown'), 'requires os.chown') def test_ownership_after_flush(self): # See issue gh-117467 @@ -1103,10 +1110,7 @@ def test_ownership_after_flush(self): else: self.skipTest("test needs more than one group") - try: - os.chown(self._path, other_uid, other_gid) - except OSError: - self.skipTest('test needs root privilege') + os.chown(self._path, other_uid, other_gid) # Change permissions as in test_permissions_after_flush. mode = st.st_mode | 0o666 os.chmod(self._path, mode) @@ -1122,6 +1126,16 @@ def test_ownership_after_flush(self): self.assertEqual(st.st_gid, other_gid) self.assertEqual(st.st_mode, mode) + def test_context_manager_locks_and_closes(self): + # Context manager locks/unlocks and closes. + # (This test uses an implementation detail to get the state.) + self.assertFalse(self._box._locked) + with self._box as context_object: + self.assertIs(self._box, context_object) + self.assertTrue(self._box._locked) + self.assertFalse(self._box._file.closed) + self.assertFalse(self._box._locked) + self.assertTrue(self._box._file.closed) class _TestMboxMMDF(_TestSingleFile): @@ -1212,6 +1226,7 @@ def test_add_and_close(self): self.assertEqual(contents, f.read()) self._box = self._factory(self._path) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.requires_fork() @unittest.skipUnless(hasattr(socket, 'socketpair'), "Test needs socketpair().") def test_lock_conflict(self): diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index 9f5cef9be63..057dff1e671 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -116,6 +116,14 @@ def test_bytes(self): for s in [b"", b"Andr\xe8 Previn", b"abc", b" "*10000]: self.helper(s) + @support.cpython_only + def test_bytes_singleton(self): + for version in range(marshal.version + 1): + for sample in [b"", b"x"]: + new = marshal.loads(marshal.dumps(sample, version)) + self.assertIs(new, sample) + + class ExceptionTestCase(unittest.TestCase): def test_exceptions(self): new = marshal.loads(marshal.dumps(StopIteration)) @@ -382,6 +390,12 @@ def test_reference_loop_slice(self): for v in range(marshal.version + 1): self.assertRaises(ValueError, marshal.dumps, a, v) + def test_reference_loop_frozendict(self): + a = frozendict({None: []}) + a[None].append(a) + for v in range(marshal.version + 1): + self.assertRaises(ValueError, marshal.dumps, a, v) + def test_loads_reference_loop_list(self): data = b'\xdb\x01\x00\x00\x00r\x00\x00\x00\x00' # [] a = marshal.loads(data) @@ -431,9 +445,13 @@ def test_loads_abnormal_reference_loops(self): b'\xba{Nr\x00\x00\x00\x000NN', # slice({None: }, None) b'\xbaN{Nr\x00\x00\x00\x000N', # slice(None, {None: }) b'\xbaNN{Nr\x00\x00\x00\x000', # slice(None, None, {None: }) + b'\xfdN[\x01\x00\x00\x00r\x00\x00\x00\x000', # frozendict({None: []}) + b'\xfdN{Nr\x00\x00\x00\x0000', # frozendict({None: {None: }) # Direct self-references which cannot be created in Python. b'\xbe\x01\x00\x00\x00r\x00\x00\x00\x00', # frozenset({}) + b'\xfdNr\x00\x00\x00\x000', # frozendict({None: }) + b'\xfdr\x00\x00\x00\x00N0', # frozendict({: None}) b'\xbar\x00\x00\x00\x00NN', # slice(, None) b'\xbaNr\x00\x00\x00\x00N', # slice(None, ) b'\xbaNNr\x00\x00\x00\x00', # slice(None, None, ) @@ -695,6 +713,15 @@ def testDict(self): self.helper(dictobj) self.helper3(dictobj) + def testFrozenDict(self): + for obj in self.keys: + dictobj = frozendict({"hello": obj, "goodbye": obj, obj: "hello"}) + self.helper(dictobj) + + for version in range(6): + with self.assertRaises(ValueError): + marshal.dumps(dictobj, version) + def testModule(self): with open(__file__, "rb") as f: code = f.read() @@ -760,7 +787,7 @@ def test_slice(self): with self.subTest(obj=str(obj)): self.helper(obj) - for version in range(4): + for version in range(5): with self.assertRaises(ValueError): marshal.dumps(obj, version) diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index 500d8e8bbac..9bf83a8518c 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -480,7 +480,15 @@ def testGetAttributeNS(self): self.assertEqual(child2.getAttributeNS("http://www.python.org", "missing"), '') - def testGetAttributeNode(self): pass + def testGetAttributeNode(self): + dom = parseString("") + elem = dom.documentElement + attr = elem.getAttributeNode("a") + self.assertEqual(attr.name, "a") + self.assertEqual(attr.value, "1") + self.assertIs(attr.ownerElement, elem) + self.assertIsNone(elem.getAttributeNode("b")) + dom.unlink() @unittest.expectedFailure # TODO: RUSTPYTHON def testGetElementsByTagNameNS(self): @@ -671,9 +679,34 @@ def testTextRepr(self): self.assertEqual(str(el), repr(el)) self.assertEqual('', str(el)) - def testWriteText(self): pass + def testWriteText(self): + dom = parseString("text<&>") + elem = dom.documentElement + writer = io.StringIO() + elem.writexml(writer) + self.assertEqual(writer.getvalue(), + "text<&>") + writer = io.StringIO() + elem.writexml(writer, indent=" ", addindent=" ", newl="\n") + self.assertEqual(writer.getvalue(), + " \n" + " text\n" + " <&>\n" + " \n") + dom.unlink() - def testDocumentElement(self): pass + def testDocumentElement(self): + dom = parseString("") + elem = dom.documentElement + self.assertEqual(elem.tagName, "doc") + self.assertIs(elem, dom.childNodes[1]) + dom.unlink() + + dom = Document() + self.assertIsNone(dom.documentElement) + elem = dom.appendChild(dom.createElement("doc")) + self.assertIs(dom.documentElement, elem) + dom.unlink() def testTooManyDocumentElements(self): doc = parseString("") @@ -683,25 +716,126 @@ def testTooManyDocumentElements(self): elem.unlink() doc.unlink() - def testCreateElementNS(self): pass + def testCreateElementNS(self): + dom = Document() + elem = dom.createElementNS("http://xml.python.org/ns", "p:elem") + self.assertEqual(elem.nodeType, Node.ELEMENT_NODE) + self.assertEqual(elem.tagName, "p:elem") + self.assertEqual(elem.nodeName, "p:elem") + self.assertEqual(elem.namespaceURI, "http://xml.python.org/ns") + self.assertEqual(elem.prefix, "p") + self.assertEqual(elem.localName, "elem") + self.assertIs(elem.ownerDocument, dom) + self.assertIsNone(elem.parentNode) + + elem = dom.createElementNS("http://xml.python.org/ns", "elem") + self.assertEqual(elem.tagName, "elem") + self.assertIsNone(elem.prefix) + self.assertEqual(elem.localName, "elem") + dom.unlink() - def testCreateAttributeNS(self): pass + def testCreateAttributeNS(self): + dom = Document() + attr = dom.createAttributeNS("http://xml.python.org/ns", "p:attr") + self.assertEqual(attr.nodeType, Node.ATTRIBUTE_NODE) + self.assertEqual(attr.name, "p:attr") + self.assertEqual(attr.nodeName, "p:attr") + self.assertEqual(attr.namespaceURI, "http://xml.python.org/ns") + self.assertEqual(attr.prefix, "p") + self.assertEqual(attr.localName, "attr") + self.assertEqual(attr.value, "") + self.assertIs(attr.ownerDocument, dom) + self.assertIsNone(attr.ownerElement) + + elem = dom.appendChild(dom.createElement("doc")) + elem.setAttributeNode(attr) + self.assertIs(attr.ownerElement, elem) + self.assertIs(elem.getAttributeNodeNS("http://xml.python.org/ns", + "attr"), attr) + dom.unlink() - def testParse(self): pass + def testParse(self): + # parsing from a file object is tested in testParseFromBinaryFile + # and testParseFromTextFile + dom = parse(tstfile) + self.assertEqual(dom.nodeType, Node.DOCUMENT_NODE) + self.assertEqual(dom.documentElement.tagName, "HTML") + dom.unlink() - def testParseString(self): pass + self.assertRaises(ExpatError, parseString, "") - def testComment(self): pass + def testParseString(self): + dom = parseString("text") + self.assertEqual(dom.nodeType, Node.DOCUMENT_NODE) + self.assertEqual(dom.documentElement.tagName, "doc") + self.assertEqual(dom.documentElement.firstChild.data, "text") + dom.unlink() + + dom = parseString(b"" + b"\xc3\xa9") + self.assertEqual(dom.documentElement.firstChild.data, "\xe9") + dom.unlink() + + def testComment(self): + dom = Document() + comment = dom.createComment("comment") + self.assertEqual(comment.nodeType, Node.COMMENT_NODE) + self.assertEqual(comment.nodeName, "#comment") + self.assertEqual(comment.data, "comment") + self.assertEqual(comment.nodeValue, "comment") + self.assertIsNone(comment.attributes) + dom.appendChild(comment) + self.assertEqual(dom.toxml(), + '') + dom.unlink() - def testAttrListItem(self): pass + dom = parseString("") + comment = dom.documentElement.firstChild + self.assertEqual(comment.nodeType, Node.COMMENT_NODE) + self.assertEqual(comment.data, "comment") + dom.unlink() - def testAttrListItems(self): pass + def testAttrListItem(self): + dom = parseString("") + attrs = dom.documentElement.attributes + self.assertEqual(attrs.item(0).name, "a") + self.assertEqual(attrs.item(1).name, "b") + self.assertIsNone(attrs.item(2)) + dom.unlink() - def testAttrListItemNS(self): pass + def testAttrListItems(self): + dom = parseString("") + attrs = dom.documentElement.attributes + self.assertEqual(attrs.items(), [("a", "1"), ("b", "2")]) + dom.unlink() - def testAttrListKeys(self): pass + def testAttrListItemNS(self): + dom = parseString("") + attrs = dom.documentElement.attributes + self.assertEqual(attrs.itemsNS(), [ + ((xml.dom.XMLNS_NAMESPACE, "p"), "http://xml.python.org/ns"), + (("http://xml.python.org/ns", "a"), "1"), + ((None, "b"), "2"), + ]) + dom.unlink() - def testAttrListKeysNS(self): pass + def testAttrListKeys(self): + dom = parseString("") + attrs = dom.documentElement.attributes + self.assertEqual(list(attrs.keys()), ["a", "b"]) + dom.unlink() + + def testAttrListKeysNS(self): + dom = parseString("") + attrs = dom.documentElement.attributes + self.assertEqual(list(attrs.keysNS()), [ + (xml.dom.XMLNS_NAMESPACE, "p"), + ("http://xml.python.org/ns", "a"), + (None, "b"), + ]) + dom.unlink() def testRemoveNamedItem(self): doc = parseString("") @@ -722,29 +856,161 @@ def testRemoveNamedItemNS(self): self.assertRaises(xml.dom.NotFoundErr, attrs.removeNamedItemNS, "http://xml.python.org/", "b") - def testAttrListValues(self): pass + def testAttrListValues(self): + dom = parseString("") + attrs = dom.documentElement.attributes + self.assertEqual([attr.name for attr in attrs.values()], ["a", "b"]) + self.assertEqual([attr.value for attr in attrs.values()], ["1", "2"]) + dom.unlink() + + def testAttrListLength(self): + dom = parseString("") + attrs = dom.documentElement.attributes + self.assertEqual(attrs.length, 2) + self.assertEqual(len(attrs), 2) + dom.unlink() + + dom = parseString("") + self.assertEqual(dom.documentElement.attributes.length, 0) + dom.unlink() + + def testAttrList__getitem__(self): + dom = parseString("") + attrs = dom.documentElement.attributes + self.assertEqual(attrs["b"].value, "2") + self.assertEqual(attrs[("http://xml.python.org/ns", "a")].value, "1") + self.assertRaises(KeyError, attrs.__getitem__, "missing") + self.assertRaises(KeyError, attrs.__getitem__, (None, "missing")) + dom.unlink() - def testAttrListLength(self): pass + def testAttrList__setitem__(self): + dom = parseString("") + elem = dom.documentElement + attrs = elem.attributes + attrs["a"] = "2" + self.assertEqual(elem.getAttribute("a"), "2") + attrs["b"] = "3" + self.assertEqual(elem.getAttribute("b"), "3") + self.assertEqual(attrs.length, 2) + + attr = dom.createAttribute("c") + attr.value = "4" + attrs["c"] = attr + self.assertIs(elem.getAttributeNode("c"), attr) + self.assertEqual(elem.getAttribute("c"), "4") + dom.unlink() - def testAttrList__getitem__(self): pass + def testSetAttrValueandNodeValue(self): + dom = parseString("") + attr = dom.documentElement.getAttributeNode("a") + self.assertEqual(attr.value, "1") + self.assertEqual(attr.nodeValue, "1") + attr.value = "2" + self.assertEqual(attr.nodeValue, "2") + attr.nodeValue = "3" + self.assertEqual(attr.value, "3") + self.assertEqual(dom.documentElement.getAttribute("a"), "3") + dom.unlink() - def testAttrList__setitem__(self): pass + def testParseElement(self): + dom = parseString("") + elem = dom.documentElement + self.assertEqual(elem.nodeType, Node.ELEMENT_NODE) + self.assertEqual(elem.tagName, "doc") + self.assertIsNone(elem.namespaceURI) + self.assertIs(elem.parentNode, dom) + self.assertIs(elem.ownerDocument, dom) + self.assertEqual([child.tagName for child in elem.childNodes], + ["child", "child"]) + dom.unlink() - def testSetAttrValueandNodeValue(self): pass + def testParseAttributes(self): + dom = parseString("") + elem = dom.documentElement + self.assertEqual(elem.getAttribute("a"), "1") + self.assertEqual(elem.getAttribute("b"), "&") + self.assertEqual(elem.getAttribute("missing"), "") + self.assertTrue(elem.hasAttribute("a")) + self.assertFalse(elem.hasAttribute("missing")) + attr = elem.getAttributeNode("a") + self.assertIsNone(attr.namespaceURI) + dom.unlink() - def testParseElement(self): pass + def testParseElementNamespaces(self): + dom = parseString("" + "") + elem = dom.documentElement + self.assertEqual(elem.tagName, "p:doc") + self.assertEqual(elem.namespaceURI, "http://xml.python.org/ns") + self.assertEqual(elem.prefix, "p") + self.assertEqual(elem.localName, "doc") + child = elem.getElementsByTagName("child")[0] + self.assertEqual(child.namespaceURI, "http://xml.python.org/default") + self.assertIsNone(child.prefix) + self.assertEqual(child.localName, "child") + dom.unlink() - def testParseAttributes(self): pass + def testParseAttributeNamespaces(self): + dom = parseString("") + elem = dom.documentElement + self.assertEqual(elem.getAttributeNS("http://xml.python.org/ns", "a"), + "1") + self.assertEqual(elem.getAttributeNS(None, "b"), "2") + attr = elem.getAttributeNodeNS("http://xml.python.org/ns", "a") + self.assertEqual(attr.name, "p:a") + self.assertEqual(attr.prefix, "p") + self.assertEqual(attr.localName, "a") + declaration = elem.getAttributeNode("xmlns:p") + self.assertEqual(declaration.namespaceURI, xml.dom.XMLNS_NAMESPACE) + self.assertEqual(declaration.value, "http://xml.python.org/ns") + dom.unlink() - def testParseElementNamespaces(self): pass + def testParseProcessingInstructions(self): + # the content of a processing instruction is tested + # in testProcessingInstruction + dom = parseString("") + pi = dom.childNodes[0] + self.assertEqual(pi.nodeType, Node.PROCESSING_INSTRUCTION_NODE) + self.assertEqual(pi.target, "before") + self.assertEqual(pi.data, "data") + self.assertIs(pi.parentNode, dom) + pi = dom.childNodes[2] + self.assertEqual(pi.target, "after") + self.assertEqual(pi.data, "") + dom.unlink() - def testParseAttributeNamespaces(self): pass + def testChildNodes(self): + dom = parseString("text") + children = dom.documentElement.childNodes + self.assertEqual(len(children), 3) + self.assertEqual([child.nodeType for child in children], + [Node.TEXT_NODE, Node.ELEMENT_NODE, Node.COMMENT_NODE]) + for child in children: + self.assertIs(child.parentNode, dom.documentElement) + dom.unlink() - def testParseProcessingInstructions(self): pass + dom = parseString("") + self.assertEqual(len(dom.documentElement.childNodes), 0) + dom.unlink() - def testChildNodes(self): pass + def testFirstChild(self): + dom = parseString("") + elem = dom.documentElement + self.assertEqual(elem.firstChild.tagName, "a") + self.assertEqual(elem.lastChild.tagName, "b") + self.assertIs(elem.firstChild, elem.childNodes[0]) + self.assertIs(elem.lastChild, elem.childNodes[-1]) + self.assertIsNone(elem.firstChild.previousSibling) + self.assertIs(elem.firstChild.nextSibling, elem.lastChild) + dom.unlink() - def testFirstChild(self): pass + dom = parseString("") + self.assertIsNone(dom.documentElement.firstChild) + self.assertIsNone(dom.documentElement.lastChild) + dom.unlink() def testHasChildNodes(self): dom = parseString("") diff --git a/Lib/test/test_modulefinder.py b/Lib/test/test_modulefinder.py index b64e684f805..703bfbf94d7 100644 --- a/Lib/test/test_modulefinder.py +++ b/Lib/test/test_modulefinder.py @@ -76,6 +76,18 @@ from sys import version_info """] +namespace_package_test = [ + "module", + ["a", "module"], + ["a.c", "blahblah"], [], + """\ +module.py + import a + import a.c + import blahblah +a/b.py +"""] + absolute_import_test = [ "a.module", ["a", "a.module", @@ -353,6 +365,9 @@ def _do_test(self, info, report=False, debug=0, replace_paths=[], modulefinder_c def test_package(self): self._do_test(package_test) + def test_namespace_package(self): + self._do_test(namespace_package_test) + def test_maybe(self): self._do_test(maybe_test) diff --git a/Lib/test/test_multiprocessing_forkserver/__init__.py b/Lib/test/test_multiprocessing_forkserver/__init__.py index d91715a344d..162adf2071f 100644 --- a/Lib/test/test_multiprocessing_forkserver/__init__.py +++ b/Lib/test/test_multiprocessing_forkserver/__init__.py @@ -1,3 +1,4 @@ +import multiprocessing import os.path import sys import unittest @@ -9,5 +10,13 @@ if sys.platform == "win32": raise unittest.SkipTest("forkserver is not available on Windows") +if not support.has_fork_support: + raise unittest.SkipTest("requires working os.fork()") + +# The forkserver start method requires passing file descriptors over a Unix +# socket, which is not available on every platform (e.g. Solaris/illumos). +if "forkserver" not in multiprocessing.get_all_start_methods(): + raise unittest.SkipTest("forkserver start method is not available") + def load_tests(*args): return support.load_package_tests(os.path.dirname(__file__), *args) diff --git a/Lib/test/test_multiprocessing_forkserver/test_preload.py b/Lib/test/test_multiprocessing_forkserver/test_preload.py new file mode 100644 index 00000000000..f8e119aa367 --- /dev/null +++ b/Lib/test/test_multiprocessing_forkserver/test_preload.py @@ -0,0 +1,230 @@ +"""Tests for forkserver preload functionality.""" + +import contextlib +import multiprocessing +import os +import shutil +import sys +import tempfile +import unittest +from multiprocessing import forkserver, spawn + + +class TestForkserverPreload(unittest.TestCase): + """Tests for forkserver preload functionality.""" + + def setUp(self): + self._saved_warnoptions = sys.warnoptions.copy() + # Remove warning options that would convert ImportWarning to errors: + # - 'error' converts all warnings to errors + # - 'error::ImportWarning' specifically converts ImportWarning + # Keep other specific options like 'error::BytesWarning' that + # subprocess's _args_from_interpreter_flags() expects to remove + sys.warnoptions[:] = [ + opt for opt in sys.warnoptions + if opt not in ('error', 'error::ImportWarning') + ] + self.ctx = multiprocessing.get_context('forkserver') + forkserver._forkserver._stop() + + def tearDown(self): + sys.warnoptions[:] = self._saved_warnoptions + forkserver._forkserver._stop() + + @staticmethod + def _send_value(conn, value): + """Send value through connection. Static method to be picklable as Process target.""" + conn.send(value) + + @contextlib.contextmanager + def capture_forkserver_stderr(self): + """Capture stderr from forkserver by preloading a module that redirects it. + + Yields (module_name, capture_file_path). The capture file can be read + after the forkserver has processed preloads. This works because + forkserver.main() calls util._flush_std_streams() after preloading, + ensuring captured output is written before we read it. + """ + tmpdir = tempfile.mkdtemp() + capture_module = os.path.join(tmpdir, '_capture_stderr.py') + capture_file = os.path.join(tmpdir, 'stderr.txt') + try: + with open(capture_module, 'w') as f: + # Use line buffering (buffering=1) to ensure warnings are written. + # Enable ImportWarning since it's ignored by default. + f.write( + f'import sys, warnings; ' + f'sys.stderr = open({capture_file!r}, "w", buffering=1); ' + f'warnings.filterwarnings("always", category=ImportWarning)\n' + ) + sys.path.insert(0, tmpdir) + yield '_capture_stderr', capture_file + finally: + sys.path.remove(tmpdir) + shutil.rmtree(tmpdir, ignore_errors=True) + + def test_preload_on_error_ignore_default(self): + """Test that invalid modules are silently ignored by default.""" + self.ctx.set_forkserver_preload(['nonexistent_module_xyz']) + + r, w = self.ctx.Pipe(duplex=False) + p = self.ctx.Process(target=self._send_value, args=(w, 42)) + p.start() + w.close() + result = r.recv() + r.close() + p.join() + + self.assertEqual(result, 42) + self.assertEqual(p.exitcode, 0) + + def test_preload_on_error_ignore_explicit(self): + """Test that invalid modules are silently ignored with on_error='ignore'.""" + self.ctx.set_forkserver_preload(['nonexistent_module_xyz'], on_error='ignore') + + r, w = self.ctx.Pipe(duplex=False) + p = self.ctx.Process(target=self._send_value, args=(w, 99)) + p.start() + w.close() + result = r.recv() + r.close() + p.join() + + self.assertEqual(result, 99) + self.assertEqual(p.exitcode, 0) + + def test_preload_on_error_warn(self): + """Test that invalid modules emit warnings with on_error='warn'.""" + with self.capture_forkserver_stderr() as (capture_mod, stderr_file): + self.ctx.set_forkserver_preload( + [capture_mod, 'nonexistent_module_xyz'], on_error='warn') + + r, w = self.ctx.Pipe(duplex=False) + p = self.ctx.Process(target=self._send_value, args=(w, 123)) + p.start() + w.close() + result = r.recv() + r.close() + p.join() + + self.assertEqual(result, 123) + self.assertEqual(p.exitcode, 0) + + with open(stderr_file) as f: + stderr_output = f.read() + self.assertIn('nonexistent_module_xyz', stderr_output) + self.assertIn('ImportWarning', stderr_output) + + def test_preload_on_error_fail_breaks_context(self): + """Test that invalid modules with on_error='fail' breaks the forkserver.""" + with self.capture_forkserver_stderr() as (capture_mod, stderr_file): + self.ctx.set_forkserver_preload( + [capture_mod, 'nonexistent_module_xyz'], on_error='fail') + + r, w = self.ctx.Pipe(duplex=False) + try: + p = self.ctx.Process(target=self._send_value, args=(w, 42)) + with self.assertRaises((EOFError, ConnectionError, BrokenPipeError)) as cm: + p.start() + notes = getattr(cm.exception, '__notes__', []) + self.assertTrue(notes, "Expected exception to have __notes__") + self.assertIn('Forkserver process may have crashed', notes[0]) + + with open(stderr_file) as f: + stderr_output = f.read() + self.assertIn('nonexistent_module_xyz', stderr_output) + self.assertIn('ModuleNotFoundError', stderr_output) + finally: + w.close() + r.close() + + def test_preload_valid_modules_with_on_error_fail(self): + """Test that valid modules work fine with on_error='fail'.""" + self.ctx.set_forkserver_preload(['os', 'sys'], on_error='fail') + + r, w = self.ctx.Pipe(duplex=False) + p = self.ctx.Process(target=self._send_value, args=(w, 'success')) + p.start() + w.close() + result = r.recv() + r.close() + p.join() + + self.assertEqual(result, 'success') + self.assertEqual(p.exitcode, 0) + + def test_preload_invalid_on_error_value(self): + """Test that invalid on_error values raise ValueError.""" + with self.assertRaises(ValueError) as cm: + self.ctx.set_forkserver_preload(['os'], on_error='invalid') + self.assertIn("on_error must be 'ignore', 'warn', or 'fail'", str(cm.exception)) + + +class TestHandlePreload(unittest.TestCase): + """Unit tests for _handle_preload() function.""" + + def setUp(self): + self._saved_main = sys.modules['__main__'] + + def tearDown(self): + spawn.old_main_modules.clear() + sys.modules['__main__'] = self._saved_main + + def test_handle_preload_main_on_error_fail(self): + """Test that __main__ import failures raise with on_error='fail'.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f: + f.write('raise RuntimeError("test error in __main__")\n') + f.flush() + with self.assertRaises(RuntimeError) as cm: + forkserver._handle_preload(['__main__'], main_path=f.name, on_error='fail') + self.assertIn("test error in __main__", str(cm.exception)) + + def test_handle_preload_main_on_error_warn(self): + """Test that __main__ import failures warn with on_error='warn'.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f: + f.write('raise ImportError("test import error")\n') + f.flush() + with self.assertWarns(ImportWarning) as cm: + forkserver._handle_preload(['__main__'], main_path=f.name, on_error='warn') + self.assertIn("Failed to preload __main__", str(cm.warning)) + self.assertIn("test import error", str(cm.warning)) + + def test_handle_preload_main_on_error_ignore(self): + """Test that __main__ import failures are ignored with on_error='ignore'.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f: + f.write('raise ImportError("test import error")\n') + f.flush() + forkserver._handle_preload(['__main__'], main_path=f.name, on_error='ignore') + + def test_handle_preload_main_valid(self): + """Test that valid __main__ preload works.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f: + f.write('test_var = 42\n') + f.flush() + forkserver._handle_preload(['__main__'], main_path=f.name, on_error='fail') + + def test_handle_preload_module_on_error_fail(self): + """Test that module import failures raise with on_error='fail'.""" + with self.assertRaises(ModuleNotFoundError): + forkserver._handle_preload(['nonexistent_test_module_xyz'], on_error='fail') + + def test_handle_preload_module_on_error_warn(self): + """Test that module import failures warn with on_error='warn'.""" + with self.assertWarns(ImportWarning) as cm: + forkserver._handle_preload(['nonexistent_test_module_xyz'], on_error='warn') + self.assertIn("Failed to preload module", str(cm.warning)) + + def test_handle_preload_module_on_error_ignore(self): + """Test that module import failures are ignored with on_error='ignore'.""" + forkserver._handle_preload(['nonexistent_test_module_xyz'], on_error='ignore') + + def test_handle_preload_combined(self): + """Test preloading both __main__ and modules.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f: + f.write('import sys\n') + f.flush() + forkserver._handle_preload(['__main__', 'os', 'sys'], main_path=f.name, on_error='fail') + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_multiprocessing_main_handling.py b/Lib/test/test_multiprocessing_main_handling.py index 6b30a893167..fbd44fb0d3d 100644 --- a/Lib/test/test_multiprocessing_main_handling.py +++ b/Lib/test/test_multiprocessing_main_handling.py @@ -196,9 +196,8 @@ def test_ipython_workaround(self): def test_script_compiled(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, 'script') - py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) self._check_script(pyc_file) def test_directory(self): @@ -213,9 +212,8 @@ def test_directory_compiled(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, '__main__', source=source) - py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) self._check_script(script_dir) def test_zipfile(self): @@ -231,7 +229,8 @@ def test_zipfile_compiled(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, '__main__', source=source) - compiled_name = py_compile.compile(script_name, doraise=True) + compiled_name = script_name + 'c' + py_compile.compile(script_name, compiled_name, doraise=True) zip_name, run_name = make_zip_script(script_dir, 'test_zip', compiled_name) self._check_script(zip_name) @@ -273,9 +272,8 @@ def test_package_compiled(self): make_pkg(pkg_dir) script_name = _make_test_script(pkg_dir, '__main__', source=source) - compiled_name = py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg') self._check_script(launch_name) diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py index 1f89986c777..68c8aadeb50 100644 --- a/Lib/test/test_operator.py +++ b/Lib/test/test_operator.py @@ -2,6 +2,7 @@ import inspect import pickle import sys +import weakref from decimal import Decimal from fractions import Fraction @@ -511,6 +512,21 @@ def return_arguments(self, *args, **kwds): f = operator.methodcaller('return_arguments', *many_positional_arguments, **many_kw_arguments) self.assertEqual(f(a), (many_positional_arguments, many_kw_arguments)) + def test_methodcaller_cyclic_gc(self): + # gh-156762: Check for undefined behavior on calling methodcaller_clear() + operator = self.module + + class C: + pass + + c = C() + ref = weakref.ref(c) + c.m = operator.methodcaller('foo', c) + del c + + support.gc_collect() + self.assertIsNone(ref()) + def test_inplace(self): operator = self.module class C(object): diff --git a/Lib/test/test_ordered_dict.py b/Lib/test/test_ordered_dict.py index 04942effa1f..ac24110cf63 100644 --- a/Lib/test/test_ordered_dict.py +++ b/Lib/test/test_ordered_dict.py @@ -74,6 +74,9 @@ def test_update(self): od.update(dict(pairs)) self.assertEqual(sorted(od.items()), pairs) # dict input od = OrderedDict() + od.update(frozendict(pairs)) + self.assertEqual(sorted(od.items()), pairs) # frozendict input + od = OrderedDict() od.update(**dict(pairs)) self.assertEqual(sorted(od.items()), pairs) # kwds input od = OrderedDict() @@ -288,9 +291,11 @@ def test_equality(self): pairs = pairs[2:] + pairs[:2] od2 = OrderedDict(pairs) self.assertNotEqual(od1, od2) # different order implies inequality - # comparison to regular dict is not order sensitive + # comparison to regular (frozen)dict is not order sensitive self.assertEqual(od1, dict(od2)) self.assertEqual(dict(od2), od1) + self.assertEqual(od1, frozendict(od2)) + self.assertEqual(frozendict(od1), od2) # different length implied inequality self.assertNotEqual(od1, OrderedDict(pairs[:-1])) @@ -698,6 +703,7 @@ def test_merge_operator(self): d |= list(b.items()) expected = OrderedDict({0: 0, 1: 1, 2: 2, 3: 3}) self.assertEqual(a | dict(b), expected) + self.assertEqual(a | frozendict(b), expected) self.assertEqual(a | b, expected) self.assertEqual(c, expected) self.assertEqual(d, expected) @@ -706,12 +712,17 @@ def test_merge_operator(self): c |= a expected = OrderedDict({1: 1, 2: 1, 3: 3, 0: 0}) self.assertEqual(dict(b) | a, expected) + self.assertEqual(frozendict(b) | a, expected) + self.assertEqual(a.__ror__(frozendict(b)), expected) self.assertEqual(b | a, expected) self.assertEqual(c, expected) self.assertIs(type(a | b), OrderedDict) self.assertIs(type(dict(a) | b), OrderedDict) + self.assertIs(type(frozendict(a) | b), frozendict) + self.assertIs(type(b.__ror__(frozendict(a))), OrderedDict) self.assertIs(type(a | dict(b)), OrderedDict) + self.assertIs(type(a | frozendict(b)), OrderedDict) expected = a.copy() a |= () diff --git a/Lib/test/test_pathlib/support/lexical_path.py b/Lib/test/test_pathlib/support/lexical_path.py index f29a521af9b..fd7fbf283a6 100644 --- a/Lib/test/test_pathlib/support/lexical_path.py +++ b/Lib/test/test_pathlib/support/lexical_path.py @@ -9,9 +9,10 @@ from . import is_pypi if is_pypi: - from pathlib_abc import _JoinablePath + from pathlib_abc import vfspath, _JoinablePath else: from pathlib.types import _JoinablePath + from pathlib._os import vfspath class LexicalPath(_JoinablePath): @@ -22,20 +23,20 @@ def __init__(self, *pathsegments): self._segments = pathsegments def __hash__(self): - return hash(str(self)) + return hash(vfspath(self)) def __eq__(self, other): if not isinstance(other, LexicalPath): return NotImplemented - return str(self) == str(other) + return vfspath(self) == vfspath(other) - def __str__(self): + def __vfspath__(self): if not self._segments: return '' return self.parser.join(*self._segments) def __repr__(self): - return f'{type(self).__name__}({str(self)!r})' + return f'{type(self).__name__}({vfspath(self)!r})' def with_segments(self, *pathsegments): return type(self)(*pathsegments) diff --git a/Lib/test/test_pathlib/support/local_path.py b/Lib/test/test_pathlib/support/local_path.py index d481fd45ead..ddfd6fd4195 100644 --- a/Lib/test/test_pathlib/support/local_path.py +++ b/Lib/test/test_pathlib/support/local_path.py @@ -97,7 +97,7 @@ class LocalPathInfo(PathInfo): __slots__ = ('_path', '_exists', '_is_dir', '_is_file', '_is_symlink') def __init__(self, path): - self._path = str(path) + self._path = os.fspath(path) self._exists = None self._is_dir = None self._is_file = None @@ -139,15 +139,13 @@ class ReadableLocalPath(_ReadablePath, LexicalPath): Simple implementation of a ReadablePath class for local filesystem paths. """ __slots__ = ('info',) + __fspath__ = LexicalPath.__vfspath__ def __init__(self, *pathsegments): super().__init__(*pathsegments) self.info = LocalPathInfo(self) - def __fspath__(self): - return str(self) - - def __open_rb__(self, buffering=-1): + def __open_reader__(self): return open(self, 'rb') def iterdir(self): @@ -163,12 +161,10 @@ class WritableLocalPath(_WritablePath, LexicalPath): """ __slots__ = () + __fspath__ = LexicalPath.__vfspath__ - def __fspath__(self): - return str(self) - - def __open_wb__(self, buffering=-1): - return open(self, 'wb') + def __open_writer__(self, mode): + return open(self, f'{mode}b') def mkdir(self, mode=0o777): os.mkdir(self, mode) diff --git a/Lib/test/test_pathlib/support/zip_path.py b/Lib/test/test_pathlib/support/zip_path.py index 2905260c9df..90b939b6a59 100644 --- a/Lib/test/test_pathlib/support/zip_path.py +++ b/Lib/test/test_pathlib/support/zip_path.py @@ -16,9 +16,10 @@ from . import is_pypi if is_pypi: - from pathlib_abc import PathInfo, _ReadablePath, _WritablePath + from pathlib_abc import vfspath, PathInfo, _ReadablePath, _WritablePath else: from pathlib.types import PathInfo, _ReadablePath, _WritablePath + from pathlib._os import vfspath class ZipPathGround: @@ -34,16 +35,16 @@ def teardown(self, root): root.zip_file.close() def create_file(self, path, data=b''): - path.zip_file.writestr(str(path), data) + path.zip_file.writestr(vfspath(path), data) def create_dir(self, path): - zip_info = zipfile.ZipInfo(str(path) + '/') + zip_info = zipfile.ZipInfo(vfspath(path) + '/') zip_info.external_attr |= stat.S_IFDIR << 16 zip_info.external_attr |= stat.FILE_ATTRIBUTE_DIRECTORY path.zip_file.writestr(zip_info, '') def create_symlink(self, path, target): - zip_info = zipfile.ZipInfo(str(path)) + zip_info = zipfile.ZipInfo(vfspath(path)) zip_info.external_attr = stat.S_IFLNK << 16 path.zip_file.writestr(zip_info, target.encode()) @@ -62,28 +63,28 @@ def create_hierarchy(self, p): self.create_symlink(p.joinpath('brokenLinkLoop'), 'brokenLinkLoop') def readtext(self, p): - with p.zip_file.open(str(p), 'r') as f: + with p.zip_file.open(vfspath(p), 'r') as f: f = io.TextIOWrapper(f, encoding='utf-8') return f.read() def readbytes(self, p): - with p.zip_file.open(str(p), 'r') as f: + with p.zip_file.open(vfspath(p), 'r') as f: return f.read() readlink = readtext def isdir(self, p): - path_str = str(p) + "/" + path_str = vfspath(p) + "/" return path_str in p.zip_file.NameToInfo def isfile(self, p): - info = p.zip_file.NameToInfo.get(str(p)) + info = p.zip_file.NameToInfo.get(vfspath(p)) if info is None: return False return not stat.S_ISLNK(info.external_attr >> 16) def islink(self, p): - info = p.zip_file.NameToInfo.get(str(p)) + info = p.zip_file.NameToInfo.get(vfspath(p)) if info is None: return False return stat.S_ISLNK(info.external_attr >> 16) @@ -240,20 +241,20 @@ def __init__(self, *pathsegments, zip_file): zip_file.filelist = ZipFileList(zip_file) def __hash__(self): - return hash((str(self), self.zip_file)) + return hash((vfspath(self), self.zip_file)) def __eq__(self, other): if not isinstance(other, ReadableZipPath): return NotImplemented - return str(self) == str(other) and self.zip_file is other.zip_file + return vfspath(self) == vfspath(other) and self.zip_file is other.zip_file - def __str__(self): + def __vfspath__(self): if not self._segments: return '' return self.parser.join(*self._segments) def __repr__(self): - return f'{type(self).__name__}({str(self)!r}, zip_file={self.zip_file!r})' + return f'{type(self).__name__}({vfspath(self)!r}, zip_file={self.zip_file!r})' def with_segments(self, *pathsegments): return type(self)(*pathsegments, zip_file=self.zip_file) @@ -261,15 +262,15 @@ def with_segments(self, *pathsegments): @property def info(self): tree = self.zip_file.filelist.tree - return tree.resolve(str(self), follow_symlinks=False) + return tree.resolve(vfspath(self), follow_symlinks=False) - def __open_rb__(self, buffering=-1): + def __open_reader__(self): info = self.info.resolve() if not info.exists(): raise FileNotFoundError(errno.ENOENT, "File not found", self) elif info.is_dir(): raise IsADirectoryError(errno.EISDIR, "Is a directory", self) - return self.zip_file.open(info.zip_info, 'r') + return self.zip_file.open(info.zip_info) def iterdir(self): info = self.info.resolve() @@ -301,36 +302,36 @@ def __init__(self, *pathsegments, zip_file): self.zip_file = zip_file def __hash__(self): - return hash((str(self), self.zip_file)) + return hash((vfspath(self), self.zip_file)) def __eq__(self, other): if not isinstance(other, WritableZipPath): return NotImplemented - return str(self) == str(other) and self.zip_file is other.zip_file + return vfspath(self) == vfspath(other) and self.zip_file is other.zip_file - def __str__(self): + def __vfspath__(self): if not self._segments: return '' return self.parser.join(*self._segments) def __repr__(self): - return f'{type(self).__name__}({str(self)!r}, zip_file={self.zip_file!r})' + return f'{type(self).__name__}({vfspath(self)!r}, zip_file={self.zip_file!r})' def with_segments(self, *pathsegments): return type(self)(*pathsegments, zip_file=self.zip_file) - def __open_wb__(self, buffering=-1): - return self.zip_file.open(str(self), 'w') + def __open_writer__(self, mode): + return self.zip_file.open(vfspath(self), mode) def mkdir(self, mode=0o777): - zinfo = zipfile.ZipInfo(str(self) + '/') + zinfo = zipfile.ZipInfo(vfspath(self) + '/') zinfo.external_attr |= stat.S_IFDIR << 16 zinfo.external_attr |= stat.FILE_ATTRIBUTE_DIRECTORY self.zip_file.writestr(zinfo, '') def symlink_to(self, target, target_is_directory=False): - zinfo = zipfile.ZipInfo(str(self)) + zinfo = zipfile.ZipInfo(vfspath(self)) zinfo.external_attr = stat.S_IFLNK << 16 if target_is_directory: zinfo.external_attr |= 0x10 - self.zip_file.writestr(zinfo, str(target)) + self.zip_file.writestr(zinfo, target) diff --git a/Lib/test/test_pathlib/test_join.py b/Lib/test/test_pathlib/test_join.py index 6b51a09e5ac..4630210e492 100644 --- a/Lib/test/test_pathlib/test_join.py +++ b/Lib/test/test_pathlib/test_join.py @@ -376,6 +376,61 @@ def test_with_suffix(self): self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.') self.assertRaises(TypeError, P('a/b').with_suffix, None) + def test_relative_to(self): + P = self.cls + p = P('a/b') + self.assertEqual(p.relative_to(P('')), P('a', 'b')) + self.assertEqual(p.relative_to(P('a')), P('b')) + self.assertEqual(p.relative_to(P('a/b')), P('')) + self.assertEqual(p.relative_to(P(''), walk_up=True), P('a', 'b')) + self.assertEqual(p.relative_to(P('a'), walk_up=True), P('b')) + self.assertEqual(p.relative_to(P('a/b'), walk_up=True), P('')) + self.assertEqual(p.relative_to(P('a/c'), walk_up=True), P('..', 'b')) + self.assertEqual(p.relative_to(P('a/b/c'), walk_up=True), P('..')) + self.assertEqual(p.relative_to(P('c'), walk_up=True), P('..', 'a', 'b')) + self.assertRaises(ValueError, p.relative_to, P('c')) + self.assertRaises(ValueError, p.relative_to, P('a/b/c')) + self.assertRaises(ValueError, p.relative_to, P('a/c')) + self.assertRaises(ValueError, p.relative_to, P('/a')) + self.assertRaises(ValueError, p.relative_to, P('../a')) + self.assertRaises(ValueError, p.relative_to, P('a/..')) + self.assertRaises(ValueError, p.relative_to, P('/a/..')) + self.assertRaises(ValueError, p.relative_to, P('/'), walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('/a'), walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('../a'), walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('a/..'), walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('/a/..'), walk_up=True) + class Q(self.cls): + __eq__ = object.__eq__ + __hash__ = object.__hash__ + q = Q('a/b') + self.assertTrue(q.relative_to(q)) + self.assertRaises(ValueError, q.relative_to, Q('')) + self.assertRaises(ValueError, q.relative_to, Q('a')) + self.assertRaises(ValueError, q.relative_to, Q('a'), walk_up=True) + self.assertRaises(ValueError, q.relative_to, Q('a/b')) + self.assertRaises(ValueError, q.relative_to, Q('c')) + + def test_is_relative_to(self): + P = self.cls + p = P('a/b') + self.assertTrue(p.is_relative_to(P(''))) + self.assertTrue(p.is_relative_to(P('a'))) + self.assertTrue(p.is_relative_to(P('a/b'))) + self.assertFalse(p.is_relative_to(P('c'))) + self.assertFalse(p.is_relative_to(P('a/b/c'))) + self.assertFalse(p.is_relative_to(P('a/c'))) + self.assertFalse(p.is_relative_to(P('/a'))) + class Q(self.cls): + __eq__ = object.__eq__ + __hash__ = object.__hash__ + q = Q('a/b') + self.assertTrue(q.is_relative_to(q)) + self.assertFalse(q.is_relative_to(Q(''))) + self.assertFalse(q.is_relative_to(Q('a'))) + self.assertFalse(q.is_relative_to(Q('a/b'))) + self.assertFalse(q.is_relative_to(Q('c'))) + class LexicalPathJoinTest(JoinTestBase, unittest.TestCase): cls = LexicalPath diff --git a/Lib/test/test_pathlib/test_join_windows.py b/Lib/test/test_pathlib/test_join_windows.py index 2cc634f25ef..5afefa61b58 100644 --- a/Lib/test/test_pathlib/test_join_windows.py +++ b/Lib/test/test_pathlib/test_join_windows.py @@ -8,6 +8,11 @@ from .support import is_pypi from .support.lexical_path import LexicalWindowsPath +if is_pypi: + from pathlib_abc import vfspath +else: + from pathlib._os import vfspath + class JoinTestBase: def test_join(self): @@ -70,17 +75,22 @@ def test_div(self): self.assertEqual(p / './dd:s', P(r'C:/a/b\./dd:s')) self.assertEqual(p / 'E:d:s', P('E:d:s')) - def test_str(self): + def test_vfspath(self): p = self.cls(r'a\b\c') - self.assertEqual(str(p), 'a\\b\\c') + self.assertEqual(vfspath(p), 'a\\b\\c') p = self.cls(r'c:\a\b\c') - self.assertEqual(str(p), 'c:\\a\\b\\c') + self.assertEqual(vfspath(p), 'c:\\a\\b\\c') p = self.cls('\\\\a\\b\\') - self.assertEqual(str(p), '\\\\a\\b\\') + self.assertEqual(vfspath(p), '\\\\a\\b\\') p = self.cls(r'\\a\b\c') - self.assertEqual(str(p), '\\\\a\\b\\c') + self.assertEqual(vfspath(p), '\\\\a\\b\\c') p = self.cls(r'\\a\b\c\d') - self.assertEqual(str(p), '\\\\a\\b\\c\\d') + self.assertEqual(vfspath(p), '\\\\a\\b\\c\\d') + + def test_invalid_vspath(self): + msg = "expected JoinablePath object, not NoneType" + with self.assertRaisesRegex(TypeError, msg): + vfspath(None) def test_parts(self): P = self.cls diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index a1ea69a6b90..aff66c8efed 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -20,6 +20,8 @@ from test.support import is_emscripten, is_wasi, is_wasm32 from test.support import infinite_recursion from test.support import os_helper +from test.support import requires_root_user +from test.support import requires_non_root_user from test.support.os_helper import TESTFN, FS_NONASCII, FakePath try: import fcntl @@ -35,11 +37,6 @@ posix = None -root_in_posix = False -if hasattr(os, 'geteuid'): - root_in_posix = (os.geteuid() == 0) - - def patch_replace(old_test): def new_replace(self, target): raise OSError(errno.EXDEV, "Cross-device link", self, target) @@ -84,7 +81,7 @@ def test_is_notimplemented(self): class LazyImportTest(unittest.TestCase): @cpython_only def test_lazy_import(self): - import_helper.ensure_lazy_imports("pathlib", {"shutil"}) + import_helper.ensure_lazy_imports("pathlib", {"glob", "shutil"}) # @@ -545,12 +542,6 @@ def test_with_stem_empty(self): self.assertRaises(ValueError, P('a/b').with_stem, '') self.assertRaises(ValueError, P('a/b').with_stem, '.') - def test_is_reserved_deprecated(self): - P = self.cls - p = P('a/b') - with self.assertWarns(DeprecationWarning): - p.is_reserved() - def test_full_match_case_sensitive(self): P = self.cls self.assertFalse(P('A.py').full_match('a.PY', case_sensitive=True)) @@ -1560,7 +1551,7 @@ def raiser(*args, **kwargs): self.assertRaises(FileNotFoundError, source.copy, target) @unittest.skipIf(sys.platform == "win32" or sys.platform == "wasi", "directories are always readable on Windows and WASI") - @unittest.skipIf(root_in_posix, "test fails with root privilege") + @requires_non_root_user def test_copy_dir_no_read_permission(self): base = self.cls(self.base) source = base / 'dirE' @@ -2033,7 +2024,7 @@ def test_owner(self): self.assertEqual(expected_name, p.owner()) @unittest.skipUnless(pwd, "the pwd module is needed for this test") - @unittest.skipUnless(root_in_posix, "test needs root privilege") + @requires_root_user def test_owner_no_follow_symlinks(self): all_users = [u.pw_uid for u in pwd.getpwall()] if len(all_users) < 2: @@ -2068,7 +2059,7 @@ def test_group(self): self.assertEqual(expected_name, p.group()) @unittest.skipUnless(grp, "the grp module is needed for this test") - @unittest.skipUnless(root_in_posix, "test needs root privilege") + @requires_root_user def test_group_no_follow_symlinks(self): all_groups = [g.gr_gid for g in grp.getgrall()] if len(all_groups) < 2: @@ -2501,6 +2492,116 @@ def my_mkdir(path, mode=0o777): self.assertNotIn(str(p12), concurrently_created) self.assertTrue(p.exists()) + @unittest.skipIf( + is_emscripten or is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_mkdir_parents_umask(self): + # Test that parent directories respect umask when parent_mode is not set + p = self.cls(self.base, 'umasktest', 'child') + self.assertFalse(p.exists()) + if os.name != 'nt': + with os_helper.temp_umask(0o002): + p.mkdir(0o755, parents=True) + self.assertTrue(p.exists()) + # Leaf directory gets the specified mode + self.assertEqual(p.stat().st_mode & 0o777, 0o755) + # Parent directory respects umask (0o777 & ~0o002 = 0o775) + self.assertEqual(p.parent.stat().st_mode & 0o777, 0o775) + + @unittest.skipIf( + is_emscripten or is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_mkdir_with_parent_mode(self): + # Test the parent_mode parameter + p = self.cls(self.base, 'newdirPM', 'subdirPM') + self.assertFalse(p.exists()) + if os.name != 'nt': + with os_helper.temp_umask(0o022): + # Specify different modes for parent and leaf directories + p.mkdir(0o755, parents=True, parent_mode=0o750) + self.assertTrue(p.exists()) + self.assertTrue(p.is_dir()) + # Leaf directory gets the mode parameter + self.assertEqual(p.stat().st_mode & 0o777, 0o755) + # Parent directory gets the parent_mode parameter + self.assertEqual(p.parent.stat().st_mode & 0o777, 0o750) + + @unittest.skipIf( + is_emscripten or is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_mkdir_parent_mode_deep_hierarchy(self): + # Test parent_mode with deep directory hierarchy + p = self.cls(self.base, 'level1PM', 'level2PM', 'level3PM') + self.assertFalse(p.exists()) + if os.name != 'nt': + with os_helper.temp_umask(0o022): + p.mkdir(0o755, parents=True, parent_mode=0o700) + self.assertTrue(p.exists()) + # Check that all parent directories have parent_mode + level1 = self.cls(self.base, 'level1PM') + level2 = level1 / 'level2PM' + self.assertEqual(level1.stat().st_mode & 0o777, 0o700) + self.assertEqual(level2.stat().st_mode & 0o777, 0o700) + # Leaf directory has the regular mode + self.assertEqual(p.stat().st_mode & 0o777, 0o755) + + @unittest.skipIf( + is_emscripten or is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_mkdir_parent_mode_combined_with_umask(self): + # parent_mode, like mode, is combined with the process umask; it does + # not bypass it. + p = self.cls(self.base, 'umaskPM', 'child') + self.assertFalse(p.exists()) + if os.name != 'nt': + with os_helper.temp_umask(0o022): + p.mkdir(0o777, parents=True, parent_mode=0o777) + self.assertTrue(p.exists()) + # 0o777 is masked down to 0o755 by the 0o022 umask, for both + # the leaf (mode) and the parent (parent_mode). + self.assertEqual(p.stat().st_mode & 0o777, 0o755) + self.assertEqual(p.parent.stat().st_mode & 0o777, 0o755) + + @unittest.skipIf( + is_emscripten or is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_mkdir_parent_mode_same_as_mode(self): + # Test setting parent_mode same as mode + p = self.cls(self.base, 'samedirPM', 'subdirPM') + self.assertFalse(p.exists()) + if os.name != 'nt': + with os_helper.temp_umask(0o022): + p.mkdir(0o705, parents=True, parent_mode=0o705) + self.assertTrue(p.exists()) + # Both directories should have the same mode + self.assertEqual(p.stat().st_mode & 0o777, 0o705) + self.assertEqual(p.parent.stat().st_mode & 0o777, 0o705) + @needs_symlinks def test_symlink_to(self): P = self.cls(self.base) @@ -2769,6 +2870,7 @@ def test_is_socket_true(self): if (isinstance(e, PermissionError) or "AF_UNIX path too long" in str(e)): self.skipTest("cannot bind Unix socket: " + str(e)) + raise self.assertTrue(P.is_socket()) self.assertFalse(P.is_fifo()) self.assertFalse(P.is_file()) diff --git a/Lib/test/test_pathlib/test_read.py b/Lib/test/test_pathlib/test_read.py index 482203c290a..51eede9d9eb 100644 --- a/Lib/test/test_pathlib/test_read.py +++ b/Lib/test/test_pathlib/test_read.py @@ -13,10 +13,10 @@ if is_pypi: from pathlib_abc import PathInfo, _ReadablePath - from pathlib_abc._os import magic_open + from pathlib_abc._os import vfsopen else: from pathlib.types import PathInfo, _ReadablePath - from pathlib._os import magic_open + from pathlib._os import vfsopen class ReadTestBase: @@ -32,10 +32,16 @@ def test_is_readable(self): def test_open_r(self): p = self.root / 'fileA' - with magic_open(p, 'r', encoding='utf-8') as f: + with vfsopen(p, 'r', encoding='utf-8') as f: self.assertIsInstance(f, io.TextIOBase) self.assertEqual(f.read(), 'this is file A\n') + def test_open_r_buffering_error(self): + p = self.root / 'fileA' + self.assertRaises(ValueError, vfsopen, p, 'r', buffering=0) + self.assertRaises(ValueError, vfsopen, p, 'r', buffering=1) + self.assertRaises(ValueError, vfsopen, p, 'r', buffering=1024) + @unittest.skipIf( not getattr(sys.flags, 'warn_default_encoding', 0), "Requires warn_default_encoding", @@ -43,17 +49,17 @@ def test_open_r(self): def test_open_r_encoding_warning(self): p = self.root / 'fileA' with self.assertWarns(EncodingWarning) as wc: - with magic_open(p, 'r'): + with vfsopen(p, 'r'): pass self.assertEqual(wc.filename, __file__) def test_open_rb(self): p = self.root / 'fileA' - with magic_open(p, 'rb') as f: + with vfsopen(p, 'rb') as f: self.assertEqual(f.read(), b'this is file A\n') - self.assertRaises(ValueError, magic_open, p, 'rb', encoding='utf8') - self.assertRaises(ValueError, magic_open, p, 'rb', errors='strict') - self.assertRaises(ValueError, magic_open, p, 'rb', newline='') + self.assertRaises(ValueError, vfsopen, p, 'rb', encoding='utf8') + self.assertRaises(ValueError, vfsopen, p, 'rb', errors='strict') + self.assertRaises(ValueError, vfsopen, p, 'rb', newline='') def test_read_bytes(self): p = self.root / 'fileA' @@ -323,6 +329,11 @@ def test_info_is_symlink(self): self.assertFalse((p / 'fileA\udfff').info.is_symlink()) self.assertFalse((p / 'fileA\x00').info.is_symlink()) + def test_invalid_mode(self): + p = self.root / 'fileA' + with self.assertRaisesRegex(ValueError, 'invalid mode'): + vfsopen(p, 'q') + class ZipPathReadTest(ReadTestBase, unittest.TestCase): ground = ZipPathGround(ReadableZipPath) diff --git a/Lib/test/test_pathlib/test_write.py b/Lib/test/test_pathlib/test_write.py index 15054e804ec..fa2a81a47b3 100644 --- a/Lib/test/test_pathlib/test_write.py +++ b/Lib/test/test_pathlib/test_write.py @@ -13,10 +13,10 @@ if is_pypi: from pathlib_abc import _WritablePath - from pathlib_abc._os import magic_open + from pathlib_abc._os import vfsopen else: from pathlib.types import _WritablePath - from pathlib._os import magic_open + from pathlib._os import vfsopen class WriteTestBase: @@ -31,11 +31,17 @@ def test_is_writable(self): def test_open_w(self): p = self.root / 'fileA' - with magic_open(p, 'w', encoding='utf-8') as f: + with vfsopen(p, 'w', encoding='utf-8') as f: self.assertIsInstance(f, io.TextIOBase) f.write('this is file A\n') self.assertEqual(self.ground.readtext(p), 'this is file A\n') + def test_open_w_buffering_error(self): + p = self.root / 'fileA' + self.assertRaises(ValueError, vfsopen, p, 'w', buffering=0) + self.assertRaises(ValueError, vfsopen, p, 'w', buffering=1) + self.assertRaises(ValueError, vfsopen, p, 'w', buffering=1024) + @unittest.skipIf( not getattr(sys.flags, 'warn_default_encoding', 0), "Requires warn_default_encoding", @@ -43,19 +49,19 @@ def test_open_w(self): def test_open_w_encoding_warning(self): p = self.root / 'fileA' with self.assertWarns(EncodingWarning) as wc: - with magic_open(p, 'w'): + with vfsopen(p, 'w'): pass self.assertEqual(wc.filename, __file__) def test_open_wb(self): p = self.root / 'fileA' - with magic_open(p, 'wb') as f: + with vfsopen(p, 'wb') as f: #self.assertIsInstance(f, io.BufferedWriter) f.write(b'this is file A\n') self.assertEqual(self.ground.readbytes(p), b'this is file A\n') - self.assertRaises(ValueError, magic_open, p, 'wb', encoding='utf8') - self.assertRaises(ValueError, magic_open, p, 'wb', errors='strict') - self.assertRaises(ValueError, magic_open, p, 'wb', newline='') + self.assertRaises(ValueError, vfsopen, p, 'wb', encoding='utf8') + self.assertRaises(ValueError, vfsopen, p, 'wb', errors='strict') + self.assertRaises(ValueError, vfsopen, p, 'wb', newline='') def test_write_bytes(self): p = self.root / 'fileA' diff --git a/Lib/test/test_patma.py b/Lib/test/test_patma.py index 5d0857b059e..0825d9d980c 100644 --- a/Lib/test/test_patma.py +++ b/Lib/test/test_patma.py @@ -2762,6 +2762,97 @@ def test_patma_255(self): self.assertEqual(y, 1) self.assertIs(z, x) + def test_patma_256(self): + x = 0 + match x: + case +0: + y = 0 + self.assertEqual(x, 0) + self.assertEqual(y, 0) + + def test_patma_257(self): + x = 0 + match x: + case +0.0: + y = 0 + self.assertEqual(x, 0) + self.assertEqual(y, 0) + + def test_patma_258(self): + x = 0 + match x: + case +0j: + y = 0 + self.assertEqual(x, 0) + self.assertEqual(y, 0) + + def test_patma_259(self): + x = 0 + match x: + case +0.0j: + y = 0 + self.assertEqual(x, 0) + self.assertEqual(y, 0) + + def test_patma_260(self): + x = 1 + match x: + case +1: + y = 0 + self.assertEqual(x, 1) + self.assertEqual(y, 0) + + def test_patma_261(self): + x = 1.5 + match x: + case +1.5: + y = 0 + self.assertEqual(x, 1.5) + self.assertEqual(y, 0) + + def test_patma_262(self): + x = 1j + match x: + case +1j: + y = 0 + self.assertEqual(x, 1j) + self.assertEqual(y, 0) + + def test_patma_263(self): + x = 1.5j + match x: + case +1.5j: + y = 0 + self.assertEqual(x, 1.5j) + self.assertEqual(y, 0) + + def test_patma_264(self): + x = 0.25 + 1.75j + match x: + case +0.25 + 1.75j: + y = 0 + self.assertEqual(x, 0.25 + 1.75j) + self.assertEqual(y, 0) + + def test_patma_265(self): + x = 0 + match x: + case +1e1000: + y = 0 + case 0: + y = 1 + self.assertEqual(x, 0) + self.assertEqual(y, 1) + + def test_patma_frozendict_class_self(self): + x = frozendict() + match x: + case frozendict(z): + y = 0 + self.assertEqual(x, frozendict()) + self.assertEqual(y, 0) + self.assertIs(z, x) + def test_patma_runtime_checkable_protocol(self): # Runtime-checkable protocol from typing import Protocol, runtime_checkable @@ -3230,6 +3321,34 @@ def test_mapping_pattern_duplicate_key_edge_case3(self): pass """) + def test_duplicate_sign_in_complex_1(self): + self.assert_syntax_error(""" + match ...: + case 0 ++ 0j: + pass + """) + + def test_duplicate_sign_in_complex_2(self): + self.assert_syntax_error(""" + match ...: + case 0 -+ 0j: + pass + """) + + def test_duplicate_sign_in_complex_3(self): + self.assert_syntax_error(""" + match ...: + case 0 +- 0j: + pass + """) + + def test_duplicate_sign_in_complex_4(self): + self.assert_syntax_error(""" + match ...: + case 0 -- 0j: + pass + """) + class TestTypeErrors(unittest.TestCase): def test_accepts_positional_subpatterns_0(self): diff --git a/Lib/test/test_pickle.py b/Lib/test/test_pickle.py index 1b85eceac17..21b981155dc 100644 --- a/Lib/test/test_pickle.py +++ b/Lib/test/test_pickle.py @@ -52,8 +52,6 @@ class PyPickleTests(AbstractPickleModuleTests, unittest.TestCase): Unpickler = pickle._Unpickler - - class PyUnpicklerTests(AbstractUnpickleTests, unittest.TestCase): unpickler = pickle._Unpickler @@ -61,6 +59,8 @@ class PyUnpicklerTests(AbstractUnpickleTests, unittest.TestCase): truncated_errors = (pickle.UnpicklingError, EOFError, AttributeError, ValueError, struct.error, IndexError, ImportError) + truncated_data_error = (EOFError, '') + size_overflow_error = (pickle.UnpicklingError, 'exceeds') def loads(self, buf, **kwds): f = io.BytesIO(buf) @@ -80,9 +80,6 @@ def dumps(self, arg, proto=None, **kwargs): return bytes(f.read()) - - - class PyPicklerTests(AbstractPickleTests, unittest.TestCase): pickler = pickle._Pickler @@ -100,9 +97,6 @@ def loads(self, buf, **kwds): u = self.unpickler(f, **kwds) return u.load() - - - @unittest.expectedFailure # TODO: RUSTPYTHON def test_c_methods(self): return super().test_c_methods() @@ -112,9 +106,6 @@ def test_deep_nested_struct_frozendict(self): return super().test_deep_nested_struct_frozendict() - - - class InMemoryPickleTests(AbstractPickleTests, AbstractUnpickleTests, BigmemPickleTests, unittest.TestCase): @@ -122,6 +113,8 @@ class InMemoryPickleTests(AbstractPickleTests, AbstractUnpickleTests, truncated_errors = (pickle.UnpicklingError, EOFError, AttributeError, ValueError, struct.error, IndexError, ImportError) + truncated_data_error = ((pickle.UnpicklingError, EOFError), '') + size_overflow_error = ((OverflowError, pickle.UnpicklingError), 'exceeds') def dumps(self, arg, protocol=None, **kwargs): return pickle.dumps(arg, protocol, **kwargs) @@ -133,17 +126,11 @@ def loads(self, buf, **kwds): test_find_class = None test_custom_find_class = None - - - @unittest.expectedFailure # TODO: RUSTPYTHON def test_c_methods(self): return super().test_c_methods() - - - class PersistentPicklerUnpicklerMixin(object): def dumps(self, arg, proto=None): @@ -404,6 +391,8 @@ class CUnpicklerTests(PyUnpicklerTests): unpickler = _pickle.Unpickler bad_stack_errors = (pickle.UnpicklingError,) truncated_errors = (pickle.UnpicklingError,) + truncated_data_error = (pickle.UnpicklingError, 'truncated') + size_overflow_error = (OverflowError, 'exceeds') class CPicklingErrorTests(PyPicklingErrorTests): pickler = _pickle.Pickler @@ -555,7 +544,7 @@ def test_pickler(self): 0) # Write buffer is cleared after every dump(). def test_unpickler(self): - basesize = support.calcobjsize('2P2n2P 2P2n2i5P 2P5n8P2n3i') + basesize = support.calcobjsize('2P2n3P 2P2n2i5P 2P5n8P2n3i') unpickler = _pickle.Unpickler P = struct.calcsize('P') # Size of memo table entry. n = struct.calcsize('n') # Size of mark table entry. diff --git a/Lib/test/test_picklebuffer.py b/Lib/test/test_picklebuffer.py index 748d6f5a5b1..fff695fa385 100644 --- a/Lib/test/test_picklebuffer.py +++ b/Lib/test/test_picklebuffer.py @@ -64,8 +64,7 @@ def test_release(self): # Idempotency pb.release() - # TODO: RUSTPYTHON; PickleBuffer is not traversed by the cycle collector - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON; PickleBuffer is not traversed by the cycle collector def test_cycle(self): b = B(b"foo") pb = PickleBuffer(b) diff --git a/Lib/test/test_pkgutil.py b/Lib/test/test_pkgutil.py index d4faaaeca00..4623b7eb443 100644 --- a/Lib/test/test_pkgutil.py +++ b/Lib/test/test_pkgutil.py @@ -1,3 +1,5 @@ +import logging +import logging.handlers from pathlib import Path from test.support.import_helper import unload from test.support.warnings_helper import check_warnings @@ -232,9 +234,6 @@ def test_walk_packages_raises_on_string_or_bytes_input(self): list(pkgutil.walk_packages(bytes_input)) def test_name_resolution(self): - import logging - import logging.handlers - success_cases = ( ('os', os), ('os.path', os.path), @@ -322,6 +321,53 @@ def test_name_resolution(self): with self.assertRaises(exc): pkgutil.resolve_name(s) + def test_name_resolution_strict(self): + # PEP 829: strict=True accepts only the pkg.mod:callable form + # (W(.W)*:W(.W)*) -- both the colon and the callable are required. + success_cases = ( + ('os.path:pathsep', os.path.pathsep), + ('logging.handlers:SysLogHandler', + logging.handlers.SysLogHandler), + ('logging.handlers:SysLogHandler.LOG_ALERT', + logging.handlers.SysLogHandler.LOG_ALERT), + ('builtins:int', int), + ('builtins:int.from_bytes', int.from_bytes), + ('os:path', os.path), + ) + + # All of these are accepted under strict=False but must be + # rejected under strict=True. + failure_cases = ( + 'os', # no colon (non-strict form) + 'os.path', # no colon + 'logging:', # colon, empty callable + 'os.foo:', # colon, empty callable + ':int', # empty package + 'os.path:join:extra', # extra colon + 'os.path.9abc:join', # invalid identifier in package + 'os.path:9abc', # invalid identifier in callable + '', # empty + '?abc:foo', # invalid character + ) + + for s, expected in success_cases: + with self.subTest(s=s): + self.assertEqual( + pkgutil.resolve_name(s, strict=True), expected) + + for s in failure_cases: + with self.subTest(s=s): + with self.assertRaises(ValueError): + pkgutil.resolve_name(s, strict=True) + + # Cache independence: a strict=True call must not poison + # strict=False (and vice versa). Exercise both orderings. + self.assertEqual( + pkgutil.resolve_name('os:path', strict=True), os.path) + self.assertEqual(pkgutil.resolve_name('os.path'), os.path) + self.assertEqual( + pkgutil.resolve_name('os:path', strict=True), os.path) + def test_name_resolution_import_rebinding(self): # The same data is also used for testing import in test_import and # mock.patch in test_unittest. diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index e879e48571f..63c130813ec 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -11,7 +11,7 @@ from unittest import mock from test import support -from test.support import os_helper +from test.support import os_helper, warnings_helper try: # Some of the iOS tests need ctypes to operate. @@ -133,6 +133,22 @@ def test_platform(self): for terse in (False, True): res = platform.platform(aliased, terse) + def test__platform(self): + for src, res in [ + ('foo bar', 'foo_bar'), + ( + '1/2\\3:4;5"6(7)8(7)6"5;4:3\\2/1', + '1-2-3-4-5-6-7-8-7-6-5-4-3-2-1' + ), + ('--', ''), + ('-f', '-f'), + ('-foo----', '-foo'), + ('--foo---', '-foo'), + ('---foo--', '-foo'), + ]: + with self.subTest(src=src): + self.assertEqual(platform._platform(src), res) + def test_system(self): res = platform.system() @@ -383,15 +399,6 @@ def raises_oserror(*a): finally: platform._uname_cache = None - def test_java_ver(self): - import re - msg = re.escape( - "'java_ver' is deprecated and slated for removal in Python 3.15" - ) - with self.assertWarnsRegex(DeprecationWarning, msg): - res = platform.java_ver() - self.assertEqual(len(res), 4) - @unittest.skipUnless(support.MS_WINDOWS, 'This test only makes sense on Windows') def test_win32_ver(self): release1, version1, csd1, ptype1 = 'a', 'b', 'c', 'd' @@ -458,7 +465,7 @@ def test_mac_ver(self): else: self.assertEqual(res[2], 'PowerPC') - + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless(sys.platform == 'darwin', "OSX only test") def test_mac_ver_with_fork(self): # Issue7895: platform.mac_ver() crashes when using fork without exec @@ -527,7 +534,7 @@ def test_ios_ver(self): def test_libc_ver(self): if support.is_emscripten: - assert platform.libc_ver() == ("emscripten", "4.0.12") + assert platform.libc_ver() == ("emscripten", "4.0.19") return # check that libc_ver(executable) doesn't raise an exception if os.path.isdir(sys.executable) and \ @@ -769,13 +776,14 @@ def invoke_platform(self, *flags): platform._main(args=flags) return output.getvalue() + @support.force_not_colorized def test_unknown_flag(self): + output = io.StringIO() with self.assertRaises(SystemExit): - output = io.StringIO() # suppress argparse error message with contextlib.redirect_stderr(output): _ = self.invoke_platform('--unknown') - self.assertStartsWith(output, "usage: ") + self.assertStartsWith(output.getvalue(), "usage: ") def test_invocation(self): flags = ( diff --git a/Lib/test/test_positional_only_arg.py b/Lib/test/test_positional_only_arg.py index eea0625012d..e412cb1d58d 100644 --- a/Lib/test/test_positional_only_arg.py +++ b/Lib/test/test_positional_only_arg.py @@ -37,8 +37,8 @@ def test_invalid_syntax_errors(self): check_syntax_error(self, "def f(/): pass") check_syntax_error(self, "def f(*, a, /): pass") check_syntax_error(self, "def f(*, /, a): pass") - check_syntax_error(self, "def f(a, /, a): pass", "duplicate argument 'a' in function definition") - check_syntax_error(self, "def f(a, /, *, a): pass", "duplicate argument 'a' in function definition") + check_syntax_error(self, "def f(a, /, a): pass", "duplicate parameter 'a' in function definition") + check_syntax_error(self, "def f(a, /, *, a): pass", "duplicate parameter 'a' in function definition") check_syntax_error(self, "def f(a, b/2, c): pass") check_syntax_error(self, "def f(a, /, c, /): pass") check_syntax_error(self, "def f(a, /, c, /, d): pass") @@ -59,8 +59,8 @@ def test_invalid_syntax_errors_async(self): check_syntax_error(self, "async def f(/): pass") check_syntax_error(self, "async def f(*, a, /): pass") check_syntax_error(self, "async def f(*, /, a): pass") - check_syntax_error(self, "async def f(a, /, a): pass", "duplicate argument 'a' in function definition") - check_syntax_error(self, "async def f(a, /, *, a): pass", "duplicate argument 'a' in function definition") + check_syntax_error(self, "async def f(a, /, a): pass", "duplicate parameter 'a' in function definition") + check_syntax_error(self, "async def f(a, /, *, a): pass", "duplicate parameter 'a' in function definition") check_syntax_error(self, "async def f(a, b/2, c): pass") check_syntax_error(self, "async def f(a, /, c, /): pass") check_syntax_error(self, "async def f(a, /, c, /, d): pass") @@ -247,8 +247,8 @@ def test_invalid_syntax_lambda(self): check_syntax_error(self, "lambda /: None") check_syntax_error(self, "lambda *, a, /: None") check_syntax_error(self, "lambda *, /, a: None") - check_syntax_error(self, "lambda a, /, a: None", "duplicate argument 'a' in function definition") - check_syntax_error(self, "lambda a, /, *, a: None", "duplicate argument 'a' in function definition") + check_syntax_error(self, "lambda a, /, a: None", "duplicate parameter 'a' in function definition") + check_syntax_error(self, "lambda a, /, *, a: None", "duplicate parameter 'a' in function definition") check_syntax_error(self, "lambda a, /, b, /: None") check_syntax_error(self, "lambda a, /, b, /, c: None") check_syntax_error(self, "lambda a, /, b, /, c, *, d: None") diff --git a/Lib/test/test_profile.py b/Lib/test/test_profile.py index 56ccc84a2e6..4e396191927 100644 --- a/Lib/test/test_profile.py +++ b/Lib/test/test_profile.py @@ -4,12 +4,16 @@ import pstats import unittest import os +import warnings from difflib import unified_diff from io import StringIO from test.support.os_helper import TESTFN, unlink, temp_dir, change_cwd from contextlib import contextmanager, redirect_stdout -import profile +# Suppress deprecation warning for profile module (PEP 799) +with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + import profile from test.profilee import testfunc, timer from test.support.script_helper import assert_python_failure, assert_python_ok diff --git a/Lib/test/test_profiling/__init__.py b/Lib/test/test_profiling/__init__.py new file mode 100644 index 00000000000..4b16ecc3115 --- /dev/null +++ b/Lib/test/test_profiling/__init__.py @@ -0,0 +1,5 @@ +import os +from test.support import load_package_tests + +def load_tests(*args): + return load_package_tests(os.path.dirname(__file__), *args) diff --git a/Lib/test/test_profiling/__main__.py b/Lib/test/test_profiling/__main__.py new file mode 100644 index 00000000000..40a23a297ec --- /dev/null +++ b/Lib/test/test_profiling/__main__.py @@ -0,0 +1,4 @@ +from . import load_tests +import unittest + +unittest.main() diff --git a/Lib/test/test_profiling/test_heatmap.py b/Lib/test/test_profiling/test_heatmap.py new file mode 100644 index 00000000000..ee27fdd3fa3 --- /dev/null +++ b/Lib/test/test_profiling/test_heatmap.py @@ -0,0 +1,862 @@ +"""Tests for the heatmap collector (profiling.sampling).""" + +import os +import shutil +import tempfile +import unittest +from collections import namedtuple +from pathlib import Path + +# Matches the C structseq LocationInfo from _remote_debugging +LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset']) + +from profiling.sampling.heatmap_collector import ( + HeatmapCollector, + get_python_path_info, + extract_module_name, +) + +from test.support import captured_stdout, captured_stderr + + +# ============================================================================= +# Unit Tests for Public Helper Functions +# ============================================================================= + +class TestPathInfoFunctions(unittest.TestCase): + """Test public helper functions for path information.""" + + def test_get_python_path_info_returns_dict(self): + """Test that get_python_path_info returns a dictionary with expected keys.""" + path_info = get_python_path_info() + + self.assertIsInstance(path_info, dict) + self.assertIn('stdlib', path_info) + self.assertIn('site_packages', path_info) + self.assertIn('sys_path', path_info) + + def test_get_python_path_info_stdlib_is_path_or_none(self): + """Test that stdlib is either a Path object or None.""" + path_info = get_python_path_info() + + if path_info['stdlib'] is not None: + self.assertIsInstance(path_info['stdlib'], Path) + + def test_get_python_path_info_site_packages_is_list(self): + """Test that site_packages is a list.""" + path_info = get_python_path_info() + + self.assertIsInstance(path_info['site_packages'], list) + for item in path_info['site_packages']: + self.assertIsInstance(item, Path) + + def test_get_python_path_info_sys_path_is_list(self): + """Test that sys_path is a list of Path objects.""" + path_info = get_python_path_info() + + self.assertIsInstance(path_info['sys_path'], list) + for item in path_info['sys_path']: + self.assertIsInstance(item, Path) + + def test_extract_module_name_with_none(self): + """Test extract_module_name with None filename.""" + path_info = get_python_path_info() + module_name, module_type = extract_module_name(None, path_info) + + self.assertEqual(module_name, 'unknown') + self.assertEqual(module_type, 'other') + + def test_extract_module_name_with_empty_string(self): + """Test extract_module_name with empty filename.""" + path_info = get_python_path_info() + module_name, module_type = extract_module_name('', path_info) + + self.assertEqual(module_name, 'unknown') + self.assertEqual(module_type, 'other') + + def test_extract_module_name_with_stdlib_file(self): + """Test extract_module_name with a standard library file.""" + path_info = get_python_path_info() + + # Use os module as a known stdlib file + if path_info['stdlib']: + stdlib_file = str(path_info['stdlib'] / 'os.py') + module_name, module_type = extract_module_name(stdlib_file, path_info) + + self.assertEqual(module_type, 'stdlib') + self.assertIn('os', module_name) + + def test_extract_module_name_with_project_file(self): + """Test extract_module_name with a project file.""" + path_info = get_python_path_info() + + # Create a mock project file path + if path_info['sys_path']: + # Use current directory as project path + project_file = '/some/project/path/mymodule.py' + module_name, module_type = extract_module_name(project_file, path_info) + + # Should classify as 'other' if not in sys.path + self.assertIn(module_type, ['project', 'other']) + + def test_extract_module_name_removes_py_extension(self): + """Test that .py extension is removed from module names.""" + path_info = get_python_path_info() + + # Test with a simple .py file + module_name, module_type = extract_module_name('/path/to/test.py', path_info) + + # Module name should not contain .py + self.assertNotIn('.py', module_name) + + def test_extract_module_name_with_special_files(self): + """Test extract_module_name with special filenames like .""" + path_info = get_python_path_info() + + special_files = ['', '', '[eval]'] + for special_file in special_files: + module_name, module_type = extract_module_name(special_file, path_info) + self.assertEqual(module_type, 'other') + + +# ============================================================================= +# Unit Tests for HeatmapCollector Public API +# ============================================================================= + +class TestHeatmapCollectorInit(unittest.TestCase): + """Test HeatmapCollector initialization.""" + + def test_init_creates_empty_data_structures(self): + """Test that __init__ creates empty data structures.""" + collector = HeatmapCollector(sample_interval_usec=100) + + # Check that data structures are initialized + self.assertIsInstance(collector.line_samples, dict) + self.assertIsInstance(collector.file_samples, dict) + self.assertIsInstance(collector.line_self_samples, dict) + self.assertIsInstance(collector.file_self_samples, dict) + self.assertIsInstance(collector.call_graph, dict) + self.assertIsInstance(collector.callers_graph, dict) + self.assertIsInstance(collector.function_definitions, dict) + self.assertIsInstance(collector.edge_samples, dict) + + # Check that they're empty + self.assertEqual(len(collector.line_samples), 0) + self.assertEqual(len(collector.file_samples), 0) + self.assertEqual(len(collector.line_self_samples), 0) + self.assertEqual(len(collector.file_self_samples), 0) + + def test_init_sets_total_samples_to_zero(self): + """Test that total samples starts at zero.""" + collector = HeatmapCollector(sample_interval_usec=100) + self.assertEqual(collector._total_samples, 0) + + def test_init_gets_path_info(self): + """Test that path info is retrieved during init.""" + collector = HeatmapCollector(sample_interval_usec=100) + self.assertIsNotNone(collector._path_info) + self.assertIn('stdlib', collector._path_info) + + +class TestHeatmapCollectorSetStats(unittest.TestCase): + """Test HeatmapCollector.set_stats() method.""" + + def test_set_stats_stores_all_parameters(self): + """Test that set_stats stores all provided parameters.""" + collector = HeatmapCollector(sample_interval_usec=100) + + collector.set_stats( + sample_interval_usec=500, + duration_sec=10.5, + sample_rate=99.5, + error_rate=0.5 + ) + + self.assertEqual(collector.stats['sample_interval_usec'], 500) + self.assertEqual(collector.stats['duration_sec'], 10.5) + self.assertEqual(collector.stats['sample_rate'], 99.5) + self.assertEqual(collector.stats['error_rate'], 0.5) + + def test_set_stats_includes_system_info(self): + """Test that set_stats includes Python and platform info.""" + collector = HeatmapCollector(sample_interval_usec=100) + collector.set_stats(sample_interval_usec=100, duration_sec=1.0, sample_rate=100.0) + + self.assertIn('python_version', collector.stats) + self.assertIn('python_implementation', collector.stats) + self.assertIn('platform', collector.stats) + + def test_set_stats_accepts_kwargs(self): + """Test that set_stats accepts additional kwargs.""" + collector = HeatmapCollector(sample_interval_usec=100) + + collector.set_stats( + sample_interval_usec=100, + duration_sec=1.0, + sample_rate=100.0, + custom_key='custom_value', + another_key=42 + ) + + self.assertEqual(collector.stats['custom_key'], 'custom_value') + self.assertEqual(collector.stats['another_key'], 42) + + def test_set_stats_with_none_error_rate(self): + """Test set_stats with error_rate=None.""" + collector = HeatmapCollector(sample_interval_usec=100) + collector.set_stats(sample_interval_usec=100, duration_sec=1.0, sample_rate=100.0) + + self.assertIn('error_rate', collector.stats) + self.assertIsNone(collector.stats['error_rate']) + + +class TestHeatmapCollectorProcessFrames(unittest.TestCase): + """Test HeatmapCollector.process_frames() method.""" + + def test_process_frames_increments_total_samples(self): + """Test that process_frames increments total samples count.""" + collector = HeatmapCollector(sample_interval_usec=100) + + initial_count = collector._total_samples + frames = [('file.py', (10, 10, -1, -1), 'func', None)] + collector.process_frames(frames, thread_id=1) + + self.assertEqual(collector._total_samples, initial_count + 1) + + def test_process_frames_records_line_samples(self): + """Test that process_frames records line samples.""" + collector = HeatmapCollector(sample_interval_usec=100) + + frames = [('test.py', (5, 5, -1, -1), 'test_func', None)] + collector.process_frames(frames, thread_id=1) + + # Check that line was recorded + self.assertIn(('test.py', 5), collector.line_samples) + self.assertEqual(collector.line_samples[('test.py', 5)], 1) + + def test_process_frames_records_multiple_lines_in_stack(self): + """Test that process_frames records all lines in a stack.""" + collector = HeatmapCollector(sample_interval_usec=100) + + frames = [ + ('file1.py', (10, 10, -1, -1), 'func1', None), + ('file2.py', (20, 20, -1, -1), 'func2', None), + ('file3.py', (30, 30, -1, -1), 'func3', None) + ] + collector.process_frames(frames, thread_id=1) + + # All frames should be recorded + self.assertIn(('file1.py', 10), collector.line_samples) + self.assertIn(('file2.py', 20), collector.line_samples) + self.assertIn(('file3.py', 30), collector.line_samples) + + def test_process_frames_distinguishes_self_samples(self): + """Test that process_frames distinguishes self (leaf) samples.""" + collector = HeatmapCollector(sample_interval_usec=100) + + frames = [ + ('leaf.py', (5, 5, -1, -1), 'leaf_func', None), # This is the leaf (top of stack) + ('caller.py', (10, 10, -1, -1), 'caller_func', None) + ] + collector.process_frames(frames, thread_id=1) + + # Leaf should have self sample + self.assertIn(('leaf.py', 5), collector.line_self_samples) + self.assertEqual(collector.line_self_samples[('leaf.py', 5)], 1) + + # Caller should NOT have self sample + self.assertNotIn(('caller.py', 10), collector.line_self_samples) + + def test_process_frames_accumulates_samples(self): + """Test that multiple calls accumulate samples.""" + collector = HeatmapCollector(sample_interval_usec=100) + + frames = [('file.py', (10, 10, -1, -1), 'func', None)] + + collector.process_frames(frames, thread_id=1) + collector.process_frames(frames, thread_id=1) + collector.process_frames(frames, thread_id=1) + + self.assertEqual(collector.line_samples[('file.py', 10)], 3) + self.assertEqual(collector._total_samples, 3) + + def test_process_frames_ignores_invalid_frames(self): + """Test that process_frames ignores invalid frames.""" + collector = HeatmapCollector(sample_interval_usec=100) + + # These should be ignored + invalid_frames = [ + ('', (1, 1, -1, -1), 'test', None), + ('[eval]', (1, 1, -1, -1), 'test', None), + ('', (1, 1, -1, -1), 'test', None), + (None, (1, 1, -1, -1), 'test', None), + ('__init__', (0, 0, -1, -1), 'test', None), # Special invalid frame + ] + + for frame in invalid_frames: + collector.process_frames([frame], thread_id=1) + + # Should not record these invalid frames + for frame in invalid_frames: + if frame[0]: + self.assertNotIn((frame[0], frame[1][0]), collector.line_samples) + + def test_process_frames_builds_call_graph(self): + """Test that process_frames builds call graph relationships.""" + collector = HeatmapCollector(sample_interval_usec=100) + + frames = [ + ('callee.py', (5, 5, -1, -1), 'callee_func', None), + ('caller.py', (10, 10, -1, -1), 'caller_func', None) + ] + collector.process_frames(frames, thread_id=1) + + # Check that call relationship was recorded + caller_key = ('caller.py', 10) + self.assertIn(caller_key, collector.call_graph) + + # Check callers graph + callee_key = ('callee.py', 5) + self.assertIn(callee_key, collector.callers_graph) + + def test_process_frames_records_function_definitions(self): + """Test that process_frames records function definition locations.""" + collector = HeatmapCollector(sample_interval_usec=100) + + frames = [('module.py', (42, 42, -1, -1), 'my_function', None)] + collector.process_frames(frames, thread_id=1) + + self.assertIn(('module.py', 'my_function'), collector.function_definitions) + self.assertEqual(collector.function_definitions[('module.py', 'my_function')], 42) + + def test_process_frames_tracks_edge_samples(self): + """Test that process_frames tracks edge sample counts.""" + collector = HeatmapCollector(sample_interval_usec=100) + + frames = [ + ('callee.py', (5, 5, -1, -1), 'callee', None), + ('caller.py', (10, 10, -1, -1), 'caller', None) + ] + + # Process same call stack multiple times + collector.process_frames(frames, thread_id=1) + collector.process_frames(frames, thread_id=1) + + # Check that edge count is tracked + self.assertGreater(len(collector.edge_samples), 0) + + def test_process_frames_weight_applies_to_identical_samples(self): + collector = HeatmapCollector(sample_interval_usec=100) + + frames = [ + ('callee.py', (5, 5, -1, -1), 'callee', None), + ('caller.py', (10, 10, -1, -1), 'caller', None), + ] + + collector.process_frames(frames, thread_id=1, weight=5) + + edge_key = (('caller.py', 10), ('callee.py', 5)) + self.assertEqual(collector.edge_samples[edge_key], 5) + self.assertEqual(collector.line_samples[('callee.py', 5)], 5) + self.assertEqual(collector.line_samples[('caller.py', 10)], 5) + + def test_process_frames_handles_empty_frames(self): + """Test that process_frames handles empty frame list.""" + collector = HeatmapCollector(sample_interval_usec=100) + + initial_count = collector._total_samples + collector.process_frames([], thread_id=1) + + # Should still increment total samples + self.assertEqual(collector._total_samples, initial_count + 1) + + def test_process_frames_with_file_samples_dict(self): + """Test that file_samples dict is properly populated.""" + collector = HeatmapCollector(sample_interval_usec=100) + + frames = [('test.py', (10, 10, -1, -1), 'func', None)] + collector.process_frames(frames, thread_id=1) + + self.assertIn('test.py', collector.file_samples) + self.assertIn(10, collector.file_samples['test.py']) + self.assertEqual(collector.file_samples['test.py'][10], 1) + + +def frame(filename, line, func): + """Create a frame tuple: (filename, location, funcname, opcode).""" + return (filename, (line, line, -1, -1), func, None) + + +class TestHeatmapCollectorNavigationButtons(unittest.TestCase): + """Test navigation button behavior for caller/callee relationships. + + For every call stack: + - Root frames (entry points): only DOWN arrow (callees) + - Middle frames: both UP and DOWN arrows + - Leaf frames: only UP arrow (callers) + """ + + def collect(self, *stacks): + """Create collector and process frame stacks.""" + collector = HeatmapCollector(sample_interval_usec=100) + for stack in stacks: + collector.process_frames(stack, thread_id=1) + return collector + + def test_deep_call_stack_relationships(self): + """Test root/middle/leaf navigation in a 5-level call stack.""" + # Stack: root -> A -> B -> C -> leaf + stack = [ + frame('leaf.py', 5, 'leaf'), + frame('c.py', 10, 'func_c'), + frame('b.py', 15, 'func_b'), + frame('a.py', 20, 'func_a'), + frame('root.py', 25, 'root'), + ] + c = self.collect(stack) + + # Root: only callees (no one calls it) + self.assertIn(('root.py', 25), c.call_graph) + self.assertNotIn(('root.py', 25), c.callers_graph) + + # Middle frames: both callers and callees + for key in [('a.py', 20), ('b.py', 15), ('c.py', 10)]: + self.assertIn(key, c.call_graph) + self.assertIn(key, c.callers_graph) + + # Leaf: only callers (doesn't call anyone) + self.assertNotIn(('leaf.py', 5), c.call_graph) + self.assertIn(('leaf.py', 5), c.callers_graph) + + def test_all_lines_in_function_see_callers(self): + """Test that interior lines map to their function for caller lookup.""" + # Same function sampled at different lines (12, 15, 10) + c = self.collect( + [frame('mod.py', 12, 'my_func'), frame('caller.py', 100, 'caller')], + [frame('mod.py', 15, 'my_func'), frame('caller.py', 100, 'caller')], + [frame('mod.py', 10, 'my_func'), frame('caller.py', 100, 'caller')], + ) + + # All lines should map to same function + for line in [10, 12, 15]: + self.assertEqual(c.line_to_function[('mod.py', line)], 'my_func') + + # Function definition line should have callers + func_def = c.function_definitions[('mod.py', 'my_func')] + self.assertIn(('mod.py', func_def), c.callers_graph) + + def test_multiple_callers_and_callees(self): + """Test multiple callers/callees are recorded correctly.""" + # Two callers -> target, and caller -> two callees + c = self.collect( + [frame('target.py', 10, 'target'), frame('caller1.py', 20, 'c1')], + [frame('target.py', 10, 'target'), frame('caller2.py', 30, 'c2')], + [frame('callee1.py', 5, 'f1'), frame('dispatcher.py', 40, 'dispatch')], + [frame('callee2.py', 6, 'f2'), frame('dispatcher.py', 40, 'dispatch')], + ) + + # Target has 2 callers + callers = c.callers_graph[('target.py', 10)] + self.assertEqual({x[0] for x in callers}, {'caller1.py', 'caller2.py'}) + + # Dispatcher has 2 callees + callees = c.call_graph[('dispatcher.py', 40)] + self.assertEqual({x[0] for x in callees}, {'callee1.py', 'callee2.py'}) + + def test_edge_samples_counted(self): + """Test that repeated calls accumulate edge counts.""" + stack = [frame('callee.py', 10, 'callee'), frame('caller.py', 20, 'caller')] + c = self.collect(stack, stack, stack) + + edge_key = (('caller.py', 20), ('callee.py', 10)) + self.assertEqual(c.edge_samples[edge_key], 3) + + +class TestHeatmapCollectorExport(unittest.TestCase): + """Test HeatmapCollector.export() method.""" + + def setUp(self): + """Set up test directory.""" + self.test_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.test_dir) + + def test_export_creates_output_directory(self): + """Test that export creates the output directory.""" + collector = HeatmapCollector(sample_interval_usec=100) + + # Add some data + frames = [('test.py', (10, 10, -1, -1), 'func', None)] + collector.process_frames(frames, thread_id=1) + + output_path = os.path.join(self.test_dir, 'heatmap_output') + + with captured_stdout(), captured_stderr(): + collector.export(output_path) + + self.assertTrue(os.path.exists(output_path)) + self.assertTrue(os.path.isdir(output_path)) + + def test_export_creates_index_html(self): + """Test that export creates index.html.""" + collector = HeatmapCollector(sample_interval_usec=100) + + frames = [('test.py', (10, 10, -1, -1), 'func', None)] + collector.process_frames(frames, thread_id=1) + + output_path = os.path.join(self.test_dir, 'heatmap_output') + + with captured_stdout(), captured_stderr(): + collector.export(output_path) + + index_path = os.path.join(output_path, 'index.html') + self.assertTrue(os.path.exists(index_path)) + + def test_export_creates_file_htmls(self): + """Test that export creates individual file HTMLs.""" + collector = HeatmapCollector(sample_interval_usec=100) + + frames = [('test.py', (10, 10, -1, -1), 'func', None)] + collector.process_frames(frames, thread_id=1) + + output_path = os.path.join(self.test_dir, 'heatmap_output') + + with captured_stdout(), captured_stderr(): + collector.export(output_path) + + # Check for file_XXXX.html files + html_files = [f for f in os.listdir(output_path) + if f.startswith('file_') and f.endswith('.html')] + self.assertGreater(len(html_files), 0) + + def test_export_with_empty_data(self): + """Test export with no data collected.""" + collector = HeatmapCollector(sample_interval_usec=100) + + output_path = os.path.join(self.test_dir, 'empty_output') + + # Should handle empty data gracefully + with captured_stdout(), captured_stderr(): + collector.export(output_path) + + def test_export_handles_html_suffix(self): + """Test that export handles .html suffix in output path.""" + collector = HeatmapCollector(sample_interval_usec=100) + + frames = [('test.py', (10, 10, -1, -1), 'func', None)] + collector.process_frames(frames, thread_id=1) + + # Path with .html suffix should be stripped + output_path = os.path.join(self.test_dir, 'output.html') + + with captured_stdout(), captured_stderr(): + collector.export(output_path) + + # Should create directory without .html + expected_dir = os.path.join(self.test_dir, 'output') + self.assertTrue(os.path.exists(expected_dir)) + + def test_export_with_multiple_files(self): + """Test export with multiple files.""" + collector = HeatmapCollector(sample_interval_usec=100) + + # Add samples for multiple files + collector.process_frames([('file1.py', (10, 10, -1, -1), 'func1', None)], thread_id=1) + collector.process_frames([('file2.py', (20, 20, -1, -1), 'func2', None)], thread_id=1) + collector.process_frames([('file3.py', (30, 30, -1, -1), 'func3', None)], thread_id=1) + + output_path = os.path.join(self.test_dir, 'multi_file') + + with captured_stdout(), captured_stderr(): + collector.export(output_path) + + # Should create HTML for each file + html_files = [f for f in os.listdir(output_path) + if f.startswith('file_') and f.endswith('.html')] + self.assertGreaterEqual(len(html_files), 3) + + def test_export_index_contains_file_references(self): + """Test that index.html contains references to profiled files.""" + collector = HeatmapCollector(sample_interval_usec=100) + collector.set_stats(sample_interval_usec=100, duration_sec=1.0, sample_rate=100.0) + + frames = [('mytest.py', (10, 10, -1, -1), 'my_func', None)] + collector.process_frames(frames, thread_id=1) + + output_path = os.path.join(self.test_dir, 'test_output') + + with captured_stdout(), captured_stderr(): + collector.export(output_path) + + index_path = os.path.join(output_path, 'index.html') + with open(index_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Should contain reference to the file + self.assertIn('mytest', content) + + def test_export_file_html_has_line_numbers(self): + """Test that exported file HTML contains line numbers.""" + collector = HeatmapCollector(sample_interval_usec=100) + + # Create a temporary Python file + temp_file = os.path.join(self.test_dir, 'temp_source.py') + with open(temp_file, 'w') as f: + f.write('def test():\n pass\n') + + frames = [(temp_file, (1, 1, -1, -1), 'test', None)] + collector.process_frames(frames, thread_id=1) + + output_path = os.path.join(self.test_dir, 'line_test') + + with captured_stdout(), captured_stderr(): + collector.export(output_path) + + # Find the generated file HTML + html_files = [f for f in os.listdir(output_path) + if f.startswith('file_') and f.endswith('.html')] + + if html_files: + with open(os.path.join(output_path, html_files[0]), 'r', encoding='utf-8') as f: + content = f.read() + + # Should have line-related content + self.assertIn('line-', content) + + +class MockFrameInfo: + """Mock FrameInfo for testing. + + Frame format: (filename, location, funcname, opcode) where: + - location is a tuple (lineno, end_lineno, col_offset, end_col_offset) + - opcode is an int or None + """ + + def __init__(self, filename, lineno, funcname, opcode=None): + self.filename = filename + self.funcname = funcname + self.opcode = opcode + self.location = (lineno, lineno, -1, -1) + + def __iter__(self): + return iter((self.filename, self.location, self.funcname, self.opcode)) + + def __getitem__(self, index): + return (self.filename, self.location, self.funcname, self.opcode)[index] + + def __len__(self): + return 4 + + def __repr__(self): + return f"MockFrameInfo('{self.filename}', {self.location}, '{self.funcname}', {self.opcode})" + + +class MockThreadInfo: + """Mock ThreadInfo for testing since the real one isn't accessible.""" + + def __init__(self, thread_id, frame_info, status=0): + self.thread_id = thread_id + self.frame_info = frame_info + self.status = status # Thread status flags + + def __repr__(self): + return f"MockThreadInfo(thread_id={self.thread_id}, frame_info={self.frame_info})" + + +class MockInterpreterInfo: + """Mock InterpreterInfo for testing since the real one isn't accessible.""" + + def __init__(self, interpreter_id, threads): + self.interpreter_id = interpreter_id + self.threads = threads + + def __repr__(self): + return f"MockInterpreterInfo(interpreter_id={self.interpreter_id}, threads={self.threads})" + + +class TestHeatmapCollector(unittest.TestCase): + """Tests for HeatmapCollector functionality.""" + + def test_heatmap_collector_basic(self): + """Test basic HeatmapCollector functionality.""" + collector = HeatmapCollector(sample_interval_usec=100) + + # Test empty state + self.assertEqual(len(collector.file_samples), 0) + self.assertEqual(len(collector.line_samples), 0) + + # Test collecting sample data - frames are 4-tuples: (filename, location, funcname, opcode) + test_frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo( + 1, + [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")], + )] + ) + ] + collector.collect(test_frames) + + # Should have recorded samples for the file + self.assertGreater(len(collector.line_samples), 0) + self.assertIn("file.py", collector.file_samples) + + # Check that line samples were recorded + file_data = collector.file_samples["file.py"] + self.assertGreater(len(file_data), 0) + + def test_heatmap_collector_export(self): + """Test heatmap HTML export functionality.""" + heatmap_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, heatmap_dir) + + collector = HeatmapCollector(sample_interval_usec=100) + + # Create test data with multiple files using MockFrameInfo + test_frames1 = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")])], + ) + ] + test_frames2 = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")])], + ) + ] # Same stack + test_frames3 = [ + MockInterpreterInfo(0, [MockThreadInfo(1, [MockFrameInfo("other.py", 5, "other_func")])]) + ] + + collector.collect(test_frames1) + collector.collect(test_frames2) + collector.collect(test_frames3) + + # Export heatmap + with (captured_stdout(), captured_stderr()): + collector.export(heatmap_dir) + + # Verify index.html was created + index_path = os.path.join(heatmap_dir, "index.html") + self.assertTrue(os.path.exists(index_path)) + self.assertGreater(os.path.getsize(index_path), 0) + + # Check index contains HTML content + with open(index_path, "r", encoding="utf-8") as f: + content = f.read() + + # Should be valid HTML + self.assertIn("", content.lower()) + self.assertIn(" Counter of linenos + self.assertIn("test.py", collector.file_samples) + # Line 10 should have samples + self.assertIn(10, collector.file_samples["test.py"]) + + def test_heatmap_with_none_location(self): + """Test HeatmapCollector handles None location gracefully.""" + collector = HeatmapCollector(sample_interval_usec=1000) + + # Synthetic frame with None location + frame = MockFrameInfo("~", 0, "") + frame.location = None + frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame])] + ) + ] + # Should not raise + collector.collect(frames) + + def test_heatmap_export_with_location_data(self): + """Test HeatmapCollector export includes location info.""" + tmp_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, tmp_dir) + + collector = HeatmapCollector(sample_interval_usec=1000) + + frame = MockFrameInfo("test.py", 10, "process") + frame.location = LocationInfo(10, 12, 0, 30) + frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame])] + ) + ] + collector.collect(frames) + + # Export should work + with (captured_stdout(), captured_stderr()): + collector.export(tmp_dir) + self.assertTrue(os.path.exists(os.path.join(tmp_dir, "index.html"))) + + def test_heatmap_collector_frame_format(self): + """Test HeatmapCollector with 4-element frame format.""" + collector = HeatmapCollector(sample_interval_usec=1000) + + frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("app.py", 100, "main", opcode=90), + MockFrameInfo("utils.py", 50, "helper", opcode=100), + MockFrameInfo("lib.py", 25, "process", opcode=None), + ], + ) + ], + ) + ] + collector.collect(frames) + + # Should have recorded data for the files + self.assertIn("app.py", collector.file_samples) + self.assertIn("utils.py", collector.file_samples) + self.assertIn("lib.py", collector.file_samples) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_profiling/test_sampling_profiler/__init__.py b/Lib/test/test_profiling/test_sampling_profiler/__init__.py new file mode 100644 index 00000000000..616ae5b49f0 --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/__init__.py @@ -0,0 +1,9 @@ +"""Tests for the sampling profiler (profiling.sampling).""" + +import os +from test.support import load_package_tests + + +def load_tests(*args): + """Load all tests from this subpackage.""" + return load_package_tests(os.path.dirname(__file__), *args) diff --git a/Lib/test/test_profiling/test_sampling_profiler/_live_collector_helpers.py b/Lib/test/test_profiling/test_sampling_profiler/_live_collector_helpers.py new file mode 100644 index 00000000000..2c672895099 --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/_live_collector_helpers.py @@ -0,0 +1,62 @@ +"""Common test helpers and mocks for live collector tests.""" + +from collections import namedtuple + +from profiling.sampling.constants import ( + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_ON_CPU, +) + + +# Matches the C structseq LocationInfo from _remote_debugging +LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset']) + + +class MockFrameInfo: + """Mock FrameInfo for testing. + + Frame format: (filename, location, funcname, opcode) where: + - location is a tuple (lineno, end_lineno, col_offset, end_col_offset) + - opcode is an int or None + """ + + def __init__(self, filename, lineno, funcname, opcode=None): + self.filename = filename + self.funcname = funcname + self.opcode = opcode + self.location = LocationInfo(lineno, lineno, -1, -1) + + def __iter__(self): + return iter((self.filename, self.location, self.funcname, self.opcode)) + + def __getitem__(self, index): + return (self.filename, self.location, self.funcname, self.opcode)[index] + + def __len__(self): + return 4 + + def __repr__(self): + return f"MockFrameInfo('{self.filename}', {self.location}, '{self.funcname}', {self.opcode})" + + +class MockThreadInfo: + """Mock ThreadInfo for testing.""" + + def __init__(self, thread_id, frame_info, status=THREAD_STATUS_HAS_GIL | THREAD_STATUS_ON_CPU): + self.thread_id = thread_id + self.frame_info = frame_info + self.status = status + + def __repr__(self): + return f"MockThreadInfo(thread_id={self.thread_id}, frame_info={self.frame_info}, status={self.status})" + + +class MockInterpreterInfo: + """Mock InterpreterInfo for testing.""" + + def __init__(self, interpreter_id, threads): + self.interpreter_id = interpreter_id + self.threads = threads + + def __repr__(self): + return f"MockInterpreterInfo(interpreter_id={self.interpreter_id}, threads={self.threads})" diff --git a/Lib/test/test_profiling/test_sampling_profiler/helpers.py b/Lib/test/test_profiling/test_sampling_profiler/helpers.py new file mode 100644 index 00000000000..b07776d415b --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/helpers.py @@ -0,0 +1,202 @@ +"""Helper utilities for sampling profiler tests.""" + +import contextlib +import socket +import subprocess +import sys +import unittest +from collections import namedtuple + +from test.support import SHORT_TIMEOUT +from test.support.socket_helper import find_unused_port +from test.support.os_helper import unlink + + +PROCESS_VM_READV_SUPPORTED = False + +try: + from _remote_debugging import PROCESS_VM_READV_SUPPORTED # noqa: F401 + import _remote_debugging # noqa: F401 +except ImportError: + raise unittest.SkipTest( + "Test only runs when _remote_debugging is available" + ) +else: + import profiling.sampling # noqa: F401 + from profiling.sampling.sample import SampleProfiler # noqa: F401 + + +skip_if_not_supported = unittest.skipIf( + ( + sys.platform != "darwin" + and sys.platform != "linux" + and sys.platform != "win32" + ), + "Test only runs on Linux, Windows and MacOS", +) + +SubprocessInfo = namedtuple("SubprocessInfo", ["process", "socket"]) + + +def _wait_for_signal(sock, expected_signals, timeout=SHORT_TIMEOUT): + """ + Wait for expected signal(s) from a socket with proper timeout and EOF handling. + + Args: + sock: Connected socket to read from + expected_signals: Single bytes object or list of bytes objects to wait for + timeout: Socket timeout in seconds + + Returns: + bytes: Complete accumulated response buffer + + Raises: + RuntimeError: If connection closed before signal received or timeout + """ + if isinstance(expected_signals, bytes): + expected_signals = [expected_signals] + + sock.settimeout(timeout) + buffer = b"" + + while True: + # Check if all expected signals are in buffer + if all(sig in buffer for sig in expected_signals): + return buffer + + try: + chunk = sock.recv(4096) + if not chunk: + raise RuntimeError( + f"Connection closed before receiving expected signals. " + f"Expected: {expected_signals}, Got: {buffer[-200:]!r}" + ) + buffer += chunk + except socket.timeout: + raise RuntimeError( + f"Timeout waiting for signals. " + f"Expected: {expected_signals}, Got: {buffer[-200:]!r}" + ) from None + except OSError as e: + raise RuntimeError( + f"Socket error while waiting for signals: {e}. " + f"Expected: {expected_signals}, Got: {buffer[-200:]!r}" + ) from None + + +def _cleanup_sockets(*sockets): + """Safely close multiple sockets, ignoring errors.""" + for sock in sockets: + if sock is not None: + try: + sock.close() + except OSError: + pass + + +def _cleanup_process(proc, timeout=SHORT_TIMEOUT): + """Terminate a process gracefully, escalating to kill if needed.""" + if proc.poll() is not None: + return + proc.terminate() + try: + proc.wait(timeout=timeout) + return + except subprocess.TimeoutExpired: + pass + proc.kill() + try: + proc.wait(timeout=timeout) + except subprocess.TimeoutExpired: + pass # Process refuses to die, nothing more we can do + + +@contextlib.contextmanager +def test_subprocess(script, wait_for_working=False): + """Context manager to create a test subprocess with socket synchronization. + + Args: + script: Python code to execute in the subprocess. If wait_for_working + is True, script should send b"working" after starting work. + wait_for_working: If True, wait for both "ready" and "working" signals. + Default False for backward compatibility. + + Yields: + SubprocessInfo: Named tuple with process and socket objects + """ + # Find an unused port for socket communication + port = find_unused_port() + + # Inject socket connection code at the beginning of the script + socket_code = f""" +import socket +_test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +_test_sock.connect(('localhost', {port})) +_test_sock.sendall(b"ready") +""" + + # Combine socket code with user script + full_script = socket_code + script + + # Create server socket to wait for process to be ready + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(("localhost", port)) + server_socket.settimeout(SHORT_TIMEOUT) + server_socket.listen(1) + + proc = subprocess.Popen( + [sys.executable, "-c", full_script], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + client_socket = None + try: + # Wait for process to connect and send ready signal + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None + + # Wait for ready signal, and optionally working signal + if wait_for_working: + _wait_for_signal(client_socket, [b"ready", b"working"]) + else: + _wait_for_signal(client_socket, b"ready") + + yield SubprocessInfo(proc, client_socket) + finally: + _cleanup_sockets(client_socket, server_socket) + _cleanup_process(proc) + + +def close_and_unlink(file): + """Close a file and unlink it from the filesystem.""" + file.close() + unlink(file.name) + + +def jsonl_tables(records): + """Extract the canonical sections of a parsed JSONL profile. + + Returns ``(meta, str_defs, frame_defs, agg, end)`` where ``str_defs`` is a + ``{str_id: value}`` dict, ``frame_defs`` is a flat list of all frame + definitions across chunks, and ``agg`` is the first agg record (sufficient + for tests that only emit one chunk). + """ + meta = next(record for record in records if record["type"] == "meta") + end = next(record for record in records if record["type"] == "end") + agg = next(record for record in records if record["type"] == "agg") + str_defs = { + item["str_id"]: item["value"] + for record in records + if record["type"] == "string_table" + for item in record["strings"] + } + frame_defs = [ + item + for record in records + if record["type"] == "frame_table" + for item in record["frames"] + ] + return meta, str_defs, frame_defs, agg, end diff --git a/Lib/test/test_profiling/test_sampling_profiler/mocks.py b/Lib/test/test_profiling/test_sampling_profiler/mocks.py new file mode 100644 index 00000000000..6ac2d08e898 --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/mocks.py @@ -0,0 +1,112 @@ +"""Mock classes for sampling profiler tests.""" + +from collections import namedtuple + +# Matches the C structseq LocationInfo from _remote_debugging +LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset']) + + +class MockFrameInfo: + """Mock FrameInfo for testing. + + Frame format: (filename, location, funcname, opcode) where: + - location is a tuple (lineno, end_lineno, col_offset, end_col_offset) + - opcode is an int or None + """ + + def __init__(self, filename, lineno, funcname, opcode=None): + self.filename = filename + self.funcname = funcname + self.opcode = opcode + self.location = LocationInfo(lineno, lineno, -1, -1) + + def __iter__(self): + return iter((self.filename, self.location, self.funcname, self.opcode)) + + def __getitem__(self, index): + return (self.filename, self.location, self.funcname, self.opcode)[index] + + def __len__(self): + return 4 + + def __repr__(self): + return f"MockFrameInfo('{self.filename}', {self.location}, '{self.funcname}', {self.opcode})" + + +class MockThreadInfo: + """Mock ThreadInfo for testing since the real one isn't accessible.""" + + def __init__( + self, thread_id, frame_info, status=0 + ): # Default to THREAD_STATE_RUNNING (0) + self.thread_id = thread_id + self.frame_info = frame_info + self.status = status + + def __repr__(self): + return f"MockThreadInfo(thread_id={self.thread_id}, frame_info={self.frame_info}, status={self.status})" + + +class MockInterpreterInfo: + """Mock InterpreterInfo for testing since the real one isn't accessible.""" + + def __init__(self, interpreter_id, threads): + self.interpreter_id = interpreter_id + self.threads = threads + + def __repr__(self): + return f"MockInterpreterInfo(interpreter_id={self.interpreter_id}, threads={self.threads})" + + +class MockCoroInfo: + """Mock CoroInfo for testing async tasks.""" + + def __init__(self, task_name, call_stack): + self.task_name = task_name # In reality, this is the parent task ID + self.call_stack = call_stack + + def __repr__(self): + return f"MockCoroInfo(task_name={self.task_name}, call_stack={self.call_stack})" + + +class MockTaskInfo: + """Mock TaskInfo for testing async tasks.""" + + def __init__(self, task_id, task_name, coroutine_stack, awaited_by=None): + self.task_id = task_id + self.task_name = task_name + self.coroutine_stack = coroutine_stack # List of CoroInfo objects + self.awaited_by = awaited_by or [] # List of CoroInfo objects (parents) + + def __repr__(self): + return f"MockTaskInfo(task_id={self.task_id}, task_name={self.task_name})" + + +class MockAwaitedInfo: + """Mock AwaitedInfo for testing async tasks.""" + + def __init__(self, thread_id, awaited_by): + self.thread_id = thread_id + self.awaited_by = awaited_by # List of TaskInfo objects + + def __repr__(self): + return f"MockAwaitedInfo(thread_id={self.thread_id}, awaited_by={len(self.awaited_by)} tasks)" + + +def make_diff_collector_with_mock_baseline(baseline_samples): + """Create a DiffFlamegraphCollector with baseline injected directly, + skipping the binary round-trip that _load_baseline normally does.""" + from profiling.sampling.stack_collector import ( + DiffFlamegraphCollector, + FlamegraphCollector, + ) + + baseline = FlamegraphCollector(1000) + for sample in baseline_samples: + baseline.collect(sample) + + # Path is unused since we inject _baseline_collector directly; + # use __file__ as a dummy path that passes the existence check. + diff = DiffFlamegraphCollector(1000, baseline_binary_path=__file__) + diff._baseline_collector = baseline + return diff diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_advanced.py b/Lib/test/test_profiling/test_sampling_profiler/test_advanced.py new file mode 100644 index 00000000000..11b1ad84242 --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_advanced.py @@ -0,0 +1,236 @@ +"""Tests for advanced sampling profiler features (GC tracking, native frames, ProcessPoolExecutor support).""" + +import io +import os +import subprocess +import tempfile +import unittest +from unittest import mock + +try: + import _remote_debugging # noqa: F401 + import profiling.sampling + import profiling.sampling.sample + from profiling.sampling.pstats_collector import PstatsCollector + from profiling.sampling.stack_collector import CollapsedStackCollector +except ImportError: + raise unittest.SkipTest( + "Test only runs when _remote_debugging is available" + ) + +from test.support import ( + SHORT_TIMEOUT, + SuppressCrashReport, + os_helper, + requires_remote_subprocess_debugging, + script_helper, +) + +from .helpers import close_and_unlink, test_subprocess + + +@requires_remote_subprocess_debugging() +class TestGCFrameTracking(unittest.TestCase): + """Tests for GC frame tracking in the sampling profiler.""" + + @classmethod + def setUpClass(cls): + """Create a static test script with GC frames and CPU-intensive work.""" + cls.gc_test_script = ''' +import gc + +class ExpensiveGarbage: + def __init__(self): + self.cycle = self + + def __del__(self): + result = 0 + for i in range(100000): + result += i * i + if i % 1000 == 0: + result = result % 1000000 + +_test_sock.sendall(b"working") +while True: + ExpensiveGarbage() + gc.collect() +''' + + def test_gc_frames_enabled(self): + """Test that GC frames appear when gc tracking is enabled.""" + with ( + test_subprocess(self.gc_test_script, wait_for_working=True) as subproc, + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = PstatsCollector(sample_interval_usec=5000, skip_idle=False) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=1, + native=False, + gc=True, + ) + collector.print_stats(show_summary=False) + + output = captured_output.getvalue() + + # Should capture samples + self.assertIn("Captured", output) + self.assertIn("samples", output) + + # GC frames should be present + self.assertIn("", output) + + def test_gc_frames_disabled(self): + """Test that GC frames do not appear when gc tracking is disabled.""" + with ( + test_subprocess(self.gc_test_script, wait_for_working=True) as subproc, + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = PstatsCollector(sample_interval_usec=5000, skip_idle=False) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=1, + native=False, + gc=False, + ) + collector.print_stats(show_summary=False) + + output = captured_output.getvalue() + + # Should capture samples + self.assertIn("Captured", output) + self.assertIn("samples", output) + + # GC frames should NOT be present + self.assertNotIn("", output) + + +@requires_remote_subprocess_debugging() +class TestNativeFrameTracking(unittest.TestCase): + """Tests for native frame tracking in the sampling profiler.""" + + @classmethod + def setUpClass(cls): + """Create a static test script with native frames and CPU-intensive work.""" + cls.native_test_script = """ +import operator + +def inner(): + for _ in range(1_000_0000): + pass + +_test_sock.sendall(b"working") +while True: + operator.call(inner) +""" + + def test_native_frames_enabled(self): + """Test that native frames appear when native tracking is enabled.""" + collapsed_file = tempfile.NamedTemporaryFile( + suffix=".txt", delete=False + ) + self.addCleanup(close_and_unlink, collapsed_file) + + with test_subprocess(self.native_test_script, wait_for_working=True) as subproc: + with ( + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = CollapsedStackCollector(1000, skip_idle=False) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=1, + native=True, + ) + collector.export(collapsed_file.name) + + # Verify file was created and contains valid data + self.assertTrue(os.path.exists(collapsed_file.name)) + self.assertGreater(os.path.getsize(collapsed_file.name), 0) + + # Check file format + with open(collapsed_file.name, "r") as f: + content = f.read() + + lines = content.strip().split("\n") + self.assertGreater(len(lines), 0) + + stacks = [line.rsplit(" ", 1)[0] for line in lines] + + # Most samples should have native code in the middle of the stack: + self.assertTrue(any(";;" in stack for stack in stacks)) + + # No samples should have native code at the top of the stack: + self.assertFalse(any(stack.endswith(";") for stack in stacks)) + + def test_native_frames_disabled(self): + """Test that native frames do not appear when native tracking is disabled.""" + with ( + test_subprocess(self.native_test_script, wait_for_working=True) as subproc, + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = PstatsCollector(sample_interval_usec=5000, skip_idle=False) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=1, + ) + collector.print_stats(show_summary=False) + output = captured_output.getvalue() + # Native frames should NOT be present: + self.assertNotIn("", output) + + +@requires_remote_subprocess_debugging() +class TestProcessPoolExecutorSupport(unittest.TestCase): + """ + Test that ProcessPoolExecutor works correctly with profiling.sampling. + """ + + def test_process_pool_executor_pickle(self): + # gh-140729: test use ProcessPoolExecutor.map() can sampling + test_script = """ +import concurrent.futures + +def worker(x): + return x * 2 + +if __name__ == "__main__": + with concurrent.futures.ProcessPoolExecutor() as executor: + results = list(executor.map(worker, [1, 2, 3])) + print(f"Results: {results}") +""" + with os_helper.temp_dir() as temp_dir: + script = script_helper.make_script( + temp_dir, "test_process_pool_executor_pickle", test_script + ) + with SuppressCrashReport(): + with script_helper.spawn_python( + "-m", + "profiling.sampling", + "run", + "-d", + "5", + "-r", + "10", + script, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) as proc: + try: + stdout, stderr = proc.communicate( + timeout=SHORT_TIMEOUT + ) + except subprocess.TimeoutExpired: + proc.kill() + stdout, stderr = proc.communicate() + + self.assertIn("Results: [2, 4, 6]", stdout) + self.assertNotIn("Can't pickle", stderr) diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_async.py b/Lib/test/test_profiling/test_sampling_profiler/test_async.py new file mode 100644 index 00000000000..1f5685717b6 --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_async.py @@ -0,0 +1,787 @@ +"""Tests for async stack reconstruction in the sampling profiler. + +Each test covers a distinct algorithm path or edge case: +1. Graph building: _build_task_graph() +2. Leaf identification: _find_leaf_tasks() +3. Stack traversal: _build_linear_stacks() with BFS +""" + +import inspect +import unittest + +try: + import _remote_debugging # noqa: F401 + from profiling.sampling.pstats_collector import PstatsCollector + from profiling.sampling.stack_collector import FlamegraphCollector + from profiling.sampling.sample import sample, sample_live, SampleProfiler +except ImportError: + raise unittest.SkipTest( + "Test only runs when _remote_debugging is available" + ) + +from .mocks import MockFrameInfo, MockCoroInfo, MockTaskInfo, MockAwaitedInfo + + +class TestAsyncStackReconstruction(unittest.TestCase): + """Test async task tree linear stack reconstruction algorithm.""" + + def test_empty_input(self): + """Test _build_task_graph with empty awaited_info_list.""" + collector = PstatsCollector(sample_interval_usec=1000) + stacks = list(collector._iter_async_frames([])) + self.assertEqual(len(stacks), 0) + + def test_single_root_task(self): + """Test _find_leaf_tasks: root task with no parents is its own leaf.""" + collector = PstatsCollector(sample_interval_usec=1000) + + root = MockTaskInfo( + task_id=123, + task_name="Task-1", + coroutine_stack=[ + MockCoroInfo( + task_name="Task-1", + call_stack=[MockFrameInfo("main.py", 10, "main")] + ) + ], + awaited_by=[] + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=100, awaited_by=[root])] + stacks = list(collector._iter_async_frames(awaited_info_list)) + + # Single root is both leaf and root + self.assertEqual(len(stacks), 1) + frames, thread_id, leaf_id = stacks[0] + self.assertEqual(leaf_id, 123) + self.assertEqual(thread_id, 100) + + def test_parent_child_chain(self): + """Test _build_linear_stacks: BFS follows parent links from leaf to root. + + Task graph: + + Parent (id=1) + | + Child (id=2) + """ + collector = PstatsCollector(sample_interval_usec=1000) + + child = MockTaskInfo( + task_id=2, + task_name="Child", + coroutine_stack=[ + MockCoroInfo(task_name="Child", call_stack=[MockFrameInfo("c.py", 5, "child_fn")]) + ], + awaited_by=[ + MockCoroInfo(task_name=1, call_stack=[MockFrameInfo("p.py", 10, "parent_await")]) + ] + ) + + parent = MockTaskInfo( + task_id=1, + task_name="Parent", + coroutine_stack=[ + MockCoroInfo(task_name="Parent", call_stack=[MockFrameInfo("p.py", 15, "parent_fn")]) + ], + awaited_by=[] + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=200, awaited_by=[child, parent])] + stacks = list(collector._iter_async_frames(awaited_info_list)) + + # Leaf is child, traverses to parent + self.assertEqual(len(stacks), 1) + frames, thread_id, leaf_id = stacks[0] + self.assertEqual(leaf_id, 2) + + # Verify both child and parent frames present + func_names = [f.funcname for f in frames] + self.assertIn("child_fn", func_names) + self.assertIn("parent_fn", func_names) + + def test_multiple_leaf_tasks(self): + """Test _find_leaf_tasks: identifies multiple leaves correctly. + + Task graph (fan-out from root): + + Root (id=1) + / \ + Leaf1 (id=10) Leaf2 (id=20) + + Expected: 2 stacks (one for each leaf). + """ + collector = PstatsCollector(sample_interval_usec=1000) + leaf1 = MockTaskInfo( + task_id=10, + task_name="Leaf1", + coroutine_stack=[MockCoroInfo(task_name="Leaf1", call_stack=[MockFrameInfo("l1.py", 1, "f1")])], + awaited_by=[MockCoroInfo(task_name=1, call_stack=[MockFrameInfo("r.py", 5, "root")])] + ) + + leaf2 = MockTaskInfo( + task_id=20, + task_name="Leaf2", + coroutine_stack=[MockCoroInfo(task_name="Leaf2", call_stack=[MockFrameInfo("l2.py", 2, "f2")])], + awaited_by=[MockCoroInfo(task_name=1, call_stack=[MockFrameInfo("r.py", 5, "root")])] + ) + + root = MockTaskInfo( + task_id=1, + task_name="Root", + coroutine_stack=[MockCoroInfo(task_name="Root", call_stack=[MockFrameInfo("r.py", 10, "main")])], + awaited_by=[] + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=300, awaited_by=[leaf1, leaf2, root])] + stacks = list(collector._iter_async_frames(awaited_info_list)) + + # Two leaves = two stacks + self.assertEqual(len(stacks), 2) + leaf_ids = {leaf_id for _, _, leaf_id in stacks} + self.assertEqual(leaf_ids, {10, 20}) + + def test_cycle_detection(self): + """Test _build_linear_stacks: cycle detection prevents infinite loops. + + Task graph (cyclic dependency): + + A (id=1) <---> B (id=2) + + Neither task is a leaf (both have parents), so no stacks are produced. + """ + collector = PstatsCollector(sample_interval_usec=1000) + task_a = MockTaskInfo( + task_id=1, + task_name="A", + coroutine_stack=[MockCoroInfo(task_name="A", call_stack=[MockFrameInfo("a.py", 1, "a")])], + awaited_by=[MockCoroInfo(task_name=2, call_stack=[MockFrameInfo("b.py", 5, "b")])] + ) + + task_b = MockTaskInfo( + task_id=2, + task_name="B", + coroutine_stack=[MockCoroInfo(task_name="B", call_stack=[MockFrameInfo("b.py", 10, "b")])], + awaited_by=[MockCoroInfo(task_name=1, call_stack=[MockFrameInfo("a.py", 15, "a")])] + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=400, awaited_by=[task_a, task_b])] + stacks = list(collector._iter_async_frames(awaited_info_list)) + + # No leaves (both have parents), should return empty + self.assertEqual(len(stacks), 0) + + def test_orphaned_parent_reference(self): + """Test _build_linear_stacks: handles parent ID not in task_map.""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Task references non-existent parent + orphan = MockTaskInfo( + task_id=5, + task_name="Orphan", + coroutine_stack=[MockCoroInfo(task_name="Orphan", call_stack=[MockFrameInfo("o.py", 1, "orphan")])], + awaited_by=[MockCoroInfo(task_name=999, call_stack=[])] # 999 doesn't exist + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=500, awaited_by=[orphan])] + stacks = list(collector._iter_async_frames(awaited_info_list)) + + # Stops at missing parent, yields what it has + self.assertEqual(len(stacks), 1) + frames, _, leaf_id = stacks[0] + self.assertEqual(leaf_id, 5) + + def test_multiple_coroutines_per_task(self): + """Test _build_linear_stacks: collects frames from all coroutines in task.""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Task with multiple coroutines (e.g., nested async generators) + task = MockTaskInfo( + task_id=7, + task_name="Multi", + coroutine_stack=[ + MockCoroInfo(task_name="Multi", call_stack=[MockFrameInfo("g.py", 5, "gen1")]), + MockCoroInfo(task_name="Multi", call_stack=[MockFrameInfo("g.py", 10, "gen2")]), + ], + awaited_by=[] + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=600, awaited_by=[task])] + stacks = list(collector._iter_async_frames(awaited_info_list)) + + self.assertEqual(len(stacks), 1) + frames, _, _ = stacks[0] + + # Both coroutine frames should be present + func_names = [f.funcname for f in frames] + self.assertIn("gen1", func_names) + self.assertIn("gen2", func_names) + + def test_multiple_threads(self): + """Test _build_task_graph: handles multiple AwaitedInfo (different threads).""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Two threads with separate task trees + thread1_task = MockTaskInfo( + task_id=100, + task_name="T1", + coroutine_stack=[MockCoroInfo(task_name="T1", call_stack=[MockFrameInfo("t1.py", 1, "t1")])], + awaited_by=[] + ) + + thread2_task = MockTaskInfo( + task_id=200, + task_name="T2", + coroutine_stack=[MockCoroInfo(task_name="T2", call_stack=[MockFrameInfo("t2.py", 1, "t2")])], + awaited_by=[] + ) + + awaited_info_list = [ + MockAwaitedInfo(thread_id=1, awaited_by=[thread1_task]), + MockAwaitedInfo(thread_id=2, awaited_by=[thread2_task]), + ] + + stacks = list(collector._iter_async_frames(awaited_info_list)) + + # Two threads = two stacks + self.assertEqual(len(stacks), 2) + + # Verify thread IDs preserved + thread_ids = {thread_id for _, thread_id, _ in stacks} + self.assertEqual(thread_ids, {1, 2}) + + def test_collect_public_interface(self): + """Test collect() method correctly routes to async frame processing.""" + collector = PstatsCollector(sample_interval_usec=1000) + + child = MockTaskInfo( + task_id=50, + task_name="Child", + coroutine_stack=[MockCoroInfo(task_name="Child", call_stack=[MockFrameInfo("c.py", 1, "child")])], + awaited_by=[MockCoroInfo(task_name=51, call_stack=[])] + ) + + parent = MockTaskInfo( + task_id=51, + task_name="Parent", + coroutine_stack=[MockCoroInfo(task_name="Parent", call_stack=[MockFrameInfo("p.py", 1, "parent")])], + awaited_by=[] + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=999, awaited_by=[child, parent])] + + # Public interface: collect() + collector.collect(awaited_info_list) + + # Verify stats collected + self.assertGreater(len(collector.result), 0) + func_names = [loc[2] for loc in collector.result.keys()] + self.assertIn("child", func_names) + self.assertIn("parent", func_names) + + def test_diamond_pattern_multiple_parents(self): + """Test _build_linear_stacks: task with 2+ parents picks one deterministically. + + CRITICAL: Tests that when a task has multiple parents, we pick one parent + deterministically (sorted, first one) and annotate the task name with parent count. + """ + collector = PstatsCollector(sample_interval_usec=1000) + + # Diamond pattern: Root spawns A and B, both await Child + # + # Root (id=1) + # / \ + # A (id=2) B (id=3) + # \ / + # Child (id=4) + # + + child = MockTaskInfo( + task_id=4, + task_name="Child", + coroutine_stack=[MockCoroInfo(task_name="Child", call_stack=[MockFrameInfo("c.py", 1, "child_work")])], + awaited_by=[ + MockCoroInfo(task_name=2, call_stack=[MockFrameInfo("a.py", 5, "a_await")]), # Parent A + MockCoroInfo(task_name=3, call_stack=[MockFrameInfo("b.py", 5, "b_await")]), # Parent B + ] + ) + + parent_a = MockTaskInfo( + task_id=2, + task_name="A", + coroutine_stack=[MockCoroInfo(task_name="A", call_stack=[MockFrameInfo("a.py", 10, "a_work")])], + awaited_by=[MockCoroInfo(task_name=1, call_stack=[MockFrameInfo("root.py", 5, "root_spawn")])] + ) + + parent_b = MockTaskInfo( + task_id=3, + task_name="B", + coroutine_stack=[MockCoroInfo(task_name="B", call_stack=[MockFrameInfo("b.py", 10, "b_work")])], + awaited_by=[MockCoroInfo(task_name=1, call_stack=[MockFrameInfo("root.py", 5, "root_spawn")])] + ) + + root = MockTaskInfo( + task_id=1, + task_name="Root", + coroutine_stack=[MockCoroInfo(task_name="Root", call_stack=[MockFrameInfo("root.py", 20, "main")])], + awaited_by=[] + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=777, awaited_by=[child, parent_a, parent_b, root])] + stacks = list(collector._iter_async_frames(awaited_info_list)) + + # Should get 1 stack: Child->A->Root (picks parent with lowest ID: 2) + self.assertEqual(len(stacks), 1, "Diamond should create only 1 path, picking first sorted parent") + + # Verify the single stack + frames, thread_id, leaf_id = stacks[0] + self.assertEqual(leaf_id, 4) + self.assertEqual(thread_id, 777) + + func_names = [f.funcname for f in frames] + # Stack should contain child, parent A (id=2, first when sorted), and root + self.assertIn("child_work", func_names) + self.assertIn("a_work", func_names, "Should use parent A (id=2, first when sorted)") + self.assertNotIn("b_work", func_names, "Should not include parent B") + self.assertIn("main", func_names) + + # Verify Child task is annotated with parent count + self.assertIn("Child (2 parents)", func_names, "Child task should be annotated with parent count") + + def test_empty_coroutine_stack(self): + """Test _build_linear_stacks: handles empty coroutine_stack (line 109 condition false).""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Task with no coroutine_stack + task = MockTaskInfo( + task_id=99, + task_name="EmptyStack", + coroutine_stack=[], # Empty! + awaited_by=[] + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=111, awaited_by=[task])] + stacks = list(collector._iter_async_frames(awaited_info_list)) + + self.assertEqual(len(stacks), 1) + frames, _, _ = stacks[0] + + # Should only have task marker, no function frames + func_names = [f.funcname for f in frames] + self.assertEqual(len(func_names), 1, "Should only have task marker") + self.assertIn("EmptyStack", func_names) + + def test_orphaned_parent_with_no_frames_collected(self): + """Test _build_linear_stacks: orphaned parent at start with empty frames (line 94-96).""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Leaf that doesn't exist in task_map (should not happen normally, but test robustness) + # We'll create a scenario where the leaf_id is present but empty + + # Task references non-existent parent, and has no coroutine_stack + orphan = MockTaskInfo( + task_id=88, + task_name="Orphan", + coroutine_stack=[], # No frames + awaited_by=[MockCoroInfo(task_name=999, call_stack=[])] # Parent doesn't exist + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=222, awaited_by=[orphan])] + stacks = list(collector._iter_async_frames(awaited_info_list)) + + # Should yield because we have the task marker even with no function frames + self.assertEqual(len(stacks), 1) + frames, _, leaf_id = stacks[0] + self.assertEqual(leaf_id, 88) + # Has task marker but no function frames + self.assertGreater(len(frames), 0, "Should have at least task marker") + + def test_frame_ordering(self): + """Test _build_linear_stacks: frames are collected in correct order (leaf->root). + + Task graph (3-level chain): + + Root (id=1) <- root_bottom, root_top + | + Middle (id=2) <- mid_bottom, mid_top + | + Leaf (id=3) <- leaf_bottom, leaf_top + + Expected frame order: leaf_bottom, leaf_top, mid_bottom, mid_top, root_bottom, root_top + (stack is built bottom-up: leaf frames first, then parent frames). + """ + collector = PstatsCollector(sample_interval_usec=1000) + leaf = MockTaskInfo( + task_id=3, + task_name="Leaf", + coroutine_stack=[ + MockCoroInfo(task_name="Leaf", call_stack=[ + MockFrameInfo("leaf.py", 1, "leaf_bottom"), + MockFrameInfo("leaf.py", 2, "leaf_top"), + ]) + ], + awaited_by=[MockCoroInfo(task_name=2, call_stack=[])] + ) + + middle = MockTaskInfo( + task_id=2, + task_name="Middle", + coroutine_stack=[ + MockCoroInfo(task_name="Middle", call_stack=[ + MockFrameInfo("mid.py", 1, "mid_bottom"), + MockFrameInfo("mid.py", 2, "mid_top"), + ]) + ], + awaited_by=[MockCoroInfo(task_name=1, call_stack=[])] + ) + + root = MockTaskInfo( + task_id=1, + task_name="Root", + coroutine_stack=[ + MockCoroInfo(task_name="Root", call_stack=[ + MockFrameInfo("root.py", 1, "root_bottom"), + MockFrameInfo("root.py", 2, "root_top"), + ]) + ], + awaited_by=[] + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=333, awaited_by=[leaf, middle, root])] + stacks = list(collector._iter_async_frames(awaited_info_list)) + + self.assertEqual(len(stacks), 1) + frames, _, _ = stacks[0] + + func_names = [f.funcname for f in frames] + + # Order should be: leaf frames, leaf marker, middle frames, middle marker, root frames, root marker + leaf_bottom_idx = func_names.index("leaf_bottom") + leaf_top_idx = func_names.index("leaf_top") + mid_bottom_idx = func_names.index("mid_bottom") + root_bottom_idx = func_names.index("root_bottom") + + # Verify leaf comes before middle comes before root + self.assertLess(leaf_bottom_idx, leaf_top_idx, "Leaf frames in order") + self.assertLess(leaf_top_idx, mid_bottom_idx, "Leaf before middle") + self.assertLess(mid_bottom_idx, root_bottom_idx, "Middle before root") + + def test_complex_multi_parent_convergence(self): + """Test _build_linear_stacks: multiple leaves with same parents pick deterministically. + + Tests that when multiple leaves have multiple parents, each leaf picks the same + parent (sorted, first one) and all leaves are annotated with parent count. + + Task graph structure (both leaves awaited by both A and B):: + + Root (id=1) + / \\ + A (id=2) B (id=3) + | \\ / | + | \\ / | + | \\/ | + | /\\ | + | / \\ | + LeafX (id=4) LeafY (id=5) + + Expected behavior: Both leaves pick parent A (lowest id=2) for their stack path. + Result: 2 stacks, both going through A -> Root (B is skipped). + """ + collector = PstatsCollector(sample_interval_usec=1000) + + leaf_x = MockTaskInfo( + task_id=4, + task_name="LeafX", + coroutine_stack=[MockCoroInfo(task_name="LeafX", call_stack=[MockFrameInfo("x.py", 1, "x")])], + awaited_by=[ + MockCoroInfo(task_name=2, call_stack=[]), + MockCoroInfo(task_name=3, call_stack=[]), + ] + ) + + leaf_y = MockTaskInfo( + task_id=5, + task_name="LeafY", + coroutine_stack=[MockCoroInfo(task_name="LeafY", call_stack=[MockFrameInfo("y.py", 1, "y")])], + awaited_by=[ + MockCoroInfo(task_name=2, call_stack=[]), + MockCoroInfo(task_name=3, call_stack=[]), + ] + ) + + parent_a = MockTaskInfo( + task_id=2, + task_name="A", + coroutine_stack=[MockCoroInfo(task_name="A", call_stack=[MockFrameInfo("a.py", 1, "a")])], + awaited_by=[MockCoroInfo(task_name=1, call_stack=[])] + ) + + parent_b = MockTaskInfo( + task_id=3, + task_name="B", + coroutine_stack=[MockCoroInfo(task_name="B", call_stack=[MockFrameInfo("b.py", 1, "b")])], + awaited_by=[MockCoroInfo(task_name=1, call_stack=[])] + ) + + root = MockTaskInfo( + task_id=1, + task_name="Root", + coroutine_stack=[MockCoroInfo(task_name="Root", call_stack=[MockFrameInfo("r.py", 1, "root")])], + awaited_by=[] + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=444, awaited_by=[leaf_x, leaf_y, parent_a, parent_b, root])] + stacks = list(collector._iter_async_frames(awaited_info_list)) + + # 2 leaves, each picks same parent (A, id=2) = 2 paths + self.assertEqual(len(stacks), 2, "Should create 2 paths: X->A->Root, Y->A->Root") + + # Verify both leaves pick parent A (id=2, first when sorted) + leaf_ids_seen = set() + for frames, _, leaf_id in stacks: + leaf_ids_seen.add(leaf_id) + func_names = [f.funcname for f in frames] + + # Both stacks should go through parent A only + self.assertIn("a", func_names, "Should use parent A (id=2, first when sorted)") + self.assertNotIn("b", func_names, "Should not include parent B") + self.assertIn("root", func_names, "Should reach root") + + # Check for parent count annotation on the leaf + if leaf_id == 4: + self.assertIn("x", func_names) + self.assertIn("LeafX (2 parents)", func_names, "LeafX should be annotated with parent count") + elif leaf_id == 5: + self.assertIn("y", func_names) + self.assertIn("LeafY (2 parents)", func_names, "LeafY should be annotated with parent count") + + # Both leaves should be represented + self.assertEqual(leaf_ids_seen, {4, 5}, "Both LeafX and LeafY should have paths") + + +class TestFlamegraphCollectorAsync(unittest.TestCase): + """Test FlamegraphCollector with async frames.""" + + def test_flamegraph_with_async_frames(self): + """Test FlamegraphCollector correctly processes async task frames.""" + collector = FlamegraphCollector(sample_interval_usec=1000) + + # Build async task tree: Root -> Child + child = MockTaskInfo( + task_id=2, + task_name="ChildTask", + coroutine_stack=[ + MockCoroInfo( + task_name="ChildTask", + call_stack=[MockFrameInfo("child.py", 10, "child_work")] + ) + ], + awaited_by=[MockCoroInfo(task_name=1, call_stack=[])] + ) + + root = MockTaskInfo( + task_id=1, + task_name="RootTask", + coroutine_stack=[ + MockCoroInfo( + task_name="RootTask", + call_stack=[MockFrameInfo("root.py", 20, "root_work")] + ) + ], + awaited_by=[] + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=100, awaited_by=[child, root])] + + # Collect async frames + collector.collect(awaited_info_list) + + # Verify samples were collected + self.assertGreater(collector._total_samples, 0) + + # Verify the flamegraph tree structure contains our functions + root_node = collector._root + self.assertGreater(root_node["samples"], 0) + + # Check that thread ID was tracked + self.assertIn(100, collector._all_threads) + + def test_flamegraph_with_task_markers(self): + """Test FlamegraphCollector includes boundary markers.""" + collector = FlamegraphCollector(sample_interval_usec=1000) + + task = MockTaskInfo( + task_id=42, + task_name="MyTask", + coroutine_stack=[ + MockCoroInfo( + task_name="MyTask", + call_stack=[MockFrameInfo("work.py", 5, "do_work")] + ) + ], + awaited_by=[] + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=200, awaited_by=[task])] + collector.collect(awaited_info_list) + + # Find marker in the tree + def find_task_marker(node, depth=0): + for func, child in node.get("children", {}).items(): + if func[0] == "": + return func + result = find_task_marker(child, depth + 1) + if result: + return result + return None + + task_marker = find_task_marker(collector._root) + self.assertIsNotNone(task_marker, "Should have marker in tree") + self.assertEqual(task_marker[0], "") + self.assertIn("MyTask", task_marker[2]) + + def test_flamegraph_multiple_async_samples(self): + """Test FlamegraphCollector aggregates multiple async samples correctly.""" + collector = FlamegraphCollector(sample_interval_usec=1000) + + task = MockTaskInfo( + task_id=1, + task_name="Task", + coroutine_stack=[ + MockCoroInfo( + task_name="Task", + call_stack=[MockFrameInfo("work.py", 10, "work")] + ) + ], + awaited_by=[] + ) + + awaited_info_list = [MockAwaitedInfo(thread_id=300, awaited_by=[task])] + + # Collect multiple samples + for _ in range(5): + collector.collect(awaited_info_list) + + # Verify sample count + self.assertEqual(collector._sample_count, 5) + self.assertEqual(collector._total_samples, 5) + + +class TestAsyncAwareParameterFlow(unittest.TestCase): + """Integration tests for async_aware parameter flow from CLI to unwinder.""" + + def test_sample_function_accepts_async_aware(self): + """Test that sample() function accepts async_aware parameter.""" + sig = inspect.signature(sample) + self.assertIn("async_aware", sig.parameters) + + def test_sample_live_function_accepts_async_aware(self): + """Test that sample_live() function accepts async_aware parameter.""" + sig = inspect.signature(sample_live) + self.assertIn("async_aware", sig.parameters) + + def test_sample_profiler_sample_accepts_async_aware(self): + """Test that SampleProfiler.sample() accepts async_aware parameter.""" + sig = inspect.signature(SampleProfiler.sample) + self.assertIn("async_aware", sig.parameters) + + def test_async_aware_all_sees_sleeping_and_running_tasks(self): + """Test async_aware='all' captures both sleeping and CPU-running tasks.""" + # Sleeping task (awaiting) + sleeping_task = MockTaskInfo( + task_id=1, + task_name="SleepingTask", + coroutine_stack=[ + MockCoroInfo( + task_name="SleepingTask", + call_stack=[MockFrameInfo("sleeper.py", 10, "sleep_work")] + ) + ], + awaited_by=[] + ) + + # CPU-running task (active) + running_task = MockTaskInfo( + task_id=2, + task_name="RunningTask", + coroutine_stack=[ + MockCoroInfo( + task_name="RunningTask", + call_stack=[MockFrameInfo("runner.py", 20, "cpu_work")] + ) + ], + awaited_by=[] + ) + + # Both tasks returned by get_all_awaited_by + awaited_info_list = [MockAwaitedInfo(thread_id=100, awaited_by=[sleeping_task, running_task])] + + collector = PstatsCollector(sample_interval_usec=1000) + collector.collect(awaited_info_list) + collector.create_stats() + + # Both tasks should be visible + sleeping_key = ("sleeper.py", 10, "sleep_work") + running_key = ("runner.py", 20, "cpu_work") + + self.assertIn(sleeping_key, collector.stats) + self.assertIn(running_key, collector.stats) + + # Task markers should also be present + task_keys = [k for k in collector.stats if k[0] == ""] + self.assertGreater(len(task_keys), 0, "Should have markers in stats") + + # Verify task names are in the markers + task_names = [k[2] for k in task_keys] + self.assertTrue( + any("SleepingTask" in name for name in task_names), + "SleepingTask should be in task markers" + ) + self.assertTrue( + any("RunningTask" in name for name in task_names), + "RunningTask should be in task markers" + ) + + def test_async_aware_running_sees_only_running_task(self): + """Test async_aware='running' only shows the currently running task stack.""" + # Only the running task's stack is returned by get_async_stack_trace + running_task = MockTaskInfo( + task_id=2, + task_name="RunningTask", + coroutine_stack=[ + MockCoroInfo( + task_name="RunningTask", + call_stack=[MockFrameInfo("runner.py", 20, "cpu_work")] + ) + ], + awaited_by=[] + ) + + # get_async_stack_trace only returns the running task + awaited_info_list = [MockAwaitedInfo(thread_id=100, awaited_by=[running_task])] + + collector = PstatsCollector(sample_interval_usec=1000) + collector.collect(awaited_info_list) + collector.create_stats() + + # Only running task should be visible + running_key = ("runner.py", 20, "cpu_work") + self.assertIn(running_key, collector.stats) + + # Verify we don't see the sleeping task (it wasn't in the input) + sleeping_key = ("sleeper.py", 10, "sleep_work") + self.assertNotIn(sleeping_key, collector.stats) + + # Task marker for running task should be present + task_keys = [k for k in collector.stats if k[0] == ""] + self.assertGreater(len(task_keys), 0, "Should have markers in stats") + + task_names = [k[2] for k in task_keys] + self.assertTrue( + any("RunningTask" in name for name in task_names), + "RunningTask should be in task markers" + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py b/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py new file mode 100644 index 00000000000..ff944b3163e --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py @@ -0,0 +1,1788 @@ +"""Tests for binary format round-trip functionality.""" + +import json +import os +import pathlib +import random +import struct +import tempfile +import unittest +from collections import defaultdict + +try: + import _remote_debugging + from _remote_debugging import ( + InterpreterInfo, + ThreadInfo, + FrameInfo, + LocationInfo, + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_ON_CPU, + THREAD_STATUS_UNKNOWN, + THREAD_STATUS_GIL_REQUESTED, + THREAD_STATUS_HAS_EXCEPTION, + THREAD_STATUS_MAIN_THREAD, + ) + from profiling.sampling.binary_collector import BinaryCollector + from profiling.sampling.binary_reader import BinaryReader, convert_binary_to_format + from profiling.sampling.constants import PROFILING_MODE_CPU + from profiling.sampling.gecko_collector import GeckoCollector + + ZSTD_AVAILABLE = _remote_debugging.zstd_available() +except ImportError: + raise unittest.SkipTest( + "Test only runs when _remote_debugging is available" + ) + +from .helpers import jsonl_tables + + +def make_frame(filename, lineno, funcname, end_lineno=None, column=None, + end_column=None, opcode=None): + """Create a FrameInfo struct sequence with full location info and opcode.""" + if end_lineno is None: + end_lineno = lineno + if column is None: + column = 0 + if end_column is None: + end_column = 0 + location = LocationInfo((lineno, end_lineno, column, end_column)) + return FrameInfo((filename, location, funcname, opcode)) + + +def make_thread(thread_id, frames, status=0): + """Create a ThreadInfo struct sequence.""" + return ThreadInfo((thread_id, status, frames)) + + +def make_interpreter(interp_id, threads): + """Create an InterpreterInfo struct sequence.""" + return InterpreterInfo((interp_id, threads)) + + +def extract_lineno(location): + """Extract line number from location (tuple or int or None).""" + if location is None: + return 0 # Treat None as 0 + if isinstance(location, tuple): + return location[0] if location[0] is not None else 0 + return location + + +def extract_location(location): + """Extract full location info as dict from location tuple or None.""" + if location is None: + return {"lineno": 0, "end_lineno": 0, "column": 0, "end_column": 0} + if isinstance(location, tuple) and len(location) >= 4: + return { + "lineno": location[0] if location[0] is not None else 0, + "end_lineno": location[1] if location[1] is not None else 0, + "column": location[2] if location[2] is not None else 0, + "end_column": location[3] if location[3] is not None else 0, + } + # Fallback for old-style location + lineno = location[0] if isinstance(location, tuple) else location + return {"lineno": lineno or 0, "end_lineno": lineno or 0, "column": 0, "end_column": 0} + + +def frame_to_dict(frame): + """Convert a FrameInfo to a dict.""" + loc = extract_location(frame.location) + return { + "filename": frame.filename, + "funcname": frame.funcname, + "lineno": loc["lineno"], + "end_lineno": loc["end_lineno"], + "column": loc["column"], + "end_column": loc["end_column"], + "opcode": frame.opcode, + } + + +class RawCollector: + """Collector that captures all raw data grouped by thread.""" + + def __init__(self): + # Key: (interpreter_id, thread_id) -> list of samples for that thread + self.by_thread = defaultdict(list) + self.total_count = 0 + + def collect(self, stack_frames, timestamps_us): + """Capture the raw sample data.""" + # timestamps_us is a list; add one sample per timestamp + count = len(timestamps_us) + for interp in stack_frames: + for thread in interp.threads: + frames = [frame_to_dict(f) for f in thread.frame_info] + key = (interp.interpreter_id, thread.thread_id) + sample = {"status": thread.status, "frames": frames} + for _ in range(count): + self.by_thread[key].append(sample) + self.total_count += count + + def export(self, filename): + pass + + +def samples_to_by_thread(samples): + """Convert input samples to by-thread format for comparison.""" + by_thread = defaultdict(list) + for sample in samples: + for interp in sample: + for thread in interp.threads: + frames = [frame_to_dict(f) for f in thread.frame_info] + key = (interp.interpreter_id, thread.thread_id) + by_thread[key].append( + { + "status": thread.status, + "frames": frames, + } + ) + return by_thread + + +class BinaryFormatTestBase(unittest.TestCase): + """Base class with common setup/teardown for binary format tests.""" + + def setUp(self): + self.temp_files = [] + + def tearDown(self): + for f in self.temp_files: + if os.path.exists(f): + os.unlink(f) + + def create_binary_file(self, samples, interval=1000, compression="none", + mode=None, capture_config=None): + """Create a test binary file and track it for cleanup.""" + filename, _ = self.write_binary_file( + samples, interval, compression, mode, capture_config + ) + return filename + + def write_binary_file(self, samples, interval=1000, compression="none", + mode=None, capture_config=None): + """Like create_binary_file but also returns the writer collector.""" + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: + filename = f.name + self.temp_files.append(filename) + + collector = BinaryCollector( + filename, interval, compression=compression, mode=mode, + capture_config=capture_config, + ) + for sample in samples: + collector.collect(sample) + collector.export(None) + return filename, collector + + def roundtrip(self, samples, interval=1000, compression="none"): + """Write samples to binary and read back.""" + filename = self.create_binary_file(samples, interval, compression) + collector = RawCollector() + with BinaryReader(filename) as reader: + count = reader.replay_samples(collector) + return collector, count + + def assert_samples_equal(self, expected_samples, collector): + """Assert that roundtripped samples match input exactly, per-thread.""" + expected = samples_to_by_thread(expected_samples) + + # Same threads present + self.assertEqual( + set(expected.keys()), + set(collector.by_thread.keys()), + "Thread set mismatch", + ) + + # For each thread, samples match in order + for key in expected: + exp_samples = expected[key] + act_samples = collector.by_thread[key] + interp_id, thread_id = key + + self.assertEqual( + len(exp_samples), + len(act_samples), + f"Thread ({interp_id}, {thread_id}): sample count mismatch " + f"(expected {len(exp_samples)}, got {len(act_samples)})", + ) + + for i, (exp, act) in enumerate(zip(exp_samples, act_samples)): + self.assertEqual( + exp["status"], + act["status"], + f"Thread ({interp_id}, {thread_id}), sample {i}: " + f"status mismatch (expected {exp['status']}, got {act['status']})", + ) + + self.assertEqual( + len(exp["frames"]), + len(act["frames"]), + f"Thread ({interp_id}, {thread_id}), sample {i}: " + f"frame count mismatch", + ) + + for j, (exp_frame, act_frame) in enumerate( + zip(exp["frames"], act["frames"]) + ): + for field in ("filename", "funcname", "lineno", "end_lineno", + "column", "end_column", "opcode"): + self.assertEqual( + exp_frame[field], + act_frame[field], + f"Thread ({interp_id}, {thread_id}), sample {i}, " + f"frame {j}: {field} mismatch " + f"(expected {exp_frame[field]!r}, got {act_frame[field]!r})", + ) + + +class TestBinaryRoundTrip(BinaryFormatTestBase): + """Tests for exact binary format round-trip.""" + + def test_single_sample_single_frame(self): + """Single sample with one frame roundtrips exactly.""" + samples = [ + [ + make_interpreter( + 0, + [ + make_thread( + 12345, [make_frame("test.py", 42, "myfunc")] + ) + ], + ) + ] + ] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_single_sample_multi_frame(self): + """Single sample with call stack roundtrips exactly.""" + frames = [ + make_frame("inner.py", 10, "inner"), + make_frame("middle.py", 20, "middle"), + make_frame("outer.py", 30, "outer"), + ] + samples = [[make_interpreter(0, [make_thread(100, frames)])]] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_multiple_samples_same_stack(self): + """Multiple identical samples roundtrip exactly (tests RLE).""" + frame = make_frame("hot.py", 99, "hot_func") + samples = [ + [make_interpreter(0, [make_thread(1, [frame])])] + for _ in range(100) + ] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 100) + self.assert_samples_equal(samples, collector) + + def test_multiple_samples_varying_stacks(self): + """Multiple samples with varying stacks roundtrip exactly.""" + samples = [] + for i in range(20): + depth = i % 5 + 1 + frames = [ + make_frame(f"f{j}.py", j * 10 + i, f"func{j}") + for j in range(depth) + ] + samples.append([make_interpreter(0, [make_thread(1, frames)])]) + collector, count = self.roundtrip(samples) + self.assertEqual(count, 20) + self.assert_samples_equal(samples, collector) + + def test_thread_ids_preserved(self): + """Thread IDs are preserved exactly.""" + thread_ids = [1, 12345, 0x7FFF12345678, 999999] + samples = [] + for tid in thread_ids: + samples.append( + [ + make_interpreter( + 0, [make_thread(tid, [make_frame("t.py", 10, "f")])] + ) + ] + ) + collector, count = self.roundtrip(samples) + self.assertEqual(count, len(thread_ids)) + self.assert_samples_equal(samples, collector) + + def test_interpreter_ids_preserved(self): + """Interpreter IDs are preserved exactly.""" + interp_ids = [0, 1, 5, 100] + samples = [] + for iid in interp_ids: + samples.append( + [ + make_interpreter( + iid, [make_thread(1, [make_frame("i.py", 10, "f")])] + ) + ] + ) + collector, count = self.roundtrip(samples) + self.assertEqual(count, len(interp_ids)) + self.assert_samples_equal(samples, collector) + + def test_status_flags_preserved(self): + """All thread status flags are preserved exactly.""" + statuses = [ + 0, + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_ON_CPU, + THREAD_STATUS_UNKNOWN, + THREAD_STATUS_GIL_REQUESTED, + THREAD_STATUS_HAS_EXCEPTION, + THREAD_STATUS_MAIN_THREAD, + THREAD_STATUS_HAS_GIL | THREAD_STATUS_ON_CPU, + THREAD_STATUS_HAS_GIL | THREAD_STATUS_HAS_EXCEPTION, + THREAD_STATUS_HAS_GIL + | THREAD_STATUS_ON_CPU + | THREAD_STATUS_GIL_REQUESTED, + ] + samples = [] + for i, status in enumerate(statuses): + samples.append( + [ + make_interpreter( + 0, + [ + make_thread( + 1, [make_frame("s.py", 10 + i, "f")], status + ) + ], + ) + ] + ) + collector, count = self.roundtrip(samples) + self.assertEqual(count, len(statuses)) + self.assert_samples_equal(samples, collector) + + def test_binary_replay_preserves_main_thread_for_gecko(self): + """Binary replay preserves main thread identity for GeckoCollector.""" + samples = [ + [ + make_interpreter( + 0, + [ + make_thread( + 1, + [make_frame("main.py", 10, "main")], + THREAD_STATUS_MAIN_THREAD, + ), + make_thread(2, [make_frame("worker.py", 20, "worker")]), + ], + ) + ] + ] + filename = self.create_binary_file(samples) + collector = GeckoCollector(1000) + + with BinaryReader(filename) as reader: + count = reader.replay_samples(collector) + + self.assertEqual(count, 2) + profile = collector._build_profile() + threads = {thread["tid"]: thread for thread in profile["threads"]} + self.assertTrue(threads[1]["isMainThread"]) + self.assertFalse(threads[2]["isMainThread"]) + + def test_multiple_threads_per_sample(self): + """Multiple threads in one sample roundtrip exactly.""" + threads = [ + make_thread( + 1, [make_frame("t1.py", 10, "t1")], THREAD_STATUS_HAS_GIL + ), + make_thread( + 2, [make_frame("t2.py", 20, "t2")], THREAD_STATUS_ON_CPU + ), + make_thread(3, [make_frame("t3.py", 30, "t3")], 0), + ] + samples = [[make_interpreter(0, threads)] for _ in range(10)] + collector, count = self.roundtrip(samples) + # 10 samples × 3 threads = 30 thread-samples + self.assertEqual(count, 30) + self.assert_samples_equal(samples, collector) + + def test_multiple_interpreters_per_sample(self): + """Multiple interpreters in one sample roundtrip exactly.""" + samples = [ + [ + make_interpreter( + 0, [make_thread(1, [make_frame("i0.py", 10, "i0")])] + ), + make_interpreter( + 1, [make_thread(2, [make_frame("i1.py", 20, "i1")])] + ), + ] + for _ in range(5) + ] + collector, count = self.roundtrip(samples) + # 5 samples × 2 interpreters × 1 thread = 10 thread-samples + self.assertEqual(count, 10) + self.assert_samples_equal(samples, collector) + + def test_same_thread_id_different_interpreters(self): + """Same thread_id in different interpreters must be tracked separately.""" + # This test catches bugs where thread state is keyed only by thread_id + # without considering interpreter_id + samples = [] + # Interleave samples from interpreter 0 and 1, both using thread_id=1 + for i in range(20): + interp_id = i % 2 # Alternate between interpreter 0 and 1 + frame = make_frame( + f"interp{interp_id}.py", 10 + i, f"func{interp_id}" + ) + samples.append( + [make_interpreter(interp_id, [make_thread(1, [frame])])] + ) + + collector, count = self.roundtrip(samples) + self.assertEqual(count, 20) + self.assert_samples_equal(samples, collector) + + # Verify both interpreters are present + keys = set(collector.by_thread.keys()) + self.assertIn((0, 1), keys) # interpreter 0, thread 1 + self.assertIn((1, 1), keys) # interpreter 1, thread 1 + + # Verify each interpreter got 10 samples + self.assertEqual(len(collector.by_thread[(0, 1)]), 10) + self.assertEqual(len(collector.by_thread[(1, 1)]), 10) + + # Verify the samples are in the right order for each interpreter + for i, sample in enumerate(collector.by_thread[(0, 1)]): + expected_lineno = 10 + i * 2 # 10, 12, 14, ... + self.assertEqual(sample["frames"][0]["lineno"], expected_lineno) + self.assertEqual(sample["frames"][0]["filename"], "interp0.py") + + for i, sample in enumerate(collector.by_thread[(1, 1)]): + expected_lineno = 11 + i * 2 # 11, 13, 15, ... + self.assertEqual(sample["frames"][0]["lineno"], expected_lineno) + self.assertEqual(sample["frames"][0]["filename"], "interp1.py") + + def test_deep_call_stack(self): + """Deep call stack roundtrips exactly.""" + depth = 100 + frames = [ + make_frame(f"f{i}.py", i + 1, f"func{i}") for i in range(depth) + ] + samples = [[make_interpreter(0, [make_thread(1, frames)])]] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_line_numbers_preserved(self): + """Various line numbers are preserved exactly.""" + linenos = [1, 100, 1000, 65535, 100000] + samples = [] + for lineno in linenos: + samples.append( + [ + make_interpreter( + 0, [make_thread(1, [make_frame("l.py", lineno, "f")])] + ) + ] + ) + collector, count = self.roundtrip(samples) + self.assertEqual(count, len(linenos)) + self.assert_samples_equal(samples, collector) + + @unittest.skipUnless(ZSTD_AVAILABLE, "zstd compression not available") + def test_zstd_compression_roundtrip(self): + """Zstd compressed data roundtrips exactly.""" + samples = [] + for i in range(200): + frames = [ + make_frame(f"z{j}.py", j * 10 + i + 1, f"zfunc{j}") + for j in range(3) + ] + samples.append([make_interpreter(0, [make_thread(1, frames)])]) + collector, count = self.roundtrip(samples, compression="zstd") + self.assertEqual(count, 200) + self.assert_samples_equal(samples, collector) + + def test_sample_interval_preserved(self): + """Sample interval is preserved in file metadata.""" + intervals = [100, 500, 1000, 5000, 10000] + for interval in intervals: + with self.subTest(interval=interval): + samples = [ + [ + make_interpreter( + 0, [make_thread(1, [make_frame("i.py", 1, "f")])] + ) + ] + ] + filename = self.create_binary_file(samples, interval=interval) + with BinaryReader(filename) as reader: + info = reader.get_info() + self.assertEqual(info["sample_interval_us"], interval) + + def test_profiling_mode_preserved(self): + filename = self.create_binary_file([], mode=PROFILING_MODE_CPU) + with BinaryReader(filename) as reader: + self.assertEqual(reader.get_info()["mode"], PROFILING_MODE_CPU) + + def test_missing_profiling_mode_is_unknown(self): + filename = self.create_binary_file([]) + with BinaryReader(filename) as reader: + self.assertIsNone(reader.get_info()["mode"]) + + def test_capture_config_preserved(self): + capture_config = { + "all_threads": True, + "native": True, + "gc": False, + "opcodes": True, + "blocking": False, + } + filename = self.create_binary_file( + [], capture_config=capture_config + ) + with BinaryReader(filename) as reader: + self.assertEqual( + reader.get_info()["capture_config"], capture_config + ) + + def test_missing_capture_config_is_unknown(self): + filename = self.create_binary_file([]) + with BinaryReader(filename) as reader: + self.assertIsNone(reader.get_info()["capture_config"]) + + def test_threads_interleaved_samples(self): + """Multiple threads with interleaved varying samples.""" + samples = [] + for i in range(30): + threads = [ + make_thread( + 1, + [make_frame("t1.py", 10 + i, "t1")], + THREAD_STATUS_HAS_GIL if i % 2 == 0 else 0, + ), + make_thread( + 2, + [make_frame("t2.py", 20 + i, "t2")], + THREAD_STATUS_ON_CPU if i % 3 == 0 else 0, + ), + ] + samples.append([make_interpreter(0, threads)]) + collector, count = self.roundtrip(samples) + self.assertEqual(count, 60) + self.assert_samples_equal(samples, collector) + + def test_full_location_roundtrip(self): + """Full source location (end_lineno, column, end_column) roundtrips.""" + frames = [ + make_frame("test.py", 10, "func1", end_lineno=12, column=4, end_column=20), + make_frame("test.py", 20, "func2", end_lineno=20, column=8, end_column=45), + make_frame("test.py", 30, "func3", end_lineno=35, column=0, end_column=100), + ] + samples = [[make_interpreter(0, [make_thread(1, frames)])]] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_opcode_roundtrip(self): + """Opcode values roundtrip exactly.""" + opcodes = [0, 1, 50, 100, 150, 200, 254] # Valid Python opcodes + samples = [] + for opcode in opcodes: + frame = make_frame("test.py", 10, "func", opcode=opcode) + samples.append([make_interpreter(0, [make_thread(1, [frame])])]) + collector, count = self.roundtrip(samples) + self.assertEqual(count, len(opcodes)) + self.assert_samples_equal(samples, collector) + + def test_opcode_none_roundtrip(self): + """Opcode=None (sentinel 255) roundtrips as None.""" + frame = make_frame("test.py", 10, "func", opcode=None) + samples = [[make_interpreter(0, [make_thread(1, [frame])])]] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_mixed_location_and_opcode(self): + """Mixed full location and opcode data roundtrips.""" + frames = [ + make_frame("a.py", 10, "a", end_lineno=15, column=4, end_column=30, opcode=100), + make_frame("b.py", 20, "b", end_lineno=20, column=0, end_column=50, opcode=None), + make_frame("c.py", 30, "c", end_lineno=32, column=8, end_column=25, opcode=50), + ] + samples = [[make_interpreter(0, [make_thread(1, frames)])]] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_delta_encoding_multiline(self): + """Multi-line spans (large end_lineno delta) roundtrip correctly.""" + # This tests the delta encoding: end_lineno = lineno + delta + frames = [ + make_frame("test.py", 1, "small", end_lineno=1, column=0, end_column=10), + make_frame("test.py", 100, "medium", end_lineno=110, column=0, end_column=50), + make_frame("test.py", 1000, "large", end_lineno=1500, column=0, end_column=200), + ] + samples = [[make_interpreter(0, [make_thread(1, frames)])]] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_column_positions_preserved(self): + """Various column positions are preserved exactly.""" + columns = [(0, 10), (4, 50), (8, 100), (100, 200)] + samples = [] + for col, end_col in columns: + frame = make_frame("test.py", 10, "func", column=col, end_column=end_col) + samples.append([make_interpreter(0, [make_thread(1, [frame])])]) + collector, count = self.roundtrip(samples) + self.assertEqual(count, len(columns)) + self.assert_samples_equal(samples, collector) + + def test_same_line_different_opcodes(self): + """Same line with different opcodes creates distinct frames.""" + # This tests that opcode is part of the frame key + frames = [ + make_frame("test.py", 10, "func", opcode=100), + make_frame("test.py", 10, "func", opcode=101), + make_frame("test.py", 10, "func", opcode=102), + ] + samples = [[make_interpreter(0, [make_thread(1, [f])]) for f in frames]] + collector, count = self.roundtrip(samples) + # Verify all three opcodes are preserved distinctly + self.assertEqual(count, 3) + + def test_same_line_different_columns(self): + """Same line with different columns creates distinct frames.""" + frames = [ + make_frame("test.py", 10, "func", column=0, end_column=10), + make_frame("test.py", 10, "func", column=15, end_column=25), + make_frame("test.py", 10, "func", column=30, end_column=40), + ] + samples = [[make_interpreter(0, [make_thread(1, [f])]) for f in frames]] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 3) + + +class TestBinaryEdgeCases(BinaryFormatTestBase): + """Tests for edge cases in binary format.""" + + def test_unicode_filenames(self): + """Unicode filenames roundtrip exactly.""" + filenames = [ + "/путь/файл.py", + "/路径/文件.py", + "/パス/ファイル.py", + "/chemin/café.py", + ] + for fname in filenames: + with self.subTest(filename=fname): + samples = [ + [ + make_interpreter( + 0, [make_thread(1, [make_frame(fname, 1, "func")])] + ) + ] + ] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_unicode_funcnames(self): + """Unicode function names roundtrip exactly.""" + funcnames = [ + "функция", + "函数", + "関数", + "función", + ] + for funcname in funcnames: + with self.subTest(funcname=funcname): + samples = [ + [ + make_interpreter( + 0, + [ + make_thread( + 1, [make_frame("test.py", 1, funcname)] + ) + ], + ) + ] + ] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_special_char_filenames(self): + """Filenames with special characters roundtrip exactly.""" + filenames = [ + "/path/with spaces/file.py", + "/path/with\ttab/file.py", + "/path/with'quote/file.py", + '/path/with"double/file.py', + "/path/with\\backslash/file.py", + ] + for fname in filenames: + with self.subTest(filename=fname): + samples = [ + [ + make_interpreter( + 0, [make_thread(1, [make_frame(fname, 1, "func")])] + ) + ] + ] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_special_funcnames(self): + """Function names with special characters roundtrip exactly.""" + funcnames = [ + "", + "", + "", + "", + "__init__", + "func.inner", + ] + for funcname in funcnames: + with self.subTest(funcname=funcname): + samples = [ + [ + make_interpreter( + 0, + [ + make_thread( + 1, [make_frame("test.py", 1, funcname)] + ) + ], + ) + ] + ] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_long_filename(self): + """Long filename roundtrips exactly.""" + long_file = "/very/long/path/" + "sub/" * 50 + "file.py" + samples = [ + [ + make_interpreter( + 0, [make_thread(1, [make_frame(long_file, 1, "func")])] + ) + ] + ] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_long_funcname(self): + """Long function name roundtrips exactly.""" + long_func = "very_long_function_name_" + "x" * 200 + samples = [ + [ + make_interpreter( + 0, [make_thread(1, [make_frame("test.py", 1, long_func)])] + ) + ] + ] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_empty_funcname(self): + """Empty function name roundtrips exactly.""" + samples = [ + [ + make_interpreter( + 0, [make_thread(1, [make_frame("test.py", 1, "")])] + ) + ] + ] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + @unittest.skipUnless(ZSTD_AVAILABLE, "zstd compression not available") + def test_large_sample_count(self): + """Large number of samples roundtrips exactly.""" + num = 5000 + samples = [ + [ + make_interpreter( + 0, + [ + make_thread( + 1, [make_frame("test.py", (i % 100) + 1, "func")] + ) + ], + ) + ] + for i in range(num) + ] + collector, count = self.roundtrip(samples, compression="zstd") + self.assertEqual(count, num) + self.assert_samples_equal(samples, collector) + + def test_context_manager_cleanup(self): + """Reader cleans up on context exit.""" + samples = [ + [ + make_interpreter( + 0, [make_thread(1, [make_frame("t.py", 1, "f")])] + ) + ] + ] + filename = self.create_binary_file(samples) + reader = BinaryReader(filename) + with reader: + collector = RawCollector() + count = reader.replay_samples(collector) + self.assertEqual(count, 1) + with self.assertRaises(RuntimeError): + reader.replay_samples(collector) + + def test_invalid_file_path(self): + """Invalid file path raises appropriate error.""" + with self.assertRaises((FileNotFoundError, OSError, ValueError)): + with BinaryReader("/nonexistent/path/file.bin") as reader: + reader.replay_samples(RawCollector()) + + def test_path_arguments_round_trip(self): + """Reader and writer accept str, bytes or os.PathLike.""" + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: + filename = f.name + self.temp_files.append(filename) + + for path_arg in (filename, os.fsencode(filename), pathlib.Path(filename)): + with self.subTest(path_type=type(path_arg).__name__): + writer = _remote_debugging.BinaryWriter(path_arg, 1000, 0) + writer.finalize() + reader = _remote_debugging.BinaryReader(path_arg) + info = reader.get_info() + reader.close() + self.assertEqual(info["sample_count"], 0) + + def test_rejects_non_pathlike(self): + """Reader and writer raise TypeError on non-path-like filenames.""" + with self.assertRaises(TypeError): + _remote_debugging.BinaryWriter(123, 1000, 0) + with self.assertRaises(TypeError): + _remote_debugging.BinaryReader(123) + + def test_invalid_path_error_preserves_pathlib(self): + """Missing path: OSError carries the original path object, not a string.""" + missing = pathlib.Path("/i/do/not/exist") + with self.assertRaises(FileNotFoundError) as cm: + _remote_debugging.BinaryReader(missing) + self.assertEqual(os.fspath(cm.exception.filename), os.fspath(missing)) + + def test_writer_handles_empty_stack_first_sample(self): + """BinaryWriter.write_sample tolerates an empty stack on a fresh thread. + + Regression test for the C-level RLE bug in process_thread_sample: a + freshly-created ThreadEntry has prev_stack_depth == 0, so an empty + curr_stack compares as STACK_REPEAT against the zero-initialized + previous stack. Before the fix, this fell through the + `&& !is_new_thread` guard into write_sample_with_encoding, which had + no handler for STACK_REPEAT and raised + RuntimeError("Invalid stack encoding type"). Goes through + BinaryWriter.write_sample directly so the test cannot be masked by + any Python-level filtering. + """ + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: + filename = f.name + self.temp_files.append(filename) + + writer = _remote_debugging.BinaryWriter(filename, 1000, 0, compression=0) + empty_sample = [ + make_interpreter( + 0, [make_thread(99, [], status=THREAD_STATUS_UNKNOWN)] + ) + ] + # First sample for a fresh thread has empty frame_info — the exact + # scenario that exposes the bug. + writer.write_sample(empty_sample, 1000) + writer.write_sample(empty_sample, 2000) + # Mix in a real sample to exercise the transition out of the + # empty-stack RLE buffer. + real_sample = [ + make_interpreter(0, [make_thread(1, [make_frame("a.py", 1, "f")])]) + ] + writer.write_sample(real_sample, 3000) + writer.finalize() + + reader_collector = RawCollector() + with BinaryReader(filename) as reader: + count = reader.replay_samples(reader_collector) + # Empty-stack samples are recorded as STACK_REPEAT records with + # depth-0 stacks; the file must replay all three samples. + self.assertEqual(count, 3) + + def test_writer_handles_mixed_empty_and_real_first_sample(self): + """First sample with one empty + one real thread roundtrips through C.""" + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: + filename = f.name + self.temp_files.append(filename) + + writer = _remote_debugging.BinaryWriter(filename, 1000, 0, compression=0) + sample = [ + make_interpreter( + 0, + [ + make_thread(1, [make_frame("a.py", 1, "f")]), + make_thread(99, [], status=THREAD_STATUS_UNKNOWN), + ], + ) + ] + # Two samples so RLE state is exercised. + writer.write_sample(sample, 1000) + writer.write_sample(sample, 2000) + writer.finalize() + + # Replay must succeed without raising RuntimeError, and the real + # thread's frames must round-trip. + reader_collector = RawCollector() + with BinaryReader(filename) as reader: + reader.replay_samples(reader_collector) + self.assertIn((0, 1), reader_collector.by_thread) + self.assertEqual(len(reader_collector.by_thread[(0, 1)]), 2) + + def test_writer_total_samples_after_finalize_matches_reader(self): + """BinaryWriter.total_samples after finalize() matches the reader's count.""" + # Five IDENTICAL samples force every sample beyond the first into the + # per-thread RLE buffer. Regression for the cached_total_samples + # ordering bug: capturing the cache BEFORE binary_writer_finalize() + # missed the buffered samples that flush_pending_rle() counts. Keep + # the samples identical to preserve coverage. See gh-149342. + samples = [ + [make_interpreter(0, [make_thread(1, [make_frame("a.py", 1, "f")])])] + ] * 5 + filename, writer_collector = self.write_binary_file(samples) + reader_collector = RawCollector() + with BinaryReader(filename) as reader: + replayed = reader.replay_samples(reader_collector) + self.assertEqual(writer_collector.total_samples, len(samples)) + self.assertEqual(writer_collector.total_samples, replayed) + + def test_rle_buffer_flushes_before_finalize(self): + """RLE buffer flushes before the writer is destroyed during finalize().""" + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: + filename = f.name + self.temp_files.append(filename) + + frame = make_frame("rle_boundary.py", 1, "hot") + sample = [make_interpreter(0, [make_thread(1, [frame])])] + + writer = _remote_debugging.BinaryWriter(filename, 1000, 0, compression=0) + for i in range(10_000): + writer.write_sample(sample, i * 1000) + self.assertGreater(writer.get_stats()["repeat_records"], 0) + + writer.finalize() + + reader_collector = RawCollector() + with BinaryReader(filename) as reader: + replayed = reader.replay_samples(reader_collector) + + self.assertEqual(replayed, 10_000) + self.assertEqual(writer.total_samples, 10_000) + + def test_writer_total_samples_after_context_manager_matches_reader(self): + """total_samples after `with BinaryWriter(...)` matches the reader's count. + + Regression for the asymmetry between finalize() and __exit__ in + module.c: __exit__ also calls binary_writer_finalize and must + preserve cached_total_samples like finalize() does, otherwise the + getter returns 0 once self->writer is NULL. + """ + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: + filename = f.name + self.temp_files.append(filename) + + sample = [ + make_interpreter(0, [make_thread(1, [make_frame("a.py", 1, "f")])]) + ] + with _remote_debugging.BinaryWriter(filename, 1000, 0, compression=0) as w: + for i in range(5): + w.write_sample(sample, i * 1000) + self.assertEqual(w.total_samples, 5) + + reader_collector = RawCollector() + with BinaryReader(filename) as reader: + self.assertEqual(reader.replay_samples(reader_collector), 5) + + def test_writer_total_samples_after_close_returns_zero(self): + """close() discards data; total_samples reflects no cached count.""" + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: + filename = f.name + self.temp_files.append(filename) + + w = _remote_debugging.BinaryWriter(filename, 1000, 0, compression=0) + sample = [ + make_interpreter(0, [make_thread(1, [make_frame("a.py", 1, "f")])]) + ] + for i in range(5): + w.write_sample(sample, i * 1000) + w.close() + self.assertEqual(w.total_samples, 0) + + +class TestBinaryFormatValidation(BinaryFormatTestBase): + """Tests for malformed binary files.""" + + HDR_OFF_SAMPLES = 28 + HDR_OFF_THREADS = 36 + HDR_OFF_STR_TABLE = 40 + HDR_OFF_FRAME_TABLE = 48 + FILE_HEADER_PLACEHOLDER_SIZE = 64 + FILE_FOOTER_SIZE = 32 + FTR_OFF_STRINGS = 0 + FTR_OFF_FRAMES = 4 + + def test_replay_rejects_more_threads_than_declared(self): + """Replay rejects files with more unique threads than the header declares.""" + threads = [ + make_thread(1, [make_frame("t1.py", 10, "t1")]), + make_thread(2, [make_frame("t2.py", 20, "t2")]), + ] + samples = [[make_interpreter(0, threads)]] + filename = self.create_binary_file(samples, compression="none") + + with open(filename, "r+b") as raw: + raw.seek(self.HDR_OFF_THREADS) + raw.write(struct.pack("=I", 1)) + + with BinaryReader(filename) as reader: + self.assertEqual(reader.get_info()["thread_count"], 1) + with self.assertRaises(ValueError) as cm: + reader.replay_samples(RawCollector()) + self.assertEqual( + str(cm.exception), + "Invalid thread count: sample data contains more unique " + "threads than declared in header (declared 1, found at least 2)", + ) + + def test_replay_rejects_sample_count_mismatch(self): + """Replay rejects files whose decoded samples disagree with the header.""" + samples = [[make_interpreter(0, [ + make_thread(1, [make_frame("sample.py", 10, "sample")]) + ])]] + filename = self.create_binary_file(samples, compression="none") + + with open(filename, "r+b") as raw: + raw.seek(self.HDR_OFF_SAMPLES) + raw.write(struct.pack("=Q", 2)) + + with BinaryReader(filename) as reader: + self.assertEqual(reader.get_info()["sample_count"], 2) + with self.assertRaises(ValueError) as cm: + reader.replay_samples(RawCollector()) + self.assertEqual( + str(cm.exception), + "Sample count mismatch: header declares 2 samples " + "but replay decoded 1", + ) + + def test_replay_rejects_trailing_partial_sample_header(self): + """Replay rejects partial sample bytes instead of silently stopping.""" + filename = self.create_binary_file([], compression="none") + sample_data_end = self.FILE_HEADER_PLACEHOLDER_SIZE + 1 + + with open(filename, "r+b") as raw: + raw.seek(self.HDR_OFF_STR_TABLE) + raw.write(struct.pack("=Q", sample_data_end)) + raw.seek(self.HDR_OFF_FRAME_TABLE) + raw.write(struct.pack("=Q", sample_data_end)) + + with BinaryReader(filename) as reader: + with self.assertRaises(ValueError) as cm: + reader.replay_samples(RawCollector()) + self.assertEqual(str(cm.exception), "Truncated sample data: 1 trailing bytes") + + # Minimum on-disk size of one table entry (see binary_io.h). + MIN_STRING_ENTRY_SIZE = 1 + MIN_FRAME_ENTRY_SIZE = 7 + + def _read_offset(self, filename, hdr_off): + with open(filename, "rb") as raw: + raw.seek(hdr_off) + return struct.unpack("=Q", raw.read(8))[0] + + def _patch_footer_count(self, filename, ftr_off, value): + size = os.path.getsize(filename) + with open(filename, "r+b") as raw: + raw.seek(size - self.FILE_FOOTER_SIZE + ftr_off) + raw.write(struct.pack("=I", value)) + + def test_open_rejects_string_count_larger_than_file(self): + """Open rejects a footer string count larger than the file.""" + samples = [[make_interpreter(0, [ + make_thread(1, [make_frame("s.py", 10, "s")]) + ])]] + filename = self.create_binary_file(samples, compression="none") + size = os.path.getsize(filename) + str_off = self._read_offset(filename, self.HDR_OFF_STR_TABLE) + max_strings = (size - str_off) // self.MIN_STRING_ENTRY_SIZE + self._patch_footer_count(filename, self.FTR_OFF_STRINGS, 0xFFFFFFFF) + + with self.assertRaises(ValueError) as cm: + with BinaryReader(filename): + pass + self.assertEqual( + str(cm.exception), + f"Invalid string count 4294967295 exceeds maximum " + f"possible {max_strings}", + ) + + def test_open_rejects_frame_count_larger_than_file(self): + """Open rejects a footer frame count larger than the file.""" + samples = [[make_interpreter(0, [ + make_thread(1, [make_frame("f.py", 10, "f")]) + ])]] + filename = self.create_binary_file(samples, compression="none") + size = os.path.getsize(filename) + frame_off = self._read_offset(filename, self.HDR_OFF_FRAME_TABLE) + max_frames = (size - frame_off) // self.MIN_FRAME_ENTRY_SIZE + self._patch_footer_count(filename, self.FTR_OFF_FRAMES, 0xFFFFFFFF) + + # On a 32-bit build this count overflows the allocation size, so the + # size_t overflow guard (OverflowError) fires before the count check. + with self.assertRaises((ValueError, OverflowError)) as cm: + with BinaryReader(filename): + pass + if isinstance(cm.exception, ValueError): + self.assertEqual( + str(cm.exception), + f"Invalid frame count 4294967295 exceeds maximum " + f"possible {max_frames}", + ) + + def test_open_accepts_frame_count_at_capacity_boundary(self): + """A frame count at the file-size cap opens; one more is rejected.""" + samples = [[make_interpreter(0, [ + make_thread(1, [make_frame("f.py", 10, "f")]) + ])]] + filename = self.create_binary_file(samples, compression="none") + size = os.path.getsize(filename) + frame_off = self._read_offset(filename, self.HDR_OFF_FRAME_TABLE) + max_frames = (size - frame_off) // self.MIN_FRAME_ENTRY_SIZE + + self._patch_footer_count(filename, self.FTR_OFF_FRAMES, max_frames) + with BinaryReader(filename): + pass + + self._patch_footer_count(filename, self.FTR_OFF_FRAMES, max_frames + 1) + with self.assertRaises(ValueError) as cm: + with BinaryReader(filename): + pass + self.assertEqual( + str(cm.exception), + f"Invalid frame count {max_frames + 1} exceeds maximum " + f"possible {max_frames}", + ) + + def test_sample_count_reads_full_64_bits(self): + """sample_count values requiring the upper 32 bits decode correctly.""" + filename = self.create_binary_file([], compression="none") + big_count = 0x1_0002_0003 + + with open(filename, "r+b") as raw: + raw.seek(self.HDR_OFF_SAMPLES) + raw.write(struct.pack("=Q", big_count)) + + with BinaryReader(filename) as reader: + self.assertEqual(reader.get_info()["sample_count"], big_count) + + def test_sample_count_boundary_values(self): + """Values above the old u32 ceiling decode fine.""" + filename = self.create_binary_file([], compression="none") + + for value in (0xFFFFFFFF - 1, 0xFFFFFFFF, 0xFFFFFFFF + 1): + with self.subTest(value=value): + with open(filename, "r+b") as raw: + raw.seek(self.HDR_OFF_SAMPLES) + raw.write(struct.pack("=Q", value)) + + with BinaryReader(filename) as reader: + self.assertEqual(reader.get_info()["sample_count"], value) + + +class TestBinaryEncodings(BinaryFormatTestBase): + """Tests specifically targeting different stack encodings.""" + + def test_stack_full_encoding(self): + """First sample uses STACK_FULL encoding and roundtrips.""" + frames = [make_frame(f"f{i}.py", i + 1, f"func{i}") for i in range(5)] + samples = [[make_interpreter(0, [make_thread(1, frames)])]] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + + def test_stack_repeat_encoding(self): + """Identical consecutive samples use RLE and roundtrip.""" + frame = make_frame("repeat.py", 42, "repeat_func") + samples = [ + [make_interpreter(0, [make_thread(1, [frame])])] + for _ in range(1000) + ] + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1000) + self.assert_samples_equal(samples, collector) + + def test_stack_suffix_encoding(self): + """Samples sharing suffix use STACK_SUFFIX and roundtrip.""" + samples = [] + for i in range(10): + frames = [make_frame(f"new{i}.py", i + 1, f"new{i}")] + frames.extend( + [ + make_frame(f"shared{j}.py", j + 1, f"shared{j}") + for j in range(5) + ] + ) + samples.append([make_interpreter(0, [make_thread(1, frames)])]) + collector, count = self.roundtrip(samples) + self.assertEqual(count, 10) + self.assert_samples_equal(samples, collector) + + def test_stack_pop_push_encoding(self): + """Samples with pop+push pattern roundtrip.""" + samples = [] + base_frames = [make_frame("base.py", 10, "base")] + + # Call deeper + samples.append([make_interpreter(0, [make_thread(1, base_frames)])]) + samples.append( + [ + make_interpreter( + 0, + [ + make_thread( + 1, + [make_frame("call1.py", 20, "call1")] + + base_frames, + ) + ], + ) + ] + ) + samples.append( + [ + make_interpreter( + 0, + [ + make_thread( + 1, + [ + make_frame("call2.py", 30, "call2"), + make_frame("call1.py", 20, "call1"), + ] + + base_frames, + ) + ], + ) + ] + ) + # Return + samples.append( + [ + make_interpreter( + 0, + [ + make_thread( + 1, + [make_frame("call1.py", 25, "call1")] + + base_frames, + ) + ], + ) + ] + ) + samples.append([make_interpreter(0, [make_thread(1, base_frames)])]) + + collector, count = self.roundtrip(samples) + self.assertEqual(count, 5) + self.assert_samples_equal(samples, collector) + + def test_mixed_encodings(self): + """Mix of different encoding patterns roundtrips.""" + samples = [] + # Some repeated samples (RLE) + frame1 = make_frame("hot.py", 1, "hot") + for _ in range(20): + samples.append([make_interpreter(0, [make_thread(1, [frame1])])]) + # Some varying samples + for i in range(20): + frames = [make_frame(f"vary{i}.py", i + 1, f"vary{i}")] + samples.append([make_interpreter(0, [make_thread(1, frames)])]) + # More repeated + for _ in range(20): + samples.append([make_interpreter(0, [make_thread(1, [frame1])])]) + + collector, count = self.roundtrip(samples) + self.assertEqual(count, 60) + self.assert_samples_equal(samples, collector) + + def test_alternating_threads_status_changes(self): + """Alternating thread status changes roundtrip correctly.""" + samples = [] + for i in range(50): + status1 = THREAD_STATUS_HAS_GIL if i % 2 == 0 else 0 + status2 = ( + THREAD_STATUS_ON_CPU if i % 3 == 0 else THREAD_STATUS_HAS_GIL + ) + threads = [ + make_thread(1, [make_frame("t1.py", 10, "t1")], status1), + make_thread(2, [make_frame("t2.py", 20, "t2")], status2), + ] + samples.append([make_interpreter(0, threads)]) + collector, count = self.roundtrip(samples) + self.assertEqual(count, 100) + self.assert_samples_equal(samples, collector) + + +class TestBinaryStress(BinaryFormatTestBase): + """Randomized stress tests for binary format.""" + + @unittest.skipUnless(ZSTD_AVAILABLE, "zstd compression not available") + def test_random_samples_stress(self): + """Stress test with random samples - exercises hash table resizing.""" + random.seed(42) # Reproducible + + # Large pools to force hash table resizing (exceeds initial 8192/4096 sizes) + filenames = [f"file{i}.py" for i in range(200)] + funcnames = [f"func{i}" for i in range(300)] + thread_ids = list(range(1, 50)) + interp_ids = list(range(10)) + statuses = [ + 0, + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_ON_CPU, + THREAD_STATUS_HAS_GIL | THREAD_STATUS_ON_CPU, + THREAD_STATUS_HAS_EXCEPTION, + ] + + samples = [] + for _ in range(1000): + num_interps = random.randint(1, 3) + interps = [] + for _ in range(num_interps): + iid = random.choice(interp_ids) + num_threads = random.randint(1, 5) + threads = [] + for _ in range(num_threads): + tid = random.choice(thread_ids) + status = random.choice(statuses) + depth = random.randint(1, 15) + frames = [] + for _ in range(depth): + fname = random.choice(filenames) + func = random.choice(funcnames) + # Wide line number range to create many unique frames + lineno = random.randint(1, 5000) + frames.append(make_frame(fname, lineno, func)) + threads.append(make_thread(tid, frames, status)) + interps.append(make_interpreter(iid, threads)) + samples.append(interps) + + collector, count = self.roundtrip(samples, compression="zstd") + self.assertGreater(count, 0) + self.assert_samples_equal(samples, collector) + + def test_rle_stress(self): + """Stress test RLE encoding with identical samples.""" + random.seed(123) + + # Create a few distinct stacks + stacks = [] + for i in range(5): + depth = random.randint(1, 8) + frames = [ + make_frame(f"rle{j}.py", j * 10, f"rle{j}") + for j in range(depth) + ] + stacks.append(frames) + + # Generate samples with repeated stacks (should trigger RLE) + samples = [] + for _ in range(100): + stack = random.choice(stacks) + repeat = random.randint(1, 50) + for _ in range(repeat): + samples.append([make_interpreter(0, [make_thread(1, stack)])]) + + collector, count = self.roundtrip(samples) + self.assertEqual(count, len(samples)) + self.assert_samples_equal(samples, collector) + + @unittest.skipUnless(ZSTD_AVAILABLE, "zstd compression not available") + def test_multi_thread_stress(self): + """Stress test with many threads and interleaved samples.""" + random.seed(456) + + thread_ids = list(range(1, 20)) + samples = [] + + for i in range(300): + # Randomly select 1-5 threads for this sample + num_threads = random.randint(1, 5) + selected = random.sample(thread_ids, num_threads) + threads = [] + for tid in selected: + status = random.choice( + [0, THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU] + ) + depth = random.randint(1, 5) + frames = [ + make_frame(f"mt{tid}_{j}.py", i + j, f"f{j}") + for j in range(depth) + ] + threads.append(make_thread(tid, frames, status)) + samples.append([make_interpreter(0, threads)]) + + collector, count = self.roundtrip(samples, compression="zstd") + self.assertGreater(count, 0) + self.assert_samples_equal(samples, collector) + + def test_encoding_transitions_stress(self): + """Stress test stack encoding transitions.""" + random.seed(789) + + base_frames = [ + make_frame(f"base{i}.py", i, f"base{i}") for i in range(5) + ] + samples = [] + + for i in range(200): + choice = random.randint(0, 4) + if choice == 0: + # Full new stack + depth = random.randint(1, 8) + frames = [ + make_frame(f"new{i}_{j}.py", j, f"new{j}") + for j in range(depth) + ] + elif choice == 1: + # Repeat previous (will use RLE if identical) + frames = base_frames[: random.randint(1, 5)] + elif choice == 2: + # Add frames on top (suffix encoding) + extra = random.randint(1, 3) + frames = [ + make_frame(f"top{i}_{j}.py", j, f"top{j}") + for j in range(extra) + ] + frames.extend(base_frames[: random.randint(2, 4)]) + else: + # Pop and push (pop-push encoding) + keep = random.randint(1, 3) + push = random.randint(0, 2) + frames = [ + make_frame(f"push{i}_{j}.py", j, f"push{j}") + for j in range(push) + ] + frames.extend(base_frames[:keep]) + + samples.append([make_interpreter(0, [make_thread(1, frames)])]) + + collector, count = self.roundtrip(samples) + self.assertEqual(count, len(samples)) + self.assert_samples_equal(samples, collector) + + @unittest.skipUnless(ZSTD_AVAILABLE, "zstd compression not available") + def test_same_thread_id_multiple_interpreters_stress(self): + """Stress test: same thread_id across multiple interpreters with interleaved samples. + + This test catches bugs where thread state is keyed only by thread_id + without considering interpreter_id (both in writer and reader). + """ + random.seed(999) + + # Multiple interpreters, each with overlapping thread_ids + interp_ids = [0, 1, 2, 3] + # Same thread_ids used across all interpreters + shared_thread_ids = [1, 2, 3] + + filenames = [f"file{i}.py" for i in range(10)] + funcnames = [f"func{i}" for i in range(15)] + statuses = [0, THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU] + + samples = [] + for i in range(1000): + # Randomly pick an interpreter + iid = random.choice(interp_ids) + # Randomly pick 1-3 threads (from shared pool) + num_threads = random.randint(1, 3) + selected_tids = random.sample(shared_thread_ids, num_threads) + + threads = [] + for tid in selected_tids: + status = random.choice(statuses) + depth = random.randint(1, 6) + frames = [] + for d in range(depth): + # Include interpreter and thread info in frame data for verification + fname = f"i{iid}_t{tid}_{random.choice(filenames)}" + func = random.choice(funcnames) + lineno = i * 10 + d + 1 # Unique per sample + frames.append(make_frame(fname, lineno, func)) + threads.append(make_thread(tid, frames, status)) + + samples.append([make_interpreter(iid, threads)]) + + collector, count = self.roundtrip(samples, compression="zstd") + self.assertGreater(count, 0) + self.assert_samples_equal(samples, collector) + + # Verify that we have samples from multiple (interpreter, thread) combinations + # with the same thread_id + keys = set(collector.by_thread.keys()) + # Should have samples for same thread_id in different interpreters + for tid in shared_thread_ids: + interps_with_tid = [iid for (iid, t) in keys if t == tid] + self.assertGreater( + len(interps_with_tid), + 1, + f"Thread {tid} should appear in multiple interpreters", + ) + + +class TimestampCollector: + """Collector that captures timestamps for verification.""" + + def __init__(self): + self.all_timestamps = [] + + def collect(self, stack_frames, timestamps_us=None): + if timestamps_us is not None: + self.all_timestamps.extend(timestamps_us) + + def export(self, filename): + pass + + +class TestTimestampPreservation(BinaryFormatTestBase): + """Tests for timestamp preservation during binary round-trip.""" + + def test_timestamp_preservation(self): + """Timestamps are preserved during round-trip.""" + frame = make_frame("test.py", 10, "func") + timestamps = [1000000, 2000000, 3000000] + + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: + filename = f.name + self.temp_files.append(filename) + + collector = BinaryCollector(filename, 1000, compression="none") + for ts in timestamps: + sample = [make_interpreter(0, [make_thread(1, [frame])])] + collector.collect(sample, timestamp_us=ts) + collector.export(None) + + ts_collector = TimestampCollector() + with BinaryReader(filename) as reader: + count = reader.replay_samples(ts_collector) + + self.assertEqual(count, 3) + self.assertEqual(ts_collector.all_timestamps, timestamps) + + def test_timestamp_preservation_with_rle(self): + """RLE-batched samples preserve individual timestamps.""" + frame = make_frame("rle.py", 42, "rle_func") + + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: + filename = f.name + self.temp_files.append(filename) + + # Identical samples (triggers RLE) with different timestamps + collector = BinaryCollector(filename, 1000, compression="none") + expected_timestamps = [] + for i in range(50): + ts = 1000000 + i * 100 + expected_timestamps.append(ts) + sample = [make_interpreter(0, [make_thread(1, [frame])])] + collector.collect(sample, timestamp_us=ts) + collector.export(None) + + ts_collector = TimestampCollector() + with BinaryReader(filename) as reader: + count = reader.replay_samples(ts_collector) + + self.assertEqual(count, 50) + self.assertEqual(ts_collector.all_timestamps, expected_timestamps) + + +class TestBinaryReplayToFlamegraph(BinaryFormatTestBase): + def test_replay_includes_persisted_stats(self): + frames = [ + make_frame("hot.py", 99, "hot_func"), + make_frame("main.py", 1, "main"), + ] + samples = [ + [ + make_interpreter( + 0, + [make_thread(1, frames, THREAD_STATUS_HAS_GIL)], + ) + ] + for _ in range(5) + ] + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as file: + bin_path = file.name + self.temp_files.append(bin_path) + collector = BinaryCollector(bin_path, 2000, compression="none") + for sample in samples: + collector.collect(sample) + collector.set_stats( + 2000, 1.25, 4.0, error_rate=2.5, missed_samples=1.5 + ) + collector.export(None) + + with BinaryReader(bin_path) as reader: + info = reader.get_info() + self.assertEqual(info["duration_sec"], 1.25) + self.assertEqual(info["sample_rate"], 4.0) + self.assertEqual(info["error_rate"], 2.5) + self.assertEqual(info["missed_samples"], 1.5) + + with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as file: + html_path = file.name + self.temp_files.append(html_path) + + convert_binary_to_format(bin_path, html_path, "flamegraph") + + with open(html_path, encoding="utf-8") as file: + content = file.read() + self.assertIn('"duration_sec": 1.25', content) + self.assertIn('"sample_rate": 4.0', content) + self.assertIn('"error_rate": 2.5', content) + self.assertIn('"missed_samples": 1.5', content) + + def test_legacy_binary_has_no_measured_stats(self): + frame = make_frame("hot.py", 99, "hot_func") + bin_path = self.create_binary_file([ + [make_interpreter(0, [make_thread(1, [frame])])] + ]) + + with BinaryReader(bin_path) as reader: + info = reader.get_info() + + self.assertIsNone(info["duration_sec"]) + self.assertIsNone(info["sample_rate"]) + self.assertIsNone(info["error_rate"]) + self.assertIsNone(info["missed_samples"]) + + def test_original_stats_extension_remains_readable(self): + frame = make_frame("hot.py", 99, "hot_func") + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as file: + bin_path = file.name + self.temp_files.append(bin_path) + collector = BinaryCollector(bin_path, 2000, compression="none") + collector.collect([ + make_interpreter(0, [make_thread(1, [frame])]) + ]) + collector.set_stats(2000, 1.25, 4.0) + collector.export(None) + + with open(bin_path, "rb") as file: + data = file.read() + stats = data[-88:-32] + footer = bytearray(data[-32:]) + old_stats = stats[:16] + b"TACHSTAT" + struct.pack("=II", 1, 32) + old_data = bytearray(data[:-88] + old_stats + footer) + struct.pack_into("=Q", old_data, -24, len(old_data)) + with open(bin_path, "wb") as file: + file.write(old_data) + + with BinaryReader(bin_path) as reader: + info = reader.get_info() + self.assertEqual(info["duration_sec"], 1.25) + self.assertEqual(info["sample_rate"], 4.0) + self.assertIsNone(info["error_rate"]) + self.assertIsNone(info["missed_samples"]) + + +class TestBinaryReplayToJsonl(BinaryFormatTestBase): + """Tests for binary -> JSONL replay via convert_binary_to_format.""" + + def _replay_to_jsonl(self, samples, interval=1000, mode=None): + bin_path = self.create_binary_file( + samples, interval=interval, mode=mode + ) + with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=False) as f: + jsonl_path = f.name + self.temp_files.append(jsonl_path) + + convert_binary_to_format(bin_path, jsonl_path, "jsonl") + + with open(jsonl_path, "r", encoding="utf-8") as f: + return [json.loads(line) for line in f] + + def test_binary_replay_to_jsonl_basic(self): + """Replay a small .bin to JSONL: meta/end shape, samples_total, run_id.""" + frame = make_frame("hot.py", 99, "hot_func") + samples = [ + [make_interpreter(0, [make_thread(1, [frame])])] + for _ in range(5) + ] + records = self._replay_to_jsonl(samples, interval=2000) + meta, _, frame_defs, _, end = jsonl_tables(records) + + self.assertEqual(meta["sample_interval_usec"], 2000) + self.assertEqual(end["samples_total"], 5) + + run_ids = {r["run_id"] for r in records} + self.assertEqual(len(run_ids), 1) + self.assertRegex(next(iter(run_ids)), r"^[0-9a-f]{32}$") + + self.assertEqual(len(frame_defs), 1) + self.assertEqual(frame_defs[0]["line"], 99) + + def test_binary_replay_to_jsonl_preserves_mode(self): + frame = make_frame("hot.py", 99, "hot_func") + records = self._replay_to_jsonl( + [[make_interpreter(0, [make_thread(1, [frame])])]], + mode=PROFILING_MODE_CPU, + ) + + meta = next(record for record in records if record["type"] == "meta") + self.assertEqual(meta["mode"], "cpu") + + def test_binary_replay_to_jsonl_rle_weight_propagation(self): + """RLE-batched identical samples land as a single agg entry with the right total.""" + frame = make_frame("rle.py", 42, "rle_func") + samples = [ + [make_interpreter(0, [make_thread(1, [frame])])] + for _ in range(50) + ] + records = self._replay_to_jsonl(samples) + _, _, _, agg, end = jsonl_tables(records) + + self.assertEqual(end["samples_total"], 50) + self.assertEqual(agg["entries"], [ + {"frame_id": 0, "self": 50, "cumulative": 50}, + ]) + + def test_binary_replay_to_jsonl_omits_unavailable_columns(self): + """Columns the binary recorder did not capture are omitted, not 0.""" + # make_frame defaults column/end_column to 0; pass column=-1 / end_column=-1 + # so the binary side records LOCATION_NOT_AVAILABLE. + frame = make_frame("nocol.py", 7, "no_col", column=-1, end_column=-1) + samples = [[make_interpreter(0, [make_thread(1, [frame])])]] + records = self._replay_to_jsonl(samples) + _, _, frame_defs, _, _ = jsonl_tables(records) + + self.assertEqual(len(frame_defs), 1) + fd = frame_defs[0] + self.assertEqual(fd["line"], 7) + self.assertNotIn("col", fd) + self.assertNotIn("end_col", fd) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_blocking.py b/Lib/test/test_profiling/test_sampling_profiler/test_blocking.py new file mode 100644 index 00000000000..0a5541c733d --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_blocking.py @@ -0,0 +1,215 @@ +"""Tests for blocking mode sampling profiler.""" + +import io +import os +import subprocess +import sys +import textwrap +import unittest +from unittest import mock + +try: + import _remote_debugging # noqa: F401 + import profiling.sampling + import profiling.sampling.sample + from profiling.sampling.stack_collector import CollapsedStackCollector +except ImportError: + raise unittest.SkipTest( + "Test only runs when _remote_debugging is available" + ) + +from test.support import ( + SHORT_TIMEOUT, + os_helper, + requires_remote_subprocess_debugging, +) + +from .helpers import test_subprocess + +# Duration for profiling in tests +PROFILING_DURATION_SEC = 1 + + +@requires_remote_subprocess_debugging() +class TestBlockingModeStackAccuracy(unittest.TestCase): + """Test that blocking mode produces accurate stack traces. + + When using blocking mode, the target process is stopped during sampling. + This ensures that we see accurate stack traces where functions appear + in the correct caller/callee relationship. + + These tests verify that generator functions are correctly shown at the + top of the stack when they are actively executing, and not incorrectly + shown under their caller's code. + """ + + @classmethod + def setUpClass(cls): + # Test script that uses a generator consumed in a loop. + # When consume_generator is the executing leaf frame on the arithmetic + # lines (temp1, temp2, etc.), fibonacci_generator should NOT be in the + # stack at all. + # Line numbers are important here - see ARITHMETIC_LINES below. + cls.generator_script = textwrap.dedent(''' + def fibonacci_generator(n): + a, b = 0, 1 + for _ in range(n): + yield a + a, b = b, a + b + + def consume_generator(): + gen = fibonacci_generator(10000) + for value in gen: + temp1 = value + 1 + temp2 = value * 2 + temp3 = value - 1 + result = temp1 + temp2 + temp3 + + def main(): + while True: + consume_generator() + + _test_sock.sendall(b"working") + main() + ''') + # Line numbers of the arithmetic operations in consume_generator. + # These are the lines where fibonacci_generator should NOT be in the + # stack when consume_generator is the executing leaf frame. They account + # for the socket prelude added by test_subprocess(). + # temp1 = value + 1 -> line 16 + # temp2 = value * 2 -> line 17 + # temp3 = value - 1 -> line 18 + # result = ... -> line 19 + cls.ARITHMETIC_LINES = {16, 17, 18, 19} + + def test_generator_not_under_consumer_arithmetic(self): + """Test that fibonacci_generator doesn't appear when consume_generator does arithmetic. + + When consume_generator is the leaf frame on arithmetic lines (temp1, + temp2, etc.), fibonacci_generator should NOT be anywhere in the stack - + it's not being called at that point. Non-leaf frame line numbers are + caller/resume metadata, not proof that the frame is executing. + + Valid stacks: + - fibonacci_generator at the top (generator is executing), with + consume_generator below it + - consume_generator at arithmetic lines WITHOUT fibonacci_generator + (we're just doing math, not calling the generator) + + Invalid stacks (indicate torn/inconsistent reads): + - consume_generator leaf frame at arithmetic lines WITH + fibonacci_generator + anywhere in the stack + + Note: call_tree is ordered from bottom (index 0) to top (index -1). + """ + with test_subprocess(self.generator_script, wait_for_working=True) as subproc: + collector = CollapsedStackCollector(sample_interval_usec=100, skip_idle=False) + + with ( + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=PROFILING_DURATION_SEC, + blocking=True, + ) + + # Analyze collected stacks + total_samples = 0 + invalid_stacks = 0 + arithmetic_samples = 0 + generator_samples = 0 + generator_not_leaf_samples = 0 + + for (call_tree, _thread_id), count in collector.stack_counter.items(): + total_samples += count + + if not call_tree: + continue + + # Non-leaf frame line numbers can point at resume locations while + # a callee is the executing leaf frame. + _, lineno, funcname = call_tree[-1] + func_names = [frame[2] for frame in call_tree] + + if "fibonacci_generator" in func_names: + generator_samples += count + if funcname != "fibonacci_generator": + generator_not_leaf_samples += count + + if funcname == "consume_generator" and lineno in self.ARITHMETIC_LINES: + arithmetic_samples += count + # Check if fibonacci_generator appears anywhere in this stack. + if "fibonacci_generator" in func_names: + invalid_stacks += count + + self.assertGreater(total_samples, 10, + f"Expected at least 10 samples, got {total_samples}") + + # We should have some samples on the arithmetic lines + self.assertGreater(arithmetic_samples, 0, + f"Expected some samples on arithmetic lines, got {arithmetic_samples}") + + self.assertGreater(generator_samples, 0, + f"Expected some samples in fibonacci_generator, got {generator_samples}") + + self.assertEqual(generator_not_leaf_samples, 0, + f"Found {generator_not_leaf_samples}/{generator_samples} stacks where " + f"fibonacci_generator appears but is not the leaf frame.") + + self.assertEqual(invalid_stacks, 0, + f"Found {invalid_stacks}/{arithmetic_samples} invalid stacks where " + f"fibonacci_generator appears in the stack when consume_generator " + f"is the leaf frame on an arithmetic line. This indicates " + f"torn/inconsistent stack traces are being captured.") + + +@requires_remote_subprocess_debugging() +@unittest.skipUnless(sys.platform == "win32", "Windows only") +class TestBlockingModeCLI(unittest.TestCase): + def test_run_blocking_exits_after_target_process_exits(self): + script = 'print("done")\n' + + tmpdir = os.path.abspath(os_helper.TESTFN + "_profiling_blocking") + with os_helper.temp_dir(tmpdir) as tmpdir: + script_path = os.path.join(tmpdir, "tiny_target.py") + profile_path = os.path.join(tmpdir, "blocking.bin") + with open(script_path, "w", encoding="utf-8") as file: + file.write(script) + + cmd = [ + sys.executable, "-m", "profiling.sampling", "run", + "--binary", "-o", profile_path, + "--mode=cpu", "--blocking", "-r", "100", + script_path, + ] + result = subprocess.run( + cmd, + cwd=tmpdir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=SHORT_TIMEOUT, + ) + + self.assertEqual( + result.returncode, 0, + f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}", + ) + self.assertGreater(os.path.getsize(profile_path), 0) + + replay = subprocess.run( + [sys.executable, "-m", "profiling.sampling", "replay", + profile_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=SHORT_TIMEOUT, + ) + self.assertEqual( + replay.returncode, 0, + f"stdout:\n{replay.stdout}\nstderr:\n{replay.stderr}", + ) diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_children.py b/Lib/test/test_profiling/test_sampling_profiler/test_children.py new file mode 100644 index 00000000000..e64d917eedd --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_children.py @@ -0,0 +1,1082 @@ +"""Tests for --subprocesses subprocess profiling support.""" + +import argparse +import io +import os +import signal +import subprocess +import sys +import tempfile +import threading +import time +import unittest +from unittest.mock import MagicMock, patch + +from test.support import ( + SHORT_TIMEOUT, + reap_children, + requires_remote_subprocess_debugging, +) + +# Guard imports that require _remote_debugging module. +# This module is not available on all platforms (e.g., WASI). +try: + from profiling.sampling._child_monitor import ( + get_child_pids, + ChildProcessMonitor, + is_python_process, + _MAX_CHILD_PROFILERS, + _CLEANUP_INTERVAL_CYCLES, + ) +except ImportError: + # Module will be skipped via @requires_remote_subprocess_debugging decorators + get_child_pids = None + ChildProcessMonitor = None + is_python_process = None + _MAX_CHILD_PROFILERS = None + _CLEANUP_INTERVAL_CYCLES = None + +try: + from profiling.sampling.cli import ( + _add_sampling_options, + _validate_args, + _build_child_profiler_args, + _build_output_pattern, + _setup_child_monitor, + ) +except ImportError: + # cli module imports sample module which requires _remote_debugging + _add_sampling_options = None + _validate_args = None + _build_child_profiler_args = None + _build_output_pattern = None + _setup_child_monitor = None + +from .helpers import _cleanup_process + +# String to check for in stderr when profiler lacks permissions (e.g., macOS) +_PERMISSION_ERROR_MSG = "Permission Error" + + +def _readline_with_timeout(file_obj, timeout): + # Thread-based readline with timeout - works across all platforms + # including Windows where select() doesn't work with pipes. + # Returns the line read, or None if timeout occurred. + result = [None] + exception = [None] + + def reader(): + try: + result[0] = file_obj.readline() + except Exception as e: + exception[0] = e + + thread = threading.Thread(target=reader, daemon=True) + thread.start() + thread.join(timeout=timeout) + + if thread.is_alive(): + return None + + if exception[0] is not None: + raise exception[0] + + return result[0] + + +def _wait_for_process_ready(proc, timeout): + # Wait for a subprocess to be ready using polling instead of fixed sleep. + # Returns True if process is ready, False if it exited or timeout. + deadline = time.time() + timeout + poll_interval = 0.01 + + while time.time() < deadline: + if proc.poll() is not None: + return False + + try: + if sys.platform == "linux": + if os.path.exists(f"/proc/{proc.pid}/exe"): + return True + else: + return True + except OSError: + pass + + time.sleep(poll_interval) + poll_interval = min(poll_interval * 2, 0.1) + + return proc.poll() is None + + +@unittest.skipIf( + _build_child_profiler_args is None, + "profiling.sampling.cli unavailable", +) +class TestChildProfilerArgBuilder(unittest.TestCase): + """Tests for child profiler CLI argument construction.""" + + def test_build_child_profiler_args_diff_flamegraph(self): + """Test child args use the real --diff-flamegraph flag.""" + args = argparse.Namespace( + sample_interval_usec=1000, + duration=None, + all_threads=False, + realtime_stats=False, + native=False, + gc=True, + opcodes=False, + async_aware=False, + mode="wall", + format="diff_flamegraph", + diff_baseline="baseline.bin", + ) + + child_args = _build_child_profiler_args(args) + + self.assertIn("--diff-flamegraph", child_args) + self.assertNotIn("--diff_flamegraph", child_args) + + flag_index = child_args.index("--diff-flamegraph") + self.assertGreater(len(child_args), flag_index + 1) + self.assertEqual(child_args[flag_index + 1], "baseline.bin") + + +@requires_remote_subprocess_debugging() +class TestGetChildPids(unittest.TestCase): + """Tests for the get_child_pids function.""" + + def setUp(self): + reap_children() + + def tearDown(self): + reap_children() + + def test_get_child_pids_from_remote_debugging(self): + """Test get_child_pids from _remote_debugging module.""" + try: + import _remote_debugging + + # Test that the function exists + self.assertTrue(hasattr(_remote_debugging, "get_child_pids")) + + # Test with current process (should return empty or have children if any) + result = _remote_debugging.get_child_pids(os.getpid()) + self.assertIsInstance(result, list) + except (ImportError, AttributeError): + self.skipTest("_remote_debugging.get_child_pids not available") + + def test_get_child_pids_fallback(self): + """Test the fallback implementation for get_child_pids.""" + # Test with current process + result = get_child_pids(os.getpid()) + self.assertIsInstance(result, list) + + @unittest.skipUnless(sys.platform == "linux", "Linux only") + def test_discover_child_process_linux(self): + """Test that we can discover child processes on Linux.""" + # Create a child process + proc = subprocess.Popen( + [sys.executable, "-c", "import time; time.sleep(10)"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + try: + # Poll until child appears + deadline = time.time() + SHORT_TIMEOUT + children = [] + while time.time() < deadline: + children = get_child_pids(os.getpid()) + if proc.pid in children: + break + time.sleep(0.05) + + self.assertIn( + proc.pid, + children, + f"Child PID {proc.pid} not discovered within {SHORT_TIMEOUT}s. " + f"Found PIDs: {children}", + ) + finally: + _cleanup_process(proc) + + def test_recursive_child_discovery(self): + """Test that recursive=True finds grandchildren.""" + # Create a child that spawns a grandchild and keeps a reference to it + # so we can clean it up via the child process + code = """ +import subprocess +import sys +import threading +grandchild = subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(60)']) +print(grandchild.pid, flush=True) +# Wait for parent to send signal byte (cross-platform) +# Using threading with timeout so test doesn't hang if something goes wrong +# Timeout is 60s (2x test timeout) to ensure child outlives test in worst case +def wait_for_signal(): + try: + sys.stdin.buffer.read(1) + except: + pass +t = threading.Thread(target=wait_for_signal, daemon=True) +t.start() +t.join(timeout=60) +# Clean up grandchild before exiting +if grandchild.poll() is None: + grandchild.terminate() + try: + grandchild.wait(timeout=2) + except subprocess.TimeoutExpired: + grandchild.kill() + try: + grandchild.wait(timeout=2) + except subprocess.TimeoutExpired: + grandchild.wait() +""" + proc = subprocess.Popen( + [sys.executable, "-c", code], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + + grandchild_pid = None + try: + # Read grandchild PID with thread-based timeout + # This prevents indefinite blocking on all platforms + grandchild_pid_line = _readline_with_timeout( + proc.stdout, SHORT_TIMEOUT + ) + if grandchild_pid_line is None: + self.fail( + f"Timeout waiting for grandchild PID from child process " + f"(child PID: {proc.pid})" + ) + if not grandchild_pid_line: + self.fail( + f"Child process {proc.pid} closed stdout without printing " + f"grandchild PID" + ) + grandchild_pid = int(grandchild_pid_line.strip()) + + # Poll until grandchild is visible + deadline = time.time() + SHORT_TIMEOUT + pids_recursive = [] + while time.time() < deadline: + pids_recursive = get_child_pids(os.getpid(), recursive=True) + if grandchild_pid in pids_recursive: + break + time.sleep(0.05) + + self.assertIn( + proc.pid, + pids_recursive, + f"Child PID {proc.pid} not found in recursive discovery. " + f"Found: {pids_recursive}", + ) + self.assertIn( + grandchild_pid, + pids_recursive, + f"Grandchild PID {grandchild_pid} not found in recursive discovery. " + f"Found: {pids_recursive}", + ) + + # Non-recursive should find only direct child + pids_direct = get_child_pids(os.getpid(), recursive=False) + self.assertIn( + proc.pid, + pids_direct, + f"Child PID {proc.pid} not found in non-recursive discovery. " + f"Found: {pids_direct}", + ) + self.assertNotIn( + grandchild_pid, + pids_direct, + f"Grandchild PID {grandchild_pid} should NOT be in non-recursive " + f"discovery. Found: {pids_direct}", + ) + finally: + # Send signal byte to child to trigger cleanup, then close stdin + try: + proc.stdin.write(b"x") + proc.stdin.flush() + proc.stdin.close() + except OSError: + pass + proc.stdout.close() + _cleanup_process(proc) + # The grandchild may not have been cleaned up by the child process + # (e.g., if the child was killed). Explicitly terminate the + # grandchild to prevent PermissionError on Windows when removing + # temp directories. + if grandchild_pid is not None: + try: + os.kill(grandchild_pid, signal.SIGTERM) + except (OSError, ProcessLookupError): + pass # Process already exited + + def test_nonexistent_pid_returns_empty(self): + """Test that nonexistent PID returns empty list.""" + # Use a very high PID that's unlikely to exist + result = get_child_pids(999999999) + self.assertEqual(result, []) + + +@requires_remote_subprocess_debugging() +class TestChildProcessMonitor(unittest.TestCase): + """Tests for the ChildProcessMonitor class.""" + + def setUp(self): + reap_children() + + def tearDown(self): + reap_children() + + def test_monitor_creation(self): + """Test that ChildProcessMonitor can be created.""" + monitor = ChildProcessMonitor( + pid=os.getpid(), + cli_args=["-r", "10khz", "-d", "5"], + output_pattern="test_{pid}.pstats", + ) + self.assertEqual(monitor.parent_pid, os.getpid()) + self.assertEqual(monitor.cli_args, ["-r", "10khz", "-d", "5"]) + self.assertEqual(monitor.output_pattern, "test_{pid}.pstats") + + def test_monitor_lifecycle(self): + """Test monitor lifecycle via context manager.""" + monitor = ChildProcessMonitor( + pid=os.getpid(), cli_args=[], output_pattern=None + ) + + # Before entering context, thread should not exist + self.assertIsNone(monitor._monitor_thread) + + with monitor: + # Inside context, thread should be running + self.assertIsNotNone(monitor._monitor_thread) + self.assertTrue(monitor._monitor_thread.is_alive()) + + # After exiting context, thread should be stopped + self.assertFalse(monitor._monitor_thread.is_alive()) + + def test_spawned_profilers_property(self): + """Test that spawned_profilers returns a copy of the list.""" + monitor = ChildProcessMonitor( + pid=os.getpid(), cli_args=[], output_pattern=None + ) + + # Should return empty list initially + profilers = monitor.spawned_profilers + self.assertEqual(profilers, []) + self.assertIsNot(profilers, monitor._spawned_profilers) + + def test_context_manager(self): + """Test that ChildProcessMonitor works as a context manager.""" + with ChildProcessMonitor( + pid=os.getpid(), cli_args=[], output_pattern=None + ) as monitor: + self.assertIsNotNone(monitor._monitor_thread) + self.assertTrue(monitor._monitor_thread.is_alive()) + + # After exiting context, thread should be stopped + self.assertFalse(monitor._monitor_thread.is_alive()) + + +@requires_remote_subprocess_debugging() +class TestCLIChildrenFlag(unittest.TestCase): + """Tests for the --subprocesses CLI flag.""" + + def setUp(self): + reap_children() + + def tearDown(self): + reap_children() + + def test_subprocesses_flag_parsed(self): + """Test that --subprocesses flag is recognized.""" + parser = argparse.ArgumentParser() + _add_sampling_options(parser) + + # Parse with --subprocesses + args = parser.parse_args(["--subprocesses"]) + self.assertTrue(args.subprocesses) + + # Parse without --subprocesses + args = parser.parse_args([]) + self.assertFalse(args.subprocesses) + + def test_subprocesses_incompatible_with_live(self): + """Test that --subprocesses is incompatible with --live.""" + # Create mock args with both subprocesses and live + args = argparse.Namespace( + subprocesses=True, + live=True, + async_aware=False, + format="pstats", + mode="wall", + sort=None, + limit=None, + no_summary=False, + opcodes=False, + blocking=False, + interval=1000, + ) + + parser = argparse.ArgumentParser() + + with self.assertRaises(SystemExit): + _validate_args(args, parser) + + def test_build_child_profiler_args(self): + """Test building CLI args for child profilers.""" + args = argparse.Namespace( + sample_interval_usec=200, + duration=15, + all_threads=True, + realtime_stats=False, + native=True, + gc=True, + opcodes=False, + async_aware=False, + mode="cpu", + format="flamegraph", + ) + + child_args = _build_child_profiler_args(args) + + # Verify flag-value pairs are correctly paired (flag followed by value) + def assert_flag_value_pair(flag, value): + self.assertIn( + flag, + child_args, + f"Flag '{flag}' not found in args: {child_args}", + ) + flag_index = child_args.index(flag) + self.assertGreater( + len(child_args), + flag_index + 1, + f"No value after flag '{flag}' in args: {child_args}", + ) + self.assertEqual( + child_args[flag_index + 1], + str(value), + f"Flag '{flag}' should be followed by '{value}', got " + f"'{child_args[flag_index + 1]}' in args: {child_args}", + ) + + assert_flag_value_pair("-r", 5000) + assert_flag_value_pair("-d", 15) + assert_flag_value_pair("--mode", "cpu") + + # Verify standalone flags are present + self.assertIn( + "-a", child_args, f"Flag '-a' not found in args: {child_args}" + ) + self.assertIn( + "--native", + child_args, + f"Flag '--native' not found in args: {child_args}", + ) + self.assertIn( + "--flamegraph", + child_args, + f"Flag '--flamegraph' not found in args: {child_args}", + ) + self.assertNotIn( + "--browser", + child_args, + f"Flag '--browser' should not be in child args: {child_args}", + ) + + def test_build_child_profiler_args_no_gc(self): + """Test building CLI args with --no-gc.""" + args = argparse.Namespace( + sample_interval_usec=100, + duration=5, + all_threads=False, + realtime_stats=False, + native=False, + gc=False, # Explicitly disabled + opcodes=False, + async_aware=False, + mode="wall", + format="pstats", + ) + + child_args = _build_child_profiler_args(args) + + self.assertIn( + "--no-gc", + child_args, + f"Flag '--no-gc' not found when gc=False. Args: {child_args}", + ) + + def test_build_output_pattern_with_outfile(self): + """Test output pattern generation with user-specified output.""" + # With extension + args = argparse.Namespace(outfile="output.html", format="flamegraph") + pattern = _build_output_pattern(args) + self.assertEqual(pattern, "output_{pid}.html") + + # Without extension + args = argparse.Namespace(outfile="output", format="pstats") + pattern = _build_output_pattern(args) + self.assertEqual(pattern, "output_{pid}") + + def test_build_output_pattern_default(self): + """Test output pattern generation with default output.""" + # Flamegraph format + args = argparse.Namespace(outfile=None, format="flamegraph") + pattern = _build_output_pattern(args) + self.assertIn("{pid}", pattern) + self.assertIn("flamegraph", pattern) + self.assertTrue(pattern.endswith(".html")) + + # Heatmap format + args = argparse.Namespace(outfile=None, format="heatmap") + pattern = _build_output_pattern(args) + self.assertEqual(pattern, "heatmap_{pid}") + + +@requires_remote_subprocess_debugging() +class TestChildrenIntegration(unittest.TestCase): + """Integration tests for --subprocesses functionality.""" + + def setUp(self): + reap_children() + + def tearDown(self): + reap_children() + + def test_setup_child_monitor(self): + """Test setting up a child monitor from args.""" + args = argparse.Namespace( + sample_interval_usec=100, + duration=5, + all_threads=False, + realtime_stats=False, + native=False, + gc=True, + opcodes=False, + async_aware=False, + mode="wall", + format="pstats", + outfile=None, + ) + + monitor = _setup_child_monitor(args, os.getpid()) + # Use addCleanup to ensure monitor is properly cleaned up even if + # assertions fail + self.addCleanup(monitor.__exit__, None, None, None) + + self.assertIsNotNone(monitor) + self.assertEqual( + monitor.parent_pid, + os.getpid(), + f"Monitor parent_pid should be {os.getpid()}, got {monitor.parent_pid}", + ) + + +@requires_remote_subprocess_debugging() +class TestIsPythonProcess(unittest.TestCase): + """Tests for the is_python_process function.""" + + def setUp(self): + reap_children() + + def tearDown(self): + reap_children() + + def test_is_python_process_current_process(self): + """Test that current process is detected as Python.""" + # Current process should be Python + result = is_python_process(os.getpid()) + self.assertTrue( + result, + f"Current process (PID {os.getpid()}) should be detected as Python", + ) + + def test_is_python_process_python_subprocess(self): + """Test that a Python subprocess is detected as Python.""" + # Start a Python subprocess + proc = subprocess.Popen( + [sys.executable, "-c", "import time; time.sleep(10)"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + try: + # Poll until Python runtime structures are initialized + # (is_python_process probes for runtime structures which take + # time to initialize after process start) + deadline = time.time() + SHORT_TIMEOUT + detected = False + while time.time() < deadline: + if proc.poll() is not None: + self.fail(f"Process {proc.pid} exited unexpectedly") + if is_python_process(proc.pid): + detected = True + break + time.sleep(0.05) + + self.assertTrue( + detected, + f"Python subprocess (PID {proc.pid}) should be detected as Python " + f"within {SHORT_TIMEOUT}s", + ) + finally: + _cleanup_process(proc) + + @unittest.skipUnless(sys.platform == "linux", "Linux only test") + def test_is_python_process_non_python_subprocess(self): + """Test that a non-Python subprocess is not detected as Python.""" + # Start a non-Python subprocess (sleep command) + proc = subprocess.Popen( + ["sleep", "10"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + try: + # Wait for process to be ready using polling + self.assertTrue( + _wait_for_process_ready(proc, SHORT_TIMEOUT), + f"Process {proc.pid} should be ready within {SHORT_TIMEOUT}s", + ) + + self.assertFalse( + is_python_process(proc.pid), + f"Non-Python subprocess 'sleep' (PID {proc.pid}) should NOT be " + f"detected as Python", + ) + finally: + _cleanup_process(proc) + + def test_is_python_process_nonexistent_pid(self): + """Test that nonexistent PID returns False.""" + # Use a very high PID that's unlikely to exist + result = is_python_process(999999999) + self.assertFalse( + result, + "Nonexistent PID 999999999 should return False", + ) + + def test_is_python_process_exited_process(self): + """Test handling of a process that exits quickly.""" + # Start a process that exits immediately + proc = subprocess.Popen( + [sys.executable, "-c", "pass"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + # Wait for it to exit + proc.wait(timeout=SHORT_TIMEOUT) + + # Should return False for exited process (not raise) + result = is_python_process(proc.pid) + self.assertFalse( + result, f"Exited process (PID {proc.pid}) should return False" + ) + + +@requires_remote_subprocess_debugging() +class TestMaxChildProfilersLimit(unittest.TestCase): + """Tests for the _MAX_CHILD_PROFILERS limit.""" + + def setUp(self): + reap_children() + + def tearDown(self): + reap_children() + + def test_max_profilers_constant_exists(self): + """Test that _MAX_CHILD_PROFILERS constant is defined.""" + self.assertEqual( + _MAX_CHILD_PROFILERS, + 100, + f"_MAX_CHILD_PROFILERS should be 100, got {_MAX_CHILD_PROFILERS}", + ) + + def test_cleanup_interval_constant_exists(self): + """Test that _CLEANUP_INTERVAL_CYCLES constant is defined.""" + self.assertEqual( + _CLEANUP_INTERVAL_CYCLES, + 10, + f"_CLEANUP_INTERVAL_CYCLES should be 10, got {_CLEANUP_INTERVAL_CYCLES}", + ) + + def test_monitor_respects_max_limit(self): + """Test that monitor refuses to spawn more than _MAX_CHILD_PROFILERS.""" + # Create a monitor + monitor = ChildProcessMonitor( + pid=os.getpid(), + cli_args=["-r", "10khz", "-d", "5"], + output_pattern="test_{pid}.pstats", + ) + + # Manually fill up the profilers list to the limit + mock_profilers = [MagicMock() for _ in range(_MAX_CHILD_PROFILERS)] + for mock_proc in mock_profilers: + mock_proc.poll.return_value = None # Simulate running process + monitor._spawned_profilers = mock_profilers + + # Try to spawn another profiler - should be rejected + stderr_capture = io.StringIO() + with patch("sys.stderr", stderr_capture): + monitor._spawn_profiler_for_child(99999) + + # Verify warning was printed + stderr_output = stderr_capture.getvalue() + self.assertIn( + "Max child profilers", + stderr_output, + f"Expected warning about max profilers, got: {stderr_output}", + ) + self.assertIn( + str(_MAX_CHILD_PROFILERS), + stderr_output, + f"Warning should mention limit ({_MAX_CHILD_PROFILERS}): {stderr_output}", + ) + + # Verify no new profiler was added + self.assertEqual( + len(monitor._spawned_profilers), + _MAX_CHILD_PROFILERS, + f"Should still have {_MAX_CHILD_PROFILERS} profilers, got " + f"{len(monitor._spawned_profilers)}", + ) + + +@requires_remote_subprocess_debugging() +class TestWaitForProfilers(unittest.TestCase): + """Tests for the wait_for_profilers method.""" + + def setUp(self): + reap_children() + + def tearDown(self): + reap_children() + + def test_wait_for_profilers_empty_list(self): + """Test that wait_for_profilers returns immediately with no profilers.""" + monitor = ChildProcessMonitor( + pid=os.getpid(), cli_args=[], output_pattern=None + ) + + # Should return immediately without printing anything + stderr_capture = io.StringIO() + with unittest.mock.patch("sys.stderr", stderr_capture): + start = time.time() + monitor.wait_for_profilers(timeout=10.0) + elapsed = time.time() - start + + # Should complete very quickly (less than 1 second) + self.assertLess( + elapsed, + 1.0, + f"wait_for_profilers with empty list took {elapsed:.2f}s, expected < 1s", + ) + # No "Waiting for..." message should be printed + self.assertNotIn( + "Waiting for", + stderr_capture.getvalue(), + "Should not print waiting message when no profilers", + ) + + def test_wait_for_profilers_with_completed_process(self): + """Test waiting for profilers that complete quickly.""" + monitor = ChildProcessMonitor( + pid=os.getpid(), cli_args=[], output_pattern=None + ) + + # Start a process that exits quickly + proc = subprocess.Popen( + [sys.executable, "-c", "pass"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + # Add to spawned profilers + monitor._spawned_profilers.append(proc) + + try: + stderr_capture = io.StringIO() + with unittest.mock.patch("sys.stderr", stderr_capture): + start = time.time() + monitor.wait_for_profilers(timeout=SHORT_TIMEOUT) + elapsed = time.time() - start + + # Should complete quickly since process exits fast + self.assertLess( + elapsed, + 5.0, + f"wait_for_profilers took {elapsed:.2f}s for quick process", + ) + # Should print waiting message + self.assertIn( + "Waiting for 1 child profiler", + stderr_capture.getvalue(), + "Should print waiting message", + ) + finally: + _cleanup_process(proc) + + def test_wait_for_profilers_timeout(self): + """Test that wait_for_profilers respects timeout.""" + monitor = ChildProcessMonitor( + pid=os.getpid(), cli_args=[], output_pattern=None + ) + + # Start a process that runs for a long time + proc = subprocess.Popen( + [sys.executable, "-c", "import time; time.sleep(60)"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + # Add to spawned profilers + monitor._spawned_profilers.append(proc) + + try: + stderr_capture = io.StringIO() + with unittest.mock.patch("sys.stderr", stderr_capture): + start = time.time() + # Use short timeout + monitor.wait_for_profilers(timeout=0.5) + elapsed = time.time() - start + + # Should timeout after approximately 0.5 seconds + self.assertGreater( + elapsed, + 0.4, + f"wait_for_profilers returned too quickly ({elapsed:.2f}s)", + ) + self.assertLess( + elapsed, + 2.0, + f"wait_for_profilers took too long ({elapsed:.2f}s), timeout not respected", + ) + finally: + _cleanup_process(proc) + + def test_wait_for_profilers_multiple(self): + """Test waiting for multiple profilers.""" + monitor = ChildProcessMonitor( + pid=os.getpid(), cli_args=[], output_pattern=None + ) + + # Start multiple processes + procs = [] + for _ in range(3): + proc = subprocess.Popen( + [sys.executable, "-c", "import time; time.sleep(0.1)"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + procs.append(proc) + monitor._spawned_profilers.append(proc) + + try: + stderr_capture = io.StringIO() + with unittest.mock.patch("sys.stderr", stderr_capture): + monitor.wait_for_profilers(timeout=SHORT_TIMEOUT) + + # Should report correct count + self.assertIn( + "Waiting for 3 child profiler", + stderr_capture.getvalue(), + "Should report correct profiler count", + ) + finally: + for proc in procs: + _cleanup_process(proc) + + +@requires_remote_subprocess_debugging() +class TestEndToEndChildrenCLI(unittest.TestCase): + """End-to-end tests for --subprocesses CLI flag.""" + + def setUp(self): + reap_children() + + def tearDown(self): + reap_children() + + def test_subprocesses_flag_spawns_child_and_creates_output(self): + """Test that --subprocesses flag works end-to-end with actual subprocesses.""" + # Create a temporary directory for output files + with tempfile.TemporaryDirectory() as tmpdir: + # Create a script that spawns a child Python process + parent_script = f""" +import subprocess +import sys +import time + +# Spawn a child that does some work +child = subprocess.Popen([ + sys.executable, '-c', + 'import time; [i**2 for i in range(1000)]; time.sleep(2)' +]) +# Do some work in parent +for i in range(1000): + _ = i ** 2 +time.sleep(2) +child.wait() +""" + script_file = os.path.join(tmpdir, "parent_script.py") + with open(script_file, "w") as f: + f.write(parent_script) + + output_file = os.path.join(tmpdir, "profile.pstats") + + # Run the profiler with --subprocesses flag + result = subprocess.run( + [ + sys.executable, + "-m", + "profiling.sampling", + "run", + "--subprocesses", + "-d", + "3", + "-r", + "100", + "-o", + output_file, + script_file, + ], + capture_output=True, + text=True, + timeout=SHORT_TIMEOUT, + ) + + # Check that parent output file was created + self.assertTrue( + os.path.exists(output_file), + f"Parent profile output not created. " + f"stdout: {result.stdout}, stderr: {result.stderr}", + ) + + # Check for child profiler output files (pattern: profile_{pid}.pstats) + output_files = os.listdir(tmpdir) + child_profiles = [ + f + for f in output_files + if f.startswith("profile_") and f.endswith(".pstats") + ] + + # Note: Child profiling is best-effort; the child may exit before + # profiler attaches, or the process may not be detected as Python. + # We just verify the mechanism doesn't crash. + if result.returncode != 0: + self.fail( + f"Profiler exited with code {result.returncode}. " + f"stdout: {result.stdout}, stderr: {result.stderr}" + ) + + def test_subprocesses_flag_with_flamegraph_output(self): + """Test --subprocesses with flamegraph output format.""" + with tempfile.TemporaryDirectory() as tmpdir: + # Simple parent that spawns a child + parent_script = f""" +import subprocess +import sys +import time +child = subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(1)']) +time.sleep(1) +child.wait() +""" + script_file = os.path.join(tmpdir, "parent.py") + with open(script_file, "w") as f: + f.write(parent_script) + + output_file = os.path.join(tmpdir, "flame.html") + + result = subprocess.run( + [ + sys.executable, + "-m", + "profiling.sampling", + "run", + "--subprocesses", + "-d", + "2", + "-r", + "100", + "--flamegraph", + "-o", + output_file, + script_file, + ], + capture_output=True, + text=True, + timeout=SHORT_TIMEOUT, + ) + + self.assertTrue( + os.path.exists(output_file), + f"Flamegraph output not created. stderr: {result.stderr}", + ) + + # Verify it's valid HTML + with open(output_file, "r") as f: + content = f.read() + self.assertIn( + " 'jsonl' so default filenames work.""" + self.assertEqual(FORMAT_EXTENSIONS["jsonl"], "jsonl") + self.assertEqual(_generate_output_filename("jsonl", 12345), "jsonl_12345.jsonl") + + def test_cli_jsonl_create_collector_propagates_mode(self): + """_create_collector('jsonl', ..., mode=X) lands X in the meta record.""" + collector = _create_collector( + "jsonl", + sample_interval_usec=1000, + skip_idle=False, + mode=PROFILING_MODE_CPU, + ) + self.assertIsInstance(collector, JsonlCollector) + + with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=False) as f: + jsonl_path = f.name + self.addCleanup(os.unlink, jsonl_path) + collector.export(jsonl_path) + with open(jsonl_path, "r", encoding="utf-8") as f: + records = [json.loads(line) for line in f] + meta = next(r for r in records if r["type"] == "meta") + self.assertEqual(meta["mode"], "cpu") + + def test_cli_binary_create_collector_propagates_mode(self): + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: + binary_path = f.name + self.addCleanup(os.unlink, binary_path) + collector = _create_collector( + "binary", + sample_interval_usec=1000, + skip_idle=True, + mode=PROFILING_MODE_CPU, + output_file=binary_path, + compression="none", + capture_config={"native": True}, + ) + collector.export(None) + + with BinaryReader(binary_path) as reader: + info = reader.get_info() + self.assertEqual(info["mode"], PROFILING_MODE_CPU) + self.assertTrue(info["capture_config"]["native"]) + + def test_cli_jsonl_rejects_opcodes_combination(self): + """--opcodes is incompatible with --jsonl per opcodes_compatible_formats.""" + test_args = [ + "profiling.sampling.cli", + "attach", + "12345", + "--jsonl", + "--opcodes", + ] + with ( + mock.patch("sys.argv", test_args), + mock.patch("sys.stderr", io.StringIO()) as mock_stderr, + mock.patch("profiling.sampling.cli.sample"), + self.assertRaises(SystemExit) as cm, + ): + main() + + self.assertEqual(cm.exception.code, 2) + self.assertIn("--opcodes", mock_stderr.getvalue()) diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py new file mode 100644 index 00000000000..533ca36376e --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -0,0 +1,3543 @@ +"""Tests for sampling profiler collector components.""" + +import json +import marshal +import opcode +import os +import tempfile +import unittest + +from test.support import is_emscripten + +try: + import _remote_debugging # noqa: F401 + from profiling.sampling import gecko_collector + from profiling.sampling.pstats_collector import PstatsCollector + from profiling.sampling.stack_collector import ( + CollapsedStackCollector, + FlamegraphCollector, + ) + from profiling.sampling.jsonl_collector import JsonlCollector + from profiling.sampling.gecko_collector import GeckoCollector + from profiling.sampling.heatmap_collector import _TemplateLoader + from profiling.sampling.collector import extract_lineno, normalize_location + from profiling.sampling.opcode_utils import get_opcode_info, format_opcode + from profiling.sampling.constants import ( + PROFILING_MODE_WALL, + PROFILING_MODE_CPU, + DEFAULT_LOCATION, + ) + from _remote_debugging import ( + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_ON_CPU, + THREAD_STATUS_GIL_REQUESTED, + THREAD_STATUS_MAIN_THREAD, + ) +except ImportError: + raise unittest.SkipTest( + "Test only runs when _remote_debugging is available" + ) + +from test.support import captured_stdout, captured_stderr + +from .mocks import ( + MockAwaitedInfo, + MockCoroInfo, + MockFrameInfo, + MockInterpreterInfo, + MockTaskInfo, + MockThreadInfo, + LocationInfo, + make_diff_collector_with_mock_baseline, +) +from .helpers import close_and_unlink, jsonl_tables + + +def resolve_name(node, strings): + """Resolve a flamegraph node's name from the string table.""" + idx = node.get("name", 0) + if isinstance(idx, int) and 0 <= idx < len(strings): + return strings[idx] + return str(idx) + + +def find_child_by_name(children, strings, substr): + """Find a child node whose resolved name contains substr.""" + for child in children: + if substr in resolve_name(child, strings): + return child + return None + + +def export_gecko_profile(testcase, collector): + gecko_out = tempfile.NamedTemporaryFile(suffix=".json", delete=False) + testcase.addCleanup(close_and_unlink, gecko_out) + # We cannot overwrite an open file on Windows. + gecko_out.close() + + with captured_stdout(), captured_stderr(): + collector.export(gecko_out.name) + + testcase.assertGreater(os.path.getsize(gecko_out.name), 0) + with open(gecko_out.name, encoding="utf-8") as file: + return json.load(file) + + +def assert_gecko_column_lengths(testcase, table, columns): + expected = table["length"] + for column in columns: + testcase.assertEqual( + len(table[column]), expected, + f"{column!r} has wrong length", + ) + + +def gecko_marker_names(profile, markers): + string_array = profile["shared"]["stringArray"] + return [string_array[idx] for idx in markers["name"]] + + +def gecko_opcode_marker_data(profile): + markers = profile["threads"][0]["markers"] + return [ + data for data in markers["data"] + if data.get("type") == "Opcode" + ] + + +class TestSampleProfilerComponents(unittest.TestCase): + """Unit tests for individual profiler components.""" + + def test_mock_frame_info_with_empty_and_unicode_values(self): + """Test MockFrameInfo handles empty strings, unicode characters, and very long names correctly.""" + # Test with empty strings + frame = MockFrameInfo("", 0, "") + self.assertEqual(frame.filename, "") + self.assertEqual(frame.location.lineno, 0) + self.assertEqual(frame.funcname, "") + + # Test with unicode characters + frame = MockFrameInfo("文件.py", 42, "函数名") + self.assertEqual(frame.filename, "文件.py") + self.assertEqual(frame.funcname, "函数名") + + # Test with very long names + long_filename = "x" * 1000 + ".py" + long_funcname = "func_" + "x" * 1000 + frame = MockFrameInfo(long_filename, 999999, long_funcname) + self.assertEqual(frame.filename, long_filename) + self.assertEqual(frame.location.lineno, 999999) + self.assertEqual(frame.funcname, long_funcname) + + def test_heatmap_navigation_restarts_line_highlight(self): + """Test heatmap navigation can replay target line highlights.""" + loader = _TemplateLoader() + + self.assertIn(".code-line:target", loader.file_css) + self.assertIn("function restartLineHighlight(target)", loader.file_js) + self.assertIn("target.style.animation = 'none'", loader.file_js) + self.assertIn("void target.offsetWidth", loader.file_js) + self.assertIn("url.href === window.location.href", loader.file_js) + self.assertIn("navigateToLine(JSON.parse(navData).link)", loader.file_js) + self.assertIn("navigateToLine(linkData.link)", loader.file_js) + + def test_pstats_collector_with_extreme_intervals_and_empty_data(self): + """Test PstatsCollector handles zero/large intervals, empty frames, None thread IDs, and duplicate frames.""" + # Test with zero interval + collector = PstatsCollector(sample_interval_usec=0) + self.assertEqual(collector.sample_interval_usec, 0) + + # Test with very large interval + collector = PstatsCollector(sample_interval_usec=1000000000) + self.assertEqual(collector.sample_interval_usec, 1000000000) + + # Test collecting empty frames list + collector = PstatsCollector(sample_interval_usec=1000) + collector.collect([]) + self.assertEqual(len(collector.result), 0) + + # Test collecting frames with None thread id + test_frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(None, [MockFrameInfo("file.py", 10, "func", None)])], + ) + ] + collector.collect(test_frames) + # Should still process the frames + self.assertEqual(len(collector.result), 1) + + # Test collecting duplicate frames in same sample (recursive function) + test_frames = [ + MockInterpreterInfo( + 0, # interpreter_id + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("file.py", 10, "func1"), + MockFrameInfo("file.py", 10, "func1"), # Duplicate (recursion) + ], + ) + ], + ) + ] + collector = PstatsCollector(sample_interval_usec=1000) + collector.collect(test_frames) + # Should count only once per sample to avoid over-counting recursive functions + self.assertEqual( + collector.result[("file.py", 10, "func1")]["cumulative_calls"], 1 + ) + + def test_pstats_collector_single_frame_stacks(self): + """Test PstatsCollector with single-frame call stacks to trigger len(frames) <= 1 branch.""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Test with exactly one frame (should trigger the <= 1 condition) + single_frame = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, [MockFrameInfo("single.py", 10, "single_func")] + ) + ], + ) + ] + collector.collect(single_frame) + + # Should record the single frame with inline call + self.assertEqual(len(collector.result), 1) + single_key = ("single.py", 10, "single_func") + self.assertIn(single_key, collector.result) + self.assertEqual(collector.result[single_key]["direct_calls"], 1) + self.assertEqual(collector.result[single_key]["cumulative_calls"], 1) + + # Test with empty frames (should also trigger <= 1 condition) + empty_frames = [MockInterpreterInfo(0, [MockThreadInfo(1, [])])] + collector.collect(empty_frames) + + # Should not add any new entries + self.assertEqual( + len(collector.result), 1 + ) # Still just the single frame + + # Test mixed single and multi-frame stacks + mixed_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [MockFrameInfo("single2.py", 20, "single_func2")], + ), # Single frame + MockThreadInfo( + 2, + [ # Multi-frame stack + MockFrameInfo("multi.py", 30, "multi_func1"), + MockFrameInfo("multi.py", 40, "multi_func2"), + ], + ), + ], + ), + ] + collector.collect(mixed_frames) + + # Should have recorded all functions + self.assertEqual( + len(collector.result), 4 + ) # single + single2 + multi1 + multi2 + + # Verify single frame handling + single2_key = ("single2.py", 20, "single_func2") + self.assertIn(single2_key, collector.result) + self.assertEqual(collector.result[single2_key]["direct_calls"], 1) + self.assertEqual(collector.result[single2_key]["cumulative_calls"], 1) + + # Verify multi-frame handling still works + multi1_key = ("multi.py", 30, "multi_func1") + multi2_key = ("multi.py", 40, "multi_func2") + self.assertIn(multi1_key, collector.result) + self.assertIn(multi2_key, collector.result) + self.assertEqual(collector.result[multi1_key]["direct_calls"], 1) + self.assertEqual( + collector.result[multi2_key]["cumulative_calls"], 1 + ) # Called from multi1 + + def test_collapsed_stack_collector_with_empty_and_deep_stacks(self): + """Test CollapsedStackCollector handles empty frames, single-frame stacks, and very deep call stacks.""" + collector = CollapsedStackCollector(1000) + + # Test with empty frames + collector.collect([]) + self.assertEqual(len(collector.stack_counter), 0) + + # Test with single frame stack + test_frames = [ + MockInterpreterInfo( + 0, [MockThreadInfo(1, [MockFrameInfo("file.py", 10, "func")])] + ) + ] + collector.collect(test_frames) + self.assertEqual(len(collector.stack_counter), 1) + (((path, thread_id), count),) = collector.stack_counter.items() + self.assertEqual(path, (("file.py", 10, "func"),)) + self.assertEqual(thread_id, 1) + self.assertEqual(count, 1) + + # Test with very deep stack + deep_stack = [MockFrameInfo(f"file{i}.py", i, f"func{i}") for i in range(100)] + test_frames = [MockInterpreterInfo(0, [MockThreadInfo(1, deep_stack)])] + collector = CollapsedStackCollector(1000) + collector.collect(test_frames) + # One aggregated path with 100 frames (reversed) + (((path_tuple, thread_id),),) = (collector.stack_counter.keys(),) + self.assertEqual(len(path_tuple), 100) + self.assertEqual(path_tuple[0], ("file99.py", 99, "func99")) + self.assertEqual(path_tuple[-1], ("file0.py", 0, "func0")) + self.assertEqual(thread_id, 1) + + def test_pstats_collector_basic(self): + """Test basic PstatsCollector functionality.""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Test empty state + self.assertEqual(len(collector.result), 0) + self.assertEqual(len(collector.stats), 0) + + # Test collecting sample data + test_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("file.py", 10, "func1"), + MockFrameInfo("file.py", 20, "func2"), + ], + ) + ], + ) + ] + collector.collect(test_frames) + + # Should have recorded calls for both functions + self.assertEqual(len(collector.result), 2) + self.assertIn(("file.py", 10, "func1"), collector.result) + self.assertIn(("file.py", 20, "func2"), collector.result) + + # Top-level function should have direct call + self.assertEqual( + collector.result[("file.py", 10, "func1")]["direct_calls"], 1 + ) + self.assertEqual( + collector.result[("file.py", 10, "func1")]["cumulative_calls"], 1 + ) + + # Calling function should have cumulative call but no direct calls + self.assertEqual( + collector.result[("file.py", 20, "func2")]["cumulative_calls"], 1 + ) + self.assertEqual( + collector.result[("file.py", 20, "func2")]["direct_calls"], 0 + ) + + def test_pstats_collector_create_stats(self): + """Test PstatsCollector stats creation.""" + collector = PstatsCollector( + sample_interval_usec=1000000 + ) # 1 second intervals + + test_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("file.py", 10, "func1"), + MockFrameInfo("file.py", 20, "func2"), + ], + ) + ], + ) + ] + collector.collect(test_frames) + collector.collect(test_frames) # Collect twice + + collector.create_stats() + + # Check stats format: (direct_calls, cumulative_calls, tt, ct, callers) + func1_stats = collector.stats[("file.py", 10, "func1")] + self.assertEqual(func1_stats[0], 2) # direct_calls (top of stack) + self.assertEqual(func1_stats[1], 2) # cumulative_calls + self.assertEqual( + func1_stats[2], 2.0 + ) # tt (total time - 2 samples * 1 sec) + self.assertEqual(func1_stats[3], 2.0) # ct (cumulative time) + + func2_stats = collector.stats[("file.py", 20, "func2")] + self.assertEqual( + func2_stats[0], 0 + ) # direct_calls (never top of stack) + self.assertEqual( + func2_stats[1], 2 + ) # cumulative_calls (appears in stack) + self.assertEqual(func2_stats[2], 0.0) # tt (no direct calls) + self.assertEqual(func2_stats[3], 2.0) # ct (cumulative time) + + def test_collapsed_stack_collector_basic(self): + collector = CollapsedStackCollector(1000) + + # Test empty state + self.assertEqual(len(collector.stack_counter), 0) + + # Test collecting sample data + test_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")] + ) + ], + ) + ] + collector.collect(test_frames) + + # Should store one reversed path + self.assertEqual(len(collector.stack_counter), 1) + (((path, thread_id), count),) = collector.stack_counter.items() + expected_tree = (("file.py", 20, "func2"), ("file.py", 10, "func1")) + self.assertEqual(path, expected_tree) + self.assertEqual(thread_id, 1) + self.assertEqual(count, 1) + + def test_collapsed_stack_collector_export(self): + collapsed_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, collapsed_out) + + collector = CollapsedStackCollector(1000) + + test_frames1 = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")] + ) + ], + ) + ] + test_frames2 = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")] + ) + ], + ) + ] # Same stack + test_frames3 = [ + MockInterpreterInfo( + 0, [MockThreadInfo(1, [MockFrameInfo("other.py", 5, "other_func")])] + ) + ] + + collector.collect(test_frames1) + collector.collect(test_frames2) + collector.collect(test_frames3) + + with captured_stdout(), captured_stderr(): + collector.export(collapsed_out.name) + # Check file contents + with open(collapsed_out.name, "r") as f: + content = f.read() + + lines = content.strip().split("\n") + self.assertEqual(len(lines), 2) # Two unique stacks + + # Check collapsed format: tid:X;file:func:line;file:func:line count + stack1_expected = "tid:1;file.py:func2:20;file.py:func1:10 2" + stack2_expected = "tid:1;other.py:other_func:5 1" + + self.assertIn(stack1_expected, lines) + self.assertIn(stack2_expected, lines) + + def test_flamegraph_collector_basic(self): + """Test basic FlamegraphCollector functionality.""" + collector = FlamegraphCollector(1000) + + # Empty collector should produce 'No Data' + data = collector._convert_to_flamegraph_format() + # With string table, name is now an index - resolve it using the strings array + strings = data.get("strings", []) + self.assertIn(resolve_name(data, strings), ("No Data", "No significant data")) + + # Test collecting sample data + test_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")] + ) + ], + ) + ] + collector.collect(test_frames) + + # Convert and verify structure: func2 -> func1 with counts = 1 + data = collector._convert_to_flamegraph_format() + # Expect promotion: root is the single child (func2), with func1 as its only child + strings = data.get("strings", []) + name = resolve_name(data, strings) + self.assertTrue(name.startswith("Program Root: ")) + self.assertIn("func2 (file.py:20)", name) + label = strings[data["label"]] + self.assertTrue(label.startswith("Program Root: ")) + self.assertEqual(data["self"], 0) # non-leaf: no self time + children = data.get("children", []) + self.assertEqual(len(children), 1) + child = children[0] + self.assertIn("func1 (file.py:10)", resolve_name(child, strings)) + self.assertEqual(child["value"], 1) + self.assertEqual(child["self"], 1) # leaf: all time is self + self.assertEqual(data["stats"]["sample_interval_usec"], 1000) + + def test_flamegraph_collector_export(self): + """Test flamegraph HTML export functionality.""" + flamegraph_out = tempfile.NamedTemporaryFile( + suffix=".html", delete=False + ) + self.addCleanup(close_and_unlink, flamegraph_out) + + collector = FlamegraphCollector(10000) + + # Create some test data (use Interpreter/Thread objects like runtime) + test_frames1 = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")] + ) + ], + ) + ] + test_frames2 = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")] + ) + ], + ) + ] # Same stack + test_frames3 = [ + MockInterpreterInfo( + 0, [MockThreadInfo(1, [MockFrameInfo("other.py", 5, "other_func")])] + ) + ] + + collector.collect(test_frames1) + collector.collect(test_frames2) + collector.collect(test_frames3) + + # Export flamegraph + with captured_stdout(), captured_stderr(): + export_ok = collector.export(flamegraph_out.name) + + # Verify file was created and contains valid data + self.assertTrue(export_ok) + self.assertTrue(os.path.exists(flamegraph_out.name)) + self.assertGreater(os.path.getsize(flamegraph_out.name), 0) + + # Check file contains HTML content + with open(flamegraph_out.name, "r", encoding="utf-8") as f: + content = f.read() + + # Should be valid HTML + self.assertIn("", content.lower()) + self.assertIn("")], status=THREAD_STATUS_HAS_GIL), + ], + ) + ] + collector.collect(stack_frames_gc) + self.assertEqual(collector.samples_with_gc_frames, 1) + + # Another sample without GC + collector.collect(stack_frames_1) + self.assertEqual(collector.samples_with_gc_frames, 1) # Still 1 + + # Another GC sample + collector.collect(stack_frames_gc) + self.assertEqual(collector.samples_with_gc_frames, 2) + + def test_flamegraph_collector_restores_replay_stats(self): + """Replay restores measured values from binary metadata.""" + collector = FlamegraphCollector(1000) + collector.set_replay_stats({ + "duration_sec": 1.25, + "sample_rate": 4.0, + "error_rate": 2.5, + "missed_samples": 1.5, + }) + + self.assertEqual(collector.stats["duration_sec"], 1.25) + self.assertEqual(collector.stats["sample_rate"], 4.0) + self.assertEqual(collector.stats["error_rate"], 2.5) + self.assertEqual(collector.stats["missed_samples"], 1.5) + + def test_flamegraph_collector_leaves_legacy_replay_stats_unavailable(self): + collector = FlamegraphCollector(1000) + original_stats = collector.stats.copy() + collector.set_replay_stats({ + "duration_sec": None, + "sample_rate": None, + }) + self.assertEqual(collector.stats, original_stats) + + def test_flamegraph_collector_per_thread_stats(self): + """Test per-thread statistics tracking in FlamegraphCollector.""" + collector = FlamegraphCollector(sample_interval_usec=1000) + + # Multiple threads with different states + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL), + MockThreadInfo(2, [MockFrameInfo("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU), + MockThreadInfo(3, [MockFrameInfo("c.py", 3, "func_c")], status=THREAD_STATUS_GIL_REQUESTED), + ], + ) + ] + collector.collect(stack_frames) + + # Check per-thread stats + self.assertIn(1, collector.per_thread_stats) + self.assertIn(2, collector.per_thread_stats) + self.assertIn(3, collector.per_thread_stats) + + # Thread 1: has GIL + self.assertEqual(collector.per_thread_stats[1]["has_gil"], 1) + self.assertEqual(collector.per_thread_stats[1]["on_cpu"], 0) + self.assertEqual(collector.per_thread_stats[1]["total"], 1) + + # Thread 2: on CPU + self.assertEqual(collector.per_thread_stats[2]["has_gil"], 0) + self.assertEqual(collector.per_thread_stats[2]["on_cpu"], 1) + self.assertEqual(collector.per_thread_stats[2]["total"], 1) + + # Thread 3: waiting + self.assertEqual(collector.per_thread_stats[3]["gil_requested"], 1) + self.assertEqual(collector.per_thread_stats[3]["total"], 1) + + # Test accumulation across samples + stack_frames_2 = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo(1, [MockFrameInfo("a.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU), + ], + ) + ] + collector.collect(stack_frames_2) + + self.assertEqual(collector.per_thread_stats[1]["has_gil"], 1) + self.assertEqual(collector.per_thread_stats[1]["on_cpu"], 1) + self.assertEqual(collector.per_thread_stats[1]["total"], 2) + + def test_flamegraph_collector_percentage_calculations(self): + """Test that percentage calculations are correct in exported data.""" + collector = FlamegraphCollector(sample_interval_usec=1000) + + # Create scenario: 60% GIL held, 40% not held + for i in range(6): + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func")], status=THREAD_STATUS_HAS_GIL), + ], + ) + ] + collector.collect(stack_frames) + + for i in range(4): + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func")], status=THREAD_STATUS_ON_CPU), + ], + ) + ] + collector.collect(stack_frames) + + # Export to get calculated percentages + data = collector._convert_to_flamegraph_format() + thread_stats = data["stats"]["thread_stats"] + + self.assertAlmostEqual(thread_stats["has_gil_pct"], 60.0, places=1) + self.assertAlmostEqual(thread_stats["on_cpu_pct"], 40.0, places=1) + self.assertEqual(thread_stats["total"], 10) + + def test_flamegraph_collector_mode_handling(self): + """Test that profiling mode is correctly passed through to exported data.""" + collector = FlamegraphCollector(sample_interval_usec=1000) + + # Collect some data + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func")], status=THREAD_STATUS_HAS_GIL), + ], + ) + ] + collector.collect(stack_frames) + + # Set stats with mode + collector.set_stats( + sample_interval_usec=1000, + duration_sec=1.0, + sample_rate=1000.0, + mode=PROFILING_MODE_CPU + ) + + data = collector._convert_to_flamegraph_format() + self.assertEqual(data["stats"]["mode"], PROFILING_MODE_CPU) + + def test_flamegraph_collector_zero_samples_edge_case(self): + """Test that collector handles zero samples gracefully.""" + collector = FlamegraphCollector(sample_interval_usec=1000) + + # Export without collecting any samples + data = collector._convert_to_flamegraph_format() + + # Should return a valid structure with no data + self.assertIn("name", data) + self.assertEqual(data["value"], 0) + self.assertIn("children", data) + self.assertEqual(len(data["children"]), 0) + + def test_flamegraph_collector_json_structure_includes_stats(self): + """Test that exported JSON includes thread_stats and per_thread_stats.""" + collector = FlamegraphCollector(sample_interval_usec=1000) + + # Collect some data with multiple threads + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL), + MockThreadInfo(2, [MockFrameInfo("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU), + ], + ) + ] + collector.collect(stack_frames) + + # Set stats + collector.set_stats( + sample_interval_usec=1000, + duration_sec=1.0, + sample_rate=1000.0, + mode=PROFILING_MODE_WALL + ) + + # Export and verify structure + data = collector._convert_to_flamegraph_format() + + # Check that stats object exists and contains expected fields + self.assertIn("stats", data) + stats = data["stats"] + + # Verify thread_stats exists and has expected structure + self.assertIn("thread_stats", stats) + thread_stats = stats["thread_stats"] + self.assertIn("has_gil_pct", thread_stats) + self.assertIn("on_cpu_pct", thread_stats) + self.assertIn("gil_requested_pct", thread_stats) + self.assertIn("gc_pct", thread_stats) + self.assertIn("total", thread_stats) + + # Verify per_thread_stats exists and has data for both threads + self.assertIn("per_thread_stats", stats) + per_thread_stats = stats["per_thread_stats"] + self.assertIn(1, per_thread_stats) + self.assertIn(2, per_thread_stats) + + # Check per-thread structure + for thread_id in [1, 2]: + thread_data = per_thread_stats[thread_id] + self.assertIn("has_gil_pct", thread_data) + self.assertIn("on_cpu_pct", thread_data) + self.assertIn("gil_requested_pct", thread_data) + self.assertIn("gc_pct", thread_data) + self.assertIn("total", thread_data) + + def test_flamegraph_nodes_include_per_thread_values(self): + collector = FlamegraphCollector(sample_interval_usec=1000) + root = MockFrameInfo("app.py", 1, "main") + collector.process_frames( + [MockFrameInfo("app.py", 10, "worker_a"), root], + thread_id=1, + weight=2, + ) + collector.process_frames( + [MockFrameInfo("app.py", 20, "worker_b"), root], + thread_id=2, + weight=3, + ) + + data = collector._convert_to_flamegraph_format() + + self.assertEqual(data["thread_values"], {1: [2, 0], 2: [3, 0]}) + children_by_line = {child["lineno"]: child for child in data["children"]} + self.assertEqual(children_by_line[10]["thread_values"], {1: [2, 2]}) + self.assertEqual(children_by_line[20]["thread_values"], {2: [3, 3]}) + + def test_flamegraph_pruning_preserves_low_volume_thread(self): + collector = FlamegraphCollector(sample_interval_usec=1000) + collector.process_frames( + [MockFrameInfo("app.py", 10, "busy")], + thread_id=1, + weight=1999, + ) + collector.process_frames( + [MockFrameInfo("app.py", 20, "rare")], + thread_id=2, + ) + + data = collector._convert_to_flamegraph_format() + + self.assertEqual(data["thread_values"], {1: [1999, 0], 2: [1, 0]}) + children_by_line = {child["lineno"]: child for child in data["children"]} + self.assertEqual(children_by_line[10]["thread_values"], {1: [1999, 1999]}) + self.assertEqual(children_by_line[20]["thread_values"], {2: [1, 1]}) + + def test_flamegraph_does_not_promote_incomplete_root(self): + collector = FlamegraphCollector(sample_interval_usec=1000) + collector.process_frames( + [MockFrameInfo("app.py", 1, "busy")], + thread_id=1, + weight=2000, + ) + for line in range(2, 2002): + collector.process_frames( + [MockFrameInfo("app.py", line, f"fragment_{line}")], + thread_id=2, + ) + + data = collector._convert_to_flamegraph_format() + + self.assertNotIn("filename", data) + self.assertEqual(data["thread_values"], {1: [2000, 0], 2: [2000, 0]}) + self.assertEqual(len(data["children"]), 1) + self.assertEqual(data["children"][0]["thread_values"], {1: [2000, 2000]}) + + def test_flamegraph_nodes_include_per_thread_opcodes(self): + collector = FlamegraphCollector(sample_interval_usec=1000) + collector.process_frames( + [MockFrameInfo("app.py", 10, "worker", opcode=100)], + thread_id=1, + weight=2, + ) + collector.process_frames( + [MockFrameInfo("app.py", 10, "worker", opcode=101)], + thread_id=2, + weight=3, + ) + + data = collector._convert_to_flamegraph_format() + + self.assertEqual(data["opcodes"], {100: 2, 101: 3}) + self.assertEqual( + data["thread_opcodes"], + {1: {100: 2}, 2: {101: 3}}, + ) + + def test_flamegraph_collector_per_thread_gc_percentage(self): + """Test that per-thread GC percentage uses total samples as denominator.""" + collector = FlamegraphCollector(sample_interval_usec=1000) + + # Create 10 samples total: + # - Thread 1 appears in all 10 samples, has GC in 2 of them + # - Thread 2 appears in only 5 samples, has GC in 1 of them + + # First 5 samples: both threads, thread 1 has GC in 2 + for i in range(5): + has_gc = i < 2 # First 2 samples have GC for thread 1 + frames_1 = [MockFrameInfo("~", 0, "")] if has_gc else [MockFrameInfo("a.py", 1, "func_a")] + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo(1, frames_1, status=THREAD_STATUS_HAS_GIL), + MockThreadInfo(2, [MockFrameInfo("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU), + ], + ) + ] + collector.collect(stack_frames) + + # Next 5 samples: only thread 1, thread 2 appears in first of these with GC + for i in range(5): + if i == 0: + # Thread 2 appears in this sample with GC + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL), + MockThreadInfo(2, [MockFrameInfo("~", 0, "")], status=THREAD_STATUS_ON_CPU), + ], + ) + ] + else: + # Only thread 1 + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL), + ], + ) + ] + collector.collect(stack_frames) + + # Set stats and export + collector.set_stats( + sample_interval_usec=1000, + duration_sec=1.0, + sample_rate=1000.0, + mode=PROFILING_MODE_WALL + ) + + data = collector._convert_to_flamegraph_format() + per_thread_stats = data["stats"]["per_thread_stats"] + + # Thread 1: appeared in 10 samples, had GC in 2 + # GC percentage should be 2/10 = 20% (using total samples, not thread appearances) + self.assertEqual(collector.per_thread_stats[1]["gc_samples"], 2) + self.assertEqual(collector.per_thread_stats[1]["total"], 10) + self.assertAlmostEqual(per_thread_stats[1]["gc_pct"], 20.0, places=1) + + # Thread 2: appeared in 6 samples, had GC in 1 + # GC percentage should be 1/10 = 10% (using total samples, not thread appearances) + self.assertEqual(collector.per_thread_stats[2]["gc_samples"], 1) + self.assertEqual(collector.per_thread_stats[2]["total"], 6) + self.assertAlmostEqual(per_thread_stats[2]["gc_pct"], 10.0, places=1) + + def test_diff_flamegraph_identical_profiles(self): + """When baseline and current are identical, diff should be ~0.""" + test_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 10, "func1"), + MockFrameInfo("file.py", 20, "func2"), + ]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline([test_frames] * 3) + for _ in range(3): + diff.collect(test_frames) + + data = diff._convert_to_flamegraph_format() + strings = data.get("strings", []) + + self.assertTrue(data["stats"]["is_differential"]) + self.assertEqual(data["stats"]["baseline_samples"], 3) + self.assertEqual(data["stats"]["current_samples"], 3) + self.assertAlmostEqual(data["stats"]["baseline_scale"], 1.0) + + children = data.get("children", []) + self.assertEqual(len(children), 1) + child = children[0] + self.assertIn("func1", resolve_name(child, strings)) + self.assertEqual(child["self_time"], 3) + self.assertAlmostEqual(child["baseline"], 3.0) + self.assertAlmostEqual(child["diff"], 0.0, places=1) + self.assertAlmostEqual(child["diff_pct"], 0.0, places=1) + + self.assertEqual(data["stats"]["elided_count"], 0) + self.assertNotIn("elided_flamegraph", data["stats"]) + + def test_diff_flamegraph_new_function(self): + """A function only in current should have diff_pct=100 and baseline=0.""" + baseline_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 10, "func1"), + MockFrameInfo("file.py", 20, "func2"), + ]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline([baseline_frames]) + diff.collect([ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 30, "new_func"), + MockFrameInfo("file.py", 10, "func1"), + MockFrameInfo("file.py", 20, "func2"), + ]) + ]) + ]) + + data = diff._convert_to_flamegraph_format() + strings = data.get("strings", []) + + children = data.get("children", []) + self.assertEqual(len(children), 1) + func1_node = children[0] + self.assertIn("func1", resolve_name(func1_node, strings)) + + func1_children = func1_node.get("children", []) + self.assertEqual(len(func1_children), 1) + new_func_node = func1_children[0] + self.assertIn("new_func", resolve_name(new_func_node, strings)) + self.assertEqual(new_func_node["baseline"], 0) + self.assertGreater(new_func_node["self_time"], 0) + self.assertEqual(new_func_node["diff"], new_func_node["self_time"]) + self.assertAlmostEqual(new_func_node["diff_pct"], 100.0) + + def test_diff_flamegraph_changed_functions(self): + """Functions with different sample counts should have correct diff and diff_pct.""" + hot_leaf_sample = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 10, "hot_leaf"), + MockFrameInfo("file.py", 20, "caller"), + ]) + ]) + ] + cold_leaf_sample = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 30, "cold_leaf"), + MockFrameInfo("file.py", 20, "caller"), + ]) + ]) + ] + + # Baseline: 2 samples, current: 4, scale = 2.0 + diff = make_diff_collector_with_mock_baseline( + [hot_leaf_sample, cold_leaf_sample] + ) + for _ in range(3): + diff.collect(hot_leaf_sample) + diff.collect(cold_leaf_sample) + + data = diff._convert_to_flamegraph_format() + strings = data.get("strings", []) + self.assertAlmostEqual(data["stats"]["baseline_scale"], 2.0) + + children = data.get("children", []) + hot_node = find_child_by_name(children, strings, "hot_leaf") + cold_node = find_child_by_name(children, strings, "cold_leaf") + self.assertIsNotNone(hot_node) + self.assertIsNotNone(cold_node) + + # hot_leaf regressed (+50%) + self.assertAlmostEqual(hot_node["baseline"], 2.0) + self.assertEqual(hot_node["self_time"], 3) + self.assertAlmostEqual(hot_node["diff"], 1.0) + self.assertAlmostEqual(hot_node["diff_pct"], 50.0) + + # cold_leaf improved (-50%) + self.assertAlmostEqual(cold_node["baseline"], 2.0) + self.assertEqual(cold_node["self_time"], 1) + self.assertAlmostEqual(cold_node["diff"], -1.0) + self.assertAlmostEqual(cold_node["diff_pct"], -50.0) + + def test_diff_flamegraph_rejects_mismatched_profiling_modes(self): + from profiling.sampling.binary_collector import BinaryCollector + from profiling.sampling.stack_collector import DiffFlamegraphCollector + + bin_file = tempfile.NamedTemporaryFile(suffix=".bin", delete=False) + self.addCleanup(close_and_unlink, bin_file) + writer = BinaryCollector( + bin_file.name, + sample_interval_usec=1000, + compression="none", + mode=PROFILING_MODE_CPU, + ) + writer.export(None) + + diff = DiffFlamegraphCollector( + 1000, + baseline_binary_path=bin_file.name, + mode=PROFILING_MODE_WALL, + ) + with self.assertRaisesRegex(ValueError, "profiling mode"): + diff._convert_to_flamegraph_format() + + def test_diff_flamegraph_rejects_mismatched_capture_config(self): + from profiling.sampling.binary_collector import BinaryCollector + from profiling.sampling.stack_collector import DiffFlamegraphCollector + + bin_file = tempfile.NamedTemporaryFile(suffix=".bin", delete=False) + self.addCleanup(close_and_unlink, bin_file) + writer = BinaryCollector( + bin_file.name, + sample_interval_usec=1000, + compression="none", + capture_config={"all_threads": True}, + ) + writer.export(None) + + diff = DiffFlamegraphCollector( + 1000, + baseline_binary_path=bin_file.name, + capture_config={"all_threads": False}, + ) + with self.assertRaisesRegex(ValueError, "all_threads"): + diff._convert_to_flamegraph_format() + + def test_diff_flamegraph_scale_factor(self): + """Scale factor adjusts when sample counts differ.""" + baseline_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 10, "func1"), + MockFrameInfo("file.py", 20, "func2"), + ]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline([baseline_frames]) + for _ in range(4): + diff.collect(baseline_frames) + + data = diff._convert_to_flamegraph_format() + self.assertAlmostEqual(data["stats"]["baseline_scale"], 4.0) + + children = data.get("children", []) + self.assertEqual(len(children), 1) + func1_node = children[0] + self.assertEqual(func1_node["self_time"], 4) + self.assertAlmostEqual(func1_node["baseline"], 4.0) + self.assertAlmostEqual(func1_node["diff"], 0.0) + self.assertAlmostEqual(func1_node["diff_pct"], 0.0) + + def test_diff_flamegraph_elided_stacks(self): + """Paths in baseline but not current produce elided stacks.""" + baseline_frames_1 = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 10, "func1"), + MockFrameInfo("file.py", 20, "func2"), + ]) + ]) + ] + baseline_frames_2 = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 30, "old_func"), + MockFrameInfo("file.py", 20, "func2"), + ]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline([baseline_frames_1, baseline_frames_2]) + for _ in range(2): + diff.collect(baseline_frames_1) + + data = diff._convert_to_flamegraph_format() + + self.assertEqual(data["stats"]["elided_count"], 1) + self.assertIn("elided_flamegraph", data["stats"]) + elided = data["stats"]["elided_flamegraph"] + self.assertTrue(elided["stats"]["is_differential"]) + self.assertIn("strings", elided) + + elided_strings = elided.get("strings", []) + children = elided.get("children", []) + self.assertEqual(len(children), 1) + child = children[0] + self.assertIn("old_func", resolve_name(child, elided_strings)) + self.assertEqual(child["self_time"], 0) + self.assertAlmostEqual(child["diff_pct"], -100.0) + self.assertGreater(child["baseline"], 0) + self.assertAlmostEqual(child["diff"], -child["baseline"]) + + def test_diff_flamegraph_counts_elided_stacks_not_paths(self): + """Internal and leaf stack endings are counted separately.""" + internal_stack = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 20, "old_mid"), + MockFrameInfo("file.py", 10, "root"), + ]) + ]) + ] + leaf_stack = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 30, "old_leaf"), + MockFrameInfo("file.py", 20, "old_mid"), + MockFrameInfo("file.py", 10, "root"), + ]) + ]) + ] + current_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [MockFrameInfo("file.py", 10, "root")]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline( + [internal_stack, leaf_stack] + ) + diff.collect(current_frames) + + data = diff._convert_to_flamegraph_format() + self.assertEqual(data["stats"]["elided_count"], 2) + + def test_diff_flamegraph_renders_small_elided_stack(self): + """Elided stacks are not removed by the significance filter.""" + common_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 20, "common"), + MockFrameInfo("file.py", 10, "root"), + ]) + ]) + ] + old_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 30, "old_tiny"), + MockFrameInfo("file.py", 10, "root"), + ]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline( + [common_frames] * 1999 + [old_frames] + ) + for _ in range(1999): + diff.collect(common_frames) + + data = diff._convert_to_flamegraph_format() + self.assertEqual(data["stats"]["elided_count"], 1) + self.assertIn("elided_flamegraph", data["stats"]) + + elided = data["stats"]["elided_flamegraph"] + strings = elided["strings"] + self.assertIsNotNone( + find_child_by_name(elided.get("children", []), strings, "old_tiny") + ) + + def test_diff_flamegraph_elided_top_level_root(self): + """Elided top-level roots do not crash metadata generation.""" + baseline_frames_1 = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 10, "kept_leaf"), + MockFrameInfo("file.py", 20, "kept_root"), + ]) + ]) + ] + baseline_frames_2 = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 30, "old_leaf"), + MockFrameInfo("file.py", 40, "old_root"), + ]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline([ + baseline_frames_1, + baseline_frames_2, + ]) + diff.collect(baseline_frames_1) + + data = diff._convert_to_flamegraph_format() + elided = data["stats"]["elided_flamegraph"] + elided_strings = elided.get("strings", []) + children = elided.get("children", []) + + self.assertEqual(len(children), 1) + self.assertIn("old_root", resolve_name(children[0], elided_strings)) + + def test_diff_flamegraph_function_matched_despite_line_change(self): + """Functions match by (filename, funcname), ignoring lineno.""" + baseline_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 10, "func1"), + MockFrameInfo("file.py", 20, "func2"), + ]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline([baseline_frames]) + # Same functions but different line numbers + diff.collect([ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 99, "func1"), + MockFrameInfo("file.py", 55, "func2"), + ]) + ]) + ]) + + data = diff._convert_to_flamegraph_format() + strings = data.get("strings", []) + + children = data.get("children", []) + self.assertEqual(len(children), 1) + child = children[0] + self.assertIn("func1", resolve_name(child, strings)) + self.assertGreater(child["baseline"], 0) + self.assertGreater(child["self_time"], 0) + self.assertAlmostEqual(child["diff"], 0.0, places=1) + self.assertAlmostEqual(child["diff_pct"], 0.0, places=1) + + def test_diff_flamegraph_does_not_duplicate_line_values(self): + """Function aggregates are apportioned across line nodes.""" + def sample(line): + return [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", line, "func"), + MockFrameInfo("file.py", 1, "caller"), + ]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline( + [sample(10), sample(20)] + ) + diff.collect(sample(10)) + diff.collect(sample(20)) + + data = diff._convert_to_flamegraph_format() + children = data["children"] + self.assertEqual(sum(node["self"] for node in children), 2) + self.assertEqual(sum(node["self_time"] for node in children), 2) + self.assertEqual(sum(node["baseline"] for node in children), 2) + for node in children: + self.assertEqual(node["self"], 1) + self.assertEqual(node["self_time"], 1) + self.assertAlmostEqual(node["baseline"], 1.0) + self.assertAlmostEqual(node["diff"], 0.0) + + def test_diff_flamegraph_line_totals_include_allocated_self(self): + """A line's baseline self time cannot exceed its inclusive time.""" + def sample(*frames): + return [ + MockInterpreterInfo(0, [MockThreadInfo(1, list(frames))]) + ] + + target_10 = MockFrameInfo("file.py", 10, "target") + target_20 = MockFrameInfo("file.py", 20, "target") + child = MockFrameInfo("file.py", 30, "child") + + diff = make_diff_collector_with_mock_baseline( + [sample(target_10)] * 100 + ) + for _ in range(10): + diff.collect(sample(target_10)) + for _ in range(90): + diff.collect(sample(child, target_20)) + + data = diff._convert_to_flamegraph_format() + nodes = data["children"] + self.assertEqual(sum(node["baseline"] for node in nodes), 100) + self.assertEqual(sum(node["baseline_total"] for node in nodes), 100) + for node in nodes: + self.assertGreaterEqual(node["baseline"], 0) + self.assertLessEqual(node["baseline"], node["baseline_total"]) + + def test_diff_flamegraph_does_not_duplicate_elided_line_values(self): + """Elided metadata uses each rendered line node's samples.""" + def sample(line, funcname="old_func"): + return [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", line, funcname), + MockFrameInfo("file.py", 1, "caller"), + ]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline( + [sample(10), sample(20)] + ) + diff.collect(sample(30, "new_func")) + + data = diff._convert_to_flamegraph_format() + elided = data["stats"]["elided_flamegraph"] + children = elided["children"] + scale = data["stats"]["baseline_scale"] + self.assertEqual(sum(node["self"] for node in children), 2) + self.assertEqual(sum(node["baseline"] for node in children), 2 * scale) + for node in children: + self.assertEqual(node["self"], 1) + self.assertAlmostEqual(node["baseline"], scale) + self.assertAlmostEqual(node["diff"], -scale) + + def test_diff_flamegraph_elided_ancestors_have_no_lost_self_time(self): + """Matched ancestors only carry inclusive elided geometry.""" + root = MockFrameInfo("file.py", 10, "root") + common = MockFrameInfo("file.py", 20, "common", opcode=100) + old = MockFrameInfo("file.py", 30, "old") + + common_sample = [ + MockInterpreterInfo(0, [MockThreadInfo(1, [common, root])]) + ] + old_sample = [ + MockInterpreterInfo(0, [MockThreadInfo(1, [old, common, root])]) + ] + + diff = make_diff_collector_with_mock_baseline( + [common_sample] * 3 + [old_sample] + ) + diff.collect(common_sample) + + data = diff._convert_to_flamegraph_format() + elided_root = data["stats"]["elided_flamegraph"] + common_node = elided_root["children"][0] + old_node = common_node["children"][0] + + for ancestor in (elided_root, common_node): + self.assertEqual(ancestor["self"], 0) + self.assertEqual(ancestor["baseline"], 0) + self.assertNotIn("opcodes", ancestor) + self.assertLessEqual( + ancestor["baseline"], ancestor["baseline_total"] + ) + self.assertEqual(old_node["self"], 1) + self.assertEqual(old_node["baseline"], old_node["baseline_total"]) + + def test_diff_flamegraph_empty_current(self): + """Empty current profile still produces differential metadata and elided paths.""" + baseline_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [MockFrameInfo("file.py", 10, "func1")]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline([baseline_frames]) + # Don't collect anything in current + + data = diff._convert_to_flamegraph_format() + self.assertIn("name", data) + self.assertEqual(data["value"], 0) + # Differential metadata should still be populated + self.assertTrue(data["stats"]["is_differential"]) + # All baseline paths should be elided since current is empty + self.assertGreater(data["stats"]["elided_count"], 0) + + def test_diff_flamegraph_empty_baseline(self): + """Empty baseline with non-empty current uses scale=1.0 fallback.""" + diff = make_diff_collector_with_mock_baseline([]) + diff.collect([ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 10, "func1"), + MockFrameInfo("file.py", 20, "func2"), + ]) + ]) + ]) + + data = diff._convert_to_flamegraph_format() + strings = data.get("strings", []) + + self.assertTrue(data["stats"]["is_differential"]) + self.assertEqual(data["stats"]["baseline_samples"], 0) + self.assertEqual(data["stats"]["current_samples"], 1) + self.assertAlmostEqual(data["stats"]["baseline_scale"], 1.0) + self.assertEqual(data["stats"]["elided_count"], 0) + + children = data.get("children", []) + self.assertEqual(len(children), 1) + child = children[0] + self.assertIn("func1", resolve_name(child, strings)) + self.assertEqual(child["self_time"], 1) + self.assertAlmostEqual(child["baseline"], 0.0) + self.assertAlmostEqual(child["diff"], 1.0) + self.assertAlmostEqual(child["diff_pct"], 100.0) + + def test_diff_flamegraph_export(self): + """DiffFlamegraphCollector export produces differential HTML.""" + test_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 10, "func1"), + MockFrameInfo("file.py", 20, "func2"), + ]) + ]) + ] + + diff = make_diff_collector_with_mock_baseline([test_frames]) + diff.collect(test_frames) + + flamegraph_out = tempfile.NamedTemporaryFile( + suffix=".html", delete=False + ) + self.addCleanup(close_and_unlink, flamegraph_out) + + with captured_stdout(), captured_stderr(): + export_ok = diff.export(flamegraph_out.name) + + self.assertTrue(export_ok) + self.assertTrue(os.path.exists(flamegraph_out.name)) + self.assertGreater(os.path.getsize(flamegraph_out.name), 0) + + with open(flamegraph_out.name, "r", encoding="utf-8") as f: + content = f.read() + + self.assertIn("", content.lower()) + self.assertIn("Differential Flamegraph", content) + self.assertIn('"is_differential": true', content) + self.assertIn("d3-flame-graph", content) + self.assertIn('id="diff-legend-section"', content) + self.assertIn("Differential Colors", content) + + def test_diff_flamegraph_preserves_metadata(self): + """Differential mode preserves threads and opcodes metadata.""" + test_frames = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [MockFrameInfo("a.py", 10, "func_a", opcode=100)]), + MockThreadInfo(2, [MockFrameInfo("b.py", 20, "func_b", opcode=200)]), + ]) + ] + + diff = make_diff_collector_with_mock_baseline([test_frames]) + diff.collect(test_frames) + + data = diff._convert_to_flamegraph_format() + strings = data.get("strings", []) + + self.assertTrue(data["stats"]["is_differential"]) + + self.assertIn("threads", data) + self.assertEqual(len(data["threads"]), 2) + + children = data.get("children", []) + self.assertEqual(len(children), 2) + + opcodes_found = set() + for child in children: + self.assertIn("diff", child) + self.assertIn("diff_pct", child) + self.assertIn("baseline", child) + self.assertIn("self_time", child) + self.assertIn("threads", child) + + if "opcodes" in child: + opcodes_found.update(child["opcodes"].keys()) + + self.assertIn(100, opcodes_found) + self.assertIn(200, opcodes_found) + + self.assertIn("per_thread_stats", data["stats"]) + per_thread_stats = data["stats"]["per_thread_stats"] + self.assertIn(1, per_thread_stats) + self.assertIn(2, per_thread_stats) + + def test_diff_flamegraph_elided_preserves_metadata(self): + """Elided flamegraph preserves thread_stats, per_thread_stats, and opcodes.""" + baseline_frames_1 = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 10, "func1", opcode=100), + MockFrameInfo("file.py", 20, "func2", opcode=101), + ], status=THREAD_STATUS_HAS_GIL) + ]) + ] + baseline_frames_2 = [ + MockInterpreterInfo(0, [ + MockThreadInfo(1, [ + MockFrameInfo("file.py", 30, "old_func", opcode=200), + MockFrameInfo("file.py", 20, "func2", opcode=101), + ], status=THREAD_STATUS_HAS_GIL) + ]) + ] + + diff = make_diff_collector_with_mock_baseline([baseline_frames_1, baseline_frames_2]) + for _ in range(2): + diff.collect(baseline_frames_1) + + data = diff._convert_to_flamegraph_format() + elided = data["stats"]["elided_flamegraph"] + + self.assertTrue(elided["stats"]["is_differential"]) + self.assertIn("thread_stats", elided["stats"]) + self.assertIn("per_thread_stats", elided["stats"]) + self.assertIn("baseline_samples", elided["stats"]) + self.assertIn("current_samples", elided["stats"]) + self.assertIn("strings", elided) + + elided_strings = elided.get("strings", []) + children = elided.get("children", []) + self.assertEqual(len(children), 1) + old_func_node = children[0] + if "opcodes" in old_func_node: + self.assertIn(200, old_func_node["opcodes"]) + self.assertEqual(old_func_node["self_time"], 0) + self.assertAlmostEqual(old_func_node["diff_pct"], -100.0) + + def test_diff_flamegraph_load_baseline(self): + """Diff annotations work when baseline is loaded from a binary file.""" + from profiling.sampling.binary_collector import BinaryCollector + from profiling.sampling.stack_collector import DiffFlamegraphCollector + from .test_binary_format import make_frame, make_thread, make_interpreter + + hot_sample = [make_interpreter(0, [make_thread(1, [ + make_frame("file.py", 10, "hot_leaf"), + make_frame("file.py", 20, "caller"), + ])])] + cold_sample = [make_interpreter(0, [make_thread(1, [ + make_frame("file.py", 30, "cold_leaf"), + make_frame("file.py", 20, "caller"), + ])])] + + # Baseline: 2 samples, current: 4, scale = 2.0 + bin_file = tempfile.NamedTemporaryFile(suffix=".bin", delete=False) + self.addCleanup(close_and_unlink, bin_file) + + writer = BinaryCollector( + bin_file.name, sample_interval_usec=1000, compression='none' + ) + writer.collect(hot_sample) + writer.collect(cold_sample) + writer.export(None) + + diff = DiffFlamegraphCollector( + 1000, baseline_binary_path=bin_file.name + ) + hot_mock = [MockInterpreterInfo(0, [MockThreadInfo(1, [ + MockFrameInfo("file.py", 10, "hot_leaf"), + MockFrameInfo("file.py", 20, "caller"), + ])])] + cold_mock = [MockInterpreterInfo(0, [MockThreadInfo(1, [ + MockFrameInfo("file.py", 30, "cold_leaf"), + MockFrameInfo("file.py", 20, "caller"), + ])])] + for _ in range(3): + diff.collect(hot_mock) + diff.collect(cold_mock) + + data = diff._convert_to_flamegraph_format() + strings = data.get("strings", []) + + self.assertTrue(data["stats"]["is_differential"]) + self.assertAlmostEqual(data["stats"]["baseline_scale"], 2.0) + + children = data.get("children", []) + hot_node = find_child_by_name(children, strings, "hot_leaf") + cold_node = find_child_by_name(children, strings, "cold_leaf") + self.assertIsNotNone(hot_node) + self.assertIsNotNone(cold_node) + + # hot_leaf regressed (+50%) + self.assertAlmostEqual(hot_node["baseline"], 2.0) + self.assertEqual(hot_node["self_time"], 3) + self.assertAlmostEqual(hot_node["diff"], 1.0) + self.assertAlmostEqual(hot_node["diff_pct"], 50.0) + + # cold_leaf improved (-50%) + self.assertAlmostEqual(cold_node["baseline"], 2.0) + self.assertEqual(cold_node["self_time"], 1) + self.assertAlmostEqual(cold_node["diff"], -1.0) + self.assertAlmostEqual(cold_node["diff_pct"], -50.0) + + def test_jsonl_collector_export_exact_output(self): + jsonl_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, jsonl_out) + + collector = JsonlCollector(1000) + collector.run_id = "run-123" + + test_frames1 = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("file.py", 10, "func1"), + MockFrameInfo("file.py", 20, "func2"), + ], + ) + ], + ) + ] + test_frames2 = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("file.py", 10, "func1"), + MockFrameInfo("file.py", 20, "func2"), + ], + ) + ], + ) + ] # Same stack + test_frames3 = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, [MockFrameInfo("other.py", 5, "other_func")] + ) + ], + ) + ] + + collector.collect(test_frames1) + collector.collect(test_frames2) + collector.collect(test_frames3) + + collector.export(jsonl_out.name) + + with open(jsonl_out.name, "r", encoding="utf-8") as f: + content = f.read() + + self.assertEqual( + content, + ( + '{"type":"meta","v":0,"run_id":"run-123","sample_interval_usec":1000}\n' + '{"type":"string_table","v":0,"run_id":"run-123","strings":[{"str_id":0,"value":"func1"},{"str_id":1,"value":"file.py"},{"str_id":2,"value":"func2"},{"str_id":3,"value":"other_func"},{"str_id":4,"value":"other.py"}]}\n' + '{"type":"frame_table","v":0,"run_id":"run-123","frames":[{"frame_id":0,"path_str_id":1,"func_str_id":0,"line":10,"end_line":10},{"frame_id":1,"path_str_id":1,"func_str_id":2,"line":20,"end_line":20},{"frame_id":2,"path_str_id":4,"func_str_id":3,"line":5,"end_line":5}]}\n' + '{"type":"agg","v":0,"run_id":"run-123","kind":"frame","scope":"final","samples_total":3,"entries":[{"frame_id":0,"self":2,"cumulative":2},{"frame_id":1,"self":0,"cumulative":2},{"frame_id":2,"self":1,"cumulative":1}]}\n' + '{"type":"end","v":0,"run_id":"run-123","samples_total":3}\n' + ), + ) + + def test_jsonl_collector_export_includes_mode_in_meta(self): + jsonl_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, jsonl_out) + + collector = JsonlCollector(1000, mode=PROFILING_MODE_CPU) + collector.collect( + [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, [MockFrameInfo("file.py", 10, "func")] + ) + ], + ) + ] + ) + collector.export(jsonl_out.name) + + with open(jsonl_out.name, "r", encoding="utf-8") as f: + records = [json.loads(line) for line in f] + + meta_record = next( + record for record in records if record["type"] == "meta" + ) + self.assertEqual(meta_record["mode"], "cpu") + + def test_jsonl_collector_export_empty_profile(self): + jsonl_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, jsonl_out) + + collector = JsonlCollector(1000) + collector.run_id = "run-123" + collector.export(jsonl_out.name) + + with open(jsonl_out.name, "r", encoding="utf-8") as f: + records = [json.loads(line) for line in f] + + self.assertEqual( + [record["type"] for record in records], ["meta", "end"] + ) + self.assertEqual(records[0]["sample_interval_usec"], 1000) + self.assertEqual(records[0]["run_id"], "run-123") + self.assertEqual(records[1]["samples_total"], 0) + self.assertEqual(records[1]["run_id"], "run-123") + + def test_jsonl_collector_recursive_frames_counted_once_per_sample(self): + jsonl_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, jsonl_out) + + collector = JsonlCollector(1000) + collector.collect( + [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo( + "recursive.py", 10, "recursive_func" + ), + MockFrameInfo( + "recursive.py", 10, "recursive_func" + ), + MockFrameInfo( + "recursive.py", 10, "recursive_func" + ), + ], + ) + ], + ) + ] + ) + collector.export(jsonl_out.name) + + with open(jsonl_out.name, "r", encoding="utf-8") as f: + records = [json.loads(line) for line in f] + + _, _, frame_defs, agg_record, end_record = jsonl_tables(records) + self.assertEqual(len(frame_defs), 1) + self.assertEqual( + agg_record["entries"], + [ + { + "frame_id": frame_defs[0]["frame_id"], + "self": 1, + "cumulative": 1, + } + ], + ) + self.assertEqual(agg_record["samples_total"], 1) + self.assertEqual(end_record["samples_total"], 1) + + def test_jsonl_collector_skip_idle_filters_threads(self): + jsonl_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, jsonl_out) + + active_status = THREAD_STATUS_HAS_GIL | THREAD_STATUS_ON_CPU + frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [MockFrameInfo("active1.py", 10, "active_func1")], + status=active_status, + ), + MockThreadInfo( + 2, + [MockFrameInfo("idle.py", 20, "idle_func")], + status=0, + ), + MockThreadInfo( + 3, + [MockFrameInfo("active2.py", 30, "active_func2")], + status=active_status, + ), + ], + ) + ] + + def export_summary(skip_idle): + collector = JsonlCollector(1000, skip_idle=skip_idle) + collector.collect(frames) + collector.export(jsonl_out.name) + + with open(jsonl_out.name, "r", encoding="utf-8") as f: + records = [json.loads(line) for line in f] + + _, str_defs, frame_defs, agg_record, _ = jsonl_tables(records) + paths = {str_defs[item["path_str_id"]] for item in frame_defs} + funcs = {str_defs[item["func_str_id"]] for item in frame_defs} + return paths, funcs, agg_record["samples_total"] + + paths, funcs, samples_total = export_summary(skip_idle=True) + self.assertEqual(paths, {"active1.py", "active2.py"}) + self.assertEqual(funcs, {"active_func1", "active_func2"}) + self.assertEqual(samples_total, 2) + + paths, funcs, samples_total = export_summary(skip_idle=False) + self.assertEqual(paths, {"active1.py", "idle.py", "active2.py"}) + self.assertEqual(funcs, {"active_func1", "idle_func", "active_func2"}) + self.assertEqual(samples_total, 3) + + def test_jsonl_collector_splits_large_exports_into_chunks(self): + jsonl_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, jsonl_out) + + collector = JsonlCollector(1000) + + for i in range(257): + collector.collect( + [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo( + f"file{i}.py", i + 1, f"func{i}" + ) + ], + ) + ], + ) + ] + ) + + collector.export(jsonl_out.name) + + with open(jsonl_out.name, "r", encoding="utf-8") as f: + records = [json.loads(line) for line in f] + + run_ids = {record["run_id"] for record in records} + self.assertEqual(len(run_ids), 1) + self.assertRegex(next(iter(run_ids)), r"^[0-9a-f]{32}$") + + _, str_defs, frame_defs, agg_record, end_record = jsonl_tables( + records + ) + str_chunks = [ + record for record in records if record["type"] == "string_table" + ] + frame_chunks = [ + record for record in records if record["type"] == "frame_table" + ] + agg_chunks = [record for record in records if record["type"] == "agg"] + + self.assertEqual( + [len(record["strings"]) for record in str_chunks], + [256, 256, 2], + ) + self.assertEqual( + [len(record["frames"]) for record in frame_chunks], [256, 1] + ) + self.assertEqual( + [len(record["entries"]) for record in agg_chunks], [256, 1] + ) + self.assertEqual(len(str_defs), 514) + self.assertEqual(len(frame_defs), 257) + self.assertEqual(agg_record["samples_total"], 257) + self.assertEqual(end_record["samples_total"], 257) + + def test_jsonl_collector_respects_weight_for_rle_batched_samples(self): + """weight>1 (from binary replay RLE) is honored in self/cumulative.""" + jsonl_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, jsonl_out) + + collector = JsonlCollector(1000) + leaf = MockFrameInfo("file.py", 10, "leaf") + non_leaf = MockFrameInfo("file.py", 20, "non_leaf") + + collector.process_frames([leaf, non_leaf], _thread_id=1, weight=5) + collector.export(jsonl_out.name) + + with open(jsonl_out.name, "r", encoding="utf-8") as f: + records = [json.loads(line) for line in f] + + _, str_defs, frame_defs, agg, end = jsonl_tables(records) + self.assertEqual(end["samples_total"], 5) + self.assertEqual(agg["samples_total"], 5) + self.assertEqual( + {str_defs[fd["func_str_id"]]: fd["frame_id"] for fd in frame_defs}, + {"leaf": 0, "non_leaf": 1}, + ) + self.assertEqual(agg["entries"], [ + {"frame_id": 0, "self": 5, "cumulative": 5}, + {"frame_id": 1, "self": 0, "cumulative": 5}, + ]) + + def test_jsonl_collector_recursion_with_weight(self): + """Recursion dedup respects weight, not occurrence count.""" + jsonl_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, jsonl_out) + + collector = JsonlCollector(1000) + recursive = MockFrameInfo("rec.py", 10, "f") + + collector.process_frames([recursive] * 3, _thread_id=1, weight=3) + collector.export(jsonl_out.name) + + with open(jsonl_out.name, "r", encoding="utf-8") as f: + records = [json.loads(line) for line in f] + + _, _, frame_defs, agg, _ = jsonl_tables(records) + self.assertEqual(len(frame_defs), 1) + self.assertEqual(agg["entries"], [ + {"frame_id": 0, "self": 3, "cumulative": 3}, + ]) + + def test_jsonl_collector_emits_col_and_end_col_when_present(self): + """All four location fields are emitted when col/end_col are >= 0.""" + jsonl_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, jsonl_out) + + collector = JsonlCollector(1000) + frame = MockFrameInfo("test.py", 0, "f") + frame.location = LocationInfo(42, 45, 4, 12) + frames = [ + MockInterpreterInfo( + 0, [MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames) + collector.export(jsonl_out.name) + + with open(jsonl_out.name, "r", encoding="utf-8") as f: + records = [json.loads(line) for line in f] + + _, str_defs, frame_defs, _, _ = jsonl_tables(records) + self.assertEqual(frame_defs, [ + { + "frame_id": 0, + "path_str_id": 1, + "func_str_id": 0, + "line": 42, + "end_line": 45, + "col": 4, + "end_col": 12, + }, + ]) + self.assertEqual(str_defs, {0: "f", 1: "test.py"}) + + def test_jsonl_collector_partial_location_elision(self): + """Negative col/end_col/end_line fields are individually elided.""" + # _get_or_create_frame_id interns funcname before filename, so + # func_str_id=0 ("f") and path_str_id=1 ("test.py"). + common = {"frame_id": 0, "path_str_id": 1, "func_str_id": 0} + cases = [ + (LocationInfo(42, 45, -1, 12), + {**common, "line": 42, "end_line": 45, "end_col": 12}), + (LocationInfo(42, 45, 4, -1), + {**common, "line": 42, "end_line": 45, "col": 4}), + (LocationInfo(42, 0, 4, 8), + {**common, "line": 42, "col": 4, "end_col": 8}), + ] + for loc, expected_frame_def in cases: + with self.subTest(location=loc): + jsonl_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, jsonl_out) + + collector = JsonlCollector(1000) + frame = MockFrameInfo("test.py", 0, "f") + frame.location = loc + frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)], + ) + ] + collector.collect(frames) + collector.export(jsonl_out.name) + + with open(jsonl_out.name, "r", encoding="utf-8") as f: + records = [json.loads(line) for line in f] + + _, _, frame_defs, _, _ = jsonl_tables(records) + self.assertEqual(frame_defs, [expected_frame_def]) + + +class TestRecursiveFunctionHandling(unittest.TestCase): + """Tests for correct handling of recursive functions in cumulative stats.""" + + def test_pstats_collector_recursive_function_single_sample(self): + """Test that recursive functions are counted once per sample, not per occurrence.""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Simulate a recursive function appearing 5 times in one sample + recursive_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + ], + ) + ], + ) + ] + collector.collect(recursive_frames) + + location = ("test.py", 10, "recursive_func") + # Should count as 1 cumulative call (present in 1 sample), not 5 + self.assertEqual(collector.result[location]["cumulative_calls"], 1) + # Direct calls should be 1 (top of stack) + self.assertEqual(collector.result[location]["direct_calls"], 1) + + def test_pstats_collector_recursive_function_multiple_samples(self): + """Test cumulative counting across multiple samples with recursion.""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Sample 1: recursive function at depth 3 + sample1 = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + ], + ) + ], + ) + ] + # Sample 2: recursive function at depth 2 + sample2 = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + ], + ) + ], + ) + ] + # Sample 3: recursive function at depth 4 + sample3 = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + ], + ) + ], + ) + ] + + collector.collect(sample1) + collector.collect(sample2) + collector.collect(sample3) + + location = ("test.py", 10, "recursive_func") + # Should count as 3 cumulative calls (present in 3 samples) + # Not 3+2+4=9 which would be the buggy behavior + self.assertEqual(collector.result[location]["cumulative_calls"], 3) + self.assertEqual(collector.result[location]["direct_calls"], 3) + + def test_pstats_collector_mixed_recursive_and_nonrecursive(self): + """Test a call stack with both recursive and non-recursive functions.""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Stack: main -> foo (recursive x3) -> bar + frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("test.py", 50, "bar"), # top of stack + MockFrameInfo("test.py", 20, "foo"), # recursive + MockFrameInfo("test.py", 20, "foo"), # recursive + MockFrameInfo("test.py", 20, "foo"), # recursive + MockFrameInfo("test.py", 10, "main"), # bottom + ], + ) + ], + ) + ] + collector.collect(frames) + + # bar: 1 cumulative (in stack), 1 direct (top) + self.assertEqual(collector.result[("test.py", 50, "bar")]["cumulative_calls"], 1) + self.assertEqual(collector.result[("test.py", 50, "bar")]["direct_calls"], 1) + + # foo: 1 cumulative (counted once despite 3 occurrences), 0 direct + self.assertEqual(collector.result[("test.py", 20, "foo")]["cumulative_calls"], 1) + self.assertEqual(collector.result[("test.py", 20, "foo")]["direct_calls"], 0) + + # main: 1 cumulative, 0 direct + self.assertEqual(collector.result[("test.py", 10, "main")]["cumulative_calls"], 1) + self.assertEqual(collector.result[("test.py", 10, "main")]["direct_calls"], 0) + + def test_pstats_collector_cumulative_percentage_cannot_exceed_100(self): + """Test that cumulative percentage stays <= 100% even with deep recursion.""" + collector = PstatsCollector(sample_interval_usec=1000000) # 1 second for easy math + + # Collect 10 samples, each with recursive function at depth 100 + for _ in range(10): + frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [MockFrameInfo("test.py", 10, "deep_recursive")] * 100, + ) + ], + ) + ] + collector.collect(frames) + + location = ("test.py", 10, "deep_recursive") + # Cumulative calls should be 10 (number of samples), not 1000 + self.assertEqual(collector.result[location]["cumulative_calls"], 10) + + # Verify stats calculation gives correct percentage + collector.create_stats() + stats = collector.stats[location] + # stats format: (direct_calls, cumulative_calls, total_time, cumulative_time, callers) + cumulative_calls = stats[1] + self.assertEqual(cumulative_calls, 10) + + def test_pstats_collector_different_lines_same_function_counted_separately(self): + """Test that different line numbers in same function are tracked separately.""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Function with multiple line numbers (e.g., different call sites within recursion) + frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("test.py", 15, "func"), # line 15 + MockFrameInfo("test.py", 12, "func"), # line 12 + MockFrameInfo("test.py", 15, "func"), # line 15 again + MockFrameInfo("test.py", 10, "func"), # line 10 + ], + ) + ], + ) + ] + collector.collect(frames) + + # Each unique (file, line, func) should be counted once + self.assertEqual(collector.result[("test.py", 15, "func")]["cumulative_calls"], 1) + self.assertEqual(collector.result[("test.py", 12, "func")]["cumulative_calls"], 1) + self.assertEqual(collector.result[("test.py", 10, "func")]["cumulative_calls"], 1) + + +class TestLocationHelpers(unittest.TestCase): + """Tests for location handling helper functions.""" + + def test_extract_lineno_from_location_info(self): + """Test extracting lineno from LocationInfo namedtuple.""" + loc = LocationInfo(42, 45, 0, 10) + self.assertEqual(extract_lineno(loc), 42) + + def test_extract_lineno_from_tuple(self): + """Test extracting lineno from plain tuple.""" + loc = (100, 105, 5, 20) + self.assertEqual(extract_lineno(loc), 100) + + def test_extract_lineno_from_none(self): + """Test extracting lineno from None (synthetic frames).""" + self.assertEqual(extract_lineno(None), 0) + + def test_extract_lineno_from_int(self): + """Test extracting lineno from a bare integer line number. + + Mirrors normalize_location's int contract so callers like the + collapsed/flamegraph collectors do not crash on a bare-int location. + """ + self.assertEqual(extract_lineno(42), 42) + self.assertEqual(extract_lineno(0), 0) + + def test_normalize_location_with_int(self): + """Test normalize_location expands a legacy integer line number.""" + result = normalize_location(42) + self.assertEqual(result, (42, 42, -1, -1)) + + def test_normalize_location_with_location_info(self): + """Test normalize_location passes through LocationInfo.""" + loc = LocationInfo(10, 15, 0, 5) + result = normalize_location(loc) + self.assertEqual(result, loc) + + def test_normalize_location_with_tuple(self): + """Test normalize_location passes through tuple.""" + loc = (10, 15, 0, 5) + result = normalize_location(loc) + self.assertEqual(result, loc) + + def test_normalize_location_with_none(self): + """Test normalize_location returns DEFAULT_LOCATION for None.""" + result = normalize_location(None) + self.assertEqual(result, DEFAULT_LOCATION) + self.assertEqual(result, (0, 0, -1, -1)) + + +class TestOpcodeFormatting(unittest.TestCase): + """Tests for opcode formatting utilities.""" + + def test_get_opcode_info_standard_opcode(self): + """Test get_opcode_info for a standard opcode.""" + # LOAD_CONST is a standard opcode + load_const = opcode.opmap.get('LOAD_CONST') + if load_const is not None: + info = get_opcode_info(load_const) + self.assertEqual(info['opname'], 'LOAD_CONST') + self.assertEqual(info['base_opname'], 'LOAD_CONST') + self.assertFalse(info['is_specialized']) + + def test_get_opcode_info_unknown_opcode(self): + """Test get_opcode_info for an unknown opcode.""" + info = get_opcode_info(999) + self.assertEqual(info['opname'], '<999>') + self.assertEqual(info['base_opname'], '<999>') + self.assertFalse(info['is_specialized']) + + def test_format_opcode_standard(self): + """Test format_opcode for a standard opcode.""" + load_const = opcode.opmap.get('LOAD_CONST') + if load_const is not None: + formatted = format_opcode(load_const) + self.assertEqual(formatted, 'LOAD_CONST') + + def test_format_opcode_specialized(self): + """Test format_opcode for a specialized opcode shows base in parens.""" + if not hasattr(opcode, '_specialized_opmap'): + self.skipTest("No specialized opcodes in this Python version") + if not hasattr(opcode, '_specializations'): + self.skipTest("No specialization info in this Python version") + + # Find any specialized opcode to test + for base_name, variants in opcode._specializations.items(): + if not variants: + continue + variant_name = variants[0] + variant_opcode = opcode._specialized_opmap.get(variant_name) + if variant_opcode is None: + continue + formatted = format_opcode(variant_opcode) + # Should show: VARIANT_NAME (BASE_NAME) + self.assertIn(variant_name, formatted) + self.assertIn(f'({base_name})', formatted) + return + + self.skipTest("No specialized opcodes found") + + def test_format_opcode_unknown(self): + """Test format_opcode for an unknown opcode.""" + formatted = format_opcode(999) + self.assertEqual(formatted, '<999>') + + +class TestLocationInCollectors(unittest.TestCase): + """Tests for location tuple handling in each collector.""" + + def _make_frames_with_location(self, location, opcode=None): + """Create test frames with a specific location.""" + frame = MockFrameInfo("test.py", 0, "test_func", opcode) + # Override the location + frame.location = location + return [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)] + ) + ] + + def test_pstats_collector_with_location_info(self): + """Test PstatsCollector handles LocationInfo properly.""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Frame with LocationInfo + frame = MockFrameInfo("test.py", 42, "my_function") + frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames) + + # Should extract lineno from location + key = ("test.py", 42, "my_function") + self.assertIn(key, collector.result) + self.assertEqual(collector.result[key]["direct_calls"], 1) + + def test_pstats_collector_with_none_location(self): + """Test PstatsCollector handles None location (synthetic frames).""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Create frame with None location (like GC frame) + frame = MockFrameInfo("~", 0, "") + frame.location = None # Synthetic frame has no location + frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames) + + # Should use lineno=0 for None location + key = ("~", 0, "") + self.assertIn(key, collector.result) + + def test_collapsed_stack_with_location_info(self): + """Test CollapsedStackCollector handles LocationInfo properly.""" + collector = CollapsedStackCollector(1000) + + frame1 = MockFrameInfo("main.py", 10, "main") + frame2 = MockFrameInfo("utils.py", 25, "helper") + frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame1, frame2], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames) + + # Check that linenos were extracted correctly + self.assertEqual(len(collector.stack_counter), 1) + (path, _), count = list(collector.stack_counter.items())[0] + # Reversed order: helper at top, main at bottom + self.assertEqual(path[0], ("utils.py", 25, "helper")) + self.assertEqual(path[1], ("main.py", 10, "main")) + + def test_flamegraph_collector_with_location_info(self): + """Test FlamegraphCollector handles LocationInfo properly.""" + collector = FlamegraphCollector(sample_interval_usec=1000) + + frame = MockFrameInfo("app.py", 100, "process_data") + frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames) + + data = collector._convert_to_flamegraph_format() + # Verify the function name includes lineno from location + strings = data.get("strings", []) + name_found = any("process_data" in s and "100" in s for s in strings if isinstance(s, str)) + self.assertTrue(name_found, f"Expected to find 'process_data' with line 100 in {strings}") + + def test_gecko_collector_with_location_info(self): + """Test GeckoCollector handles LocationInfo properly.""" + collector = GeckoCollector(sample_interval_usec=1000) + + frame = MockFrameInfo("server.py", 50, "handle_request") + frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames) + + profile = collector._build_profile() + # Check that the function was recorded + self.assertEqual(len(profile["threads"]), 1) + thread_data = profile["threads"][0] + string_array = profile["shared"]["stringArray"] + + # Verify function name is in string table + self.assertIn("handle_request", string_array) + + def test_jsonl_collector_with_location_info(self): + """Test JsonlCollector handles LocationInfo properly.""" + jsonl_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, jsonl_out) + + collector = JsonlCollector(sample_interval_usec=1000) + + # Frame with LocationInfo + frame = MockFrameInfo("test.py", 42, "my_function") + frames = [ + MockInterpreterInfo( + 0, [MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames) + + collector.export(jsonl_out.name) + + with open(jsonl_out.name, "r", encoding="utf-8") as f: + records = [json.loads(line) for line in f] + + meta, str_defs, frame_defs, agg, end = jsonl_tables(records) + self.assertEqual(meta["sample_interval_usec"], 1000) + self.assertEqual(agg["samples_total"], 1) + self.assertEqual(end["samples_total"], 1) + self.assertEqual(len(frame_defs), 1) + self.assertEqual(str_defs[frame_defs[0]["path_str_id"]], "test.py") + self.assertEqual(str_defs[frame_defs[0]["func_str_id"]], "my_function") + self.assertEqual( + frame_defs[0], + { + "frame_id": 0, + "path_str_id": frame_defs[0]["path_str_id"], + "func_str_id": frame_defs[0]["func_str_id"], + "line": 42, + "end_line": 42, + }, + ) + + def test_jsonl_collector_with_none_location(self): + """Test JsonlCollector handles None location (synthetic frames).""" + jsonl_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, jsonl_out) + + collector = JsonlCollector(sample_interval_usec=1000) + + # Create frame with None location (like GC frame) + frame = MockFrameInfo("~", 0, "") + frame.location = None # Synthetic frame has no location + frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames) + + collector.export(jsonl_out.name) + + with open(jsonl_out.name, "r", encoding="utf-8") as f: + records = [json.loads(line) for line in f] + + meta, str_defs, frame_defs, agg, end = jsonl_tables(records) + self.assertEqual(meta["sample_interval_usec"], 1000) + self.assertEqual(agg["samples_total"], 1) + self.assertEqual(end["samples_total"], 1) + self.assertEqual(len(frame_defs), 1) + self.assertEqual(str_defs[frame_defs[0]["path_str_id"]], "~") + self.assertEqual(str_defs[frame_defs[0]["func_str_id"]], "") + self.assertEqual( + frame_defs[0], + { + "frame_id": 0, + "path_str_id": frame_defs[0]["path_str_id"], + "func_str_id": frame_defs[0]["func_str_id"], + "line": 0, + }, + ) + + +class TestOpcodeHandling(unittest.TestCase): + """Tests for opcode field handling in collectors.""" + + def test_frame_with_opcode(self): + """Test MockFrameInfo properly stores opcode.""" + frame = MockFrameInfo("test.py", 10, "my_func", opcode=90) + self.assertEqual(frame.opcode, 90) + # Verify tuple representation includes opcode + self.assertEqual(frame[3], 90) + self.assertEqual(len(frame), 4) + + def test_frame_without_opcode(self): + """Test MockFrameInfo with no opcode defaults to None.""" + frame = MockFrameInfo("test.py", 10, "my_func") + self.assertIsNone(frame.opcode) + self.assertIsNone(frame[3]) + + def test_collectors_ignore_opcode_for_key_generation(self): + """Test that collectors use (filename, lineno, funcname) as key, not opcode.""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Same function, different opcodes + frame1 = MockFrameInfo("test.py", 10, "func", opcode=90) + frame2 = MockFrameInfo("test.py", 10, "func", opcode=100) + + frames1 = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame1], status=THREAD_STATUS_HAS_GIL)] + ) + ] + frames2 = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame2], status=THREAD_STATUS_HAS_GIL)] + ) + ] + + collector.collect(frames1) + collector.collect(frames2) + + # Should be counted as same function (opcode not in key) + key = ("test.py", 10, "func") + self.assertIn(key, collector.result) + self.assertEqual(collector.result[key]["direct_calls"], 2) + + +class TestGeckoOpcodeMarkers(unittest.TestCase): + """Tests for GeckoCollector opcode interval markers.""" + + def test_gecko_collector_opcodes_disabled_by_default(self): + """Test that opcode tracking is disabled by default.""" + collector = GeckoCollector(sample_interval_usec=1000) + self.assertFalse(collector.opcodes_enabled) + + def test_gecko_collector_opcodes_enabled(self): + """Test that opcode tracking can be enabled.""" + collector = GeckoCollector(sample_interval_usec=1000, opcodes=True) + self.assertTrue(collector.opcodes_enabled) + + def test_gecko_opcode_state_tracking(self): + """Test that GeckoCollector tracks opcode state changes.""" + collector = GeckoCollector(sample_interval_usec=1000, opcodes=True) + self.addCleanup(collector._cleanup_spills) + + # First sample with opcode 90 (RAISE_VARARGS) + frame1 = MockFrameInfo("test.py", 10, "func", opcode=90) + frames1 = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame1], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames1) + + # Should start tracking this opcode state + self.assertIn(1, collector.opcode_state) + state = collector.opcode_state[1] + self.assertEqual(state[0], 90) # opcode + self.assertEqual(state[1], 10) # lineno + self.assertEqual(state[3], "func") # funcname + + def test_gecko_opcode_state_change_emits_marker(self): + """Test that opcode state change emits an interval marker.""" + collector = GeckoCollector(sample_interval_usec=1000, opcodes=True) + + # First sample: opcode 90 + frame1 = MockFrameInfo("test.py", 10, "func", opcode=90) + frames1 = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame1], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames1) + + # Second sample: different opcode 100 + frame2 = MockFrameInfo("test.py", 10, "func", opcode=100) + frames2 = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame2], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames2) + + # Should have emitted a marker for the first opcode + profile = collector._build_profile() + markers = profile["threads"][0]["markers"] + assert_gecko_column_lengths( + self, markers, + ("data", "name", "startTime", "endTime", "phase", "category"), + ) + opcode_markers = gecko_opcode_marker_data(profile) + self.assertIn( + { + "opcode": 90, + "line": 10, + "function": "func", + }, + [ + { + "opcode": marker["opcode"], + "line": marker["line"], + "function": marker["function"], + } + for marker in opcode_markers + ], + ) + + def test_gecko_opcode_markers_not_emitted_when_disabled(self): + """Test that no opcode markers when opcodes=False.""" + collector = GeckoCollector(sample_interval_usec=1000, opcodes=False) + + frame1 = MockFrameInfo("test.py", 10, "func", opcode=90) + frames1 = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame1], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames1) + + frame2 = MockFrameInfo("test.py", 10, "func", opcode=100) + frames2 = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame2], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames2) + + profile = collector._build_profile() + self.assertEqual(gecko_opcode_marker_data(profile), []) + self.assertEqual(profile["meta"]["markerSchema"], []) + + def test_gecko_opcode_with_none_opcode(self): + """Test that None opcode doesn't cause issues.""" + collector = GeckoCollector(sample_interval_usec=1000, opcodes=True) + + # Frame with no opcode (None) + frame = MockFrameInfo("test.py", 10, "func", opcode=None) + frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)] + ) + ] + collector.collect(frames) + + profile = collector._build_profile() + self.assertEqual(gecko_opcode_marker_data(profile), []) + + +class TestCollectorFrameFormat(unittest.TestCase): + """Tests verifying all collectors handle the 4-element frame format.""" + + def _make_sample_frames(self): + """Create sample frames with full format: (filename, location, funcname, opcode).""" + return [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("app.py", 100, "main", opcode=90), + MockFrameInfo("utils.py", 50, "helper", opcode=100), + MockFrameInfo("lib.py", 25, "process", opcode=None), + ], + status=THREAD_STATUS_HAS_GIL, + ) + ], + ) + ] + + def test_pstats_collector_frame_format(self): + """Test PstatsCollector with 4-element frame format.""" + collector = PstatsCollector(sample_interval_usec=1000) + collector.collect(self._make_sample_frames()) + + # All three functions should be recorded + self.assertEqual(len(collector.result), 3) + self.assertIn(("app.py", 100, "main"), collector.result) + self.assertIn(("utils.py", 50, "helper"), collector.result) + self.assertIn(("lib.py", 25, "process"), collector.result) + + def test_collapsed_stack_frame_format(self): + """Test CollapsedStackCollector with 4-element frame format.""" + collector = CollapsedStackCollector(sample_interval_usec=1000) + collector.collect(self._make_sample_frames()) + + self.assertEqual(len(collector.stack_counter), 1) + (path, _), _ = list(collector.stack_counter.items())[0] + # 3 frames in the path (reversed order) + self.assertEqual(len(path), 3) + + def test_flamegraph_collector_frame_format(self): + """Test FlamegraphCollector with 4-element frame format.""" + collector = FlamegraphCollector(sample_interval_usec=1000) + collector.collect(self._make_sample_frames()) + + data = collector._convert_to_flamegraph_format() + # Should have processed the frames + self.assertIn("children", data) + + def test_gecko_collector_frame_format(self): + """Test GeckoCollector with 4-element frame format.""" + collector = GeckoCollector(sample_interval_usec=1000) + collector.collect(self._make_sample_frames()) + + profile = collector._build_profile() + # Should have one thread with the frames + self.assertEqual(len(profile["threads"]), 1) + thread = profile["threads"][0] + # Should have recorded 3 functions + self.assertEqual(thread["funcTable"]["length"], 3) + + def test_jsonl_collector_frame_format(self): + """Test JsonlCollector with 4-element frame format.""" + collector = JsonlCollector(sample_interval_usec=1000) + collector.collect(self._make_sample_frames()) + + with tempfile.NamedTemporaryFile(delete=False) as f: + self.addClassCleanup(close_and_unlink, f) + collector.export(f.name) + + with open(f.name, "r", encoding="utf-8") as fp: + records = [json.loads(line) for line in fp] + + _, str_defs, frame_defs, _, _ = jsonl_tables(records) + + self.assertEqual(len(frame_defs), 3) + + paths = {str_defs[item["path_str_id"]] for item in frame_defs} + funcs = {str_defs[item["func_str_id"]] for item in frame_defs} + + self.assertEqual(paths, {"app.py", "utils.py", "lib.py"}) + self.assertEqual(funcs, {"main", "helper", "process"}) + + +class TestInternalFrameFiltering(unittest.TestCase): + """Tests for filtering internal profiler frames from output.""" + + def test_filter_internal_frames(self): + """Test that _sync_coordinator frames are filtered from anywhere in stack.""" + from profiling.sampling.collector import filter_internal_frames + + # Stack with _sync_coordinator in the middle (realistic scenario) + frames = [ + MockFrameInfo("user_script.py", 10, "user_func"), + MockFrameInfo("/path/to/_sync_coordinator.py", 100, "main"), + MockFrameInfo("", 87, "_run_code"), + ] + + filtered = filter_internal_frames(frames) + self.assertEqual(len(filtered), 2) + self.assertEqual(filtered[0].filename, "user_script.py") + self.assertEqual(filtered[1].filename, "") + + def test_pstats_collector_filters_internal_frames(self): + """Test that PstatsCollector filters out internal frames.""" + collector = PstatsCollector(sample_interval_usec=1000) + + frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("user_script.py", 10, "user_func"), + MockFrameInfo("/path/to/_sync_coordinator.py", 100, "main"), + MockFrameInfo("", 87, "_run_code"), + ], + status=THREAD_STATUS_HAS_GIL, + ) + ], + ) + ] + collector.collect(frames) + + self.assertEqual(len(collector.result), 2) + self.assertIn(("user_script.py", 10, "user_func"), collector.result) + self.assertIn(("", 87, "_run_code"), collector.result) + + def test_gecko_collector_filters_internal_frames(self): + """Test that GeckoCollector filters out internal frames.""" + collector = GeckoCollector(sample_interval_usec=1000) + + frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("app.py", 50, "run"), + MockFrameInfo("/lib/_sync_coordinator.py", 100, "main"), + ], + status=THREAD_STATUS_HAS_GIL, + ) + ], + ) + ] + collector.collect(frames) + + profile = collector._build_profile() + string_array = profile["shared"]["stringArray"] + + # Should not contain _sync_coordinator functions + for s in string_array: + self.assertNotIn("_sync_coordinator", s) + + def test_flamegraph_collector_filters_internal_frames(self): + """Test that FlamegraphCollector filters out internal frames.""" + collector = FlamegraphCollector(sample_interval_usec=1000) + + frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("app.py", 50, "run"), + MockFrameInfo("/lib/_sync_coordinator.py", 100, "main"), + MockFrameInfo("", 87, "_run_code"), + ], + status=THREAD_STATUS_HAS_GIL, + ) + ], + ) + ] + collector.collect(frames) + + data = collector._convert_to_flamegraph_format() + strings = data.get("strings", []) + + for s in strings: + self.assertNotIn("_sync_coordinator", s) + + def test_collapsed_stack_collector_filters_internal_frames(self): + """Test that CollapsedStackCollector filters out internal frames.""" + collector = CollapsedStackCollector(sample_interval_usec=1000) + + frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("app.py", 50, "run"), + MockFrameInfo("/lib/_sync_coordinator.py", 100, "main"), + ], + status=THREAD_STATUS_HAS_GIL, + ) + ], + ) + ] + collector.collect(frames) + + # Check that no stack contains _sync_coordinator + for (call_tree, _), _ in collector.stack_counter.items(): + for filename, _, _ in call_tree: + self.assertNotIn("_sync_coordinator", filename) + + def test_jsonl_collector_filters_internal_frames(self): + """Test that JsonlCollector filters out internal frames.""" + jsonl_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, jsonl_out) + + frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("app.py", 50, "run"), + MockFrameInfo("/lib/_sync_coordinator.py", 100, "main"), + MockFrameInfo("", 87, "_run_code"), + ], + status=THREAD_STATUS_HAS_GIL, + ) + ], + ) + ] + + collector = JsonlCollector(sample_interval_usec=1000) + collector.collect(frames) + collector.export(jsonl_out.name) + + with open(jsonl_out.name, "r", encoding="utf-8") as f: + records = [json.loads(line) for line in f] + + _, str_defs, frame_defs, _, _ = jsonl_tables(records) + + paths = {str_defs[item["path_str_id"]] for item in frame_defs} + + self.assertIn("app.py", paths) + self.assertIn("", paths) + + for path in paths: + self.assertNotIn("_sync_coordinator", path) diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_dump.py b/Lib/test/test_profiling/test_sampling_profiler/test_dump.py new file mode 100644 index 00000000000..7a0f8df38a8 --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_dump.py @@ -0,0 +1,454 @@ +"""Tests for one-shot sampling profiler stack dumps.""" + +from collections import namedtuple +import os +import opcode +import tempfile +import unittest + +import _colorize + +from profiling.sampling.constants import ( + THREAD_STATUS_GIL_REQUESTED, + THREAD_STATUS_HAS_EXCEPTION, + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_MAIN_THREAD, + THREAD_STATUS_ON_CPU, + THREAD_STATUS_UNKNOWN, +) +from profiling.sampling.dump import format_stack_dump + +from .mocks import ( + LocationInfo as StructseqLocationInfo, + MockAwaitedInfo, + MockCoroInfo, + MockFrameInfo, + MockInterpreterInfo, + MockTaskInfo, + MockThreadInfo, +) + +try: + import _remote_debugging # noqa: F401 +except ImportError: + _remote_debugging = None + + +StructseqInterpreterInfo = namedtuple( + "StructseqInterpreterInfo", + ["interpreter_id", "threads"], +) +StructseqThreadInfo = namedtuple( + "StructseqThreadInfo", + ["thread_id", "status", "frame_info"], +) +StructseqFrameInfo = namedtuple( + "StructseqFrameInfo", + ["filename", "location", "funcname", "opcode"], +) + + +class TestStackDumpFormatting(unittest.TestCase): + def test_format_stack_dump_single_thread(self): + frames = [ + MockFrameInfo("leaf.py", 10, "leaf"), + MockFrameInfo("root.py", 1, "root"), + ] + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 123, + frames, + status=( + THREAD_STATUS_MAIN_THREAD + | THREAD_STATUS_HAS_GIL + | THREAD_STATUS_ON_CPU + ), + ) + ], + ) + ] + + output = format_stack_dump(stack_frames, pid=42, colorize=False) + + self.assertIn( + "Stack dump for PID 42, thread 123 " + "(main thread, has GIL, on CPU; most recent call last):", + output, + ) + self.assertNotIn("Interpreter 0", output) + self.assertLess( + output.index('File "root.py", line 1, in root'), + output.index('File "leaf.py", line 10, in leaf'), + ) + self.assertIn(' File "root.py", line 1, in root', output) + self.assertIn(' File "leaf.py", line 10, in leaf', output) + self.assertNotIn("\x1b[", output) + + def test_format_stack_dump_with_structseq_tuples(self): + stack_frames = [ + StructseqInterpreterInfo( + 0, + [ + StructseqThreadInfo( + 123, + THREAD_STATUS_HAS_GIL, + [ + StructseqFrameInfo( + "file.py", + StructseqLocationInfo(5, 5, -1, -1), + "func", + None, + ) + ], + ) + ], + ) + ] + + output = format_stack_dump(stack_frames, pid=42, colorize=False) + + self.assertIn( + "Stack dump for PID 42, thread 123 " + "(has GIL; most recent call last):", + output, + ) + self.assertIn(' File "file.py", line 5, in func', output) + + def test_format_stack_dump_shows_interpreter_ids_when_multiple(self): + stack_frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(100, [MockFrameInfo("main.py", 1, "main")])], + ), + MockInterpreterInfo( + 1, + [MockThreadInfo(200, [MockFrameInfo("sub.py", 2, "sub")])], + ), + ] + + output = format_stack_dump(stack_frames, pid=42, colorize=False) + + self.assertIn( + "Stack dump for PID 42, thread 100 " + "(interpreter 0; idle; most recent call last):", + output, + ) + self.assertIn( + "Stack dump, thread 200 " + "(interpreter 1; idle; most recent call last):", + output, + ) + + def test_format_stack_dump_omits_unknown_status(self): + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 123, + [MockFrameInfo("file.py", 5, "func")], + status=THREAD_STATUS_UNKNOWN, + ) + ], + ) + ] + + output = format_stack_dump(stack_frames, pid=42, colorize=False) + + self.assertIn( + "Stack dump for PID 42, thread 123 (most recent call last):", + output, + ) + self.assertNotIn("unknown", output) + + def test_format_stack_dump_omits_unknown_when_other_status_exists(self): + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 123, + [MockFrameInfo("file.py", 5, "func")], + status=THREAD_STATUS_UNKNOWN | THREAD_STATUS_ON_CPU, + ) + ], + ) + ] + + output = format_stack_dump(stack_frames, pid=42, colorize=False) + + self.assertIn( + "Stack dump for PID 42, thread 123 " + "(on CPU; most recent call last):", + output, + ) + self.assertNotIn("unknown", output) + + def test_format_stack_dump_labels_known_idle_status(self): + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 123, + [MockFrameInfo("file.py", 5, "func")], + status=0, + ) + ], + ) + ] + + output = format_stack_dump(stack_frames, pid=42, colorize=False) + + self.assertIn( + "Stack dump for PID 42, thread 123 " + "(idle; most recent call last):", + output, + ) + + def test_format_stack_dump_status_does_not_add_idle_to_waiting_thread(self): + status = ( + THREAD_STATUS_MAIN_THREAD + | THREAD_STATUS_GIL_REQUESTED + | THREAD_STATUS_HAS_EXCEPTION + ) + stack_frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(123, [MockFrameInfo("file.py", 5, "func")], status=status)], + ) + ] + + output = format_stack_dump(stack_frames, pid=42, colorize=False) + + self.assertIn( + "Stack dump for PID 42, thread 123 " + "(main thread, waiting for GIL, has exception; most recent call last):", + output, + ) + self.assertNotIn("idle", output) + + def test_format_stack_dump_formats_opcode_name(self): + load_const = opcode.opmap["LOAD_CONST"] + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 123, + [ + StructseqFrameInfo( + "file.py", + StructseqLocationInfo(5, 5, -1, -1), + "func", + load_const, + ) + ], + ) + ], + ) + ] + + output = format_stack_dump(stack_frames, pid=42, colorize=False) + + self.assertIn("opcode=LOAD_CONST", output) + self.assertNotIn(f"opcode={load_const}", output) + + def test_format_stack_dump_formats_unknown_opcode(self): + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 123, + [ + StructseqFrameInfo( + "file.py", + StructseqLocationInfo(5, 5, -1, -1), + "func", + 999, + ) + ], + ) + ], + ) + ] + + output = format_stack_dump(stack_frames, pid=42, colorize=False) + + self.assertIn("opcode=<999>", output) + + def test_format_stack_dump_prefers_qualname_attribute(self): + class Frame: + filename = "file.py" + location = StructseqLocationInfo(5, 5, -1, -1) + funcname = "inner" + qualname = "Outer.inner" + opcode = None + + stack_frames = [ + MockInterpreterInfo(0, [MockThreadInfo(123, [Frame()])]) + ] + + output = format_stack_dump(stack_frames, pid=42, colorize=False) + + self.assertIn(' File "file.py", line 5, in Outer.inner', output) + self.assertNotIn("in inner", output) + + def test_format_stack_dump_filters_internal_frames(self): + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 123, + [ + MockFrameInfo("user.py", 10, "user"), + MockFrameInfo("_sync_coordinator.py", 20, "internal"), + ], + ) + ], + ) + ] + + output = format_stack_dump(stack_frames, pid=42, colorize=False) + + self.assertIn(' File "user.py", line 10, in user', output) + self.assertNotIn("_sync_coordinator.py", output) + + @unittest.skipIf(_remote_debugging is None, "requires _remote_debugging") + def test_format_stack_dump_async_task(self): + task = MockTaskInfo( + task_id=1, + task_name="Task-1", + coroutine_stack=[ + MockCoroInfo( + "Task-1", + [MockFrameInfo("task.py", 5, "waiter")], + ) + ], + ) + stack_frames = [MockAwaitedInfo(thread_id=123, awaited_by=[task])] + + output = format_stack_dump(stack_frames, pid=42, colorize=False) + + self.assertIn( + "Stack dump for PID 42, thread 123 " + "(task 1; most recent call last):", + output, + ) + self.assertIn(' File "task.py", line 5, in waiter', output) + + def test_format_stack_dump_strips_source_like_traceback(self): + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 123, + [ + StructseqFrameInfo( + __file__, + StructseqLocationInfo(1, 1, -1, -1), + "", + None, + ) + ], + ) + ], + ) + ] + + output = format_stack_dump(stack_frames, pid=42, colorize=False) + + self.assertIn(' File "', output) + self.assertIn('", line 1, in ', output) + self.assertIn('"""Tests for one-shot sampling profiler stack dumps."""', output) + self.assertNotIn("^^^^^^^^", output) + + def test_format_stack_dump_highlights_source_range(self): + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 123, + [ + StructseqFrameInfo( + __file__, + StructseqLocationInfo(1, 1, 0, 3), + "", + None, + ) + ], + ) + ], + ) + ] + theme = _colorize.get_theme(force_color=True).profiler_dump + + output = format_stack_dump(stack_frames, pid=42, colorize=True) + + self.assertIn( + f"{theme.source_highlight}\"\"\"{theme.reset}", + output, + ) + self.assertNotIn("^^^^^^^^", _colorize.decolor(output)) + + def test_format_stack_dump_highlights_source_range_after_trimming(self): + with tempfile.TemporaryDirectory() as tmp_dir: + filename = os.path.join(tmp_dir, "target.py") + with open(filename, "w", encoding="utf-8") as file: + file.write(" result = call(arg)\n") + stack_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 123, + [ + StructseqFrameInfo( + filename, + StructseqLocationInfo(1, 1, 13, 17), + "", + None, + ) + ], + ) + ], + ) + ] + theme = _colorize.get_theme(force_color=True).profiler_dump + + output = format_stack_dump(stack_frames, pid=42, colorize=True) + + self.assertIn(f"{theme.source_highlight}call{theme.reset}", output) + self.assertIn("\n result = call(arg)\n", _colorize.decolor(output)) + self.assertNotIn("\n result = call(arg)\n", _colorize.decolor(output)) + + def test_format_stack_dump_empty(self): + output = format_stack_dump([], pid=42, colorize=False) + + self.assertEqual("No Python stacks found for PID 42\n", output) + + def test_format_stack_dump_colorized(self): + stack_frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo(123, [MockFrameInfo("file.py", 5, "func")])], + ) + ] + theme = _colorize.get_theme(force_color=True).profiler_dump + + output = format_stack_dump(stack_frames, pid=42, colorize=True) + + self.assertIn(theme.header, output) + self.assertIn(theme.filename, output) + self.assertIn(theme.reset, output) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_integration.py b/Lib/test/test_profiling/test_sampling_profiler/test_integration.py new file mode 100644 index 00000000000..3487647b766 --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_integration.py @@ -0,0 +1,953 @@ +"""Tests for sampling profiler integration and error handling.""" + +import contextlib +import io +import marshal +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + +try: + import _remote_debugging + import profiling.sampling + import profiling.sampling.sample + from profiling.sampling.pstats_collector import PstatsCollector + from profiling.sampling.stack_collector import CollapsedStackCollector + from profiling.sampling.sample import SampleProfiler, _is_process_running + from profiling.sampling.cli import main +except ImportError: + raise unittest.SkipTest( + "Test only runs when _remote_debugging is available" + ) + +from test.support import ( + requires_remote_subprocess_debugging, + SHORT_TIMEOUT, +) + +from .helpers import ( + test_subprocess, + close_and_unlink, +) +from .mocks import MockFrameInfo, MockThreadInfo, MockInterpreterInfo + +# Duration for profiling tests - long enough for process to complete naturally +PROFILING_TIMEOUT = str(int(SHORT_TIMEOUT)) + +# Duration for profiling in tests - short enough to complete quickly +PROFILING_DURATION_SEC = 2 + + +@requires_remote_subprocess_debugging() +class TestRecursiveFunctionProfiling(unittest.TestCase): + """Test profiling of recursive functions and complex call patterns.""" + + def test_recursive_function_call_counting(self): + """Test that recursive function calls are counted correctly.""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Simulate a recursive call pattern: fibonacci(5) calling itself + recursive_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ # First sample: deep in recursion + MockFrameInfo("fib.py", 10, "fibonacci"), + MockFrameInfo( + "fib.py", 10, "fibonacci" + ), # recursive call + MockFrameInfo( + "fib.py", 10, "fibonacci" + ), # deeper recursion + MockFrameInfo( + "fib.py", 10, "fibonacci" + ), # even deeper + MockFrameInfo("main.py", 5, "main"), # main caller + ], + ) + ], + ), + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ # Second sample: different recursion depth + MockFrameInfo("fib.py", 10, "fibonacci"), + MockFrameInfo( + "fib.py", 10, "fibonacci" + ), # recursive call + MockFrameInfo("main.py", 5, "main"), # main caller + ], + ) + ], + ), + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ # Third sample: back to deeper recursion + MockFrameInfo("fib.py", 10, "fibonacci"), + MockFrameInfo("fib.py", 10, "fibonacci"), + MockFrameInfo("fib.py", 10, "fibonacci"), + MockFrameInfo("main.py", 5, "main"), + ], + ) + ], + ), + ] + + for frames in recursive_frames: + collector.collect([frames]) + + collector.create_stats() + + # Check that recursive calls are counted properly + fib_key = ("fib.py", 10, "fibonacci") + main_key = ("main.py", 5, "main") + + self.assertIn(fib_key, collector.stats) + self.assertIn(main_key, collector.stats) + + # Fibonacci: counted once per sample, not per occurrence + fib_stats = collector.stats[fib_key] + direct_calls, cumulative_calls, tt, ct, callers = fib_stats + + # Should count 3 (present in 3 samples), not 9 (total occurrences) + self.assertEqual(cumulative_calls, 3) + self.assertEqual(direct_calls, 3) # Top of stack in all samples + self.assertGreater(tt, 0) + self.assertGreater(ct, 0) + + # Main should also have 3 cumulative calls (in all 3 samples) + main_stats = collector.stats[main_key] + main_direct_calls, main_cumulative_calls = main_stats[0], main_stats[1] + self.assertEqual(main_direct_calls, 0) # Never directly executing + self.assertEqual(main_cumulative_calls, 3) # Appears in all 3 samples + + def test_nested_function_hierarchy(self): + """Test profiling of deeply nested function calls.""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Simulate a deep call hierarchy + deep_call_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("level1.py", 10, "level1_func"), + MockFrameInfo("level2.py", 20, "level2_func"), + MockFrameInfo("level3.py", 30, "level3_func"), + MockFrameInfo("level4.py", 40, "level4_func"), + MockFrameInfo("level5.py", 50, "level5_func"), + MockFrameInfo("main.py", 5, "main"), + ], + ) + ], + ), + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ # Same hierarchy sampled again + MockFrameInfo("level1.py", 10, "level1_func"), + MockFrameInfo("level2.py", 20, "level2_func"), + MockFrameInfo("level3.py", 30, "level3_func"), + MockFrameInfo("level4.py", 40, "level4_func"), + MockFrameInfo("level5.py", 50, "level5_func"), + MockFrameInfo("main.py", 5, "main"), + ], + ) + ], + ), + ] + + for frames in deep_call_frames: + collector.collect([frames]) + + collector.create_stats() + + # All levels should be recorded + for level in range(1, 6): + key = (f"level{level}.py", level * 10, f"level{level}_func") + self.assertIn(key, collector.stats) + + stats = collector.stats[key] + direct_calls, cumulative_calls, tt, ct, callers = stats + + # Each level should appear in stack twice (2 samples) + self.assertEqual(cumulative_calls, 2) + + # Only level1 (deepest) should have direct calls + if level == 1: + self.assertEqual(direct_calls, 2) + else: + self.assertEqual(direct_calls, 0) + + # Deeper levels should have lower cumulative time than higher levels + # (since they don't include time from functions they call) + if level == 1: # Deepest level with most time + self.assertGreater(ct, 0) + + def test_alternating_call_patterns(self): + """Test profiling with alternating call patterns.""" + collector = PstatsCollector(sample_interval_usec=1000) + + # Simulate alternating execution paths + pattern_frames = [ + # Pattern A: path through func_a + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("module.py", 10, "func_a"), + MockFrameInfo("module.py", 30, "shared_func"), + MockFrameInfo("main.py", 5, "main"), + ], + ) + ], + ), + # Pattern B: path through func_b + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("module.py", 20, "func_b"), + MockFrameInfo("module.py", 30, "shared_func"), + MockFrameInfo("main.py", 5, "main"), + ], + ) + ], + ), + # Pattern A again + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("module.py", 10, "func_a"), + MockFrameInfo("module.py", 30, "shared_func"), + MockFrameInfo("main.py", 5, "main"), + ], + ) + ], + ), + # Pattern B again + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("module.py", 20, "func_b"), + MockFrameInfo("module.py", 30, "shared_func"), + MockFrameInfo("main.py", 5, "main"), + ], + ) + ], + ), + ] + + for frames in pattern_frames: + collector.collect([frames]) + + collector.create_stats() + + # Check that both paths are recorded equally + func_a_key = ("module.py", 10, "func_a") + func_b_key = ("module.py", 20, "func_b") + shared_key = ("module.py", 30, "shared_func") + main_key = ("main.py", 5, "main") + + # func_a and func_b should each be directly executing twice + self.assertEqual(collector.stats[func_a_key][0], 2) # direct_calls + self.assertEqual(collector.stats[func_a_key][1], 2) # cumulative_calls + self.assertEqual(collector.stats[func_b_key][0], 2) # direct_calls + self.assertEqual(collector.stats[func_b_key][1], 2) # cumulative_calls + + # shared_func should appear in all samples (4 times) but never directly executing + self.assertEqual(collector.stats[shared_key][0], 0) # direct_calls + self.assertEqual(collector.stats[shared_key][1], 4) # cumulative_calls + + # main should appear in all samples but never directly executing + self.assertEqual(collector.stats[main_key][0], 0) # direct_calls + self.assertEqual(collector.stats[main_key][1], 4) # cumulative_calls + + def test_collapsed_stack_with_recursion(self): + """Test collapsed stack collector with recursive patterns.""" + collector = CollapsedStackCollector(1000) + + # Recursive call pattern + recursive_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("factorial.py", 10, "factorial"), + MockFrameInfo("factorial.py", 10, "factorial"), # recursive + MockFrameInfo("factorial.py", 10, "factorial"), # deeper + MockFrameInfo("main.py", 5, "main"), + ], + ) + ], + ), + MockInterpreterInfo( + 0, + [ + MockThreadInfo( + 1, + [ + MockFrameInfo("factorial.py", 10, "factorial"), + MockFrameInfo("factorial.py", 10, "factorial"), # different depth + MockFrameInfo("main.py", 5, "main"), + ], + ) + ], + ), + ] + + for frames in recursive_frames: + collector.collect([frames]) + + # Should capture both call paths + self.assertEqual(len(collector.stack_counter), 2) + + # First path should be longer (deeper recursion) than the second + path_tuples = list(collector.stack_counter.keys()) + paths = [p[0] for p in path_tuples] # Extract just the call paths + lengths = [len(p) for p in paths] + self.assertNotEqual(lengths[0], lengths[1]) + + # Both should contain factorial calls + self.assertTrue( + any(any(f[2] == "factorial" for f in p) for p in paths) + ) + + # Verify total occurrences via aggregation + factorial_key = ("factorial.py", 10, "factorial") + main_key = ("main.py", 5, "main") + + def total_occurrences(func): + total = 0 + for (path, thread_id), count in collector.stack_counter.items(): + total += sum(1 for f in path if f == func) * count + return total + + self.assertEqual(total_occurrences(factorial_key), 5) + self.assertEqual(total_occurrences(main_key), 2) + + +# Shared workload functions for test scripts +_WORKLOAD_FUNCTIONS = ''' +def slow_fibonacci(n): + if n <= 1: + return n + return slow_fibonacci(n-1) + slow_fibonacci(n-2) + +def cpu_intensive_work(): + result = 0 + for i in range(10000): + result += i * i + if i % 100 == 0: + result = result % 1000000 + return result + +def do_work(): + iteration = 0 + while True: + if iteration % 2 == 0: + slow_fibonacci(15) + else: + cpu_intensive_work() + iteration += 1 +''' + + +@requires_remote_subprocess_debugging() +class TestSampleProfilerIntegration(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Test script for use with test_subprocess() - signals when work starts + cls.test_script = _WORKLOAD_FUNCTIONS + ''' +_test_sock.sendall(b"working") +do_work() +''' + # CLI test script - runs for fixed duration (no socket sync) + cls.cli_test_script = ''' +import time +''' + _WORKLOAD_FUNCTIONS.replace( + 'while True:', 'end_time = time.time() + 30\n while time.time() < end_time:' +) + ''' +do_work() +''' + + def test_sampling_basic_functionality(self): + with ( + test_subprocess(self.test_script, wait_for_working=True) as subproc, + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = PstatsCollector(sample_interval_usec=1000, skip_idle=False) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=PROFILING_DURATION_SEC, + ) + collector.print_stats(show_summary=False) + + output = captured_output.getvalue() + + # Basic checks on output + self.assertIn("Captured", output) + self.assertIn("samples", output) + self.assertIn("Profile Stats", output) + + # Should see some of our test functions + self.assertIn("slow_fibonacci", output) + + def test_sampling_with_pstats_export(self): + pstats_out = tempfile.NamedTemporaryFile( + suffix=".pstats", delete=False + ) + self.addCleanup(close_and_unlink, pstats_out) + + with test_subprocess(self.test_script, wait_for_working=True) as subproc: + # Suppress profiler output when testing file export + with ( + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = PstatsCollector(sample_interval_usec=10000, skip_idle=False) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=PROFILING_DURATION_SEC, + ) + collector.export(pstats_out.name) + + # Verify file was created and contains valid data + self.assertTrue(os.path.exists(pstats_out.name)) + self.assertGreater(os.path.getsize(pstats_out.name), 0) + + # Try to load the stats file + with open(pstats_out.name, "rb") as f: + stats_data = marshal.load(f) + + # Should be a dictionary with the sampled marker + self.assertIsInstance(stats_data, dict) + self.assertIn(("__sampled__",), stats_data) + self.assertTrue(stats_data[("__sampled__",)]) + + # Should have some function data + function_entries = [ + k for k in stats_data.keys() if k != ("__sampled__",) + ] + self.assertGreater(len(function_entries), 0) + + def test_sampling_with_collapsed_export(self): + collapsed_file = tempfile.NamedTemporaryFile( + suffix=".txt", delete=False + ) + self.addCleanup(close_and_unlink, collapsed_file) + + with ( + test_subprocess(self.test_script, wait_for_working=True) as subproc, + ): + # Suppress profiler output when testing file export + with ( + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = CollapsedStackCollector(1000, skip_idle=False) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=PROFILING_DURATION_SEC, + ) + collector.export(collapsed_file.name) + + # Verify file was created and contains valid data + self.assertTrue(os.path.exists(collapsed_file.name)) + self.assertGreater(os.path.getsize(collapsed_file.name), 0) + + # Check file format + with open(collapsed_file.name, "r") as f: + content = f.read() + + lines = content.strip().split("\n") + self.assertGreater(len(lines), 0) + + # Each line should have format: stack_trace count + for line in lines: + parts = line.rsplit(" ", 1) + self.assertEqual(len(parts), 2) + + stack_trace, count_str = parts + self.assertGreater(len(stack_trace), 0) + self.assertTrue(count_str.isdigit()) + self.assertGreater(int(count_str), 0) + + # Stack trace should contain semicolon-separated entries + if ";" in stack_trace: + stack_parts = stack_trace.split(";") + for part in stack_parts: + # Each part should be file:function:line + self.assertIn(":", part) + + def test_sampling_all_threads(self): + with ( + test_subprocess(self.test_script, wait_for_working=True) as subproc, + # Suppress profiler output + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = PstatsCollector(sample_interval_usec=10000, skip_idle=False) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=PROFILING_DURATION_SEC, + all_threads=True, + ) + collector.print_stats(show_summary=False) + + # Just verify that sampling completed without error + # We're not testing output format here + + def test_sample_target_script(self): + script_file = tempfile.NamedTemporaryFile(delete=False) + script_file.write(self.cli_test_script.encode("utf-8")) + script_file.flush() + self.addCleanup(close_and_unlink, script_file) + + # Sample for PROFILING_DURATION_SEC seconds + test_args = [ + "profiling.sampling.sample", "run", + "-d", str(PROFILING_DURATION_SEC), + script_file.name + ] + + with ( + mock.patch("sys.argv", test_args), + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + main() + + output = captured_output.getvalue() + + # Basic checks on output + self.assertIn("Captured", output) + self.assertIn("samples", output) + self.assertIn("Profile Stats", output) + + # Should see some of our test functions + self.assertIn("slow_fibonacci", output) + + def test_sample_target_module(self): + tempdir = tempfile.TemporaryDirectory(delete=False) + self.addCleanup(lambda x: shutil.rmtree(x), tempdir.name) + + module_path = os.path.join(tempdir.name, "test_module.py") + + with open(module_path, "w") as f: + f.write(self.cli_test_script) + + test_args = [ + "profiling.sampling.cli", + "run", + "-d", + str(PROFILING_DURATION_SEC), + "-m", + "test_module", + ] + + with ( + mock.patch("sys.argv", test_args), + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + # Change to temp directory so subprocess can find the module + contextlib.chdir(tempdir.name), + ): + main() + + output = captured_output.getvalue() + + # Basic checks on output + self.assertIn("Captured", output) + self.assertIn("samples", output) + self.assertIn("Profile Stats", output) + + # Should see some of our test functions + self.assertIn("slow_fibonacci", output) + + +@requires_remote_subprocess_debugging() +class TestSampleProfilerErrorHandling(unittest.TestCase): + def test_invalid_pid(self): + with self.assertRaises((SystemExit, PermissionError)): + collector = PstatsCollector(sample_interval_usec=100, skip_idle=False) + profiling.sampling.sample.sample(-1, collector, duration_sec=1) + + def test_process_dies_during_sampling(self): + # Use wait_for_working=False since this simple script doesn't send "working" + with test_subprocess( + "import time; time.sleep(0.5); exit()", + wait_for_working=False + ) as subproc: + with ( + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = PstatsCollector(sample_interval_usec=50000, skip_idle=False) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=2, # Longer than process lifetime + ) + + output = captured_output.getvalue() + + self.assertIn("Error rate", output) + + def test_is_process_running(self): + # Use wait_for_working=False since this simple script doesn't send "working" + with test_subprocess( + "import time; time.sleep(1000)", + wait_for_working=False + ) as subproc: + profiler = SampleProfiler( + pid=subproc.process.pid, + sample_interval_usec=1000, + all_threads=False, + ) + self.assertTrue(_is_process_running(profiler.pid)) + self.assertIsNotNone(profiler.unwinder.get_stack_trace()) + subproc.process.kill() + subproc.process.wait() + self.assertRaises( + ProcessLookupError, profiler.unwinder.get_stack_trace + ) + + # Exit the context manager to ensure the process is terminated + self.assertFalse(_is_process_running(profiler.pid)) + self.assertRaises( + ProcessLookupError, profiler.unwinder.get_stack_trace + ) + + @unittest.skipUnless(sys.platform == "linux", "Only valid on Linux") + def test_esrch_signal_handling(self): + # Use wait_for_working=False since this simple script doesn't send "working" + with test_subprocess( + "import time; time.sleep(1000)", + wait_for_working=False + ) as subproc: + unwinder = _remote_debugging.RemoteUnwinder( + subproc.process.pid + ) + initial_trace = unwinder.get_stack_trace() + self.assertIsNotNone(initial_trace) + + subproc.process.kill() + + # Wait for the process to die and try to get another trace + subproc.process.wait() + + with self.assertRaises(ProcessLookupError): + unwinder.get_stack_trace() + + def test_script_error_treatment(self): + script_file = tempfile.NamedTemporaryFile( + "w", delete=False, suffix=".py" + ) + script_file.write("open('nonexistent_file.txt')\n") + script_file.close() + self.addCleanup(os.unlink, script_file.name) + + result = subprocess.run( + [ + sys.executable, + "-m", + "profiling.sampling.cli", + "run", + script_file.name, + ], + capture_output=True, + text=True, + ) + output = result.stdout + result.stderr + + self.assertNotIn("Script file not found", output) + self.assertIn( + "No such file or directory: 'nonexistent_file.txt'", output + ) + + def test_live_incompatible_with_pstats_options(self): + """Test that --live is incompatible with individual pstats options.""" + test_cases = [ + (["--sort", "tottime"], "--sort"), + (["--limit", "30"], "--limit"), + (["--no-summary"], "--no-summary"), + ] + + for args, expected_flag in test_cases: + with self.subTest(args=args): + test_args = ["profiling.sampling.cli", "run", "--live"] + args + ["test.py"] + with mock.patch("sys.argv", test_args): + with self.assertRaises(SystemExit) as cm: + main() + self.assertNotEqual(cm.exception.code, 0) + + def test_live_incompatible_with_multiple_pstats_options(self): + """Test that --live is incompatible with multiple pstats options.""" + test_args = [ + "profiling.sampling.cli", "run", "--live", + "--sort", "cumtime", "--limit", "25", "--no-summary", "test.py" + ] + + with mock.patch("sys.argv", test_args): + with self.assertRaises(SystemExit) as cm: + main() + self.assertNotEqual(cm.exception.code, 0) + + def test_live_incompatible_with_pstats_default_values(self): + """Test that --live blocks pstats options even with default values.""" + # Test with --sort=nsamples (the default value) + test_args = ["profiling.sampling.cli", "run", "--live", "--sort=nsamples", "test.py"] + + with mock.patch("sys.argv", test_args): + with self.assertRaises(SystemExit) as cm: + main() + self.assertNotEqual(cm.exception.code, 0) + + # Test with --limit=15 (the default value) + test_args = ["profiling.sampling.cli", "run", "--live", "--limit=15", "test.py"] + + with mock.patch("sys.argv", test_args): + with self.assertRaises(SystemExit) as cm: + main() + self.assertNotEqual(cm.exception.code, 0) + + +@requires_remote_subprocess_debugging() +class TestAsyncAwareProfilingIntegration(unittest.TestCase): + """Integration tests for async-aware profiling mode.""" + + @classmethod + def setUpClass(cls): + # Async test script that runs indefinitely until killed. + # Sends "working" signal AFTER tasks are created and scheduled. + cls.async_script = ''' +import asyncio + +async def sleeping_leaf(): + while True: + await asyncio.sleep(0.02) + +async def cpu_leaf(): + total = 0 + while True: + for i in range(10000): + total += i * i + await asyncio.sleep(0) + +async def supervisor(): + tasks = [ + asyncio.create_task(sleeping_leaf(), name="Sleeper-0"), + asyncio.create_task(sleeping_leaf(), name="Sleeper-1"), + asyncio.create_task(sleeping_leaf(), name="Sleeper-2"), + asyncio.create_task(cpu_leaf(), name="Worker"), + ] + await asyncio.sleep(0) # Let tasks get scheduled + _test_sock.sendall(b"working") + await asyncio.gather(*tasks) + +asyncio.run(supervisor()) +''' + + def _collect_async_samples(self, async_aware_mode): + """Helper to collect samples and count function occurrences. + + Returns a dict mapping function names to their sample counts. + """ + with test_subprocess(self.async_script, wait_for_working=True) as subproc: + collector = CollapsedStackCollector(1000, skip_idle=False) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=PROFILING_DURATION_SEC, + async_aware=async_aware_mode, + ) + + # Count samples per function from collapsed stacks + # stack_counter keys are (call_tree, thread_id) where call_tree + # is a tuple of (file, line, func) tuples + func_samples = {} + total = 0 + for (call_tree, _thread_id), count in collector.stack_counter.items(): + total += count + for _file, _line, func in call_tree: + func_samples[func] = func_samples.get(func, 0) + count + + func_samples["_total"] = total + return func_samples + + def test_async_aware_all_sees_sleeping_and_running_tasks(self): + """Test that async_aware='all' captures both sleeping and CPU-running tasks. + + Task tree structure: + main + └── supervisor + ├── Sleeper-0 (sleeping_leaf) + ├── Sleeper-1 (sleeping_leaf) + ├── Sleeper-2 (sleeping_leaf) + └── Worker (cpu_leaf) + + async_aware='all' should see ALL 4 leaf tasks in the output. + """ + samples = self._collect_async_samples("all") + + self.assertGreater(samples["_total"], 0, "Should have collected samples") + self.assertIn("sleeping_leaf", samples) + self.assertIn("cpu_leaf", samples) + self.assertIn("supervisor", samples) + + def test_async_aware_running_sees_only_cpu_task(self): + """Test that async_aware='running' only captures the actively running task. + + Task tree structure: + main + └── supervisor + ├── Sleeper-0 (sleeping_leaf) - NOT visible in 'running' + ├── Sleeper-1 (sleeping_leaf) - NOT visible in 'running' + ├── Sleeper-2 (sleeping_leaf) - NOT visible in 'running' + └── Worker (cpu_leaf) - VISIBLE in 'running' + + async_aware='running' should only see the Worker task doing CPU work. + """ + samples = self._collect_async_samples("running") + + total = samples["_total"] + cpu_leaf_samples = samples.get("cpu_leaf", 0) + + self.assertGreater(total, 0, "Should have collected some samples") + self.assertGreater(cpu_leaf_samples, 0, "cpu_leaf should appear in samples") + + # cpu_leaf should have at least 90% of samples (typically 99%+) + # sleeping_leaf may occasionally appear with very few samples (< 1%) + # when tasks briefly wake up to check sleep timers + cpu_percentage = (cpu_leaf_samples / total) * 100 + self.assertGreater(cpu_percentage, 90.0, + f"cpu_leaf should dominate samples in 'running' mode, " + f"got {cpu_percentage:.1f}% ({cpu_leaf_samples}/{total})") + + +def _generate_deep_generators_script(chain_depth=20, recurse_depth=150): + """Generate a script with deep nested generators for stress testing.""" + lines = [ + 'import sys', + 'sys.setrecursionlimit(5000)', + '', + ] + # Generate chain of yield-from functions + for i in range(chain_depth - 1): + lines.extend([ + f'def deep_yield_chain_{i}(n):', + f' yield ("L{i}", n)', + f' yield from deep_yield_chain_{i + 1}(n)', + '', + ]) + # Last chain function calls recursive_diver + lines.extend([ + f'def deep_yield_chain_{chain_depth - 1}(n):', + f' yield ("L{chain_depth - 1}", n)', + f' yield from recursive_diver(n, {chain_depth})', + '', + 'def recursive_diver(n, depth):', + ' yield (f"DIVE_{depth}", n)', + f' if depth < {recurse_depth}:', + ' yield from recursive_diver(n, depth + 1)', + ' else:', + ' for i in range(5):', + ' yield (f"BOTTOM_{depth}", i)', + '', + 'def oscillating_generator(iterations=1000):', + ' for i in range(iterations):', + ' yield ("OSCILLATE", i)', + ' yield from deep_yield_chain_0(i)', + '', + 'def run_forever():', + ' while True:', + ' for _ in oscillating_generator(10):', + ' pass', + '', + '_test_sock.sendall(b"working")', + 'run_forever()', + ]) + return '\n'.join(lines) + + +@requires_remote_subprocess_debugging() +class TestDeepGeneratorFrameCache(unittest.TestCase): + """Test frame cache consistency with deep oscillating generator stacks.""" + + def test_all_stacks_share_same_base_frame(self): + """Verify all sampled stacks reach the entry point function. + + When profiling deep generators that oscillate up and down the call + stack, every sample should include the entry point function + (run_forever) in its call chain. If the frame cache stores + incomplete stacks, some samples will be missing this base function, + causing broken flamegraphs. + """ + script = _generate_deep_generators_script() + with test_subprocess(script, wait_for_working=True) as subproc: + collector = CollapsedStackCollector(sample_interval_usec=1, skip_idle=False) + + with ( + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=2, + ) + + samples_with_entry_point = 0 + samples_without_entry_point = 0 + total_samples = 0 + + for (call_tree, _thread_id), count in collector.stack_counter.items(): + total_samples += count + if call_tree: + has_entry_point = call_tree and call_tree[0][2] == "" + if has_entry_point: + samples_with_entry_point += count + else: + samples_without_entry_point += count + + self.assertGreater(total_samples, 100, + f"Expected at least 100 samples, got {total_samples}") + + self.assertEqual(samples_without_entry_point, 0, + f"Found {samples_without_entry_point}/{total_samples} samples " + f"missing the entry point function 'run_forever'. This indicates " + f"incomplete stacks are being returned, likely due to frame cache " + f"storing partial stack traces.") diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_live_collector_core.py b/Lib/test/test_profiling/test_sampling_profiler/test_live_collector_core.py new file mode 100644 index 00000000000..f67a900822c --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_live_collector_core.py @@ -0,0 +1,698 @@ +"""Core functionality tests for LiveStatsCollector. + +Tests for path simplification, frame processing, collect method, +statistics building, sorting, and formatting. +""" + +import os +import unittest +from test.support import requires +from test.support.import_helper import import_module + +# Only run these tests if curses is available +requires("curses") +curses = import_module("curses") + +from profiling.sampling.live_collector import LiveStatsCollector, MockDisplay +from profiling.sampling.constants import ( + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_ON_CPU, +) +from ._live_collector_helpers import ( + MockFrameInfo, + MockThreadInfo, + MockInterpreterInfo, +) + + +class TestLiveStatsCollectorPathSimplification(unittest.TestCase): + """Tests for path simplification functionality.""" + + def test_simplify_stdlib_path(self): + """Test simplification of standard library paths.""" + collector = LiveStatsCollector(1000) + # Get actual os module path + os_file = os.__file__ + if os_file: + stdlib_dir = os.path.dirname(os.path.abspath(os_file)) + test_path = os.path.join(stdlib_dir, "json", "decoder.py") + simplified = collector.simplify_path(test_path) + # Should remove the stdlib prefix + self.assertNotIn(stdlib_dir, simplified) + self.assertIn("json", simplified) + + def test_simplify_unknown_path(self): + """Test that unknown paths are returned unchanged.""" + collector = LiveStatsCollector(1000) + test_path = "/some/unknown/path/file.py" + simplified = collector.simplify_path(test_path) + self.assertEqual(simplified, test_path) + + +class TestLiveStatsCollectorFrameProcessing(unittest.TestCase): + """Tests for frame processing functionality.""" + + def test_process_single_frame(self): + """Test processing a single frame.""" + collector = LiveStatsCollector(1000) + frames = [MockFrameInfo("test.py", 10, "test_func")] + collector.process_frames(frames) + + location = ("test.py", 10, "test_func") + self.assertEqual(collector.result[location]["direct_calls"], 1) + self.assertEqual(collector.result[location]["cumulative_calls"], 1) + + def test_process_multiple_frames(self): + """Test processing a stack of multiple frames.""" + collector = LiveStatsCollector(1000) + frames = [ + MockFrameInfo("test.py", 10, "inner_func"), + MockFrameInfo("test.py", 20, "middle_func"), + MockFrameInfo("test.py", 30, "outer_func"), + ] + collector.process_frames(frames) + + # Top frame (inner_func) should have both direct and cumulative + inner_loc = ("test.py", 10, "inner_func") + self.assertEqual(collector.result[inner_loc]["direct_calls"], 1) + self.assertEqual(collector.result[inner_loc]["cumulative_calls"], 1) + + # Other frames should only have cumulative + middle_loc = ("test.py", 20, "middle_func") + self.assertEqual(collector.result[middle_loc]["direct_calls"], 0) + self.assertEqual(collector.result[middle_loc]["cumulative_calls"], 1) + + outer_loc = ("test.py", 30, "outer_func") + self.assertEqual(collector.result[outer_loc]["direct_calls"], 0) + self.assertEqual(collector.result[outer_loc]["cumulative_calls"], 1) + + def test_process_empty_frames(self): + """Test processing empty frames list.""" + collector = LiveStatsCollector(1000) + collector.process_frames([]) + # Should not raise an error and result should remain empty + self.assertEqual(len(collector.result), 0) + + def test_process_frames_accumulation(self): + """Test that multiple calls accumulate correctly.""" + collector = LiveStatsCollector(1000) + frames = [MockFrameInfo("test.py", 10, "test_func")] + + collector.process_frames(frames) + collector.process_frames(frames) + collector.process_frames(frames) + + location = ("test.py", 10, "test_func") + self.assertEqual(collector.result[location]["direct_calls"], 3) + self.assertEqual(collector.result[location]["cumulative_calls"], 3) + + def test_process_frames_with_thread_id(self): + """Test processing frames with per-thread tracking.""" + collector = LiveStatsCollector(1000) + frames = [MockFrameInfo("test.py", 10, "test_func")] + + # Process frames with thread_id + collector.process_frames(frames, thread_id=123) + + # Check aggregated result + location = ("test.py", 10, "test_func") + self.assertEqual(collector.result[location]["direct_calls"], 1) + self.assertEqual(collector.result[location]["cumulative_calls"], 1) + + # Check per-thread result + self.assertIn(123, collector.per_thread_data) + self.assertEqual( + collector.per_thread_data[123].result[location]["direct_calls"], 1 + ) + self.assertEqual( + collector.per_thread_data[123].result[location]["cumulative_calls"], 1 + ) + + def test_process_frames_multiple_threads(self): + """Test processing frames from multiple threads.""" + collector = LiveStatsCollector(1000) + frames1 = [MockFrameInfo("test.py", 10, "test_func")] + frames2 = [MockFrameInfo("test.py", 20, "other_func")] + + # Process frames from different threads + collector.process_frames(frames1, thread_id=123) + collector.process_frames(frames2, thread_id=456) + + # Check that both threads have their own data + self.assertIn(123, collector.per_thread_data) + self.assertIn(456, collector.per_thread_data) + + loc1 = ("test.py", 10, "test_func") + loc2 = ("test.py", 20, "other_func") + + # Thread 123 should only have func1 + self.assertEqual( + collector.per_thread_data[123].result[loc1]["direct_calls"], 1 + ) + self.assertNotIn(loc2, collector.per_thread_data[123].result) + + # Thread 456 should only have func2 + self.assertEqual( + collector.per_thread_data[456].result[loc2]["direct_calls"], 1 + ) + self.assertNotIn(loc1, collector.per_thread_data[456].result) + + def test_process_recursive_frames_counted_once(self): + """Test that recursive functions are counted once per sample.""" + collector = LiveStatsCollector(1000) + # Simulate recursive function appearing 5 times in stack + frames = [ + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + ] + collector.process_frames(frames) + + location = ("test.py", 10, "recursive_func") + # Should count as 1 cumulative (present in 1 sample), not 5 + self.assertEqual(collector.result[location]["cumulative_calls"], 1) + self.assertEqual(collector.result[location]["direct_calls"], 1) + + def test_process_recursive_frames_multiple_samples(self): + """Test cumulative counting across multiple samples with recursion.""" + collector = LiveStatsCollector(1000) + + # Sample 1: depth 3 + frames1 = [ + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + ] + # Sample 2: depth 2 + frames2 = [ + MockFrameInfo("test.py", 10, "recursive_func"), + MockFrameInfo("test.py", 10, "recursive_func"), + ] + + collector.process_frames(frames1) + collector.process_frames(frames2) + + location = ("test.py", 10, "recursive_func") + # Should count as 2 (present in 2 samples), not 5 + self.assertEqual(collector.result[location]["cumulative_calls"], 2) + self.assertEqual(collector.result[location]["direct_calls"], 2) + + def test_process_mixed_recursive_nonrecursive(self): + """Test stack with both recursive and non-recursive functions.""" + collector = LiveStatsCollector(1000) + + # Stack: main -> foo (recursive x3) -> bar + frames = [ + MockFrameInfo("test.py", 50, "bar"), + MockFrameInfo("test.py", 20, "foo"), + MockFrameInfo("test.py", 20, "foo"), + MockFrameInfo("test.py", 20, "foo"), + MockFrameInfo("test.py", 10, "main"), + ] + collector.process_frames(frames) + + # foo: 1 cumulative despite 3 occurrences + self.assertEqual(collector.result[("test.py", 20, "foo")]["cumulative_calls"], 1) + self.assertEqual(collector.result[("test.py", 20, "foo")]["direct_calls"], 0) + + # bar and main: 1 cumulative each + self.assertEqual(collector.result[("test.py", 50, "bar")]["cumulative_calls"], 1) + self.assertEqual(collector.result[("test.py", 10, "main")]["cumulative_calls"], 1) + + +class TestLiveStatsCollectorCollect(unittest.TestCase): + """Tests for the collect method.""" + + def test_collect_initializes_start_time(self): + """Test that collect initializes start_time on first call.""" + collector = LiveStatsCollector(1000) + self.assertIsNone(collector.start_time) + + # Create mock stack frames + thread_info = MockThreadInfo(123, []) + interpreter_info = MockInterpreterInfo(0, [thread_info]) + stack_frames = [interpreter_info] + + collector.collect(stack_frames) + self.assertIsNotNone(collector.start_time) + + def test_collect_increments_sample_count(self): + """Test that collect increments total_samples.""" + collector = LiveStatsCollector(1000) + thread_info = MockThreadInfo(123, []) + interpreter_info = MockInterpreterInfo(0, [thread_info]) + stack_frames = [interpreter_info] + + self.assertEqual(collector.total_samples, 0) + collector.collect(stack_frames) + self.assertEqual(collector.total_samples, 1) + collector.collect(stack_frames) + self.assertEqual(collector.total_samples, 2) + + def test_collect_with_frames(self): + """Test collect with actual frame data.""" + collector = LiveStatsCollector(1000) + frames = [MockFrameInfo("test.py", 10, "test_func")] + thread_info = MockThreadInfo(123, frames) + interpreter_info = MockInterpreterInfo(0, [thread_info]) + stack_frames = [interpreter_info] + + collector.collect(stack_frames) + + location = ("test.py", 10, "test_func") + self.assertEqual(collector.result[location]["direct_calls"], 1) + self.assertEqual(collector.successful_samples, 1) + self.assertEqual(collector.failed_samples, 0) + + def test_collect_with_empty_frames(self): + """Test collect with empty frames counts as successful. + + A sample is considered successful if the profiler could read from the + target process, even if no frames matched the current filter (e.g., + --mode exception when no thread has an active exception). The sample + itself worked; it just didn't produce frame data. + """ + collector = LiveStatsCollector(1000) + thread_info = MockThreadInfo(123, []) + interpreter_info = MockInterpreterInfo(0, [thread_info]) + stack_frames = [interpreter_info] + + collector.collect(stack_frames) + + # Empty frames still count as successful - the sample worked even + # though no frames matched the filter + self.assertEqual(collector.successful_samples, 1) + self.assertEqual(collector.total_samples, 1) + self.assertEqual(collector.failed_samples, 0) + + def test_sample_counts_invariant(self): + """Test that total_samples == successful_samples + failed_samples. + + Empty frame data (e.g., from --mode exception with no active exception) + still counts as successful since the profiler could read process state. + """ + collector = LiveStatsCollector(1000) + + # Mix of samples with and without frame data + frames = [MockFrameInfo("test.py", 10, "func")] + thread_with_frames = MockThreadInfo(123, frames) + thread_empty = MockThreadInfo(456, []) + interp_with_frames = MockInterpreterInfo(0, [thread_with_frames]) + interp_empty = MockInterpreterInfo(0, [thread_empty]) + + # Collect various samples + collector.collect([interp_with_frames]) # Has frames + collector.collect([interp_empty]) # No frames (filtered) + collector.collect([interp_with_frames]) # Has frames + collector.collect([interp_empty]) # No frames (filtered) + collector.collect([interp_empty]) # No frames (filtered) + + # All 5 samples are successful (profiler could read process state) + self.assertEqual(collector.total_samples, 5) + self.assertEqual(collector.successful_samples, 5) + self.assertEqual(collector.failed_samples, 0) + + # Invariant must hold + self.assertEqual( + collector.total_samples, + collector.successful_samples + collector.failed_samples + ) + + def test_collect_skip_idle_threads(self): + """Test that idle threads are skipped when skip_idle=True.""" + collector = LiveStatsCollector(1000, skip_idle=True) + + frames = [MockFrameInfo("test.py", 10, "test_func")] + running_thread = MockThreadInfo( + 123, frames, status=THREAD_STATUS_HAS_GIL | THREAD_STATUS_ON_CPU + ) + idle_thread = MockThreadInfo(124, frames, status=0) # No flags = idle + interpreter_info = MockInterpreterInfo( + 0, [running_thread, idle_thread] + ) + stack_frames = [interpreter_info] + + collector.collect(stack_frames) + + # Only one thread should be processed + location = ("test.py", 10, "test_func") + self.assertEqual(collector.result[location]["direct_calls"], 1) + + def test_collect_multiple_threads(self): + """Test collect with multiple threads.""" + collector = LiveStatsCollector(1000) + + frames1 = [MockFrameInfo("test1.py", 10, "func1")] + frames2 = [MockFrameInfo("test2.py", 20, "func2")] + thread1 = MockThreadInfo(123, frames1) + thread2 = MockThreadInfo(124, frames2) + interpreter_info = MockInterpreterInfo(0, [thread1, thread2]) + stack_frames = [interpreter_info] + + collector.collect(stack_frames) + + loc1 = ("test1.py", 10, "func1") + loc2 = ("test2.py", 20, "func2") + self.assertEqual(collector.result[loc1]["direct_calls"], 1) + self.assertEqual(collector.result[loc2]["direct_calls"], 1) + + # Check thread IDs are tracked + self.assertIn(123, collector.thread_ids) + self.assertIn(124, collector.thread_ids) + + def test_collect_filtered_mode_percentage_calculation(self): + """Test that percentages use successful_samples, not total_samples. + + With the current behavior, all samples are considered successful + (the profiler could read from the process), even when filters result + in no frame data. This means percentages are relative to all sampling + attempts that succeeded in reading process state. + """ + collector = LiveStatsCollector(1000) + + # Simulate 10 samples where only 2 had matching data (e.g., exception mode) + frames_with_data = [MockFrameInfo("test.py", 10, "exception_handler")] + thread_with_data = MockThreadInfo(123, frames_with_data) + interpreter_with_data = MockInterpreterInfo(0, [thread_with_data]) + + # Empty thread simulates filtered-out data at C level + thread_empty = MockThreadInfo(456, []) + interpreter_empty = MockInterpreterInfo(0, [thread_empty]) + + # 2 samples with data + collector.collect([interpreter_with_data]) + collector.collect([interpreter_with_data]) + + # 8 samples without data (filtered out at C level, but sample still succeeded) + for _ in range(8): + collector.collect([interpreter_empty]) + + # All 10 samples are successful - the profiler could read from the process + self.assertEqual(collector.total_samples, 10) + self.assertEqual(collector.successful_samples, 10) + + # Build stats and check percentage + stats_list = collector.build_stats_list() + self.assertEqual(len(stats_list), 1) + + # The function appeared in 2 out of 10 successful samples = 20% + location = ("test.py", 10, "exception_handler") + self.assertEqual(collector.result[location]["direct_calls"], 2) + + def test_percentage_values_use_successful_samples(self): + """Test that percentages are calculated from successful_samples. + + This verifies the fix where percentages use successful_samples (samples with + frame data) instead of total_samples (all sampling attempts). Critical for + filtered modes like --mode exception. + """ + collector = LiveStatsCollector(1000) + + # Simulate scenario: 100 total samples, only 20 had frame data + collector.total_samples = 100 + collector.successful_samples = 20 + + # Function appeared in 10 out of 20 successful samples + collector.result[("test.py", 10, "handler")] = { + "direct_calls": 10, + "cumulative_calls": 15, + "total_rec_calls": 0, + } + + stats_list = collector.build_stats_list() + self.assertEqual(len(stats_list), 1) + + stat = stats_list[0] + # Calculate expected percentages using successful_samples + expected_sample_pct = stat["direct_calls"] / collector.successful_samples * 100 + expected_cumul_pct = stat["cumulative_calls"] / collector.successful_samples * 100 + + # Percentage should be 10/20 * 100 = 50%, NOT 10/100 * 100 = 10% + self.assertAlmostEqual(expected_sample_pct, 50.0) + # Cumulative percentage should be 15/20 * 100 = 75%, NOT 15/100 * 100 = 15% + self.assertAlmostEqual(expected_cumul_pct, 75.0) + + # Verify sorting by percentage works correctly + collector.result[("test.py", 20, "other")] = { + "direct_calls": 5, # 25% of successful samples + "cumulative_calls": 8, + "total_rec_calls": 0, + } + collector.sort_by = "sample_pct" + stats_list = collector.build_stats_list() + # handler (50%) should come before other (25%) + self.assertEqual(stats_list[0]["func"][2], "handler") + self.assertEqual(stats_list[1]["func"][2], "other") + + def test_build_stats_list_zero_successful_samples(self): + """Test build_stats_list handles zero successful_samples without division by zero. + + When all samples are filtered out (e.g., exception mode with no exceptions), + percentage calculations should return 0 without raising ZeroDivisionError. + """ + collector = LiveStatsCollector(1000) + + # Edge case: data exists but no successful samples + collector.result[("test.py", 10, "func")] = { + "direct_calls": 10, + "cumulative_calls": 10, + "total_rec_calls": 0, + } + collector.total_samples = 100 + collector.successful_samples = 0 # All samples filtered out + + # Should not raise ZeroDivisionError + stats_list = collector.build_stats_list() + self.assertEqual(len(stats_list), 1) + + # Verify percentage-based sorting also works with zero successful_samples + collector.sort_by = "sample_pct" + stats_list = collector.build_stats_list() + self.assertEqual(len(stats_list), 1) + + collector.sort_by = "cumul_pct" + stats_list = collector.build_stats_list() + self.assertEqual(len(stats_list), 1) + + +class TestLiveStatsCollectorStatisticsBuilding(unittest.TestCase): + """Tests for statistics building and sorting.""" + + def setUp(self): + """Set up test fixtures.""" + self.collector = LiveStatsCollector(1000) + # Add some test data + self.collector.result[("file1.py", 10, "func1")] = { + "direct_calls": 100, + "cumulative_calls": 150, + "total_rec_calls": 0, + } + self.collector.result[("file2.py", 20, "func2")] = { + "direct_calls": 50, + "cumulative_calls": 200, + "total_rec_calls": 0, + } + self.collector.result[("file3.py", 30, "func3")] = { + "direct_calls": 75, + "cumulative_calls": 75, + "total_rec_calls": 0, + } + self.collector.total_samples = 300 + # successful_samples is used for percentage calculations + self.collector.successful_samples = 300 + + def test_build_stats_list(self): + """Test that stats list is built correctly.""" + stats_list = self.collector.build_stats_list() + self.assertEqual(len(stats_list), 3) + + # Check that all expected keys are present + for stat in stats_list: + self.assertIn("func", stat) + self.assertIn("direct_calls", stat) + self.assertIn("cumulative_calls", stat) + self.assertIn("total_time", stat) + self.assertIn("cumulative_time", stat) + + def test_sort_by_nsamples(self): + """Test sorting by number of samples.""" + self.collector.sort_by = "nsamples" + stats_list = self.collector.build_stats_list() + + # Should be sorted by direct_calls descending + self.assertEqual(stats_list[0]["func"][2], "func1") # 100 samples + self.assertEqual(stats_list[1]["func"][2], "func3") # 75 samples + self.assertEqual(stats_list[2]["func"][2], "func2") # 50 samples + + def test_sort_by_tottime(self): + """Test sorting by total time.""" + self.collector.sort_by = "tottime" + stats_list = self.collector.build_stats_list() + + # Should be sorted by total_time descending + # total_time = direct_calls * sample_interval_sec + self.assertEqual(stats_list[0]["func"][2], "func1") + self.assertEqual(stats_list[1]["func"][2], "func3") + self.assertEqual(stats_list[2]["func"][2], "func2") + + def test_sort_by_cumtime(self): + """Test sorting by cumulative time.""" + self.collector.sort_by = "cumtime" + stats_list = self.collector.build_stats_list() + + # Should be sorted by cumulative_time descending + self.assertEqual(stats_list[0]["func"][2], "func2") # 200 cumulative + self.assertEqual(stats_list[1]["func"][2], "func1") # 150 cumulative + self.assertEqual(stats_list[2]["func"][2], "func3") # 75 cumulative + + def test_sort_by_sample_pct(self): + """Test sorting by sample percentage.""" + self.collector.sort_by = "sample_pct" + stats_list = self.collector.build_stats_list() + + # Should be sorted by percentage of direct_calls + self.assertEqual(stats_list[0]["func"][2], "func1") # 33.3% + self.assertEqual(stats_list[1]["func"][2], "func3") # 25% + self.assertEqual(stats_list[2]["func"][2], "func2") # 16.7% + + def test_sort_by_cumul_pct(self): + """Test sorting by cumulative percentage.""" + self.collector.sort_by = "cumul_pct" + stats_list = self.collector.build_stats_list() + + # Should be sorted by percentage of cumulative_calls + self.assertEqual(stats_list[0]["func"][2], "func2") # 66.7% + self.assertEqual(stats_list[1]["func"][2], "func1") # 50% + self.assertEqual(stats_list[2]["func"][2], "func3") # 25% + + +class TestLiveStatsCollectorSortCycle(unittest.TestCase): + """Tests for sort mode cycling.""" + + def test_cycle_sort_from_nsamples(self): + """Test cycling from nsamples.""" + collector = LiveStatsCollector(1000, sort_by="nsamples") + collector._cycle_sort() + self.assertEqual(collector.sort_by, "sample_pct") + + def test_cycle_sort_from_sample_pct(self): + """Test cycling from sample_pct.""" + collector = LiveStatsCollector(1000, sort_by="sample_pct") + collector._cycle_sort() + self.assertEqual(collector.sort_by, "tottime") + + def test_cycle_sort_from_tottime(self): + """Test cycling from tottime.""" + collector = LiveStatsCollector(1000, sort_by="tottime") + collector._cycle_sort() + self.assertEqual(collector.sort_by, "cumul_pct") + + def test_cycle_sort_from_cumul_pct(self): + """Test cycling from cumul_pct.""" + collector = LiveStatsCollector(1000, sort_by="cumul_pct") + collector._cycle_sort() + self.assertEqual(collector.sort_by, "cumtime") + + def test_cycle_sort_from_cumtime(self): + """Test cycling from cumtime back to nsamples.""" + collector = LiveStatsCollector(1000, sort_by="cumtime") + collector._cycle_sort() + self.assertEqual(collector.sort_by, "nsamples") + + def test_cycle_sort_invalid_mode(self): + """Test cycling from invalid mode resets to nsamples.""" + collector = LiveStatsCollector(1000) + collector.sort_by = "invalid_mode" + collector._cycle_sort() + self.assertEqual(collector.sort_by, "nsamples") + + def test_cycle_sort_backward_from_nsamples(self): + """Test cycling backward from nsamples goes to cumtime.""" + collector = LiveStatsCollector(1000, sort_by="nsamples") + collector._cycle_sort(reverse=True) + self.assertEqual(collector.sort_by, "cumtime") + + def test_cycle_sort_backward_from_cumtime(self): + """Test cycling backward from cumtime goes to cumul_pct.""" + collector = LiveStatsCollector(1000, sort_by="cumtime") + collector._cycle_sort(reverse=True) + self.assertEqual(collector.sort_by, "cumul_pct") + + def test_cycle_sort_backward_from_sample_pct(self): + """Test cycling backward from sample_pct goes to nsamples.""" + collector = LiveStatsCollector(1000, sort_by="sample_pct") + collector._cycle_sort(reverse=True) + self.assertEqual(collector.sort_by, "nsamples") + + def test_input_lowercase_s_cycles_forward(self): + """Test that lowercase 's' cycles forward.""" + display = MockDisplay() + collector = LiveStatsCollector( + 1000, sort_by="nsamples", display=display + ) + + display.simulate_input(ord("s")) + collector._handle_input() + + self.assertEqual(collector.sort_by, "sample_pct") + + def test_input_uppercase_s_cycles_backward(self): + """Test that uppercase 'S' cycles backward.""" + display = MockDisplay() + collector = LiveStatsCollector( + 1000, sort_by="nsamples", display=display + ) + + display.simulate_input(ord("S")) + collector._handle_input() + + self.assertEqual(collector.sort_by, "cumtime") + + +class TestLiveStatsCollectorFormatting(unittest.TestCase): + """Tests for formatting methods.""" + + def test_format_uptime_seconds(self): + """Test uptime formatting for seconds only.""" + collector = LiveStatsCollector(1000, display=MockDisplay()) + colors = collector._setup_colors() + collector._initialize_widgets(colors) + self.assertEqual(collector.header_widget.format_uptime(45), "0m45s") + + def test_format_uptime_minutes(self): + """Test uptime formatting for minutes.""" + collector = LiveStatsCollector(1000, display=MockDisplay()) + colors = collector._setup_colors() + collector._initialize_widgets(colors) + self.assertEqual(collector.header_widget.format_uptime(125), "2m05s") + + def test_format_uptime_hours(self): + """Test uptime formatting for hours.""" + collector = LiveStatsCollector(1000, display=MockDisplay()) + colors = collector._setup_colors() + collector._initialize_widgets(colors) + self.assertEqual( + collector.header_widget.format_uptime(3661), "1h01m01s" + ) + + def test_format_uptime_large_values(self): + """Test uptime formatting for large time values.""" + collector = LiveStatsCollector(1000, display=MockDisplay()) + colors = collector._setup_colors() + collector._initialize_widgets(colors) + self.assertEqual( + collector.header_widget.format_uptime(86400), "24h00m00s" + ) + + def test_format_uptime_zero(self): + """Test uptime formatting for zero.""" + collector = LiveStatsCollector(1000, display=MockDisplay()) + colors = collector._setup_colors() + collector._initialize_widgets(colors) + self.assertEqual(collector.header_widget.format_uptime(0), "0m00s") + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_live_collector_interaction.py b/Lib/test/test_profiling/test_sampling_profiler/test_live_collector_interaction.py new file mode 100644 index 00000000000..6dfdb9a192e --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_live_collector_interaction.py @@ -0,0 +1,1234 @@ +"""Interactive controls tests for LiveStatsCollector. + +Tests for interactive controls, filtering, filter input, and thread navigation. +""" + +import time +import unittest +from test.support import requires +from test.support.import_helper import import_module + +# Only run these tests if curses is available +requires("curses") +curses = import_module("curses") + +from profiling.sampling.live_collector import LiveStatsCollector, MockDisplay +from profiling.sampling.constants import ( + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_ON_CPU, +) +from ._live_collector_helpers import ( + MockFrameInfo, + MockThreadInfo, + MockInterpreterInfo, +) + + +class TestLiveCollectorInteractiveControls(unittest.TestCase): + """Tests for interactive control features.""" + + def setUp(self): + """Set up collector with mock display.""" + self.display = MockDisplay(height=40, width=160) + self.collector = LiveStatsCollector( + 1000, pid=12345, display=self.display + ) + self.collector.start_time = time.perf_counter() + # Set a consistent display update interval for tests + self.collector.display_update_interval_sec = 0.1 + + def tearDown(self): + """Clean up after test.""" + pass + + def test_pause_functionality(self): + """Test pause/resume functionality.""" + self.assertFalse(self.collector.paused) + + # Simulate 'p' key press + self.display.simulate_input(ord("p")) + self.collector._handle_input() + + self.assertTrue(self.collector.paused) + + # Press 'p' again to resume + self.display.simulate_input(ord("p")) + self.collector._handle_input() + + self.assertFalse(self.collector.paused) + + def test_pause_stops_ui_updates(self): + """Test that pausing stops UI updates but profiling continues.""" + # Add some data + self.collector.total_samples = 10 + self.collector.result[("test.py", 1, "func")] = { + "direct_calls": 5, + "cumulative_calls": 10, + "total_rec_calls": 0, + } + + # Pause + self.collector.paused = True + + # Simulate a collect call (profiling continues) + thread_info = MockThreadInfo(123, []) + interpreter_info = MockInterpreterInfo(0, [thread_info]) + stack_frames = [interpreter_info] + + initial_samples = self.collector.total_samples + self.collector.collect(stack_frames) + + # Samples should still increment + self.assertEqual(self.collector.total_samples, initial_samples + 1) + + # But display should not have been updated (buffer stays clear) + self.display.cleared = False + self.collector.collect(stack_frames) + self.assertFalse( + self.display.cleared, "Display should not update when paused" + ) + + def test_reset_stats(self): + """Test reset statistics functionality.""" + # Add some stats + self.collector.total_samples = 100 + self.collector.successful_samples = 90 + self.collector.failed_samples = 10 + self.collector.result[("test.py", 1, "func")] = { + "direct_calls": 50, + "cumulative_calls": 75, + "total_rec_calls": 0, + } + self.collector.opcode_stats[("test.py", 1, "func")][100] = 5 + + # Reset + self.collector.reset_stats() + + self.assertEqual(self.collector.total_samples, 0) + self.assertEqual(self.collector.successful_samples, 0) + self.assertEqual(self.collector.failed_samples, 0) + self.assertEqual(len(self.collector.result), 0) + self.assertEqual(len(self.collector.opcode_stats), 0) + + def test_increase_refresh_rate(self): + """Test increasing refresh rate (faster updates).""" + initial_interval = self.collector.display_update_interval_sec + + # Simulate '+' key press (faster = smaller interval) + self.display.simulate_input(ord("+")) + self.collector._handle_input() + + self.assertLess(self.collector.display_update_interval_sec, initial_interval) + + def test_decrease_refresh_rate(self): + """Test decreasing refresh rate (slower updates).""" + initial_interval = self.collector.display_update_interval_sec + + # Simulate '-' key press (slower = larger interval) + self.display.simulate_input(ord("-")) + self.collector._handle_input() + + self.assertGreater(self.collector.display_update_interval_sec, initial_interval) + + def test_refresh_rate_minimum(self): + """Test that refresh rate has a minimum (max speed).""" + self.collector.display_update_interval_sec = 0.05 # Set to minimum + + # Try to go faster + self.display.simulate_input(ord("+")) + self.collector._handle_input() + + # Should stay at minimum + self.assertEqual(self.collector.display_update_interval_sec, 0.05) + + def test_refresh_rate_maximum(self): + """Test that refresh rate has a maximum (min speed).""" + self.collector.display_update_interval_sec = 1.0 # Set to maximum + + # Try to go slower + self.display.simulate_input(ord("-")) + self.collector._handle_input() + + # Should stay at maximum + self.assertEqual(self.collector.display_update_interval_sec, 1.0) + + def test_help_toggle(self): + """Test help screen toggle.""" + self.assertFalse(self.collector.show_help) + + # Show help + self.display.simulate_input(ord("h")) + self.collector._handle_input() + + self.assertTrue(self.collector.show_help) + + # Pressing any key closes help + self.display.simulate_input(ord("x")) + self.collector._handle_input() + + self.assertFalse(self.collector.show_help) + + def test_help_with_question_mark(self): + """Test help screen with '?' key.""" + self.display.simulate_input(ord("?")) + self.collector._handle_input() + + self.assertTrue(self.collector.show_help) + + def test_help_dismiss_with_q_does_not_quit(self): + """Test that pressing 'q' while help is shown only closes help, not quit""" + self.assertFalse(self.collector.show_help) + self.display.simulate_input(ord("h")) + self.collector._handle_input() + self.assertTrue(self.collector.show_help) + + self.display.simulate_input(ord("q")) + self.collector._handle_input() + + self.assertFalse(self.collector.show_help) + self.assertTrue(self.collector.running) + + def test_filter_clear(self): + """Test clearing filter.""" + self.collector.filter_pattern = "test" + + # Clear filter + self.display.simulate_input(ord("c")) + self.collector._handle_input() + + self.assertIsNone(self.collector.filter_pattern) + + def test_filter_clear_when_none(self): + """Test clearing filter when no filter is set.""" + self.assertIsNone(self.collector.filter_pattern) + + # Should not crash + self.display.simulate_input(ord("c")) + self.collector._handle_input() + + self.assertIsNone(self.collector.filter_pattern) + + def test_paused_status_in_footer(self): + """Test that paused status appears in footer.""" + self.collector.total_samples = 10 + self.collector.paused = True + + self.collector._update_display() + + # Check that PAUSED appears in display + self.assertTrue(self.display.contains_text("PAUSED")) + + def test_filter_status_in_footer(self): + """Test that filter status appears in footer.""" + self.collector.total_samples = 10 + self.collector.filter_pattern = "mytest" + + self.collector._update_display() + + # Check that filter info appears + self.assertTrue(self.display.contains_text("Filter")) + + def test_help_screen_display(self): + """Test that help screen is displayed.""" + self.collector.show_help = True + + self.collector._update_display() + + # Check for help content + self.assertTrue(self.display.contains_text("Interactive Commands")) + + def test_pause_uppercase(self): + """Test pause with uppercase 'P' key.""" + self.assertFalse(self.collector.paused) + + self.display.simulate_input(ord("P")) + self.collector._handle_input() + + self.assertTrue(self.collector.paused) + + def test_help_uppercase(self): + """Test help with uppercase 'H' key.""" + self.assertFalse(self.collector.show_help) + + self.display.simulate_input(ord("H")) + self.collector._handle_input() + + self.assertTrue(self.collector.show_help) + + def test_reset_lowercase(self): + """Test reset with lowercase 'r' key.""" + # Add some stats + self.collector.total_samples = 100 + self.collector.result[("test.py", 1, "func")] = { + "direct_calls": 50, + "cumulative_calls": 75, + "total_rec_calls": 0, + } + + self.display.simulate_input(ord("r")) + self.collector._handle_input() + + self.assertEqual(self.collector.total_samples, 0) + self.assertEqual(len(self.collector.result), 0) + + def test_reset_uppercase(self): + """Test reset with uppercase 'R' key.""" + self.collector.total_samples = 100 + + self.display.simulate_input(ord("R")) + self.collector._handle_input() + + self.assertEqual(self.collector.total_samples, 0) + + def test_filter_clear_uppercase(self): + """Test clearing filter with uppercase 'C' key.""" + self.collector.filter_pattern = "test" + + self.display.simulate_input(ord("C")) + self.collector._handle_input() + + self.assertIsNone(self.collector.filter_pattern) + + def test_increase_refresh_rate_with_equals(self): + """Test increasing refresh rate with '=' key.""" + initial_interval = self.collector.display_update_interval_sec + + # Simulate '=' key press (alternative to '+') + self.display.simulate_input(ord("=")) + self.collector._handle_input() + + self.assertLess(self.collector.display_update_interval_sec, initial_interval) + + def test_decrease_refresh_rate_with_underscore(self): + """Test decreasing refresh rate with '_' key.""" + initial_interval = self.collector.display_update_interval_sec + + # Simulate '_' key press (alternative to '-') + self.display.simulate_input(ord("_")) + self.collector._handle_input() + + self.assertGreater(self.collector.display_update_interval_sec, initial_interval) + + def test_finished_state_displays_banner(self): + """Test that finished state shows prominent banner.""" + # Add some sample data + thread_info = MockThreadInfo( + 123, + [ + MockFrameInfo("test.py", 10, "work"), + MockFrameInfo("test.py", 20, "main"), + ], + ) + interpreter_info = MockInterpreterInfo(0, [thread_info]) + stack_frames = [interpreter_info] + self.collector.collect(stack_frames) + + # Mark as finished + self.collector.mark_finished() + + # Check that finished flag is set + self.assertTrue(self.collector.finished) + + # Check that the banner message is displayed + self.assertTrue(self.display.contains_text("PROFILING COMPLETE")) + self.assertTrue(self.display.contains_text("Press 'q' to Quit")) + + def test_finished_state_allows_ui_controls(self): + """Test that finished state allows UI controls but prioritizes quit.""" + self.collector.finished = True + self.collector.running = True + + # Try pressing 's' (sort) - should work and trigger display update + original_sort = self.collector.sort_by + self.display.simulate_input(ord("s")) + self.collector._handle_input() + self.assertTrue(self.collector.running) # Still running + self.assertNotEqual(self.collector.sort_by, original_sort) # Sort changed + + # Try pressing 'p' (pause) - should work + self.display.simulate_input(ord("p")) + self.collector._handle_input() + self.assertTrue(self.collector.running) # Still running + self.assertTrue(self.collector.paused) # Now paused + + # Try pressing 'r' (reset) - should be ignored when finished + self.collector.total_samples = 100 + self.display.simulate_input(ord("r")) + self.collector._handle_input() + self.assertTrue(self.collector.running) # Still running + self.assertEqual(self.collector.total_samples, 100) # NOT reset when finished + + # Press 'q' - should stop + self.display.simulate_input(ord("q")) + self.collector._handle_input() + self.assertFalse(self.collector.running) # Stopped + + def test_finished_state_footer_message(self): + """Test that footer shows appropriate message when finished.""" + # Add some sample data + thread_info = MockThreadInfo( + 123, + [ + MockFrameInfo("test.py", 10, "work"), + MockFrameInfo("test.py", 20, "main"), + ], + ) + interpreter_info = MockInterpreterInfo(0, [thread_info]) + stack_frames = [interpreter_info] + self.collector.collect(stack_frames) + + # Mark as finished + self.collector.mark_finished() + + # Check that footer contains finished message + self.assertTrue(self.display.contains_text("PROFILING FINISHED")) + + def test_finished_state_freezes_time(self): + """Test that time displays are frozen when finished.""" + + # Set up collector with known start time + self.collector.start_time = time.perf_counter() - 10.0 # 10 seconds ago + + # Mark as finished - this should freeze the time + self.collector.mark_finished() + + # Get the frozen elapsed time + frozen_elapsed = self.collector.elapsed_time + frozen_time_display = self.collector.current_time_display + + # Wait a bit to ensure time would advance + time.sleep(0.1) + + # Time should remain frozen + self.assertEqual(self.collector.elapsed_time, frozen_elapsed) + self.assertEqual(self.collector.current_time_display, frozen_time_display) + + # Verify finish timestamp was set + self.assertIsNotNone(self.collector.finish_timestamp) + + # Reset should clear the frozen state + self.collector.reset_stats() + self.assertFalse(self.collector.finished) + self.assertIsNone(self.collector.finish_timestamp) + + +class TestLiveCollectorFiltering(unittest.TestCase): + """Tests for filtering functionality.""" + + def setUp(self): + """Set up collector with test data.""" + self.display = MockDisplay(height=40, width=160) + self.collector = LiveStatsCollector( + 1000, pid=12345, display=self.display + ) + self.collector.start_time = time.perf_counter() + self.collector.total_samples = 100 + + # Add test data + self.collector.result[("app/models.py", 10, "save")] = { + "direct_calls": 50, + "cumulative_calls": 75, + "total_rec_calls": 0, + } + self.collector.result[("app/views.py", 20, "render")] = { + "direct_calls": 30, + "cumulative_calls": 40, + "total_rec_calls": 0, + } + self.collector.result[("lib/utils.py", 30, "helper")] = { + "direct_calls": 20, + "cumulative_calls": 25, + "total_rec_calls": 0, + } + + def test_filter_by_filename(self): + """Test filtering by filename pattern.""" + self.collector.filter_pattern = "models" + + stats_list = self.collector.build_stats_list() + + # Only models.py should be included + self.assertEqual(len(stats_list), 1) + self.assertIn("models.py", stats_list[0]["func"][0]) + + def test_filter_by_function_name(self): + """Test filtering by function name.""" + self.collector.filter_pattern = "render" + + stats_list = self.collector.build_stats_list() + + self.assertEqual(len(stats_list), 1) + self.assertEqual(stats_list[0]["func"][2], "render") + + def test_filter_case_insensitive(self): + """Test that filtering is case-insensitive.""" + self.collector.filter_pattern = "MODELS" + + stats_list = self.collector.build_stats_list() + + # Should still match models.py + self.assertEqual(len(stats_list), 1) + + def test_filter_substring_matching(self): + """Test substring filtering.""" + self.collector.filter_pattern = "app/" + + stats_list = self.collector.build_stats_list() + + # Should match both app files + self.assertEqual(len(stats_list), 2) + + def test_no_filter(self): + """Test with no filter applied.""" + self.collector.filter_pattern = None + + stats_list = self.collector.build_stats_list() + + # All items should be included + self.assertEqual(len(stats_list), 3) + + def test_filter_partial_function_name(self): + """Test filtering by partial function name.""" + self.collector.filter_pattern = "save" + + stats_list = self.collector.build_stats_list() + + self.assertEqual(len(stats_list), 1) + self.assertEqual(stats_list[0]["func"][2], "save") + + def test_filter_combined_filename_funcname(self): + """Test filtering matches filename:funcname pattern.""" + self.collector.filter_pattern = "views.py:render" + + stats_list = self.collector.build_stats_list() + + # Should match the combined pattern + self.assertEqual(len(stats_list), 1) + self.assertEqual(stats_list[0]["func"][2], "render") + + def test_filter_no_matches(self): + """Test filter that matches nothing.""" + self.collector.filter_pattern = "nonexistent" + + stats_list = self.collector.build_stats_list() + + self.assertEqual(len(stats_list), 0) + + +class TestLiveCollectorFilterInput(unittest.TestCase): + """Tests for filter input mode.""" + + def setUp(self): + """Set up collector with mock display.""" + self.display = MockDisplay(height=40, width=160) + self.collector = LiveStatsCollector( + 1000, pid=12345, display=self.display + ) + self.collector.start_time = time.perf_counter() + + def test_enter_filter_mode(self): + """Test entering filter input mode.""" + self.assertFalse(self.collector.filter_input_mode) + + # Press '/' to enter filter mode + self.display.simulate_input(ord("/")) + self.collector._handle_input() + + self.assertTrue(self.collector.filter_input_mode) + + def test_filter_input_typing(self): + """Test typing characters in filter input mode.""" + self.collector.filter_input_mode = True + self.collector.filter_input_buffer = "" + + # Type 't', 'e', 's', 't' + for ch in "test": + self.display.simulate_input(ord(ch)) + self.collector._handle_input() + + self.assertEqual(self.collector.filter_input_buffer, "test") + + def test_filter_input_backspace(self): + """Test backspace in filter input mode.""" + self.collector.filter_input_mode = True + self.collector.filter_input_buffer = "test" + + # Press backspace (127) + self.display.simulate_input(127) + self.collector._handle_input() + + self.assertEqual(self.collector.filter_input_buffer, "tes") + + def test_filter_input_backspace_alt(self): + """Test alternative backspace key (263) in filter input mode.""" + self.collector.filter_input_mode = True + self.collector.filter_input_buffer = "test" + + # Press backspace (263) + self.display.simulate_input(263) + self.collector._handle_input() + + self.assertEqual(self.collector.filter_input_buffer, "tes") + + def test_filter_input_backspace_empty(self): + """Test backspace on empty buffer.""" + self.collector.filter_input_mode = True + self.collector.filter_input_buffer = "" + + # Press backspace - should not crash + self.display.simulate_input(127) + self.collector._handle_input() + + self.assertEqual(self.collector.filter_input_buffer, "") + + def test_filter_input_enter_applies_filter(self): + """Test pressing Enter applies the filter.""" + self.collector.filter_input_mode = True + self.collector.filter_input_buffer = "myfilter" + + # Press Enter (10) + self.display.simulate_input(10) + self.collector._handle_input() + + self.assertFalse(self.collector.filter_input_mode) + self.assertEqual(self.collector.filter_pattern, "myfilter") + self.assertEqual(self.collector.filter_input_buffer, "") + + def test_filter_input_enter_alt(self): + """Test alternative Enter key (13) applies filter.""" + self.collector.filter_input_mode = True + self.collector.filter_input_buffer = "myfilter" + + # Press Enter (13) + self.display.simulate_input(13) + self.collector._handle_input() + + self.assertFalse(self.collector.filter_input_mode) + self.assertEqual(self.collector.filter_pattern, "myfilter") + + def test_filter_input_enter_empty_clears_filter(self): + """Test pressing Enter with empty buffer clears filter.""" + self.collector.filter_input_mode = True + self.collector.filter_input_buffer = "" + self.collector.filter_pattern = "oldfilter" + + # Press Enter + self.display.simulate_input(10) + self.collector._handle_input() + + self.assertFalse(self.collector.filter_input_mode) + self.assertIsNone(self.collector.filter_pattern) + + def test_filter_input_escape_cancels(self): + """Test pressing ESC cancels filter input.""" + self.collector.filter_input_mode = True + self.collector.filter_input_buffer = "newfilter" + self.collector.filter_pattern = "oldfilter" + + # Press ESC (27) + self.display.simulate_input(27) + self.collector._handle_input() + + self.assertFalse(self.collector.filter_input_mode) + self.assertEqual( + self.collector.filter_pattern, "oldfilter" + ) # Unchanged + self.assertEqual(self.collector.filter_input_buffer, "") + + def test_filter_input_start_with_existing_filter(self): + """Test entering filter mode with existing filter pre-fills buffer.""" + self.collector.filter_pattern = "existing" + + # Enter filter mode + self.display.simulate_input(ord("/")) + self.collector._handle_input() + + # Buffer should be pre-filled with existing pattern + self.assertEqual(self.collector.filter_input_buffer, "existing") + + def test_filter_input_start_without_filter(self): + """Test entering filter mode with no existing filter.""" + self.collector.filter_pattern = None + + # Enter filter mode + self.display.simulate_input(ord("/")) + self.collector._handle_input() + + # Buffer should be empty + self.assertEqual(self.collector.filter_input_buffer, "") + + def test_filter_input_mode_blocks_other_commands(self): + """Test that filter input mode blocks other commands.""" + self.collector.filter_input_mode = True + initial_sort = self.collector.sort_by + + # Try to press 's' (sort) - should be captured as input + self.display.simulate_input(ord("s")) + self.collector._handle_input() + + # Sort should not change, 's' should be in buffer + self.assertEqual(self.collector.sort_by, initial_sort) + self.assertEqual(self.collector.filter_input_buffer, "s") + + def test_filter_input_non_printable_ignored(self): + """Test that non-printable characters are ignored.""" + self.collector.filter_input_mode = True + self.collector.filter_input_buffer = "test" + + # Try to input a control character (< 32) + self.display.simulate_input(1) # Ctrl-A + self.collector._handle_input() + + # Buffer should be unchanged + self.assertEqual(self.collector.filter_input_buffer, "test") + + def test_filter_input_high_ascii_ignored(self): + """Test that high ASCII characters (>= 127, except backspace) are ignored.""" + self.collector.filter_input_mode = True + self.collector.filter_input_buffer = "test" + + # Try to input high ASCII (128) + self.display.simulate_input(128) + self.collector._handle_input() + + # Buffer should be unchanged + self.assertEqual(self.collector.filter_input_buffer, "test") + + def test_filter_prompt_displayed(self): + """Test that filter prompt is displayed when in input mode.""" + self.collector.filter_input_mode = True + self.collector.filter_input_buffer = "myfilter" + self.collector.total_samples = 10 + + self.collector._update_display() + + # Should show the filter prompt + self.assertTrue(self.display.contains_text("Function filter")) + self.assertTrue(self.display.contains_text("myfilter")) + + +class TestLiveCollectorThreadNavigation(unittest.TestCase): + """Tests for thread navigation functionality.""" + + def setUp(self): + """Set up collector with mock display and multiple threads.""" + self.mock_display = MockDisplay(height=40, width=160) + self.collector = LiveStatsCollector( + 1000, pid=12345, display=self.mock_display + ) + self.collector.start_time = time.perf_counter() + + # Simulate data from multiple threads + frames1 = [MockFrameInfo("file1.py", 10, "func1")] + frames2 = [MockFrameInfo("file2.py", 20, "func2")] + frames3 = [MockFrameInfo("file3.py", 30, "func3")] + + thread1 = MockThreadInfo(111, frames1) + thread2 = MockThreadInfo(222, frames2) + thread3 = MockThreadInfo(333, frames3) + + interpreter_info = MockInterpreterInfo(0, [thread1, thread2, thread3]) + stack_frames = [interpreter_info] + + # Collect data to populate thread IDs + self.collector.collect(stack_frames) + + def test_initial_view_mode_is_all(self): + """Test that collector starts in ALL mode.""" + self.assertEqual(self.collector.view_mode, "ALL") + self.assertEqual(self.collector.current_thread_index, 0) + + def test_thread_ids_are_tracked(self): + """Test that thread IDs are tracked during collection.""" + self.assertIn(111, self.collector.thread_ids) + self.assertIn(222, self.collector.thread_ids) + self.assertIn(333, self.collector.thread_ids) + self.assertEqual(len(self.collector.thread_ids), 3) + + def test_toggle_to_per_thread_mode(self): + """Test toggling from ALL to PER_THREAD mode with 't' key.""" + self.assertEqual(self.collector.view_mode, "ALL") + + self.mock_display.simulate_input(ord("t")) + self.collector._handle_input() + + self.assertEqual(self.collector.view_mode, "PER_THREAD") + self.assertEqual(self.collector.current_thread_index, 0) + + def test_toggle_back_to_all_mode(self): + """Test toggling back from PER_THREAD to ALL mode.""" + # Switch to PER_THREAD + self.mock_display.simulate_input(ord("t")) + self.collector._handle_input() + self.assertEqual(self.collector.view_mode, "PER_THREAD") + + # Switch back to ALL + self.mock_display.simulate_input(ord("T")) + self.collector._handle_input() + self.assertEqual(self.collector.view_mode, "ALL") + + def test_arrow_right_navigates_threads_in_per_thread_mode(self): + """Test that arrow keys navigate threads in PER_THREAD mode.""" + # Switch to PER_THREAD mode + self.mock_display.simulate_input(ord("t")) + self.collector._handle_input() + + # Navigate forward + self.assertEqual(self.collector.current_thread_index, 0) + + self.mock_display.simulate_input(curses.KEY_RIGHT) + self.collector._handle_input() + self.assertEqual(self.collector.current_thread_index, 1) + + self.mock_display.simulate_input(curses.KEY_RIGHT) + self.collector._handle_input() + self.assertEqual(self.collector.current_thread_index, 2) + + def test_arrow_left_navigates_threads_backward(self): + """Test that left arrow navigates threads backward.""" + # Switch to PER_THREAD mode + self.mock_display.simulate_input(ord("t")) + self.collector._handle_input() + + # Navigate backward (should wrap around) + self.mock_display.simulate_input(curses.KEY_LEFT) + self.collector._handle_input() + self.assertEqual( + self.collector.current_thread_index, 2 + ) # Wrapped to last + + self.mock_display.simulate_input(curses.KEY_LEFT) + self.collector._handle_input() + self.assertEqual(self.collector.current_thread_index, 1) + + def test_arrow_down_navigates_like_right(self): + """Test that down arrow works like right arrow.""" + # Switch to PER_THREAD mode + self.mock_display.simulate_input(ord("t")) + self.collector._handle_input() + + self.mock_display.simulate_input(curses.KEY_DOWN) + self.collector._handle_input() + self.assertEqual(self.collector.current_thread_index, 1) + + def test_arrow_up_navigates_like_left(self): + """Test that up arrow works like left arrow.""" + # Switch to PER_THREAD mode + self.mock_display.simulate_input(ord("t")) + self.collector._handle_input() + + self.mock_display.simulate_input(curses.KEY_UP) + self.collector._handle_input() + self.assertEqual(self.collector.current_thread_index, 2) # Wrapped + + def test_arrow_keys_switch_to_per_thread_mode(self): + """Test that arrow keys switch from ALL mode to PER_THREAD mode.""" + self.assertEqual(self.collector.view_mode, "ALL") + + self.mock_display.simulate_input(curses.KEY_RIGHT) + self.collector._handle_input() + self.assertEqual(self.collector.view_mode, "PER_THREAD") + self.assertEqual(self.collector.current_thread_index, 0) + + def test_stats_list_in_all_mode(self): + """Test that stats list uses aggregated data in ALL mode.""" + stats_list = self.collector.build_stats_list() + + # Should have all 3 functions + self.assertEqual(len(stats_list), 3) + func_names = {stat["func"][2] for stat in stats_list} + self.assertEqual(func_names, {"func1", "func2", "func3"}) + + def test_stats_list_in_per_thread_mode(self): + """Test that stats list filters by thread in PER_THREAD mode.""" + # Switch to PER_THREAD mode + self.collector.view_mode = "PER_THREAD" + self.collector.current_thread_index = 0 # First thread (111) + + stats_list = self.collector.build_stats_list() + + # Should only have func1 from thread 111 + self.assertEqual(len(stats_list), 1) + self.assertEqual(stats_list[0]["func"][2], "func1") + + def test_stats_list_switches_with_thread_navigation(self): + """Test that stats list updates when navigating threads.""" + self.collector.view_mode = "PER_THREAD" + + # Thread 0 (111) -> func1 + self.collector.current_thread_index = 0 + stats_list = self.collector.build_stats_list() + self.assertEqual(len(stats_list), 1) + self.assertEqual(stats_list[0]["func"][2], "func1") + + # Thread 1 (222) -> func2 + self.collector.current_thread_index = 1 + stats_list = self.collector.build_stats_list() + self.assertEqual(len(stats_list), 1) + self.assertEqual(stats_list[0]["func"][2], "func2") + + # Thread 2 (333) -> func3 + self.collector.current_thread_index = 2 + stats_list = self.collector.build_stats_list() + self.assertEqual(len(stats_list), 1) + self.assertEqual(stats_list[0]["func"][2], "func3") + + def test_reset_stats_clears_thread_data(self): + """Test that reset_stats clears thread tracking data.""" + self.assertGreater(len(self.collector.thread_ids), 0) + self.assertGreater(len(self.collector.per_thread_data), 0) + + self.collector.reset_stats() + + self.assertEqual(len(self.collector.thread_ids), 0) + self.assertEqual(len(self.collector.per_thread_data), 0) + self.assertEqual(self.collector.view_mode, "ALL") + self.assertEqual(self.collector.current_thread_index, 0) + + def test_toggle_with_no_threads_stays_in_all_mode(self): + """Test that toggle does nothing when no threads exist.""" + collector = LiveStatsCollector(1000, display=MockDisplay()) + self.assertEqual(len(collector.thread_ids), 0) + + collector.display.simulate_input(ord("t")) + collector._handle_input() + + # Should remain in ALL mode since no threads + self.assertEqual(collector.view_mode, "ALL") + + def test_per_thread_data_isolation(self): + """Test that per-thread data is properly isolated.""" + # Check that each thread has its own isolated data + self.assertIn(111, self.collector.per_thread_data) + self.assertIn(222, self.collector.per_thread_data) + self.assertIn(333, self.collector.per_thread_data) + + # Thread 111 should only have func1 + thread1_funcs = list(self.collector.per_thread_data[111].result.keys()) + self.assertEqual(len(thread1_funcs), 1) + self.assertEqual(thread1_funcs[0][2], "func1") + + # Thread 222 should only have func2 + thread2_funcs = list(self.collector.per_thread_data[222].result.keys()) + self.assertEqual(len(thread2_funcs), 1) + self.assertEqual(thread2_funcs[0][2], "func2") + + def test_aggregated_data_sums_all_threads(self): + """Test that ALL mode shows aggregated data from all threads.""" + # All three functions should be in the aggregated result + self.assertEqual(len(self.collector.result), 3) + + # Each function should have 1 direct call + for func_location, counts in self.collector.result.items(): + self.assertEqual(counts["direct_calls"], 1) + + def test_per_thread_status_tracking(self): + """Test that per-thread status statistics are tracked.""" + # Each thread should have status counts + self.assertIn(111, self.collector.per_thread_data) + self.assertIn(222, self.collector.per_thread_data) + self.assertIn(333, self.collector.per_thread_data) + + # Each thread should have the expected attributes + for thread_id in [111, 222, 333]: + thread_data = self.collector.per_thread_data[thread_id] + self.assertIsNotNone(thread_data.has_gil) + self.assertIsNotNone(thread_data.on_cpu) + self.assertIsNotNone(thread_data.gil_requested) + self.assertIsNotNone(thread_data.unknown) + self.assertIsNotNone(thread_data.total) + # Each thread was sampled once + self.assertEqual(thread_data.total, 1) + + def test_reset_stats_clears_thread_status(self): + """Test that reset_stats clears per-thread status data.""" + self.assertGreater(len(self.collector.per_thread_data), 0) + + self.collector.reset_stats() + + self.assertEqual(len(self.collector.per_thread_data), 0) + + def test_per_thread_sample_counts(self): + """Test that per-thread sample counts are tracked correctly.""" + # Each thread should have exactly 1 sample (we collected once) + for thread_id in [111, 222, 333]: + self.assertIn(thread_id, self.collector.per_thread_data) + self.assertEqual(self.collector.per_thread_data[thread_id].sample_count, 1) + + def test_per_thread_gc_samples(self): + """Test that per-thread GC samples are tracked correctly.""" + # Initially no threads have GC frames + for thread_id in [111, 222, 333]: + self.assertIn(thread_id, self.collector.per_thread_data) + self.assertEqual( + self.collector.per_thread_data[thread_id].gc_frame_samples, 0 + ) + + # Now collect a sample with a GC frame in thread 222 + gc_frames = [MockFrameInfo("gc.py", 100, "gc_collect")] + thread_with_gc = MockThreadInfo(222, gc_frames) + interpreter_info = MockInterpreterInfo(0, [thread_with_gc]) + stack_frames = [interpreter_info] + + self.collector.collect(stack_frames) + + # Thread 222 should now have 1 GC sample + self.assertEqual(self.collector.per_thread_data[222].gc_frame_samples, 1) + # Other threads should still have 0 + self.assertEqual(self.collector.per_thread_data[111].gc_frame_samples, 0) + self.assertEqual(self.collector.per_thread_data[333].gc_frame_samples, 0) + + def test_only_threads_with_frames_are_tracked(self): + """Test that only threads with actual frame data are added to thread_ids.""" + # Create a new collector + collector = LiveStatsCollector(1000, display=MockDisplay()) + + # Create threads: one with frames, one without + frames = [MockFrameInfo("test.py", 10, "test_func")] + thread_with_frames = MockThreadInfo(111, frames) + thread_without_frames = MockThreadInfo(222, None) # No frames + interpreter_info = MockInterpreterInfo( + 0, [thread_with_frames, thread_without_frames] + ) + stack_frames = [interpreter_info] + + collector.collect(stack_frames) + + # Only thread 111 should be tracked (it has frames) + self.assertIn(111, collector.thread_ids) + self.assertNotIn(222, collector.thread_ids) + + def test_per_thread_status_isolation(self): + """Test that per-thread status counts are isolated per thread.""" + # Create threads with different status flags + + frames1 = [MockFrameInfo("file1.py", 10, "func1")] + frames2 = [MockFrameInfo("file2.py", 20, "func2")] + + # Thread 444: has GIL but not on CPU + thread1 = MockThreadInfo(444, frames1, status=THREAD_STATUS_HAS_GIL) + # Thread 555: on CPU but not has GIL + thread2 = MockThreadInfo(555, frames2, status=THREAD_STATUS_ON_CPU) + + interpreter_info = MockInterpreterInfo(0, [thread1, thread2]) + stack_frames = [interpreter_info] + + collector = LiveStatsCollector(1000, display=MockDisplay()) + collector.collect(stack_frames) + + # Check thread 444 status + self.assertEqual(collector.per_thread_data[444].has_gil, 1) + self.assertEqual(collector.per_thread_data[444].on_cpu, 0) + + # Check thread 555 status + self.assertEqual(collector.per_thread_data[555].has_gil, 0) + self.assertEqual(collector.per_thread_data[555].on_cpu, 1) + + def test_display_uses_per_thread_stats_in_per_thread_mode(self): + """Test that display widget uses per-thread stats when in PER_THREAD mode.""" + + # Create collector with mock display + collector = LiveStatsCollector(1000, display=MockDisplay()) + collector.start_time = time.perf_counter() + + # Create 2 threads with different characteristics + # Thread 111: always has GIL (10 samples) + # Thread 222: never has GIL (10 samples) + for _ in range(10): + frames1 = [MockFrameInfo("file1.py", 10, "func1")] + frames2 = [MockFrameInfo("file2.py", 20, "func2")] + thread1 = MockThreadInfo( + 111, frames1, status=THREAD_STATUS_HAS_GIL + ) + thread2 = MockThreadInfo(222, frames2, status=0) # No flags + interpreter_info = MockInterpreterInfo(0, [thread1, thread2]) + collector.collect([interpreter_info]) + + # In ALL mode, should show mixed stats (50% on GIL, 50% off GIL) + self.assertEqual(collector.view_mode, "ALL") + total_has_gil = collector.thread_status_counts["has_gil"] + total_threads = collector.thread_status_counts["total"] + self.assertEqual(total_has_gil, 10) # Only thread 111 has GIL + self.assertEqual(total_threads, 20) # 10 samples * 2 threads + + # Switch to PER_THREAD mode and select thread 111 + collector.view_mode = "PER_THREAD" + collector.current_thread_index = 0 # Thread 111 + + # Thread 111 should show 100% on GIL + thread_111_data = collector.per_thread_data[111] + self.assertEqual(thread_111_data.has_gil, 10) + self.assertEqual(thread_111_data.total, 10) + + # Switch to thread 222 + collector.current_thread_index = 1 # Thread 222 + + # Thread 222 should show 0% on GIL + thread_222_data = collector.per_thread_data[222] + self.assertEqual(thread_222_data.has_gil, 0) + self.assertEqual(thread_222_data.total, 10) + + def test_display_uses_per_thread_gc_stats_in_per_thread_mode(self): + """Test that GC percentage uses per-thread data in PER_THREAD mode.""" + # Create collector with mock display + collector = LiveStatsCollector(1000, display=MockDisplay()) + collector.start_time = time.perf_counter() + + # Thread 111: 5 samples, 2 with GC + # Thread 222: 5 samples, 0 with GC + for i in range(5): + if i < 2: + # First 2 samples for thread 111 have GC + frames1 = [MockFrameInfo("gc.py", 100, "gc_collect")] + else: + frames1 = [MockFrameInfo("file1.py", 10, "func1")] + + frames2 = [MockFrameInfo("file2.py", 20, "func2")] # No GC + + thread1 = MockThreadInfo(111, frames1) + thread2 = MockThreadInfo(222, frames2) + interpreter_info = MockInterpreterInfo(0, [thread1, thread2]) + collector.collect([interpreter_info]) + + # Check aggregated GC stats (ALL mode) + # 2 GC samples out of 10 total = 20% + self.assertEqual(collector.gc_frame_samples, 2) + self.assertEqual(collector.total_samples, 5) # 5 collect() calls + + # Check per-thread GC stats + # Thread 111: 2 GC samples out of 5 = 40% + self.assertEqual(collector.per_thread_data[111].gc_frame_samples, 2) + self.assertEqual(collector.per_thread_data[111].sample_count, 5) + + # Thread 222: 0 GC samples out of 5 = 0% + self.assertEqual(collector.per_thread_data[222].gc_frame_samples, 0) + self.assertEqual(collector.per_thread_data[222].sample_count, 5) + + # Now verify the display would use the correct stats + collector.view_mode = "PER_THREAD" + + # For thread 111 + collector.current_thread_index = 0 + thread_id = collector.thread_ids[0] + self.assertEqual(thread_id, 111) + thread_gc_pct = ( + collector.per_thread_data[111].gc_frame_samples + / collector.per_thread_data[111].sample_count + ) * 100 + self.assertEqual(thread_gc_pct, 40.0) + + # For thread 222 + collector.current_thread_index = 1 + thread_id = collector.thread_ids[1] + self.assertEqual(thread_id, 222) + thread_gc_pct = ( + collector.per_thread_data[222].gc_frame_samples + / collector.per_thread_data[222].sample_count + ) * 100 + self.assertEqual(thread_gc_pct, 0.0) + + def test_function_counts_are_per_thread_in_per_thread_mode(self): + """Test that function counts (total/exec/stack) are per-thread in PER_THREAD mode.""" + # Create collector with mock display + collector = LiveStatsCollector(1000, display=MockDisplay()) + collector.start_time = time.perf_counter() + + # Thread 111: calls func1, func2, func3 (3 functions) + # Thread 222: calls func4, func5 (2 functions) + frames1 = [ + MockFrameInfo("file1.py", 10, "func1"), + MockFrameInfo("file1.py", 20, "func2"), + MockFrameInfo("file1.py", 30, "func3"), + ] + frames2 = [ + MockFrameInfo("file2.py", 40, "func4"), + MockFrameInfo("file2.py", 50, "func5"), + ] + + thread1 = MockThreadInfo(111, frames1) + thread2 = MockThreadInfo(222, frames2) + interpreter_info = MockInterpreterInfo(0, [thread1, thread2]) + collector.collect([interpreter_info]) + + # In ALL mode, should have 5 total functions + self.assertEqual(len(collector.result), 5) + + # In PER_THREAD mode for thread 111, should have 3 functions + collector.view_mode = "PER_THREAD" + collector.current_thread_index = 0 # Thread 111 + thread_111_result = collector.per_thread_data[111].result + self.assertEqual(len(thread_111_result), 3) + + # Verify the functions are the right ones + thread_111_funcs = {loc[2] for loc in thread_111_result.keys()} + self.assertEqual(thread_111_funcs, {"func1", "func2", "func3"}) + + # In PER_THREAD mode for thread 222, should have 2 functions + collector.current_thread_index = 1 # Thread 222 + thread_222_result = collector.per_thread_data[222].result + self.assertEqual(len(thread_222_result), 2) + + # Verify the functions are the right ones + thread_222_funcs = {loc[2] for loc in thread_222_result.keys()} + self.assertEqual(thread_222_funcs, {"func4", "func5"}) + + +class TestLiveCollectorNewFeatures(unittest.TestCase): + """Tests for new features added to live collector.""" + + def setUp(self): + """Set up test fixtures.""" + self.display = MockDisplay() + self.collector = LiveStatsCollector(1000, display=self.display) + self.collector.start_time = time.perf_counter() + + def test_filter_input_takes_precedence_over_commands(self): + """Test that filter input mode blocks command keys like 'h' and 'p'.""" + # Enter filter input mode + self.collector.filter_input_mode = True + self.collector.filter_input_buffer = "" + + # Press 'h' - should add to filter buffer, not show help + self.display.simulate_input(ord("h")) + self.collector._handle_input() + + self.assertFalse(self.collector.show_help) # Help not triggered + self.assertEqual(self.collector.filter_input_buffer, "h") # Added to filter + self.assertTrue(self.collector.filter_input_mode) # Still in filter mode + + def test_reset_blocked_when_finished(self): + """Test that reset command is blocked when profiling is finished.""" + # Set up some sample data and mark as finished + self.collector.total_samples = 100 + self.collector.finished = True + + # Press 'r' for reset + self.display.simulate_input(ord("r")) + self.collector._handle_input() + + # Should NOT have been reset + self.assertEqual(self.collector.total_samples, 100) + self.assertTrue(self.collector.finished) + + def test_time_display_fix_when_finished(self): + """Test that time display shows correct frozen time when finished.""" + + # Mark as finished to freeze time + self.collector.mark_finished() + + # Should have set both timestamps correctly + self.assertIsNotNone(self.collector.finish_timestamp) + self.assertIsNotNone(self.collector.finish_wall_time) + + # Get the frozen time display + frozen_time = self.collector.current_time_display + + # Wait a bit + time.sleep(0.1) + + # Should still show the same frozen time (not jump to wrong time) + self.assertEqual(self.collector.current_time_display, frozen_time) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_live_collector_ui.py b/Lib/test/test_profiling/test_sampling_profiler/test_live_collector_ui.py new file mode 100644 index 00000000000..ae4ee969f34 --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_live_collector_ui.py @@ -0,0 +1,906 @@ +"""UI and display tests for LiveStatsCollector. + +Tests for MockDisplay, curses integration, display methods, +edge cases, update display, and display helpers. +""" + +import functools +import io +import sys +import tempfile +import time +import unittest +from unittest import mock +from test.support import requires, requires_remote_subprocess_debugging +from test.support.import_helper import import_module + +# Only run these tests if curses is available +requires("curses") +curses = import_module("curses") + +from profiling.sampling.live_collector import LiveStatsCollector, MockDisplay +from profiling.sampling.cli import main +from ._live_collector_helpers import ( + MockThreadInfo, + MockInterpreterInfo, +) +from .helpers import close_and_unlink + + +class TestLiveStatsCollectorWithMockDisplay(unittest.TestCase): + """Tests for display functionality using MockDisplay.""" + + def setUp(self): + """Set up collector with mock display.""" + self.mock_display = MockDisplay(height=40, width=160) + self.collector = LiveStatsCollector( + 1000, pid=12345, display=self.mock_display + ) + self.collector.start_time = time.perf_counter() + + def test_update_display_with_mock(self): + """Test that update_display works with MockDisplay.""" + self.collector.total_samples = 100 + self.collector.result[("test.py", 10, "test_func")] = { + "direct_calls": 50, + "cumulative_calls": 75, + "total_rec_calls": 0, + } + + self.collector._update_display() + + # Verify display operations were called + self.assertTrue(self.mock_display.cleared) + self.assertTrue(self.mock_display.refreshed) + self.assertTrue(self.mock_display.redrawn) + + # Verify some content was written + self.assertGreater(len(self.mock_display.buffer), 0) + + def test_handle_input_quit(self): + """Test that 'q' input stops the collector.""" + self.mock_display.simulate_input(ord("q")) + self.collector._handle_input() + self.assertFalse(self.collector.running) + + def test_handle_input_sort_cycle(self): + """Test that 's' input cycles sort mode.""" + self.collector.sort_by = "tottime" + self.mock_display.simulate_input(ord("s")) + self.collector._handle_input() + self.assertEqual(self.collector.sort_by, "cumul_pct") + + def test_draw_methods_with_mock_display(self): + """Test that draw methods write to mock display.""" + self.collector.total_samples = 500 + self.collector.successful_samples = 450 + self.collector.failed_samples = 50 + + colors = self.collector._setup_colors() + self.collector._initialize_widgets(colors) + + # Test individual widget methods + line = self.collector.header_widget.draw_header_info(0, 160, 100.5) + self.assertEqual(line, 2) # Title + header info line + self.assertGreater(len(self.mock_display.buffer), 0) + + # Clear buffer and test next method + self.mock_display.buffer.clear() + line = self.collector.header_widget.draw_sample_stats(0, 160, 10.0) + self.assertEqual(line, 1) + self.assertGreater(len(self.mock_display.buffer), 0) + + def test_terminal_too_small_message(self): + """Test terminal too small warning.""" + small_display = MockDisplay(height=10, width=50) + self.collector.display = small_display + + self.collector._show_terminal_too_small(10, 50) + + # Should have written warning message + text = small_display.get_text_at(3, 15) # Approximate center + self.assertIsNotNone(text) + + def test_full_display_rendering_with_data(self): + """Test complete display rendering with realistic data.""" + # Add multiple functions with different call counts + self.collector.total_samples = 1000 + self.collector.successful_samples = 950 + self.collector.failed_samples = 50 + + self.collector.result[("app.py", 10, "main")] = { + "direct_calls": 100, + "cumulative_calls": 500, + "total_rec_calls": 0, + } + self.collector.result[("utils.py", 20, "helper")] = { + "direct_calls": 300, + "cumulative_calls": 400, + "total_rec_calls": 0, + } + self.collector.result[("db.py", 30, "query")] = { + "direct_calls": 50, + "cumulative_calls": 100, + "total_rec_calls": 0, + } + + self.collector._update_display() + + # Verify the display has content + self.assertGreater(len(self.mock_display.buffer), 10) + + # Verify PID is shown + found_pid = False + for (line, col), (text, attr) in self.mock_display.buffer.items(): + if "12345" in text: + found_pid = True + break + self.assertTrue(found_pid, "PID should be displayed") + + def test_efficiency_bar_visualization(self): + """Test that efficiency bar shows correct proportions.""" + self.collector.total_samples = 100 + self.collector.successful_samples = 75 + self.collector.failed_samples = 25 + + colors = self.collector._setup_colors() + self.collector._initialize_widgets(colors) + self.collector.header_widget.draw_efficiency_bar(0, 160) + + # Check that something was drawn to the display + self.assertGreater(len(self.mock_display.buffer), 0) + + def test_stats_display_with_different_sort_modes(self): + """Test that stats are displayed correctly with different sort modes.""" + self.collector.total_samples = 100 + self.collector.successful_samples = 100 # For percentage calculations + self.collector.result[("a.py", 1, "func_a")] = { + "direct_calls": 10, + "cumulative_calls": 20, + "total_rec_calls": 0, + } + self.collector.result[("b.py", 2, "func_b")] = { + "direct_calls": 30, + "cumulative_calls": 40, + "total_rec_calls": 0, + } + + # Test each sort mode + for sort_mode in [ + "nsamples", + "tottime", + "cumtime", + "sample_pct", + "cumul_pct", + ]: + self.mock_display.buffer.clear() + self.collector.sort_by = sort_mode + + stats_list = self.collector.build_stats_list() + self.assertEqual(len(stats_list), 2) + + # Verify sorting worked (func_b should be first for most modes) + if sort_mode in ["nsamples", "tottime", "sample_pct"]: + self.assertEqual(stats_list[0]["func"][2], "func_b") + + def test_narrow_terminal_column_hiding(self): + """Test that columns are hidden on narrow terminals.""" + narrow_display = MockDisplay(height=40, width=70) + collector = LiveStatsCollector(1000, pid=12345, display=narrow_display) + collector.start_time = time.perf_counter() + + colors = collector._setup_colors() + collector._initialize_widgets(colors) + line, show_sample_pct, show_tottime, show_cumul_pct, show_cumtime = ( + collector.table_widget.draw_column_headers(0, 70) + ) + + # On narrow terminal, some columns should be hidden + self.assertFalse( + show_cumul_pct or show_cumtime, + "Some columns should be hidden on narrow terminal", + ) + + def test_very_narrow_terminal_minimal_columns(self): + """Test minimal display on very narrow terminal.""" + very_narrow = MockDisplay(height=40, width=60) + collector = LiveStatsCollector(1000, pid=12345, display=very_narrow) + collector.start_time = time.perf_counter() + + colors = collector._setup_colors() + collector._initialize_widgets(colors) + line, show_sample_pct, show_tottime, show_cumul_pct, show_cumtime = ( + collector.table_widget.draw_column_headers(0, 60) + ) + + # Very narrow should hide even more columns + self.assertFalse( + show_sample_pct, + "Sample % should be hidden on very narrow terminal", + ) + + def test_display_updates_only_at_interval(self): + """Test that display updates respect the update interval.""" + # Create collector with display + collector = LiveStatsCollector(1000, display=self.mock_display) + + # Simulate multiple rapid collections + thread_info = MockThreadInfo(123, []) + interpreter_info = MockInterpreterInfo(0, [thread_info]) + stack_frames = [interpreter_info] + + # First collect should update display + collector.collect(stack_frames) + first_cleared = self.mock_display.cleared + + # Reset flags + self.mock_display.cleared = False + self.mock_display.refreshed = False + + # Immediate second collect should NOT update display (too soon) + collector.collect(stack_frames) + self.assertFalse( + self.mock_display.cleared, + "Display should not update too frequently", + ) + + def test_top_functions_display(self): + """Test that top functions are highlighted correctly.""" + self.collector.total_samples = 1000 + + # Create functions with different sample counts + for i in range(10): + self.collector.result[(f"file{i}.py", i * 10, f"func{i}")] = { + "direct_calls": (10 - i) * 10, # Decreasing counts + "cumulative_calls": (10 - i) * 20, + "total_rec_calls": 0, + } + + colors = self.collector._setup_colors() + self.collector._initialize_widgets(colors) + stats_list = self.collector.build_stats_list() + + self.collector.header_widget.draw_top_functions(0, 160, stats_list) + + # Top functions section should have written something + self.assertGreater(len(self.mock_display.buffer), 0) + + +class TestLiveStatsCollectorCursesIntegration(unittest.TestCase): + """Tests for curses-related functionality using mocks.""" + + def setUp(self): + """Set up mock curses screen.""" + self.mock_stdscr = mock.MagicMock() + self.mock_stdscr.getmaxyx.return_value = (40, 160) # height, width + self.mock_stdscr.getch.return_value = -1 # No input + # Save original stdout/stderr + self._orig_stdout = sys.stdout + self._orig_stderr = sys.stderr + + def tearDown(self): + """Restore stdout/stderr if changed.""" + sys.stdout = self._orig_stdout + sys.stderr = self._orig_stderr + + def test_init_curses(self): + """Test curses initialization.""" + collector = LiveStatsCollector(1000) + + with ( + mock.patch("curses.curs_set"), + mock.patch("curses.has_colors", return_value=True), + mock.patch("curses.start_color"), + mock.patch("curses.use_default_colors"), + mock.patch("builtins.open", mock.mock_open()) as mock_open_func, + ): + collector.init_curses(self.mock_stdscr) + + self.assertIsNotNone(collector.stdscr) + self.mock_stdscr.nodelay.assert_called_with(True) + self.mock_stdscr.scrollok.assert_called_with(False) + + # Clean up properly + if collector._devnull: + collector._devnull.close() + collector._saved_stdout = None + collector._saved_stderr = None + + def test_cleanup_curses(self): + """Test curses cleanup.""" + mock_display = MockDisplay() + collector = LiveStatsCollector(1000, display=mock_display) + collector.stdscr = self.mock_stdscr + + # Mock devnull file to avoid resource warnings + mock_devnull = mock.MagicMock() + mock_saved_stdout = mock.MagicMock() + mock_saved_stderr = mock.MagicMock() + + collector._devnull = mock_devnull + collector._saved_stdout = mock_saved_stdout + collector._saved_stderr = mock_saved_stderr + + with mock.patch("curses.curs_set"): + collector.cleanup_curses() + + mock_devnull.close.assert_called_once() + # Verify stdout/stderr were set back to the saved values + self.assertEqual(sys.stdout, mock_saved_stdout) + self.assertEqual(sys.stderr, mock_saved_stderr) + # Verify the saved values were cleared + self.assertIsNone(collector._saved_stdout) + self.assertIsNone(collector._saved_stderr) + self.assertIsNone(collector._devnull) + + def test_add_str_with_mock_display(self): + """Test safe_addstr with MockDisplay.""" + mock_display = MockDisplay(height=40, width=160) + collector = LiveStatsCollector(1000, display=mock_display) + colors = collector._setup_colors() + collector._initialize_widgets(colors) + + collector.header_widget.add_str(5, 10, "Test", 0) + # Verify it was added to the buffer + self.assertIn((5, 10), mock_display.buffer) + + def test_setup_colors_with_color_support(self): + """Test color setup when colors are supported.""" + mock_display = MockDisplay(height=40, width=160) + mock_display.colors_supported = True + collector = LiveStatsCollector(1000, display=mock_display) + + colors = collector._setup_colors() + + self.assertIn("header", colors) + self.assertIn("cyan", colors) + self.assertIn("yellow", colors) + self.assertIn("green", colors) + self.assertIn("magenta", colors) + self.assertIn("red", colors) + + def test_setup_colors_without_color_support(self): + """Test color setup when colors are not supported.""" + mock_display = MockDisplay(height=40, width=160) + mock_display.colors_supported = False + collector = LiveStatsCollector(1000, display=mock_display) + + colors = collector._setup_colors() + + # Should still have all keys but with fallback values + self.assertIn("header", colors) + self.assertIn("cyan", colors) + + def test_handle_input_quit(self): + """Test handling 'q' key to quit.""" + mock_display = MockDisplay() + mock_display.simulate_input(ord("q")) + collector = LiveStatsCollector(1000, display=mock_display) + + self.assertTrue(collector.running) + collector._handle_input() + self.assertFalse(collector.running) + + def test_handle_input_quit_uppercase(self): + """Test handling 'Q' key to quit.""" + mock_display = MockDisplay() + mock_display.simulate_input(ord("Q")) + collector = LiveStatsCollector(1000, display=mock_display) + + self.assertTrue(collector.running) + collector._handle_input() + self.assertFalse(collector.running) + + def test_handle_input_cycle_sort(self): + """Test handling 's' key to cycle sort.""" + mock_display = MockDisplay() + mock_display.simulate_input(ord("s")) + collector = LiveStatsCollector( + 1000, sort_by="nsamples", display=mock_display + ) + + collector._handle_input() + self.assertEqual(collector.sort_by, "sample_pct") + + def test_handle_input_cycle_sort_uppercase(self): + """Test handling 'S' key to cycle sort backward.""" + mock_display = MockDisplay() + mock_display.simulate_input(ord("S")) + collector = LiveStatsCollector( + 1000, sort_by="nsamples", display=mock_display + ) + + collector._handle_input() + self.assertEqual(collector.sort_by, "cumtime") + + def test_handle_input_no_key(self): + """Test handling when no key is pressed.""" + mock_display = MockDisplay() + collector = LiveStatsCollector(1000, display=mock_display) + + collector._handle_input() + # Should not change state + self.assertTrue(collector.running) + + +class TestLiveStatsCollectorDisplayMethods(unittest.TestCase): + """Tests for display-related methods.""" + + def setUp(self): + """Set up collector with mock display.""" + self.mock_display = MockDisplay(height=40, width=160) + self.collector = LiveStatsCollector( + 1000, pid=12345, display=self.mock_display + ) + self.collector.start_time = time.perf_counter() + + def test_show_terminal_too_small(self): + """Test terminal too small message display.""" + self.collector._show_terminal_too_small(10, 50) + # Should have written some content to the display buffer + self.assertGreater(len(self.mock_display.buffer), 0) + + def test_draw_header_info(self): + """Test drawing header information.""" + colors = { + "cyan": curses.A_BOLD, + "green": curses.A_BOLD, + "yellow": curses.A_BOLD, + "magenta": curses.A_BOLD, + } + self.collector._initialize_widgets(colors) + + line = self.collector.header_widget.draw_header_info(0, 160, 100.5) + self.assertEqual(line, 2) # Title + header info line + + def test_draw_sample_stats(self): + """Test drawing sample statistics.""" + self.collector.total_samples = 1000 + colors = {"cyan": curses.A_BOLD, "green": curses.A_BOLD} + self.collector._initialize_widgets(colors) + + line = self.collector.header_widget.draw_sample_stats(0, 160, 10.0) + self.assertEqual(line, 1) + self.assertGreater(self.collector.max_sample_rate, 0) + + def test_progress_bar_uses_target_rate(self): + """Test that progress bar uses target rate instead of max rate.""" + # Set up collector with specific sampling interval + collector = LiveStatsCollector( + 10000, pid=12345, display=self.mock_display + ) # 10ms = 100Hz target + collector.start_time = time.perf_counter() + collector.total_samples = 500 + collector.max_sample_rate = ( + 150 # Higher than target to test we don't use this + ) + + colors = {"cyan": curses.A_BOLD, "green": curses.A_BOLD} + collector._initialize_widgets(colors) + + # Clear the display buffer to capture only our progress bar content + self.mock_display.buffer.clear() + + # Draw sample stats with a known elapsed time that gives us a specific sample rate + elapsed = 10.0 # 500 samples in 10 seconds = 50 samples/second + line = collector.header_widget.draw_sample_stats(0, 160, elapsed) + + # Verify display was updated + self.assertEqual(line, 1) + self.assertGreater(len(self.mock_display.buffer), 0) + + # Verify the label shows current/target format with units instead of "max" + found_current_target_label = False + found_max_label = False + for (line_num, col), (text, attr) in self.mock_display.buffer.items(): + # Should show "50.0Hz/100.0Hz (50.0%)" since we're at 50% of target (50/100) + if "50.0Hz/100.0Hz" in text and "50.0%" in text: + found_current_target_label = True + if "max:" in text: + found_max_label = True + + self.assertTrue( + found_current_target_label, + "Should display current/target rate with percentage", + ) + self.assertFalse(found_max_label, "Should not display max rate label") + + def test_progress_bar_different_intervals(self): + """Test that progress bar adapts to different sampling intervals.""" + test_cases = [ + ( + 1000, + "1.0KHz", + "100.0Hz", + ), # 1ms interval -> 1000Hz target (1.0KHz), 100Hz current + ( + 5000, + "200.0Hz", + "100.0Hz", + ), # 5ms interval -> 200Hz target, 100Hz current + ( + 20000, + "50.0Hz", + "100.0Hz", + ), # 20ms interval -> 50Hz target, 100Hz current + ( + 100000, + "10.0Hz", + "100.0Hz", + ), # 100ms interval -> 10Hz target, 100Hz current + ] + + for ( + interval_usec, + expected_target_formatted, + expected_current_formatted, + ) in test_cases: + with self.subTest(interval=interval_usec): + collector = LiveStatsCollector( + interval_usec, display=MockDisplay() + ) + collector.start_time = time.perf_counter() + collector.total_samples = 100 + + colors = {"cyan": curses.A_BOLD, "green": curses.A_BOLD} + collector._initialize_widgets(colors) + + # Clear buffer + collector.display.buffer.clear() + + # Draw with 1 second elapsed time (gives us current rate of 100Hz) + collector.header_widget.draw_sample_stats(0, 160, 1.0) + + # Check that the current/target format appears in the display with proper units + found_current_target_format = False + for (line_num, col), ( + text, + attr, + ) in collector.display.buffer.items(): + # Looking for format like "100.0Hz/1.0KHz" or "100.0Hz/200.0Hz" + expected_format = f"{expected_current_formatted}/{expected_target_formatted}" + if expected_format in text and "%" in text: + found_current_target_format = True + break + + self.assertTrue( + found_current_target_format, + f"Should display current/target rate format with units for {interval_usec}µs interval", + ) + + def test_draw_efficiency_bar(self): + """Test drawing efficiency bar.""" + self.collector.successful_samples = 900 + self.collector.failed_samples = 100 + self.collector.total_samples = 1000 + colors = {"green": curses.A_BOLD, "red": curses.A_BOLD} + self.collector._initialize_widgets(colors) + + line = self.collector.header_widget.draw_efficiency_bar(0, 160) + self.assertEqual(line, 1) + + def test_draw_function_stats(self): + """Test drawing function statistics.""" + self.collector.result[("test.py", 10, "func1")] = { + "direct_calls": 100, + "cumulative_calls": 150, + "total_rec_calls": 0, + } + self.collector.result[("test.py", 20, "func2")] = { + "direct_calls": 0, + "cumulative_calls": 50, + "total_rec_calls": 0, + } + + stats_list = self.collector.build_stats_list() + colors = { + "cyan": curses.A_BOLD, + "green": curses.A_BOLD, + "yellow": curses.A_BOLD, + "magenta": curses.A_BOLD, + } + self.collector._initialize_widgets(colors) + + line = self.collector.header_widget.draw_function_stats( + 0, 160, stats_list + ) + self.assertEqual(line, 1) + + def test_draw_top_functions(self): + """Test drawing top functions.""" + self.collector.total_samples = 300 + self.collector.result[("test.py", 10, "hot_func")] = { + "direct_calls": 100, + "cumulative_calls": 150, + "total_rec_calls": 0, + } + + stats_list = self.collector.build_stats_list() + colors = { + "red": curses.A_BOLD, + "yellow": curses.A_BOLD, + "green": curses.A_BOLD, + } + self.collector._initialize_widgets(colors) + + line = self.collector.header_widget.draw_top_functions( + 0, 160, stats_list + ) + self.assertEqual(line, 1) + + def test_draw_column_headers(self): + """Test drawing column headers.""" + colors = { + "sorted_header": curses.A_BOLD, + "normal_header": curses.A_NORMAL, + } + self.collector._initialize_widgets(colors) + + ( + line, + show_sample_pct, + show_tottime, + show_cumul_pct, + show_cumtime, + ) = self.collector.table_widget.draw_column_headers(0, 160) + self.assertEqual(line, 1) + self.assertTrue(show_sample_pct) + self.assertTrue(show_tottime) + self.assertTrue(show_cumul_pct) + self.assertTrue(show_cumtime) + + def test_draw_column_headers_narrow_terminal(self): + """Test column headers adapt to narrow terminal.""" + colors = { + "sorted_header": curses.A_BOLD, + "normal_header": curses.A_NORMAL, + } + self.collector._initialize_widgets(colors) + + ( + line, + show_sample_pct, + show_tottime, + show_cumul_pct, + show_cumtime, + ) = self.collector.table_widget.draw_column_headers(0, 70) + self.assertEqual(line, 1) + # Some columns should be hidden on narrow terminal + self.assertFalse(show_cumul_pct) + + def test_draw_footer(self): + """Test drawing footer.""" + colors = self.collector._setup_colors() + self.collector._initialize_widgets(colors) + self.collector.footer_widget.render(38, 160) + # Should have written some content to the display buffer + self.assertGreater(len(self.mock_display.buffer), 0) + + def test_draw_progress_bar(self): + """Test progress bar drawing.""" + colors = self.collector._setup_colors() + self.collector._initialize_widgets(colors) + bar, length = self.collector.header_widget.progress_bar.render_bar( + 50, 100, 30 + ) + + self.assertIn("[", bar) + self.assertIn("]", bar) + self.assertGreater(length, 0) + # Should be roughly 50% filled + self.assertIn("█", bar) + self.assertIn("░", bar) + + +class TestLiveStatsCollectorEdgeCases(unittest.TestCase): + """Tests for edge cases and error handling.""" + + def test_very_long_function_name(self): + """Test handling of very long function names.""" + collector = LiveStatsCollector(1000) + long_name = "x" * 200 + collector.result[("test.py", 10, long_name)] = { + "direct_calls": 10, + "cumulative_calls": 20, + "total_rec_calls": 0, + } + + stats_list = collector.build_stats_list() + self.assertEqual(len(stats_list), 1) + self.assertEqual(stats_list[0]["func"][2], long_name) + + +class TestLiveStatsCollectorUpdateDisplay(unittest.TestCase): + """Tests for the _update_display method.""" + + def setUp(self): + """Set up collector with mock display.""" + self.mock_display = MockDisplay(height=40, width=160) + self.collector = LiveStatsCollector( + 1000, pid=12345, display=self.mock_display + ) + self.collector.start_time = time.perf_counter() + + def test_update_display_terminal_too_small(self): + """Test update_display when terminal is too small.""" + small_display = MockDisplay(height=10, width=50) + self.collector.display = small_display + + with mock.patch.object( + self.collector, "_show_terminal_too_small" + ) as mock_show: + self.collector._update_display() + mock_show.assert_called_once() + + def test_update_display_normal(self): + """Test normal update_display operation.""" + self.collector.total_samples = 100 + self.collector.successful_samples = 90 + self.collector.failed_samples = 10 + self.collector.result[("test.py", 10, "func")] = { + "direct_calls": 50, + "cumulative_calls": 75, + "total_rec_calls": 0, + } + + self.collector._update_display() + + self.assertTrue(self.mock_display.cleared) + self.assertTrue(self.mock_display.refreshed) + + def test_update_display_handles_exception(self): + """Test that update_display handles exceptions gracefully.""" + # Make one of the methods raise an exception + with mock.patch.object( + self.collector, + "_prepare_display_data", + side_effect=Exception("Test error"), + ): + # Should not raise an exception (it catches and logs via trace_exception) + try: + self.collector._update_display() + except Exception: + self.fail( + "_update_display should handle exceptions gracefully" + ) + + +class TestLiveCollectorWithMockDisplayHelpers(unittest.TestCase): + """Tests using the new MockDisplay helper methods.""" + + def test_verify_pid_display_with_contains(self): + """Test verifying PID is displayed using contains_text helper.""" + display = MockDisplay(height=40, width=160) + collector = LiveStatsCollector(1000, pid=99999, display=display) + collector.start_time = time.perf_counter() + collector.total_samples = 10 + + collector._update_display() + + # Use the helper method + self.assertTrue( + display.contains_text("99999"), "PID should be visible in display" + ) + + def test_verify_function_names_displayed(self): + """Test verifying function names appear in display.""" + display = MockDisplay(height=40, width=160) + collector = LiveStatsCollector(1000, pid=12345, display=display) + collector.start_time = time.perf_counter() + + collector.total_samples = 100 + collector.result[("mymodule.py", 42, "my_special_function")] = { + "direct_calls": 50, + "cumulative_calls": 75, + "total_rec_calls": 0, + } + + collector._update_display() + + # Verify function name appears + self.assertTrue( + display.contains_text("my_special_function"), + "Function name should be visible", + ) + + def test_get_all_lines_full_display(self): + """Test getting all lines from a full display render.""" + display = MockDisplay(height=40, width=160) + collector = LiveStatsCollector(1000, pid=12345, display=display) + collector.start_time = time.perf_counter() + collector.total_samples = 100 + + collector._update_display() + + lines = display.get_all_lines() + + # Should have multiple lines of content + self.assertGreater(len(lines), 5) + + # Should have header content + self.assertTrue(any("PID" in line for line in lines)) + + +@requires_remote_subprocess_debugging() +class TestLiveModeErrors(unittest.TestCase): + """Tests running error commands in the live mode fails gracefully.""" + + class QuitWhenFinishedDisplay(MockDisplay): + def __init__(self, collector): + super().__init__() + self.collector = collector + + def get_input(self): + ch = super().get_input() + if ch != -1: + return ch + # Sampling only stops once the target process has exited, at + # which point the collector is marked finished. Quit then so the + # run can surface the target's stderr. We must not rely on the + # target's pid still being signalable: once it exits it lingers + # as a zombie (it is reaped after sample_live returns), so a + # liveness check would never observe it gone and would hang. + if self.collector.finished: + return ord('q') + return -1 + + def mock_curses_wrapper(self, func): + func(mock.MagicMock()) + + def mock_init_curses_side_effect(self, n_times, mock_self, stdscr): + mock_self.display = self.QuitWhenFinishedDisplay(mock_self) + # Feed non-input events so live mode keeps polling while the target + # process is still running; once it exits the display quits on its own. + for _ in range(n_times): + mock_self.display.simulate_input(-1) + + def test_run_failed_module_live(self): + """Test that running a existing module that fails exits with clean error.""" + + args = [ + "profiling.sampling.cli", "run", "--live", "-m", "test", + "test_asdasd" + ] + + with ( + mock.patch( + 'profiling.sampling.live_collector.collector.LiveStatsCollector.init_curses', + autospec=True, + side_effect=functools.partial(self.mock_init_curses_side_effect, 1000) + ), + mock.patch('curses.wrapper', side_effect=self.mock_curses_wrapper), + mock.patch("sys.argv", args), + mock.patch('sys.stderr', new=io.StringIO()) as fake_stderr + ): + main() + self.assertIn( + 'test test_asdasd crashed -- Traceback (most recent call last):', + fake_stderr.getvalue() + ) + + def test_run_failed_script_live(self): + """Test that running a failing script exits with clean error.""" + script = tempfile.NamedTemporaryFile(suffix=".py") + self.addCleanup(close_and_unlink, script) + script.write(b'1/0\n') + script.seek(0) + + args = ["profiling.sampling.cli", "run", "--live", script.name] + + with ( + mock.patch( + 'profiling.sampling.live_collector.collector.LiveStatsCollector.init_curses', + autospec=True, + side_effect=functools.partial(self.mock_init_curses_side_effect, 200) + ), + mock.patch('curses.wrapper', side_effect=self.mock_curses_wrapper), + mock.patch("sys.argv", args), + mock.patch('sys.stderr', new=io.StringIO()) as fake_stderr + ): + main() + stderr = fake_stderr.getvalue() + self.assertIn('ZeroDivisionError', stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_modes.py b/Lib/test/test_profiling/test_sampling_profiler/test_modes.py new file mode 100644 index 00000000000..0b38fb4ad4b --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_modes.py @@ -0,0 +1,587 @@ +"""Tests for sampling profiler mode filtering (CPU and GIL modes).""" + +import io +import unittest +from unittest import mock + +try: + import _remote_debugging # noqa: F401 + import profiling.sampling + import profiling.sampling.sample + from profiling.sampling.pstats_collector import PstatsCollector + from profiling.sampling.cli import main, _parse_mode + from profiling.sampling.constants import PROFILING_MODE_EXCEPTION + from _remote_debugging import ( + THREAD_STATUS_HAS_GIL, + THREAD_STATUS_ON_CPU, + ) +except ImportError: + raise unittest.SkipTest( + "Test only runs when _remote_debugging is available" + ) + +from test.support import requires_remote_subprocess_debugging + +from .helpers import test_subprocess +from .mocks import MockFrameInfo, MockInterpreterInfo + + +@requires_remote_subprocess_debugging() +class TestCpuModeFiltering(unittest.TestCase): + """Test CPU mode filtering functionality (--mode=cpu).""" + + def test_mode_validation(self): + """Test that CLI validates mode choices correctly.""" + # Invalid mode choice should raise SystemExit + test_args = [ + "profiling.sampling.cli", + "attach", + "12345", + "--mode", + "invalid", + ] + + with ( + mock.patch("sys.argv", test_args), + mock.patch("sys.stderr", io.StringIO()) as mock_stderr, + self.assertRaises(SystemExit) as cm, + ): + main() + + self.assertEqual(cm.exception.code, 2) # argparse error + error_msg = mock_stderr.getvalue() + self.assertIn("invalid choice", error_msg) + + def test_frames_filtered_with_skip_idle(self): + """Test that frames are actually filtered when skip_idle=True.""" + # Create mock frames with different thread statuses + class MockThreadInfoWithStatus: + def __init__(self, thread_id, frame_info, status): + self.thread_id = thread_id + self.frame_info = frame_info + self.status = status + + # Create test data: active thread (HAS_GIL | ON_CPU), idle thread (neither), and another active thread + ACTIVE_STATUS = ( + THREAD_STATUS_HAS_GIL | THREAD_STATUS_ON_CPU + ) # Has GIL and on CPU + IDLE_STATUS = 0 # Neither has GIL nor on CPU + + test_frames = [ + MockInterpreterInfo( + 0, + [ + MockThreadInfoWithStatus( + 1, + [MockFrameInfo("active1.py", 10, "active_func1")], + ACTIVE_STATUS, + ), + MockThreadInfoWithStatus( + 2, + [MockFrameInfo("idle.py", 20, "idle_func")], + IDLE_STATUS, + ), + MockThreadInfoWithStatus( + 3, + [MockFrameInfo("active2.py", 30, "active_func2")], + ACTIVE_STATUS, + ), + ], + ) + ] + + # Test with skip_idle=True - should only process running threads + collector_skip = PstatsCollector( + sample_interval_usec=1000, skip_idle=True + ) + collector_skip.collect(test_frames) + + # Should only have functions from running threads (status 0) + active1_key = ("active1.py", 10, "active_func1") + active2_key = ("active2.py", 30, "active_func2") + idle_key = ("idle.py", 20, "idle_func") + + self.assertIn(active1_key, collector_skip.result) + self.assertIn(active2_key, collector_skip.result) + self.assertNotIn( + idle_key, collector_skip.result + ) # Idle thread should be filtered out + + # Test with skip_idle=False - should process all threads + collector_no_skip = PstatsCollector( + sample_interval_usec=1000, skip_idle=False + ) + collector_no_skip.collect(test_frames) + + # Should have functions from all threads + self.assertIn(active1_key, collector_no_skip.result) + self.assertIn(active2_key, collector_no_skip.result) + self.assertIn( + idle_key, collector_no_skip.result + ) # Idle thread should be included + + def test_cpu_mode_integration_filtering(self): + """Integration test: CPU mode should only capture active threads, not idle ones.""" + # Script with one mostly-idle thread and one CPU-active thread + cpu_vs_idle_script = """ +import time +import threading + +cpu_ready = threading.Event() + +def idle_worker(): + time.sleep(999999) + +def cpu_active_worker(): + cpu_ready.set() + x = 1 + while True: + x += 1 + +idle_thread = threading.Thread(target=idle_worker) +cpu_thread = threading.Thread(target=cpu_active_worker) +idle_thread.start() +cpu_thread.start() +cpu_ready.wait() +_test_sock.sendall(b"working") +idle_thread.join() +cpu_thread.join() +""" + with test_subprocess(cpu_vs_idle_script, wait_for_working=True) as subproc: + + with ( + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = PstatsCollector(sample_interval_usec=5000, skip_idle=True) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=2.0, + mode=1, # CPU mode + all_threads=True, + ) + collector.print_stats(show_summary=False, mode=1) + + cpu_mode_output = captured_output.getvalue() + + # Test wall-clock mode (mode=0) - should capture both functions + with ( + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = PstatsCollector(sample_interval_usec=5000, skip_idle=False) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=2.0, + mode=0, # Wall-clock mode + all_threads=True, + ) + collector.print_stats(show_summary=False) + + wall_mode_output = captured_output.getvalue() + + # Verify both modes captured samples + self.assertIn("Captured", cpu_mode_output) + self.assertIn("samples", cpu_mode_output) + self.assertIn("Captured", wall_mode_output) + self.assertIn("samples", wall_mode_output) + + # CPU mode should strongly favor cpu_active_worker over mostly_idle_worker + self.assertIn("cpu_active_worker", cpu_mode_output) + self.assertNotIn("idle_worker", cpu_mode_output) + + # Wall-clock mode should capture both types of work + self.assertIn("cpu_active_worker", wall_mode_output) + self.assertIn("idle_worker", wall_mode_output) + + def test_cpu_mode_with_no_samples(self): + """Test that CPU mode handles no samples gracefully when no samples are collected.""" + # Mock a collector that returns empty stats + mock_collector = PstatsCollector(sample_interval_usec=5000, skip_idle=True) + mock_collector.stats = {} + + with ( + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + mock.patch( + "profiling.sampling.sample.SampleProfiler" + ) as mock_profiler_class, + ): + mock_profiler = mock.MagicMock() + mock_profiler_class.return_value = mock_profiler + + profiling.sampling.sample.sample( + 12345, # dummy PID + mock_collector, + duration_sec=0.5, + mode=1, # CPU mode + all_threads=True, + ) + + mock_collector.print_stats(show_summary=False, mode=1) + + output = captured_output.getvalue() + + # Should see the "No samples were collected" message + self.assertIn("No samples were collected", output) + self.assertIn("CPU mode", output) + + +@requires_remote_subprocess_debugging() +class TestGilModeFiltering(unittest.TestCase): + """Test GIL mode filtering functionality (--mode=gil).""" + + def test_gil_mode_validation(self): + """Test that CLI accepts gil mode choice correctly.""" + + test_args = [ + "profiling.sampling.cli", + "attach", + "12345", + "--mode", + "gil", + ] + + with ( + mock.patch("sys.argv", test_args), + mock.patch("profiling.sampling.cli._is_process_running", return_value=True), + mock.patch("profiling.sampling.cli.sample") as mock_sample, + ): + try: + main() + except (SystemExit, OSError, RuntimeError): + pass # Expected due to invalid PID + + # Should have attempted to call sample with mode=2 (GIL mode) + mock_sample.assert_called_once() + call_args = mock_sample.call_args + # Check the mode parameter (should be in kwargs) + self.assertEqual(call_args.kwargs.get("mode"), 2) # PROFILING_MODE_GIL + + def test_gil_mode_sample_function_call(self): + """Test that sample() function correctly uses GIL mode.""" + with ( + mock.patch( + "profiling.sampling.sample.SampleProfiler" + ) as mock_profiler, + ): + # Mock the profiler instance + mock_instance = mock.Mock() + mock_profiler.return_value = mock_instance + + # Create a real collector instance + collector = PstatsCollector(sample_interval_usec=1000, skip_idle=True) + + # Call sample with GIL mode + profiling.sampling.sample.sample( + 12345, + collector, + mode=2, # PROFILING_MODE_GIL + duration_sec=1, + ) + + # Verify SampleProfiler was created with correct mode + mock_profiler.assert_called_once() + call_args = mock_profiler.call_args + self.assertEqual(call_args[1]["mode"], 2) # mode parameter + + # Verify profiler.sample was called + mock_instance.sample.assert_called_once() + + def test_gil_mode_cli_argument_parsing(self): + """Test CLI argument parsing for GIL mode with various options.""" + + test_args = [ + "profiling.sampling.cli", + "attach", + "12345", + "--mode", + "gil", + "-r", + "2000", + "-d", + "5", + ] + + with ( + mock.patch("sys.argv", test_args), + mock.patch("profiling.sampling.cli._is_process_running", return_value=True), + mock.patch("profiling.sampling.cli.sample") as mock_sample, + ): + try: + main() + except (SystemExit, OSError, RuntimeError): + pass # Expected due to invalid PID + + # Verify all arguments were parsed correctly + mock_sample.assert_called_once() + call_args = mock_sample.call_args + self.assertEqual(call_args.kwargs.get("mode"), 2) # GIL mode + self.assertEqual(call_args.kwargs.get("duration_sec"), 5) + + def test_gil_mode_integration_behavior(self): + """Integration test: GIL mode should capture GIL-holding threads.""" + # Create a test script with GIL-releasing operations + gil_test_script = """ +import time +import threading + +gil_ready = threading.Event() + +def gil_releasing_work(): + time.sleep(999999) + +def gil_holding_work(): + gil_ready.set() + x = 1 + while True: + x += 1 + +idle_thread = threading.Thread(target=gil_releasing_work) +cpu_thread = threading.Thread(target=gil_holding_work) +idle_thread.start() +cpu_thread.start() +gil_ready.wait() +_test_sock.sendall(b"working") +idle_thread.join() +cpu_thread.join() +""" + with test_subprocess(gil_test_script, wait_for_working=True) as subproc: + + with ( + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = PstatsCollector(sample_interval_usec=5000, skip_idle=True) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=2.0, + mode=2, # GIL mode + all_threads=True, + ) + collector.print_stats(show_summary=False) + + gil_mode_output = captured_output.getvalue() + + # Test wall-clock mode for comparison + with ( + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = PstatsCollector(sample_interval_usec=5000, skip_idle=False) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=0.5, + mode=0, # Wall-clock mode + all_threads=True, + ) + collector.print_stats(show_summary=False) + + wall_mode_output = captured_output.getvalue() + + # GIL mode should primarily capture GIL-holding work + # (Note: actual behavior depends on threading implementation) + self.assertIn("gil_holding_work", gil_mode_output) + + # Wall-clock mode should capture both types of work + self.assertIn("gil_holding_work", wall_mode_output) + + def test_mode_constants_are_defined(self): + """Test that all profiling mode constants are properly defined.""" + self.assertEqual(profiling.sampling.sample.PROFILING_MODE_WALL, 0) + self.assertEqual(profiling.sampling.sample.PROFILING_MODE_CPU, 1) + self.assertEqual(profiling.sampling.sample.PROFILING_MODE_GIL, 2) + + def test_parse_mode_function(self): + """Test the _parse_mode function with all valid modes.""" + self.assertEqual(_parse_mode("wall"), 0) + self.assertEqual(_parse_mode("cpu"), 1) + self.assertEqual(_parse_mode("gil"), 2) + self.assertEqual(_parse_mode("exception"), 4) + + # Test invalid mode raises KeyError + with self.assertRaises(KeyError): + _parse_mode("invalid") + + +@requires_remote_subprocess_debugging() +class TestExceptionModeFiltering(unittest.TestCase): + """Test exception mode filtering functionality (--mode=exception).""" + + def test_exception_mode_validation(self): + """Test that CLI accepts exception mode choice correctly.""" + + test_args = [ + "profiling.sampling.cli", + "attach", + "12345", + "--mode", + "exception", + ] + + with ( + mock.patch("sys.argv", test_args), + mock.patch("profiling.sampling.cli._is_process_running", return_value=True), + mock.patch("profiling.sampling.cli.sample") as mock_sample, + ): + try: + main() + except (SystemExit, OSError, RuntimeError): + pass # Expected due to invalid PID + + # Should have attempted to call sample with mode=4 (exception mode) + mock_sample.assert_called_once() + call_args = mock_sample.call_args + # Check the mode parameter (should be in kwargs) + self.assertEqual(call_args.kwargs.get("mode"), 4) # PROFILING_MODE_EXCEPTION + + def test_exception_mode_sample_function_call(self): + """Test that sample() function correctly uses exception mode.""" + with ( + mock.patch( + "profiling.sampling.sample.SampleProfiler" + ) as mock_profiler, + ): + # Mock the profiler instance + mock_instance = mock.Mock() + mock_profiler.return_value = mock_instance + + # Create a real collector instance + collector = PstatsCollector(sample_interval_usec=1000, skip_idle=True) + + # Call sample with exception mode + profiling.sampling.sample.sample( + 12345, + collector, + mode=4, # PROFILING_MODE_EXCEPTION + duration_sec=1, + ) + + # Verify SampleProfiler was created with correct mode + mock_profiler.assert_called_once() + call_args = mock_profiler.call_args + self.assertEqual(call_args[1]["mode"], 4) # mode parameter + + # Verify profiler.sample was called + mock_instance.sample.assert_called_once() + + def test_exception_mode_cli_argument_parsing(self): + """Test CLI argument parsing for exception mode with various options.""" + + test_args = [ + "profiling.sampling.cli", + "attach", + "12345", + "--mode", + "exception", + "-r", + "2000", + "-d", + "5", + ] + + with ( + mock.patch("sys.argv", test_args), + mock.patch("profiling.sampling.cli._is_process_running", return_value=True), + mock.patch("profiling.sampling.cli.sample") as mock_sample, + ): + try: + main() + except (SystemExit, OSError, RuntimeError): + pass # Expected due to invalid PID + + # Verify all arguments were parsed correctly + mock_sample.assert_called_once() + call_args = mock_sample.call_args + self.assertEqual(call_args.kwargs.get("mode"), 4) # exception mode + self.assertEqual(call_args.kwargs.get("duration_sec"), 5) + + def test_exception_mode_constants_are_defined(self): + """Test that exception mode constant is properly defined.""" + self.assertEqual(PROFILING_MODE_EXCEPTION, 4) + + def test_exception_mode_integration_filtering(self): + """Integration test: Exception mode should only capture threads with active exceptions.""" + # Script with one thread handling an exception and one normal thread + exception_vs_normal_script = """ +import time +import threading + +exception_ready = threading.Event() + +def normal_worker(): + x = 0 + while True: + x += 1 + +def exception_handling_worker(): + try: + raise ValueError("test exception") + except ValueError: + # Signal AFTER entering except block, then do CPU work + exception_ready.set() + x = 0 + while True: + x += 1 + +normal_thread = threading.Thread(target=normal_worker) +exception_thread = threading.Thread(target=exception_handling_worker) +normal_thread.start() +exception_thread.start() +exception_ready.wait() +_test_sock.sendall(b"working") +normal_thread.join() +exception_thread.join() +""" + with test_subprocess(exception_vs_normal_script, wait_for_working=True) as subproc: + + with ( + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = PstatsCollector(sample_interval_usec=5000, skip_idle=True) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=2.0, + mode=4, # Exception mode + all_threads=True, + ) + collector.print_stats(show_summary=False, mode=4) + + exception_mode_output = captured_output.getvalue() + + # Test wall-clock mode (mode=0) - should capture both functions + with ( + io.StringIO() as captured_output, + mock.patch("sys.stdout", captured_output), + ): + collector = PstatsCollector(sample_interval_usec=5000, skip_idle=False) + profiling.sampling.sample.sample( + subproc.process.pid, + collector, + duration_sec=2.0, + mode=0, # Wall-clock mode + all_threads=True, + ) + collector.print_stats(show_summary=False) + + wall_mode_output = captured_output.getvalue() + + # Verify both modes captured samples + self.assertIn("Captured", exception_mode_output) + self.assertIn("samples", exception_mode_output) + self.assertIn("Captured", wall_mode_output) + self.assertIn("samples", wall_mode_output) + + # Exception mode should strongly favor exception_handling_worker over normal_worker + self.assertIn("exception_handling_worker", exception_mode_output) + self.assertNotIn("normal_worker", exception_mode_output) + + # Wall-clock mode should capture both types of work + self.assertIn("exception_handling_worker", wall_mode_output) + self.assertIn("normal_worker", wall_mode_output) diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_profiler.py b/Lib/test/test_profiling/test_sampling_profiler/test_profiler.py new file mode 100644 index 00000000000..b1fbfb5a0de --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_profiler.py @@ -0,0 +1,888 @@ +"""Tests for sampling profiler core functionality.""" + +import contextlib +import io +import re +import types +from unittest import mock +import unittest + +try: + import _remote_debugging # noqa: F401 + from profiling.sampling.constants import PROFILING_MODE_ALL, PROFILING_MODE_WALL + from profiling.sampling.sample import SampleProfiler, dump_stack + from profiling.sampling.pstats_collector import PstatsCollector +except ImportError: + raise unittest.SkipTest( + "Test only runs when _remote_debugging is available" + ) + +from test.support import force_colorized, force_not_colorized_test_class + + +def print_sampled_stats(stats, sort=-1, limit=None, show_summary=True, sample_interval_usec=100): + """Helper function to maintain compatibility with old test API. + + This wraps the new PstatsCollector.print_stats() API to work with the + existing test infrastructure. + """ + # Create a mock collector that populates stats correctly + collector = PstatsCollector(sample_interval_usec=sample_interval_usec) + + # Override create_stats to populate self.stats with the provided stats + def mock_create_stats(): + collector.stats = stats.stats + collector.create_stats = mock_create_stats + + # Call the new print_stats method + collector.print_stats(sort=sort, limit=limit, show_summary=show_summary) + + +class TestSampleProfiler(unittest.TestCase): + """Test the SampleProfiler class.""" + + def test_sample_profiler_initialization(self): + """Test SampleProfiler initialization with various parameters.""" + + # Mock RemoteUnwinder to avoid permission issues + with mock.patch( + "_remote_debugging.RemoteUnwinder" + ) as mock_unwinder_class: + mock_unwinder_class.return_value = mock.MagicMock() + + # Test basic initialization + profiler = SampleProfiler( + pid=12345, sample_interval_usec=1000, all_threads=False + ) + self.assertEqual(profiler.pid, 12345) + self.assertEqual(profiler.sample_interval_usec, 1000) + self.assertEqual(profiler.all_threads, False) + + # Test with all_threads=True + profiler = SampleProfiler( + pid=54321, sample_interval_usec=5000, all_threads=True + ) + self.assertEqual(profiler.pid, 54321) + self.assertEqual(profiler.sample_interval_usec, 5000) + self.assertEqual(profiler.all_threads, True) + + @staticmethod + @contextlib.contextmanager + def _patched_unwinder(): + """Yield a namespace exposing the mock unwinder ``instance`` and ``cls``.""" + instance = mock.MagicMock() + with mock.patch( + "_remote_debugging.RemoteUnwinder", return_value=instance + ) as cls: + yield types.SimpleNamespace(instance=instance, cls=cls) + + def test_dump_stack_uses_single_unwinder_snapshot(self): + stack_frames = [mock.sentinel.stack_frames] + with self._patched_unwinder() as u: + u.instance.get_stack_trace.return_value = stack_frames + result = dump_stack(12345) + + self.assertIs(result, stack_frames) + self.assertEqual(u.cls.call_args.kwargs["mode"], PROFILING_MODE_ALL) + u.instance.get_stack_trace.assert_called_once_with() + + def test_dump_stack_returns_empty_async_snapshot(self): + with self._patched_unwinder() as u: + u.instance.get_async_stack_trace.return_value = [] + result = dump_stack(12345, async_aware="running") + + self.assertEqual(result, []) + u.instance.get_async_stack_trace.assert_called_once_with() + u.instance.get_stack_trace.assert_not_called() + + def test_dump_stack_async_all_uses_all_awaited_by(self): + stack_frames = [mock.sentinel.awaited_info] + with self._patched_unwinder() as u: + u.instance.get_all_awaited_by.return_value = stack_frames + result = dump_stack(12345, async_aware="all") + + self.assertIs(result, stack_frames) + u.instance.get_all_awaited_by.assert_called_once_with() + u.instance.get_stack_trace.assert_not_called() + u.instance.get_async_stack_trace.assert_not_called() + + def test_dump_stack_passes_unwinder_options(self): + with self._patched_unwinder() as u: + u.instance.get_stack_trace.return_value = [] + dump_stack( + 12345, + all_threads=True, + native=True, + gc=False, + opcodes=True, + ) + + call_kwargs = u.cls.call_args.kwargs + self.assertTrue(call_kwargs["all_threads"]) + self.assertEqual(call_kwargs["mode"], PROFILING_MODE_ALL) + self.assertTrue(call_kwargs["native"]) + self.assertFalse(call_kwargs["gc"]) + self.assertTrue(call_kwargs["opcodes"]) + self.assertFalse(call_kwargs["skip_non_matching_threads"]) + self.assertTrue(call_kwargs["cache_frames"]) + + def test_dump_stack_wall_mode_skips_non_matching_threads(self): + with self._patched_unwinder() as u: + u.instance.get_stack_trace.return_value = [] + dump_stack(12345, mode=PROFILING_MODE_WALL) + + self.assertTrue(u.cls.call_args.kwargs["skip_non_matching_threads"]) + + def test_dump_stack_blocking_pauses_and_resumes_threads(self): + stack_frames = [mock.sentinel.stack_frames] + with self._patched_unwinder() as u: + u.instance.get_stack_trace.return_value = stack_frames + result = dump_stack(12345, blocking=True) + + self.assertIs(result, stack_frames) + u.instance.pause_threads.assert_called_once_with() + u.instance.resume_threads.assert_called_once_with() + + def test_dump_stack_blocking_resumes_threads_after_failure(self): + with self._patched_unwinder() as u: + u.instance.get_stack_trace.side_effect = RuntimeError("boom") + with self.assertRaises(RuntimeError): + dump_stack(12345, blocking=True) + + u.instance.pause_threads.assert_called_once_with() + u.instance.resume_threads.assert_called_once_with() + + def test_sample_profiler_sample_method_timing(self): + """Test that the sample method respects duration and handles timing correctly.""" + + # Mock the unwinder to avoid needing a real process + mock_unwinder = mock.MagicMock() + mock_unwinder.get_stack_trace.return_value = [ + ( + 1, + [ + mock.MagicMock( + filename="test.py", lineno=10, funcname="test_func" + ) + ], + ) + ] + + with mock.patch( + "_remote_debugging.RemoteUnwinder" + ) as mock_unwinder_class: + mock_unwinder_class.return_value = mock_unwinder + + profiler = SampleProfiler( + pid=12345, sample_interval_usec=100000, all_threads=False + ) # 100ms interval + + # Mock collector + mock_collector = mock.MagicMock() + + # Mock time to control the sampling loop + start_time = 1000.0 + times = [ + start_time + i * 0.1 for i in range(12) + ] # 0, 0.1, 0.2, ..., 1.1 seconds + + with mock.patch("time.perf_counter", side_effect=times): + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + profiler.sample(mock_collector, duration_sec=1) + + result = output.getvalue() + + # Should have captured approximately 10 samples (1 second / 0.1 second interval) + self.assertIn("Captured", result) + self.assertIn("samples", result) + + # Verify collector was called multiple times + total_weight = sum( + len(c.kwargs.get("timestamps_us") or [None]) + for c in mock_collector.collect.call_args_list + ) + self.assertGreaterEqual(total_weight, 5) + self.assertLessEqual(total_weight, 11) + + def test_sample_profiler_does_not_buffer_non_aggregating_collectors(self): + """Test that non-aggregating collectors get each sample immediately.""" + + stack_frames = [mock.sentinel.stack_frames] + mock_collector = mock.MagicMock() + mock_collector.aggregating = False + + with self._patched_unwinder() as u: + u.instance.get_stack_trace.return_value = stack_frames + + manager = mock.Mock() + manager.attach_mock(u.instance.get_stack_trace, "unwind") + manager.attach_mock(mock_collector.collect, "collect") + + profiler = SampleProfiler( + pid=12345, sample_interval_usec=10000, all_threads=False + ) + + times = [0.0, 0.01, 0.011, 0.02, 0.03] + with mock.patch("time.perf_counter", side_effect=times): + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + profiler.sample(mock_collector, duration_sec=0.025) + + self.assertEqual( + manager.mock_calls, + [ + mock.call.unwind(), + mock.call.collect(stack_frames), + mock.call.unwind(), + mock.call.collect(stack_frames), + ], + ) + + def test_sample_profiler_flushes_aggregated_batches_at_limit(self): + """Test that aggregating collectors flush after MAX_PENDING_SAMPLES samples.""" + + stack_frames = [mock.sentinel.stack_frames] + mock_collector = mock.MagicMock() + mock_collector.aggregating = True + + with self._patched_unwinder() as u: + u.instance.get_stack_trace.return_value = stack_frames + + profiler = SampleProfiler( + pid=12345, sample_interval_usec=10000, all_threads=False + ) + + times = [ + 0.0, + 0.01, 0.011, + 0.02, 0.021, + 0.03, 0.031, + 0.04, 0.041, + 0.05, 0.051, + ] + with mock.patch("profiling.sampling.sample.MAX_PENDING_SAMPLES", 2): + with mock.patch("time.perf_counter", side_effect=times): + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + profiler.sample(mock_collector, duration_sec=0.045) + + batches = [ + (c.args[0], len(c.kwargs["timestamps_us"])) + for c in mock_collector.collect.call_args_list + ] + self.assertEqual( + batches, + [(stack_frames, 2), (stack_frames, 2), (stack_frames, 1)], + ) + + def test_sample_profiler_error_handling(self): + """Test that the sample method handles errors gracefully.""" + + # Mock unwinder that raises errors + mock_unwinder = mock.MagicMock() + error_sequence = [ + RuntimeError("Process died"), + [ + ( + 1, + [ + mock.MagicMock( + filename="test.py", lineno=10, funcname="test_func" + ) + ], + ) + ], + UnicodeDecodeError("utf-8", b"", 0, 1, "invalid"), + [ + ( + 1, + [ + mock.MagicMock( + filename="test.py", + lineno=20, + funcname="test_func2", + ) + ], + ) + ], + OSError("Permission denied"), + ] + mock_unwinder.get_stack_trace.side_effect = error_sequence + + with mock.patch( + "_remote_debugging.RemoteUnwinder" + ) as mock_unwinder_class: + mock_unwinder_class.return_value = mock_unwinder + + profiler = SampleProfiler( + pid=12345, sample_interval_usec=10000, all_threads=False + ) + + mock_collector = mock.MagicMock() + + # Control timing to run exactly 5 samples + times = [0.0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06] + + with mock.patch("time.perf_counter", side_effect=times): + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + profiler.sample(mock_collector, duration_sec=0.05) + + result = output.getvalue() + + # Should report error rate + self.assertIn("Error rate:", result) + self.assertIn("%", result) + + # Collector should have been called only for successful samples (should be > 0) + self.assertGreater(mock_collector.collect.call_count, 0) + self.assertLessEqual(mock_collector.collect.call_count, 3) + + def test_sample_profiler_missed_samples_warning(self): + """Test that the profiler warns about missed samples when sampling is too slow.""" + + mock_unwinder = mock.MagicMock() + mock_unwinder.get_stack_trace.return_value = [ + ( + 1, + [ + mock.MagicMock( + filename="test.py", lineno=10, funcname="test_func" + ) + ], + ) + ] + + with mock.patch( + "_remote_debugging.RemoteUnwinder" + ) as mock_unwinder_class: + mock_unwinder_class.return_value = mock_unwinder + + # Use very short interval that we'll miss + profiler = SampleProfiler( + pid=12345, sample_interval_usec=1000, all_threads=False + ) # 1ms interval + + mock_collector = mock.MagicMock() + + # Simulate slow sampling where we miss many samples + times = [ + 0.0, + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + ] # Extra time points to avoid StopIteration + + with mock.patch("time.perf_counter", side_effect=times): + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + profiler.sample(mock_collector, duration_sec=0.5) + + result = output.getvalue() + + # Should warn about missed samples + self.assertIn("Warning: missed", result) + self.assertIn("samples from the expected total", result) + + def test_sample_profiler_keyboard_interrupt(self): + mock_unwinder = mock.MagicMock() + mock_unwinder.get_stack_trace.side_effect = [ + [ + ( + 1, + [ + mock.MagicMock( + filename="test.py", lineno=10, funcname="test_func" + ) + ], + ) + ], + KeyboardInterrupt(), + ] + + with mock.patch( + "_remote_debugging.RemoteUnwinder" + ) as mock_unwinder_class: + mock_unwinder_class.return_value = mock_unwinder + profiler = SampleProfiler( + pid=12345, sample_interval_usec=10000, all_threads=False + ) + mock_collector = mock.MagicMock() + times = [0.0, 0.01, 0.02, 0.03, 0.04] + with mock.patch("time.perf_counter", side_effect=times): + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + try: + profiler.sample(mock_collector, duration_sec=1.0) + except KeyboardInterrupt: + self.fail( + "KeyboardInterrupt was not handled by the profiler" + ) + result = output.getvalue() + self.assertIn("Interrupted by user.", result) + self.assertIn("Captured", result) + self.assertIn("samples", result) + self.assertNotIn("Warning: missed", result) + + +@force_not_colorized_test_class +class TestPrintSampledStats(unittest.TestCase): + """Test the print_sampled_stats function.""" + + def setUp(self): + """Set up test data.""" + # Mock stats data + self.mock_stats = mock.MagicMock() + self.mock_stats.stats = { + ("file1.py", 10, "func1"): ( + 100, + 100, + 0.5, + 0.5, + {}, + ), # cc, nc, tt, ct, callers + ("file2.py", 20, "func2"): (50, 50, 0.25, 0.3, {}), + ("file3.py", 30, "func3"): (200, 200, 1.5, 2.0, {}), + ("file4.py", 40, "func4"): ( + 10, + 10, + 0.001, + 0.001, + {}, + ), # millisecond range + ("file5.py", 50, "func5"): ( + 5, + 5, + 0.000001, + 0.000002, + {}, + ), # microsecond range + } + + def test_print_sampled_stats_basic(self): + """Test basic print_sampled_stats functionality.""" + + # Capture output + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats(self.mock_stats, sample_interval_usec=100) + + result = output.getvalue() + + # Check header is present + self.assertIn("Profile Stats:", result) + self.assertIn("nsamples", result) + self.assertIn("tottime", result) + self.assertIn("cumtime", result) + + # Check functions are present + self.assertIn("func1", result) + self.assertIn("func2", result) + self.assertIn("func3", result) + self.assertNotIn("\x1b[", result) + + @force_colorized + def test_print_sampled_stats_colorized(self): + with io.StringIO() as output, mock.patch("sys.stdout", output): + print_sampled_stats(self.mock_stats) + result = output.getvalue() + + self.assertIn("\x1b[1;34mProfile Stats:", result) + + def test_print_sampled_stats_sorting(self): + """Test different sorting options.""" + + # Test sort by calls + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats( + self.mock_stats, sort=0, sample_interval_usec=100 + ) + + result = output.getvalue() + lines = result.strip().split("\n") + + # Find the data lines (skip header) + data_lines = [l for l in lines if "file" in l and ".py" in l] + # func3 should be first (200 calls) + self.assertIn("func3", data_lines[0]) + + # Test sort by time + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats( + self.mock_stats, sort=1, sample_interval_usec=100 + ) + + result = output.getvalue() + lines = result.strip().split("\n") + + data_lines = [l for l in lines if "file" in l and ".py" in l] + # func3 should be first (1.5s time) + self.assertIn("func3", data_lines[0]) + + def test_print_sampled_stats_limit(self): + """Test limiting output rows.""" + + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats( + self.mock_stats, limit=2, sample_interval_usec=100 + ) + + result = output.getvalue() + + # Count function entries in the main stats section (not in summary) + lines = result.split("\n") + # Find where the main stats section ends (before summary) + main_section_lines = [] + for line in lines: + if "Summary of Interesting Functions:" in line: + break + main_section_lines.append(line) + + # Count function entries only in main section + func_count = sum( + 1 + for line in main_section_lines + if "func" in line and ".py" in line + ) + self.assertEqual(func_count, 2) + + def test_print_sampled_stats_time_units(self): + """Test proper time unit selection.""" + + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats(self.mock_stats, sample_interval_usec=100) + + result = output.getvalue() + + # Should use seconds for the header since max time is > 1s + self.assertIn("tottime (s)", result) + self.assertIn("cumtime (s)", result) + + # Test with only microsecond-range times + micro_stats = mock.MagicMock() + micro_stats.stats = { + ("file1.py", 10, "func1"): (100, 100, 0.000005, 0.000010, {}), + } + + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats(micro_stats, sample_interval_usec=100) + + result = output.getvalue() + + # Should use microseconds + self.assertIn("tottime (μs)", result) + self.assertIn("cumtime (μs)", result) + + def test_print_sampled_stats_summary(self): + """Test summary section generation.""" + + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats( + self.mock_stats, + show_summary=True, + sample_interval_usec=100, + ) + + result = output.getvalue() + + # Check summary sections are present + self.assertIn("Summary of Interesting Functions:", result) + self.assertIn( + "Functions with Highest Direct/Cumulative Ratio (Hot Spots):", + result, + ) + self.assertIn( + "Functions with Highest Call Frequency (Indirect Calls):", result + ) + self.assertIn( + "Functions with Highest Call Magnification (Cumulative/Direct):", + result, + ) + + def test_print_sampled_stats_no_summary(self): + """Test disabling summary output.""" + + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats( + self.mock_stats, + show_summary=False, + sample_interval_usec=100, + ) + + result = output.getvalue() + + # Summary should not be present + self.assertNotIn("Summary of Interesting Functions:", result) + + def test_print_sampled_stats_empty_stats(self): + """Test with empty stats.""" + + empty_stats = mock.MagicMock() + empty_stats.stats = {} + + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats(empty_stats, sample_interval_usec=100) + + result = output.getvalue() + + # Should print message about no samples + self.assertIn("No samples were collected.", result) + + def test_print_sampled_stats_sample_percentage_sorting(self): + """Test sample percentage sorting options.""" + + # Add a function with high sample percentage (more direct calls than func3's 200) + self.mock_stats.stats[("expensive.py", 60, "expensive_func")] = ( + 300, # direct calls (higher than func3's 200) + 300, # cumulative calls + 1.0, # total time + 1.0, # cumulative time + {}, + ) + + # Test sort by sample percentage + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats( + self.mock_stats, sort=3, sample_interval_usec=100 + ) # sample percentage + + result = output.getvalue() + lines = result.strip().split("\n") + + data_lines = [l for l in lines if ".py" in l and "func" in l] + # expensive_func should be first (highest sample percentage) + self.assertIn("expensive_func", data_lines[0]) + + def test_print_sampled_stats_with_recursive_calls(self): + """Test print_sampled_stats with recursive calls where nc != cc.""" + + # Create stats with recursive calls (nc != cc) + recursive_stats = mock.MagicMock() + recursive_stats.stats = { + # (direct_calls, cumulative_calls, tt, ct, callers) - recursive function + ("recursive.py", 10, "factorial"): ( + 5, # direct_calls + 10, # cumulative_calls (appears more times in stack due to recursion) + 0.5, + 0.6, + {}, + ), + ("normal.py", 20, "normal_func"): ( + 3, # direct_calls + 3, # cumulative_calls (same as direct for non-recursive) + 0.2, + 0.2, + {}, + ), + } + + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats(recursive_stats, sample_interval_usec=100) + + result = output.getvalue() + + # Should display recursive calls as "5/10" format + self.assertIn("5/10", result) # nc/cc format for recursive calls + self.assertIn("3", result) # just nc for non-recursive calls + self.assertIn("factorial", result) + self.assertIn("normal_func", result) + + def test_print_sampled_stats_with_zero_call_counts(self): + """Test print_sampled_stats with zero call counts to trigger division protection.""" + + # Create stats with zero call counts + zero_stats = mock.MagicMock() + zero_stats.stats = { + ("file.py", 10, "zero_calls"): (0, 0, 0.0, 0.0, {}), # Zero calls + ("file.py", 20, "normal_func"): ( + 5, + 5, + 0.1, + 0.1, + {}, + ), # Normal function + } + + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats(zero_stats, sample_interval_usec=100) + + result = output.getvalue() + + # Should handle zero call counts gracefully + self.assertIn("zero_calls", result) + self.assertIn("zero_calls", result) + self.assertIn("normal_func", result) + + def test_print_sampled_stats_sort_by_name(self): + """Test sort by function name option.""" + + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats( + self.mock_stats, sort=-1, sample_interval_usec=100 + ) # sort by name + + result = output.getvalue() + lines = result.strip().split("\n") + + # Find the data lines (skip header and summary) + # Data lines start with whitespace and numbers, and contain filename:lineno(function) + data_lines = [] + for line in lines: + # Skip header lines and summary sections + if ( + line.startswith(" ") + and "(" in line + and ")" in line + and not line.startswith( + " 1." + ) # Skip summary lines that start with times + and not line.startswith( + " 0." + ) # Skip summary lines that start with times + and not "per call" in line # Skip summary lines + and not "calls" in line # Skip summary lines + and not "total time" in line # Skip summary lines + and not "cumulative time" in line + and not "filename:lineno(function)" in line # Skip header line + ): # Skip summary lines + data_lines.append(line) + + # Extract just the function names for comparison + func_names = [] + + for line in data_lines: + # Function name is between the last ( and ), accounting for ANSI color codes + match = re.search(r"\(([^)]+)\)$", line) + if match: + func_name = match.group(1) + # Remove ANSI color codes + func_name = re.sub(r"\x1b\[[0-9;]*m", "", func_name) + func_names.append(func_name) + + # Verify we extracted function names and they are sorted + self.assertGreater( + len(func_names), 0, "Should have extracted some function names" + ) + self.assertEqual( + func_names, + sorted(func_names), + f"Function names {func_names} should be sorted alphabetically", + ) + + def test_print_sampled_stats_with_zero_time_functions(self): + """Test summary sections with functions that have zero time.""" + + # Create stats with zero-time functions + zero_time_stats = mock.MagicMock() + zero_time_stats.stats = { + ("file1.py", 10, "zero_time_func"): ( + 5, + 5, + 0.0, + 0.0, + {}, + ), # Zero time + ("file2.py", 20, "normal_func"): ( + 3, + 3, + 0.1, + 0.1, + {}, + ), # Normal time + } + + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats( + zero_time_stats, + show_summary=True, + sample_interval_usec=100, + ) + + result = output.getvalue() + + # Should handle zero-time functions gracefully in summary + self.assertIn("Summary of Interesting Functions:", result) + self.assertIn("zero_time_func", result) + self.assertIn("normal_func", result) + + def test_print_sampled_stats_with_malformed_qualified_names(self): + """Test summary generation with function names that don't contain colons.""" + + # Create stats with function names that would create malformed qualified names + malformed_stats = mock.MagicMock() + malformed_stats.stats = { + # Function name without clear module separation + ("no_colon_func", 10, "func"): (3, 3, 0.1, 0.1, {}), + ("", 20, "empty_filename_func"): (2, 2, 0.05, 0.05, {}), + ("normal.py", 30, "normal_func"): (5, 5, 0.2, 0.2, {}), + } + + with io.StringIO() as output: + with mock.patch("sys.stdout", output): + print_sampled_stats( + malformed_stats, + show_summary=True, + sample_interval_usec=100, + ) + + result = output.getvalue() + + # Should handle malformed names gracefully in summary aggregation + self.assertIn("Summary of Interesting Functions:", result) + # All function names should appear somewhere in the output + self.assertIn("func", result) + self.assertIn("empty_filename_func", result) + self.assertIn("normal_func", result) + + def test_print_sampled_stats_with_recursive_call_stats_creation(self): + """Test create_stats with recursive call data to trigger total_rec_calls branch.""" + collector = PstatsCollector(sample_interval_usec=1000000) # 1 second + + # Simulate recursive function data where total_rec_calls would be set + # We need to manually manipulate the collector result to test this branch + collector.result = { + ("recursive.py", 10, "factorial"): { + "total_rec_calls": 3, # Non-zero recursive calls + "direct_calls": 5, + "cumulative_calls": 10, + }, + ("normal.py", 20, "normal_func"): { + "total_rec_calls": 0, # Zero recursive calls + "direct_calls": 2, + "cumulative_calls": 5, + }, + } + + collector.create_stats() + + # Check that recursive calls are handled differently from non-recursive + factorial_stats = collector.stats[("recursive.py", 10, "factorial")] + normal_stats = collector.stats[("normal.py", 20, "normal_func")] + + # factorial should use cumulative_calls (10) as nc + self.assertEqual( + factorial_stats[1], 10 + ) # nc should be cumulative_calls + self.assertEqual(factorial_stats[0], 5) # cc should be direct_calls + + # normal_func should use cumulative_calls as nc + self.assertEqual(normal_stats[1], 5) # nc should be cumulative_calls + self.assertEqual(normal_stats[0], 2) # cc should be direct_calls diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_trend_tracker.py b/Lib/test/test_profiling/test_sampling_profiler/test_trend_tracker.py new file mode 100644 index 00000000000..43b23be0fe3 --- /dev/null +++ b/Lib/test/test_profiling/test_sampling_profiler/test_trend_tracker.py @@ -0,0 +1,97 @@ +"""Simple unit tests for TrendTracker.""" + +import unittest +from test.support import requires +from test.support.import_helper import import_module + +# Only run these tests if curses is available +requires("curses") +curses = import_module("curses") + +from profiling.sampling.live_collector.trend_tracker import TrendTracker + + +class TestTrendTracker(unittest.TestCase): + """Tests for TrendTracker class.""" + + def setUp(self): + """Set up test fixtures.""" + self.colors = { + "trend_up": curses.A_BOLD, + "trend_down": curses.A_REVERSE, + "trend_stable": curses.A_NORMAL, + } + + def test_basic_trend_detection(self): + """Test basic up/down/stable trend detection.""" + tracker = TrendTracker(self.colors, enabled=True) + + # First value is always stable + self.assertEqual(tracker.update("func1", "nsamples", 10), "stable") + + # Increasing value + self.assertEqual(tracker.update("func1", "nsamples", 20), "up") + + # Decreasing value + self.assertEqual(tracker.update("func1", "nsamples", 15), "down") + + # Small change (within threshold) is stable + self.assertEqual(tracker.update("func1", "nsamples", 15.0001), "stable") + + def test_multiple_metrics(self): + """Test tracking multiple metrics simultaneously.""" + tracker = TrendTracker(self.colors, enabled=True) + + trends = tracker.update_metrics("func1", { + "nsamples": 10, + "tottime": 5.0, + }) + + self.assertEqual(trends["nsamples"], "stable") + self.assertEqual(trends["tottime"], "stable") + + # Update with changes + trends = tracker.update_metrics("func1", { + "nsamples": 15, + "tottime": 3.0, + }) + + self.assertEqual(trends["nsamples"], "up") + self.assertEqual(trends["tottime"], "down") + + def test_toggle_enabled(self): + """Test enable/disable toggle.""" + tracker = TrendTracker(self.colors, enabled=True) + self.assertTrue(tracker.enabled) + + tracker.toggle() + self.assertFalse(tracker.enabled) + + # When disabled, should return A_NORMAL + self.assertEqual(tracker.get_color("up"), curses.A_NORMAL) + + def test_get_color(self): + """Test color selection for trends.""" + tracker = TrendTracker(self.colors, enabled=True) + + self.assertEqual(tracker.get_color("up"), curses.A_BOLD) + self.assertEqual(tracker.get_color("down"), curses.A_REVERSE) + self.assertEqual(tracker.get_color("stable"), curses.A_NORMAL) + + def test_clear(self): + """Test clearing tracked values.""" + tracker = TrendTracker(self.colors, enabled=True) + + # Add some data + tracker.update("func1", "nsamples", 10) + tracker.update("func1", "nsamples", 20) + + # Clear + tracker.clear() + + # After clear, first update should be stable + self.assertEqual(tracker.update("func1", "nsamples", 30), "stable") + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_profiling/test_tracing_profiler.py b/Lib/test/test_profiling/test_tracing_profiler.py new file mode 100644 index 00000000000..50de9af86e8 --- /dev/null +++ b/Lib/test/test_profiling/test_tracing_profiler.py @@ -0,0 +1,308 @@ +"""Test suite for the cProfile module.""" +import multiprocessing +import sys +import unittest + +# rip off all interesting stuff from test_profile +import profiling.tracing as cProfile +import tempfile +import textwrap +from test.test_profile import ProfileTest, regenerate_expected_output +from test.support.script_helper import assert_python_failure, assert_python_ok +from test import support + + +class CProfileTest(ProfileTest): + profilerclass = cProfile.Profile + profilermodule = cProfile + expected_max_output = "{built-in method builtins.max}" + + def get_expected_output(self): + return _ProfileOutput + + def test_bad_counter_during_dealloc(self): + # bpo-3895 + import _lsprof + + with support.catch_unraisable_exception() as cm: + obj = _lsprof.Profiler(lambda: int) + obj.enable() + obj.disable() + obj.clear() + + self.assertEqual(cm.unraisable.exc_type, TypeError) + + def test_crash_with_not_enough_args(self): + # gh-126220 + import _lsprof + + for profile in [_lsprof.Profiler(), cProfile.Profile()]: + for method in [ + "_pystart_callback", + "_pyreturn_callback", + "_ccall_callback", + "_creturn_callback", + ]: + with self.subTest(profile=profile, method=method): + method_obj = getattr(profile, method) + with self.assertRaises(TypeError): + method_obj() # should not crash + + def test_evil_external_timer(self): + # gh-120289 + # Disabling profiler in external timer should not crash + import _lsprof + class EvilTimer(): + def __init__(self, disable_count): + self.count = 0 + self.disable_count = disable_count + + def __call__(self): + self.count += 1 + if self.count == self.disable_count: + profiler_with_evil_timer.disable() + return self.count + + # this will trigger external timer to disable profiler at + # call event - in initContext in _lsprof.c + with support.catch_unraisable_exception() as cm: + profiler_with_evil_timer = _lsprof.Profiler(EvilTimer(1)) + profiler_with_evil_timer.enable() + # Make a call to trigger timer + (lambda: None)() + profiler_with_evil_timer.disable() + profiler_with_evil_timer.clear() + self.assertEqual(cm.unraisable.exc_type, RuntimeError) + + # this will trigger external timer to disable profiler at + # return event - in Stop in _lsprof.c + with support.catch_unraisable_exception() as cm: + profiler_with_evil_timer = _lsprof.Profiler(EvilTimer(2)) + profiler_with_evil_timer.enable() + # Make a call to trigger timer + (lambda: None)() + profiler_with_evil_timer.disable() + profiler_with_evil_timer.clear() + self.assertEqual(cm.unraisable.exc_type, RuntimeError) + + def test_enable_in_external_timer(self): + # gh-157639: Enabling the profiler from an external timer should not crash + import _lsprof + + # the timer re-arms monitoring from inside disable(), so the tool + # id stays claimed once the profiler is torn down + self.addCleanup(sys.monitoring.free_tool_id, sys.monitoring.PROFILER_ID) + + def timer(): + try: + profiler.enable() + except Exception: + pass + return 0 + + profiler = _lsprof.Profiler(timer=timer) + profiler.enable() + (lambda: None)() + profiler.disable() + profiler.clear() + + def test_profile_enable_disable(self): + prof = self.profilerclass() + # Make sure we clean ourselves up if the test fails for some reason. + self.addCleanup(prof.disable) + + prof.enable() + self.assertEqual( + sys.monitoring.get_tool(sys.monitoring.PROFILER_ID), "cProfile") + + prof.disable() + self.assertIs(sys.monitoring.get_tool(sys.monitoring.PROFILER_ID), None) + + def test_profile_as_context_manager(self): + prof = self.profilerclass() + # Make sure we clean ourselves up if the test fails for some reason. + self.addCleanup(prof.disable) + + with prof as __enter__return_value: + # profile.__enter__ should return itself. + self.assertIs(prof, __enter__return_value) + + # profile should be set as the global profiler inside the + # with-block + self.assertEqual( + sys.monitoring.get_tool(sys.monitoring.PROFILER_ID), "cProfile") + + # profile shouldn't be set once we leave the with-block. + self.assertIs(sys.monitoring.get_tool(sys.monitoring.PROFILER_ID), None) + + def test_second_profiler(self): + pr = self.profilerclass() + pr2 = self.profilerclass() + pr.enable() + self.addCleanup(pr.disable) + msg = f"tool {sys.monitoring.PROFILER_ID} is already in use" + self.assertRaisesRegex(ValueError, msg, pr2.enable) + + def test_throw(self): + """ + gh-106152 + generator.throw() should trigger a call in cProfile + """ + + def gen(): + yield + + pr = self.profilerclass() + pr.enable() + g = gen() + try: + g.throw(SyntaxError) + except SyntaxError: + pass + pr.disable() + pr.create_stats() + + self.assertTrue(any("throw" in func[2] for func in pr.stats.keys())), + + def test_clear_with_nested_calls(self): + # Calling clear() during nested profiled calls should not leak + # ProfilerContexts. clearEntries() must walk the entire linked list, + # not just free the top context. + import _lsprof + + def level3(profiler): + profiler.clear() + + def level2(profiler): + level3(profiler) + + def level1(profiler): + level2(profiler) + + profiler = _lsprof.Profiler() + profiler.enable() + for _ in range(100): + level1(profiler) + profiler.disable() + profiler.clear() + + def test_bad_descriptor(self): + # gh-132250 + # cProfile should not crash when the profiler callback fails to locate + # the actual function of a method. + with self.profilerclass() as prof: + with self.assertRaises(TypeError): + bytes.find(str()) + + +class TestCommandLine(unittest.TestCase): + def test_sort(self): + rc, out, err = assert_python_failure('-m', 'cProfile', '-s', 'demo') + self.assertGreater(rc, 0) + self.assertIn(b"option -s: invalid choice: 'demo'", err) + + def test_profile_script_importing_main(self): + """Check that scripts that reference __main__ see their own namespace + when being profiled.""" + with tempfile.NamedTemporaryFile("w+", delete_on_close=False) as f: + f.write(textwrap.dedent("""\ + class Foo: + pass + import __main__ + assert Foo == __main__.Foo + """)) + f.close() + assert_python_ok('-m', "cProfile", f.name) + + def _test_process_run_pickle(self, start_method): + val = 10 + with tempfile.NamedTemporaryFile("w+", delete_on_close=False) as f: + f.write(textwrap.dedent( + f'''\ + import multiprocessing + + def worker(x): + print(__name__) + exit(x ** 2) + + if __name__ == "__main__": + multiprocessing.set_start_method('{start_method}') + p = multiprocessing.Process(target=worker, args=({val},)) + p.start() + p.join() + print("p.exitcode =", p.exitcode) + ''')) + f.close() + _, out, err = assert_python_ok('-m', "cProfile", f.name) + self.assertIn(b"__mp_main__", out) + self.assertIn(bytes(f"exitcode = {val**2}", encoding='utf8'), out) + self.assertNotIn(b"Can't pickle", err) + + def test_process_spawn_pickle(self): + # gh-140729: test use Process in cProfile. + self._test_process_run_pickle('spawn') + + @unittest.skipUnless("forkserver" in multiprocessing.get_all_start_methods(), + "forkserver start method is not available") + def test_process_forkserver_pickle(self): + # gh-140729: test use Process in cProfile. + self._test_process_run_pickle('forkserver') + + +def main(): + if '-r' not in sys.argv: + unittest.main() + else: + regenerate_expected_output(__file__, CProfileTest) + + +# Don't remove this comment. Everything below it is auto-generated. +#--cut-------------------------------------------------------------------------- +_ProfileOutput = {} +_ProfileOutput['print_stats'] = """\ + 28 0.028 0.001 0.028 0.001 profilee.py:110(__getattr__) + 1 0.270 0.270 1.000 1.000 profilee.py:25(testfunc) + 23/3 0.150 0.007 0.170 0.057 profilee.py:35(factorial) + 20 0.020 0.001 0.020 0.001 profilee.py:48(mul) + 2 0.040 0.020 0.600 0.300 profilee.py:55(helper) + 4 0.116 0.029 0.120 0.030 profilee.py:73(helper1) + 2 0.000 0.000 0.140 0.070 profilee.py:84(helper2_indirect) + 8 0.312 0.039 0.400 0.050 profilee.py:88(helper2) + 8 0.064 0.008 0.080 0.010 profilee.py:98(subhelper)""" +_ProfileOutput['print_callers'] = """\ +profilee.py:110(__getattr__) <- 16 0.016 0.016 profilee.py:98(subhelper) +profilee.py:25(testfunc) <- 1 0.270 1.000 :1() +profilee.py:35(factorial) <- 1 0.014 0.130 profilee.py:25(testfunc) + 20/3 0.130 0.147 profilee.py:35(factorial) + 2 0.006 0.040 profilee.py:84(helper2_indirect) +profilee.py:48(mul) <- 20 0.020 0.020 profilee.py:35(factorial) +profilee.py:55(helper) <- 2 0.040 0.600 profilee.py:25(testfunc) +profilee.py:73(helper1) <- 4 0.116 0.120 profilee.py:55(helper) +profilee.py:84(helper2_indirect) <- 2 0.000 0.140 profilee.py:55(helper) +profilee.py:88(helper2) <- 6 0.234 0.300 profilee.py:55(helper) + 2 0.078 0.100 profilee.py:84(helper2_indirect) +profilee.py:98(subhelper) <- 8 0.064 0.080 profilee.py:88(helper2) +{built-in method builtins.hasattr} <- 4 0.000 0.004 profilee.py:73(helper1) + 8 0.000 0.008 profilee.py:88(helper2) +{built-in method sys.exception} <- 4 0.000 0.000 profilee.py:73(helper1) +{method 'append' of 'list' objects} <- 4 0.000 0.000 profilee.py:73(helper1)""" +_ProfileOutput['print_callees'] = """\ +:1() -> 1 0.270 1.000 profilee.py:25(testfunc) +profilee.py:110(__getattr__) -> +profilee.py:25(testfunc) -> 1 0.014 0.130 profilee.py:35(factorial) + 2 0.040 0.600 profilee.py:55(helper) +profilee.py:35(factorial) -> 20/3 0.130 0.147 profilee.py:35(factorial) + 20 0.020 0.020 profilee.py:48(mul) +profilee.py:48(mul) -> +profilee.py:55(helper) -> 4 0.116 0.120 profilee.py:73(helper1) + 2 0.000 0.140 profilee.py:84(helper2_indirect) + 6 0.234 0.300 profilee.py:88(helper2) +profilee.py:73(helper1) -> 4 0.000 0.004 {built-in method builtins.hasattr} +profilee.py:84(helper2_indirect) -> 2 0.006 0.040 profilee.py:35(factorial) + 2 0.078 0.100 profilee.py:88(helper2) +profilee.py:88(helper2) -> 8 0.064 0.080 profilee.py:98(subhelper) +profilee.py:98(subhelper) -> 16 0.016 0.016 profilee.py:110(__getattr__) +{built-in method builtins.hasattr} -> 12 0.012 0.012 profilee.py:110(__getattr__)""" + +if __name__ == "__main__": + main() diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py index acd2dde8867..6eb01ce7bbc 100644 --- a/Lib/test/test_py_compile.py +++ b/Lib/test/test_py_compile.py @@ -1,4 +1,3 @@ -import functools import importlib.util import os import py_compile @@ -11,43 +10,7 @@ from test import support from test.support import os_helper, script_helper - - -def without_source_date_epoch(fxn): - """Runs function with SOURCE_DATE_EPOCH unset.""" - @functools.wraps(fxn) - def wrapper(*args, **kwargs): - with os_helper.EnvironmentVarGuard() as env: - env.unset('SOURCE_DATE_EPOCH') - return fxn(*args, **kwargs) - return wrapper - - -def with_source_date_epoch(fxn): - """Runs function with SOURCE_DATE_EPOCH set.""" - @functools.wraps(fxn) - def wrapper(*args, **kwargs): - with os_helper.EnvironmentVarGuard() as env: - env['SOURCE_DATE_EPOCH'] = '123456789' - return fxn(*args, **kwargs) - return wrapper - - -# Run tests with SOURCE_DATE_EPOCH set or unset explicitly. -class SourceDateEpochTestMeta(type(unittest.TestCase)): - def __new__(mcls, name, bases, dct, *, source_date_epoch): - cls = super().__new__(mcls, name, bases, dct) - - for attr in dir(cls): - if attr.startswith('test_'): - meth = getattr(cls, attr) - if source_date_epoch: - wrapper = with_source_date_epoch(meth) - else: - wrapper = without_source_date_epoch(meth) - setattr(cls, attr, wrapper) - - return cls +from test.support.os_helper import SourceDateEpochTestMeta class PyCompileTestsBase: @@ -56,7 +19,10 @@ def setUp(self): self.directory = tempfile.mkdtemp(dir=os.getcwd()) self.source_path = os.path.join(self.directory, '_test.py') self.pyc_path = self.source_path + 'c' - self.cache_path = importlib.util.cache_from_source(self.source_path) + try: + self.cache_path = importlib.util.cache_from_source(self.source_path) + except NotImplementedError: + self.cache_path = None self.cwd_drive = os.path.splitdrive(os.getcwd())[0] # In these tests we compute relative paths. When using Windows, the # current working directory path and the 'self.source_path' might be @@ -73,10 +39,31 @@ def tearDown(self): if self.cwd_drive: os.chdir(self.cwd_drive) + def assert_cache_path_exists(self, should_exist=True): + if self.cache_path: + if should_exist: + self.assertTrue(os.path.exists(self.cache_path)) + else: + self.assertFalse(os.path.exists(self.cache_path)) + return + cache_dir = os.path.join(self.directory, '__pycache__') + if not os.path.isdir(cache_dir): + if should_exist: + self.fail('no __pycache__ directory exists') + return + for f in os.listdir(cache_dir): + if f.startswith('_test.') and f.endswith('.pyc'): + if should_exist: + return + self.fail(f'__pycache__/{f} was created') + else: + if should_exist: + self.fail('no __pycache__/_test.*.pyc file exists') + def test_absolute_path(self): py_compile.compile(self.source_path, self.pyc_path) self.assertTrue(os.path.exists(self.pyc_path)) - self.assertFalse(os.path.exists(self.cache_path)) + self.assert_cache_path_exists(False) def test_do_not_overwrite_symlinks(self): # In the face of a cfile argument being a symlink, bail out. @@ -98,23 +85,25 @@ def test_do_not_overwrite_nonregular_files(self): with self.assertRaises(FileExistsError): py_compile.compile(self.source_path, os.devnull) + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag is not None') def test_cache_path(self): py_compile.compile(self.source_path) - self.assertTrue(os.path.exists(self.cache_path)) + self.assert_cache_path_exists(True) def test_cwd(self): with os_helper.change_cwd(self.directory): py_compile.compile(os.path.basename(self.source_path), os.path.basename(self.pyc_path)) self.assertTrue(os.path.exists(self.pyc_path)) - self.assertFalse(os.path.exists(self.cache_path)) + self.assert_cache_path_exists(False) @unittest.expectedFailureIf(sys.platform == "darwin" and int(__import__("platform").release().split(".")[0]) < 20, "TODO: RUSTPYTHON") def test_relative_path(self): py_compile.compile(os.path.relpath(self.source_path), os.path.relpath(self.pyc_path)) self.assertTrue(os.path.exists(self.pyc_path)) - self.assertFalse(os.path.exists(self.cache_path)) + self.assert_cache_path_exists(False) @os_helper.skip_if_dac_override @unittest.skipIf(os.name == 'nt', @@ -137,14 +126,14 @@ def test_bad_coding(self): 'tokenizedata', 'bad_coding2.py') with support.captured_stderr(): - self.assertIsNone(py_compile.compile(bad_coding, doraise=False)) - self.assertFalse(os.path.exists( - importlib.util.cache_from_source(bad_coding))) + self.assertIsNone(py_compile.compile(bad_coding, self.pyc_path, + doraise=False)) + self.assertFalse(os.path.exists(self.pyc_path)) def test_source_date_epoch(self): py_compile.compile(self.source_path, self.pyc_path) self.assertTrue(os.path.exists(self.pyc_path)) - self.assertFalse(os.path.exists(self.cache_path)) + self.assert_cache_path_exists(False) with open(self.pyc_path, 'rb') as fp: flags = importlib._bootstrap_external._classify_pyc( fp.read(), 'test', {}) @@ -156,6 +145,8 @@ def test_source_date_epoch(self): self.assertEqual(flags, expected_flags) @unittest.skipIf(sys.flags.optimize > 0, 'test does not work with -O') + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag is not None') def test_double_dot_no_clobber(self): # http://bugs.python.org/issue22966 # py_compile foo.bar.py -> __pycache__/foo.cpython-34.pyc @@ -178,6 +169,8 @@ def test_double_dot_no_clobber(self): self.assertTrue(os.path.exists(cache_path)) self.assertFalse(os.path.exists(pyc_path)) + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag is not None') def test_optimization_path(self): # Specifying optimized bytecode should lead to a path reflecting that. self.assertIn('opt-2', py_compile.compile(self.source_path, optimize=2)) @@ -185,17 +178,19 @@ def test_optimization_path(self): def test_invalidation_mode(self): py_compile.compile( self.source_path, + self.pyc_path, invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH, ) - with open(self.cache_path, 'rb') as fp: + with open(self.pyc_path, 'rb') as fp: flags = importlib._bootstrap_external._classify_pyc( fp.read(), 'test', {}) self.assertEqual(flags, 0b11) py_compile.compile( self.source_path, + self.pyc_path, invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH, ) - with open(self.cache_path, 'rb') as fp: + with open(self.pyc_path, 'rb') as fp: flags = importlib._bootstrap_external._classify_pyc( fp.read(), 'test', {}) self.assertEqual(flags, 0b1) @@ -205,11 +200,11 @@ def test_quiet(self): 'tokenizedata', 'bad_coding2.py') with support.captured_stderr() as stderr: - self.assertIsNone(py_compile.compile(bad_coding, doraise=False, quiet=2)) - self.assertIsNone(py_compile.compile(bad_coding, doraise=True, quiet=2)) + self.assertIsNone(py_compile.compile(bad_coding, self.pyc_path, doraise=False, quiet=2)) + self.assertIsNone(py_compile.compile(bad_coding, self.pyc_path, doraise=True, quiet=2)) self.assertEqual(stderr.getvalue(), '') with self.assertRaises(py_compile.PyCompileError): - py_compile.compile(bad_coding, doraise=True, quiet=1) + py_compile.compile(bad_coding, self.pyc_path, doraise=True, quiet=1) def test_utf7_decoded_cr_compiles(self): with open(self.source_path, 'wb') as file: @@ -239,8 +234,12 @@ class PyCompileCLITestCase(unittest.TestCase): def setUp(self): self.directory = tempfile.mkdtemp() self.source_path = os.path.join(self.directory, '_test.py') - self.cache_path = importlib.util.cache_from_source(self.source_path, - optimization='' if __debug__ else 1) + try: + self.cache_path = importlib.util.cache_from_source(self.source_path, + optimization='' if __debug__ else 1) + except NotImplementedError: + # py_compile.main() assumes legacy pyc path if there is no cache_tag + self.cache_path = self.source_path + 'c' with open(self.source_path, 'w') as file: file.write('x = 123\n') diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index 144acb7afd7..a9400831e3a 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -13,12 +13,11 @@ from io import BytesIO from test import support from test.support import import_helper, os_helper - +from test.support import sortdict +from unittest import mock from xml.parsers import expat from xml.parsers.expat import errors -from test.support import sortdict - class SetAttributeTest(unittest.TestCase): def setUp(self): @@ -568,6 +567,19 @@ def test7(self): "", "4", "", "5", ""], "buffered text not properly split") + def test_change_character_data_handler_in_callback(self): + # Test that xmlparse_handler_setter() properly handles + # the special case "parser.CharacterDataHandler = None". + def handler(*args): + parser.CharacterDataHandler = None + + handler_wrapper = mock.Mock(wraps=handler) + parser = expat.ParserCreate() + parser.CharacterDataHandler = handler_wrapper + parser.Parse(b"12345 ", True) + handler_wrapper.assert_called_once() + self.assertIsNone(parser.CharacterDataHandler) + # Test handling of exception from callback: class HandlerExceptionTest(unittest.TestCase): @@ -626,6 +638,34 @@ def _test_exception(self, have_source): self.assertIn('call_with_frame("StartElement"', entries[1].line) + def test_invalid_NotStandalone(self): + parser = expat.ParserCreate() + parser.NotStandaloneHandler = mock.Mock(return_value="bad value") + parser.ElementDeclHandler = lambda _1, _2: None + + payload = b"""\ +]> +""" + with self.assertRaises(TypeError) as cm: + parser.Parse(payload, True) + parser.NotStandaloneHandler.assert_called_once() + + notes = ["invalid 'NotStandalone' event handler return value"] + self.assertEqual(cm.exception.__notes__, notes) + + def test_invalid_ExternalEntityRefHandler(self): + parser = expat.ParserCreate() + parser.UseForeignDTD() + parser.SetParamEntityParsing(expat.XML_PARAM_ENTITY_PARSING_ALWAYS) + parser.ExternalEntityRefHandler = mock.Mock(return_value=None) + + with self.assertRaises(TypeError) as cm: + parser.Parse(b"", True) + parser.ExternalEntityRefHandler.assert_called_once() + + notes = ["invalid 'ExternalEntityRef' event handler return value"] + self.assertEqual(cm.exception.__notes__, notes) + # Test Current* members: class PositionTest(unittest.TestCase): @@ -648,9 +688,6 @@ def check_pos(self, event): self.upto += 1 @unittest.expectedFailure # TODO: RUSTPYTHON; the xml-rs backend reports a - # token's *current parse cursor* position rather than libexpat's *start of - # token* position, so exact byte/column offsets for StartElementHandler - # don't match upstream expat. def test(self): self.parser = expat.ParserCreate() self.parser.StartElementHandler = self.StartElementHandler @@ -737,7 +774,7 @@ def test_unchanged_size(self): def test_disabling_buffer(self): xml1 = b"" + b'a' * 512 xml2 = b'b' * 1024 - xml3 = b'c' * 1024 + b''; + xml3 = b'c' * 1024 + b'' parser = expat.ParserCreate() parser.CharacterDataHandler = self.counting_handler parser.buffer_text = 1 @@ -815,7 +852,7 @@ def test_change_size_2(self): @support.requires_resource('cpu') @support.requires_resource('walltime') @support.bigmemtest(size=2**31, memuse=4, dry_run=False) - def test_large_character_data_does_not_crash(self): + def test_large_character_data_does_not_crash(self, size): # See https://github.com/python/cpython/issues/148441 parser = expat.ParserCreate() parser.buffer_text = True @@ -845,7 +882,7 @@ def test_trigger_leak(self): self.assertRaises(TypeError, parser.Parse, data, True) @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: RecursionError not raised - @support.skip_if_unlimited_stack_size + @support.skip_if_huge_c_stack(800_000) @support.skip_emscripten_stack_overflow() @support.skip_wasi_stack_overflow() def test_deeply_nested_content_model(self): @@ -995,6 +1032,16 @@ def test_parent_parser_outlives_its_subparsers__chain(self): del parser del subparser + # gh-155485: GetReparseDeferralEnabled always returns False with Expat <2.6.0. + @unittest.skipIf(not expat.ParserCreate().GetReparseDeferralEnabled(), + "requires Python compiled with Expat >= 2.6.0") + def test_subparser_inherits_reparse_deferral(self): + for enabled in (True, False): + parser = expat.ParserCreate() + parser.SetReparseDeferralEnabled(enabled) + subparser = parser.ExternalEntityParserCreate(None) + self.assertEqual(subparser.GetReparseDeferralEnabled(), enabled) + class ExternalEntityParserCreateErrorTest(unittest.TestCase): """ExternalEntityParserCreate error paths should not crash or leak @@ -1007,8 +1054,7 @@ class ExternalEntityParserCreateErrorTest(unittest.TestCase): def setUpClass(cls): cls.testcapi = import_helper.import_module('_testcapi') - @unittest.skipIf(support.Py_TRACE_REFS, - 'Py_TRACE_REFS conflicts with testcapi.set_nomemory') + @support.nomemtest def test_error_path_no_crash(self): # When an allocation inside ExternalEntityParserCreate fails, # the partially-initialized subparser is deallocated. This diff --git a/Lib/test/test_pyrepl/__init__.py b/Lib/test/test_pyrepl/__init__.py index 2f37bff6df8..1534d63352c 100644 --- a/Lib/test/test_pyrepl/__init__.py +++ b/Lib/test/test_pyrepl/__init__.py @@ -3,6 +3,9 @@ from test.support import import_helper, load_package_tests +import_helper.import_module("_pyrepl") + + if sys.platform != "win32": import_helper.import_module("termios") diff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py index 307bf450555..c879a2f93b6 100644 --- a/Lib/test/test_pyrepl/support.py +++ b/Lib/test/test_pyrepl/support.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock from _pyrepl.console import Console, Event +from _pyrepl.render import RenderLine, RenderedScreen from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig from _pyrepl.simple_interact import _strip_final_indent from _pyrepl.utils import unbracket, ANSI_ESCAPE_SEQUENCE @@ -15,7 +16,13 @@ def assert_screen_equal( ): actual = clean_screen(reader) if clean else reader.screen expected = expected.split("\n") - self.assertListEqual(actual, expected) + if clean: + self.assertListEqual(actual, expected) + return + + actual_lines = [RenderLine.from_rendered_text(line) for line in actual] + expected_lines = [RenderLine.from_rendered_text(line) for line in expected] + self.assertListEqual(actual_lines, expected_lines) def multiline_input(reader: ReadlineAlikeReader, namespace: dict | None = None): @@ -120,9 +127,11 @@ class FakeConsole(Console): def __init__(self, events, encoding="utf-8") -> None: self.events = iter(events) self.encoding = encoding - self.screen = [] + self._rendered_screen = RenderedScreen.empty() self.height = 100 self.width = 80 + self.posxy = (0, 0) + self._redraw_visual_cycle = 0 def get_event(self, block: bool = True) -> Event | None: return next(self.events) @@ -133,7 +142,7 @@ def getpending(self) -> Event: def getheightwidth(self) -> tuple[int, int]: return self.height, self.width - def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: + def refresh(self, rendered_screen: RenderedScreen) -> None: pass def prepare(self) -> None: diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py new file mode 100644 index 00000000000..0ffc1ed97b5 --- /dev/null +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -0,0 +1,250 @@ +import importlib +import inspect +import os +import types +import unittest + +from _colorize import ANSIColors, get_theme +from _pyrepl.completing_reader import stripcolor +from _pyrepl.fancycompleter import ( + Completer, + colorize_matches, + commonprefix, + _color_for_obj, +) +from test.support.import_helper import ready_to_import + +class MockPatch: + def __init__(self): + self.original_values = {} + + def setattr(self, obj, name, value): + if obj not in self.original_values: + self.original_values[obj] = {} + if name not in self.original_values[obj]: + self.original_values[obj][name] = getattr(obj, name) + setattr(obj, name, value) + + def restore_all(self): + for obj, attrs in self.original_values.items(): + for name, value in attrs.items(): + setattr(obj, name, value) + +class FancyCompleterTests(unittest.TestCase): + def setUp(self): + self.mock_patch = MockPatch() + + def tearDown(self): + self.mock_patch.restore_all() + + def test_commonprefix(self): + self.assertEqual(commonprefix(['isalpha', 'isdigit', 'foo']), '') + self.assertEqual(commonprefix(['isalpha', 'isdigit']), 'is') + self.assertEqual(commonprefix([]), '') + + def test_colorize_matches_signature(self): + signature = inspect.signature(colorize_matches) + + self.assertEqual(list(signature.parameters), ["names", "values", "theme"]) + + def test_complete_attribute(self): + compl = Completer({'a': None}, use_colors=False) + self.assertEqual(compl.attr_matches('a.'), ['a.__']) + matches = compl.attr_matches('a.__') + self.assertNotIn('__class__', matches) + self.assertIn('a.__class__', matches) + match = compl.attr_matches('a.__class') + self.assertEqual(len(match), 1) + self.assertTrue(match[0].startswith('a.__class__')) + + def test_complete_attribute_prefix(self): + class C(object): + attr = 1 + _attr = 2 + __attr__attr = 3 + compl = Completer({'a': C}, use_colors=False) + self.assertEqual(compl.attr_matches('a.'), ['a.attr', 'a.mro']) + self.assertEqual( + compl.attr_matches('a._'), + ['a._C__attr__attr', 'a._attr'], + ) + matches = compl.attr_matches('a.__') + self.assertNotIn('__class__', matches) + self.assertIn('a.__class__', matches) + match = compl.attr_matches('a.__class') + self.assertEqual(len(match), 1) + self.assertTrue(match[0].startswith('a.__class__')) + + compl = Completer({'a': None}, use_colors=False) + self.assertEqual(compl.attr_matches('a._'), ['a.__']) + + def test_complete_attribute_colored(self): + theme = get_theme() + compl = Completer({'a': 42}, use_colors=True) + matches = compl.attr_matches('a.__') + self.assertGreater(len(matches), 2) + expected_color = theme.fancycompleter.type + expected_part = f'{expected_color}a.__class__{ANSIColors.RESET}' + for match in matches: + if expected_part in match: + break + else: + self.assertFalse(True, matches) + self.assertNotIn(' ', matches) + + def test_preserves_callable_postfix_for_single_attribute_match(self): + compl = Completer({'os': os}, use_colors=False) + self.assertEqual(compl.attr_matches('os.getpid'), ['os.getpid()']) + + def test_property_method_not_called(self): + class Foo: + property_called = False + + @property + def bar(self): + self.property_called = True + return 1 + + foo = Foo() + compl = Completer({'foo': foo}, use_colors=False) + self.assertEqual(compl.attr_matches('foo.b'), ['foo.bar']) + self.assertFalse(foo.property_called) + + def test_excessive_getattr(self): + class Foo: + calls = 0 + bar = '' + + def __getattribute__(self, name): + if name == 'bar': + self.calls += 1 + return None + return super().__getattribute__(name) + + foo = Foo() + compl = Completer({'foo': foo}, use_colors=False) + self.assertEqual(compl.complete('foo.b', 0), 'foo.bar') + self.assertEqual(foo.calls, 1) + + def test_uncreated_attr(self): + class Foo: + __slots__ = ('bar',) + + compl = Completer({'foo': Foo()}, use_colors=False) + self.assertEqual(compl.complete('foo.', 0), 'foo.bar') + + def test_module_attributes_do_not_reify_lazy_imports(self): + with ready_to_import("test_pyrepl_lazy_mod", "lazy import json\n") as (name, _): + module = importlib.import_module(name) + self.assertIs(type(module.__dict__["json"]), types.LazyImportType) + + compl = Completer({name: module}, use_colors=False) + self.assertEqual(compl.attr_matches(f"{name}.j"), [f"{name}.json"]) + self.assertIs(type(module.__dict__["json"]), types.LazyImportType) + + def test_complete_colored_single_match(self): + """No coloring, via commonprefix.""" + compl = Completer({'foobar': 42}, use_colors=True) + matches = compl.global_matches('foob') + self.assertEqual(matches, ['foobar']) + + def test_does_not_color_single_match(self): + class obj: + msgs = [] + + compl = Completer({'obj': obj}, use_colors=True) + matches = compl.attr_matches('obj.msgs') + self.assertEqual(matches, ['obj.msgs']) + + def test_complete_global(self): + compl = Completer({'foobar': 1, 'foobazzz': 2}, use_colors=False) + self.assertEqual(compl.global_matches('foo'), ['fooba']) + matches = compl.global_matches('fooba') + self.assertEqual(set(matches), set(['foobar', 'foobazzz'])) + self.assertEqual(compl.global_matches('foobaz'), ['foobazzz']) + self.assertEqual(compl.global_matches('nothing'), []) + + def test_complete_global_colored(self): + theme = get_theme() + compl = Completer({'foobar': 1, 'foobazzz': 2}, use_colors=True) + self.assertEqual(compl.global_matches('foo'), ['fooba']) + matches = compl.global_matches('fooba') + + int_color = theme.fancycompleter.int + self.assertEqual(matches, [ + f'{int_color}foobar{ANSIColors.RESET}', + f'{int_color}foobazzz{ANSIColors.RESET}', + ]) + self.assertEqual(compl.global_matches('foobaz'), ['foobazzz']) + self.assertEqual(compl.global_matches('nothing'), []) + + def test_colorized_match_is_stripped(self): + theme = get_theme() + match = _color_for_obj('spam', 1, theme) + self.assertEqual(stripcolor(match), 'spam') + + def test_complete_with_indexer(self): + compl = Completer({'lst': [None, 2, 3]}, use_colors=False) + self.assertEqual(compl.attr_matches('lst[0].'), ['lst[0].__']) + matches = compl.attr_matches('lst[0].__') + self.assertNotIn('__class__', matches) + self.assertIn('lst[0].__class__', matches) + match = compl.attr_matches('lst[0].__class') + self.assertEqual(len(match), 1) + self.assertTrue(match[0].startswith('lst[0].__class__')) + + def test_autocomplete(self): + class A: + aaa = None + abc_1 = None + abc_2 = None + abc_3 = None + bbb = None + compl = Completer({'A': A}, use_colors=False) + # + # In this case, we want to display all attributes which start with + # 'a'. + matches = compl.attr_matches('A.a') + self.assertEqual( + sorted(matches), + ['A.aaa', 'A.abc_1', 'A.abc_2', 'A.abc_3'], + ) + # + # If there is an actual common prefix, we return just it, so that readline + # will insert it into place + matches = compl.attr_matches('A.ab') + self.assertEqual(matches, ['A.abc_']) + # + # Finally, at the next tab, we display again all the completions + # available for this common prefix. + matches = compl.attr_matches('A.abc_') + self.assertEqual( + sorted(matches), + ['A.abc_1', 'A.abc_2', 'A.abc_3'], + ) + + def test_complete_exception(self): + compl = Completer({}, use_colors=False) + self.assertEqual(compl.attr_matches('xxx.'), []) + + def test_complete_invalid_attr(self): + compl = Completer({'str': str}, use_colors=False) + self.assertEqual(compl.attr_matches('str.xx'), []) + + def test_complete_function_skipped(self): + compl = Completer({'str': str}, use_colors=False) + self.assertEqual(compl.attr_matches('str.split().'), []) + + def test_unicode_in___dir__(self): + class Foo(object): + def __dir__(self): + return ['hello', 'world'] + + compl = Completer({'a': Foo()}, use_colors=False) + matches = compl.attr_matches('a.') + self.assertEqual(matches, ['a.hello', 'a.world']) + self.assertIs(type(matches[0]), str) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index 7cbe523a92f..67142eeeb33 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -114,7 +114,7 @@ def test_runsource_show_syntax_error_location(self): r = """ def f(x, x): ... ^ -SyntaxError: duplicate argument 'x' in function definition""" +SyntaxError: duplicate parameter 'x' in function definition""" self.assertIn(r, f.getvalue()) def test_runsource_shows_syntax_error_for_failed_compilation(self): diff --git a/Lib/test/test_pyrepl/test_layout.py b/Lib/test/test_pyrepl/test_layout.py new file mode 100644 index 00000000000..6825659b772 --- /dev/null +++ b/Lib/test/test_pyrepl/test_layout.py @@ -0,0 +1,309 @@ +from unittest import TestCase +from _pyrepl.content import ( + ContentFragment, + ContentLine, + PromptContent, + SourceLine, +) +from _pyrepl.layout import ( + LayoutMap, + LayoutRow, + layout_content_lines, +) + + +def _source(text, lineno=0, start_offset=0, has_newline=True, cursor_index=None): + return SourceLine(lineno, text, start_offset, has_newline, cursor_index) + + +def _prompt(text=">>> ", width=4): + return PromptContent((), text, width) + + +def _body_from_text(text): + return tuple(ContentFragment(ch, 1) for ch in text) + + +def _content_line(text, prompt=None, has_newline=True, start_offset=0): + if prompt is None: + prompt = _prompt() + body = _body_from_text(text) + source = _source(text, start_offset=start_offset, has_newline=has_newline) + return ContentLine(source, prompt, body) + + +class TestLayoutRow(TestCase): + def test_width_basic(self): + row = LayoutRow(4, (1, 1, 1)) + self.assertEqual(row.width, 7) + + def test_width_with_suffix(self): + row = LayoutRow(4, (1, 1), suffix_width=1) + self.assertEqual(row.width, 7) + + def test_screeninfo_without_suffix(self): + row = LayoutRow(4, (1, 1, 1)) + prompt_w, widths = row.screeninfo + self.assertEqual(prompt_w, 4) + self.assertEqual(widths, [1, 1, 1]) + + def test_screeninfo_with_suffix(self): + row = LayoutRow(4, (1, 1), suffix_width=1) + prompt_w, widths = row.screeninfo + self.assertEqual(prompt_w, 4) + self.assertEqual(widths, [1, 1, 1]) + + +class TestLayoutMap(TestCase): + def test_empty(self): + lm = LayoutMap.empty() + self.assertEqual(len(lm.rows), 1) + self.assertEqual(lm.max_row(), 0) + self.assertEqual(lm.max_column(0), 0) + + def test_screeninfo(self): + lm = LayoutMap(( + LayoutRow(4, (1, 1, 1)), + LayoutRow(0, (1, 1)), + )) + info = lm.screeninfo + self.assertEqual(len(info), 2) + self.assertEqual(info[0], (4, [1, 1, 1])) + self.assertEqual(info[1], (0, [1, 1])) + + def test_max_column(self): + lm = LayoutMap((LayoutRow(4, (1, 1, 1)),)) + self.assertEqual(lm.max_column(0), 7) + + def test_max_row(self): + lm = LayoutMap((LayoutRow(0, ()), LayoutRow(0, ()))) + self.assertEqual(lm.max_row(), 1) + + def test_pos_to_xy_empty_rows(self): + lm = LayoutMap(()) + self.assertEqual(lm.pos_to_xy(0), (0, 0)) + + def test_pos_to_xy_single_row(self): + lm = LayoutMap((LayoutRow(4, (1, 1, 1), buffer_advance=3),)) + self.assertEqual(lm.pos_to_xy(0), (4, 0)) + self.assertEqual(lm.pos_to_xy(1), (5, 0)) + self.assertEqual(lm.pos_to_xy(2), (6, 0)) + self.assertEqual(lm.pos_to_xy(3), (7, 0)) + + def test_pos_to_xy_multi_row(self): + lm = LayoutMap(( + LayoutRow(4, (1, 1, 1), buffer_advance=4), + LayoutRow(4, (1, 1), buffer_advance=2), + )) + # First row: pos 0-3 + self.assertEqual(lm.pos_to_xy(0), (4, 0)) + self.assertEqual(lm.pos_to_xy(3), (7, 0)) + # Second row: pos 4-5 + self.assertEqual(lm.pos_to_xy(4), (4, 1)) + self.assertEqual(lm.pos_to_xy(5), (5, 1)) + + def test_pos_to_xy_past_end_clamps(self): + lm = LayoutMap((LayoutRow(4, (1, 1), buffer_advance=2),)) + self.assertEqual(lm.pos_to_xy(99), (6, 0)) + + def test_pos_to_xy_skips_prompt_only_leading_rows(self): + lm = LayoutMap(( + LayoutRow(0, (), buffer_advance=0), # leading prompt-only row + LayoutRow(4, (1, 1), buffer_advance=3), + )) + # pos 0 should skip the leading row and land on the real row + self.assertEqual(lm.pos_to_xy(0), (4, 1)) + + def test_pos_to_xy_with_suffix(self): + lm = LayoutMap(( + LayoutRow(4, (1, 1), suffix_width=1, buffer_advance=2), + LayoutRow(0, (1,), buffer_advance=2), + )) + # pos=2 fits within first row's char_widths (len=2), cursor at end + self.assertEqual(lm.pos_to_xy(2), (6, 0)) + # pos=3 exceeds first row, lands on second row + self.assertEqual(lm.pos_to_xy(3), (1, 1)) + + def test_xy_to_pos_empty_rows(self): + lm = LayoutMap(()) + self.assertEqual(lm.xy_to_pos(0, 0), 0) + + def test_xy_to_pos_single_row(self): + lm = LayoutMap((LayoutRow(4, (1, 1, 1), buffer_advance=3),)) + self.assertEqual(lm.xy_to_pos(4, 0), 0) + self.assertEqual(lm.xy_to_pos(5, 0), 1) + self.assertEqual(lm.xy_to_pos(6, 0), 2) + self.assertEqual(lm.xy_to_pos(7, 0), 3) + + def test_xy_to_pos_multi_row(self): + lm = LayoutMap(( + LayoutRow(4, (1, 1, 1), buffer_advance=4), + LayoutRow(4, (1, 1), buffer_advance=2), + )) + self.assertEqual(lm.xy_to_pos(4, 0), 0) + self.assertEqual(lm.xy_to_pos(4, 1), 4) + self.assertEqual(lm.xy_to_pos(5, 1), 5) + + def test_xy_to_pos_before_prompt_returns_zero(self): + lm = LayoutMap((LayoutRow(4, (1, 1), buffer_advance=2),)) + self.assertEqual(lm.xy_to_pos(0, 0), 0) + + def test_xy_to_pos_with_zero_width_chars(self): + # Simulates combining characters (zero-width) after a base char + lm = LayoutMap((LayoutRow(4, (1, 0, 1), buffer_advance=3),)) + # x=5 is past the first char; trailing zero-width combining is included + self.assertEqual(lm.xy_to_pos(5, 0), 2) + + def test_xy_to_pos_zero_width_skipped(self): + lm = LayoutMap((LayoutRow(0, (0, 1, 1), buffer_advance=3),)) + # x=0: the zero-width char at index 0 is skipped, pos advances + self.assertEqual(lm.xy_to_pos(0, 0), 1) + + def test_xy_to_pos_zero_width_before_target(self): + # Zero-width char between two normal chars; target x is past it + lm = LayoutMap((LayoutRow(0, (1, 0, 1), buffer_advance=3),)) + # x=2: passes char at x=0 (w=1), skips zero-width at x=1, lands at x=2 + self.assertEqual(lm.xy_to_pos(2, 0), 3) + + def test_xy_to_pos_trailing_all_zero_width(self): + # All remaining chars from cursor position are zero-width + lm = LayoutMap((LayoutRow(0, (1, 0), buffer_advance=2),)) + # x=1: past first char, trailing loop exhausts (all remaining are 0-width) + self.assertEqual(lm.xy_to_pos(1, 0), 2) + + +class TestLayoutContentLines(TestCase): + def test_zero_width_returns_empty(self): + result = layout_content_lines((), 0, 0) + self.assertEqual(result.wrapped_rows, ()) + self.assertEqual(result.layout_map.rows, ()) + + def test_negative_width_returns_empty(self): + result = layout_content_lines((), -1, 0) + self.assertEqual(result.wrapped_rows, ()) + + def test_single_short_line(self): + line = _content_line("abc") + result = layout_content_lines((line,), 80, 0) + + self.assertEqual(len(result.wrapped_rows), 1) + row = result.wrapped_rows[0] + self.assertEqual(row.prompt_text, ">>> ") + self.assertEqual(row.prompt_width, 4) + self.assertEqual(row.suffix, "") + self.assertEqual(row.buffer_advance, 4) # 3 chars + newline + + def test_single_line_no_newline(self): + line = _content_line("ab", has_newline=False) + result = layout_content_lines((line,), 80, 0) + + self.assertEqual(len(result.wrapped_rows), 1) + self.assertEqual(result.wrapped_rows[0].buffer_advance, 2) + + def test_empty_body(self): + source = _source("", has_newline=True) + prompt = _prompt() + line = ContentLine(source, prompt, ()) + result = layout_content_lines((line,), 80, 0) + + self.assertEqual(len(result.wrapped_rows), 1) + self.assertEqual(result.wrapped_rows[0].buffer_advance, 1) # just newline + + def test_line_wraps(self): + # prompt ">>> " is 4 wide, terminal is 10 wide, so 6 chars fit per row + line = _content_line("abcdefgh") # 8 chars, needs 2 rows + result = layout_content_lines((line,), 10, 0) + + self.assertEqual(len(result.wrapped_rows), 2) + first, second = result.wrapped_rows + self.assertEqual(first.prompt_text, ">>> ") + self.assertEqual(first.suffix, "\\") + self.assertEqual(first.suffix_width, 1) + # First row: 10 - 4(prompt) - 1(suffix) = 5 chars + self.assertEqual(first.buffer_advance, 5) + # Second row: continuation with no prompt + self.assertEqual(second.prompt_text, "") + self.assertEqual(second.prompt_width, 0) + self.assertEqual(second.buffer_advance, 4) # remaining 3 + newline + self.assertEqual(second.suffix, "") + + def test_wrapping_forces_progress(self): + # When a single character is wider than available space, force 1 char + prompt = _prompt("P", 1) + body = (ContentFragment("W", 1),) + source = _source("W", has_newline=False) + line = ContentLine(source, prompt, body) + # width=2 means prompt(1) + char(1) = 2, which fits (< width would be + # false for width=2 since 1+1 >= 2), so it wraps but forces progress + result = layout_content_lines((line,), 2, 0) + + self.assertEqual(len(result.wrapped_rows), 1) + self.assertEqual(result.wrapped_rows[0].buffer_advance, 1) + + def test_layout_map_matches_wrapped_rows(self): + line = _content_line("abc") + result = layout_content_lines((line,), 80, 0) + + self.assertEqual(len(result.layout_map.rows), len(result.wrapped_rows)) + self.assertEqual(result.layout_map.rows[0].prompt_width, 4) + self.assertEqual(result.layout_map.rows[0].char_widths, (1, 1, 1)) + + def test_line_end_offsets(self): + line1 = _content_line("ab") + line2 = _content_line("cd") + result = layout_content_lines((line1, line2), 80, 0) + + self.assertEqual(len(result.line_end_offsets), 2) + # line1: 2 chars + 1 newline = offset 3 + self.assertEqual(result.line_end_offsets[0], 3) + # line2: offset 3 + 2 chars + 1 newline = 6 + self.assertEqual(result.line_end_offsets[1], 6) + + def test_start_offset_shifts_offsets(self): + line = _content_line("ab") + result = layout_content_lines((line,), 80, 10) + + self.assertEqual(result.line_end_offsets[0], 13) + + def test_multiple_lines(self): + line1 = _content_line("abc") + line2 = _content_line("de") + result = layout_content_lines((line1, line2), 80, 0) + + self.assertEqual(len(result.wrapped_rows), 2) + self.assertEqual(result.wrapped_rows[0].buffer_advance, 4) # abc + \n + self.assertEqual(result.wrapped_rows[1].buffer_advance, 3) # de + \n + + def test_leading_prompt_lines(self): + leading = (ContentFragment("header", 6),) + prompt = PromptContent(leading, ">>> ", 4) + body = _body_from_text("x") + source = _source("x", has_newline=False) + line = ContentLine(source, prompt, body) + result = layout_content_lines((line,), 80, 0) + + # Leading line + body line + self.assertEqual(len(result.wrapped_rows), 2) + # Leading row has the fragment but no prompt + self.assertEqual(result.wrapped_rows[0].fragments, leading) + # Body row has prompt and content + self.assertEqual(result.wrapped_rows[1].prompt_text, ">>> ") + + def test_wrapped_line_layout_rows_have_suffix(self): + line = _content_line("abcdefgh") + result = layout_content_lines((line,), 10, 0) + + first_layout = result.layout_map.rows[0] + self.assertEqual(first_layout.suffix_width, 1) + second_layout = result.layout_map.rows[1] + self.assertEqual(second_layout.suffix_width, 0) + + def test_pos_to_xy_through_layout(self): + line = _content_line("abc") + result = layout_content_lines((line,), 80, 0) + lm = result.layout_map + + self.assertEqual(lm.pos_to_xy(0), (4, 0)) # after prompt + self.assertEqual(lm.pos_to_xy(1), (5, 0)) + self.assertEqual(lm.pos_to_xy(2), (6, 0)) + self.assertEqual(lm.pos_to_xy(3), (7, 0)) # end of line diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 4b7579d821c..ef8c5784c00 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1,3 +1,4 @@ +import contextlib import importlib import io import itertools @@ -10,10 +11,18 @@ import subprocess import sys import tempfile +from functools import partial from pkgutil import ModuleInfo -from unittest import TestCase, skipUnless, skipIf, SkipTest +from unittest import TestCase, skipUnless, SkipTest from unittest.mock import Mock, patch -from test.support import force_not_colorized, make_clean_env, Py_DEBUG +import warnings +from test.support import ( + captured_stdout, + captured_stderr, + force_not_colorized, + make_clean_env, + Py_DEBUG, +) from test.support import has_subprocess_support, SHORT_TIMEOUT, STDLIB_DIR from test.support.import_helper import import_module from test.support.os_helper import EnvironmentVarGuard, unlink @@ -27,12 +36,17 @@ multiline_input, code_to_events, ) +from _colorize import ANSIColors, get_theme +from _pyrepl import terminfo from _pyrepl.console import Event +from _pyrepl.completing_reader import stripcolor from _pyrepl._module_completer import ( ImportParser, ModuleCompleter, HARDCODED_SUBMODULES, ) +from _pyrepl.fancycompleter import Completer as FancyCompleter, colorize_matches +import _pyrepl.readline as pyrepl_readline from _pyrepl.readline import ( ReadlineAlikeReader, ReadlineConfig, @@ -50,6 +64,10 @@ import readline as readline_module except ImportError: readline_module = None +try: + import tkinter +except ImportError: + tkinter = None class ReplTestCase(TestCase): @@ -758,6 +776,64 @@ def test_history_with_multiline_entries(self): self.assert_screen_equal(reader, expected, clean=True) self.assertEqual(output, expected) + def test_up_arrow_stays_within_recalled_multiline_entry(self): + code = ( + "def fo():\n" + "...\n" + "...\n" + "a = 1\n" + "b = 2\n" + "x = 1\n" + "\n" + "def fo():\n" + "...\n" + "...\n" + "a = 1\n" + "b = 2\n" + "x = 1\n" + "z = 2\n" + "\n" + ) + events = list(itertools.chain( + code_to_events(code), + [ + Event(evt="key", data="up", raw=bytearray(b"\x1bOA")), + Event(evt="key", data="up", raw=bytearray(b"\x1bOA")), + ] + )) + + reader = self.prepare_reader(events) + multiline_input(reader) + multiline_input(reader) + + expected = ( + "def fo():\n" + " ...\n" + " ...\n" + " a = 1\n" + " b = 2\n" + " x = 1\n" + " z = 2" + ) + reader.more_lines = partial(more_lines, namespace=None) + reader.ps1 = reader.ps2 = ">>> " + reader.ps3 = reader.ps4 = "... " + try: + reader.prepare() + reader.refresh() + + reader.handle1() + self.assertEqual(reader.historyi, 1) + self.assertEqual(reader.get_unicode(), expected) + first_cxy = reader.cxy + + reader.handle1() + self.assertEqual(reader.historyi, 1) + self.assertEqual(reader.get_unicode(), expected) + self.assertLess(reader.cxy[1], first_cxy[1]) + finally: + reader.restore() + def test_history_navigation_with_down_arrow(self): events = itertools.chain( @@ -803,12 +879,28 @@ def test_history_search(self): self.assertEqual(output, "1+1") self.assert_screen_equal(reader, "1+1", clean=True) + def test_history_file_embedded_nuls_are_sanitized(self): + reader = self.prepare_reader([]) + wrapper = _ReadlineWrapper(reader=reader, f_in=0, f_out=1) + with tempfile.NamedTemporaryFile("wb", delete=False) as history_file: + history_file.write(b"good\n") + history_file.write(b"ba\0d\n") + history_file.write(b"line1\r\nline2\0\n") + filename = history_file.name + + try: + wrapper.read_history_file(filename) + finally: + unlink(filename) + + self.assertEqual(reader.history, ["good", "bad", "line1\nline2"]) + def test_control_character(self): events = code_to_events("c\x1d\n") reader = self.prepare_reader(events) output = multiline_input(reader) self.assertEqual(output, "c\x1d") - self.assert_screen_equal(reader, "c\x1d", clean=True) + self.assert_screen_equal(reader, "c^]", clean=True) def test_history_search_backward(self): # Test history search backward with "imp" input @@ -943,6 +1035,115 @@ def test_func(self): self.assertEqual(mock_stderr.getvalue(), "") +class TestPyReplFancyCompleter(TestCase): + def prepare_reader(self, events, namespace, *, use_colors): + console = FakeConsole(events) + config = ReadlineConfig() + config.readline_completer = FancyCompleter( + namespace, use_colors=use_colors + ).complete + reader = ReadlineAlikeReader(console=console, config=config) + return reader + + def test_simple_completion_preserves_callable_postfix(self): + events = code_to_events("os.getpid\t\n") + + namespace = {"os": os} + reader = self.prepare_reader(events, namespace, use_colors=False) + + output = multiline_input(reader, namespace) + self.assertEqual(output, "os.getpid()") + + def test_attribute_menu_tracks_typed_stem(self): + class Obj: + apple = 1 + apricot = 2 + banana = 3 + + namespace = {"obj": Obj} + reader = self.prepare_reader( + code_to_events("obj.\t\ta"), + namespace, + use_colors=True, + ) + + with self.assertRaises(StopIteration): + while True: + reader.handle1() + + self.assertEqual("".join(reader.buffer), "obj.a") + self.assertTrue(reader.cmpltn_menu_visible) + menu = "\n".join(reader.cmpltn_menu) + self.assertIn("apple", menu) + self.assertIn("apricot", menu) + self.assertNotIn("banana", menu) + self.assertNotIn("mro", menu) + + def test_get_completions_sorts_colored_matches_by_visible_text(self): + console = FakeConsole(iter(())) + config = ReadlineConfig() + config.readline_completer = FancyCompleter( + { + "foo_str": "value", + "foo_int": 1, + "foo_none": None, + }, + use_colors=True, + ).complete + reader = ReadlineAlikeReader(console=console, config=config) + + matches, action = reader.get_completions("foo_") + + self.assertIsNone(action) + self.assertEqual( + [stripcolor(match) for match in matches], + ["foo_int", "foo_none", "foo_str"], + ) + + +class TestPyReplReadlineSetup(TestCase): + def test_setup_ignores_basic_completer_env_when_env_is_disabled(self): + class FakeFancyCompleter: + def __init__(self, namespace): + self.namespace = namespace + self.use_colors = Mock() + self.theme = Mock() + + def complete(self, text, state): + return None + + class FakeBasicCompleter(FakeFancyCompleter): + pass + + wrapper = Mock() + wrapper.config = ReadlineConfig() + stdin = Mock() + stdout = Mock() + stdin.fileno.return_value = 0 + stdout.fileno.return_value = 1 + + with ( + patch.object(pyrepl_readline, "_wrapper", wrapper), + patch.object(pyrepl_readline, "raw_input", None), + patch.object(pyrepl_readline, "FancyCompleter", FakeFancyCompleter), + patch.object(pyrepl_readline, "RLCompleter", FakeBasicCompleter), + patch.object(pyrepl_readline.sys, "stdin", stdin), + patch.object(pyrepl_readline.sys, "stdout", stdout), + patch.object(pyrepl_readline.sys, "flags", Mock(ignore_environment=True)), + patch.object(pyrepl_readline.os, "isatty", return_value=True), + patch.object(pyrepl_readline.os, "getenv") as mock_getenv, + patch("builtins.input", lambda prompt="": prompt), + ): + mock_getenv.return_value = "1" + pyrepl_readline._setup({}) + + self.assertIsInstance( + wrapper.config.readline_completer.__self__, + FakeFancyCompleter, + ) + mock_getenv.assert_not_called() + + class TestPyReplModuleCompleter(TestCase): def setUp(self): # Make iter_modules() search only the standard library. @@ -964,7 +1165,9 @@ def prepare_reader(self, events, namespace): reader = ReadlineAlikeReader(console=console, config=config) return reader - def test_import_completions(self): + @patch.dict(sys.modules, + {"importlib.resources": object()}) # don't propose to import it + def test_completions(self): cases = ( ("import path\t\n", "import pathlib"), ("import importlib.\t\tres\t\n", "import importlib.resources"), @@ -1018,7 +1221,7 @@ def test_sub_module_private_completions(self): # Return public methods by default ("from foo import \t\n", "from foo import public"), # Return private methods if explicitly specified - ("from foo import _\t\n", "from foo import _private"), + ("from foo import _p\t\n", "from foo import _private"), ) for code, expected in cases: with self.subTest(code=code): @@ -1039,12 +1242,13 @@ def test_builtin_completion_top_level(self): output = reader.readline() self.assertEqual(output, expected) - def test_relative_import_completions(self): + def test_relative_completions(self): cases = ( (None, "from .readl\t\n", "from .readl"), (None, "from . import readl\t\n", "from . import readl"), ("_pyrepl", "from .readl\t\n", "from .readline"), ("_pyrepl", "from . import readl\t\n", "from . import readline"), + ("_pyrepl", "from .readline import mul\t\n", "from .readline import multiline_input"), ("_pyrepl", "from .. import toodeep\t\n", "from .. import toodeep"), ("concurrent", "from .futures.i\t\n", "from .futures.interpreter"), ) @@ -1076,7 +1280,7 @@ def test_no_fallback_on_regular_completion(self): cases = ( ("import pri\t\n", "import pri"), ("from pri\t\n", "from pri"), - ("from typing import Na\t\n", "from typing import Na"), + ("from typong import Na\t\n", "from typong import Na"), ) for code, expected in cases: with self.subTest(code=code): @@ -1089,8 +1293,8 @@ def test_global_cache(self): with (tempfile.TemporaryDirectory() as _dir1, patch.object(sys, "path", [_dir1, *sys.path])): dir1 = pathlib.Path(_dir1) - (dir1 / "mod_aa.py").mkdir() - (dir1 / "mod_bb.py").mkdir() + (dir1 / "mod_aa.py").touch() + (dir1 / "mod_bb.py").touch() events = code_to_events("import mod_a\t\nimport mod_b\t\n") reader = self.prepare_reader(events, namespace={}) output_1, output_2 = reader.readline(), reader.readline() @@ -1100,7 +1304,8 @@ def test_global_cache(self): def test_hardcoded_stdlib_submodules(self): cases = ( ("import collections.\t\n", "import collections.abc"), - ("from os import \t\n", "from os import path"), + ("import os.\t\n", "import os.path"), + ("import math.\t\n", "import math.integer"), ("import xml.parsers.expat.\t\te\t\n\n", "import xml.parsers.expat.errors"), ("from xml.parsers.expat import \t\tm\t\n\n", "from xml.parsers.expat import model"), ) @@ -1214,6 +1419,118 @@ def test_already_imported_module_without_origin_or_spec(self): self.assertEqual(output, f"import {mod}.") del sys.modules[mod] + @patch.dict(sys.modules) + def test_attribute_completion(self): + with tempfile.TemporaryDirectory() as _dir: + dir = pathlib.Path(_dir) + (dir / "foo.py").write_text("bar = 42") + (dir / "bar.py").write_text("baz = 42") + (dir / "pack").mkdir() + (dir / "pack" / "__init__.py").write_text("attr = 42") + (dir / "pack" / "foo.py").touch() + (dir / "pack" / "bar.py").touch() + (dir / "pack" / "baz.py").touch() + sys.modules.pop("graphlib", None) # test modules may have been imported by previous tests + sys.modules.pop("antigravity", None) + sys.modules.pop("unittest.__main__", None) + with patch.object(sys, "path", [_dir, *sys.path]): + pkgutil.get_importer(_dir).invalidate_caches() + importlib.import_module("bar") + cases = ( + # needs 2 tabs to import (show prompt, then import) + ("from foo import \t\n", "from foo import ", set()), + ("from foo import \t\t\n", "from foo import bar", {"foo"}), + ("from foo import ba\t\n", "from foo import ba", set()), + ("from foo import ba\t\t\n", "from foo import bar", {"foo"}), + # reset if a character is inserted between tabs + ("from foo import \tb\ta\t\n", "from foo import ba", set()), + # packages: needs 3 tabs ([ not unique ], prompt, import) + ("from pack import \t\t\n", "from pack import ", set()), + ("from pack import \t\t\t\n", "from pack import ", {"pack"}), + ("from pack import \t\t\ta\t\n", "from pack import attr", {"pack"}), + # one match: needs 2 tabs (insert + show prompt, import) + ("from pack import f\t\n", "from pack import foo", set()), + ("from pack import f\t\t\n", "from pack import foo", {"pack"}), + # common prefix: needs 3 tabs (insert + [ not unique ], prompt, import) + ("from pack import b\t\n", "from pack import ba", set()), + ("from pack import b\t\t\n", "from pack import ba", set()), + ("from pack import b\t\t\t\n", "from pack import ba", {"pack"}), + # module already imported + ("from bar import b\t\n", "from bar import baz", set()), + # stdlib modules are automatically imported + ("from graphlib import T\t\n", "from graphlib import TopologicalSorter", {"graphlib"}), + # except those with known side-effects + ("from antigravity import g\t\n", "from antigravity import g", set()), + ("from unittest.__main__ import \t\n", "from unittest.__main__ import ", set()), + ) + for code, expected, expected_imports in cases: + with self.subTest(code=code), patch.dict(sys.modules): + _imported = set(sys.modules.keys()) + events = code_to_events(code) + reader = self.prepare_reader(events, namespace={}) + output = reader.readline() + self.assertEqual(output, expected) + # The reader imports its own helpers lazily. + new_imports = {name for name in + sys.modules.keys() - _imported + if not name.startswith('_pyrepl.')} + self.assertEqual(new_imports, expected_imports) + + @patch.dict(sys.modules) + def test_attribute_completion_error_on_import(self): + with tempfile.TemporaryDirectory() as _dir: + dir = pathlib.Path(_dir) + (dir / "foo.py").write_text("bar = 42") + (dir / "boom.py").write_text("1 <> 2") + with patch.object(sys, "path", [_dir, *sys.path]): + cases = ( + ("from boom import \t\t\n", "from boom import "), + ("from foo import \t\t\n", "from foo import bar"), # still working + ) + for code, expected in cases: + with self.subTest(code=code): + events = code_to_events(code) + reader = self.prepare_reader(events, namespace={}) + output = reader.readline() + self.assertEqual(output, expected) + self.assertNotIn("boom", sys.modules) + + @patch.dict(sys.modules) + def test_attribute_completion_error_on_attributes_access(self): + with tempfile.TemporaryDirectory() as _dir: + dir = pathlib.Path(_dir) + (dir / "boom").mkdir() + (dir / "boom"/"__init__.py").write_text("def __dir__(): raise ValueError()") + (dir / "boom"/"submodule.py").touch() + with patch.object(sys, "path", [_dir, *sys.path]): + events = code_to_events("from boom import \t\t\n") # trigger import + reader = self.prepare_reader(events, namespace={}) + output = reader.readline() + self.assertIn("boom", sys.modules) + # ignore attributes, just propose submodule + self.assertEqual(output, "from boom import submodule") + + @patch.dict(sys.modules) + def test_attribute_completion_private_and_invalid_names(self): + with tempfile.TemporaryDirectory() as _dir: + dir = pathlib.Path(_dir) + (dir / "foo.py").write_text("_secret = 'bar'") + with patch.object(sys, "path", [_dir, *sys.path]): + mod = importlib.import_module("foo") + mod.__dict__["invalid-identifier"] = "baz" + cases = ( + ("from foo import \t\n", "from foo import "), + ("from foo import _s\t\n", "from foo import _secret"), + ("from foo import inv\t\n", "from foo import inv"), + ) + for code, expected in cases: + with self.subTest(code=code): + events = code_to_events(code) + reader = self.prepare_reader(events, namespace={}) + output = reader.readline() + self.assertEqual(output, expected) + + def test_get_path_and_prefix(self): cases = ( ('', ('', '')), @@ -1346,8 +1663,163 @@ def test_parse_error(self): with self.subTest(code=code): self.assertEqual(actual, None) + @patch.dict(sys.modules) + def test_suggestions_and_messages(self) -> None: + # more unitary tests checking the exact suggestions provided + # (sorting, de-duplication, import action...) + _prompt = ("[ module not imported, press again to import it " + "and propose attributes ]") + _error = "[ error during import: division by zero ]" + with tempfile.TemporaryDirectory() as _dir: + dir = pathlib.Path(_dir) + (dir / "foo.py").write_text("bar = 42") + (dir / "boom.py").write_text("1/0") + (dir / "pack").mkdir() + (dir / "pack" / "__init__.py").write_text("foo = 1; bar = 2;") + (dir / "pack" / "bar.py").touch() + sys.modules.pop("graphlib", None) # test modules may have been imported by previous tests + sys.modules.pop("string.templatelib", None) + with patch.object(sys, "path", [_dir, *sys.path]): + pkgutil.get_importer(_dir).invalidate_caches() + # NOTE: Cases are intentionally sequential and share completer + # state. Earlier cases may import modules that later cases + # depend on. Do NOT reorder without understanding dependencies. + cases = ( + # no match != not an import + ("import nope", ([], None), set()), + ("improt nope", None, set()), + # names sorting + ("import col", (["collections", "colorsys"], None), set()), + # module auto-import + ("import fo", (["foo"], None), set()), + ("from foo import ", ([], (_prompt, None)), {"foo"}), + ("from foo import ", (["bar"], None), set()), # now imported + ("from foo import ba", (["bar"], None), set()), + # error during import + ("from boom import ", ([], (_prompt, _error)), set()), + ("from boom import ", ([], None), set()), # do not retry + # packages + ("from collections import a", (["abc"], None), set()), + ("from pack import ", (["bar"], (_prompt, None)), {"pack"}), + ("from pack import ", (["bar", "foo"], None), set()), + ("from pack.bar import ", ([], (_prompt, None)), {"pack.bar"}), + ("from pack.bar import ", ([], None), set()), + # stdlib = auto-imported + ("from graphlib import T", (["TopologicalSorter"], None), {"graphlib"}), + ("from string.templatelib import c", (["convert"], None), {"string.templatelib"}), + ) + completer = ModuleCompleter() + for i, (code, expected, expected_imports) in enumerate(cases): + with self.subTest(code=code, i=i): + _imported = set(sys.modules.keys()) + result = completer.get_completions(code) + self.assertEqual(result is None, expected is None) + if result: + compl, _values, act = result + self.assertEqual(compl, expected[0]) + self.assertEqual(act is None, expected[1] is None) + if act: + msg, func = act + self.assertEqual(msg, expected[1][0]) + act_result = func() + self.assertEqual(act_result, expected[1][1]) + + new_imports = sys.modules.keys() - _imported + self.assertSetEqual(new_imports, expected_imports) + + def test_colorize_import_completions(self) -> None: + theme = get_theme() + type_color = theme.fancycompleter.type + module_color = theme.fancycompleter.module + R = ANSIColors.RESET + + colorize = lambda names, values: colorize_matches(names, values, theme) + config = ReadlineConfig(colorize_completions=colorize) + reader = ReadlineAlikeReader( + console=FakeConsole(events=[]), + config=config, + ) + + # "from collections import de" -> defaultdict (type) and deque (type) + reader.buffer = list("from collections import de") + reader.pos = len(reader.buffer) + names, action = reader.get_module_completions() + self.assertEqual(names, [ + f"{type_color}defaultdict{R}", + f"{type_color}deque{R}", + ]) + self.assertIsNone(action) + + # "from importlib.m" has submodule completions colored as modules + reader.buffer = list("from importlib.m") + reader.pos = len(reader.buffer) + names, action = reader.get_module_completions() + self.assertEqual(names, [ + f"{module_color}importlib.machinery{R}", + f"{module_color}importlib.metadata{R}", + ]) + self.assertIsNone(action) + + # Make sure attributes take precedence over submodules when both exist + # Here we're using `unittest.main` which happens to be both a module and an attribute + reader.buffer = list("from unittest import m") + reader.pos = len(reader.buffer) + names, action = reader.get_module_completions() + self.assertEqual(names, [ + f"{type_color}main{R}", # Ensure that `main` is colored as an attribute (class in this case) + f"{module_color}mock{R}", + ]) + self.assertIsNone(action) + + +# Audit hook used to check for stdlib modules import side-effects +# Defined globally to avoid adding one hook per test run (refleak) +_audit_events: set[str] | None = None + + +def _hook(name: str, _args: tuple): + if _audit_events is not None: # No-op when not activated + _audit_events.add(name) +sys.addaudithook(_hook) + + +@contextlib.contextmanager +def _capture_audit_events(): + global _audit_events + _audit_events = set() + try: + yield _audit_events + finally: + _audit_events = None + + +class TestModuleCompleterAutomaticImports(TestCase): + def test_no_side_effects(self): + from test.test___all__ import AllTest # TODO: extract to a helper? + + completer = ModuleCompleter() + for _, modname in AllTest().walk_modules(completer._stdlib_path, ""): + with self.subTest(modname=modname): + with (captured_stdout() as out, + captured_stderr() as err, + _capture_audit_events() as audit_events, + (patch("tkinter._tkinter.create") if tkinter + else contextlib.nullcontext()) as tk_mock, + warnings.catch_warnings(action="ignore")): + completer._maybe_import_module(modname) + # Test no module is imported that + # 1. prints any text + self.assertEqual(out.getvalue(), "") + self.assertEqual(err.getvalue(), "") + # 2. spawn any subprocess (eg. webbrowser.open) + self.assertNotIn("subprocess.Popen", audit_events) + # 3. launch a Tk window + if tk_mock is not None: + tk_mock.assert_not_called() + class TestHardcodedSubmodules(TestCase): + @patch.dict(sys.modules) def test_hardcoded_stdlib_submodules_are_importable(self): for parent_path, submodules in HARDCODED_SUBMODULES.items(): for module_name in submodules: @@ -1534,8 +2006,20 @@ def test_dumb_terminal_exits_cleanly(self): self.assertNotIn("Traceback", output) +def supports_pyrepl(): + # pyrepl falls back to the basic REPL if the terminal lacks any of the + # capabilities which UnixConsole requires. This covers an unset or + # "dumb" TERM as well, they are resolved to the "dumb" capabilities. + try: + info = terminfo.TermInfo(None) + except Exception: + return False + return all(info.get(cap) is not None + for cap in ("bel", "clear", "cup", "el")) + + @skipUnless(pty, "requires pty") -@skipIf((os.environ.get("TERM") or "dumb") == "dumb", "can't use pyrepl in dumb terminal") +@skipUnless(supports_pyrepl(), "can't use pyrepl in this terminal") class TestMain(ReplTestCase): def setUp(self): # Cleanup from PYTHON* variables to isolate from local @@ -1560,10 +2044,10 @@ def test_exposed_globals_in_repl(self): case2 = f"{pre}, '__doc__', '__file__', {post}" in output # if `__main__` is a cached .pyc file and the .py source exists - case3 = f"{pre}, '__cached__', '__doc__', '__file__', {post}" in output + case3 = f"{pre}, '__doc__', '__file__', {post}" in output # if `__main__` is a cached .pyc file but there's no .py source file - case4 = f"{pre}, '__cached__', '__doc__', {post}" in output + case4 = f"{pre}, '__doc__', {post}" in output self.assertTrue(case1 or case2 or case3 or case4, output) @@ -1935,12 +2419,24 @@ def test_showrefcount(self): matches = re.findall(r'\[-?\d+ refs, \d+ blocks\]', output) self.assertEqual(len(matches), 3) + def test_detect_pip_usage_in_repl(self): + for pip_cmd in ("pip", "pip3", "python -m pip", "python3 -m pip"): + with self.subTest(pip_cmd=pip_cmd): + output, exit_code = self.run_repl([f"{pip_cmd} install sampleproject", "exit"]) + self.assertIn("SyntaxError", output) + hint = ( + "The Python package manager (pip) can only be used" + " outside of the Python REPL" + ) + self.assertIn(hint, output) @force_not_colorized def test_no_newline(self): env = os.environ.copy() env.pop("PYTHON_BASIC_REPL", "") env["PYTHON_BASIC_REPL"] = "1" + # Ensure user's .inputrc doesn't interfere with basic REPL output + env["INPUTRC"] = "/dev/null" commands = "print('Something pretty long', end='')\nexit()\n" expected_output_sequence = "Something pretty long>>> exit()" @@ -2112,12 +2608,13 @@ def test_ctrl_d_single_line_end_no_newline(self): class TestWindowsConsoleEolWrap(TestCase): def _make_mock_console(self, width=80): from _pyrepl import windows_console as wc + from _pyrepl.render import RenderedScreen console = object.__new__(wc.WindowsConsole) console.width = width console.posxy = (0, 0) - console.screen = [""] + console._rendered_screen = RenderedScreen.from_screen_lines([""], (0, 0)) console._hide_cursor = Mock() console._show_cursor = Mock() @@ -2128,25 +2625,29 @@ def _make_mock_console(self, width=80): return console, wc + def _apply_changed_line(self, console, wc, y, old_line, new_line, px=0): + from _pyrepl.render import RenderLine + + old_render = RenderLine.from_rendered_text(old_line) + new_render = RenderLine.from_rendered_text(new_line) + update = wc.WindowsConsole._WindowsConsole__plan_changed_line( + console, y, old_render, new_render, px + ) + if update is not None: + wc.WindowsConsole._WindowsConsole__apply_line_update( + console, update + ) + def test_short_line_sets_posxy_normally(self): width = 10 y = 3 console, wc = self._make_mock_console(width=width) - old_line = "" - new_line = "a" * 3 - wc.WindowsConsole._WindowsConsole__write_changed_line( - console, y, old_line, new_line, 0 - ) + self._apply_changed_line(console, wc, y, "", "a" * 3) self.assertEqual(console.posxy, (3, y)) def test_exact_width_line_does_not_wrap(self): width = 10 y = 3 console, wc = self._make_mock_console(width=width) - old_line = "" - new_line = "a" * width - - wc.WindowsConsole._WindowsConsole__write_changed_line( - console, y, old_line, new_line, 0 - ) + self._apply_changed_line(console, wc, y, "", "a" * width) self.assertEqual(console.posxy, (width - 1, y)) diff --git a/Lib/test/test_pyrepl/test_reader.py b/Lib/test/test_pyrepl/test_reader.py index 51644ec7ce4..34ea0f50b30 100644 --- a/Lib/test/test_pyrepl/test_reader.py +++ b/Lib/test/test_pyrepl/test_reader.py @@ -10,6 +10,8 @@ from .support import ScreenEqualMixin, code_to_events from .support import prepare_reader, prepare_console from _pyrepl.console import Event +from _pyrepl.layout import LayoutMap +from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig from _pyrepl.reader import Reader from _colorize import default_theme @@ -20,8 +22,48 @@ colors = {overrides.get(k, k[0].lower()): v for k, v in default_theme.syntax.items()} +def prepare_reader_multiline_prompt(*args, **kwargs): + reader = prepare_reader(*args, **kwargs) + del reader.get_prompt + reader.ps1 = "Python 3.15\n>>> " + reader.ps2 = "Python 3.15\n>>> " + reader.ps3 = "Python 3.15\n... " + reader.ps4 = "Python 3.15\n... " + reader.can_colorize = False + reader.paste_mode = False + return reader + + @force_not_colorized_test_class class TestReader(ScreenEqualMixin, TestCase): + def assert_multiline_prompt_screen(self, code, expected_screen, expected_cxy): + reader, _ = handle_all_events( + code_to_events(code), + prepare_reader=prepare_reader_multiline_prompt, + ) + + self.assertEqual(reader.screen, expected_screen) + self.assertEqual(reader.cxy, expected_cxy) + + def test_multiline_prompt_does_not_duplicate_leading_lines(self): + self.assert_multiline_prompt_screen( + "abc", + ["Python 3.15", ">>> abc"], + (7, 1), + ) + + def test_multiline_prompt_does_not_duplicate_leading_lines_across_buffer_lines(self): + self.assert_multiline_prompt_screen( + "if x:\n y", + [ + "Python 3.15", + ">>> if x:", + "Python 3.15", + "... y", + ], + (13, 3), + ) + def test_calc_screen_wrap_simple(self): events = code_to_events(10 * "a") reader, _ = handle_events_narrow_console(events) @@ -104,6 +146,22 @@ def test_calc_screen_backspace(self): reader, _ = handle_all_events(events) self.assert_screen_equal(reader, "aa") + def test_refresh_escapes_control_bytes_in_buffer(self): + console = prepare_console(()) + config = ReadlineConfig(readline_completer=None) + reader = ReadlineAlikeReader(console=console, config=config) + reader.can_colorize = False + reader.ps1 = reader.ps2 = ">>> " + reader.ps3 = reader.ps4 = "... " + reader.buffer = ["\x00", "\x1b"] + reader.pos = len(reader.buffer) + reader.invalidate_full() + + reader.refresh() + + self.assert_screen_equal(reader, ">>> ^@^[") + self.assertEqual(reader.cxy, (8, 0)) + def test_calc_screen_wrap_removes_after_backspace(self): events = itertools.chain( code_to_events(10 * "a"), @@ -178,7 +236,7 @@ def test_up_arrow_after_ctrl_r(self): ) reader, _ = handle_all_events(events) - self.assert_screen_equal(reader, "") + self.assertIn(reader.screen, ([], [""])) def test_newline_within_block_trailing_whitespace(self): # fmt: off @@ -303,6 +361,21 @@ def test_prompt_length(self): self.assertEqual(prompt, "\033[0;32m樂>\033[0m> ") self.assertEqual(l, 5) + def test_prepare_with_zero_width_does_not_crash(self): + console = prepare_console([], width=0) + reader = ReadlineAlikeReader(console=console, config=ReadlineConfig()) + reader.ps1 = ">>> " + reader.ps2 = ">>> " + reader.ps3 = "... " + reader.ps4 = "" + reader.can_colorize = False + reader.paste_mode = False + + reader.prepare() + + self.assertEqual(reader.cxy, (0, 0)) + self.assertEqual(reader.screen, []) + def test_completions_updated_on_key_press(self): namespace = {"itertools": itertools} code = "itertools." @@ -349,8 +422,7 @@ def test_key_press_on_tab_press_once(self): def test_pos2xy_with_no_columns(self): console = prepare_console([]) reader = prepare_reader(console) - # Simulate a resize to 0 columns - reader.screeninfo = [] + reader.layout = LayoutMap(()) self.assertEqual(reader.pos2xy(), (0, 0)) def test_setpos_from_xy_for_non_printing_char(self): diff --git a/Lib/test/test_pyrepl/test_render.py b/Lib/test/test_pyrepl/test_render.py new file mode 100644 index 00000000000..5479f2eb4ba --- /dev/null +++ b/Lib/test/test_pyrepl/test_render.py @@ -0,0 +1,311 @@ +from unittest import TestCase +from _pyrepl.render import ( + LineUpdate, + RenderCell, + RenderLine, + RenderedScreen, + ScreenOverlay, + StyleRef, + diff_render_lines, + render_cells, + requires_cursor_resync, +) + + +class TestRenderLine(TestCase): + def test_from_rendered_text_parses_style_state(self): + line = RenderLine.from_rendered_text("\x1b[31ma\x1b[0mb") + + self.assertEqual(line.width, 2) + self.assertEqual([cell.text for cell in line.cells], ["a", "b"]) + self.assertEqual([cell.style.sgr for cell in line.cells], ["\x1b[31m", ""]) + self.assertEqual([cell.controls for cell in line.cells], [(), ()]) + + def test_from_rendered_text_round_trips_trailing_reset(self): + line = RenderLine.from_rendered_text("\x1b[31ma\x1b[0m") + + self.assertEqual([cell.text for cell in line.cells], ["a"]) + self.assertEqual([cell.style.sgr for cell in line.cells], ["\x1b[31m"]) + self.assertEqual(line.text, "\x1b[31ma\x1b[0m") + + def test_from_parts_accepts_style_refs(self): + line = RenderLine.from_parts( + ["d", "e", "f", " "], + [1, 1, 1, 1], + [StyleRef.from_sgr("\x1b[1;34m")] * 3 + [None], + ) + + self.assertEqual([cell.text for cell in line.cells], ["d", "e", "f", " "]) + self.assertEqual( + [cell.style.sgr for cell in line.cells], + ["\x1b[1;34m", "\x1b[1;34m", "\x1b[1;34m", ""], + ) + self.assertEqual(line.text, "\x1b[1;34mdef\x1b[0m ") + + def test_from_parts_without_styles(self): + line = RenderLine.from_parts(["a", "b"], [1, 1]) + + self.assertEqual(line.text, "ab") + self.assertEqual(line.width, 2) + + def test_from_rendered_text_empty_string(self): + line = RenderLine.from_rendered_text("") + + self.assertEqual(line.cells, ()) + self.assertEqual(line.text, "") + self.assertEqual(line.width, 0) + + def test_from_rendered_text_with_non_sgr_controls(self): + # \x1b[H is a cursor-home control (not SGR since it doesn't end with 'm') + line = RenderLine.from_rendered_text("\x1b[Hx") + + self.assertEqual(len(line.cells), 2) + self.assertEqual(line.cells[0].controls, ("\x1b[H",)) + self.assertEqual(line.cells[0].text, "") + self.assertEqual(line.cells[1].text, "x") + + def test_from_rendered_text_trailing_non_sgr_control(self): + line = RenderLine.from_rendered_text("a\x1b[K") + + self.assertEqual(len(line.cells), 2) + self.assertEqual(line.cells[0].text, "a") + self.assertEqual(line.cells[1].controls, ("\x1b[K",)) + self.assertEqual(line.cells[1].text, "") + + def test_from_rendered_text_non_sgr_before_text(self): + # Non-SGR control immediately before text should produce a control cell + # then text cells. + line = RenderLine.from_rendered_text("\x1b[Kab") + + texts = [c.text for c in line.cells] + self.assertEqual(texts, ["", "a", "b"]) + self.assertEqual(line.cells[0].controls, ("\x1b[K",)) + + +class TestLineDiff(TestCase): + def test_diff_render_lines_ignores_unchanged_ansi_prefix(self): + old = RenderLine.from_rendered_text("\x1b[31ma\x1b[0mb") + new = RenderLine.from_rendered_text("\x1b[31ma\x1b[0mc") + + diff = diff_render_lines(old, new) + + self.assertIsNotNone(diff) + assert diff is not None + self.assertEqual(diff.start_x, 1) + self.assertEqual(diff.old_text, "b") + self.assertEqual(diff.new_text, "c") + + def test_diff_render_lines_detects_single_cell_insertion(self): + old = RenderLine.from_rendered_text("ab") + new = RenderLine.from_rendered_text("acb") + + diff = diff_render_lines(old, new) + + self.assertIsNotNone(diff) + assert diff is not None + self.assertEqual(diff.start_x, 1) + self.assertEqual(diff.old_text, "") + self.assertEqual(diff.new_text, "c") + + def test_colored_append_only_emits_new_character_and_reset(self): + old = RenderLine.from_rendered_text("\x1b[1mabc\x1b[0m") + new = RenderLine.from_rendered_text("\x1b[1mabcd\x1b[0m") + + diff = diff_render_lines(old, new) + + self.assertIsNotNone(diff) + assert diff is not None + self.assertEqual(diff.start_x, 3) + self.assertEqual(render_cells(diff.new_cells), "\x1b[1md\x1b[0m") + + def test_keyword_space_inserts_only_space_after_reset(self): + old = RenderLine.from_parts( + ["d", "e", "f"], + [1, 1, 1], + ["keyword", "keyword", "keyword"], + ) + new = RenderLine.from_parts( + ["d", "e", "f", " "], + [1, 1, 1, 1], + ["keyword", "keyword", "keyword", None], + ) + + diff = diff_render_lines(old, new) + + self.assertIsNotNone(diff) + assert diff is not None + self.assertEqual(diff.start_x, 3) + self.assertEqual(render_cells(diff.new_cells), " ") + + def test_diff_render_lines_returns_none_for_identical(self): + line = RenderLine.from_rendered_text("abc") + self.assertIsNone(diff_render_lines(line, line)) + + def test_diff_render_lines_breaks_on_controls_in_prefix(self): + old = RenderLine.from_cells([ + RenderCell("a", 1), + RenderCell("", 0, controls=("\x1b[K",)), + RenderCell("b", 1), + ]) + new = RenderLine.from_cells([ + RenderCell("a", 1), + RenderCell("", 0, controls=("\x1b[K",)), + RenderCell("c", 1), + ]) + + diff = diff_render_lines(old, new) + + self.assertIsNotNone(diff) + assert diff is not None + # Prefix scan stops at control cell, so diff starts at cell 1 + self.assertEqual(diff.start_cell, 1) + self.assertEqual(diff.start_x, 1) + + def test_diff_render_lines_extends_past_combining_chars(self): + # \u0301 is a combining acute accent (zero-width) + old = RenderLine.from_parts(["a", "b", "\u0301"], [1, 1, 0]) + new = RenderLine.from_parts(["a", "c", "\u0301"], [1, 1, 0]) + + diff = diff_render_lines(old, new) + + self.assertIsNotNone(diff) + assert diff is not None + # The combining char is included since it's zero-width + self.assertEqual(len(diff.new_cells), 2) + + def test_diff_old_and_new_changed_width(self): + old = RenderLine.from_rendered_text("ab") + new = RenderLine.from_rendered_text("acd") + + diff = diff_render_lines(old, new) + + self.assertIsNotNone(diff) + assert diff is not None + self.assertEqual(diff.old_changed_width, 1) + self.assertEqual(diff.new_changed_width, 2) + + def test_rendered_screen_round_trips_screen_lines(self): + screen = RenderedScreen.from_screen_lines( + ["a", "\x1b[31mb\x1b[0m"], + (0, 1), + ) + + self.assertEqual(screen.screen_lines, ("a", "\x1b[31mb\x1b[0m")) + + +class TestRenderedScreen(TestCase): + def test_empty(self): + screen = RenderedScreen.empty() + + self.assertEqual(screen.lines, ()) + self.assertEqual(screen.cursor, (0, 0)) + self.assertEqual(screen.overlays, ()) + self.assertEqual(screen.composed_lines, ()) + + def test_with_overlay(self): + screen = RenderedScreen.from_screen_lines(["aaa", "bbb"], (0, 0)) + overlay_line = RenderLine.from_rendered_text("xxx") + result = screen.with_overlay(1, [overlay_line]) + + self.assertEqual(len(result.overlays), 1) + self.assertEqual(result.composed_lines[0].text, "aaa") + self.assertEqual(result.composed_lines[1].text, "xxx") + + def test_compose_replace_overlay(self): + base = RenderedScreen.from_screen_lines(["aa", "bb", "cc"], (0, 0)) + overlay_line = RenderLine.from_rendered_text("XX") + screen = RenderedScreen( + base.lines, + (0, 0), + (ScreenOverlay(y=1, lines=(overlay_line,)),), + ) + + self.assertEqual(screen.composed_lines[0].text, "aa") + self.assertEqual(screen.composed_lines[1].text, "XX") + self.assertEqual(screen.composed_lines[2].text, "cc") + + def test_compose_insert_overlay(self): + base = RenderedScreen.from_screen_lines(["aa", "bb"], (0, 0)) + overlay_line = RenderLine.from_rendered_text("INS") + screen = RenderedScreen( + base.lines, + (0, 0), + (ScreenOverlay(y=1, lines=(overlay_line,), insert=True),), + ) + + self.assertEqual(len(screen.composed_lines), 3) + self.assertEqual(screen.composed_lines[0].text, "aa") + self.assertEqual(screen.composed_lines[1].text, "INS") + self.assertEqual(screen.composed_lines[2].text, "bb") + + def test_compose_replace_extends_beyond_lines(self): + base = RenderedScreen.from_screen_lines(["aa"], (0, 0)) + overlay_line = RenderLine.from_rendered_text("XX") + screen = RenderedScreen( + base.lines, + (0, 0), + (ScreenOverlay(y=1, lines=(overlay_line,)),), + ) + + self.assertEqual(len(screen.composed_lines), 2) + self.assertEqual(screen.composed_lines[1].text, "XX") + + +class TestRenderCell(TestCase): + def test_terminal_text(self): + cell = RenderCell("x", 1, style=StyleRef.from_sgr("\x1b[32m")) + self.assertEqual(cell.terminal_text, "\x1b[32mx\x1b[0m") + + def test_terminal_text_plain(self): + cell = RenderCell("y", 1) + self.assertEqual(cell.terminal_text, "y") + + +class TestRenderCells(TestCase): + def test_render_cells_with_controls(self): + cells = [ + RenderCell("", 0, controls=("\x1b[K",)), + RenderCell("a", 1), + ] + result = render_cells(cells) + self.assertEqual(result, "\x1b[Ka") + + def test_render_cells_skips_empty_text(self): + cells = [ + RenderCell("", 0), + RenderCell("a", 1), + ] + result = render_cells(cells) + self.assertEqual(result, "a") + + def test_render_cells_with_visual_style(self): + cells = [RenderCell("a", 1)] + result = render_cells(cells, visual_style="\x1b[7m") + self.assertEqual(result, "\x1b[7ma\x1b[0m") + + +class TestLineUpdate(TestCase): + def test_post_init_renders_text(self): + cells = (RenderCell("a", 1), RenderCell("b", 1)) + update = LineUpdate( + kind="insert_char", + y=0, + start_cell=0, + start_x=0, + cells=cells, + ) + self.assertEqual(update.text, "ab") + + +class TestRequiresCursorResync(TestCase): + def test_no_controls(self): + cells = [RenderCell("a", 1)] + self.assertFalse(requires_cursor_resync(cells)) + + def test_sgr_only_does_not_require_resync(self): + cells = [RenderCell("", 0, controls=("\x1b[31m",))] + self.assertFalse(requires_cursor_resync(cells)) + + def test_non_sgr_requires_resync(self): + cells = [RenderCell("", 0, controls=("\x1b[H",))] + self.assertTrue(requires_cursor_resync(cells)) diff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py index 656a1e441e0..e8157f164b3 100644 --- a/Lib/test/test_pyrepl/test_utils.py +++ b/Lib/test/test_pyrepl/test_utils.py @@ -89,10 +89,58 @@ def test_gen_colors_keyword_highlighting(self): ("obj.list", [(".", "op")]), ("obj.match", [(".", "op")]), ("b. \\\n format", [(".", "op")]), + ("lazy", []), + ("lazy()", [('(', 'op'), (')', 'op')]), # highlights ("set", [("set", "builtin")]), ("list", [("list", "builtin")]), (" \n dict", [("dict", "builtin")]), + ( + " lazy import", + [("lazy", "soft_keyword"), ("import", "keyword")], + ), + ( + "lazy from cool_people import pablo", + [ + ("lazy", "soft_keyword"), + ("from", "keyword"), + ("import", "keyword"), + ], + ), + ( + "if sad: lazy import happy", + [ + ("if", "keyword"), + (":", "op"), + ("lazy", "soft_keyword"), + ("import", "keyword"), + ], + ), + ( + "pass; lazy import z", + [ + ("pass", "keyword"), + (";", "op"), + ("lazy", "soft_keyword"), + ("import", "keyword"), + ], + ), + ( + "match +1", + [ + ("match", "soft_keyword"), + ("+", "op"), + ("1", "number"), + ], + ), + ( + "match -1", + [ + ("match", "soft_keyword"), + ("-", "op"), + ("1", "number"), + ], + ), ] for code, expected_highlights in cases: with self.subTest(code=code): diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index 52a6f3e595c..698dd75c27d 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -6,11 +6,12 @@ import itertools +from _colorize import ANSIColors from functools import partial from test.support import force_not_colorized_test_class from typing import Iterable from unittest import TestCase -from unittest.mock import MagicMock, call +from unittest.mock import MagicMock, call, patch from .support import handle_all_events, code_to_events from .support import prepare_reader as default_prepare_reader @@ -30,7 +31,20 @@ pass +def _mock_console_init(self, f_in=0, f_out=1, term="", encoding="utf-8"): + """Mock __init__ to avoid real Windows API calls in headless environments.""" + super(WindowsConsole, self).__init__(f_in, f_out, term, encoding) + self.width = 80 + self.height = 25 + self._WindowsConsole__offset = 0 + self.posxy = (0, 0) + self._WindowsConsole__vt_support = False + self._WindowsConsole_original_input_mode = 0 + self.event_queue = wc.EventQueue('utf-8') + + @force_not_colorized_test_class +@patch.object(WindowsConsole, '__init__', _mock_console_init) class WindowsConsoleTests(TestCase): def console(self, events, **kwargs) -> Console: console = WindowsConsole() @@ -293,7 +307,8 @@ def same_console(events): call(self.move_left(5)), call(self.move_up()), call(b"def f():"), - call(self.move_left(3)), + call(self.move_left(8)), + call(self.move_right(5)), call(self.move_down()), ] ) @@ -366,7 +381,30 @@ def test_multiline_ctrl_z(self): self.assertEqual(reader.cxy, (2, 3)) con.restore() + def test_reset_on_finish(self): + # gh-152068: finish() must emit the ANSI reset sequence so any + # active color does not leak past the prompt. + code = "1" + events = code_to_events(code) + _, con = self.handle_events(events) + con.finish() + con.out.write.assert_any_call(ANSIColors.RESET.encode(con.encoding)) + con.restore() + + def test_reset_on_restore(self): + # gh-152068: restore() must emit the ANSI reset sequence when VT + # support is enabled. + code = "1" + events = code_to_events(code) + _, con = self.handle_events(events) + con._WindowsConsole__vt_support = True + con._WindowsConsole__original_input_mode = 0 + with patch.object(wc, "SetConsoleMode", return_value=1): + con.restore() + con.out.write.assert_any_call(ANSIColors.RESET.encode(con.encoding)) + +@patch.object(WindowsConsole, '__init__', _mock_console_init) class WindowsConsoleGetEventTests(TestCase): # Virtual-Key Codes: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes VK_BACK = 0x08 diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py index 7a480a62b6c..3d703c0a8ce 100644 --- a/Lib/test/test_re.py +++ b/Lib/test/test_re.py @@ -91,10 +91,13 @@ def test_search_star_plus(self): self.assertEqual(re.search('x+', 'axx').span(), (1, 3)) self.assertIsNone(re.search('x', 'aaa')) self.assertEqual(re.match('a*', 'xxx').span(0), (0, 0)) + self.assertEqual(re.prefixmatch('a*', 'xxx').span(0), (0, 0)) self.assertEqual(re.match('a*', 'xxx').span(), (0, 0)) self.assertEqual(re.match('x*', 'xxxa').span(0), (0, 3)) + self.assertEqual(re.prefixmatch('x*', 'xxxa').span(0), (0, 3)) self.assertEqual(re.match('x*', 'xxxa').span(), (0, 3)) self.assertIsNone(re.match('a+', 'xxx')) + self.assertIsNone(re.prefixmatch('a+', 'xxx')) def test_branching(self): """Test Branching @@ -181,6 +184,7 @@ def test_bug_449000(self): def test_bug_1661(self): # Verify that flags do not get silently ignored with compiled patterns pattern = re.compile('.') + self.assertRaises(ValueError, re.prefixmatch, pattern, 'A', re.I) self.assertRaises(ValueError, re.match, pattern, 'A', re.I) self.assertRaises(ValueError, re.search, pattern, 'A', re.I) self.assertRaises(ValueError, re.findall, pattern, 'A', re.I) @@ -518,6 +522,8 @@ def test_re_match(self): self.assertEqual(re.match(b'(a)', string).group(0), b'a') self.assertEqual(re.match(b'(a)', string).group(1), b'a') self.assertEqual(re.match(b'(a)', string).group(1, 1), (b'a', b'a')) + self.assertEqual(re.prefixmatch(b'(a)', string).group(1, 1), + (b'a', b'a')) for a in ("\xe0", "\u0430", "\U0001d49c"): self.assertEqual(re.match(a, a).groups(), ()) self.assertEqual(re.match('(%s)' % a, a).groups(), (a,)) @@ -559,10 +565,8 @@ def __index__(self): self.assertEqual(m.group(2, 1), ('b', 'a')) self.assertEqual(m.group(Index(2), Index(1)), ('b', 'a')) - def test_match_getitem(self): - pat = re.compile('(?:(?Pa)|(?Pb))(?Pc)?') - - m = pat.match('a') + def do_test_match_getitem(self, match_fn): + m = match_fn('a') self.assertEqual(m['a1'], 'a') self.assertEqual(m['b2'], None) self.assertEqual(m['c3'], None) @@ -586,7 +590,7 @@ def test_match_getitem(self): with self.assertRaisesRegex(IndexError, 'no such group'): 'a1={a2}'.format_map(m) - m = pat.match('ac') + m = match_fn('ac') self.assertEqual(m['a1'], 'a') self.assertEqual(m['b2'], None) self.assertEqual(m['c3'], 'c') @@ -603,6 +607,14 @@ def test_match_getitem(self): # No len(). self.assertRaises(TypeError, len, m) + def test_match_getitem(self): + pat = re.compile('(?:(?Pa)|(?Pb))(?Pc)?') + self.do_test_match_getitem(pat.match) + + def test_prefixmatch_getitem(self): + pat = re.compile('(?:(?Pa)|(?Pb))(?Pc)?') + self.do_test_match_getitem(pat.prefixmatch) + def test_re_fullmatch(self): # Issue 16203: Proposal: add re.fullmatch() method. self.assertEqual(re.fullmatch(r"a", "a").span(), (0, 1)) @@ -1641,6 +1653,24 @@ def s_int(scanner, token): return int(token) (['sum', 'op=', 3, 'op*', 'foo', 'op+', 312.5, 'op+', 'bar'], '')) + def test_bug_gh140797(self): + # gh140797: Capturing groups are not allowed in re.Scanner + + msg = r"Cannot use capturing groups in re\.Scanner" + # Capturing group throws an error + with self.assertRaisesRegex(ValueError, msg): + Scanner([("(a)b", None)]) + + # Named Group + with self.assertRaisesRegex(ValueError, msg): + Scanner([("(?Pa)", None)]) + + # Non-capturing groups should pass normally + s = Scanner([("(?:a)b", lambda scanner, token: token)]) + result, rem = s.scan("ab") + self.assertEqual(result,['ab']) + self.assertEqual(rem,'') + def test_bug_448951(self): # bug 448951 (similar to 429357, but with single char match) # (Also test greedy matches.) @@ -2028,7 +2058,7 @@ def test_large_search(self, size): # The huge memuse is because of re.sub() using a list and a join() # to create the replacement result. - @bigmemtest(size=_2G, memuse=16 + 2) + @bigmemtest(size=_2G, memuse=16 + 3) def test_large_subn(self, size): # Issue #10182: indices were 32-bit-truncated. s = 'a' * size @@ -2938,33 +2968,6 @@ def test_disallow_instantiation(self): pat = re.compile("") check_disallow_instantiation(self, type(pat.scanner(""))) - def test_deprecated_modules(self): - deprecated = { - 'sre_compile': ['compile', 'error', - 'SRE_FLAG_IGNORECASE', 'SUBPATTERN', - '_compile_info'], - 'sre_constants': ['error', 'SRE_FLAG_IGNORECASE', 'SUBPATTERN', - '_NamedIntConstant'], - 'sre_parse': ['SubPattern', 'parse', - 'SRE_FLAG_IGNORECASE', 'SUBPATTERN', - '_parse_sub'], - } - for name in deprecated: - with self.subTest(module=name): - sys.modules.pop(name, None) - with self.assertWarns(DeprecationWarning) as w: - __import__(name) - self.assertEqual(str(w.warning), - f"module {name!r} is deprecated") - self.assertEqual(w.filename, __file__) - self.assertIn(name, sys.modules) - mod = sys.modules[name] - self.assertEqual(mod.__name__, name) - self.assertEqual(mod.__package__, '') - for attr in deprecated[name]: - self.assertHasAttr(mod, attr) - del sys.modules[name] - @cpython_only def test_case_helpers(self): import _sre @@ -3150,5 +3153,15 @@ def test_re_tests(self): self.assertTrue(obj.search(s)) +class TestModule(unittest.TestCase): + def test_deprecated__version__(self): + with self.assertWarnsRegex( + DeprecationWarning, + "'__version__' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(re, "__version__") + self.assertEqual(cm.filename, __file__) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py index 6f465b73f4b..65f56b5c523 100644 --- a/Lib/test/test_regrtest.py +++ b/Lib/test/test_regrtest.py @@ -166,21 +166,20 @@ def test_randomize(self): ns = self.parse_args([opt]) self.assertTrue(ns.randomize) - with os_helper.EnvironmentVarGuard() as env: - # with SOURCE_DATE_EPOCH - env['SOURCE_DATE_EPOCH'] = '1697839080' - ns = self.parse_args(['--randomize']) - regrtest = main.Regrtest(ns) - self.assertFalse(regrtest.randomize) - self.assertIsInstance(regrtest.random_seed, str) - self.assertEqual(regrtest.random_seed, '1697839080') - - # without SOURCE_DATE_EPOCH - del env['SOURCE_DATE_EPOCH'] - ns = self.parse_args(['--randomize']) - regrtest = main.Regrtest(ns) - self.assertTrue(regrtest.randomize) - self.assertIsInstance(regrtest.random_seed, int) + @os_helper.with_source_date_epoch(epoch=1697839080) + def test_randomize_with_source_date_epoch(self): + ns = self.parse_args(['--randomize']) + regrtest = main.Regrtest(ns) + self.assertFalse(regrtest.randomize) + self.assertIsInstance(regrtest.random_seed, str) + self.assertEqual(regrtest.random_seed, '1697839080') + + @os_helper.without_source_date_epoch + def test_randomize_without_source_date_epoch(self): + ns = self.parse_args(['--randomize']) + regrtest = main.Regrtest(ns) + self.assertTrue(regrtest.randomize) + self.assertIsInstance(regrtest.random_seed, int) def test_no_randomize(self): ns = self.parse_args([]) @@ -571,6 +570,13 @@ def test_single_process(self): self.assertEqual(regrtest.num_workers, 0) self.assertTrue(regrtest.single_process) + def test_pythoninfo(self): + ns = self.parse_args([]) + self.assertFalse(ns.pythoninfo) + + ns = self.parse_args(['--pythoninfo']) + self.assertTrue(ns.pythoninfo) + @dataclasses.dataclass(slots=True) class Rerun: @@ -2141,7 +2147,7 @@ def load_tests(loader, tests, pattern): self.check_executed_tests(output, [testname], failed=[testname], parallel=True, - stats=TestStats(1, 1, 0)) + stats=TestStats(1, 2, 1)) def _check_random_seed(self, run_workers: bool): # gh-109276: When -r/--randomize is used, random.seed() is called @@ -2428,6 +2434,11 @@ def test_pgo_exclude(self): self.assertNotIn('test_re', tests) self.assertEqual(len(tests), len(pgo_tests) - 1) + def test_pythoninfo(self): + testname = self.create_test() + output = self.run_tests('--pythoninfo', testname) + self.assertIn("Python build information", output) + class TestUtils(unittest.TestCase): def test_format_duration(self): diff --git a/Lib/test/test_remote_pdb.py b/Lib/test/test_remote_pdb.py new file mode 100644 index 00000000000..e5387526090 --- /dev/null +++ b/Lib/test/test_remote_pdb.py @@ -0,0 +1,1754 @@ +import io +import itertools +import json +import os +import re +import signal +import socket +import subprocess +import sys +import textwrap +import threading +import unittest +import unittest.mock +from contextlib import closing, contextmanager, redirect_stdout, redirect_stderr, ExitStack +from test.support import is_wasi, cpython_only, force_color, requires_subprocess, SHORT_TIMEOUT, subTests +from test.support.os_helper import TESTFN, unlink +from typing import List + +import pdb +from pdb import _PdbServer, _PdbClient + +try: + import pty +except ImportError: + pty = None + + +if not sys.is_remote_debug_enabled(): + raise unittest.SkipTest('remote debugging is disabled') + + +@contextmanager +def kill_on_error(proc): + """Context manager killing the subprocess if a Python exception is raised.""" + with proc: + try: + yield proc + except: + proc.kill() + raise + + +class MockSocketFile: + """Mock socket file for testing _PdbServer without actual socket connections.""" + + def __init__(self): + self.input_queue = [] + self.output_buffer = [] + + def write(self, data: bytes) -> None: + """Simulate write to socket.""" + self.output_buffer.append(data) + + def flush(self) -> None: + """No-op flush implementation.""" + pass + + def readline(self) -> bytes: + """Read a line from the prepared input queue.""" + if not self.input_queue: + return b"" + return self.input_queue.pop(0) + + def close(self) -> None: + """Close the mock socket file.""" + pass + + def add_input(self, data: dict) -> None: + """Add input that will be returned by readline.""" + self.input_queue.append(json.dumps(data).encode() + b"\n") + + def get_output(self) -> List[dict]: + """Get the output that was written by the object being tested.""" + results = [] + for data in self.output_buffer: + if isinstance(data, bytes) and data.endswith(b"\n"): + try: + results.append(json.loads(data.decode().strip())) + except json.JSONDecodeError: + pass # Ignore non-JSON output + self.output_buffer = [] + return results + + +class PdbClientTestCase(unittest.TestCase): + """Tests for the _PdbClient class.""" + + def do_test( + self, + *, + incoming, + simulate_send_failure=False, + simulate_sigint_during_stdout_write=False, + use_interrupt_socket=False, + expected_outgoing=None, + expected_outgoing_signals=None, + expected_completions=None, + expected_exception=None, + expected_stdout="", + expected_stdout_substring="", + expected_state=None, + ): + if expected_outgoing is None: + expected_outgoing = [] + if expected_outgoing_signals is None: + expected_outgoing_signals = [] + if expected_completions is None: + expected_completions = [] + if expected_state is None: + expected_state = {} + + expected_state.setdefault("write_failed", False) + messages = [m for source, m in incoming if source == "server"] + prompts = [m["prompt"] for source, m in incoming if source == "user"] + + input_iter = (m for source, m in incoming if source == "user") + completions = [] + + def mock_input(prompt): + message = next(input_iter, None) + if message is None: + raise EOFError + + if req := message.get("completion_request"): + readline_mock = unittest.mock.Mock() + readline_mock.get_line_buffer.return_value = req["line"] + readline_mock.get_begidx.return_value = req["begidx"] + readline_mock.get_endidx.return_value = req["endidx"] + unittest.mock.seal(readline_mock) + with unittest.mock.patch.dict(sys.modules, {"readline": readline_mock}): + for param in itertools.count(): + prefix = req["line"][req["begidx"] : req["endidx"]] + completion = client.complete(prefix, param) + if completion is None: + break + completions.append(completion) + + reply = message["input"] + if isinstance(reply, BaseException): + raise reply + if isinstance(reply, str): + return reply + return reply() + + with ExitStack() as stack: + client_sock, server_sock = socket.socketpair() + stack.enter_context(closing(client_sock)) + stack.enter_context(closing(server_sock)) + + server_sock = unittest.mock.Mock(wraps=server_sock) + + client_sock.sendall( + b"".join( + (m if isinstance(m, bytes) else json.dumps(m).encode()) + b"\n" + for m in messages + ) + ) + client_sock.shutdown(socket.SHUT_WR) + + if simulate_send_failure: + server_sock.sendall = unittest.mock.Mock( + side_effect=OSError("sendall failed") + ) + client_sock.shutdown(socket.SHUT_RD) + + stdout = io.StringIO() + + if simulate_sigint_during_stdout_write: + orig_stdout_write = stdout.write + + def sigint_stdout_write(s): + signal.raise_signal(signal.SIGINT) + return orig_stdout_write(s) + + stdout.write = sigint_stdout_write + + input_mock = stack.enter_context( + unittest.mock.patch("pdb.input", side_effect=mock_input) + ) + stack.enter_context(redirect_stdout(stdout)) + + if use_interrupt_socket: + interrupt_sock = unittest.mock.Mock(spec=socket.socket) + mock_kill = None + else: + interrupt_sock = None + mock_kill = stack.enter_context( + unittest.mock.patch("os.kill", spec=os.kill) + ) + + client = _PdbClient( + pid=12345, + server_socket=server_sock, + interrupt_sock=interrupt_sock, + ) + + if expected_exception is not None: + exception = expected_exception["exception"] + msg = expected_exception["msg"] + stack.enter_context(self.assertRaises(exception, msg=msg)) + + client.cmdloop() + + sent_msgs = [msg.args[0] for msg in server_sock.sendall.mock_calls] + for msg in sent_msgs: + assert msg.endswith(b"\n") + actual_outgoing = [json.loads(msg) for msg in sent_msgs] + + self.assertEqual(actual_outgoing, expected_outgoing) + self.assertEqual(completions, expected_completions) + if expected_stdout_substring and not expected_stdout: + self.assertIn(expected_stdout_substring, stdout.getvalue()) + else: + self.assertEqual(stdout.getvalue(), expected_stdout) + input_mock.assert_has_calls([unittest.mock.call(p) for p in prompts]) + actual_state = {k: getattr(client, k) for k in expected_state} + self.assertEqual(actual_state, expected_state) + + if use_interrupt_socket: + outgoing_signals = [ + signal.Signals(int.from_bytes(call.args[0])) + for call in interrupt_sock.sendall.call_args_list + ] + else: + assert mock_kill is not None + outgoing_signals = [] + for call in mock_kill.call_args_list: + pid, signum = call.args + self.assertEqual(pid, 12345) + outgoing_signals.append(signal.Signals(signum)) + self.assertEqual(outgoing_signals, expected_outgoing_signals) + + def test_remote_immediately_closing_the_connection(self): + """Test the behavior when the remote closes the connection immediately.""" + incoming = [] + expected_outgoing = [] + self.do_test( + incoming=incoming, + expected_outgoing=expected_outgoing, + ) + + def test_handling_command_list(self): + """Test handling the command_list message.""" + incoming = [ + ("server", {"command_list": ["help", "list", "continue"]}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[], + expected_state={ + "pdb_commands": {"help", "list", "continue"}, + }, + ) + + def test_handling_info_message(self): + """Test handling a message payload with type='info'.""" + incoming = [ + ("server", {"message": "Some message or other\n", "type": "info"}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[], + expected_stdout="Some message or other\n", + ) + + def test_handling_error_message(self): + """Test handling a message payload with type='error'.""" + incoming = [ + ("server", {"message": "Some message or other.", "type": "error"}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[], + expected_stdout="*** Some message or other.\n", + ) + + def test_handling_other_message(self): + """Test handling a message payload with an unrecognized type.""" + incoming = [ + ("server", {"message": "Some message.\n", "type": "unknown"}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[], + expected_stdout="Some message.\n", + ) + + @unittest.skipIf(sys.flags.optimize >= 2, "Help not available for -OO") + @subTests( + "help_request,expected_substring", + [ + # a request to display help for a command + ({"help": "ll"}, "Usage: ll | longlist"), + # a request to display a help overview + ({"help": ""}, "type help "), + # a request to display the full PDB manual + ({"help": "pdb"}, ">>> import pdb"), + ], + ) + def test_handling_help_when_available(self, help_request, expected_substring): + """Test handling help requests when help is available.""" + incoming = [ + ("server", help_request), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[], + expected_stdout_substring=expected_substring, + ) + + @unittest.skipIf(sys.flags.optimize < 2, "Needs -OO") + @subTests( + "help_request,expected_substring", + [ + # a request to display help for a command + ({"help": "ll"}, "No help for 'll'"), + # a request to display a help overview + ({"help": ""}, "Undocumented commands"), + # a request to display the full PDB manual + ({"help": "pdb"}, "No help for 'pdb'"), + ], + ) + def test_handling_help_when_not_available(self, help_request, expected_substring): + """Test handling help requests when help is not available.""" + incoming = [ + ("server", help_request), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[], + expected_stdout_substring=expected_substring, + ) + + def test_handling_pdb_prompts(self): + """Test responding to pdb's normal prompts.""" + incoming = [ + ("server", {"command_list": ["b"]}), + ("server", {"prompt": "(Pdb) ", "state": "pdb"}), + ("user", {"prompt": "(Pdb) ", "input": "lst ["}), + ("user", {"prompt": "... ", "input": "0 ]"}), + ("server", {"prompt": "(Pdb) ", "state": "pdb"}), + ("user", {"prompt": "(Pdb) ", "input": ""}), + ("server", {"prompt": "(Pdb) ", "state": "pdb"}), + ("user", {"prompt": "(Pdb) ", "input": "b ["}), + ("server", {"prompt": "(Pdb) ", "state": "pdb"}), + ("user", {"prompt": "(Pdb) ", "input": "! b ["}), + ("user", {"prompt": "... ", "input": "b ]"}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + {"reply": "lst [\n0 ]"}, + {"reply": ""}, + {"reply": "b ["}, + {"reply": "!b [\nb ]"}, + ], + expected_state={"state": "pdb"}, + ) + + def test_handling_interact_prompts(self): + """Test responding to pdb's interact mode prompts.""" + incoming = [ + ("server", {"command_list": ["b"]}), + ("server", {"prompt": ">>> ", "state": "interact"}), + ("user", {"prompt": ">>> ", "input": "lst ["}), + ("user", {"prompt": "... ", "input": "0 ]"}), + ("server", {"prompt": ">>> ", "state": "interact"}), + ("user", {"prompt": ">>> ", "input": ""}), + ("server", {"prompt": ">>> ", "state": "interact"}), + ("user", {"prompt": ">>> ", "input": "b ["}), + ("user", {"prompt": "... ", "input": "b ]"}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + {"reply": "lst [\n0 ]"}, + {"reply": ""}, + {"reply": "b [\nb ]"}, + ], + expected_state={"state": "interact"}, + ) + + def test_retry_pdb_prompt_on_syntax_error(self): + """Test re-prompting after a SyntaxError in a Python expression.""" + incoming = [ + ("server", {"prompt": "(Pdb) ", "state": "pdb"}), + ("user", {"prompt": "(Pdb) ", "input": " lst ["}), + ("user", {"prompt": "(Pdb) ", "input": "lst ["}), + ("user", {"prompt": "... ", "input": " 0 ]"}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + {"reply": "lst [\n 0 ]"}, + ], + expected_stdout_substring="*** IndentationError", + expected_state={"state": "pdb"}, + ) + + def test_retry_interact_prompt_on_syntax_error(self): + """Test re-prompting after a SyntaxError in a Python expression.""" + incoming = [ + ("server", {"prompt": ">>> ", "state": "interact"}), + ("user", {"prompt": ">>> ", "input": "!lst ["}), + ("user", {"prompt": ">>> ", "input": "lst ["}), + ("user", {"prompt": "... ", "input": " 0 ]"}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + {"reply": "lst [\n 0 ]"}, + ], + expected_stdout_substring="*** SyntaxError", + expected_state={"state": "interact"}, + ) + + def test_handling_unrecognized_prompt_type(self): + """Test fallback to "dumb" single-line mode for unknown states.""" + incoming = [ + ("server", {"prompt": "Do it? ", "state": "confirm"}), + ("user", {"prompt": "Do it? ", "input": "! ["}), + ("server", {"prompt": "Do it? ", "state": "confirm"}), + ("user", {"prompt": "Do it? ", "input": "echo hello"}), + ("server", {"prompt": "Do it? ", "state": "confirm"}), + ("user", {"prompt": "Do it? ", "input": ""}), + ("server", {"prompt": "Do it? ", "state": "confirm"}), + ("user", {"prompt": "Do it? ", "input": "echo goodbye"}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + {"reply": "! ["}, + {"reply": "echo hello"}, + {"reply": ""}, + {"reply": "echo goodbye"}, + ], + expected_state={"state": "dumb"}, + ) + + def test_sigint_at_prompt(self): + """Test signaling when a prompt gets interrupted.""" + incoming = [ + ("server", {"prompt": "(Pdb) ", "state": "pdb"}), + ( + "user", + { + "prompt": "(Pdb) ", + "input": lambda: signal.raise_signal(signal.SIGINT), + }, + ), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + {"signal": "INT"}, + ], + expected_state={"state": "pdb"}, + ) + + def test_sigint_at_continuation_prompt(self): + """Test signaling when a continuation prompt gets interrupted.""" + incoming = [ + ("server", {"prompt": "(Pdb) ", "state": "pdb"}), + ("user", {"prompt": "(Pdb) ", "input": "if True:"}), + ( + "user", + { + "prompt": "... ", + "input": lambda: signal.raise_signal(signal.SIGINT), + }, + ), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + {"signal": "INT"}, + ], + expected_state={"state": "pdb"}, + ) + + def test_sigint_when_writing(self): + """Test siginaling when sys.stdout.write() gets interrupted.""" + incoming = [ + ("server", {"message": "Some message or other\n", "type": "info"}), + ] + for use_interrupt_socket in [False, True]: + with self.subTest(use_interrupt_socket=use_interrupt_socket): + self.do_test( + incoming=incoming, + simulate_sigint_during_stdout_write=True, + use_interrupt_socket=use_interrupt_socket, + expected_outgoing=[], + expected_outgoing_signals=[signal.SIGINT], + expected_stdout="Some message or other\n", + ) + + def test_eof_at_prompt(self): + """Test signaling when a prompt gets an EOFError.""" + incoming = [ + ("server", {"prompt": "(Pdb) ", "state": "pdb"}), + ("user", {"prompt": "(Pdb) ", "input": EOFError()}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + {"signal": "EOF"}, + ], + expected_state={"state": "pdb"}, + ) + + def test_unrecognized_json_message(self): + """Test failing after getting an unrecognized payload.""" + incoming = [ + ("server", {"monty": "python"}), + ("server", {"message": "Some message or other\n", "type": "info"}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[], + expected_exception={ + "exception": RuntimeError, + "msg": 'Unrecognized payload b\'{"monty": "python"}\'', + }, + ) + + def test_continuing_after_getting_a_non_json_payload(self): + """Test continuing after getting a non JSON payload.""" + incoming = [ + ("server", b"spam"), + ("server", {"message": "Something", "type": "info"}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[], + expected_stdout="\n".join( + [ + "*** Invalid JSON from remote: b'spam\\n'", + "Something", + ] + ), + ) + + def test_write_failing(self): + """Test terminating if write fails due to a half closed socket.""" + incoming = [ + ("server", {"prompt": "(Pdb) ", "state": "pdb"}), + ("user", {"prompt": "(Pdb) ", "input": KeyboardInterrupt()}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[{"signal": "INT"}], + simulate_send_failure=True, + expected_state={"write_failed": True}, + ) + + def test_completion_in_pdb_state(self): + """Test requesting tab completions at a (Pdb) prompt.""" + # GIVEN + incoming = [ + ("server", {"prompt": "(Pdb) ", "state": "pdb"}), + ( + "user", + { + "prompt": "(Pdb) ", + "completion_request": { + "line": " mod._", + "begidx": 8, + "endidx": 9, + }, + "input": "print(\n mod.__name__)", + }, + ), + ("server", {"completions": ["__name__", "__file__"]}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + { + "complete": { + "text": "_", + "line": "mod._", + "begidx": 4, + "endidx": 5, + } + }, + {"reply": "print(\n mod.__name__)"}, + ], + expected_completions=["__name__", "__file__"], + expected_state={"state": "pdb"}, + ) + + def test_multiline_completion_in_pdb_state(self): + """Test requesting tab completions at a (Pdb) continuation prompt.""" + # GIVEN + incoming = [ + ("server", {"prompt": "(Pdb) ", "state": "pdb"}), + ("user", {"prompt": "(Pdb) ", "input": "if True:"}), + ( + "user", + { + "prompt": "... ", + "completion_request": { + "line": " b", + "begidx": 4, + "endidx": 5, + }, + "input": " bool()", + }, + ), + ("server", {"completions": ["bin", "bool", "bytes"]}), + ("user", {"prompt": "... ", "input": ""}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + { + "complete": { + "text": "b", + "line": "! b", + "begidx": 2, + "endidx": 3, + } + }, + {"reply": "if True:\n bool()\n"}, + ], + expected_completions=["bin", "bool", "bytes"], + expected_state={"state": "pdb"}, + ) + + def test_completion_in_interact_state(self): + """Test requesting tab completions at a >>> prompt.""" + incoming = [ + ("server", {"prompt": ">>> ", "state": "interact"}), + ( + "user", + { + "prompt": ">>> ", + "completion_request": { + "line": " mod.__", + "begidx": 8, + "endidx": 10, + }, + "input": "print(\n mod.__name__)", + }, + ), + ("server", {"completions": ["__name__", "__file__"]}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + { + "complete": { + "text": "__", + "line": "mod.__", + "begidx": 4, + "endidx": 6, + } + }, + {"reply": "print(\n mod.__name__)"}, + ], + expected_completions=["__name__", "__file__"], + expected_state={"state": "interact"}, + ) + + def test_completion_in_unknown_state(self): + """Test requesting tab completions at an unrecognized prompt.""" + incoming = [ + ("server", {"command_list": ["p"]}), + ("server", {"prompt": "Do it? ", "state": "confirm"}), + ( + "user", + { + "prompt": "Do it? ", + "completion_request": { + "line": "_", + "begidx": 0, + "endidx": 1, + }, + "input": "__name__", + }, + ), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + {"reply": "__name__"}, + ], + expected_state={"state": "dumb"}, + ) + + def test_write_failure_during_completion(self): + """Test failing to write to the socket to request tab completions.""" + incoming = [ + ("server", {"prompt": ">>> ", "state": "interact"}), + ( + "user", + { + "prompt": ">>> ", + "completion_request": { + "line": "xy", + "begidx": 0, + "endidx": 2, + }, + "input": "xyz", + }, + ), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + { + "complete": { + "text": "xy", + "line": "xy", + "begidx": 0, + "endidx": 2, + } + }, + {"reply": "xyz"}, + ], + simulate_send_failure=True, + expected_completions=[], + expected_state={"state": "interact", "write_failed": True}, + ) + + def test_read_failure_during_completion(self): + """Test failing to read tab completions from the socket.""" + incoming = [ + ("server", {"prompt": ">>> ", "state": "interact"}), + ( + "user", + { + "prompt": ">>> ", + "completion_request": { + "line": "xy", + "begidx": 0, + "endidx": 2, + }, + "input": "xyz", + }, + ), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + { + "complete": { + "text": "xy", + "line": "xy", + "begidx": 0, + "endidx": 2, + } + }, + {"reply": "xyz"}, + ], + expected_completions=[], + expected_state={"state": "interact"}, + ) + + def test_reading_invalid_json_during_completion(self): + """Test receiving invalid JSON when getting tab completions.""" + incoming = [ + ("server", {"prompt": ">>> ", "state": "interact"}), + ( + "user", + { + "prompt": ">>> ", + "completion_request": { + "line": "xy", + "begidx": 0, + "endidx": 2, + }, + "input": "xyz", + }, + ), + ("server", b'{"completions": '), + ("user", {"prompt": ">>> ", "input": "xyz"}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + { + "complete": { + "text": "xy", + "line": "xy", + "begidx": 0, + "endidx": 2, + } + }, + {"reply": "xyz"}, + ], + expected_stdout_substring="*** json.decoder.JSONDecodeError", + expected_completions=[], + expected_state={"state": "interact"}, + ) + + def test_reading_empty_json_during_completion(self): + """Test receiving an empty JSON object when getting tab completions.""" + incoming = [ + ("server", {"prompt": ">>> ", "state": "interact"}), + ( + "user", + { + "prompt": ">>> ", + "completion_request": { + "line": "xy", + "begidx": 0, + "endidx": 2, + }, + "input": "xyz", + }, + ), + ("server", {}), + ("user", {"prompt": ">>> ", "input": "xyz"}), + ] + self.do_test( + incoming=incoming, + expected_outgoing=[ + { + "complete": { + "text": "xy", + "line": "xy", + "begidx": 0, + "endidx": 2, + } + }, + {"reply": "xyz"}, + ], + expected_stdout=( + "*** RuntimeError: Failed to get valid completions." + " Got: {}\n" + ), + expected_completions=[], + expected_state={"state": "interact"}, + ) + + +class RemotePdbTestCase(unittest.TestCase): + """Tests for the _PdbServer class.""" + + def setUp(self): + self.sockfile = MockSocketFile() + self.pdb = _PdbServer(self.sockfile) + + # Mock some Bdb attributes that are lazily created when tracing starts + self.pdb.botframe = None + self.pdb.quitting = False + + # Create a frame for testing + self.test_globals = {'a': 1, 'b': 2, '__pdb_convenience_variables': {'x': 100}} + self.test_locals = {'c': 3, 'd': 4} + + # Create a simple test frame + frame_info = unittest.mock.Mock() + frame_info.f_globals = self.test_globals + frame_info.f_locals = self.test_locals + frame_info.f_lineno = 42 + frame_info.f_code = unittest.mock.Mock() + frame_info.f_code.co_filename = "test_file.py" + frame_info.f_code.co_name = "test_function" + + self.pdb.curframe = frame_info + + def test_message_and_error(self): + """Test message and error methods send correct JSON.""" + self.pdb.message("Test message") + self.pdb.error("Test error") + + outputs = self.sockfile.get_output() + self.assertEqual(len(outputs), 2) + self.assertEqual(outputs[0], {"message": "Test message\n", "type": "info"}) + self.assertEqual(outputs[1], {"message": "Test error", "type": "error"}) + + def test_read_command(self): + """Test reading commands from the socket.""" + # Add test input + self.sockfile.add_input({"reply": "help"}) + + # Read the command + cmd = self.pdb._read_reply() + self.assertEqual(cmd, "help") + + def test_read_command_EOF(self): + """Test reading EOF command.""" + # Simulate socket closure + self.pdb._write_failed = True + with self.assertRaises(EOFError): + self.pdb._read_reply() + + def test_completion(self): + """Test handling completion requests.""" + # Mock completenames to return specific values + with unittest.mock.patch.object(self.pdb, 'completenames', + return_value=["continue", "clear"]): + + # Add a completion request + self.sockfile.add_input({ + "complete": { + "text": "c", + "line": "c", + "begidx": 0, + "endidx": 1 + } + }) + + # Add a regular command to break the loop + self.sockfile.add_input({"reply": "help"}) + + # Read command - this should process the completion request first + cmd = self.pdb._read_reply() + + # Verify completion response was sent + outputs = self.sockfile.get_output() + self.assertEqual(len(outputs), 1) + self.assertEqual(outputs[0], {"completions": ["continue", "clear"]}) + + # The actual command should be returned + self.assertEqual(cmd, "help") + + def test_do_help(self): + """Test that do_help sends the help message.""" + self.pdb.do_help("break") + + outputs = self.sockfile.get_output() + self.assertEqual(len(outputs), 1) + self.assertEqual(outputs[0], {"help": "break"}) + + def test_interact_mode(self): + """Test interaction mode setup and execution.""" + # First set up interact mode + self.pdb.do_interact("") + + # Verify _interact_state is properly initialized + self.assertIsNotNone(self.pdb._interact_state) + self.assertIsInstance(self.pdb._interact_state, dict) + + # Test running code in interact mode + with unittest.mock.patch.object(self.pdb, '_error_exc') as mock_error: + self.pdb._run_in_python_repl("print('test')") + mock_error.assert_not_called() + + # Test with syntax error + self.pdb._run_in_python_repl("if:") + mock_error.assert_called_once() + + def test_registering_commands(self): + """Test registering breakpoint commands.""" + # Mock get_bpbynumber + with unittest.mock.patch.object(self.pdb, 'get_bpbynumber'): + # Queue up some input to send + self.sockfile.add_input({"reply": "commands 1"}) + self.sockfile.add_input({"reply": "silent"}) + self.sockfile.add_input({"reply": "print('hi')"}) + self.sockfile.add_input({"reply": "end"}) + self.sockfile.add_input({"signal": "EOF"}) + + # Run the PDB command loop + self.pdb.cmdloop() + + outputs = self.sockfile.get_output() + self.assertIn('command_list', outputs[0]) + self.assertEqual(outputs[1], {"prompt": "(Pdb) ", "state": "pdb"}) + self.assertEqual(outputs[2], {"prompt": "(com) ", "state": "commands"}) + self.assertEqual(outputs[3], {"prompt": "(com) ", "state": "commands"}) + self.assertEqual(outputs[4], {"prompt": "(com) ", "state": "commands"}) + self.assertEqual(outputs[5], {"prompt": "(Pdb) ", "state": "pdb"}) + self.assertEqual(outputs[6], {"message": "\n", "type": "info"}) + self.assertEqual(len(outputs), 7) + + self.assertEqual( + self.pdb.commands[1], + ["_pdbcmd_silence_frame_status", "print('hi')"], + ) + + def test_detach(self): + """Test the detach method.""" + with unittest.mock.patch.object(self.sockfile, 'close') as mock_close: + self.pdb.detach() + mock_close.assert_called_once() + self.assertFalse(self.pdb.quitting) + + def test_cmdloop(self): + """Test the command loop with various commands.""" + # Mock onecmd to track command execution + with unittest.mock.patch.object(self.pdb, 'onecmd', return_value=False) as mock_onecmd: + # Add commands to the queue + self.pdb.cmdqueue = ['help', 'list'] + + # Add a command from the socket for when cmdqueue is empty + self.sockfile.add_input({"reply": "next"}) + + # Add a second command to break the loop + self.sockfile.add_input({"reply": "quit"}) + + # Configure onecmd to exit the loop on "quit" + def side_effect(line): + return line == 'quit' + mock_onecmd.side_effect = side_effect + + # Run the command loop + self.pdb.quitting = False # Set this by hand because we don't want to really call set_trace() + self.pdb.cmdloop() + + # Should have processed 4 commands: 2 from cmdqueue, 2 from socket + self.assertEqual(mock_onecmd.call_count, 4) + mock_onecmd.assert_any_call('help') + mock_onecmd.assert_any_call('list') + mock_onecmd.assert_any_call('next') + mock_onecmd.assert_any_call('quit') + + # Check if prompt was sent to client + outputs = self.sockfile.get_output() + prompts = [o for o in outputs if 'prompt' in o] + self.assertEqual(len(prompts), 2) # Should have sent 2 prompts + + +@requires_subprocess() +@unittest.skipIf(is_wasi, "WASI does not support TCP sockets") +class PdbConnectTestCase(unittest.TestCase): + """Tests for the _connect mechanism using direct socket communication.""" + + def setUp(self): + # Create a server socket that will wait for the debugger to connect + self.server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_sock.bind(('127.0.0.1', 0)) # Let OS assign port + self.server_sock.listen(1) + self.port = self.server_sock.getsockname()[1] + + def _create_script(self, script=None): + # Create a file for subprocess script + if script is None: + script = textwrap.dedent( + f""" + import pdb + import sys + import time + + def foo(): + x = 42 + return bar() + + def bar(): + return 42 + + def connect_to_debugger(): + # Create a frame to debug + def dummy_function(): + x = 42 + # Call connect to establish connection + # with the test server + frame = sys._getframe() # Get the current frame + pdb._connect( + host='127.0.0.1', + port={self.port}, + frame=frame, + commands="", + version=pdb._PdbServer.protocol_version(), + signal_raising_thread=False, + colorize=False, + ) + return x # This line won't be reached in debugging + + return dummy_function() + + result = connect_to_debugger() + foo() + print(f"Function returned: {{result}}") + """) + + self.script_path = TESTFN + "_connect_test.py" + with open(self.script_path, 'w') as f: + f.write(script) + + def tearDown(self): + self.server_sock.close() + try: + unlink(self.script_path) + except OSError: + pass + + def _connect_and_get_client_file(self): + """Helper to start subprocess and get connected client file.""" + # Start the subprocess that will connect to our socket + process = subprocess.Popen( + [sys.executable, self.script_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Accept the connection from the subprocess + client_sock, _ = self.server_sock.accept() + client_file = client_sock.makefile('rwb') + self.addCleanup(client_file.close) + self.addCleanup(client_sock.close) + + return process, client_file + + def _connect_and_get_client_file_via_pty(self): + """Like _connect_and_get_client_file, but run the target under a pty. + + With a real terminal on stdin/stdout, PyREPL is available *inside the + target process*, which is the condition that exercises pdb's PyREPL + input handling in the remote server. + """ + controller, worker = pty.openpty() + self.addCleanup(os.close, controller) + env = dict(os.environ, TERM="xterm-256color") + env.pop("PYTHON_BASIC_REPL", None) # don't opt out of PyREPL + process = subprocess.Popen( + [sys.executable, self.script_path], + stdin=worker, stdout=worker, stderr=worker, env=env, close_fds=True, + ) + os.close(worker) # only the child keeps the worker end open + + # Continuously drain the terminal so the child never blocks on a write. + drainer = threading.Thread( + target=self._drain_until_eof, args=(controller,), daemon=True + ) + drainer.start() + self.addCleanup(drainer.join, SHORT_TIMEOUT) + + client_sock, _ = self.server_sock.accept() + client_file = client_sock.makefile('rwb') + self.addCleanup(client_file.close) + self.addCleanup(client_sock.close) + + return process, client_file + + @staticmethod + def _drain_until_eof(fd): + try: + while os.read(fd, 1024): + pass + except OSError: + pass # controller closed, or the pty went away with the child + + def _read_until_prompt(self, client_file): + """Helper to read messages until a prompt is received.""" + messages = [] + while True: + data = client_file.readline() + if not data: + break + msg = json.loads(data.decode()) + messages.append(msg) + if 'prompt' in msg: + break + return messages + + def _send_command(self, client_file, command): + """Helper to send a command to the debugger.""" + client_file.write(json.dumps({"reply": command}).encode() + b"\n") + client_file.flush() + + def test_connect_and_basic_commands(self): + """Test connecting to a remote debugger and sending basic commands.""" + self._create_script() + process, client_file = self._connect_and_get_client_file() + + with kill_on_error(process): + # We should receive initial data from the debugger + data = client_file.readline() + initial_data = json.loads(data.decode()) + self.assertIn('message', initial_data) + self.assertIn('pdb._connect', initial_data['message']) + + # First, look for command_list message + data = client_file.readline() + command_list = json.loads(data.decode()) + self.assertIn('command_list', command_list) + + # Then, look for the first prompt + data = client_file.readline() + prompt_data = json.loads(data.decode()) + self.assertIn('prompt', prompt_data) + self.assertEqual(prompt_data['state'], 'pdb') + + # Send 'bt' (backtrace) command + self._send_command(client_file, "bt") + + # Check for response - we should get some stack frames + messages = self._read_until_prompt(client_file) + + # Extract text messages containing stack info + text_msg = [msg['message'] for msg in messages + if 'message' in msg and 'connect_to_debugger' in msg['message']] + got_stack_info = bool(text_msg) + + expected_stacks = [ + "", + "connect_to_debugger", + ] + + for stack, msg in zip(expected_stacks, text_msg, strict=True): + self.assertIn(stack, msg) + + self.assertTrue(got_stack_info, "Should have received stack trace information") + + # Send 'c' (continue) command to let the program finish + self._send_command(client_file, "c") + + # Wait for process to finish + stdout, _ = process.communicate(timeout=SHORT_TIMEOUT) + + # Check if we got the expected output + self.assertIn("Function returned: 42", stdout) + self.assertEqual(process.returncode, 0) + + def test_breakpoints(self): + """Test setting and hitting breakpoints.""" + self._create_script() + process, client_file = self._connect_and_get_client_file() + with kill_on_error(process): + # Skip initial messages until we get to the prompt + self._read_until_prompt(client_file) + + # Set a breakpoint at the return statement + self._send_command(client_file, "break bar") + messages = self._read_until_prompt(client_file) + bp_msg = next(msg['message'] for msg in messages if 'message' in msg) + self.assertIn("Breakpoint", bp_msg) + + # Continue execution until breakpoint + self._send_command(client_file, "c") + messages = self._read_until_prompt(client_file) + + # Verify we hit the breakpoint + hit_msg = next(msg['message'] for msg in messages if 'message' in msg) + self.assertIn("bar()", hit_msg) + + # Check breakpoint list + self._send_command(client_file, "b") + messages = self._read_until_prompt(client_file) + list_msg = next(msg['message'] for msg in reversed(messages) if 'message' in msg) + self.assertIn("1 breakpoint", list_msg) + self.assertIn("breakpoint already hit 1 time", list_msg) + + # Clear breakpoint + self._send_command(client_file, "clear 1") + messages = self._read_until_prompt(client_file) + clear_msg = next(msg['message'] for msg in reversed(messages) if 'message' in msg) + self.assertIn("Deleted breakpoint", clear_msg) + + # Continue to end + self._send_command(client_file, "c") + stdout, _ = process.communicate(timeout=SHORT_TIMEOUT) + + self.assertIn("Function returned: 42", stdout) + self.assertEqual(process.returncode, 0) + + def test_keyboard_interrupt(self): + """Test that sending keyboard interrupt breaks into pdb.""" + + script = textwrap.dedent(f""" + import time + import sys + import socket + import pdb + def bar(): + frame = sys._getframe() # Get the current frame + pdb._connect( + host='127.0.0.1', + port={self.port}, + frame=frame, + commands="", + version=pdb._PdbServer.protocol_version(), + signal_raising_thread=True, + colorize=False, + ) + print("Connected to debugger") + iterations = 50 + while iterations > 0: + print("Iteration", iterations, flush=True) + time.sleep(0.2) + iterations -= 1 + return 42 + + if __name__ == "__main__": + print("Function returned:", bar()) + """) + self._create_script(script=script) + process, client_file = self._connect_and_get_client_file() + + # Accept a 2nd connection from the subprocess to tell it about signals + signal_sock, _ = self.server_sock.accept() + self.addCleanup(signal_sock.close) + + with kill_on_error(process): + # Skip initial messages until we get to the prompt + self._read_until_prompt(client_file) + + # Continue execution + self._send_command(client_file, "c") + + # Confirm that the remote is already in the while loop. We know + # it's in bar() and we can exit the loop immediately by setting + # iterations to 0. + while line := process.stdout.readline(): + if line.startswith("Iteration"): + break + + # Inject a script to interrupt the running process + signal_sock.sendall(signal.SIGINT.to_bytes()) + messages = self._read_until_prompt(client_file) + + # Verify we got the keyboard interrupt message. + interrupt_msgs = [msg['message'] for msg in messages if 'message' in msg] + expected_msg = [msg for msg in interrupt_msgs if "bar()" in msg] + self.assertGreater(len(expected_msg), 0) + + # Continue to end as fast as we can + self._send_command(client_file, "iterations = 0") + self._send_command(client_file, "c") + stdout, _ = process.communicate(timeout=SHORT_TIMEOUT) + self.assertIn("Function returned: 42", stdout) + self.assertEqual(process.returncode, 0) + + def test_handle_eof(self): + """Test that EOF signal properly exits the debugger.""" + self._create_script() + process, client_file = self._connect_and_get_client_file() + + with kill_on_error(process): + # Skip initial messages until we get to the prompt + self._read_until_prompt(client_file) + + # Send EOF signal to exit the debugger + client_file.write(json.dumps({"signal": "EOF"}).encode() + b"\n") + client_file.flush() + + # The process should complete normally after receiving EOF + stdout, stderr = process.communicate(timeout=SHORT_TIMEOUT) + + # Verify process completed correctly + self.assertIn("Function returned: 42", stdout) + self.assertEqual(process.returncode, 0) + self.assertEqual(stderr, "") + + def test_colorized_list_has_no_caret_encoded_newlines(self): + """A colorized ``list`` must not append "^J" to each source line. + + The remote server colorizes the source it sends to the client. The + colorizer renders control characters in caret notation, so a source + line's trailing newline has to be stripped *before* it is colorized; + otherwise every listed line ends with a spurious "^J". ``where`` was + unaffected because it strips the line before colorizing. See + gh-154470. + """ + # colorize=True is what attaching from a color-capable terminal passes + # to the server, and it is what makes the server colorize ``list``. + script = textwrap.dedent(f""" + import pdb, sys + def helper(): + x = 42 + return x + def connect(): + frame = sys._getframe() + pdb._connect( + host='127.0.0.1', + port={self.port}, + frame=frame, + commands="", + version=pdb._PdbServer.protocol_version(), + signal_raising_thread=False, + colorize=True, + ) + return helper() + connect() + """) + self._create_script(script=script) + process, client_file = self._connect_and_get_client_file() + + with kill_on_error(process): + self._read_until_prompt(client_file) + self._send_command(client_file, "l 1, 15") + messages = self._read_until_prompt(client_file) + source = "".join(m["message"] for m in messages if "message" in m) + + # Sanity: we really did receive colorized source ... + self.assertIn("helper", source) + self.assertIn("\x1b[", source) # ANSI color escapes are present + # ... and no trailing newline leaked through as caret notation. + self.assertNotIn("^J", source) + self._send_command(client_file, "c") + process.wait(timeout=SHORT_TIMEOUT) + self.assertEqual(process.returncode, 0) + + @unittest.skipUnless(pty, "requires pty") + def test_prompt_with_interactive_terminal(self): + """The server must send "(Pdb) " even when the target owns a terminal. + + The remote server transmits its prompt string to the client, which + displays it. When the target process has an interactive terminal, + PyREPL is enabled inside it; the base pdb machinery then blanks + ``self.prompt`` (a local PyREPL would draw the prompt itself). The + remote server has no local PyREPL -- it reads input from the socket -- + so it must keep the real prompt rather than transmit an empty one. + Regression test for gh-154467, where attaching with ``pdb -p`` to a + process running at an interactive prompt showed a blank prompt. + """ + self._create_script() + process, client_file = self._connect_and_get_client_file_via_pty() + + with kill_on_error(process): + messages = self._read_until_prompt(client_file) + # The message that ended the read is the prompt request. + self.assertEqual(messages[-1], {"prompt": "(Pdb) ", "state": "pdb"}) + + # Let the target run to completion so nothing is left attached. + self._send_command(client_file, "c") + process.wait(timeout=SHORT_TIMEOUT) + self.assertEqual(process.returncode, 0) + + def test_protocol_version(self): + """Test that incompatible protocol versions are properly detected.""" + # Create a script using an incompatible protocol version + script = textwrap.dedent(f''' + import sys + import pdb + + def run_test(): + frame = sys._getframe() + + # Use a fake version number that's definitely incompatible + fake_version = 0x01010101 # A fake version that doesn't match any real Python version + + # Connect with the wrong version + pdb._connect( + host='127.0.0.1', + port={self.port}, + frame=frame, + commands="", + version=fake_version, + signal_raising_thread=False, + colorize=False, + ) + + # This should print if the debugger detaches correctly + print("Debugger properly detected version mismatch") + return True + + if __name__ == "__main__": + print("Test result:", run_test()) + ''') + self._create_script(script=script) + process, client_file = self._connect_and_get_client_file() + + with kill_on_error(process): + # First message should be an error about protocol version mismatch + data = client_file.readline() + message = json.loads(data.decode()) + + self.assertIn('message', message) + self.assertEqual(message['type'], 'error') + self.assertIn('incompatible', message['message']) + self.assertIn('protocol version', message['message']) + + # The process should complete normally + stdout, stderr = process.communicate(timeout=SHORT_TIMEOUT) + + # Verify the process completed successfully + self.assertIn("Test result: True", stdout) + self.assertIn("Debugger properly detected version mismatch", stdout) + self.assertEqual(process.returncode, 0) + + def test_help_system(self): + """Test that the help system properly sends help text to the client.""" + self._create_script() + process, client_file = self._connect_and_get_client_file() + + with kill_on_error(process): + # Skip initial messages until we get to the prompt + self._read_until_prompt(client_file) + + # Request help for different commands + help_commands = ["help", "help break", "help continue", "help pdb"] + + for cmd in help_commands: + self._send_command(client_file, cmd) + + # Look for help message + data = client_file.readline() + message = json.loads(data.decode()) + + self.assertIn('help', message) + + if cmd == "help": + # Should just contain the command itself + self.assertEqual(message['help'], "") + else: + # Should contain the specific command we asked for help with + command = cmd.split()[1] + self.assertEqual(message['help'], command) + + # Skip to the next prompt + self._read_until_prompt(client_file) + + # Continue execution to finish the program + self._send_command(client_file, "c") + + stdout, stderr = process.communicate(timeout=SHORT_TIMEOUT) + self.assertIn("Function returned: 42", stdout) + self.assertEqual(process.returncode, 0) + + def test_multi_line_commands(self): + """Test that multi-line commands work properly over remote connection.""" + self._create_script() + process, client_file = self._connect_and_get_client_file() + + with kill_on_error(process): + # Skip initial messages until we get to the prompt + self._read_until_prompt(client_file) + + # Send a multi-line command + multi_line_commands = [ + # Define a function + "def test_func():\n return 42", + + # For loop + "for i in range(3):\n print(i)", + + # If statement + "if True:\n x = 42\nelse:\n x = 0", + + # Try/except + "try:\n result = 10/2\n print(result)\nexcept ZeroDivisionError:\n print('Error')", + + # Class definition + "class TestClass:\n def __init__(self):\n self.value = 100\n def get_value(self):\n return self.value" + ] + + for cmd in multi_line_commands: + self._send_command(client_file, cmd) + self._read_until_prompt(client_file) + + # Test executing the defined function + self._send_command(client_file, "test_func()") + messages = self._read_until_prompt(client_file) + + # Find the result message + result_msg = next(msg['message'] for msg in messages if 'message' in msg) + self.assertIn("42", result_msg) + + # Test creating an instance of the defined class + self._send_command(client_file, "obj = TestClass()") + self._read_until_prompt(client_file) + + # Test calling a method on the instance + self._send_command(client_file, "obj.get_value()") + messages = self._read_until_prompt(client_file) + + # Find the result message + result_msg = next(msg['message'] for msg in messages if 'message' in msg) + self.assertIn("100", result_msg) + + # Continue execution to finish + self._send_command(client_file, "c") + + stdout, stderr = process.communicate(timeout=SHORT_TIMEOUT) + self.assertIn("Function returned: 42", stdout) + self.assertEqual(process.returncode, 0) + + def test_exec_in_closure_result_uses_pdb_stdout(self): + """ + Expression results executed via _exec_in_closure() should be written + to the debugger output stream (pdb stdout), not to sys.stdout. + """ + self._create_script() + process, client_file = self._connect_and_get_client_file() + + with kill_on_error(process): + self._read_until_prompt(client_file) + + self._send_command(client_file, "(lambda: 123)()") + messages = self._read_until_prompt(client_file) + result_msg = "".join(msg.get("message", "") for msg in messages) + self.assertIn("123", result_msg) + + self._send_command(client_file, "sum(i for i in (1, 2, 3))") + messages = self._read_until_prompt(client_file) + result_msg = "".join(msg.get("message", "") for msg in messages) + self.assertIn("6", result_msg) + + self._send_command(client_file, "c") + stdout, _ = process.communicate(timeout=SHORT_TIMEOUT) + + self.assertNotIn("\n123\n", stdout) + self.assertNotIn("\n6\n", stdout) + self.assertEqual(process.returncode, 0) + + +def _supports_remote_attaching(): + PROCESS_VM_READV_SUPPORTED = False + + try: + from _remote_debugging import PROCESS_VM_READV_SUPPORTED + except ImportError: + pass + + return PROCESS_VM_READV_SUPPORTED + + +@unittest.skipIf(not sys.is_remote_debug_enabled(), "Remote debugging is not enabled") +@unittest.skipIf(sys.platform != "darwin" and sys.platform != "linux" and sys.platform != "win32", + "Test only runs on Linux, Windows and MacOS") +@unittest.skipIf(sys.platform == "linux" and not _supports_remote_attaching(), + "Testing on Linux requires process_vm_readv support") +@cpython_only +@requires_subprocess() +class PdbAttachTestCase(unittest.TestCase): + def setUp(self): + # Create a server socket that will wait for the debugger to connect + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.bind(('127.0.0.1', 0)) # Let OS assign port + self.sock.listen(1) + self.port = self.sock.getsockname()[1] + self._create_script() + + def _create_script(self, script=None): + # Create a file for subprocess script + script = textwrap.dedent( + f""" + import socket + import time + + def foo(): + return bar() + + def bar(): + return baz() + + def baz(): + x = 1 + # Trigger attach + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('127.0.0.1', {self.port})) + sock.close() + count = 0 + while x == 1 and count < 100: + count += 1 + time.sleep(0.1) + return x + + result = foo() + print(f"Function returned: {{result}}") + """ + ) + + self.script_path = TESTFN + "_connect_test.py" + with open(self.script_path, 'w') as f: + f.write(script) + + def tearDown(self): + self.sock.close() + try: + unlink(self.script_path) + except OSError: + pass + + def do_integration_test(self, client_stdin): + process = subprocess.Popen( + [sys.executable, self.script_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + self.addCleanup(process.stdout.close) + self.addCleanup(process.stderr.close) + + # Wait for the process to reach our attachment point + self.sock.settimeout(10) + conn, _ = self.sock.accept() + conn.close() + + client_stdin = io.StringIO(client_stdin) + client_stdout = io.StringIO() + client_stderr = io.StringIO() + + self.addCleanup(client_stdin.close) + self.addCleanup(client_stdout.close) + self.addCleanup(client_stderr.close) + self.addCleanup(process.wait) + + with ( + unittest.mock.patch("sys.stdin", client_stdin), + redirect_stdout(client_stdout), + redirect_stderr(client_stderr), + unittest.mock.patch("sys.argv", ["pdb", "-p", str(process.pid)]), + unittest.mock.patch( + "pdb.exit_with_permission_help_text", side_effect=PermissionError + ), + ): + try: + pdb.main() + except PermissionError: + self.skipTest("Insufficient permissions for remote execution") + + process.wait() + server_stdout = process.stdout.read() + server_stderr = process.stderr.read() + + if process.returncode != 0: + print("server failed") + print(f"server stdout:\n{server_stdout}") + print(f"server stderr:\n{server_stderr}") + + self.assertEqual(process.returncode, 0) + return { + "client": { + "stdout": client_stdout.getvalue(), + "stderr": client_stderr.getvalue(), + }, + "server": { + "stdout": server_stdout, + "stderr": server_stderr, + }, + } + + def test_attach_to_process_without_colors(self): + with force_color(False): + output = self.do_integration_test("ll\nx=42\n") + self.assertEqual(output["client"]["stderr"], "") + self.assertEqual(output["server"]["stderr"], "") + + self.assertEqual(output["server"]["stdout"], "Function returned: 42\n") + self.assertIn("while x == 1", output["client"]["stdout"]) + self.assertNotIn("\x1b", output["client"]["stdout"]) + + def test_attach_to_process_with_colors(self): + with force_color(True): + output = self.do_integration_test("ll\nx=42\n") + self.assertEqual(output["client"]["stderr"], "") + self.assertEqual(output["server"]["stderr"], "") + + self.assertEqual(output["server"]["stdout"], "Function returned: 42\n") + self.assertIn("\x1b", output["client"]["stdout"]) + self.assertNotIn("while x == 1", output["client"]["stdout"]) + self.assertIn("while x == 1", re.sub("\x1b[^m]*m", "", output["client"]["stdout"])) + + def test_attach_to_non_existent_process(self): + with force_color(False): + result = subprocess.run([sys.executable, "-m", "pdb", "-p", "999999"], text=True, capture_output=True) + self.assertNotEqual(result.returncode, 0) + if sys.platform == "darwin": + # On MacOS, attaching to a non-existent process gives PermissionError + error = "The specified process cannot be attached to due to insufficient permissions" + else: + error = "Cannot attach to pid 999999, please make sure that the process exists" + self.assertIn(error, result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_sax.py b/Lib/test/test_sax.py index 4abb4eb70b4..92190e7a538 100644 --- a/Lib/test/test_sax.py +++ b/Lib/test/test_sax.py @@ -1601,5 +1601,17 @@ def test_all(self): check__all__(self, sax, extra=extra) +class TestModule(unittest.TestCase): + def test_deprecated__version__and__date__(self): + for module in (sax.expatreader, sax.handler): + with self.subTest(module=module): + with self.assertWarnsRegex( + DeprecationWarning, + "'version' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(module, "version") + self.assertEqual(cm.filename, __file__) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_shelve.py b/Lib/test/test_shelve.py index 08c6562f2a2..5f6a030e018 100644 --- a/Lib/test/test_shelve.py +++ b/Lib/test/test_shelve.py @@ -1,10 +1,11 @@ +import array import unittest import dbm import shelve import pickle import os -from test.support import os_helper +from test.support import import_helper, os_helper, subTests from collections.abc import MutableMapping from test.test_dbm import dbm_iterator @@ -165,6 +166,255 @@ def test_default_protocol(self): with shelve.Shelf({}) as s: self.assertEqual(s._protocol, pickle.DEFAULT_PROTOCOL) + def test_custom_serializer_and_deserializer(self): + os.mkdir(self.dirname) + self.addCleanup(os_helper.rmtree, self.dirname) + + def serializer(obj, protocol): + if isinstance(obj, (bytes, bytearray, str)): + if protocol == 5: + if isinstance(obj, bytearray): + return bytes(obj) # DBM backends expect bytes + return obj + return type(obj).__name__ + elif isinstance(obj, array.array): + return obj.tobytes() + raise TypeError(f"Unsupported type for serialization: {type(obj)}") + + def deserializer(data): + if isinstance(data, (bytes, bytearray, str)): + return data.decode("utf-8") + elif isinstance(data, array.array): + return array.array("b", data) + raise TypeError( + f"Unsupported type for deserialization: {type(data)}" + ) + + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(proto=proto), shelve.open( + self.fn, + protocol=proto, + serializer=serializer, + deserializer=deserializer + ) as s: + bar = "bar" + bytes_data = b"Hello, world!" + bytearray_data = bytearray(b"\x00\x01\x02\x03\x04") + array_data = array.array("i", [1, 2, 3, 4, 5]) + + s["foo"] = bar + s["bytes_data"] = bytes_data + s["bytearray_data"] = bytearray_data + s["array_data"] = array_data + + if proto == 5: + self.assertEqual(s["foo"], str(bar)) + self.assertEqual(s["bytes_data"], "Hello, world!") + self.assertEqual( + s["bytearray_data"], bytearray_data.decode() + ) + self.assertEqual( + s["array_data"], array_data.tobytes().decode() + ) + else: + self.assertEqual(s["foo"], "str") + self.assertEqual(s["bytes_data"], "bytes") + self.assertEqual(s["bytearray_data"], "bytearray") + self.assertEqual( + s["array_data"], array_data.tobytes().decode() + ) + + @subTests("serialized", [None, ["invalid type"]]) + def test_custom_invalid_serializer(self, serialized): + test_dir = f"{self.dirname}_{id(serialized)}" + os.mkdir(test_dir) + self.addCleanup(os_helper.rmtree, test_dir) + test_fn = os.path.join(test_dir, "shelftemp.db") + + def serializer(obj, protocol=None): + return serialized + + def deserializer(data): + return data.decode("utf-8") + + # Since the serializer returns an invalid type or None, + # dbm.error is raised by dbm.sqlite3 and TypeError is raised + # by other backends. + with self.assertRaises((TypeError, dbm.error)): + with shelve.open(test_fn, serializer=serializer, + deserializer=deserializer) as s: + s["foo"] = "bar" + + def test_custom_incomplete_deserializer(self): + os.mkdir(self.dirname) + self.addCleanup(os_helper.rmtree, self.dirname) + + def serializer(obj, protocol=None): + return type(obj).__name__.encode("utf-8") + + def deserializer(data): + pass + + with shelve.open(self.fn, serializer=serializer, + deserializer=deserializer) as s: + s["foo"] = "bar" + self.assertNotEqual(s["foo"], "bar") + self.assertIsNone(s["foo"]) + + def test_custom_serializer_and_deserializer_bsd_db_shelf(self): + berkeleydb = import_helper.import_module("berkeleydb") + os.mkdir(self.dirname) + self.addCleanup(os_helper.rmtree, self.dirname) + + def serializer(obj, protocol=None): + data = obj.__class__.__name__ + if protocol == 5: + data = str(len(data)) + return data.encode("utf-8") + + def deserializer(data): + return data.decode("utf-8") + + def type_name_len(obj): + return f"{(len(type(obj).__name__))}" + + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(proto=proto), shelve.BsdDbShelf( + berkeleydb.btopen(self.fn), + protocol=proto, + serializer=serializer, + deserializer=deserializer, + ) as s: + bar = "bar" + bytes_obj = b"Hello, world!" + bytearray_obj = bytearray(b"\x00\x01\x02\x03\x04") + arr_obj = array.array("i", [1, 2, 3, 4, 5]) + + s["foo"] = bar + s["bytes_data"] = bytes_obj + s["bytearray_data"] = bytearray_obj + s["array_data"] = arr_obj + + if proto == 5: + self.assertEqual(s["foo"], type_name_len(bar)) + self.assertEqual(s["bytes_data"], type_name_len(bytes_obj)) + self.assertEqual(s["bytearray_data"], + type_name_len(bytearray_obj)) + self.assertEqual(s["array_data"], type_name_len(arr_obj)) + + k, v = s.set_location(b"foo") + self.assertEqual(k, "foo") + self.assertEqual(v, type_name_len(bar)) + + k, v = s.previous() + self.assertEqual(k, "bytes_data") + self.assertEqual(v, type_name_len(bytes_obj)) + + k, v = s.previous() + self.assertEqual(k, "bytearray_data") + self.assertEqual(v, type_name_len(bytearray_obj)) + + k, v = s.previous() + self.assertEqual(k, "array_data") + self.assertEqual(v, type_name_len(arr_obj)) + + k, v = s.next() + self.assertEqual(k, "bytearray_data") + self.assertEqual(v, type_name_len(bytearray_obj)) + + k, v = s.next() + self.assertEqual(k, "bytes_data") + self.assertEqual(v, type_name_len(bytes_obj)) + + k, v = s.first() + self.assertEqual(k, "array_data") + self.assertEqual(v, type_name_len(arr_obj)) + else: + k, v = s.set_location(b"foo") + self.assertEqual(k, "foo") + self.assertEqual(v, "str") + + k, v = s.previous() + self.assertEqual(k, "bytes_data") + self.assertEqual(v, "bytes") + + k, v = s.previous() + self.assertEqual(k, "bytearray_data") + self.assertEqual(v, "bytearray") + + k, v = s.previous() + self.assertEqual(k, "array_data") + self.assertEqual(v, "array") + + k, v = s.next() + self.assertEqual(k, "bytearray_data") + self.assertEqual(v, "bytearray") + + k, v = s.next() + self.assertEqual(k, "bytes_data") + self.assertEqual(v, "bytes") + + k, v = s.first() + self.assertEqual(k, "array_data") + self.assertEqual(v, "array") + + self.assertEqual(s["foo"], "str") + self.assertEqual(s["bytes_data"], "bytes") + self.assertEqual(s["bytearray_data"], "bytearray") + self.assertEqual(s["array_data"], "array") + + def test_custom_incomplete_deserializer_bsd_db_shelf(self): + berkeleydb = import_helper.import_module("berkeleydb") + os.mkdir(self.dirname) + self.addCleanup(os_helper.rmtree, self.dirname) + + def serializer(obj, protocol=None): + return type(obj).__name__.encode("utf-8") + + def deserializer(data): + pass + + with shelve.BsdDbShelf(berkeleydb.btopen(self.fn), + serializer=serializer, + deserializer=deserializer) as s: + s["foo"] = "bar" + self.assertIsNone(s["foo"]) + self.assertNotEqual(s["foo"], "bar") + + def test_custom_incomplete_serializer_bsd_db_shelf(self): + berkeleydb = import_helper.import_module("berkeleydb") + os.mkdir(self.dirname) + self.addCleanup(os_helper.rmtree, self.dirname) + + def serializer(obj, protocol=None): + pass + + def deserializer(data): + return data.decode("utf-8") + + with shelve.BsdDbShelf(berkeleydb.btopen(self.fn), + serializer=serializer, + deserializer=deserializer) as s: + s["foo"] = "bar" + self.assertEqual(s["foo"], "") + self.assertNotEqual(s["foo"], "bar") + + def test_missing_custom_deserializer(self): + def serializer(obj, protocol=None): + pass + + kwargs = dict(protocol=2, writeback=False, serializer=serializer) + self.assertRaises(shelve.ShelveError, shelve.Shelf, {}, **kwargs) + self.assertRaises(shelve.ShelveError, shelve.BsdDbShelf, {}, **kwargs) + + def test_missing_custom_serializer(self): + def deserializer(data): + pass + + kwargs = dict(protocol=2, writeback=False, deserializer=deserializer) + self.assertRaises(shelve.ShelveError, shelve.Shelf, {}, **kwargs) + self.assertRaises(shelve.ShelveError, shelve.BsdDbShelf, {}, **kwargs) + class TestShelveBase: type2test = shelve.Shelf diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index a03d7cbe16b..0d235d90f11 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -1,17 +1,26 @@ """sqlite3 CLI tests.""" import sqlite3 +import sys +import textwrap import unittest +import unittest.mock +import os from sqlite3.__main__ import main as cli +from test.support.import_helper import import_module from test.support.os_helper import TESTFN, unlink +from test.support.pty_helper import run_pty from test.support import ( captured_stdout, captured_stderr, captured_stdin, - force_not_colorized, + force_not_colorized_test_class, + requires_subprocess, + verbose, ) +@force_not_colorized_test_class class CommandLineInterface(unittest.TestCase): def _do_test(self, *args, expect_success=True): @@ -37,7 +46,6 @@ def expect_failure(self, *args): self.assertEqual(out, "") return err - @force_not_colorized def test_cli_help(self): out = self.expect_success("-h") self.assertIn("usage: ", out) @@ -69,10 +77,11 @@ def test_cli_on_disk_db(self): self.assertIn("(0,)", out) +@force_not_colorized_test_class class InteractiveSession(unittest.TestCase): MEMORY_DB_MSG = "Connected to a transient in-memory database" - PS1 = "sqlite> " - PS2 = "... " + PS1 = "\001\002sqlite> \001\002" + PS2 = "\001\002 ... \001\002" def run_cli(self, *args, commands=()): with ( @@ -129,7 +138,7 @@ def test_interact_dot_commands_unknown(self): self.assertEndsWith(out, self.PS1) self.assertEqual(out.count(self.PS1), 2) self.assertEqual(out.count(self.PS2), 0) - self.assertIn("Error", err) + self.assertIn('Error: unknown command: "', err) # test "unknown_command" is pointed out in the error message self.assertIn("unknown_command", err) @@ -190,6 +199,247 @@ def test_interact_on_disk_file(self): out, _ = self.run_cli(TESTFN, commands=("SELECT count(t) FROM t;",)) self.assertIn("(0,)\n", out) + def test_color(self): + with unittest.mock.patch("_colorize.can_colorize", return_value=True): + out, err = self.run_cli(commands="TEXT\n") + self.assertIn("\x01\x1b[1;35m\x02sqlite> \x01\x1b[0m\x02", out) + self.assertIn("\x01\x1b[1;35m\x02 ... \x01\x1b[0m\x02\x01\x1b", out) + out, err = self.run_cli(commands=("sel;",)) + self.assertIn('\x1b[1;35mOperationalError (SQLITE_ERROR)\x1b[0m: ' + '\x1b[35mnear "sel": syntax error\x1b[0m', err) + + +@requires_subprocess() +@force_not_colorized_test_class +class Completion(unittest.TestCase): + # run_pty() creates a real terminal environment, where sqlite3 CLI + # SqliteInteractiveConsole invokes GNU Readline for input. Readline's + # _rl_strip_prompt() strips \001 and \002 from the output, so test + # assertions use the plain prompt. + PS1 = "sqlite> " + + @classmethod + def setUpClass(cls): + readline = import_module("readline") + if readline.backend == "editline": + raise unittest.SkipTest("libedit readline is not supported") + if sys.platform.startswith("openbsd"): + # OpenBSD's readline does not honor "completion-query-items 0". + raise unittest.SkipTest("OpenBSD readline hangs on completion") + + def write_input(self, input_, env=None): + script = textwrap.dedent(""" + import readline + from sqlite3.__main__ import main + + # Configure readline to ...: + # - hide control sequences surrounding each candidate + # - hide "Display all xxx possibilities? (y or n)" + # - show candidates one per line + readline.parse_and_bind("set colored-completion-prefix off") + readline.parse_and_bind("set completion-query-items 0") + readline.parse_and_bind("set page-completions off") + readline.parse_and_bind("set completion-display-width 0") + + main() + """) + return run_pty(script, input_, env) + + def test_complete_sql_keywords(self): + _sqlite3 = import_module("_sqlite3") + if not hasattr(_sqlite3, "SQLITE_KEYWORDS"): + raise unittest.SkipTest("unable to determine SQLite keywords") + + # List candidates starting with 'S', there should be multiple matches. + input_ = b"S\t\tEL\t 1;\n.quit\n" + output = self.write_input(input_) + self.assertIn(b"SELECT", output) + self.assertIn(b"SET", output) + self.assertIn(b"SAVEPOINT", output) + self.assertIn(b"(1,)", output) + + # Keywords are completed in upper case for even lower case user input. + input_ = b"sel\t\t 1;\n.quit\n" + output = self.write_input(input_) + self.assertIn(b"SELECT", output) + self.assertIn(b"(1,)", output) + + # .commands are completed without changing case + input_ = b".ver\t\n.quit\n" + output = self.write_input(input_) + self.assertIn(b".version", output) + + def test_complete_table_indexes_triggers_views(self): + input_ = textwrap.dedent("""\ + CREATE TABLE _Table (id); + CREATE INDEX _Index ON _table (id); + CREATE TRIGGER _Trigger BEFORE INSERT + ON _Table BEGIN SELECT 1; END; + CREATE VIEW _View AS SELECT 1; + + CREATE TEMP TABLE _Temp_table (id); + CREATE INDEX temp._Temp_index ON _Temp_table (id); + CREATE TEMP TRIGGER _Temp_trigger BEFORE INSERT + ON _Table BEGIN SELECT 1; END; + CREATE TEMP VIEW _Temp_view AS SELECT 1; + + ATTACH ':memory:' AS attached; + CREATE TABLE attached._Attached_table (id); + CREATE INDEX attached._Attached_index ON _Attached_table (id); + CREATE TRIGGER attached._Attached_trigger BEFORE INSERT + ON _Attached_table BEGIN SELECT 1; END; + CREATE VIEW attached._Attached_view AS SELECT 1; + + SELECT id FROM _\t\tta\t; + .quit\n""").encode() + output = self.write_input(input_) + lines = output.decode().splitlines() + indices = [i for i, line in enumerate(lines) + if line.startswith(self.PS1)] + start, end = indices[-3], indices[-2] + candidates = [l.strip() for l in lines[start+1:end]] + self.assertEqual(candidates, + [ + "_Attached_index", + "_Attached_table", + "_Attached_trigger", + "_Attached_view", + "_Index", + "_Table", + "_Temp_index", + "_Temp_table", + "_Temp_trigger", + "_Temp_view", + "_Trigger", + "_View", + ], + ) + start, end = indices[-2], indices[-1] + # direct match with '_Table' completed, no candidates displayed + candidates = [l.strip() for l in lines[start+1:end]] + self.assertEqual(len(candidates), 0) + + @unittest.skipIf(sqlite3.sqlite_version_info < (3, 16, 0), + "PRAGMA table-valued function is not available until " + "SQLite 3.16.0") + def test_complete_columns(self): + input_ = textwrap.dedent("""\ + CREATE TABLE _table (_col_table); + CREATE TEMP TABLE _temp_table (_col_temp); + ATTACH ':memory:' AS attached; + CREATE TABLE attached._attached_table (_col_attached); + + SELECT _col_\t\tta\tFROM _table; + .quit\n""").encode() + output = self.write_input(input_) + lines = output.decode().splitlines() + indices = [ + i for i, line in enumerate(lines) if line.startswith(self.PS1) + ] + start, end = indices[-3], indices[-2] + candidates = [l.strip() for l in lines[start+1:end]] + + self.assertEqual( + candidates, ["_col_attached", "_col_table", "_col_temp"] + ) + + @unittest.skipIf(sqlite3.sqlite_version_info < (3, 30, 0), + "PRAGMA function_list is not available until " + "SQLite 3.30.0") + def test_complete_functions(self): + input_ = b"SELECT AV\t1);\n.quit\n" + output = self.write_input(input_) + self.assertIn(b"AVG(1);", output) + self.assertIn(b"(1.0,)", output) + + # Functions are completed in upper case for even lower case user input. + input_ = b"SELECT av\t1);\n.quit\n" + output = self.write_input(input_) + self.assertIn(b"AVG(1);", output) + self.assertIn(b"(1.0,)", output) + + def test_complete_schemata(self): + input_ = textwrap.dedent("""\ + ATTACH ':memory:' AS MixedCase; + -- Test '_' is escaped in Like pattern filtering + ATTACH ':memory:' AS _underscore; + -- Let database_list pragma have a 'temp' schema entry + CREATE TEMP TABLE _table (id); + + SELECT * FROM \t\tmIX\t.sqlite_master; + SELECT * FROM _und\t.sqlite_master; + .quit\n""").encode() + output = self.write_input(input_) + lines = output.decode().splitlines() + indices = [ + i for i, line in enumerate(lines) if line.startswith(self.PS1) + ] + start, end = indices[-4], indices[-3] + candidates = [l.strip() for l in lines[start+1:end]] + self.assertIn("MixedCase", candidates) + self.assertIn("_underscore", candidates) + self.assertIn("main", candidates) + self.assertIn("temp", candidates) + + @unittest.skipIf(sys.platform.startswith(("freebsd", "dragonfly", "sunos")), + "Two actual tabs are inserted when there are no matching" + " completions in the pseudo-terminal opened by run_pty()" + " on this platform") + def test_complete_no_match(self): + input_ = b"xyzzy\t\t\b\b\b\b\b\b\b.quit\n" + # Set NO_COLOR to disable coloring for self.PS1. + output = self.write_input(input_, env={**os.environ, "NO_COLOR": "1"}) + lines = output.decode().splitlines() + indices = ( + i for i, line in enumerate(lines, 1) + if line.startswith(f"{self.PS1}xyzzy") + ) + line_num = next(indices, -1) + self.assertNotEqual(line_num, -1) + # Completions occupy lines, assert no extra lines when there is nothing + # to complete. + self.assertEqual(line_num, len(lines)) + + def test_complete_no_input(self): + script = textwrap.dedent(""" + import readline + from sqlite3.__main__ import main + + # Configure readline to ...: + # - hide control sequences surrounding each candidate + # - hide "Display all xxx possibilities? (y or n)" + # - hide "--More--" + # - show candidates one per line + readline.parse_and_bind("set colored-completion-prefix off") + readline.parse_and_bind("set colored-stats off") + readline.parse_and_bind("set completion-query-items 0") + readline.parse_and_bind("set page-completions off") + readline.parse_and_bind("set completion-display-width 0") + readline.parse_and_bind("set show-all-if-ambiguous off") + readline.parse_and_bind("set show-all-if-unmodified off") + + main() + """) + input_ = b"\t\t.quit\n" + output = run_pty(script, input_, env={**os.environ, "NO_COLOR": "1"}) + try: + lines = output.decode().splitlines() + indices = [ + i for i, line in enumerate(lines) + if line.startswith(self.PS1) + ] + self.assertEqual(len(indices), 2) + start, end = indices + candidates = [l.strip() for l in lines[start+1:end]] + self.assertEqual(candidates, sorted(candidates)) + except: + if verbose: + print(' PTY output: '.center(30, '-')) + print(output.decode(errors='replace')) + print(' end PTY output '.center(30, '-')) + raise + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index ef8acd0f338..d4602ac4322 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -551,17 +551,9 @@ def test_connection_config(self): @unittest.expectedFailure # TODO: RUSTPYTHON; deprecation warning not emitted for positional args def test_connect_positional_arguments(self): - regex = ( - r"Passing more than 1 positional argument to sqlite3.connect\(\)" - " is deprecated. Parameters 'timeout', 'detect_types', " - "'isolation_level', 'check_same_thread', 'factory', " - "'cached_statements' and 'uri' will become keyword-only " - "parameters in Python 3.15." - ) - with self.assertWarnsRegex(DeprecationWarning, regex) as cm: - cx = sqlite.connect(":memory:", 1.0) - cx.close() - self.assertEqual(cm.filename, __file__) + with self.assertRaisesRegex(TypeError, + r'connect\(\) takes at most 1 positional arguments'): + sqlite.connect(":memory:", 1.0) @unittest.expectedFailure # TODO: RUSTPYTHON; ResourceWarning not emitted def test_connection_resource_warning(self): @@ -1409,6 +1401,18 @@ def test_blob_set_slice(self): actual = self.cx.execute("select b from test").fetchone()[0] self.assertEqual(actual, expected) + def test_blob_set_slice_with_step_keeps_bytes_intact(self): + # The buffer used for the read-patch-write cycle must not be the + # bytes object read from the blob: for a single byte it is an + # immortal singleton. + old_byte = self.data[5] + self.blob[5:6:2] = b"\xab" + self.assertEqual(bytes([old_byte])[0], old_byte) + self.assertEqual(self.blob[5:6], b"\xab") + expected = self.data[:5] + b"\xab" + self.data[6:] + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, expected) + def test_blob_set_empty_slice(self): self.blob[0:0] = b"" self.assertEqual(self.blob[:], self.data) diff --git a/Lib/test/test_sqlite3/test_dump.py b/Lib/test/test_sqlite3/test_dump.py index e18a207e9f6..7841b610cd2 100644 --- a/Lib/test/test_sqlite3/test_dump.py +++ b/Lib/test/test_sqlite3/test_dump.py @@ -56,6 +56,24 @@ def test_table_dump(self): [self.assertEqual(expected_sqls[i], actual_sqls[i]) for i in range(len(expected_sqls))] + def test_dump_single_quote_in_identifier(self): + # A single quote in a table or column name must not break the dump. + self.cu.execute("""CREATE TABLE "a'b" ("c'd" text);""") + self.cu.execute("""INSERT INTO "a'b" VALUES('x''y');""") + expected = [ + "BEGIN TRANSACTION;", + """CREATE TABLE "a'b" ("c'd" text);""", + """INSERT INTO "a'b" VALUES('x''y');""", + "COMMIT;", + ] + actual = list(self.cx.iterdump()) + self.assertEqual(expected, actual) + # The dump restores into a fresh database. + with memory_database() as cx2: + cx2.executescript("".join(actual)) + row = cx2.execute("""SELECT "c'd" FROM "a'b";""").fetchone() + self.assertEqual(row[0], "x'y") + def test_table_dump_filter(self): all_table_sqls = [ """CREATE TABLE "some_table_2" ("id_1" INTEGER);""", diff --git a/Lib/test/test_sqlite3/test_factory.py b/Lib/test/test_sqlite3/test_factory.py index 81170c653ae..79bb0b0f2d6 100644 --- a/Lib/test/test_sqlite3/test_factory.py +++ b/Lib/test/test_sqlite3/test_factory.py @@ -74,18 +74,9 @@ class Factory(sqlite.Connection): def __init__(self, *args, **kwargs): super(Factory, self).__init__(*args, **kwargs) - regex = ( - r"Passing more than 1 positional argument to _sqlite3.Connection\(\) " - r"is deprecated. Parameters 'timeout', 'detect_types', " - r"'isolation_level', 'check_same_thread', 'factory', " - r"'cached_statements' and 'uri' will become keyword-only " - r"parameters in Python 3.15." - ) - with self.assertWarnsRegex(DeprecationWarning, regex) as cm: - with memory_database(5.0, 0, None, True, Factory) as con: - self.assertIsNone(con.isolation_level) - self.assertIsInstance(con, Factory) - self.assertEqual(cm.filename, __file__) + with self.assertRaisesRegex(TypeError, + r'connect\(\) takes at most 1 positional arguments'): + memory_database(5.0, 0, None, True, Factory) class CursorFactoryTests(MemoryDatabaseMixin, unittest.TestCase): @@ -251,6 +242,17 @@ def test_sqlite_row_hash_cmp(self): self.assertEqual(hash(row_1), hash(row_2)) + def test_sqlite_row_hash_unhashable(self): + # An unhashable value must raise TypeError, not SystemError. + sqlite.register_converter("LST", lambda b: [1, 2, 3]) + self.addCleanup(sqlite.converters.pop, "LST", None) + with memory_database(detect_types=sqlite.PARSE_DECLTYPES) as con: + con.row_factory = sqlite.Row + con.execute("create table t(x LST)") + con.execute("insert into t values(?)", (b"x",)) + row = con.execute("select x from t").fetchone() + self.assertRaisesRegex(TypeError, "unhashable", hash, row) + def test_sqlite_row_as_sequence(self): # Checks if the row object can act like a sequence. row = self.con.execute("select 1 as a, 2 as b").fetchone() diff --git a/Lib/test/test_sqlite3/test_hooks.py b/Lib/test/test_sqlite3/test_hooks.py index e5b946bbaf2..77b7498ab45 100644 --- a/Lib/test/test_sqlite3/test_hooks.py +++ b/Lib/test/test_sqlite3/test_hooks.py @@ -290,16 +290,9 @@ def bad_progress(): @unittest.expectedFailure # TODO: RUSTPYTHON; keyword-only arguments not supported for set_progress_handler def test_set_progress_handler_keyword_args(self): - regex = ( - r"Passing keyword argument 'progress_handler' to " - r"_sqlite3.Connection.set_progress_handler\(\) is deprecated. " - r"Parameter 'progress_handler' will become positional-only in " - r"Python 3.15." - ) - - with self.assertWarnsRegex(DeprecationWarning, regex) as cm: + with self.assertRaisesRegex(TypeError, + 'takes at least 1 positional argument'): self.con.set_progress_handler(progress_handler=lambda: None, n=1) - self.assertEqual(cm.filename, __file__) # When a handler has an invalid signature, the exception raised is # the same that would be raised if the handler "negatively" replied. @@ -464,16 +457,9 @@ def test_trace_bad_handler(self): @unittest.expectedFailure # TODO: RUSTPYTHON; keyword-only arguments not supported for set_trace_callback def test_set_trace_callback_keyword_args(self): - regex = ( - r"Passing keyword argument 'trace_callback' to " - r"_sqlite3.Connection.set_trace_callback\(\) is deprecated. " - r"Parameter 'trace_callback' will become positional-only in " - r"Python 3.15." - ) - - with self.assertWarnsRegex(DeprecationWarning, regex) as cm: + with self.assertRaisesRegex(TypeError, + 'takes exactly 1 positional argument'): self.con.set_trace_callback(trace_callback=lambda: None) - self.assertEqual(cm.filename, __file__) # When a handler has an invalid signature, the exception raised is # the same that would be raised if the handler "negatively" replied, diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index d63bccf9696..0ee1a7d2cce 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -429,27 +429,9 @@ def test_func_return_illegal_value(self): @unittest.expectedFailure # TODO: RUSTPYTHON; deprecation warning not emitted for keyword args def test_func_keyword_args(self): - regex = ( - r"Passing keyword arguments 'name', 'narg' and 'func' to " - r"_sqlite3.Connection.create_function\(\) is deprecated. " - r"Parameters 'name', 'narg' and 'func' will become " - r"positional-only in Python 3.15." - ) - - def noop(): - return None - - with self.assertWarnsRegex(DeprecationWarning, regex) as cm: - self.con.create_function("noop", 0, func=noop) - self.assertEqual(cm.filename, __file__) - - with self.assertWarnsRegex(DeprecationWarning, regex) as cm: - self.con.create_function("noop", narg=0, func=noop) - self.assertEqual(cm.filename, __file__) - - with self.assertWarnsRegex(DeprecationWarning, regex) as cm: - self.con.create_function(name="noop", narg=0, func=noop) - self.assertEqual(cm.filename, __file__) + with self.assertRaisesRegex(TypeError, + 'takes exactly 3 positional arguments'): + self.con.create_function("noop", 0, func=lambda: None) class WindowSumInt: @@ -753,25 +735,9 @@ def test_aggr_text(self): @unittest.expectedFailure # TODO: RUSTPYTHON; keyword-only arguments not supported for create_aggregate def test_agg_keyword_args(self): - regex = ( - r"Passing keyword arguments 'name', 'n_arg' and 'aggregate_class' to " - r"_sqlite3.Connection.create_aggregate\(\) is deprecated. " - r"Parameters 'name', 'n_arg' and 'aggregate_class' will become " - r"positional-only in Python 3.15." - ) - - with self.assertWarnsRegex(DeprecationWarning, regex) as cm: + with self.assertRaisesRegex(TypeError, + 'takes exactly 3 positional arguments'): self.con.create_aggregate("test", 1, aggregate_class=AggrText) - self.assertEqual(cm.filename, __file__) - - with self.assertWarnsRegex(DeprecationWarning, regex) as cm: - self.con.create_aggregate("test", n_arg=1, aggregate_class=AggrText) - self.assertEqual(cm.filename, __file__) - - with self.assertWarnsRegex(DeprecationWarning, regex) as cm: - self.con.create_aggregate(name="test", n_arg=0, - aggregate_class=AggrText) - self.assertEqual(cm.filename, __file__) class AuthorizerTests(unittest.TestCase): @@ -817,16 +783,9 @@ def test_clear_authorizer(self): @unittest.expectedFailure # TODO: RUSTPYTHON; keyword-only arguments not supported for set_authorizer def test_authorizer_keyword_args(self): - regex = ( - r"Passing keyword argument 'authorizer_callback' to " - r"_sqlite3.Connection.set_authorizer\(\) is deprecated. " - r"Parameter 'authorizer_callback' will become positional-only in " - r"Python 3.15." - ) - - with self.assertWarnsRegex(DeprecationWarning, regex) as cm: + with self.assertRaisesRegex(TypeError, + 'takes exactly 1 positional argument'): self.con.set_authorizer(authorizer_callback=lambda: None) - self.assertEqual(cm.filename, __file__) class AuthorizerRaiseExceptionTests(AuthorizerTests): diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py index f10f41df8b9..28f4e475fcc 100644 --- a/Lib/test/test_str.py +++ b/Lib/test/test_str.py @@ -454,6 +454,13 @@ def test_maketrans_translate(self): self.assertEqual("[a\xe9]".translate(str.maketrans({'a': '<\u20ac>'})), "[<\u20ac>\xe9]") + # with frozendict + tbl = self.type2test.maketrans(frozendict({'s': 'S', 'T': 't'})) + self.assertEqual(tbl, {ord('s'): 'S', ord('T'): 't'}) + self.assertEqual('sTan'.translate(tbl), 'Stan') + tbl = self.type2test.maketrans(frozendict({'a': None, 'b': ''})) + self.checkequalnofix('c', 'abababc', 'translate', tbl) + # invalid Unicode characters invalid_char = 0x10ffff+1 for before in "a\xe9\u20ac\U0010ffff": @@ -600,6 +607,21 @@ def test_replace_id(self): text = 'abc def' self.assertIs(text.replace(pattern, pattern), text) + @support.nomemtest + def test_replace_oom(self): + # https://github.com/python/cpython/issues/152228 + s1 = "轘" * 4 + s2 = "&" + s3 = "&" + assertion = self.assertRaises(MemoryError) + _testcapi.set_nomemory(0, 0) + try: + # No allocations made in the test itself: + with assertion: + s1.replace(s2, s3) # this line used to crash before + finally: + _testcapi.remove_mem_hooks() + def test_repeat_id_preserving(self): a = '123abc1@' b = '456zyx-+' @@ -1580,17 +1602,40 @@ def __int__(self): self.assertEqual('%X' % letter_m, '6D') self.assertEqual('%o' % letter_m, '155') self.assertEqual('%c' % letter_m, 'm') - self.assertRaisesRegex(TypeError, '%x format: an integer is required, not float', operator.mod, '%x', 3.14) - self.assertRaisesRegex(TypeError, '%X format: an integer is required, not float', operator.mod, '%X', 2.11) - self.assertRaisesRegex(TypeError, '%o format: an integer is required, not float', operator.mod, '%o', 1.79) - self.assertRaisesRegex(TypeError, '%x format: an integer is required, not PseudoFloat', operator.mod, '%x', pi) - self.assertRaisesRegex(TypeError, '%x format: an integer is required, not complex', operator.mod, '%x', 3j) - self.assertRaisesRegex(TypeError, '%X format: an integer is required, not complex', operator.mod, '%X', 2j) - self.assertRaisesRegex(TypeError, '%o format: an integer is required, not complex', operator.mod, '%o', 1j) - self.assertRaisesRegex(TypeError, '%u format: a real number is required, not complex', operator.mod, '%u', 3j) - self.assertRaisesRegex(TypeError, '%i format: a real number is required, not complex', operator.mod, '%i', 2j) - self.assertRaisesRegex(TypeError, '%d format: a real number is required, not complex', operator.mod, '%d', 1j) - self.assertRaisesRegex(TypeError, r'%c requires an int or a unicode character, not .*\.PseudoFloat', operator.mod, '%c', pi) + with self.assertRaisesRegex(TypeError, + 'format argument: %x requires an integer, not float'): + '%x' % 3.14 + with self.assertRaisesRegex(TypeError, + 'format argument: %X requires an integer, not float'): + '%X' % 2.11 + with self.assertRaisesRegex(TypeError, + 'format argument: %o requires an integer, not float'): + '%o' % 1.79 + with self.assertRaisesRegex(TypeError, + r'format argument: %x requires an integer, not .*\.PseudoFloat'): + '%x' % pi + with self.assertRaisesRegex(TypeError, + 'format argument: %x requires an integer, not complex'): + '%x' % 3j + with self.assertRaisesRegex(TypeError, + 'format argument: %X requires an integer, not complex'): + '%X' % 2j + with self.assertRaisesRegex(TypeError, + 'format argument: %o requires an integer, not complex'): + '%o' % 1j + with self.assertRaisesRegex(TypeError, + 'format argument: %u requires a real number, not complex'): + '%u' % 3j + with self.assertRaisesRegex(TypeError, + 'format argument: %i requires a real number, not complex'): + '%i' % 2j + with self.assertRaisesRegex(TypeError, + 'format argument: %d requires a real number, not complex'): + '%d' % 1j + with self.assertRaisesRegex(TypeError, + r'format argument: %c requires an integer or a unicode character, ' + r'not .*\.PseudoFloat'): + '%c' % pi class RaisingNumber: def __int__(self): diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index 4a7e52cbcba..9c737e39e63 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -1130,6 +1130,39 @@ def test_communicate_timeout_large_input(self): p.kill() p.wait() + def test_communicate_timeout_resume_partial_write(self): + """Resume writing input after a partial-write TimeoutExpired. + + Exercises the _input_offset bookkeeping across the + _communicate_io_posix factoring: a first communicate() must time out + mid-write, and a subsequent communicate() must finish delivering the + remaining bytes so the child receives the full input intact. + """ + # 1 MiB easily exceeds typical pipe buffers (~64 KiB) so writing + # blocks once the buffer fills before the child starts reading. + input_data = bytes(range(256)) * 4096 # 1 MiB, distinctive pattern + self.assertEqual(len(input_data), 1024 * 1024) + + p = subprocess.Popen( + [sys.executable, "-c", + "import sys, time; " + "time.sleep(0.5); " + "sys.stdout.buffer.write(sys.stdin.buffer.read())"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + try: + with self.assertRaises(subprocess.TimeoutExpired): + p.communicate(input_data, timeout=0.05) + + # Resume: no new input, generous timeout to avoid CI flakes. + stdout, stderr = p.communicate(timeout=support.LONG_TIMEOUT) + self.assertEqual(len(stdout), len(input_data)) + self.assertEqual(stdout, input_data) + finally: + p.kill() + p.wait() + # Test for the fd leak reported in http://bugs.python.org/issue2791. def test_communicate_pipe_fd_leak(self): for stdin_pipe in (False, True): @@ -1463,6 +1496,8 @@ def test_wait(self): def test_wait_timeout(self): p = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(0.3)"]) + with self.assertRaises(subprocess.TimeoutExpired) as c: + p.wait(timeout=0) with self.assertRaises(subprocess.TimeoutExpired) as c: p.wait(timeout=0.0001) self.assertIn("0.0001", str(c.exception)) # For coverage of __str__. @@ -2016,8 +2051,9 @@ def test_encoding_warning(self): run("echo hello", shell=True, text=True) check_output("echo hello", shell=True, text=True) """) + env = support.make_clean_env() cp = subprocess.run([sys.executable, "-Xwarn_default_encoding", "-c", code], - capture_output=True) + capture_output=True, env=env) lines = cp.stderr.splitlines() self.assertEqual(len(lines), 2, lines) self.assertStartsWith(lines[0], b":2: EncodingWarning: ") @@ -4139,5 +4175,148 @@ def test_broken_pipe_cleanup(self): self.assertTrue(proc.stdin.closed) + +class FastWaitTestCase(BaseTestCase): + """Tests for efficient (pidfd_open() + poll() / kqueue()) process + waiting in subprocess.Popen.wait(). + """ + CAN_USE_PIDFD_OPEN = subprocess._CAN_USE_PIDFD_OPEN + CAN_USE_KQUEUE = subprocess._CAN_USE_KQUEUE + COMMAND = [sys.executable, "-c", "import time; time.sleep(0.3)"] + WAIT_TIMEOUT = 0.0001 # 0.1 ms + + def assert_fast_waitpid_error(self, patch_point): + # Emulate a case where pidfd_open() or kqueue() fails. + # Busy-poll wait should be used as fallback. + exc = OSError(errno.EMFILE, os.strerror(errno.EMFILE)) + with mock.patch(patch_point, side_effect=exc) as m: + p = subprocess.Popen(self.COMMAND) + with self.assertRaises(subprocess.TimeoutExpired): + p.wait(self.WAIT_TIMEOUT) + self.assertEqual(p.wait(timeout=support.SHORT_TIMEOUT), 0) + self.assertTrue(m.called) + + @unittest.skipIf(not CAN_USE_PIDFD_OPEN, reason="needs pidfd_open()") + def test_wait_pidfd_open_error(self): + self.assert_fast_waitpid_error("os.pidfd_open") + + @unittest.skipIf(not CAN_USE_KQUEUE, reason="needs kqueue() for proc") + def test_wait_kqueue_error(self): + self.assert_fast_waitpid_error("select.kqueue") + + @unittest.skipIf(not CAN_USE_KQUEUE, reason="needs kqueue() for proc") + def test_kqueue_control_error(self): + # Emulate a case where kqueue.control() fails. Busy-poll wait + # should be used as fallback. + p = subprocess.Popen(self.COMMAND) + kq_mock = mock.Mock() + kq_mock.control.side_effect = OSError( + errno.EPERM, os.strerror(errno.EPERM) + ) + kq_mock.close = mock.Mock() + + with mock.patch("select.kqueue", return_value=kq_mock) as m: + with self.assertRaises(subprocess.TimeoutExpired): + p.wait(self.WAIT_TIMEOUT) + self.assertEqual(p.wait(timeout=support.SHORT_TIMEOUT), 0) + self.assertTrue(m.called) + + def assert_wait_race_condition(self, patch_target, real_func): + # Call pidfd_open() / kqueue(), then terminate the process. + # Make sure that the wait call (poll() / kqueue.control()) + # still works for a terminated PID. + p = subprocess.Popen(self.COMMAND) + + def wrapper(*args, **kwargs): + ret = real_func(*args, **kwargs) + try: + os.kill(p.pid, signal.SIGTERM) + os.waitpid(p.pid, 0) + except OSError: + pass + return ret + + with mock.patch(patch_target, side_effect=wrapper) as m: + status = p.wait(timeout=support.SHORT_TIMEOUT) + self.assertTrue(m.called) + self.assertEqual(status, 0) + + @unittest.skipIf(not CAN_USE_PIDFD_OPEN, reason="needs pidfd_open()") + def test_pidfd_open_race(self): + self.assert_wait_race_condition("os.pidfd_open", os.pidfd_open) + + @unittest.skipIf(not CAN_USE_KQUEUE, reason="needs kqueue() for proc") + def test_kqueue_race(self): + self.assert_wait_race_condition("select.kqueue", select.kqueue) + + def assert_notification_without_immediate_reap(self, patch_target): + # Verify fallback to busy polling when poll() / kqueue() + # succeeds, but waitpid(pid, WNOHANG) returns (0, 0). + def waitpid_wrapper(pid, flags): + nonlocal ncalls + ncalls += 1 + if ncalls == 1: + return (0, 0) + return real_waitpid(pid, flags) + + ncalls = 0 + real_waitpid = os.waitpid + with mock.patch.object(subprocess.Popen, patch_target, return_value=True) as m1: + with mock.patch("os.waitpid", side_effect=waitpid_wrapper) as m2: + p = subprocess.Popen(self.COMMAND) + with self.assertRaises(subprocess.TimeoutExpired): + p.wait(self.WAIT_TIMEOUT) + self.assertEqual(p.wait(timeout=support.SHORT_TIMEOUT), 0) + self.assertTrue(m1.called) + self.assertTrue(m2.called) + + @unittest.skipIf(not CAN_USE_PIDFD_OPEN, reason="needs pidfd_open()") + def test_pidfd_open_notification_without_immediate_reap(self): + self.assert_notification_without_immediate_reap("_wait_pidfd") + + @unittest.skipIf(not CAN_USE_KQUEUE, reason="needs kqueue() for proc") + def test_kqueue_notification_without_immediate_reap(self): + self.assert_notification_without_immediate_reap("_wait_kqueue") + + @unittest.skipUnless( + CAN_USE_PIDFD_OPEN or CAN_USE_KQUEUE, + "fast wait mechanism not available" + ) + def test_fast_path_avoid_busy_loop(self): + # assert that the busy loop is not called as long as the fast + # wait is available + with mock.patch('time.sleep') as m: + p = subprocess.Popen(self.COMMAND) + with self.assertRaises(subprocess.TimeoutExpired): + p.wait(self.WAIT_TIMEOUT) + self.assertEqual(p.wait(timeout=support.LONG_TIMEOUT), 0) + self.assertFalse(m.called) + + @unittest.skipIf(mswindows, "requires the POSIX wait implementation") + def test_wait_huge_timeout(self): + # gh-154836: very large timeout values used to overflow the C + # timestamp conversion in poll() / kqueue.control() and raise + # OverflowError / TypeError. + for timeout in (10**10, sys.maxsize, float('inf')): + with self.subTest(timeout=timeout): + p = subprocess.Popen(ZERO_RETURN_CMD) + self.assertEqual(p.wait(timeout=timeout), 0) + + @unittest.skipIf(mswindows, "requires the POSIX wait implementation") + def test_run_huge_timeout(self): + # gh-154836: same as test_wait_huge_timeout, via the + # subprocess.run() / communicate() code path. + cp = subprocess.run(ZERO_RETURN_CMD, timeout=1e10) + self.assertEqual(cp.returncode, 0) + + @unittest.skipIf(mswindows, "requires the POSIX wait implementation") + def test_wait_slices_do_not_expire_early(self): + # A clamped wait slice must not raise TimeoutExpired before the + # real deadline: with a tiny slice limit, a process that + # outlives many slices must still be waited for successfully. + with mock.patch.object(subprocess, "_MAXIMUM_WAIT_TIMEOUT", 0.01): + p = subprocess.Popen(self.COMMAND) # sleeps 0.3s + self.assertEqual(p.wait(timeout=support.SHORT_TIMEOUT), 0) + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 5885ab238ae..15cd12b6e9b 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -1,6 +1,8 @@ import contextlib import errno import importlib +import itertools +import inspect import io import logging import os @@ -18,6 +20,7 @@ from test import support from test.support import isolation +from test.support import hashlib_helper from test.support import import_helper from test.support import os_helper from test.support import script_helper @@ -94,9 +97,15 @@ def test_get_attribute(self): self.test_get_attribute) self.assertRaises(unittest.SkipTest, support.get_attribute, self, "foo") - @unittest.skip("failing buildbots") + @unittest.skipIf(support.is_android or support.is_apple_mobile, + 'Mobile platforms redirect stdout to system log') def test_get_original_stdout(self): - self.assertEqual(support.get_original_stdout(), sys.stdout) + if isinstance(sys.stdout, io.StringIO): + # gh-55258: When --junit-xml is used, stdout is a StringIO: + # use sys.__stdout__ in this case. + self.assertEqual(support.get_original_stdout(), sys.__stdout__) + else: + self.assertEqual(support.get_original_stdout(), sys.stdout) def test_unload(self): import sched # noqa: F401 @@ -483,6 +492,7 @@ def test_check__all__(self): self.assertRaises(AssertionError, support.check__all__, self, unittest) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless(hasattr(os, 'waitpid') and hasattr(os, 'WNOHANG'), 'need os.waitpid() and os.WNOHANG') @support.requires_fork() @@ -834,6 +844,239 @@ def test_linked_to_musl(self): # SuppressCrashReport +@hashlib_helper.requires_builtin_hashes() +class TestHashlibSupport(unittest.TestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.hashlib = import_helper.import_module("hashlib") + cls.hmac = import_helper.import_module("hmac") + + # All C extension modules must be present since blocking + # the built-in implementation while allowing OpenSSL or vice-versa + # may result in failures depending on the exposed built-in hashes. + cls._hashlib = import_helper.import_module("_hashlib") + cls._hmac = import_helper.import_module("_hmac") + cls._md5 = import_helper.import_module("_md5") + + def skip_if_fips_mode(self): + if self._hashlib.get_fips_mode(): + self.skipTest("disabled in FIPS mode") + + def skip_if_not_fips_mode(self): + if not self._hashlib.get_fips_mode(): + self.skipTest("requires FIPS mode") + + def check_context(self, disabled=True): + if disabled: + return self.assertRaises(ValueError) + return contextlib.nullcontext() + + def try_import_attribute(self, fullname, default=None): + if fullname is None: + return default + assert fullname.count('.') == 1, fullname + module_name, attribute = fullname.split('.', maxsplit=1) + try: + module = importlib.import_module(module_name) + except ImportError: + return default + try: + return getattr(module, attribute, default) + except TypeError: + return default + + def fetch_hash_function(self, name, implementation): + info = hashlib_helper.get_hash_func_info(name) + match hashlib_helper.Implementation(implementation): + case hashlib_helper.Implementation.hashlib: + method_name = info.hashlib.member_name + assert isinstance(method_name, str), method_name + return getattr(self.hashlib, method_name) + case hashlib_helper.Implementation.openssl: + method_name = info.openssl.member_name + assert isinstance(method_name, str | None), method_name + return getattr(self._hashlib, method_name or "", None) + fullname = info[implementation].fullname + return self.try_import_attribute(fullname) + + def fetch_hmac_function(self, name): + target = hashlib_helper.get_hmac_item_info(name) + return target.import_member() + + def check_openssl_hash(self, name, *, disabled=True): + """Check that OpenSSL HASH interface is enabled/disabled.""" + with self.check_context(disabled): + _ = self._hashlib.new(name) + if do_hash := self.fetch_hash_function(name, "openssl"): + self.assertStartsWith(do_hash.__name__, 'openssl_') + with self.check_context(disabled): + _ = do_hash(b"") + + def check_openssl_hmac(self, name, *, disabled=True): + """Check that OpenSSL HMAC interface is enabled/disabled.""" + if name in hashlib_helper.NON_HMAC_DIGEST_NAMES: + # HMAC-BLAKE and HMAC-SHAKE raise a ValueError as they are not + # supported at all (they do not make any sense in practice). + with self.assertRaises(ValueError): + self._hashlib.hmac_digest(b"", b"", name) + else: + with self.check_context(disabled): + _ = self._hashlib.hmac_digest(b"", b"", name) + # OpenSSL does not provide one-shot explicit HMAC functions + + def check_builtin_hash(self, name, *, disabled=True): + """Check that HACL* HASH interface is enabled/disabled.""" + if do_hash := self.fetch_hash_function(name, "builtin"): + self.assertEqual(do_hash.__name__, name) + with self.check_context(disabled): + _ = do_hash(b"") + + def check_builtin_hmac(self, name, *, disabled=True): + """Check that HACL* HMAC interface is enabled/disabled.""" + if name in hashlib_helper.NON_HMAC_DIGEST_NAMES: + # HMAC-BLAKE and HMAC-SHAKE raise a ValueError as they are not + # supported at all (they do not make any sense in practice). + with self.assertRaises(ValueError): + self._hmac.compute_digest(b"", b"", name) + else: + with self.check_context(disabled): + _ = self._hmac.compute_digest(b"", b"", name) + + with self.check_context(disabled): + _ = self._hmac.new(b"", b"", name) + + if do_hmac := self.fetch_hmac_function(name): + self.assertStartsWith(do_hmac.__name__, 'compute_') + with self.check_context(disabled): + _ = do_hmac(b"", b"") + else: + self.assertIn(name, hashlib_helper.NON_HMAC_DIGEST_NAMES) + + @support.subTests( + ('name', 'allow_openssl', 'allow_builtin'), + itertools.product( + hashlib_helper.CANONICAL_DIGEST_NAMES, + [True, False], + [True, False], + ) + ) + def test_disable_hash(self, name, allow_openssl, allow_builtin): + # In FIPS mode, the function may be available but would still need + # to raise a ValueError, so we will test the helper separately. + self.skip_if_fips_mode() + flags = dict(allow_openssl=allow_openssl, allow_builtin=allow_builtin) + is_fully_disabled = not allow_builtin and not allow_openssl + + with hashlib_helper.block_algorithm(name, **flags): + # OpenSSL's blake2s and blake2b are unknown names + # when only the OpenSSL interface is available. + if allow_openssl and not allow_builtin: + aliases = {'blake2s': 'blake2s256', 'blake2b': 'blake2b512'} + name_for_hashlib_new = aliases.get(name, name) + else: + name_for_hashlib_new = name + + with self.check_context(is_fully_disabled): + _ = self.hashlib.new(name_for_hashlib_new) + + # Since _hashlib is present, explicit blake2b/blake2s constructors + # use the built-in implementation, while others (since we are not + # in FIPS mode and since _hashlib exists) use the OpenSSL function. + with self.check_context(is_fully_disabled): + _ = getattr(self.hashlib, name)() + + self.check_openssl_hash(name, disabled=not allow_openssl) + self.check_builtin_hash(name, disabled=not allow_builtin) + + if name not in hashlib_helper.NON_HMAC_DIGEST_NAMES: + with self.check_context(is_fully_disabled): + _ = self.hmac.new(b"", b"", name) + with self.check_context(is_fully_disabled): + _ = self.hmac.HMAC(b"", b"", name) + with self.check_context(is_fully_disabled): + _ = self.hmac.digest(b"", b"", name) + + self.check_openssl_hmac(name, disabled=not allow_openssl) + self.check_builtin_hmac(name, disabled=not allow_builtin) + + @hashlib_helper.block_algorithm("md5") + def test_disable_hash_md5_in_fips_mode(self): + self.skip_if_not_fips_mode() + + self.assertRaises(ValueError, self.hashlib.new, "md5") + self.assertRaises(ValueError, self._hashlib.new, "md5") + self.assertRaises(ValueError, self.hashlib.md5) + self.assertRaises(ValueError, self._hashlib.openssl_md5) + + kwargs = dict(usedforsecurity=True) + self.assertRaises(ValueError, self.hashlib.new, "md5", **kwargs) + self.assertRaises(ValueError, self._hashlib.new, "md5", **kwargs) + self.assertRaises(ValueError, self.hashlib.md5, **kwargs) + self.assertRaises(ValueError, self._hashlib.openssl_md5, **kwargs) + + @hashlib_helper.block_algorithm("md5", allow_openssl=True) + def test_disable_hash_md5_in_fips_mode_allow_openssl(self): + self.skip_if_not_fips_mode() + # Allow the OpenSSL interface to be used but not the HACL* one. + # hashlib.new("md5") is dispatched to hashlib.openssl_md5() + self.assertRaises(ValueError, self.hashlib.new, "md5") + # dispatched to hashlib.openssl_md5() in FIPS mode + h2 = self.hashlib.new("md5", usedforsecurity=False) + self.assertIsInstance(h2, self._hashlib.HASH) + + # block_algorithm() does not mock hashlib.md5 and _hashlib.openssl_md5 + self.assertNotHasAttr(self.hashlib.md5, "__wrapped__") + self.assertNotHasAttr(self._hashlib.openssl_md5, "__wrapped__") + + hashlib_md5 = inspect.unwrap(self.hashlib.md5) + self.assertIs(hashlib_md5, self._hashlib.openssl_md5) + self.assertRaises(ValueError, self.hashlib.md5) + # allow MD5 to be used in FIPS mode if usedforsecurity=False + h3 = self.hashlib.md5(usedforsecurity=False) + self.assertIsInstance(h3, self._hashlib.HASH) + + @hashlib_helper.block_algorithm("md5", allow_builtin=True) + def test_disable_hash_md5_in_fips_mode_allow_builtin(self): + self.skip_if_not_fips_mode() + # Allow the HACL* interface to be used but not the OpenSSL one. + h1 = self.hashlib.new("md5") # dispatched to _md5.md5() + self.assertNotIsInstance(h1, self._hashlib.HASH) + h2 = self.hashlib.new("md5", usedforsecurity=False) + self.assertIsInstance(h2, type(h1)) + + # block_algorithm() mocks hashlib.md5 and _hashlib.openssl_md5 + self.assertHasAttr(self.hashlib.md5, "__wrapped__") + self.assertHasAttr(self._hashlib.openssl_md5, "__wrapped__") + + hashlib_md5 = inspect.unwrap(self.hashlib.md5) + openssl_md5 = inspect.unwrap(self._hashlib.openssl_md5) + self.assertIs(hashlib_md5, openssl_md5) + self.assertRaises(ValueError, self.hashlib.md5) + self.assertRaises(ValueError, self.hashlib.md5, + usedforsecurity=False) + + @hashlib_helper.block_algorithm("md5", + allow_openssl=True, + allow_builtin=True) + def test_disable_hash_md5_in_fips_mode_allow_all(self): + self.skip_if_not_fips_mode() + # hashlib.new() isn't blocked as it falls back to _md5.md5 + self.assertIsInstance(self.hashlib.new("md5"), self._md5.MD5Type) + self.assertRaises(ValueError, self._hashlib.new, "md5") + h = self._hashlib.new("md5", usedforsecurity=False) + self.assertIsInstance(h, self._hashlib.HASH) + + self.assertNotHasAttr(self.hashlib.md5, "__wrapped__") + self.assertNotHasAttr(self._hashlib.openssl_md5, "__wrapped__") + + self.assertIs(self.hashlib.md5, self._hashlib.openssl_md5) + self.assertRaises(ValueError, self.hashlib.md5) + h = self.hashlib.md5(usedforsecurity=False) + self.assertIsInstance(h, self._hashlib.HASH) + + class TestIsolated(unittest.TestCase): # Drive the sample tests in test._isolated_sample (which really spawn # subprocesses through @isolation.runInSubprocess()) under a private @@ -938,31 +1181,6 @@ def test_subclass_bypassing_setupclass_is_reported(self): self.assertEqual(len(result.errors), 1) self.assertIn('did not run in a subprocess', result.errors[0][1]) - @support.requires_subprocess() - def test_subprocess_dying_after_the_test_is_reported(self): - from test._isolated_sample import EXIT_CODE - result = self._run('MethodExitSample.test_passes_then_dies') - self.assertEqual(result.testsRun, 1) - self.assertEqual(len(result.errors), 1) - self.assertIn(f'exited with code {EXIT_CODE}', result.errors[0][1]) - - @support.requires_subprocess() - def test_subprocess_dying_does_not_hide_the_failure(self): - result = self._run('MethodExitSample.test_fails_and_dies') - self.assertEqual(self._names(result.failures), ['test_fails_and_dies']) - self.assertEqual(result.errors, []) - - @support.requires_subprocess() - def test_class_subprocess_dying_after_the_tests_is_reported(self): - # The tests that ran are still reported, and the crash once, for the class. - from test._isolated_sample import EXIT_CODE - result = self._run('ClassExitSample') - self.assertEqual(result.testsRun, 2) - self.assertEqual(result.failures, []) - self.assertEqual(len(result.errors), 1) - self.assertIn('tearDownClass', str(result.errors[0][0])) - self.assertIn(f'exited with code {EXIT_CODE}', result.errors[0][1]) - def test_skipped_without_subprocess_support(self): # On a platform without subprocess support the test is skipped in the # parent, before any subprocess is spawned. diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index 9537de3756f..c748243110d 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -5,6 +5,7 @@ import re import textwrap import symtable +import warnings import unittest from test import support @@ -254,6 +255,7 @@ def test_function_info(self): self.assertEqual(sorted(func.get_locals()), expected) self.assertEqual(sorted(func.get_globals()), ["bar", "glob", "some_assigned_global_var"]) self.assertEqual(self.internal.get_frees(), ("x",)) + self.assertEqual(self.spam.get_cells(), ("some_var", "x",)) def test_globals(self): self.assertTrue(self.spam.lookup("glob").is_global()) @@ -283,6 +285,9 @@ def test_local(self): def test_free(self): self.assertTrue(self.internal.lookup("x").is_free()) + def test_cells(self): + self.assertTrue(self.spam.lookup("x").is_cell()) + def test_referenced(self): self.assertTrue(self.internal.lookup("x").is_referenced()) self.assertTrue(self.spam.lookup("internal").is_referenced()) @@ -527,6 +532,58 @@ def test_symtable_entry_repr(self): expected = f"" self.assertEqual(repr(self.top._table), expected) + def test_lambda(self): + st = symtable.symtable("lambda x: x", "?", "exec") + self.assertEqual(len(st.get_children()), 1) + st = st.get_children()[0] + self.assertIs(st.get_type(), symtable.SymbolTableType.FUNCTION) + self.assertEqual(st.get_name(), "") + self.assertFalse(st.is_nested()) + self.assertEqual(sorted(st.get_identifiers()), ["x"]) + self.assertEqual(st.get_children(), []) + + def test_nested_lambda(self): + st = symtable.symtable("lambda x: lambda y=x: y", "?", "exec") + self.assertEqual(len(st.get_children()), 1) + st = st.get_children()[0] + self.assertIs(st.get_type(), symtable.SymbolTableType.FUNCTION) + self.assertEqual(st.get_name(), "") + self.assertFalse(st.is_nested()) + self.assertEqual(sorted(st.get_identifiers()), ["x"]) + self.assertEqual(len(st.get_children()), 1) + st = st.get_children()[0] + self.assertIs(st.get_type(), symtable.SymbolTableType.FUNCTION) + self.assertEqual(st.get_name(), "") + self.assertTrue(st.is_nested()) + self.assertEqual(sorted(st.get_identifiers()), ["y"]) + self.assertEqual(st.get_children(), []) + + def test_genexpr(self): + st = symtable.symtable("(x for x in a)", "?", "exec") + self.assertEqual(len(st.get_children()), 1) + st = st.get_children()[0] + self.assertIs(st.get_type(), symtable.SymbolTableType.FUNCTION) + self.assertEqual(st.get_name(), "") + self.assertFalse(st.is_nested()) + self.assertEqual(sorted(st.get_identifiers()), [".0", "x"]) + self.assertEqual(st.get_children(), []) + + def test_nested_genexpr(self): + st = symtable.symtable("((y for y in x) for x in a)", "?", "exec") + self.assertEqual(len(st.get_children()), 1) + st = st.get_children()[0] + self.assertIs(st.get_type(), symtable.SymbolTableType.FUNCTION) + self.assertEqual(st.get_name(), "") + self.assertFalse(st.is_nested()) + self.assertEqual(sorted(st.get_identifiers()), [".0", "x"]) + self.assertEqual(len(st.get_children()), 1) + st = st.get_children()[0] + self.assertIs(st.get_type(), symtable.SymbolTableType.FUNCTION) + self.assertEqual(st.get_name(), "") + self.assertTrue(st.is_nested()) + self.assertEqual(sorted(st.get_identifiers()), [".0", "y"]) + self.assertEqual(st.get_children(), []) + def test__symtable_refleak(self): # Regression test for reference leak in PyUnicode_FSDecoder. # See https://github.com/python/cpython/issues/139748. @@ -534,6 +591,30 @@ def test__symtable_refleak(self): # check error path when 'compile_type' AC conversion failed self.assertRaises(TypeError, symtable.symtable, '', mortal_str, 1) + def test_filter_syntax_warnings_by_module(self): + filename = support.findfile('test_import/data/syntax_warnings.py') + with open(filename, 'rb') as f: + source = f.read() + module_re = r'test\.test_import\.data\.syntax_warnings\z' + with warnings.catch_warnings(record=True) as wlog: + warnings.simplefilter('error') + warnings.filterwarnings('always', module=module_re) + symtable.symtable(source, filename, 'exec') + self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10]) + for wm in wlog: + self.assertEqual(wm.filename, filename) + self.assertIs(wm.category, SyntaxWarning) + + with warnings.catch_warnings(record=True) as wlog: + warnings.simplefilter('error') + warnings.filterwarnings('always', module=r'package\.module\z') + warnings.filterwarnings('error', module=module_re) + symtable.symtable(source, filename, 'exec', module='package.module') + self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10]) + for wm in wlog: + self.assertEqual(wm.filename, filename) + self.assertIs(wm.category, SyntaxWarning) + class ComprehensionTests(unittest.TestCase): def get_identifiers_recursive(self, st, res): diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index f30b609afa6..0d81f986763 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -6,6 +6,7 @@ import operator import os import random +import shutil import socket import struct import subprocess @@ -494,7 +495,7 @@ def test_getframemodulename(self): self.assertIs(f, f2) self.assertIsNone(sys._getframemodulename(i)) - # sys._current_frames() is a CPython-only gimmick. + @support.cpython_only # sys._current_frames() is a CPython-only gimmick. @threading_helper.reap_threads @threading_helper.requires_working_threading() def test_current_frames(self): @@ -562,6 +563,38 @@ def g456(): leave_g.set() t.join() + @support.cpython_only + @requires_subinterpreters + @threading_helper.requires_working_threading() + def test_current_frames_other_interpreters(self): + # gh-158364: sys._current_frames() would access frames of another + # interpreter and crash + import threading + + entered = threading.Event() + left = threading.Event() + + def park(): + entered.set() + left.wait() + + t = threading.Thread(target=park) + with threading_helper.start_threads([t], unlock=left.set): + entered.wait() + interp = interpreters.create() + try: + interp.exec(f"""if True: + import sys + import threading + + frames = sys._current_frames() + assert threading.get_ident() in frames, frames + assert frames[threading.get_ident()].f_globals is globals() + assert {t.ident} not in frames, frames + """) + finally: + interp.close() + @threading_helper.reap_threads @threading_helper.requires_working_threading() def test_current_exceptions(self): @@ -628,6 +661,39 @@ def g456(): leave_g.set() t.join() + @support.cpython_only + @requires_subinterpreters + @threading_helper.requires_working_threading() + def test_current_exceptions_other_interpreters(self): + # gh-158364: sys._current_exceptions() would hand out exceptions of + # another interpreter and crash + import threading + + entered = threading.Event() + left = threading.Event() + + def hold(): + # The thread has to be handling an exception, otherwise + # sys._current_exceptions() has nothing to report for it. + try: + raise ValueError + except ValueError: + entered.set() + left.wait() + + t = threading.Thread(target=hold) + with threading_helper.start_threads([t], unlock=left.set): + entered.wait() + interp = interpreters.create() + try: + interp.exec(f"""if True: + import sys + + assert {t.ident} not in sys._current_exceptions() + """) + finally: + interp.close() + def test_attributes(self): self.assertIsInstance(sys.api_version, int) self.assertIsInstance(sys.argv, list) @@ -729,15 +795,25 @@ def test_thread_info(self): info = sys.thread_info self.assertEqual(len(info), 3) self.assertIn(info.name, ('nt', 'pthread', 'pthread-stubs', 'solaris', None)) - self.assertIn(info.lock, ('semaphore', 'mutex+cond', None)) - if sys.platform.startswith(("linux", "android", "freebsd")): + self.assertIn(info.lock, ('pymutex', None)) + if sys.platform.startswith(("linux", "android", "freebsd", "wasi")): self.assertEqual(info.name, "pthread") elif sys.platform == "win32": self.assertEqual(info.name, "nt") elif sys.platform == "emscripten": self.assertIn(info.name, {"pthread", "pthread-stubs"}) - elif sys.platform == "wasi": - self.assertEqual(info.name, "pthread-stubs") + + def test_abi_info(self): + info = sys.abi_info + info_keys = {'pointer_bits', 'free_threaded', 'debug', 'byteorder'} + self.assertEqual(set(vars(info)), info_keys) + pointer_bits = 64 if sys.maxsize > 2**32 else 32 + self.assertEqual(info.pointer_bits, pointer_bits) + self.assertEqual(info.free_threaded, + bool(sysconfig.get_config_var('Py_GIL_DISABLED'))) + self.assertEqual(info.debug, + bool(sysconfig.get_config_var('Py_DEBUG'))) + self.assertEqual(info.byteorder, sys.byteorder) @unittest.skipUnless(support.is_emscripten, "only available on Emscripten") def test_emscripten_info(self): @@ -849,23 +925,35 @@ def test_subinterp_intern_singleton(self): ''')) self.assertTrue(sys._is_interned(s)) - def test_sys_flags(self): + def test_sys_flags_indexable_attributes(self): self.assertTrue(sys.flags) - attrs = ("debug", + # We've stopped assigning sequence indices to new sys.flags attributes: + # https://github.com/python/cpython/issues/122575#issuecomment-2416497086 + indexable_attrs = ("debug", "inspect", "interactive", "optimize", "dont_write_bytecode", "no_user_site", "no_site", "ignore_environment", "verbose", "bytes_warning", "quiet", "hash_randomization", "isolated", "dev_mode", "utf8_mode", "warn_default_encoding", "safe_path", "int_max_str_digits") - for attr in attrs: + for attr_idx, attr in enumerate(indexable_attrs): self.assertHasAttr(sys.flags, attr) attr_type = bool if attr in ("dev_mode", "safe_path") else int self.assertEqual(type(getattr(sys.flags, attr)), attr_type, attr) + attr_value = getattr(sys.flags, attr) + self.assertEqual(sys.flags[attr_idx], attr_value, + msg=f"sys.flags .{attr} vs [{attr_idx}]") self.assertTrue(repr(sys.flags)) - self.assertEqual(len(sys.flags), len(attrs)) + self.assertEqual(len(sys.flags), 18, msg="Do not increase, see GH-122575") self.assertIn(sys.flags.utf8_mode, {0, 1, 2}) + def test_sys_flags_name_only_attributes(self): + # non-tuple sequence fields (name only sys.flags attributes) + self.assertIsInstance(sys.flags.gil, int|type(None)) + self.assertIsInstance(sys.flags.thread_inherit_context, int|type(None)) + self.assertIsInstance(sys.flags.context_aware_warnings, int|type(None)) + self.assertIsInstance(sys.flags.lazy_imports, int|type(None)) + def assert_raise_on_new_sys_type(self, sys_attr): # Users are intentionally prevented from creating new instances of # sys.flags, sys.version_info, and sys.getwindowsversion. @@ -1295,6 +1383,7 @@ def test_module_names(self): for name in sys.stdlib_module_names: self.assertIsInstance(name, str) + @unittest.skipUnless(hasattr(sys, '_stdlib_dir'), 'need sys._stdlib_dir') def test_stdlib_dir(self): os = import_helper.import_fresh_module('os') marker = getattr(os, '__file__', None) @@ -1339,8 +1428,29 @@ def test_pystats(self): def test_disable_gil_abi(self): self.assertEqual('t' in sys.abiflags, support.Py_GIL_DISABLED) + def test_int_max_str_digits(self): + old_limit = sys.get_int_max_str_digits() + self.assertIsInstance(old_limit, int) + self.assertGreaterEqual(old_limit, 0) + self.addCleanup(sys.set_int_max_str_digits, old_limit) + + sys.set_int_max_str_digits(0) + self.assertEqual(sys.get_int_max_str_digits(), 0) + + sys.set_int_max_str_digits(2_048) + self.assertEqual(sys.get_int_max_str_digits(), 2_048) + + with self.assertRaises(ValueError): + # the minimum is 640 digits + sys.set_int_max_str_digits(5) + with self.assertRaises(ValueError): + sys.set_int_max_str_digits(-2) + with self.assertRaises(TypeError): + sys.set_int_max_str_digits(2_048.0) + @test.support.cpython_only +@test.support.force_not_colorized_test_class class UnraisableHookTest(unittest.TestCase): def test_original_unraisablehook(self): _testcapi = import_helper.import_module('_testcapi') @@ -1572,7 +1682,7 @@ def test_objecttypes(self): samples = [b'', b'u'*100000] for sample in samples: x = bytearray(sample) - check(x, vsize('n2Pi') + x.__alloc__()) + check(x, vsize('n2PiP') + x.__alloc__()) # bytearray_iterator check(iter(bytearray()), size('nP')) # bytes @@ -1714,9 +1824,10 @@ def get_gen(): yield 1 check(int(PyLong_BASE**2), vsize('') + 3*self.longdigit) # module if support.Py_GIL_DISABLED: - check(unittest, size('PPPPPP')) + md_gil = '?' else: - check(unittest, size('PPPPP')) + md_gil = '' + check(unittest, size('PPPP?' + md_gil + 'NPPPPP')) # None check(None, size('')) # NotImplementedType @@ -1776,7 +1887,7 @@ def delx(self): del self.__x check((1,2,3), vsize('') + self.P + 3*self.P) # type # static type: PyTypeObject - fmt = 'P2nPI13Pl4Pn9Pn12PIPc' + fmt = 'P2nPI13Pl4Pn9Pn12PI2Pc' s = vsize(fmt) check(int, s) typeid = 'n' if support.Py_GIL_DISABLED else '' @@ -1870,7 +1981,10 @@ class S(set): check(S(), set(), '3P') class FS(frozenset): __slots__ = 'a', 'b', 'c' - check(FS(), frozenset(), '3P') + + class mytuple(tuple): + pass + check(FS([mytuple()]), frozenset([mytuple()]), '3P') from collections import OrderedDict class OD(OrderedDict): __slots__ = 'a', 'b', 'c' @@ -1883,7 +1997,7 @@ def test_pythontypes(self): check = self.check_sizeof # _ast.AST import _ast - check(_ast.AST(), size('P')) + check(_ast.Module(), size('3P')) try: raise TypeError except TypeError as e: @@ -1894,10 +2008,16 @@ def test_pythontypes(self): # symtable entry # XXX # sys.flags - # FIXME: The +3 is for the 'gil', 'thread_inherit_context' and - # 'context_aware_warnings' flags and will not be necessary once - # gh-122575 is fixed - check(sys.flags, vsize('') + self.P + self.P * (3 + len(sys.flags))) + # FIXME: The non_sequence_fields adjustment is for these flags: + # - 'gil' + # - 'thread_inherit_context' + # - 'context_aware_warnings' + # - 'lazy_imports' + # Not needing to increment this every time we add a new field + # per GH-122575 would be nice... + # Q: What is the actual point of this sys.flags C size derived from PyStructSequence_Field array assertion? + non_sequence_fields = 4 + check(sys.flags, vsize('') + self.P + self.P * (non_sequence_fields + len(sys.flags))) def test_asyncgen_hooks(self): old = sys.get_asyncgen_hooks() @@ -1962,7 +2082,8 @@ def tearDown(self): test.support.reap_children() def _run_remote_exec_test(self, script_code, python_args=None, env=None, - prologue='', + python_executable=None, prologue='', + after_ready=None, script_path=os_helper.TESTFN + '_remote.py'): # Create the script that will be remotely executed self.addCleanup(os_helper.unlink, script_path) @@ -2010,7 +2131,10 @@ def _run_remote_exec_test(self, script_code, python_args=None, env=None, ''') # Start the target process and capture its output - cmd = [sys.executable] + if python_executable is None: + python_executable = sys.executable + + cmd = [python_executable] if python_args: cmd.extend(python_args) cmd.append(target) @@ -2035,6 +2159,9 @@ def _run_remote_exec_test(self, script_code, python_args=None, env=None, response = client_socket.recv(1024) self.assertEqual(response, b"ready") + if after_ready is not None: + after_ready(proc) + # Try remote exec on the target process sys.remote_exec(proc.pid, script_path) @@ -2057,6 +2184,19 @@ def _run_remote_exec_test(self, script_code, python_args=None, env=None, proc.terminate() proc.wait(timeout=SHORT_TIMEOUT) + def _run_remote_exec_with_deleted_mapping(self, deleted_path, **kwargs): + def delete_loaded_mapping(proc): + os_helper.unlink(deleted_path) + with open(f'/proc/{proc.pid}/maps', encoding='utf-8') as maps: + self.assertIn(f'{deleted_path} (deleted)', maps.read()) + + script = 'print("Remote script executed successfully!")' + returncode, stdout, stderr = self._run_remote_exec_test( + script, after_ready=delete_loaded_mapping, **kwargs) + self.assertEqual(returncode, 0) + self.assertIn(b"Remote script executed successfully!", stdout) + self.assertEqual(stderr, b"") + def test_remote_exec(self): """Test basic remote exec functionality""" script = 'print("Remote script executed successfully!")' @@ -2183,6 +2323,74 @@ def test_remote_exec_invalid_script_path(self): with self.assertRaises(OSError): sys.remote_exec(os.getpid(), "invalid_script_path") + @unittest.skipUnless(sys.platform == 'linux', 'Linux-only regression test') + @unittest.skipUnless( + sysconfig.get_config_var('Py_ENABLE_SHARED') == 1, + 'requires a shared libpython build') + def test_remote_exec_deleted_libpython(self): + """Test remote exec when the target libpython was deleted.""" + build_dir = sysconfig.get_config_var('abs_builddir') + ldlibrary = sysconfig.get_config_var('LDLIBRARY') + instsoname = sysconfig.get_config_var('INSTSONAME') + if not build_dir or not ldlibrary or not instsoname: + self.skipTest('cannot determine shared libpython location') + + source_libpython = os.path.join(build_dir, instsoname) + if not os.path.exists(source_libpython): + self.skipTest(f'{source_libpython!r} does not exist') + + with os_helper.temp_dir() as lib_dir: + copied_libpython = os.path.join(lib_dir, instsoname) + shutil.copy2(source_libpython, copied_libpython) + if ldlibrary != instsoname: + os.symlink(instsoname, os.path.join(lib_dir, ldlibrary)) + + env = os.environ.copy() + ld_library_path = env.get('LD_LIBRARY_PATH') + env['LD_LIBRARY_PATH'] = lib_dir if not ld_library_path else ( + lib_dir + os.pathsep + ld_library_path) + + self._run_remote_exec_with_deleted_mapping(copied_libpython, + env=env) + + @unittest.skipUnless(sys.platform == 'linux', 'Linux-only regression test') + @unittest.skipUnless( + sysconfig.get_config_var('Py_ENABLE_SHARED') == 0, + 'requires a static Python build') + def test_remote_exec_deleted_static_executable(self): + """Test remote exec when the target static executable was deleted.""" + build_dir = sysconfig.get_config_var('abs_builddir') + stdlib_dir = os.path.dirname(os.path.abspath(os.__file__)) + if not build_dir or not os.path.isdir(stdlib_dir): + self.skipTest('cannot determine build-tree locations') + + pybuilddir_txt = os.path.join(build_dir, 'pybuilddir.txt') + if not os.path.exists(pybuilddir_txt): + self.skipTest(f'{pybuilddir_txt!r} does not exist') + + with open(pybuilddir_txt, encoding='utf-8') as pybuilddir_file: + pybuilddir = pybuilddir_file.read().strip() + source_ext_dir = os.path.join(build_dir, pybuilddir) + if not os.path.isdir(source_ext_dir): + self.skipTest(f'{source_ext_dir!r} does not exist') + + with os_helper.temp_dir() as copied_root: + copied_build_dir = os.path.join(copied_root, 'build') + copied_pybuilddir = os.path.join(copied_build_dir, pybuilddir) + os.makedirs(os.path.dirname(copied_pybuilddir)) + os.symlink(stdlib_dir, os.path.join(copied_root, 'Lib')) + os.symlink(source_ext_dir, copied_pybuilddir) + shutil.copy2(pybuilddir_txt, + os.path.join(copied_build_dir, 'pybuilddir.txt')) + + copied_python = os.path.join(copied_build_dir, + os.path.basename(sys.executable)) + shutil.copy2(sys.executable, copied_python) + + self._run_remote_exec_with_deleted_mapping( + copied_python, python_args=['-S'], + python_executable=copied_python) + def test_remote_exec_in_process_without_debug_fails_envvar(self): """Test remote exec in a process without remote debugging enabled""" script = os_helper.TESTFN + '_remote.py' @@ -2247,9 +2455,10 @@ def frame_2_jit(expected: bool) -> None: def frame_3_jit() -> None: # JITs just before the last loop: - for i in range(_testinternalcapi.TIER2_THRESHOLD + 1): + # 1 extra iteration for tracing. + for i in range(_testinternalcapi.TIER2_THRESHOLD + 2): # Careful, doing this in the reverse order breaks tracing: - expected = {enabled} and i == _testinternalcapi.TIER2_THRESHOLD + expected = {enabled} and i >= _testinternalcapi.TIER2_THRESHOLD assert sys._jit.is_active() is expected frame_2_jit(expected) assert sys._jit.is_active() is expected diff --git a/Lib/test/test_tcl.py b/Lib/test/test_tcl.py new file mode 100644 index 00000000000..f6e49840d06 --- /dev/null +++ b/Lib/test/test_tcl.py @@ -0,0 +1,838 @@ +import unittest +import subprocess +import sys +import os +from test import support +from test.support import import_helper +from test.support import os_helper + +# Skip this test if the _tkinter module wasn't built. +_tkinter = import_helper.import_module('_tkinter') + +import tkinter +from tkinter import Tcl +from _tkinter import TclError + +try: + from _testcapi import INT_MAX, PY_SSIZE_T_MAX +except ImportError: + INT_MAX = PY_SSIZE_T_MAX = sys.maxsize + +tcl_version = tuple(map(int, _tkinter.TCL_VERSION.split('.'))) + + +class TkinterTest(unittest.TestCase): + + def testFlattenLen(self): + # Object without length. + self.assertRaises(TypeError, _tkinter._flatten, True) + # Object with length, but not sequence. + self.assertRaises(TypeError, _tkinter._flatten, {}) + # Sequence or set, but not tuple or list. + # (issue44608: there were leaks in the following cases) + self.assertRaises(TypeError, _tkinter._flatten, 'string') + self.assertRaises(TypeError, _tkinter._flatten, {'set'}) + + +class TclTest(unittest.TestCase): + + def setUp(self): + self.interp = Tcl() + self.wantobjects = self.interp.tk.wantobjects() + + def passValue(self, value): + return self.interp.call('set', '_', value) + + def testEval(self): + tcl = self.interp + tcl.eval('set a 1') + self.assertEqual(tcl.eval('set a'),'1') + + def test_eval_null_in_result(self): + tcl = self.interp + self.assertEqual(tcl.eval('set a "a\\0b"'), 'a\x00b') + + def test_eval_surrogates_in_result(self): + tcl = self.interp + result = tcl.eval(r'set a "<\ud83d\udcbb>"') + if sys.platform == 'win32' and tcl_version >= (9, 0): + self.assertEqual('<\ud83d\udcbb>', result) + else: + self.assertEqual('<\U0001f4bb>', result) + + def testEvalException(self): + tcl = self.interp + self.assertRaises(TclError,tcl.eval,'set a') + + def testEvalException2(self): + tcl = self.interp + self.assertRaises(TclError,tcl.eval,'this is wrong') + + def test_eval_returns_tcl_obj(self): + tcl = self.interp.tk + tcl.eval(r'set a "\u20ac \ud83d\udcbb \0 \udcab"; regexp -about $a') + a = tcl.eval('set a') + expected = '\u20ac \U0001f4bb \0 \udced\udcb2\udcab' + self.assertEqual(a, expected) + + def testCall(self): + tcl = self.interp + tcl.call('set','a','1') + self.assertEqual(tcl.call('set','a'),'1') + + def test_call_passing_null(self): + tcl = self.interp + tcl.call('set', 'a', 'a\0b') # ASCII-only + self.assertEqual(tcl.getvar('a'), 'a\x00b') + self.assertEqual(tcl.call('set', 'a'), 'a\x00b') + self.assertEqual(tcl.eval('set a'), 'a\x00b') + + tcl.call('set', 'a', '\u20ac\0') # non-ASCII + self.assertEqual(tcl.getvar('a'), '\u20ac\x00') + self.assertEqual(tcl.call('set', 'a'), '\u20ac\x00') + self.assertEqual(tcl.eval('set a'), '\u20ac\x00') + + def testCallException(self): + tcl = self.interp + self.assertRaises(TclError,tcl.call,'set','a') + + def testCallException2(self): + tcl = self.interp + self.assertRaises(TclError,tcl.call,'this','is','wrong') + + def test_call_returns_tcl_obj(self): + tcl = self.interp.tk + tcl.eval(r'set a "\u20ac \ud83d\udcbb \0 \udcab"; regexp -about $a') + a = tcl.call('set', 'a') + expected = '\u20ac \U0001f4bb \0 \udced\udcb2\udcab' + if self.wantobjects: + self.assertEqual(str(a), expected) + self.assertEqual(a.string, expected) + self.assertEqual(a.typename, 'regexp') + else: + self.assertEqual(a, expected) + + def testSetVar(self): + tcl = self.interp + tcl.setvar('a','1') + self.assertEqual(tcl.eval('set a'),'1') + + def test_setvar_passing_null(self): + tcl = self.interp + tcl.setvar('a', 'a\0b') # ASCII-only + self.assertEqual(tcl.getvar('a'), 'a\x00b') + self.assertEqual(tcl.call('set', 'a'), 'a\x00b') + self.assertEqual(tcl.eval('set a'), 'a\x00b') + + tcl.setvar('a', '\u20ac\0') # non-ASCII + self.assertEqual(tcl.getvar('a'), '\u20ac\x00') + self.assertEqual(tcl.call('set', 'a'), '\u20ac\x00') + self.assertEqual(tcl.eval('set a'), '\u20ac\x00') + + def testSetVarArray(self): + tcl = self.interp + tcl.setvar('a(1)','1') + self.assertEqual(tcl.eval('set a(1)'),'1') + + def testGetVar(self): + tcl = self.interp + tcl.eval('set a 1') + self.assertEqual(tcl.getvar('a'),'1') + + def testGetVarArray(self): + tcl = self.interp + tcl.eval('set a(1) 1') + self.assertEqual(tcl.getvar('a(1)'),'1') + + def testGetVarException(self): + tcl = self.interp + self.assertRaises(TclError,tcl.getvar,'a') + + def testGetVarArrayException(self): + tcl = self.interp + self.assertRaises(TclError,tcl.getvar,'a(1)') + + def test_getvar_returns_tcl_obj(self): + tcl = self.interp.tk + tcl.eval(r'set a "\u20ac \ud83d\udcbb \0 \udcab"; regexp -about $a') + a = tcl.getvar('a') + expected = '\u20ac \U0001f4bb \0 \udced\udcb2\udcab' + if self.wantobjects: + self.assertEqual(str(a), expected) + self.assertEqual(a.string, expected) + self.assertEqual(a.typename, 'regexp') + else: + self.assertEqual(a, expected) + + def testUnsetVar(self): + tcl = self.interp + tcl.setvar('a',1) + self.assertEqual(tcl.eval('info exists a'),'1') + tcl.unsetvar('a') + self.assertEqual(tcl.eval('info exists a'),'0') + + def testUnsetVarArray(self): + tcl = self.interp + tcl.setvar('a(1)',1) + tcl.setvar('a(2)',2) + self.assertEqual(tcl.eval('info exists a(1)'),'1') + self.assertEqual(tcl.eval('info exists a(2)'),'1') + tcl.unsetvar('a(1)') + self.assertEqual(tcl.eval('info exists a(1)'),'0') + self.assertEqual(tcl.eval('info exists a(2)'),'1') + + def testUnsetVarException(self): + tcl = self.interp + self.assertRaises(TclError,tcl.unsetvar,'a') + + def get_integers(self): + return (0, 1, -1, + 2**31-1, -2**31, 2**31, -2**31-1, + 2**63-1, -2**63, 2**63, -2**63-1, + 2**1000, -2**1000) + + def test_getint(self): + tcl = self.interp.tk + for i in self.get_integers(): + self.assertEqual(tcl.getint(' %d ' % i), i) + self.assertEqual(tcl.getint(' %#o ' % i), i) + # Numbers starting with 0 are parsed as decimal in Tcl 9.0 + # and as octal in older versions. + self.assertEqual(tcl.getint((' %#o ' % i).replace('o', '')), + i if tcl_version < (9, 0) else int('%o' % i)) + self.assertEqual(tcl.getint(' %#x ' % i), i) + self.assertEqual(tcl.getint(42), 42) + self.assertRaises(TypeError, tcl.getint) + self.assertRaises(TypeError, tcl.getint, '42', '10') + self.assertRaises(TypeError, tcl.getint, b'42') + self.assertRaises(TypeError, tcl.getint, 42.0) + self.assertRaises(TclError, tcl.getint, 'a') + self.assertRaises((TypeError, ValueError, TclError), + tcl.getint, '42\0') + self.assertRaises((UnicodeEncodeError, ValueError, TclError), + tcl.getint, '42\ud800') + + def test_getdouble(self): + tcl = self.interp.tk + self.assertEqual(tcl.getdouble(' 42 '), 42.0) + self.assertEqual(tcl.getdouble(' 42.5 '), 42.5) + self.assertEqual(tcl.getdouble(42.5), 42.5) + self.assertEqual(tcl.getdouble(42), 42.0) + self.assertRaises(TypeError, tcl.getdouble) + self.assertRaises(TypeError, tcl.getdouble, '42.5', '10') + self.assertRaises(TypeError, tcl.getdouble, b'42.5') + self.assertRaises(TclError, tcl.getdouble, 'a') + self.assertRaises((TypeError, ValueError, TclError), + tcl.getdouble, '42.5\0') + self.assertRaises((UnicodeEncodeError, ValueError, TclError), + tcl.getdouble, '42.5\ud800') + + def test_getboolean(self): + tcl = self.interp.tk + self.assertIs(tcl.getboolean('on'), True) + self.assertIs(tcl.getboolean('1'), True) + self.assertIs(tcl.getboolean(42), True) + self.assertIs(tcl.getboolean(0), False) + self.assertRaises(TypeError, tcl.getboolean) + self.assertRaises(TypeError, tcl.getboolean, 'on', '1') + self.assertRaises(TypeError, tcl.getboolean, b'on') + self.assertRaises(TypeError, tcl.getboolean, 1.0) + self.assertRaises(TclError, tcl.getboolean, 'a') + self.assertRaises((TypeError, ValueError, TclError), + tcl.getboolean, 'on\0') + self.assertRaises((UnicodeEncodeError, ValueError, TclError), + tcl.getboolean, 'on\ud800') + + def testEvalFile(self): + tcl = self.interp + filename = os_helper.TESTFN_ASCII + self.addCleanup(os_helper.unlink, filename) + with open(filename, 'w') as f: + f.write("""set a 1 + set b 2 + set c [ expr $a + $b ] + """) + tcl.evalfile(filename) + self.assertEqual(tcl.eval('set a'),'1') + self.assertEqual(tcl.eval('set b'),'2') + self.assertEqual(tcl.eval('set c'),'3') + + def test_evalfile_null_in_result(self): + tcl = self.interp + filename = os_helper.TESTFN_ASCII + self.addCleanup(os_helper.unlink, filename) + with open(filename, 'w') as f: + f.write(""" + set a "a\0b" + set b "a\\0b" + """) + tcl.evalfile(filename) + self.assertEqual(tcl.eval('set a'), 'a\x00b') + self.assertEqual(tcl.eval('set b'), 'a\x00b') + + def test_evalfile_surrogates_in_result(self): + tcl = self.interp + encoding = tcl.call('encoding', 'system') + self.addCleanup(tcl.call, 'encoding', 'system', encoding) + tcl.call('encoding', 'system', 'utf-8') + + filename = os_helper.TESTFN_ASCII + self.addCleanup(os_helper.unlink, filename) + with open(filename, 'wb') as f: + f.write(b""" + set a "<\xed\xa0\xbd\xed\xb2\xbb>" + """) + if tcl_version >= (9, 0): + self.assertRaises(TclError, tcl.evalfile, filename) + else: + tcl.evalfile(filename) + self.assertEqual(tcl.eval('set a'), '<\U0001f4bb>') + + with open(filename, 'wb') as f: + f.write(b""" + set b "<\\ud83d\\udcbb>" + """) + tcl.evalfile(filename) + result = tcl.eval('set b') + if sys.platform == 'win32' and tcl_version >= (9, 0): + self.assertEqual('<\ud83d\udcbb>', result) + else: + self.assertEqual('<\U0001f4bb>', result) + + def testEvalFileException(self): + tcl = self.interp + filename = "doesnotexists" + try: + os.remove(filename) + except Exception as e: + pass + self.assertRaises(TclError,tcl.evalfile,filename) + + def testPackageRequireException(self): + tcl = self.interp + self.assertRaises(TclError,tcl.eval,'package require DNE') + + @unittest.skipUnless(sys.platform == 'win32', 'Requires Windows') + def testLoadWithUNC(self): + # Build a UNC path from the regular path. + # Something like + # \\%COMPUTERNAME%\c$\python27\python.exe + + fullname = os.path.abspath(sys.executable) + if fullname[1] != ':': + raise unittest.SkipTest('Absolute path should have drive part') + unc_name = r'\\%s\%s$\%s' % (os.environ['COMPUTERNAME'], + fullname[0], + fullname[3:]) + if not os.path.exists(unc_name): + raise unittest.SkipTest('Cannot connect to UNC Path') + + with os_helper.EnvironmentVarGuard() as env: + env.unset("TCL_LIBRARY") + stdout = subprocess.check_output( + [unc_name, '-c', 'import tkinter; print(tkinter)']) + + self.assertIn(b'tkinter', stdout) + + def test_exprstring(self): + tcl = self.interp + tcl.call('set', 'a', 3) + tcl.call('set', 'b', 6) + def check(expr, expected): + result = tcl.exprstring(expr) + self.assertEqual(result, expected) + self.assertIsInstance(result, str) + + self.assertRaises(TypeError, tcl.exprstring) + self.assertRaises(TypeError, tcl.exprstring, '8.2', '+6') + self.assertRaises(TypeError, tcl.exprstring, b'8.2 + 6') + self.assertRaises(TclError, tcl.exprstring, 'spam') + check('', '0') + check('8.2 + 6', '14.2') + check('3.1 + $a', '6.1') + check('2 + "$a.$b"', '5.6') + check('4*[llength "6 2"]', '8') + check('{word one} < "word $a"', '0') + check('4*2 < 7', '0') + check('hypot($a, 4)', '5.0') + check('5 / 4', '1') + check('5 / 4.0', '1.25') + check('5 / ( [string length "abcd"] + 0.0 )', '1.25') + check('20.0/5.0', '4.0') + check('"0x03" > "2"', '1') + check('[string length "a\xbd\u20ac"]', '3') + check(r'[string length "a\xbd\u20ac"]', '3') + check('"abc"', 'abc') + check('"a\xbd\u20ac"', 'a\xbd\u20ac') + check(r'"a\xbd\u20ac"', 'a\xbd\u20ac') + check(r'"a\0b"', 'a\x00b') + check('2**64', str(2**64)) + + def test_exprdouble(self): + tcl = self.interp + tcl.call('set', 'a', 3) + tcl.call('set', 'b', 6) + def check(expr, expected): + result = tcl.exprdouble(expr) + self.assertEqual(result, expected) + self.assertIsInstance(result, float) + + self.assertRaises(TypeError, tcl.exprdouble) + self.assertRaises(TypeError, tcl.exprdouble, '8.2', '+6') + self.assertRaises(TypeError, tcl.exprdouble, b'8.2 + 6') + self.assertRaises(TclError, tcl.exprdouble, 'spam') + check('', 0.0) + check('8.2 + 6', 14.2) + check('3.1 + $a', 6.1) + check('2 + "$a.$b"', 5.6) + check('4*[llength "6 2"]', 8.0) + check('{word one} < "word $a"', 0.0) + check('4*2 < 7', 0.0) + check('hypot($a, 4)', 5.0) + check('5 / 4', 1.0) + check('5 / 4.0', 1.25) + check('5 / ( [string length "abcd"] + 0.0 )', 1.25) + check('20.0/5.0', 4.0) + check('"0x03" > "2"', 1.0) + check('[string length "a\xbd\u20ac"]', 3.0) + check(r'[string length "a\xbd\u20ac"]', 3.0) + self.assertRaises(TclError, tcl.exprdouble, '"abc"') + check('2**64', float(2**64)) + + def test_exprlong(self): + tcl = self.interp + tcl.call('set', 'a', 3) + tcl.call('set', 'b', 6) + def check(expr, expected): + result = tcl.exprlong(expr) + self.assertEqual(result, expected) + self.assertIsInstance(result, int) + + self.assertRaises(TypeError, tcl.exprlong) + self.assertRaises(TypeError, tcl.exprlong, '8.2', '+6') + self.assertRaises(TypeError, tcl.exprlong, b'8.2 + 6') + self.assertRaises(TclError, tcl.exprlong, 'spam') + check('', 0) + check('8.2 + 6', 14) + check('3.1 + $a', 6) + check('2 + "$a.$b"', 5) + check('4*[llength "6 2"]', 8) + check('{word one} < "word $a"', 0) + check('4*2 < 7', 0) + check('hypot($a, 4)', 5) + check('5 / 4', 1) + check('5 / 4.0', 1) + check('5 / ( [string length "abcd"] + 0.0 )', 1) + check('20.0/5.0', 4) + check('"0x03" > "2"', 1) + check('[string length "a\xbd\u20ac"]', 3) + check(r'[string length "a\xbd\u20ac"]', 3) + self.assertRaises(TclError, tcl.exprlong, '"abc"') + self.assertRaises(TclError, tcl.exprlong, '2**64') + + def test_exprboolean(self): + tcl = self.interp + tcl.call('set', 'a', 3) + tcl.call('set', 'b', 6) + def check(expr, expected): + result = tcl.exprboolean(expr) + self.assertEqual(result, expected) + self.assertIsInstance(result, int) + self.assertNotIsInstance(result, bool) + + self.assertRaises(TypeError, tcl.exprboolean) + self.assertRaises(TypeError, tcl.exprboolean, '8.2', '+6') + self.assertRaises(TypeError, tcl.exprboolean, b'8.2 + 6') + self.assertRaises(TclError, tcl.exprboolean, 'spam') + check('', False) + for value in ('0', 'false', 'no', 'off'): + check(value, False) + check('"%s"' % value, False) + check('{%s}' % value, False) + for value in ('1', 'true', 'yes', 'on'): + check(value, True) + check('"%s"' % value, True) + check('{%s}' % value, True) + check('8.2 + 6', True) + check('3.1 + $a', True) + check('2 + "$a.$b"', True) + check('4*[llength "6 2"]', True) + check('{word one} < "word $a"', False) + check('4*2 < 7', False) + check('hypot($a, 4)', True) + check('5 / 4', True) + check('5 / 4.0', True) + check('5 / ( [string length "abcd"] + 0.0 )', True) + check('20.0/5.0', True) + check('"0x03" > "2"', True) + check('[string length "a\xbd\u20ac"]', True) + check(r'[string length "a\xbd\u20ac"]', True) + self.assertRaises(TclError, tcl.exprboolean, '"abc"') + check('2**64', True) + + def test_booleans(self): + tcl = self.interp + def check(expr, expected): + result = tcl.call('expr', expr) + if tcl.wantobjects(): + self.assertEqual(result, expected) + self.assertIsInstance(result, int) + else: + self.assertIn(result, (expr, str(int(expected)))) + self.assertIsInstance(result, str) + check('true', True) + check('yes', True) + check('on', True) + check('false', False) + check('no', False) + check('off', False) + check('1 < 2', True) + check('1 > 2', False) + + def test_expr_bignum(self): + tcl = self.interp + for i in self.get_integers(): + result = tcl.call('expr', str(i)) + if self.wantobjects: + self.assertEqual(result, i) + self.assertIsInstance(result, int) + else: + self.assertEqual(result, str(i)) + self.assertIsInstance(result, str) + + def test_passing_values(self): + passValue = self.passValue + + self.assertEqual(passValue(True), True if self.wantobjects else '1') + self.assertEqual(passValue(False), False if self.wantobjects else '0') + self.assertEqual(passValue('string'), 'string') + self.assertEqual(passValue('string\u20ac'), 'string\u20ac') + self.assertEqual(passValue('string\U0001f4bb'), 'string\U0001f4bb') + self.assertEqual(passValue('str\x00ing'), 'str\x00ing') + self.assertEqual(passValue('str\x00ing\xbd'), 'str\x00ing\xbd') + self.assertEqual(passValue('str\x00ing\u20ac'), 'str\x00ing\u20ac') + self.assertEqual(passValue('str\x00ing\U0001f4bb'), + 'str\x00ing\U0001f4bb') + if sys.platform != 'win32': + self.assertEqual(passValue('<\udce2\udc82\udcac>'), + '<\u20ac>') + self.assertEqual(passValue('<\udced\udca0\udcbd\udced\udcb2\udcbb>'), + '<\U0001f4bb>') + self.assertEqual(passValue(b'str\x00ing'), + b'str\x00ing' if self.wantobjects else 'str\x00ing') + self.assertEqual(passValue(b'str\xc0\x80ing'), + b'str\xc0\x80ing' if self.wantobjects else 'str\xc0\x80ing') + self.assertEqual(passValue(b'str\xbding'), + b'str\xbding' if self.wantobjects else 'str\xbding') + for i in self.get_integers(): + self.assertEqual(passValue(i), i if self.wantobjects else str(i)) + for f in (0.0, 1.0, -1.0, 1/3, + sys.float_info.min, sys.float_info.max, + -sys.float_info.min, -sys.float_info.max): + if self.wantobjects: + self.assertEqual(passValue(f), f) + else: + self.assertEqual(float(passValue(f)), f) + if self.wantobjects: + f = passValue(float('nan')) + self.assertNotEqual(f, f) + self.assertEqual(passValue(float('inf')), float('inf')) + self.assertEqual(passValue(-float('inf')), -float('inf')) + else: + self.assertEqual(float(passValue(float('inf'))), float('inf')) + self.assertEqual(float(passValue(-float('inf'))), -float('inf')) + # XXX NaN representation can be not parsable by float() + self.assertEqual(passValue((1, '2', (3.4,))), + (1, '2', (3.4,)) if self.wantobjects else '1 2 3.4') + self.assertEqual(passValue(['a', ['b', 'c']]), + ('a', ('b', 'c')) if self.wantobjects else 'a {b c}') + + def test_set_object_concurrent_mutation_in_sequence_conversion(self): + # Prevent SIGSEV when the object to convert is concurrently mutated. + # See: https://github.com/python/cpython/issues/143310. + + string = "value" + + class Value: + def __str__(self): + values.clear() + return string + + class List(list): + pass + + expect = (string, "pad") if self.wantobjects else f"{string} pad" + self.assertEqual(self.passValue(values := [Value(), "pad"]), expect) + self.assertEqual(self.passValue(values := List([Value(), "pad"])), expect) + + def test_user_command(self): + result = None + def testfunc(arg): + nonlocal result + result = arg + return arg + self.interp.createcommand('testfunc', testfunc) + self.addCleanup(self.interp.tk.deletecommand, 'testfunc') + def check(value, expected1=None, expected2=None, *, eq=self.assertEqual): + expected = value + if self.wantobjects >= 2: + if expected2 is not None: + expected = expected2 + expected_type = type(expected) + else: + if expected1 is not None: + expected = expected1 + expected_type = str + nonlocal result + result = None + r = self.interp.call('testfunc', value) + self.assertIsInstance(result, expected_type) + eq(result, expected) + self.assertIsInstance(r, expected_type) + eq(r, expected) + def float_eq(actual, expected): + self.assertAlmostEqual(float(actual), expected, + delta=abs(expected) * 1e-10) + + check(True, '1', 1) + check(False, '0', 0) + check('string') + check('string\xbd') + check('string\u20ac') + check('string\U0001f4bb') + if sys.platform != 'win32': + check('<\udce2\udc82\udcac>', '<\u20ac>', '<\u20ac>') + check('<\udced\udca0\udcbd\udced\udcb2\udcbb>', '<\U0001f4bb>', '<\U0001f4bb>') + check('') + check(b'string', 'string') + check(b'string\xe2\x82\xac', 'string\xe2\x82\xac') + check(b'string\xbd', 'string\xbd') + check(b'', '') + check('str\x00ing') + check('str\x00ing\xbd') + check('str\x00ing\u20ac') + check(b'str\x00ing', 'str\x00ing') + check(b'str\xc0\x80ing', 'str\xc0\x80ing') + check(b'str\xc0\x80ing\xe2\x82\xac', 'str\xc0\x80ing\xe2\x82\xac') + for i in self.get_integers(): + check(i, str(i)) + for f in (0.0, 1.0, -1.0): + check(f, repr(f)) + for f in (1/3.0, sys.float_info.min, sys.float_info.max, + -sys.float_info.min, -sys.float_info.max): + check(f, eq=float_eq) + check(float('inf'), eq=float_eq) + check(-float('inf'), eq=float_eq) + # XXX NaN representation can be not parsable by float() + check((), '', '') + check((1, (2,), (3, 4), '5 6', ()), + '1 2 {3 4} {5 6} {}', + (1, (2,), (3, 4), '5 6', '')) + check([1, [2,], [3, 4], '5 6', []], + '1 2 {3 4} {5 6} {}', + (1, (2,), (3, 4), '5 6', '')) + + def test_passing_tcl_obj(self): + tcl = self.interp.tk + a = None + def testfunc(arg): + nonlocal a + a = arg + self.interp.createcommand('testfunc', testfunc) + self.addCleanup(self.interp.tk.deletecommand, 'testfunc') + tcl.eval(r'set a "\u20ac \ud83d\udcbb \0 \udcab"; regexp -about $a') + tcl.eval(r'testfunc $a') + expected = '\u20ac \U0001f4bb \0 \udced\udcb2\udcab' + if self.wantobjects >= 2: + self.assertEqual(str(a), expected) + self.assertEqual(a.string, expected) + self.assertEqual(a.typename, 'regexp') + else: + self.assertEqual(a, expected) + + def test_splitlist(self): + splitlist = self.interp.tk.splitlist + call = self.interp.tk.call + self.assertRaises(TypeError, splitlist) + self.assertRaises(TypeError, splitlist, 'a', 'b') + self.assertRaises(TypeError, splitlist, 2) + testcases = [ + ('2', ('2',)), + ('', ()), + ('{}', ('',)), + ('""', ('',)), + ('a\n b\t\r c\n ', ('a', 'b', 'c')), + (b'a\n b\t\r c\n ', ('a', 'b', 'c')), + ('a \u20ac', ('a', '\u20ac')), + ('a \U0001f4bb', ('a', '\U0001f4bb')), + (b'a \xe2\x82\xac', ('a', '\u20ac')), + (b'a \xf0\x9f\x92\xbb', ('a', '\U0001f4bb')), + (b'a \xed\xa0\xbd\xed\xb2\xbb', ('a', '\U0001f4bb')), + (b'a\xc0\x80b c\xc0\x80d', ('a\x00b', 'c\x00d')), + ('a {b c}', ('a', 'b c')), + (r'a b\ c', ('a', 'b c')), + (('a', 'b c'), ('a', 'b c')), + ('a 2', ('a', '2')), + (('a', 2), ('a', 2)), + ('a 3.4', ('a', '3.4')), + (('a', 3.4), ('a', 3.4)), + ((), ()), + ([], ()), + (['a', ['b', 'c']], ('a', ['b', 'c'])), + (call('list', 1, '2', (3.4,)), + (1, '2', (3.4,)) if self.wantobjects else + ('1', '2', '3.4')), + ] + if not self.wantobjects: + expected = ('12', '\u20ac', '\xe2\x82\xac', '3.4') + else: + expected = (12, '\u20ac', b'\xe2\x82\xac', (3.4,)) + testcases += [ + (call('dict', 'create', 12, '\u20ac', b'\xe2\x82\xac', (3.4,)), + expected), + ] + dbg_info = ('want objects? %s, Tcl version: %s, Tcl patchlevel: %s' + % (self.wantobjects, tcl_version, self.interp.info_patchlevel())) + for arg, res in testcases: + self.assertEqual(splitlist(arg), res, + 'arg=%a, %s' % (arg, dbg_info)) + self.assertRaises(TclError, splitlist, '{') + + def test_splitdict(self): + splitdict = tkinter._splitdict + tcl = self.interp.tk + + arg = '-a {1 2 3} -something foo status {}' + self.assertEqual(splitdict(tcl, arg, False), + {'-a': '1 2 3', '-something': 'foo', 'status': ''}) + self.assertEqual(splitdict(tcl, arg), + {'a': '1 2 3', 'something': 'foo', 'status': ''}) + + arg = ('-a', (1, 2, 3), '-something', 'foo', 'status', '{}') + self.assertEqual(splitdict(tcl, arg, False), + {'-a': (1, 2, 3), '-something': 'foo', 'status': '{}'}) + self.assertEqual(splitdict(tcl, arg), + {'a': (1, 2, 3), 'something': 'foo', 'status': '{}'}) + + self.assertRaises(RuntimeError, splitdict, tcl, '-a b -c ') + self.assertRaises(RuntimeError, splitdict, tcl, ('-a', 'b', '-c')) + + arg = tcl.call('list', + '-a', (1, 2, 3), '-something', 'foo', 'status', ()) + self.assertEqual(splitdict(tcl, arg), + {'a': (1, 2, 3) if self.wantobjects else '1 2 3', + 'something': 'foo', 'status': ''}) + + arg = tcl.call('dict', 'create', + '-a', (1, 2, 3), '-something', 'foo', 'status', ()) + if not self.wantobjects: + expected = {'a': '1 2 3', 'something': 'foo', 'status': ''} + else: + expected = {'a': (1, 2, 3), 'something': 'foo', 'status': ''} + self.assertEqual(splitdict(tcl, arg), expected) + + def test_join(self): + join = tkinter._join + tcl = self.interp.tk + def unpack(s): + return tcl.call('lindex', s, 0) + def check(value): + self.assertEqual(unpack(join([value])), value) + self.assertEqual(unpack(join([value, 0])), value) + self.assertEqual(unpack(unpack(join([[value]]))), value) + self.assertEqual(unpack(unpack(join([[value, 0]]))), value) + self.assertEqual(unpack(unpack(join([[value], 0]))), value) + self.assertEqual(unpack(unpack(join([[value, 0], 0]))), value) + check('') + check('spam') + check('sp am') + check('sp\tam') + check('sp\nam') + check(' \t\n') + check('{spam}') + check('{sp am}') + check('"spam"') + check('"sp am"') + check('{"spam"}') + check('"{spam}"') + check('sp\\am') + check('"sp\\am"') + check('"{}" "{}"') + check('"\\') + check('"{') + check('"}') + check('\n\\') + check('\n{') + check('\n}') + check('\\\n') + check('{\n') + check('}\n') + + @support.cpython_only + def test_new_tcl_obj(self): + support.check_disallow_instantiation(self, _tkinter.Tcl_Obj) + support.check_disallow_instantiation(self, _tkinter.TkttType) + support.check_disallow_instantiation(self, _tkinter.TkappType) + + +class BigmemTclTest(unittest.TestCase): + + def setUp(self): + self.interp = Tcl() + + @support.cpython_only + @unittest.skipUnless(INT_MAX < PY_SSIZE_T_MAX, "needs UINT_MAX < SIZE_MAX") + @support.bigmemtest(size=INT_MAX + 1, memuse=5, dry_run=False) + def test_huge_string_call(self, size): + value = ' ' * size + self.assertRaises(OverflowError, self.interp.call, 'string', 'index', value, 0) + + @support.cpython_only + @unittest.skipUnless(INT_MAX < PY_SSIZE_T_MAX, "needs UINT_MAX < SIZE_MAX") + @support.bigmemtest(size=INT_MAX + 1, memuse=3, dry_run=False) + def test_huge_string_builtins(self, size): + tk = self.interp.tk + value = '1' + ' ' * size + self.assertRaises(OverflowError, tk.getint, value) + self.assertRaises(OverflowError, tk.getdouble, value) + self.assertRaises(OverflowError, tk.getboolean, value) + self.assertRaises(OverflowError, tk.eval, value) + self.assertRaises(OverflowError, tk.evalfile, value) + self.assertRaises(OverflowError, tk.record, value) + self.assertRaises(OverflowError, tk.adderrorinfo, value) + self.assertRaises(OverflowError, tk.setvar, value, 'x', 'a') + self.assertRaises(OverflowError, tk.setvar, 'x', value, 'a') + self.assertRaises(OverflowError, tk.unsetvar, value) + self.assertRaises(OverflowError, tk.unsetvar, 'x', value) + self.assertRaises(OverflowError, tk.adderrorinfo, value) + self.assertRaises(OverflowError, tk.exprstring, value) + self.assertRaises(OverflowError, tk.exprlong, value) + self.assertRaises(OverflowError, tk.exprboolean, value) + self.assertRaises(OverflowError, tk.splitlist, value) + self.assertRaises(OverflowError, tk.createcommand, value, max) + self.assertRaises(OverflowError, tk.deletecommand, value) + + @support.cpython_only + @unittest.skipUnless(INT_MAX < PY_SSIZE_T_MAX, "needs UINT_MAX < SIZE_MAX") + @support.bigmemtest(size=INT_MAX + 1, memuse=6, dry_run=False) + def test_huge_string_builtins2(self, size): + # These commands require larger memory for possible error messages + tk = self.interp.tk + value = '1' + ' ' * size + self.assertRaises(OverflowError, tk.evalfile, value) + self.assertRaises(OverflowError, tk.unsetvar, value) + self.assertRaises(OverflowError, tk.unsetvar, 'x', value) + + +def setUpModule(): + wantobjects = support.get_resource_value('wantobjects') + if wantobjects is not None: + unittest.enterModuleContext( + support.swap_attr(tkinter, 'wantobjects', int(wantobjects))) + if support.verbose: + tcl = Tcl() + print('patchlevel =', tcl.call('info', 'patchlevel'), flush=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index cf8b1037194..4bb7d95a903 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -14,6 +14,7 @@ import gc import shutil import subprocess +import sysconfig from unittest import mock import unittest @@ -1848,6 +1849,54 @@ def test(target, target_is_directory): new_flags = os.stat(dir1).st_flags self.assertEqual(new_flags, old_flags) + @os_helper.skip_unless_symlink + @os_helper.skip_unless_working_chmod + @support.requires_non_root_user + @unittest.skipIf(support.is_emscripten, 'Fails due to Emscripten bug:' + 'emscripten-core/emscripten#27761') + @unittest.skipUnless(shutil.rmtree.avoids_symlink_attacks, + 'requires the fd based implementation of rmtree()') + def test_cleanup_with_symlink_race(self): + # cleanup() should not operate on files outside of the temporary + # directory when a directory is replaced with a symlink while it + # recovers from a PermissionError (CVE-2026-12345). + with self.do_create(recurse=0) as target: + target_file = os.path.join(target, 'file1') + open(target_file, 'wb').close() + target_mode = os.stat(target_file).st_mode + + d1 = self.do_create(recurse=0) + dir1 = os.path.join(d1.name, 'dir1') + os.mkdir(dir1) + open(os.path.join(dir1, 'file1'), 'wb').close() + # Removing contents of dir1 fails with a PermissionError, and + # dir1 is replaced with a symlink to target at the very moment + # cleanup() starts to recover from that error. + os.chmod(dir1, 0o500) + unlink = os.unlink + def hook(path, *, dir_fd=None): + try: + return unlink(path, dir_fd=dir_fd) + except PermissionError: + if not os.path.islink(dir1): + os.chmod(dir1, 0o700) + os.rename(dir1, dir1 + '_moved') + os.symlink(target, dir1) + raise + try: + with mock.patch('os.unlink', hook): + with contextlib.suppress(OSError): + d1.cleanup() + finally: + if os.path.islink(dir1): + os.unlink(dir1) + os.rename(dir1 + '_moved', dir1) + os.chmod(dir1, 0o700) + d1.cleanup() + + self.assertTrue(os.path.exists(target_file)) + self.assertEqual(os.stat(target_file).st_mode, target_mode) + @support.cpython_only def test_del_on_collection(self): # A TemporaryDirectory is deleted when garbage collected @@ -2020,6 +2069,29 @@ def test_modes(self): d.cleanup() self.assertFalse(os.path.exists(d.name)) + @support.subTests('ignore_errors', (True, False)) + def test_parent_mode_preserved(self, ignore_errors): + # Test that cleanup does not touch the parent directory, + # even if that prevents removal. + for mode in range(8): + mode <<= 6 + with self.subTest(mode=format(mode, '03o')): + outer = self.do_create() + with outer: + d = self.do_create(dir=outer.name, dirs=2, files=2, + ignore_cleanup_errors=ignore_errors) + with d: + os.chmod(outer.name, mode) + orig_mode = os.stat(outer.name).st_mode + try: + d.cleanup() + except PermissionError: + if ignore_errors: + raise + self.assertEqual(os.stat(outer.name).st_mode, orig_mode) + outer.cleanup() + self.assertFalse(os.path.exists(outer.name)) + def check_flags(self, flags): # skip the test if these flags are not supported (ex: FreeBSD 13) filename = os_helper.TESTFN @@ -2057,5 +2129,17 @@ def test_delete_false(self): self.assertTrue(os.path.exists(working_dir)) shutil.rmtree(working_dir) + @unittest.skipUnless( + sysconfig.get_config_var('PY_SUPPORT_TIER') + and sysconfig.get_config_var('PY_SUPPORT_TIER') <= 3, + 'regression test for supported platforms') + @unittest.skipIf(support.MS_WINDOWS, 'dirfd not used on Windows') + @unittest.skipIf(support.is_wasi, 'WASI has no chmod') + def test_cleanup_safe(self): + """Verify that cleanup uses the safer code path""" + # This is a regression test. Feel free to add exceptions for new + # platforms, but don't forget to update the docs. + self.assertTrue(tempfile._rmtree_use_dir_fd) + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py index 2b0745e947c..a2a09f9de61 100644 --- a/Lib/test/test_timeit.py +++ b/Lib/test/test_timeit.py @@ -4,8 +4,14 @@ import io from textwrap import dedent -from test.support import captured_stdout -from test.support import captured_stderr +from test.support import ( + captured_stderr, + captured_stdout, + force_colorized, + force_not_colorized_test_class, +) + +from _colorize import get_theme # timeit's default number of iterations. DEFAULT_NUMBER = 1000000 @@ -41,6 +47,7 @@ def wrap_timer(self, timer): self.saved_timer = timer return self +@force_not_colorized_test_class class TestTimeit(unittest.TestCase): def tearDown(self): @@ -252,11 +259,12 @@ def run_main(self, seconds_per_increment=1.0, switches=None, timer=None): return s.getvalue() def test_main_bad_switch(self): - s = self.run_main(switches=['--bad-switch']) - self.assertEqual(s, dedent("""\ - option --bad-switch not recognized - use -h/--help for command line help - """)) + with captured_stderr() as error_stringio: + s = self.run_main(switches=["--bad-switch"]) + self.assertEqual(s, "") + self.assertIn( + "unrecognized arguments: --bad-switch", error_stringio.getvalue() + ) def test_main_seconds(self): s = self.run_main(seconds_per_increment=5.5) @@ -294,10 +302,11 @@ def test_main_negative_reps(self): s = self.run_main(seconds_per_increment=60.0, switches=['-r-5']) self.assertEqual(s, "1 loop, best of 1: 60 sec per loop\n") - @unittest.skipIf(sys.flags.optimize >= 2, "need __doc__") def test_main_help(self): s = self.run_main(switches=['-h']) - self.assertEqual(s, timeit.__doc__) + self.assertIn("Tool for measuring execution time", s) + self.assertIn("-n", s) + self.assertIn("--number", s) def test_main_verbose(self): s = self.run_main(switches=['-v']) @@ -346,10 +355,12 @@ def test_main_with_time_unit(self): "100 loops, best of 5: 3e+03 usec per loop\n") # Test invalid unit input with captured_stderr() as error_stringio: - invalid = self.run_main(seconds_per_increment=0.003, - switches=['-u', 'parsec']) - self.assertEqual(error_stringio.getvalue(), - "Unrecognized unit. Please select nsec, usec, msec, or sec.\n") + invalid = self.run_main( + seconds_per_increment=0.003, switches=["-u", "parsec"] + ) + self.assertIn( + "choose from 'nsec', 'usec', 'msec', 'sec'", error_stringio.getvalue() + ) def test_main_exception(self): with captured_stderr() as error_stringio: @@ -361,10 +372,10 @@ def test_main_exception_fixed_reps(self): s = self.run_main(switches=['-n1', '1/0']) self.assert_exc_string(error_stringio.getvalue(), 'ZeroDivisionError') - def autorange(self, seconds_per_increment=1/1024, callback=None): + def autorange(self, seconds_per_increment=1/1024, callback=None, target_time=0.2): timer = FakeTimer(seconds_per_increment=seconds_per_increment) t = timeit.Timer(stmt=self.fake_stmt, setup=self.fake_setup, timer=timer) - return t.autorange(callback) + return t.autorange(callback, target_time=target_time) def test_autorange(self): num_loops, time_taken = self.autorange() @@ -376,6 +387,11 @@ def test_autorange_second(self): self.assertEqual(num_loops, 1) self.assertEqual(time_taken, 1.0) + def test_autorange_with_target_time(self): + num_loops, time_taken = self.autorange(target_time=1.0) + self.assertEqual(num_loops, 2000) + self.assertEqual(time_taken, 2000/1024) + def test_autorange_with_callback(self): def callback(a, b): print("{} {:.3f}".format(a, b)) @@ -395,5 +411,39 @@ def callback(a, b): self.assertEqual(s.getvalue(), expected) -if __name__ == '__main__': +class TestTimeitColor(unittest.TestCase): + + fake_stmt = TestTimeit.fake_stmt + run_main = TestTimeit.run_main + + @force_colorized + def test_main_colorized(self): + t = get_theme(force_color=True).timeit + s = self.run_main(seconds_per_increment=5.5) + self.assertEqual( + s, + "1 loop, best of 5: " + f"{t.best}5.5 sec{t.reset} " + f"{t.per_loop}per loop{t.reset}\n", + ) + + @force_colorized + def test_main_verbose_colorized(self): + t = get_theme(force_color=True).timeit + s = self.run_main(switches=["-v"]) + self.assertEqual( + s, + f"1 loop {t.punctuation}-> {t.timing}1 sec{t.reset}\n\n" + "raw times: " + f"{t.timing}1 sec{t.punctuation}, " + f"{t.timing}1 sec{t.punctuation}, " + f"{t.timing}1 sec{t.punctuation}, " + f"{t.timing}1 sec{t.punctuation}, " + f"{t.timing}1 sec{t.reset}\n\n" + f"1 loop, best of 5: {t.best}1 sec{t.reset} " + f"{t.per_loop}per loop{t.reset}\n", + ) + + +if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_tkinter/README b/Lib/test/test_tkinter/README new file mode 100644 index 00000000000..79cd16c74d7 --- /dev/null +++ b/Lib/test/test_tkinter/README @@ -0,0 +1,14 @@ +Writing new tests +================= + +Precaution +---------- + + New tests should always use only one Tk window at once, like all the + current tests do. This means that you have to destroy the current window + before creating another one, and clean up after the test. The motivation + behind this is that some tests may depend on having its window focused + while it is running to work properly, and it may be hard to force focus + on your window across platforms (right now only test_traversal at + test_ttk.test_widgets.NotebookTest depends on this). + diff --git a/Lib/test/test_tkinter/__init__.py b/Lib/test/test_tkinter/__init__.py new file mode 100644 index 00000000000..62890c705a6 --- /dev/null +++ b/Lib/test/test_tkinter/__init__.py @@ -0,0 +1,30 @@ +import os.path +import unittest + +from test.support import ( + check_sanitizer, + import_helper, + load_package_tests, + requires, + ) + + +if check_sanitizer(address=True, memory=True): + # See gh-90791 for details + raise unittest.SkipTest("Tests involving libX11 can SEGFAULT on ASAN/MSAN builds") + +# Skip test if _tkinter wasn't built. +import_helper.import_module('_tkinter') + +# Skip test if tk cannot be initialized. +requires('gui') + + +def load_tests(*args): + return load_package_tests(os.path.dirname(__file__), *args) + +def setUpModule(): + wantobjects = support.get_resource_value('wantobjects') + if wantobjects is not None: + unittest.enterModuleContext( + support.swap_attr(tkinter, 'wantobjects', int(wantobjects))) diff --git a/Lib/test/test_tkinter/__main__.py b/Lib/test/test_tkinter/__main__.py new file mode 100644 index 00000000000..40a23a297ec --- /dev/null +++ b/Lib/test/test_tkinter/__main__.py @@ -0,0 +1,4 @@ +from . import load_tests +import unittest + +unittest.main() diff --git a/Lib/test/test_tkinter/support.py b/Lib/test/test_tkinter/support.py new file mode 100644 index 00000000000..8d4ad2345a1 --- /dev/null +++ b/Lib/test/test_tkinter/support.py @@ -0,0 +1,211 @@ +import functools +import time +import tkinter +import unittest +from test import support + + +def setUpModule(): + wantobjects = support.get_resource_value('wantobjects') + if wantobjects is not None: + unittest.enterModuleContext( + support.swap_attr(tkinter, 'wantobjects', int(wantobjects))) + +class AbstractTkTest: + + @classmethod + def setUpClass(cls): + cls._old_support_default_root = tkinter._support_default_root + destroy_default_root() + tkinter.NoDefaultRoot() + cls.root = tkinter.Tk() + cls.wantobjects = cls.root.wantobjects() + if support.is_resource_enabled('wantobjects'): + assert cls.wantobjects == int(support.get_resource_value('wantobjects')) + # De-maximize main window. + # Some window managers can maximize new windows. + cls.root.wm_state('normal') + try: + cls.root.wm_attributes(zoomed=False) + except tkinter.TclError: + pass + + @classmethod + def tearDownClass(cls): + cls.root.update_idletasks() + cls.root.destroy() + del cls.root + tkinter._default_root = None + tkinter._support_default_root = cls._old_support_default_root + + def setUp(self): + self.root.deiconify() + + def tearDown(self): + for w in self.root.winfo_children(): + w.destroy() + self.root.withdraw() + + def require_mapped(self, widget, timeout=None): + """Realize *widget*, or skip the test if the window manager will + not map it (e.g. a tiling WM or a headless/contended display). + + Use this instead of a bare update() before querying realized + geometry (winfo_width(), identify(), coords(), place_info(), ...). + See gh-69134, gh-74941 and bpo-40722. + """ + if timeout is None: + timeout = support.LOOPBACK_TIMEOUT + if not wait_until_mapped(widget, timeout): + self.skipTest('widget was not mapped by the window manager ' + f'(timed out after {timeout:g}s)') + + +class AbstractDialogTest(AbstractTkTest): + # Tk delivers generated keyboard events to the focused window. Hide the + # root window, otherwise the window manager can take the focus back from + # the dialog (gh-154357). + + def setUp(self): + super().setUp() + self.root.withdraw() + + +class AbstractDefaultRootTest: + + def setUp(self): + self._old_support_default_root = tkinter._support_default_root + destroy_default_root() + tkinter._support_default_root = True + self.wantobjects = tkinter.wantobjects + + def tearDown(self): + destroy_default_root() + tkinter._default_root = None + tkinter._support_default_root = self._old_support_default_root + + def _test_widget(self, constructor): + # no master passing + x = constructor() + self.assertIsNotNone(tkinter._default_root) + self.assertIs(x.master, tkinter._default_root) + self.assertIs(x.tk, tkinter._default_root.tk) + x.destroy() + destroy_default_root() + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, constructor) + self.assertNotHasAttr(tkinter, '_default_root') + + +def destroy_default_root(): + if getattr(tkinter, '_default_root', None): + tkinter._default_root.update_idletasks() + tkinter._default_root.destroy() + tkinter._default_root = None + +def wait_until_mapped(widget, timeout=None, *, full_size=False): + """Wait until *widget* is actually mapped and laid out by the window + manager, so that realized-geometry queries (winfo_width(), identify(), + coords(), ...) return meaningful values. + + Return True once the widget is mapped with a non-trivial size, or False + if that has not happened within *timeout* seconds (default: + ``support.LOOPBACK_TIMEOUT``). Unlike Misc.wait_visibility(), this + never blocks indefinitely, so it is safe under a window manager that + never maps the window (see gh-69134, gh-74941, bpo-40722). + + If *full_size* is true, also wait until the realized size reaches the + requested size, so that per-pixel queries near an edge (e.g. identify()) + are reliable even under load. + """ + if timeout is None: + timeout = support.LOOPBACK_TIMEOUT + deadline = time.monotonic() + timeout + widget.update_idletasks() + reset = False + while True: + widget.update() # drain pending Map/Configure events + if widget.winfo_ismapped(): + if full_size: + w_ok = widget.winfo_width() >= widget.winfo_reqwidth() > 1 + h_ok = widget.winfo_height() >= widget.winfo_reqheight() > 1 + else: + w_ok = widget.winfo_width() > 1 + h_ok = widget.winfo_height() > 1 + if w_ok and h_ok: + return True + if full_size and not reset: + # Tk no longer resizes the toplevel to fit its content if + # the window manager has resized it. Undo this. + widget.winfo_toplevel().wm_geometry('') + reset = True + if time.monotonic() >= deadline: + return False + time.sleep(0.01) + + +def simulate_mouse_click(widget, x, y): + """Generate proper events to click at the x, y position (tries to act + like an X server).""" + widget.event_generate('', x=0, y=0) + widget.event_generate('', x=x, y=y) + widget.event_generate('', x=x, y=y) + widget.event_generate('', x=x, y=y) + + +import _tkinter +tcl_version = tuple(map(int, _tkinter.TCL_VERSION.split('.'))) +tk_version = tuple(map(int, _tkinter.TK_VERSION.split('.'))) + +def requires_tk(*version): + if len(version) <= 2 and tk_version >= version: + return lambda test: test + + def deco(test): + @functools.wraps(test) + def newtest(self): + root = getattr(self, 'root', None) + if get_tk_patchlevel(root) < version: + self.skipTest('requires Tk version >= ' + + '.'.join(map(str, version))) + test(self) + return newtest + return deco + +_tk_patchlevel = None +def get_tk_patchlevel(root): + global _tk_patchlevel + if _tk_patchlevel is None: + _tk_patchlevel = tkinter._parse_version(root.tk.globalgetvar('tk_patchLevel')) + return _tk_patchlevel + +units = { + 'c': 72 / 2.54, # centimeters + 'i': 72, # inches + 'm': 72 / 25.4, # millimeters + 'p': 1, # points +} + +def pixels_conv(value): + return float(value[:-1]) * units[value[-1:]] + +def tcl_obj_eq(actual, expected): + if actual == expected: + return True + if isinstance(actual, _tkinter.Tcl_Obj): + if isinstance(expected, str): + return str(actual) == expected + if isinstance(actual, tuple): + if isinstance(expected, tuple): + return (len(actual) == len(expected) and + all(tcl_obj_eq(act, exp) + for act, exp in zip(actual, expected))) + return False + +def widget_eq(actual, expected): + if actual == expected: + return True + if isinstance(actual, (str, tkinter.Widget)): + if isinstance(expected, (str, tkinter.Widget)): + return str(actual) == str(expected) + return False diff --git a/Lib/test/test_tkinter/test_colorchooser.py b/Lib/test/test_tkinter/test_colorchooser.py new file mode 100644 index 00000000000..8a7e97f207a --- /dev/null +++ b/Lib/test/test_tkinter/test_colorchooser.py @@ -0,0 +1,69 @@ +import unittest +import tkinter +from test.support import requires, swap_attr +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import AbstractDefaultRootTest, AbstractTkTest +from tkinter import colorchooser +from tkinter.colorchooser import askcolor +from tkinter.commondialog import Dialog + +requires('gui') + + +class ChooserTest(AbstractTkTest, unittest.TestCase): + + @classmethod + def setUpClass(cls): + AbstractTkTest.setUpClass.__func__(cls) + cls.cc = colorchooser.Chooser(initialcolor='dark blue slate') + + def test_fixoptions(self): + cc = self.cc + cc._fixoptions() + self.assertEqual(cc.options['initialcolor'], 'dark blue slate') + + cc.options['initialcolor'] = '#D2D269691E1E' + cc._fixoptions() + self.assertEqual(cc.options['initialcolor'], '#D2D269691E1E') + + cc.options['initialcolor'] = (210, 105, 30) + cc._fixoptions() + self.assertEqual(cc.options['initialcolor'], '#d2691e') + + def test_fixresult(self): + cc = self.cc + self.assertEqual(cc._fixresult(self.root, ()), (None, None)) + self.assertEqual(cc._fixresult(self.root, ''), (None, None)) + self.assertEqual(cc._fixresult(self.root, 'chocolate'), + ((210, 105, 30), 'chocolate')) + self.assertEqual(cc._fixresult(self.root, '#4a3c8c'), + ((74, 60, 140), '#4a3c8c')) + + +class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): + + def test_askcolor(self): + def test_callback(dialog, master): + nonlocal ismapped + master.update() + ismapped = master.winfo_ismapped() + raise ZeroDivisionError + + with swap_attr(Dialog, '_test_callback', test_callback): + ismapped = None + self.assertRaises(ZeroDivisionError, askcolor) + #askcolor() + self.assertEqual(ismapped, False) + + root = tkinter.Tk() + ismapped = None + self.assertRaises(ZeroDivisionError, askcolor) + self.assertEqual(ismapped, True) + root.destroy() + + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, askcolor) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tkinter/test_dnd.py b/Lib/test/test_tkinter/test_dnd.py new file mode 100644 index 00000000000..fe3685aefe4 --- /dev/null +++ b/Lib/test/test_tkinter/test_dnd.py @@ -0,0 +1,171 @@ +import unittest +import tkinter +from tkinter import dnd +from test.support import requires +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import AbstractTkTest + +requires('gui') + + +class Target: + def __init__(self, widget, log): + self.widget = widget + self.log = log + widget.dnd_accept = self.dnd_accept + + def dnd_accept(self, source, event): + self.log.append('accept') + return self + + def dnd_enter(self, source, event): + self.log.append('enter') + + def dnd_motion(self, source, event): + self.log.append('motion') + + def dnd_leave(self, source, event): + self.log.append('leave') + + def dnd_commit(self, source, event): + self.log.append('commit') + + +class Source: + def __init__(self, log): + self.log = log + + def dnd_end(self, target, event): + self.log.append('end') + + +class FakeEvent: + def __init__(self, widget, num=1): + self.num = num + self.widget = widget + self.x = self.y = self.x_root = self.y_root = 0 + + +class DndTest(AbstractTkTest, unittest.TestCase): + + def setUp(self): + super().setUp() + self.canvas = tkinter.Canvas(self.root) + self.canvas.pack() + # on_motion() locates the target with winfo_containing(). Bypass that + # real screen lookup, which depends on the window being visible and + # unobscured, so the test does not hinge on the window manager. + self.canvas.winfo_containing = lambda x, y: self.canvas + self.log = [] + self.source = Source(self.log) + self.target = Target(self.canvas, self.log) + + def tearDown(self): + # Make sure no drag-and-drop is left active between tests: the + # recursion guard is a name-mangled attribute on the root. + try: + del self.root._DndHandler__dnd + except AttributeError: + pass + super().tearDown() + + def test_drag_and_drop(self): + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + self.assertIsNotNone(handler) + handler.on_motion(FakeEvent(self.canvas)) # Enter the target. + handler.on_motion(FakeEvent(self.canvas)) # Move within the target. + handler.on_release(FakeEvent(self.canvas)) # Drop on the target. + self.assertEqual(self.log, + ['accept', 'enter', 'accept', 'motion', 'commit', 'end']) + + def test_cancel(self): + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + handler.on_motion(FakeEvent(self.canvas)) # Enter the target. + handler.cancel() # Leaves the target without committing. + self.assertEqual(self.log, ['accept', 'enter', 'leave', 'end']) + + def test_no_target(self): + # Nothing under the pointer accepts the drag. + self.canvas.winfo_containing = lambda x, y: None + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + handler.on_motion(FakeEvent(self.canvas)) + handler.on_release(FakeEvent(self.canvas)) + self.assertEqual(self.log, ['end']) + + def test_no_recursive_start(self): + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + self.assertIsNotNone(handler) + # A drag is already in progress, so a second start is ignored. + self.assertIsNone(dnd.dnd_start(self.source, FakeEvent(self.canvas))) + handler.cancel() + + def test_high_button_number_ignored(self): + self.assertIsNone(dnd.dnd_start(self.source, FakeEvent(self.canvas, num=6))) + + def test_restart_after_finish(self): + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + handler.cancel() + # Once a drag has finished a new one can start. + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + self.assertIsNotNone(handler) + handler.cancel() + + def test_drag_cursor(self): + self.canvas['cursor'] = 'watch' + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + # The drag cursor is shown while dragging, the original restored after. + self.assertEqual(handler.save_cursor, 'watch') + self.assertEqual(str(self.canvas['cursor']), 'hand2') + handler.cancel() + self.assertEqual(str(self.canvas['cursor']), 'watch') + + def test_bindings_added_and_removed(self): + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + self.assertIn('', self.canvas.bind()) + self.assertIn('', self.canvas.bind()) + handler.cancel() + self.assertNotIn('', self.canvas.bind()) + self.assertNotIn('', self.canvas.bind()) + + def test_switch_target(self): + log1, log2 = [], [] + w1, w2 = tkinter.Frame(self.root), tkinter.Frame(self.root) + target1, target2 = Target(w1, log1), Target(w2, log2) + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + self.canvas.winfo_containing = lambda x, y: w1 + handler.on_motion(FakeEvent(self.canvas)) # Enter target1. + self.canvas.winfo_containing = lambda x, y: w2 + handler.on_motion(FakeEvent(self.canvas)) # Leave target1, enter target2. + self.assertIs(handler.target, target2) + self.assertEqual(log1, ['accept', 'enter', 'leave']) + self.assertEqual(log2, ['accept', 'enter']) + handler.cancel() + + def test_target_in_ancestor(self): + # The widget under the pointer has no dnd_accept, but an ancestor + # does: the search walks up the master chain to find it. + parent = tkinter.Frame(self.root) + target = Target(parent, self.log) + child = tkinter.Frame(parent) + self.canvas.winfo_containing = lambda x, y: child + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + handler.on_motion(FakeEvent(self.canvas)) + self.assertIs(handler.target, target) + self.assertEqual(self.log, ['accept', 'enter']) + handler.cancel() + + def test_accept_returning_none_continues(self): + # dnd_accept() returning None means "not me, keep looking up". + parent = tkinter.Frame(self.root) + target = Target(parent, self.log) + child = tkinter.Frame(parent) + child.dnd_accept = lambda source, event: None + self.canvas.winfo_containing = lambda x, y: child + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + handler.on_motion(FakeEvent(self.canvas)) + self.assertIs(handler.target, target) + handler.cancel() + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tkinter/test_filedialog.py b/Lib/test/test_tkinter/test_filedialog.py new file mode 100644 index 00000000000..e4e0f2b26c3 --- /dev/null +++ b/Lib/test/test_tkinter/test_filedialog.py @@ -0,0 +1,73 @@ +import os +import unittest +from tkinter import filedialog +from tkinter.commondialog import Dialog +from test.support import requires, swap_attr +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import AbstractDialogTest, AbstractTkTest + +requires('gui') + + +class NativeDialogTest(AbstractTkTest, unittest.TestCase): + # Open, SaveAs and Directory wrap modal native dialogs. The _test_callback + # seam is called by show() just before the dialog would open; replacing it + # with a function that raises exercises show() without blocking on the + # dialog. + + def check(self, dialog_class, command): + self.assertEqual(dialog_class.command, command) + master = None + def test_callback(dialog, parent): + nonlocal master + master = parent + raise ZeroDivisionError + with swap_attr(Dialog, '_test_callback', test_callback): + d = dialog_class(self.root, title='Test') + self.assertRaises(ZeroDivisionError, d.show) + self.assertIs(master, self.root) + + def test_open(self): + self.check(filedialog.Open, 'tk_getOpenFile') + + def test_saveas(self): + self.check(filedialog.SaveAs, 'tk_getSaveFile') + + def test_directory(self): + self.check(filedialog.Directory, 'tk_chooseDirectory') + + +class FileDialogTest(AbstractDialogTest, unittest.TestCase): + # The pure-Python FileDialog runs its own modal loop in go(); its logic is + # exercised here without entering the loop. + + def test_selection(self): + d = filedialog.FileDialog(self.root) + d.directory = os.path.abspath(os.sep) + d.set_selection('spam.txt') + self.assertEqual(os.path.basename(d.get_selection()), 'spam.txt') + + def test_filter(self): + d = filedialog.FileDialog(self.root) + d.set_filter(os.getcwd(), '*.py') + self.assertEqual(d.get_filter(), (os.getcwd(), '*.py')) + + def test_ok_cancel(self): + d = filedialog.FileDialog(self.root) + d.directory = os.getcwd() + d.set_selection('spam.txt') + d.ok_command() # Accepts the current selection. + self.assertEqual(os.path.basename(d.how), 'spam.txt') + d.cancel_command() # Returns no selection. + self.assertIsNone(d.how) + + def test_subclasses(self): + for cls in filedialog.LoadFileDialog, filedialog.SaveFileDialog: + with self.subTest(cls=cls.__name__): + d = cls(self.root) + self.assertIsInstance(d, filedialog.FileDialog) + self.assertEqual(d.top.title(), cls.title) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tkinter/test_font.py b/Lib/test/test_tkinter/test_font.py new file mode 100644 index 00000000000..6423a4e70af --- /dev/null +++ b/Lib/test/test_tkinter/test_font.py @@ -0,0 +1,250 @@ +import collections.abc +import unittest +import tkinter +from tkinter import font +from test.support import requires, gc_collect, ALWAYS_EQ +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import AbstractTkTest, AbstractDefaultRootTest + +requires('gui') + +fontname = "TkDefaultFont" + +class FontTest(AbstractTkTest, unittest.TestCase): + + @classmethod + def setUpClass(cls): + AbstractTkTest.setUpClass.__func__(cls) + try: + cls.font = font.Font(root=cls.root, name=fontname, exists=True) + except tkinter.TclError: + cls.font = font.Font(root=cls.root, name=fontname, exists=False) + + def test_configure(self): + self.assertEqual(self.font.config, self.font.configure) + options = self.font.configure() + self.assertGreaterEqual(set(options), + {'family', 'size', 'weight', 'slant', 'underline', 'overstrike'}) + for key in options: + self.assertEqual(self.font.cget(key), options[key]) + self.assertEqual(self.font[key], options[key]) + for key in 'family', 'weight', 'slant': + self.assertIsInstance(options[key], str) + self.assertIsInstance(self.font.cget(key), str) + self.assertIsInstance(self.font[key], str) + sizetype = int if self.wantobjects else str + for key in 'size', 'underline', 'overstrike': + self.assertIsInstance(options[key], sizetype) + self.assertIsInstance(self.font.cget(key), sizetype) + self.assertIsInstance(self.font[key], sizetype) + self.assertRaisesRegex(tkinter.TclError, 'bad option "-spam"', + self.font.cget, 'spam') + self.assertRaisesRegex(tkinter.TclError, 'bad option "-spam"', + self.font.configure, spam='x') + self.assertRaises(TypeError, self.font.cget) + self.assertRaises(TypeError, self.font.cget, 'size', 'weight') + + def test_create_from_named_font(self): + # gh-143990: a font created from a named font copies its configured + # options, preserving a size specified in pixels (a negative size). + sizetype = int if self.wantobjects else str + named = font.Font(root=self.root, name='my named font', # name with spaces + family='Times', size=-20, weight='bold') + # The source is the name of a named font or a Font representing one. + for source in ['my named font', named]: + with self.subTest(source=source): + f = font.Font(root=self.root, font=source) + self.assertEqual(f.cget('size'), sizetype(-20)) + self.assertEqual(f.actual('family'), named.actual('family')) + self.assertEqual(f.actual('weight'), 'bold') + + def test_create_from_description(self): + # gh-143990: a font created from a font description is resolved via + # "font actual", so a size in pixels (negative) becomes a size in points. + descriptions = [ + ('Times', -20), # tuple + ('Times', -20, 'bold'), # tuple with a style + 'Times -20', # string + 'Times -20 bold', # string with a style + '{Times New Roman} -20', # string, family with spaces + ] + for desc in descriptions: + with self.subTest(font=desc): + f = font.Font(root=self.root, font=desc) + self.assertGreater(int(f.cget('size')), 0) # pixels -> points + + def test_copy(self): + # size=-20 (pixels): copy() copies the configured options, so the + # size is preserved rather than resolved (gh-143990). + f = font.Font(root=self.root, family='Times', size=-20, weight='bold') + copied = f.copy() + self.assertIsInstance(copied, font.Font) + self.assertIsNot(copied, f) + self.assertNotEqual(copied.name, f.name) + self.assertEqual(copied.actual(), f.actual()) + sizetype = int if self.wantobjects else str + self.assertEqual(copied.cget('size'), sizetype(-20)) + # The copy is independent of the original. + copied.configure(size=20) + self.assertEqual(f.cget('size'), sizetype(-20)) + self.assertEqual(copied.cget('size'), sizetype(20)) + self.assertRaises(TypeError, f.copy, 'x') + + def test_unicode_family(self): + family = 'MS \u30b4\u30b7\u30c3\u30af' + try: + f = font.Font(root=self.root, family=family, exists=True) + except tkinter.TclError: + f = font.Font(root=self.root, family=family, exists=False) + self.assertEqual(f.cget('family'), family) + del f + gc_collect() + + def test_actual(self): + options = self.font.actual() + self.assertGreaterEqual(set(options), + {'family', 'size', 'weight', 'slant', 'underline', 'overstrike'}) + for key in options: + self.assertEqual(self.font.actual(key), options[key]) + for key in 'family', 'weight', 'slant': + self.assertIsInstance(options[key], str) + self.assertIsInstance(self.font.actual(key), str) + sizetype = int if self.wantobjects else str + for key in 'size', 'underline', 'overstrike': + self.assertIsInstance(options[key], sizetype) + self.assertIsInstance(self.font.actual(key), sizetype) + self.assertRaisesRegex(tkinter.TclError, 'bad option "-spam"', + self.font.actual, 'spam') + self.assertRaises(TypeError, self.font.actual, 'size', 'weight', 'slant') + + def test_name(self): + self.assertEqual(self.font.name, fontname) + self.assertEqual(str(self.font), fontname) + + def test_equality(self): + font1 = font.Font(root=self.root, name=fontname, exists=True) + font2 = font.Font(root=self.root, name=fontname, exists=True) + self.assertIsNot(font1, font2) + self.assertEqual(font1, font2) + self.assertNotEqual(font1, font1.copy()) + + self.assertNotEqual(font1, 0) + self.assertEqual(font1, ALWAYS_EQ) + + root2 = tkinter.Tk() + self.addCleanup(root2.destroy) + font3 = font.Font(root=root2, name=fontname, exists=True) + self.assertEqual(str(font1), str(font3)) + self.assertNotEqual(font1, font3) + + def test_measure(self): + self.assertIsInstance(self.font.measure('abc'), int) + self.assertEqual(self.font.measure(''), 0) + self.assertIsInstance( + self.font.measure('abc', displayof=self.root), int) + self.assertRaises(TypeError, self.font.measure) + self.assertRaises(TypeError, self.font.measure, 'a', 'b', 'c') + + def test_metrics(self): + metrics = self.font.metrics() + self.assertGreaterEqual(set(metrics), + {'ascent', 'descent', 'linespace', 'fixed'}) + for key in metrics: + self.assertEqual(self.font.metrics(key), metrics[key]) + self.assertEqual(self.font.metrics(key, displayof=self.root), + metrics[key]) + self.assertIsInstance(metrics[key], int) + self.assertIsInstance(self.font.metrics(key), int) + self.assertRaisesRegex(tkinter.TclError, 'bad metric "-spam"', + self.font.metrics, 'spam') + + def test_families(self): + families = font.families(self.root) + self.assertIsInstance(families, tuple) + self.assertTrue(families) + for family in families: + self.assertIsInstance(family, str) + self.assertTrue(family) + + def test_names(self): + names = font.names(self.root) + self.assertIsInstance(names, tuple) + self.assertTrue(names) + for name in names: + self.assertIsInstance(name, str) + self.assertTrue(name) + self.assertIn(fontname, names) + + def test_nametofont(self): + testfont = font.nametofont(fontname, root=self.root) + self.assertIsInstance(testfont, font.Font) + self.assertEqual(testfont.name, fontname) + + def test_repr(self): + self.assertEqual( + repr(self.font), f'' + ) + + def test_iterable_protocol(self): + self.assertNotIsSubclass(font.Font, collections.abc.Iterable) + self.assertNotIsSubclass(font.Font, collections.abc.Container) + self.assertNotIsInstance(self.font, collections.abc.Iterable) + self.assertNotIsInstance(self.font, collections.abc.Container) + with self.assertRaisesRegex(TypeError, 'is not iterable'): + iter(self.font) + with self.assertRaisesRegex(TypeError, 'is not a container or iterable'): + self.font in self.font + + +class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): + + def test_families(self): + self.assertRaises(RuntimeError, font.families) + root = tkinter.Tk() + families = font.families() + self.assertIsInstance(families, tuple) + self.assertTrue(families) + for family in families: + self.assertIsInstance(family, str) + self.assertTrue(family) + root.destroy() + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, font.families) + + def test_names(self): + self.assertRaises(RuntimeError, font.names) + root = tkinter.Tk() + names = font.names() + self.assertIsInstance(names, tuple) + self.assertTrue(names) + for name in names: + self.assertIsInstance(name, str) + self.assertTrue(name) + self.assertIn(fontname, names) + root.destroy() + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, font.names) + + def test_nametofont(self): + self.assertRaises(RuntimeError, font.nametofont, fontname) + root = tkinter.Tk() + testfont = font.nametofont(fontname) + self.assertIsInstance(testfont, font.Font) + self.assertEqual(testfont.name, fontname) + root.destroy() + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, font.nametofont, fontname) + + +class TestModule(unittest.TestCase): + def test_deprecated__version__(self): + with self.assertWarnsRegex( + DeprecationWarning, + "'__version__' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(font, "__version__") + self.assertEqual(cm.filename, __file__) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tkinter/test_geometry_managers.py b/Lib/test/test_tkinter/test_geometry_managers.py new file mode 100644 index 00000000000..e98a6af060e --- /dev/null +++ b/Lib/test/test_tkinter/test_geometry_managers.py @@ -0,0 +1,1001 @@ +import unittest +import re +import tkinter +from tkinter import TclError +from test.support import requires + +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import pixels_conv +from test.test_tkinter.widget_tests import AbstractWidgetTest + +requires('gui') + + +EXPECTED_FLOAT_ERRMSG = 'expected floating-point number but got "{}"' +EXPECTED_FLOAT_OR_EMPTY_ERRMSG = 'expected floating-point number (or "" )?but got "{}"' +EXPECTED_SCREEN_DISTANCE_ERRMSG = '(bad|expected) screen distance (but got )?"{}"' +EXPECTED_SCREEN_DISTANCE_OR_EMPTY_ERRMSG = '(bad|expected) screen distance (or "" but got )?"{}"' + +class PackTest(AbstractWidgetTest, unittest.TestCase): + + test_keys = None + test_options_in_docstring = None + + def create2(self): + pack = tkinter.Toplevel(self.root, name='pack') + pack.wm_geometry('300x200+0+0') + pack.wm_minsize(1, 1) + a = tkinter.Frame(pack, name='a', width=20, height=40, bg='red') + b = tkinter.Frame(pack, name='b', width=50, height=30, bg='blue') + c = tkinter.Frame(pack, name='c', width=80, height=80, bg='green') + d = tkinter.Frame(pack, name='d', width=40, height=30, bg='yellow') + return pack, a, b, c, d + + def test_pack_configure_after(self): + pack, a, b, c, d = self.create2() + with self.assertRaisesRegex(TclError, 'window "%s" isn\'t packed' % b): + a.pack_configure(after=b) + with self.assertRaisesRegex(TclError, 'bad window path name ".foo"'): + a.pack_configure(after='.foo') + a.pack_configure(side='top') + b.pack_configure(side='top') + c.pack_configure(side='top') + d.pack_configure(side='top') + self.assertEqual(pack.pack_content(), [a, b, c, d]) + a.pack_configure(after=b) + self.assertEqual(pack.pack_content(), [b, a, c, d]) + a.pack_configure(after=a) + self.assertEqual(pack.pack_content(), [b, a, c, d]) + + def test_pack_configure_anchor(self): + pack, a, b, c, d = self.create2() + def check(anchor, geom): + a.pack_configure(side='top', ipadx=5, padx=10, ipady=15, pady=20, + expand=True, anchor=anchor) + self.root.update() + self.assertEqual(a.winfo_geometry(), geom) + check('n', '30x70+135+20') + check('ne', '30x70+260+20') + check('e', '30x70+260+65') + check('se', '30x70+260+110') + check('s', '30x70+135+110') + check('sw', '30x70+10+110') + check('w', '30x70+10+65') + check('nw', '30x70+10+20') + check('center', '30x70+135+65') + + def test_pack_configure_before(self): + pack, a, b, c, d = self.create2() + with self.assertRaisesRegex(TclError, 'window "%s" isn\'t packed' % b): + a.pack_configure(before=b) + with self.assertRaisesRegex(TclError, 'bad window path name ".foo"'): + a.pack_configure(before='.foo') + a.pack_configure(side='top') + b.pack_configure(side='top') + c.pack_configure(side='top') + d.pack_configure(side='top') + self.assertEqual(pack.pack_content(), [a, b, c, d]) + a.pack_configure(before=d) + self.assertEqual(pack.pack_content(), [b, c, a, d]) + a.pack_configure(before=a) + self.assertEqual(pack.pack_content(), [b, c, a, d]) + + def test_pack_configure_expand(self): + pack, a, b, c, d = self.create2() + def check(*geoms): + self.root.update() + self.assertEqual(a.winfo_geometry(), geoms[0]) + self.assertEqual(b.winfo_geometry(), geoms[1]) + self.assertEqual(c.winfo_geometry(), geoms[2]) + self.assertEqual(d.winfo_geometry(), geoms[3]) + a.pack_configure(side='left') + b.pack_configure(side='top') + c.pack_configure(side='right') + d.pack_configure(side='bottom') + check('20x40+0+80', '50x30+135+0', '80x80+220+75', '40x30+100+170') + a.pack_configure(side='left', expand='yes') + b.pack_configure(side='top', expand='on') + c.pack_configure(side='right', expand=True) + d.pack_configure(side='bottom', expand=1) + check('20x40+40+80', '50x30+175+35', '80x80+180+110', '40x30+100+135') + a.pack_configure(side='left', expand='yes', fill='both') + b.pack_configure(side='top', expand='on', fill='both') + c.pack_configure(side='right', expand=True, fill='both') + d.pack_configure(side='bottom', expand=1, fill='both') + check('100x200+0+0', '200x100+100+0', '160x100+140+100', '40x100+100+100') + + def test_pack_configure_in(self): + pack, a, b, c, d = self.create2() + a.pack_configure(side='top') + b.pack_configure(side='top') + c.pack_configure(side='top') + d.pack_configure(side='top') + a.pack_configure(in_=pack) + self.assertEqual(pack.pack_content(), [b, c, d, a]) + a.pack_configure(in_=c) + self.assertEqual(pack.pack_content(), [b, c, d]) + self.assertEqual(c.pack_content(), [a]) + with self.assertRaisesRegex( + TclError, """can't pack "?%s"? inside itself""" % (a,)): + a.pack_configure(in_=a) + with self.assertRaisesRegex(TclError, 'bad window path name ".foo"'): + a.pack_configure(in_='.foo') + + def test_pack_configure_padx_ipadx_fill(self): + pack, a, b, c, d = self.create2() + def check(geom1, geom2, **kwargs): + a.pack_forget() + b.pack_forget() + a.pack_configure(**kwargs) + b.pack_configure(expand=True, fill='both') + self.root.update() + self.assertEqual(a.winfo_geometry(), geom1) + self.assertEqual(b.winfo_geometry(), geom2) + check('20x40+260+80', '240x200+0+0', side='right', padx=20) + check('20x40+250+80', '240x200+0+0', side='right', padx=(10, 30)) + check('60x40+240+80', '240x200+0+0', side='right', ipadx=20) + check('30x40+260+80', '250x200+0+0', side='right', ipadx=5, padx=10) + check('20x40+260+80', '240x200+0+0', side='right', padx=20, fill='x') + check('20x40+249+80', '240x200+0+0', + side='right', padx=(9, 31), fill='x') + check('60x40+240+80', '240x200+0+0', side='right', ipadx=20, fill='x') + check('30x40+260+80', '250x200+0+0', + side='right', ipadx=5, padx=10, fill='x') + check('30x40+255+80', '250x200+0+0', + side='right', ipadx=5, padx=(5, 15), fill='x') + check('20x40+140+0', '300x160+0+40', side='top', padx=20) + check('20x40+120+0', '300x160+0+40', side='top', padx=(0, 40)) + check('60x40+120+0', '300x160+0+40', side='top', ipadx=20) + check('30x40+135+0', '300x160+0+40', side='top', ipadx=5, padx=10) + check('30x40+130+0', '300x160+0+40', side='top', ipadx=5, padx=(5, 15)) + check('260x40+20+0', '300x160+0+40', side='top', padx=20, fill='x') + check('260x40+25+0', '300x160+0+40', + side='top', padx=(25, 15), fill='x') + check('300x40+0+0', '300x160+0+40', side='top', ipadx=20, fill='x') + check('280x40+10+0', '300x160+0+40', + side='top', ipadx=5, padx=10, fill='x') + check('280x40+5+0', '300x160+0+40', + side='top', ipadx=5, padx=(5, 15), fill='x') + a.pack_configure(padx='1c') + self.assertEqual(a.pack_info()['padx'], + self._str(pack.winfo_pixels('1c'))) + a.pack_configure(ipadx='1c') + self.assertEqual(a.pack_info()['ipadx'], + self._str(pack.winfo_pixels('1c'))) + + def test_pack_configure_pady_ipady_fill(self): + pack, a, b, c, d = self.create2() + def check(geom1, geom2, **kwargs): + a.pack_forget() + b.pack_forget() + a.pack_configure(**kwargs) + b.pack_configure(expand=True, fill='both') + self.root.update() + self.assertEqual(a.winfo_geometry(), geom1) + self.assertEqual(b.winfo_geometry(), geom2) + check('20x40+280+80', '280x200+0+0', side='right', pady=20) + check('20x40+280+70', '280x200+0+0', side='right', pady=(10, 30)) + check('20x80+280+60', '280x200+0+0', side='right', ipady=20) + check('20x50+280+75', '280x200+0+0', side='right', ipady=5, pady=10) + check('20x40+280+80', '280x200+0+0', side='right', pady=20, fill='x') + check('20x40+280+69', '280x200+0+0', + side='right', pady=(9, 31), fill='x') + check('20x80+280+60', '280x200+0+0', side='right', ipady=20, fill='x') + check('20x50+280+75', '280x200+0+0', + side='right', ipady=5, pady=10, fill='x') + check('20x50+280+70', '280x200+0+0', + side='right', ipady=5, pady=(5, 15), fill='x') + check('20x40+140+20', '300x120+0+80', side='top', pady=20) + check('20x40+140+0', '300x120+0+80', side='top', pady=(0, 40)) + check('20x80+140+0', '300x120+0+80', side='top', ipady=20) + check('20x50+140+10', '300x130+0+70', side='top', ipady=5, pady=10) + check('20x50+140+5', '300x130+0+70', side='top', ipady=5, pady=(5, 15)) + check('300x40+0+20', '300x120+0+80', side='top', pady=20, fill='x') + check('300x40+0+25', '300x120+0+80', + side='top', pady=(25, 15), fill='x') + check('300x80+0+0', '300x120+0+80', side='top', ipady=20, fill='x') + check('300x50+0+10', '300x130+0+70', + side='top', ipady=5, pady=10, fill='x') + check('300x50+0+5', '300x130+0+70', + side='top', ipady=5, pady=(5, 15), fill='x') + a.pack_configure(pady='1c') + self.assertEqual(a.pack_info()['pady'], + self._str(pack.winfo_pixels('1c'))) + a.pack_configure(ipady='1c') + self.assertEqual(a.pack_info()['ipady'], + self._str(pack.winfo_pixels('1c'))) + + def test_pack_configure_side(self): + pack, a, b, c, d = self.create2() + def check(side, geom1, geom2): + a.pack_configure(side=side) + self.assertEqual(a.pack_info()['side'], side) + b.pack_configure(expand=True, fill='both') + self.root.update() + self.assertEqual(a.winfo_geometry(), geom1) + self.assertEqual(b.winfo_geometry(), geom2) + check('top', '20x40+140+0', '300x160+0+40') + check('bottom', '20x40+140+160', '300x160+0+0') + check('left', '20x40+0+80', '280x200+20+0') + check('right', '20x40+280+80', '280x200+0+0') + + def test_pack_forget(self): + pack, a, b, c, d = self.create2() + a.pack_configure() + b.pack_configure() + c.pack_configure() + self.assertEqual(pack.pack_content(), [a, b, c]) + b.pack_forget() + self.assertEqual(pack.pack_content(), [a, c]) + b.pack_forget() + self.assertEqual(pack.pack_content(), [a, c]) + d.pack_forget() + + def test_pack_info(self): + pack, a, b, c, d = self.create2() + with self.assertRaisesRegex(TclError, 'window "%s" isn\'t packed' % a): + a.pack_info() + a.pack_configure() + b.pack_configure(side='right', in_=a, anchor='s', expand=True, fill='x', + ipadx=5, padx=10, ipady=2, pady=(5, 15)) + info = a.pack_info() + self.assertIsInstance(info, dict) + self.assertEqual(info['anchor'], 'center') + self.assertEqual(info['expand'], self._str(0)) + self.assertEqual(info['fill'], 'none') + self.assertEqual(info['in'], pack) + self.assertEqual(info['ipadx'], self._str(0)) + self.assertEqual(info['ipady'], self._str(0)) + self.assertEqual(info['padx'], self._str(0)) + self.assertEqual(info['pady'], self._str(0)) + self.assertEqual(info['side'], 'top') + info = b.pack_info() + self.assertIsInstance(info, dict) + self.assertEqual(info['anchor'], 's') + self.assertEqual(info['expand'], self._str(1)) + self.assertEqual(info['fill'], 'x') + self.assertEqual(info['in'], a) + self.assertEqual(info['ipadx'], self._str(5)) + self.assertEqual(info['ipady'], self._str(2)) + self.assertEqual(info['padx'], self._str(10)) + self.assertEqual(info['pady'], self._str((5, 15))) + self.assertEqual(info['side'], 'right') + + def test_pack_propagate(self): + pack, a, b, c, d = self.create2() + pack.configure(width=300, height=200) + a.pack_configure() + pack.pack_propagate(False) + self.root.update() + self.assertEqual(pack.winfo_reqwidth(), 300) + self.assertEqual(pack.winfo_reqheight(), 200) + pack.pack_propagate(True) + self.root.update() + self.assertEqual(pack.winfo_reqwidth(), 20) + self.assertEqual(pack.winfo_reqheight(), 40) + + def test_pack_content(self): + pack, a, b, c, d = self.create2() + self.assertEqual(pack.pack_content(), []) + a.pack_configure() + self.assertEqual(pack.pack_content(), [a]) + b.pack_configure() + self.assertEqual(pack.pack_content(), [a, b]) + + def test_pack_slaves(self): + pack, a, b, c, d = self.create2() + self.assertEqual(pack.pack_slaves(), []) + a.pack_configure() + self.assertEqual(pack.pack_slaves(), [a]) + b.pack_configure() + self.assertEqual(pack.pack_slaves(), [a, b]) + + def test_pack_short_aliases(self): + # slaves, content and propagate are aliases of the pack_* methods + # (Misc precedes Pack, Place and Grid in the method resolution order). + pack, a, b, c, d = self.create2() + self.assertEqual(pack.slaves, pack.pack_slaves) + self.assertEqual(pack.content, pack.pack_content) + self.assertEqual(pack.propagate, pack.pack_propagate) + + self.assertEqual(pack.slaves(), []) + a.pack_configure() + self.assertEqual(pack.slaves(), [a]) + self.assertEqual(pack.content(), [a]) + + pack.configure(width=300, height=200) + pack.propagate(False) + self.root.update() + self.assertEqual(pack.winfo_reqwidth(), 300) + self.assertEqual(pack.winfo_reqheight(), 200) + + +class PlaceTest(AbstractWidgetTest, unittest.TestCase): + + test_keys = None + test_options_in_docstring = None + + def create2(self): + t = tkinter.Toplevel(self.root, width=300, height=200, bd=0) + t.wm_geometry('300x200+0+0') + f = tkinter.Frame(t, width=154, height=84, bd=2, relief='raised') + f.place_configure(x=48, y=38) + f2 = tkinter.Frame(t, width=30, height=60, bd=2, relief='raised') + self.root.update() + return t, f, f2 + + def test_place_configure_in(self): + t, f, f2 = self.create2() + self.assertEqual(f2.winfo_manager(), '') + with self.assertRaisesRegex( + TclError, + """can't place "?%s"? relative to itself""" + % re.escape(str(f2))): + f2.place_configure(in_=f2) + self.assertEqual(f2.winfo_manager(), '') + with self.assertRaisesRegex(TclError, 'bad window path name'): + f2.place_configure(in_='spam') + f2.place_configure(in_=f) + self.assertEqual(f2.winfo_manager(), 'place') + + def test_place_configure_x(self): + t, f, f2 = self.create2() + f2.place_configure(in_=f) + self.assertEqual(f2.place_info()['x'], '0') + self.root.update() + self.assertEqual(f2.winfo_x(), 50) + f2.place_configure(x=100) + self.assertEqual(f2.place_info()['x'], '100') + self.root.update() + self.assertEqual(f2.winfo_x(), 150) + f2.place_configure(x=-10, relx=1) + self.assertEqual(f2.place_info()['x'], '-10') + self.root.update() + self.assertEqual(f2.winfo_x(), 190) + with self.assertRaisesRegex(TclError, + EXPECTED_SCREEN_DISTANCE_ERRMSG.format('spam')): + f2.place_configure(in_=f, x='spam') + + def test_place_configure_y(self): + t, f, f2 = self.create2() + f2.place_configure(in_=f) + self.assertEqual(f2.place_info()['y'], '0') + self.root.update() + self.assertEqual(f2.winfo_y(), 40) + f2.place_configure(y=50) + self.assertEqual(f2.place_info()['y'], '50') + self.root.update() + self.assertEqual(f2.winfo_y(), 90) + f2.place_configure(y=-10, rely=1) + self.assertEqual(f2.place_info()['y'], '-10') + self.root.update() + self.assertEqual(f2.winfo_y(), 110) + with self.assertRaisesRegex(TclError, + EXPECTED_SCREEN_DISTANCE_ERRMSG.format('spam')): + f2.place_configure(in_=f, y='spam') + + def test_place_configure_relx(self): + t, f, f2 = self.create2() + f2.place_configure(in_=f) + self.assertEqual(f2.place_info()['relx'], '0') + self.root.update() + self.assertEqual(f2.winfo_x(), 50) + f2.place_configure(relx=0.5) + self.assertEqual(f2.place_info()['relx'], '0.5') + self.root.update() + self.assertEqual(f2.winfo_x(), 125) + f2.place_configure(relx=1) + self.assertEqual(f2.place_info()['relx'], '1') + self.root.update() + self.assertEqual(f2.winfo_x(), 200) + with self.assertRaisesRegex(TclError, EXPECTED_FLOAT_ERRMSG.format('spam')): + f2.place_configure(in_=f, relx='spam') + + def test_place_configure_rely(self): + t, f, f2 = self.create2() + f2.place_configure(in_=f) + self.assertEqual(f2.place_info()['rely'], '0') + self.root.update() + self.assertEqual(f2.winfo_y(), 40) + f2.place_configure(rely=0.5) + self.assertEqual(f2.place_info()['rely'], '0.5') + self.root.update() + self.assertEqual(f2.winfo_y(), 80) + f2.place_configure(rely=1) + self.assertEqual(f2.place_info()['rely'], '1') + self.root.update() + self.assertEqual(f2.winfo_y(), 120) + with self.assertRaisesRegex(TclError, EXPECTED_FLOAT_ERRMSG.format('spam')): + f2.place_configure(in_=f, rely='spam') + + def test_place_configure_anchor(self): + f = tkinter.Frame(self.root) + with self.assertRaisesRegex(TclError, 'bad anchor "j"'): + f.place_configure(anchor='j') + with self.assertRaisesRegex(TclError, 'ambiguous anchor ""'): + f.place_configure(anchor='') + for value in 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center': + f.place_configure(anchor=value) + self.assertEqual(f.place_info()['anchor'], value) + + def test_place_configure_width(self): + t, f, f2 = self.create2() + f2.place_configure(in_=f, width=120) + self.root.update() + self.assertEqual(f2.winfo_width(), 120) + f2.place_configure(width='') + self.root.update() + self.assertEqual(f2.winfo_width(), 30) + with self.assertRaisesRegex(TclError, + EXPECTED_SCREEN_DISTANCE_OR_EMPTY_ERRMSG.format('abcd')): + f2.place_configure(width='abcd') + + def test_place_configure_height(self): + t, f, f2 = self.create2() + f2.place_configure(in_=f, height=120) + self.root.update() + self.assertEqual(f2.winfo_height(), 120) + f2.place_configure(height='') + self.root.update() + self.assertEqual(f2.winfo_height(), 60) + with self.assertRaisesRegex(TclError, + EXPECTED_SCREEN_DISTANCE_OR_EMPTY_ERRMSG.format('abcd')): + f2.place_configure(height='abcd') + + def test_place_configure_relwidth(self): + t, f, f2 = self.create2() + f2.place_configure(in_=f, relwidth=0.5) + self.root.update() + self.assertEqual(f2.winfo_width(), 75) + f2.place_configure(relwidth='') + self.root.update() + self.assertEqual(f2.winfo_width(), 30) + with self.assertRaisesRegex(TclError, EXPECTED_FLOAT_OR_EMPTY_ERRMSG.format('abcd')): + f2.place_configure(relwidth='abcd') + + def test_place_configure_relheight(self): + t, f, f2 = self.create2() + f2.place_configure(in_=f, relheight=0.5) + self.root.update() + self.assertEqual(f2.winfo_height(), 40) + f2.place_configure(relheight='') + self.root.update() + self.assertEqual(f2.winfo_height(), 60) + with self.assertRaisesRegex(TclError, EXPECTED_FLOAT_OR_EMPTY_ERRMSG.format('abcd')): + f2.place_configure(relheight='abcd') + + def test_place_configure_bordermode(self): + f = tkinter.Frame(self.root) + with self.assertRaisesRegex(TclError, 'bad bordermode "j"'): + f.place_configure(bordermode='j') + with self.assertRaisesRegex(TclError, 'ambiguous bordermode ""'): + f.place_configure(bordermode='') + for value in 'inside', 'outside', 'ignore': + f.place_configure(bordermode=value) + self.assertEqual(f.place_info()['bordermode'], value) + + def test_place_forget(self): + foo = tkinter.Frame(self.root) + foo.place_configure(width=50, height=50) + self.root.update() + foo.place_forget() + self.root.update() + self.assertFalse(foo.winfo_ismapped()) + with self.assertRaises(TypeError): + foo.place_forget(0) + + def test_place_info(self): + t, f, f2 = self.create2() + f2.place_configure(in_=f, x=1, y=2, width=3, height=4, + relx=0.1, rely=0.2, relwidth=0.3, relheight=0.4, + anchor='se', bordermode='outside') + info = f2.place_info() + self.assertIsInstance(info, dict) + self.assertEqual(info['x'], '1') + self.assertEqual(info['y'], '2') + self.assertEqual(info['width'], '3') + self.assertEqual(info['height'], '4') + self.assertEqual(info['relx'], '0.1') + self.assertEqual(info['rely'], '0.2') + self.assertEqual(info['relwidth'], '0.3') + self.assertEqual(info['relheight'], '0.4') + self.assertEqual(info['anchor'], 'se') + self.assertEqual(info['bordermode'], 'outside') + self.assertEqual(info['x'], '1') + self.assertEqual(info['x'], '1') + with self.assertRaises(TypeError): + f2.place_info(0) + + def test_place_content(self): + foo = tkinter.Frame(self.root) + bar = tkinter.Frame(self.root) + self.assertEqual(foo.place_content(), []) + bar.place_configure(in_=foo) + self.assertEqual(foo.place_content(), [bar]) + with self.assertRaises(TypeError): + foo.place_content(0) + + def test_place_slaves(self): + foo = tkinter.Frame(self.root) + bar = tkinter.Frame(self.root) + self.assertEqual(foo.place_slaves(), []) + bar.place_configure(in_=foo) + self.assertEqual(foo.place_slaves(), [bar]) + with self.assertRaises(TypeError): + foo.place_slaves(0) + + def test_place_method_aliases(self): + # The Place manager defines configure, info, forget, slaves and + # content as aliases of its place_* methods. On a real widget the + # short names are provided by Misc and Pack (earlier in the method + # resolution order), so the aliases are checked on the class itself. + self.assertIs(tkinter.Place.configure, tkinter.Place.place_configure) + self.assertIs(tkinter.Place.config, tkinter.Place.place_configure) + self.assertIs(tkinter.Place.info, tkinter.Place.place_info) + self.assertIs(tkinter.Place.forget, tkinter.Place.place_forget) + self.assertIs(tkinter.Place.slaves, tkinter.Misc.place_slaves) + self.assertIs(tkinter.Place.content, tkinter.Misc.place_content) + + +class GridTest(AbstractWidgetTest, unittest.TestCase): + + test_keys = None + test_options_in_docstring = None + + def tearDown(self): + cols, rows = self.root.grid_size() + for i in range(cols + 1): + self.root.grid_columnconfigure(i, weight=0, minsize=0, pad=0, uniform='') + for i in range(rows + 1): + self.root.grid_rowconfigure(i, weight=0, minsize=0, pad=0, uniform='') + self.root.grid_propagate(1) + self.root.grid_anchor('nw') + super().tearDown() + + def test_grid_configure(self): + b = tkinter.Button(self.root) + self.assertEqual(b.grid_info(), {}) + b.grid_configure() + self.assertEqual(b.grid_info()['in'], self.root) + self.assertEqual(b.grid_info()['column'], self._str(0)) + self.assertEqual(b.grid_info()['row'], self._str(0)) + b.grid_configure({'column': 1}, row=2) + self.assertEqual(b.grid_info()['column'], self._str(1)) + self.assertEqual(b.grid_info()['row'], self._str(2)) + + def test_grid_configure_column(self): + b = tkinter.Button(self.root) + with self.assertRaisesRegex(TclError, 'bad column value "-1": ' + 'must be a non-negative integer'): + b.grid_configure(column=-1) + b.grid_configure(column=2) + self.assertEqual(b.grid_info()['column'], self._str(2)) + + def test_grid_configure_columnspan(self): + b = tkinter.Button(self.root) + with self.assertRaisesRegex(TclError, 'bad columnspan value "0": ' + 'must be a positive integer'): + b.grid_configure(columnspan=0) + b.grid_configure(columnspan=2) + self.assertEqual(b.grid_info()['columnspan'], self._str(2)) + + def test_grid_configure_in(self): + f = tkinter.Frame(self.root) + b = tkinter.Button(self.root) + self.assertEqual(b.grid_info(), {}) + b.grid_configure() + self.assertEqual(b.grid_info()['in'], self.root) + b.grid_configure(in_=f) + self.assertEqual(b.grid_info()['in'], f) + b.grid_configure({'in': self.root}) + self.assertEqual(b.grid_info()['in'], self.root) + + def test_grid_configure_ipadx(self): + b = tkinter.Button(self.root) + with self.assertRaisesRegex(TclError, 'bad ipadx value "-1": ' + 'must be positive screen distance'): + b.grid_configure(ipadx=-1) + b.grid_configure(ipadx=1) + self.assertEqual(b.grid_info()['ipadx'], self._str(1)) + b.grid_configure(ipadx='.5c') + self.assertEqual(b.grid_info()['ipadx'], + self._str(round(pixels_conv('.5c') * self.scaling))) + + def test_grid_configure_ipady(self): + b = tkinter.Button(self.root) + with self.assertRaisesRegex(TclError, 'bad ipady value "-1": ' + 'must be positive screen distance'): + b.grid_configure(ipady=-1) + b.grid_configure(ipady=1) + self.assertEqual(b.grid_info()['ipady'], self._str(1)) + b.grid_configure(ipady='.5c') + self.assertEqual(b.grid_info()['ipady'], + self._str(round(pixels_conv('.5c') * self.scaling))) + + def test_grid_configure_padx(self): + b = tkinter.Button(self.root) + with self.assertRaisesRegex(TclError, 'bad pad value "-1": ' + 'must be positive screen distance'): + b.grid_configure(padx=-1) + b.grid_configure(padx=1) + self.assertEqual(b.grid_info()['padx'], self._str(1)) + b.grid_configure(padx=(10, 5)) + self.assertEqual(b.grid_info()['padx'], self._str((10, 5))) + b.grid_configure(padx='.5c') + self.assertEqual(b.grid_info()['padx'], + self._str(round(pixels_conv('.5c') * self.scaling))) + + def test_grid_configure_pady(self): + b = tkinter.Button(self.root) + with self.assertRaisesRegex(TclError, 'bad pad value "-1": ' + 'must be positive screen distance'): + b.grid_configure(pady=-1) + b.grid_configure(pady=1) + self.assertEqual(b.grid_info()['pady'], self._str(1)) + b.grid_configure(pady=(10, 5)) + self.assertEqual(b.grid_info()['pady'], self._str((10, 5))) + b.grid_configure(pady='.5c') + self.assertEqual(b.grid_info()['pady'], + self._str(round(pixels_conv('.5c') * self.scaling))) + + def test_grid_configure_row(self): + b = tkinter.Button(self.root) + with self.assertRaisesRegex(TclError, 'bad (row|grid) value "-1": ' + 'must be a non-negative integer'): + b.grid_configure(row=-1) + b.grid_configure(row=2) + self.assertEqual(b.grid_info()['row'], self._str(2)) + + def test_grid_configure_rownspan(self): + b = tkinter.Button(self.root) + with self.assertRaisesRegex(TclError, 'bad rowspan value "0": ' + 'must be a positive integer'): + b.grid_configure(rowspan=0) + b.grid_configure(rowspan=2) + self.assertEqual(b.grid_info()['rowspan'], self._str(2)) + + def test_grid_configure_sticky(self): + f = tkinter.Frame(self.root, bg='red') + with self.assertRaisesRegex(TclError, 'bad stickyness value "glue"'): + f.grid_configure(sticky='glue') + f.grid_configure(sticky='ne') + self.assertEqual(f.grid_info()['sticky'], 'ne') + f.grid_configure(sticky='n,s,e,w') + self.assertEqual(f.grid_info()['sticky'], 'nesw') + + def test_grid_columnconfigure(self): + with self.assertRaises(TypeError): + self.root.grid_columnconfigure() + self.assertEqual(self.root.grid_columnconfigure(0), + {'minsize': 0, 'pad': 0, 'uniform': None, 'weight': 0}) + with self.assertRaisesRegex(TclError, 'bad option "-foo"'): + self.root.grid_columnconfigure(0, 'foo') + self.root.grid_columnconfigure((0, 3), weight=2) + with self.assertRaisesRegex(TclError, + 'must specify a single element on retrieval'): + self.root.grid_columnconfigure((0, 3)) + b = tkinter.Button(self.root) + b.grid_configure(column=0, row=0) + self.root.grid_columnconfigure('all', weight=3) + with self.assertRaisesRegex(TclError, 'expected integer but got "all"'): + self.root.grid_columnconfigure('all') + self.assertEqual(self.root.grid_columnconfigure(0, 'weight'), 3) + self.assertEqual(self.root.grid_columnconfigure(3, 'weight'), 2) + self.assertEqual(self.root.grid_columnconfigure(265, 'weight'), 0) + self.root.grid_columnconfigure(b, weight=4) + self.assertEqual(self.root.grid_columnconfigure(0, 'weight'), 4) + + def test_grid_columnconfigure_minsize(self): + with self.assertRaisesRegex(TclError, + EXPECTED_SCREEN_DISTANCE_ERRMSG.format('foo')): + self.root.grid_columnconfigure(0, minsize='foo') + self.root.grid_columnconfigure(0, minsize=10) + self.assertEqual(self.root.grid_columnconfigure(0, 'minsize'), 10) + self.assertEqual(self.root.grid_columnconfigure(0)['minsize'], 10) + + def test_grid_columnconfigure_weight(self): + with self.assertRaisesRegex(TclError, 'expected integer but got "bad"'): + self.root.grid_columnconfigure(0, weight='bad') + with self.assertRaisesRegex(TclError, 'invalid arg "-weight": ' + 'should be non-negative'): + self.root.grid_columnconfigure(0, weight=-3) + self.root.grid_columnconfigure(0, weight=3) + self.assertEqual(self.root.grid_columnconfigure(0, 'weight'), 3) + self.assertEqual(self.root.grid_columnconfigure(0)['weight'], 3) + + def test_grid_columnconfigure_pad(self): + with self.assertRaisesRegex(TclError, + EXPECTED_SCREEN_DISTANCE_ERRMSG.format('foo')): + self.root.grid_columnconfigure(0, pad='foo') + with self.assertRaisesRegex(TclError, 'invalid arg "-pad": ' + 'should be non-negative'): + self.root.grid_columnconfigure(0, pad=-3) + self.root.grid_columnconfigure(0, pad=3) + self.assertEqual(self.root.grid_columnconfigure(0, 'pad'), 3) + self.assertEqual(self.root.grid_columnconfigure(0)['pad'], 3) + + def test_grid_columnconfigure_uniform(self): + self.root.grid_columnconfigure(0, uniform='foo') + self.assertEqual(self.root.grid_columnconfigure(0, 'uniform'), 'foo') + self.assertEqual(self.root.grid_columnconfigure(0)['uniform'], 'foo') + + def test_grid_rowconfigure(self): + with self.assertRaises(TypeError): + self.root.grid_rowconfigure() + self.assertEqual(self.root.grid_rowconfigure(0), + {'minsize': 0, 'pad': 0, 'uniform': None, 'weight': 0}) + with self.assertRaisesRegex(TclError, 'bad option "-foo"'): + self.root.grid_rowconfigure(0, 'foo') + self.root.grid_rowconfigure((0, 3), weight=2) + with self.assertRaisesRegex(TclError, + 'must specify a single element on retrieval'): + self.root.grid_rowconfigure((0, 3)) + b = tkinter.Button(self.root) + b.grid_configure(column=0, row=0) + self.root.grid_rowconfigure('all', weight=3) + with self.assertRaisesRegex(TclError, 'expected integer but got "all"'): + self.root.grid_rowconfigure('all') + self.assertEqual(self.root.grid_rowconfigure(0, 'weight'), 3) + self.assertEqual(self.root.grid_rowconfigure(3, 'weight'), 2) + self.assertEqual(self.root.grid_rowconfigure(265, 'weight'), 0) + self.root.grid_rowconfigure(b, weight=4) + self.assertEqual(self.root.grid_rowconfigure(0, 'weight'), 4) + + def test_grid_rowconfigure_minsize(self): + with self.assertRaisesRegex(TclError, + EXPECTED_SCREEN_DISTANCE_ERRMSG.format('foo')): + self.root.grid_rowconfigure(0, minsize='foo') + self.root.grid_rowconfigure(0, minsize=10) + self.assertEqual(self.root.grid_rowconfigure(0, 'minsize'), 10) + self.assertEqual(self.root.grid_rowconfigure(0)['minsize'], 10) + + def test_grid_rowconfigure_weight(self): + with self.assertRaisesRegex(TclError, 'expected integer but got "bad"'): + self.root.grid_rowconfigure(0, weight='bad') + with self.assertRaisesRegex(TclError, 'invalid arg "-weight": ' + 'should be non-negative'): + self.root.grid_rowconfigure(0, weight=-3) + self.root.grid_rowconfigure(0, weight=3) + self.assertEqual(self.root.grid_rowconfigure(0, 'weight'), 3) + self.assertEqual(self.root.grid_rowconfigure(0)['weight'], 3) + + def test_grid_rowconfigure_pad(self): + with self.assertRaisesRegex(TclError, + EXPECTED_SCREEN_DISTANCE_ERRMSG.format('foo')): + self.root.grid_rowconfigure(0, pad='foo') + with self.assertRaisesRegex(TclError, 'invalid arg "-pad": ' + 'should be non-negative'): + self.root.grid_rowconfigure(0, pad=-3) + self.root.grid_rowconfigure(0, pad=3) + self.assertEqual(self.root.grid_rowconfigure(0, 'pad'), 3) + self.assertEqual(self.root.grid_rowconfigure(0)['pad'], 3) + + def test_grid_rowconfigure_uniform(self): + self.root.grid_rowconfigure(0, uniform='foo') + self.assertEqual(self.root.grid_rowconfigure(0, 'uniform'), 'foo') + self.assertEqual(self.root.grid_rowconfigure(0)['uniform'], 'foo') + + def test_grid_forget(self): + b = tkinter.Button(self.root) + c = tkinter.Button(self.root) + b.grid_configure(row=2, column=2, rowspan=2, columnspan=2, + padx=3, pady=4, sticky='ns') + self.assertEqual(self.root.grid_content(), [b]) + b.grid_forget() + c.grid_forget() + self.assertEqual(self.root.grid_content(), []) + self.assertEqual(b.grid_info(), {}) + b.grid_configure(row=0, column=0) + info = b.grid_info() + self.assertEqual(info['row'], self._str(0)) + self.assertEqual(info['column'], self._str(0)) + self.assertEqual(info['rowspan'], self._str(1)) + self.assertEqual(info['columnspan'], self._str(1)) + self.assertEqual(info['padx'], self._str(0)) + self.assertEqual(info['pady'], self._str(0)) + self.assertEqual(info['sticky'], '') + + def test_grid_remove(self): + b = tkinter.Button(self.root) + c = tkinter.Button(self.root) + b.grid_configure(row=2, column=2, rowspan=2, columnspan=2, + padx=3, pady=4, sticky='ns') + self.assertEqual(self.root.grid_content(), [b]) + b.grid_remove() + c.grid_remove() + self.assertEqual(self.root.grid_content(), []) + self.assertEqual(b.grid_info(), {}) + b.grid_configure(row=0, column=0) + info = b.grid_info() + self.assertEqual(info['row'], self._str(0)) + self.assertEqual(info['column'], self._str(0)) + self.assertEqual(info['rowspan'], self._str(2)) + self.assertEqual(info['columnspan'], self._str(2)) + self.assertEqual(info['padx'], self._str(3)) + self.assertEqual(info['pady'], self._str(4)) + self.assertEqual(info['sticky'], 'ns') + + def test_grid_info(self): + b = tkinter.Button(self.root) + self.assertEqual(b.grid_info(), {}) + b.grid_configure(row=2, column=2, rowspan=2, columnspan=2, + padx=3, pady=4, sticky='ns') + info = b.grid_info() + self.assertIsInstance(info, dict) + self.assertEqual(info['in'], self.root) + self.assertEqual(info['row'], self._str(2)) + self.assertEqual(info['column'], self._str(2)) + self.assertEqual(info['rowspan'], self._str(2)) + self.assertEqual(info['columnspan'], self._str(2)) + self.assertEqual(info['padx'], self._str(3)) + self.assertEqual(info['pady'], self._str(4)) + self.assertEqual(info['sticky'], 'ns') + + def test_grid_anchor(self): + with self.assertRaisesRegex(TclError, 'bad anchor "x"'): + self.root.grid_anchor('x') + with self.assertRaisesRegex(TclError, 'ambiguous anchor ""'): + self.root.grid_anchor('') + with self.assertRaises(TypeError): + self.root.grid_anchor('se', 'nw') + self.root.grid_anchor('se') + self.assertEqual(self.root.tk.call('grid', 'anchor', self.root), 'se') + + def test_grid_bbox(self): + self.assertEqual(self.root.grid_bbox(), (0, 0, 0, 0)) + self.assertEqual(self.root.grid_bbox(0, 0), (0, 0, 0, 0)) + self.assertEqual(self.root.grid_bbox(0, 0, 1, 1), (0, 0, 0, 0)) + with self.assertRaisesRegex(TclError, 'expected integer but got "x"'): + self.root.grid_bbox('x', 0) + with self.assertRaisesRegex(TclError, 'expected integer but got "x"'): + self.root.grid_bbox(0, 'x') + with self.assertRaisesRegex(TclError, 'expected integer but got "x"'): + self.root.grid_bbox(0, 0, 'x', 0) + with self.assertRaisesRegex(TclError, 'expected integer but got "x"'): + self.root.grid_bbox(0, 0, 0, 'x') + with self.assertRaises(TypeError): + self.root.grid_bbox(0, 0, 0, 0, 0) + t = self.root + # de-maximize + t.wm_geometry('1x1+0+0') + t.wm_geometry('') + f1 = tkinter.Frame(t, width=75, height=75, bg='red') + f2 = tkinter.Frame(t, width=90, height=90, bg='blue') + f1.grid_configure(row=0, column=0) + f2.grid_configure(row=1, column=1) + self.root.update() + self.assertEqual(t.grid_bbox(), (0, 0, 165, 165)) + self.assertEqual(t.grid_bbox(0, 0), (0, 0, 75, 75)) + self.assertEqual(t.grid_bbox(0, 0, 1, 1), (0, 0, 165, 165)) + self.assertEqual(t.grid_bbox(1, 1), (75, 75, 90, 90)) + self.assertEqual(t.grid_bbox(10, 10, 0, 0), (0, 0, 165, 165)) + self.assertEqual(t.grid_bbox(-2, -2, -1, -1), (0, 0, 0, 0)) + self.assertEqual(t.grid_bbox(10, 10, 12, 12), (165, 165, 0, 0)) + + def test_grid_location(self): + with self.assertRaises(TypeError): + self.root.grid_location() + with self.assertRaises(TypeError): + self.root.grid_location(0) + with self.assertRaises(TypeError): + self.root.grid_location(0, 0, 0) + with self.assertRaisesRegex(TclError, + EXPECTED_SCREEN_DISTANCE_ERRMSG.format('x')): + self.root.grid_location('x', 'y') + with self.assertRaisesRegex(TclError, + EXPECTED_SCREEN_DISTANCE_ERRMSG.format('y')): + self.root.grid_location('1c', 'y') + t = self.root + # de-maximize + t.wm_geometry('1x1+0+0') + t.wm_geometry('') + f = tkinter.Frame(t, width=200, height=100, + highlightthickness=0, bg='red') + self.assertEqual(f.grid_location(10, 10), (-1, -1)) + f.grid_configure() + self.root.update() + self.assertEqual(t.grid_location(-10, -10), (-1, -1)) + self.assertEqual(t.grid_location(-10, 0), (-1, 0)) + self.assertEqual(t.grid_location(-1, 0), (-1, 0)) + self.assertEqual(t.grid_location(0, -10), (0, -1)) + self.assertEqual(t.grid_location(0, -1), (0, -1)) + self.assertEqual(t.grid_location(0, 0), (0, 0)) + self.assertEqual(t.grid_location(200, 0), (0, 0)) + self.assertEqual(t.grid_location(201, 0), (1, 0)) + self.assertEqual(t.grid_location(0, 100), (0, 0)) + self.assertEqual(t.grid_location(0, 101), (0, 1)) + self.assertEqual(t.grid_location(201, 101), (1, 1)) + + def test_grid_propagate(self): + self.assertEqual(self.root.grid_propagate(), True) + with self.assertRaises(TypeError): + self.root.grid_propagate(False, False) + self.root.grid_propagate(False) + self.assertFalse(self.root.grid_propagate()) + f = tkinter.Frame(self.root, width=100, height=100, bg='red') + f.grid_configure(row=0, column=0) + self.root.update() + self.assertEqual(f.winfo_width(), 100) + self.assertEqual(f.winfo_height(), 100) + f.grid_propagate(False) + g = tkinter.Frame(self.root, width=75, height=85, bg='green') + g.grid_configure(in_=f, row=0, column=0) + self.root.update() + self.assertEqual(f.winfo_width(), 100) + self.assertEqual(f.winfo_height(), 100) + f.grid_propagate(True) + self.root.update() + self.assertEqual(f.winfo_width(), 75) + self.assertEqual(f.winfo_height(), 85) + + def test_grid_size(self): + with self.assertRaises(TypeError): + self.root.grid_size(0) + self.assertEqual(self.root.grid_size(), (0, 0)) + f = tkinter.Scale(self.root) + f.grid_configure(row=0, column=0) + self.assertEqual(self.root.grid_size(), (1, 1)) + f.grid_configure(row=4, column=5) + self.assertEqual(self.root.grid_size(), (6, 5)) + + def test_grid_content(self): + self.assertEqual(self.root.grid_content(), []) + a = tkinter.Label(self.root) + a.grid_configure(row=0, column=1) + b = tkinter.Label(self.root) + b.grid_configure(row=1, column=0) + c = tkinter.Label(self.root) + c.grid_configure(row=1, column=1) + d = tkinter.Label(self.root) + d.grid_configure(row=1, column=1) + self.assertEqual(self.root.grid_content(), [d, c, b, a]) + self.assertEqual(self.root.grid_content(row=0), [a]) + self.assertEqual(self.root.grid_content(row=1), [d, c, b]) + self.assertEqual(self.root.grid_content(column=0), [b]) + self.assertEqual(self.root.grid_content(column=1), [d, c, a]) + self.assertEqual(self.root.grid_content(row=1, column=1), [d, c]) + + def test_grid_slaves(self): + self.assertEqual(self.root.grid_slaves(), []) + a = tkinter.Label(self.root) + a.grid_configure(row=0, column=1) + b = tkinter.Label(self.root) + b.grid_configure(row=1, column=0) + c = tkinter.Label(self.root) + c.grid_configure(row=1, column=1) + d = tkinter.Label(self.root) + d.grid_configure(row=1, column=1) + self.assertEqual(self.root.grid_slaves(), [d, c, b, a]) + self.assertEqual(self.root.grid_slaves(row=0), [a]) + self.assertEqual(self.root.grid_slaves(row=1), [d, c, b]) + self.assertEqual(self.root.grid_slaves(column=0), [b]) + self.assertEqual(self.root.grid_slaves(column=1), [d, c, a]) + self.assertEqual(self.root.grid_slaves(row=1, column=1), [d, c]) + + def test_grid_short_aliases(self): + # columnconfigure, rowconfigure, size, anchor and bbox are aliases of + # the corresponding grid_* methods (Misc precedes Pack, Place and Grid + # in the method resolution order). + root = self.root + self.assertEqual(root.columnconfigure, root.grid_columnconfigure) + self.assertEqual(root.rowconfigure, root.grid_rowconfigure) + self.assertEqual(root.size, root.grid_size) + self.assertEqual(root.anchor, root.grid_anchor) + self.assertEqual(root.bbox, root.grid_bbox) + + self.assertEqual(root.size(), (0, 0)) + b = tkinter.Button(root) + b.grid_configure(column=2, row=3) + self.assertEqual(root.size(), (3, 4)) + + root.columnconfigure(0, weight=2) + self.assertEqual(root.grid_columnconfigure(0, 'weight'), 2) + root.rowconfigure(0, weight=3) + self.assertEqual(root.grid_rowconfigure(0, 'weight'), 3) + + root.anchor('se') + self.assertEqual(root.tk.call('grid', 'anchor', root), 'se') + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_tkinter/test_images.py b/Lib/test/test_tkinter/test_images.py new file mode 100644 index 00000000000..db399d66c35 --- /dev/null +++ b/Lib/test/test_tkinter/test_images.py @@ -0,0 +1,675 @@ +import collections.abc +import unittest +import tkinter +from test import support +from test.support import os_helper +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import AbstractTkTest, AbstractDefaultRootTest, requires_tk + +support.requires('gui') + + +class MiscTest(AbstractTkTest, unittest.TestCase): + + def test_image_types(self): + image_types = self.root.image_types() + self.assertIsInstance(image_types, tuple) + self.assertIn('photo', image_types) + self.assertIn('bitmap', image_types) + + def test_image_names(self): + image_names = self.root.image_names() + self.assertIsInstance(image_names, tuple) + + +class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): + + def test_image_types(self): + self.assertRaises(RuntimeError, tkinter.image_types) + root = tkinter.Tk() + image_types = tkinter.image_types() + self.assertIsInstance(image_types, tuple) + self.assertIn('photo', image_types) + self.assertIn('bitmap', image_types) + root.destroy() + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, tkinter.image_types) + + def test_image_names(self): + self.assertRaises(RuntimeError, tkinter.image_names) + root = tkinter.Tk() + image_names = tkinter.image_names() + self.assertIsInstance(image_names, tuple) + root.destroy() + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, tkinter.image_names) + + def test_image_create_bitmap(self): + self.assertRaises(RuntimeError, tkinter.BitmapImage) + root = tkinter.Tk() + image = tkinter.BitmapImage() + self.assertIn(image.name, tkinter.image_names()) + root.destroy() + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, tkinter.BitmapImage) + + def test_image_create_photo(self): + self.assertRaises(RuntimeError, tkinter.PhotoImage) + root = tkinter.Tk() + image = tkinter.PhotoImage() + self.assertIn(image.name, tkinter.image_names()) + root.destroy() + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, tkinter.PhotoImage) + + +class BaseImageTest: + def create(self): + return self.image_class('::img::test', master=self.root, + file=self.testfile) + + def test_bug_100814(self): + # gh-100814: Passing a callable option value causes AttributeError. + with self.assertRaises(tkinter.TclError): + self.image_class('::img::test', master=self.root, spam=print) + image = self.image_class('::img::test', master=self.root) + with self.assertRaises(tkinter.TclError): + image.configure(spam=print) + + def test_iterable_protocol(self): + image = self.create() + self.assertNotIsSubclass(self.image_class, collections.abc.Iterable) + self.assertNotIsSubclass(self.image_class, collections.abc.Container) + self.assertNotIsInstance(image, collections.abc.Iterable) + self.assertNotIsInstance(image, collections.abc.Container) + with self.assertRaisesRegex(TypeError, 'is not iterable'): + iter(image) + with self.assertRaisesRegex(TypeError, 'is not a container or iterable'): + image in image + + +class BitmapImageTest(BaseImageTest, AbstractTkTest, unittest.TestCase): + image_class = tkinter.BitmapImage + + @classmethod + def setUpClass(cls): + AbstractTkTest.setUpClass.__func__(cls) + cls.testfile = support.findfile('python.xbm', subdir='tkinterdata') + + def test_create_from_file(self): + image = tkinter.BitmapImage('::img::test', master=self.root, + foreground='yellow', background='blue', + file=self.testfile) + self.assertEqual(str(image), '::img::test') + self.assertEqual(image.type(), 'bitmap') + self.assertEqual(image.width(), 16) + self.assertEqual(image.height(), 16) + self.assertIn('::img::test', self.root.image_names()) + del image + support.gc_collect() # For PyPy or other GCs. + self.assertNotIn('::img::test', self.root.image_names()) + + def test_create_from_data(self): + with open(self.testfile, 'rb') as f: + data = f.read() + image = tkinter.BitmapImage('::img::test', master=self.root, + foreground='yellow', background='blue', + data=data) + self.assertEqual(str(image), '::img::test') + self.assertEqual(image.type(), 'bitmap') + self.assertEqual(image.width(), 16) + self.assertEqual(image.height(), 16) + self.assertIn('::img::test', self.root.image_names()) + del image + support.gc_collect() # For PyPy or other GCs. + self.assertNotIn('::img::test', self.root.image_names()) + + def assertEqualStrList(self, actual, expected): + self.assertIsInstance(actual, str) + self.assertEqual(self.root.splitlist(actual), expected) + + def test_configure_data(self): + image = tkinter.BitmapImage('::img::test', master=self.root) + self.assertEqual(image['data'], '-data {} {} {} {}') + with open(self.testfile, 'rb') as f: + data = f.read() + image.configure(data=data) + self.assertEqualStrList(image['data'], + ('-data', '', '', '', data.decode('ascii'))) + self.assertEqual(image.width(), 16) + self.assertEqual(image.height(), 16) + + self.assertEqual(image['maskdata'], '-maskdata {} {} {} {}') + image.configure(maskdata=data) + self.assertEqualStrList(image['maskdata'], + ('-maskdata', '', '', '', data.decode('ascii'))) + + def test_configure_file(self): + image = tkinter.BitmapImage('::img::test', master=self.root) + self.assertEqual(image['file'], '-file {} {} {} {}') + image.configure(file=self.testfile) + self.assertEqualStrList(image['file'], + ('-file', '', '', '',self.testfile)) + self.assertEqual(image.width(), 16) + self.assertEqual(image.height(), 16) + + self.assertEqual(image['maskfile'], '-maskfile {} {} {} {}') + image.configure(maskfile=self.testfile) + self.assertEqualStrList(image['maskfile'], + ('-maskfile', '', '', '', self.testfile)) + + def test_configure_background(self): + image = tkinter.BitmapImage('::img::test', master=self.root) + self.assertEqual(image['background'], '-background {} {} {} {}') + image.configure(background='blue') + self.assertEqual(image['background'], '-background {} {} {} blue') + + def test_configure_foreground(self): + image = tkinter.BitmapImage('::img::test', master=self.root) + self.assertEqual(image['foreground'], + '-foreground {} {} #000000 #000000') + image.configure(foreground='yellow') + self.assertEqual(image['foreground'], + '-foreground {} {} #000000 yellow') + + +class PhotoImageTest(BaseImageTest, AbstractTkTest, unittest.TestCase): + image_class = tkinter.PhotoImage + + @classmethod + def setUpClass(cls): + AbstractTkTest.setUpClass.__func__(cls) + cls.testfile = support.findfile('python.gif', subdir='tkinterdata') + + def colorlist(self, *args): + if tkinter.TkVersion >= 8.6 and self.wantobjects: + return args + else: + return tkinter._join(args) + + def check_create_from_file(self, ext): + testfile = support.findfile('python.' + ext, subdir='tkinterdata') + image = tkinter.PhotoImage('::img::test', master=self.root, + file=testfile) + self.assertEqual(str(image), '::img::test') + self.assertEqual(image.type(), 'photo') + self.assertEqual(image.width(), 16) + self.assertEqual(image.height(), 16) + self.assertEqual(image['data'], '') + self.assertEqual(image['file'], testfile) + self.assertIn('::img::test', self.root.image_names()) + del image + support.gc_collect() # For PyPy or other GCs. + self.assertNotIn('::img::test', self.root.image_names()) + + def check_create_from_data(self, ext): + testfile = support.findfile('python.' + ext, subdir='tkinterdata') + with open(testfile, 'rb') as f: + data = f.read() + image = tkinter.PhotoImage('::img::test', master=self.root, + data=data) + self.assertEqual(str(image), '::img::test') + self.assertEqual(image.type(), 'photo') + self.assertEqual(image.width(), 16) + self.assertEqual(image.height(), 16) + self.assertEqual(image['data'], data if self.wantobjects + else data.decode('latin1')) + self.assertEqual(image['file'], '') + self.assertIn('::img::test', self.root.image_names()) + del image + support.gc_collect() # For PyPy or other GCs. + self.assertNotIn('::img::test', self.root.image_names()) + + def test_create_from_ppm_file(self): + self.check_create_from_file('ppm') + + def test_create_from_ppm_data(self): + self.check_create_from_data('ppm') + + def test_create_from_pgm_file(self): + self.check_create_from_file('pgm') + + def test_create_from_pgm_data(self): + self.check_create_from_data('pgm') + + def test_create_from_gif_file(self): + self.check_create_from_file('gif') + + def test_create_from_gif_data(self): + self.check_create_from_data('gif') + + @requires_tk(8, 6) + def test_create_from_png_file(self): + self.check_create_from_file('png') + + @requires_tk(8, 6) + def test_create_from_png_data(self): + self.check_create_from_data('png') + + def test_configure_data(self): + image = tkinter.PhotoImage('::img::test', master=self.root) + self.assertEqual(image['data'], '') + with open(self.testfile, 'rb') as f: + data = f.read() + image.configure(data=data) + self.assertEqual(image['data'], data if self.wantobjects + else data.decode('latin1')) + self.assertEqual(image.width(), 16) + self.assertEqual(image.height(), 16) + + def test_configure_format(self): + image = tkinter.PhotoImage('::img::test', master=self.root) + self.assertEqual(image['format'], '') + image.configure(file=self.testfile, format='gif') + self.assertEqual(image['format'], ('gif',) if self.wantobjects + else 'gif') + self.assertEqual(image.width(), 16) + self.assertEqual(image.height(), 16) + + def test_configure_file(self): + image = tkinter.PhotoImage('::img::test', master=self.root) + self.assertEqual(image['file'], '') + image.configure(file=self.testfile) + self.assertEqual(image['file'], self.testfile) + self.assertEqual(image.width(), 16) + self.assertEqual(image.height(), 16) + + def test_configure_gamma(self): + image = tkinter.PhotoImage('::img::test', master=self.root) + self.assertEqual(image['gamma'], '1.0') + image.configure(gamma=2.0) + self.assertEqual(image['gamma'], '2.0') + + def test_configure_width_height(self): + image = tkinter.PhotoImage('::img::test', master=self.root) + self.assertEqual(image['width'], '0') + self.assertEqual(image['height'], '0') + image.configure(width=20) + image.configure(height=10) + self.assertEqual(image['width'], '20') + self.assertEqual(image['height'], '10') + self.assertEqual(image.cget('width'), image['width']) + self.assertEqual(image.cget('height'), image['height']) + self.assertRaises(TypeError, image.cget) + self.assertRaises(TypeError, image.cget, 'width', 'height') + self.assertEqual(image.config, image.configure) + self.assertEqual(image.width(), 20) + self.assertEqual(image.height(), 10) + + def test_configure_palette(self): + image = tkinter.PhotoImage('::img::test', master=self.root) + self.assertEqual(image['palette'], '') + image.configure(palette=256) + self.assertEqual(image['palette'], '256') + image.configure(palette='3/4/2') + self.assertEqual(image['palette'], '3/4/2') + + def test_blank(self): + image = self.create() + image.blank() + self.assertEqual(image.width(), 16) + self.assertEqual(image.height(), 16) + self.assertEqual(image.get(4, 6), self.colorlist(0, 0, 0)) + + def test_copy(self): + image = self.create() + image2 = image.copy() + self.assertEqual(image2.width(), 16) + self.assertEqual(image2.height(), 16) + self.assertEqual(image2.get(4, 6), image.get(4, 6)) + + image2 = image.copy(from_coords=(2, 3, 14, 11)) + self.assertEqual(image2.width(), 12) + self.assertEqual(image2.height(), 8) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(11, 7), image.get(13, 10)) + self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3)) + + image2 = image.copy(from_coords=(2, 3, 14, 11), zoom=2) + self.assertEqual(image2.width(), 24) + self.assertEqual(image2.height(), 16) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(23, 15), image.get(13, 10)) + self.assertEqual(image2.get(2*2, 4*2), image.get(2+2, 4+3)) + self.assertEqual(image2.get(2*2+1, 4*2+1), image.get(6+2, 2+3)) + + image2 = image.copy(from_coords=(2, 3, 14, 11), subsample=2) + self.assertEqual(image2.width(), 6) + self.assertEqual(image2.height(), 4) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(5, 3), image.get(12, 9)) + self.assertEqual(image2.get(3, 2), image.get(3*2+2, 2*2+3)) + + image2 = image.copy(from_coords=(2, 3, 14, 11), subsample=2, zoom=3) + self.assertEqual(image2.width(), 18) + self.assertEqual(image2.height(), 12) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(17, 11), image.get(12, 9)) + self.assertEqual(image2.get(1*3, 2*3), image.get(1*2+2, 2*2+3)) + self.assertEqual(image2.get(1*3+2, 2*3+2), image.get(1*2+2, 2*2+3)) + + def test_subsample(self): + image = self.create() + image2 = image.subsample(2, 3) + self.assertEqual(image2.width(), 8) + self.assertEqual(image2.height(), 6) + self.assertEqual(image2.get(2, 2), image.get(4, 6)) + + image2 = image.subsample(2) + self.assertEqual(image2.width(), 8) + self.assertEqual(image2.height(), 8) + self.assertEqual(image2.get(2, 3), image.get(4, 6)) + + image2 = image.subsample(2, from_coords=(2, 3, 14, 11)) + self.assertEqual(image2.width(), 6) + self.assertEqual(image2.height(), 4) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(5, 3), image.get(12, 9)) + self.assertEqual(image2.get(1, 2), image.get(1*2+2, 2*2+3)) + + def test_zoom(self): + image = self.create() + image2 = image.zoom(2, 3) + self.assertEqual(image2.width(), 32) + self.assertEqual(image2.height(), 48) + self.assertEqual(image2.get(8, 18), image.get(4, 6)) + self.assertEqual(image2.get(9, 20), image.get(4, 6)) + + image2 = image.zoom(2) + self.assertEqual(image2.width(), 32) + self.assertEqual(image2.height(), 32) + self.assertEqual(image2.get(8, 12), image.get(4, 6)) + self.assertEqual(image2.get(9, 13), image.get(4, 6)) + + image2 = image.zoom(2, from_coords=(2, 3, 14, 11)) + self.assertEqual(image2.width(), 24) + self.assertEqual(image2.height(), 16) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(23, 15), image.get(13, 10)) + self.assertEqual(image2.get(2*2, 4*2), image.get(2+2, 4+3)) + self.assertEqual(image2.get(2*2+1, 4*2+1), image.get(6+2, 2+3)) + + def test_copy_replace(self): + image = self.create() + image2 = tkinter.PhotoImage(master=self.root) + image2.copy_replace(image) + self.assertEqual(image2.width(), 16) + self.assertEqual(image2.height(), 16) + self.assertEqual(image2.get(4, 6), image.get(4, 6)) + + image2 = tkinter.PhotoImage(master=self.root) + image2.copy_replace(image, from_coords=(2, 3, 14, 11)) + self.assertEqual(image2.width(), 12) + self.assertEqual(image2.height(), 8) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(11, 7), image.get(13, 10)) + self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3)) + + image2 = tkinter.PhotoImage(master=self.root) + image2.copy_replace(image) + image2.copy_replace(image, from_coords=(2, 3, 14, 11), shrink=True) + self.assertEqual(image2.width(), 12) + self.assertEqual(image2.height(), 8) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(11, 7), image.get(13, 10)) + self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3)) + + image2 = tkinter.PhotoImage(master=self.root) + image2.copy_replace(image, from_coords=(2, 3, 14, 11), to=(3, 6)) + self.assertEqual(image2.width(), 15) + self.assertEqual(image2.height(), 14) + self.assertEqual(image2.get(0+3, 0+6), image.get(2, 3)) + self.assertEqual(image2.get(11+3, 7+6), image.get(13, 10)) + self.assertEqual(image2.get(2+3, 4+6), image.get(2+2, 4+3)) + + image2 = tkinter.PhotoImage(master=self.root) + image2.copy_replace(image, from_coords=(2, 3, 14, 11), to=(0, 0, 100, 50)) + self.assertEqual(image2.width(), 100) + self.assertEqual(image2.height(), 50) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(11, 7), image.get(13, 10)) + self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3)) + self.assertEqual(image2.get(2+12, 4+8), image.get(2+2, 4+3)) + self.assertEqual(image2.get(2+12*2, 4), image.get(2+2, 4+3)) + self.assertEqual(image2.get(2, 4+8*3), image.get(2+2, 4+3)) + + image2 = tkinter.PhotoImage(master=self.root) + image2.copy_replace(image, from_coords=(2, 3, 14, 11), zoom=2) + self.assertEqual(image2.width(), 24) + self.assertEqual(image2.height(), 16) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(23, 15), image.get(13, 10)) + self.assertEqual(image2.get(2*2, 4*2), image.get(2+2, 4+3)) + self.assertEqual(image2.get(2*2+1, 4*2+1), image.get(6+2, 2+3)) + + image2 = tkinter.PhotoImage(master=self.root) + image2.copy_replace(image, from_coords=(2, 3, 14, 11), subsample=2) + self.assertEqual(image2.width(), 6) + self.assertEqual(image2.height(), 4) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(5, 3), image.get(12, 9)) + self.assertEqual(image2.get(1, 2), image.get(1*2+2, 2*2+3)) + + image2 = tkinter.PhotoImage(master=self.root) + image2.copy_replace(image, from_coords=(2, 3, 14, 11), subsample=2, zoom=3) + self.assertEqual(image2.width(), 18) + self.assertEqual(image2.height(), 12) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(17, 11), image.get(12, 9)) + self.assertEqual(image2.get(3*3, 2*3), image.get(3*2+2, 2*2+3)) + self.assertEqual(image2.get(3*3+2, 2*3+2), image.get(3*2+2, 2*2+3)) + self.assertEqual(image2.get(1*3, 2*3), image.get(1*2+2, 2*2+3)) + self.assertEqual(image2.get(1*3+2, 2*3+2), image.get(1*2+2, 2*2+3)) + + def checkImgTrans(self, image, expected): + actual = {(x, y) + for x in range(image.width()) + for y in range(image.height()) + if image.transparency_get(x, y)} + self.assertEqual(actual, expected) + + def test_copy_replace_compositingrule(self): + image1 = tkinter.PhotoImage(master=self.root, width=2, height=2) + image1.blank() + image1.put('black', to=(0, 0, 2, 2)) + image1.transparency_set(0, 0, True) + + # default compositingrule + image2 = tkinter.PhotoImage(master=self.root, width=3, height=3) + image2.blank() + image2.put('white', to=(0, 0, 2, 2)) + image2.copy_replace(image1, to=(1, 1)) + self.checkImgTrans(image2, {(0, 2), (2, 0)}) + + image3 = tkinter.PhotoImage(master=self.root, width=3, height=3) + image3.blank() + image3.put('white', to=(0, 0, 2, 2)) + image3.copy_replace(image1, to=(1, 1), compositingrule='overlay') + self.checkImgTrans(image3, {(0, 2), (2, 0)}) + + image4 = tkinter.PhotoImage(master=self.root, width=3, height=3) + image4.blank() + image4.put('white', to=(0, 0, 2, 2)) + image4.copy_replace(image1, to=(1, 1), compositingrule='set') + self.checkImgTrans(image4, {(0, 2), (1, 1), (2, 0)}) + + def test_put(self): + image = self.create() + image.put('{red green} {blue yellow}', to=(4, 6)) + self.assertEqual(image.get(4, 6), self.colorlist(255, 0, 0)) + self.assertEqual(image.get(5, 6), + self.colorlist(0, 128 if tkinter.TkVersion >= 8.6 + else 255, 0)) + self.assertEqual(image.get(4, 7), self.colorlist(0, 0, 255)) + self.assertEqual(image.get(5, 7), self.colorlist(255, 255, 0)) + + image.put((('#f00', '#00ff00'), ('#000000fff', '#ffffffff0000'))) + self.assertEqual(image.get(0, 0), self.colorlist(255, 0, 0)) + self.assertEqual(image.get(1, 0), self.colorlist(0, 255, 0)) + self.assertEqual(image.get(0, 1), self.colorlist(0, 0, 255)) + self.assertEqual(image.get(1, 1), self.colorlist(255, 255, 0)) + + def test_get(self): + image = self.create() + self.assertEqual(image.get(4, 6), self.colorlist(62, 116, 162)) + self.assertEqual(image.get(0, 0), self.colorlist(0, 0, 0)) + self.assertEqual(image.get(15, 15), self.colorlist(0, 0, 0)) + self.assertRaises(tkinter.TclError, image.get, -1, 0) + self.assertRaises(tkinter.TclError, image.get, 0, -1) + self.assertRaises(tkinter.TclError, image.get, 16, 15) + self.assertRaises(tkinter.TclError, image.get, 15, 16) + + def test_read(self): + # Due to the Tk bug https://core.tcl-lang.org/tk/tktview/1576528 + # the -from option does not work correctly for GIF and PNG files. + # Use the PPM file for this test. + testfile = support.findfile('python.ppm', subdir='tkinterdata') + image = tkinter.PhotoImage(master=self.root, file=testfile) + + image2 = tkinter.PhotoImage(master=self.root) + image2.read(testfile) + self.assertEqual(image2.type(), 'photo') + self.assertEqual(image2.width(), 16) + self.assertEqual(image2.height(), 16) + self.assertEqual(image2.get(0, 0), image.get(0, 0)) + self.assertEqual(image2.get(4, 6), image.get(4, 6)) + + self.assertRaises(tkinter.TclError, image2.read, self.testfile, 'ppm') + + image2 = tkinter.PhotoImage(master=self.root) + image2.read(testfile, from_coords=(2, 3, 14, 11)) + self.assertEqual(image2.width(), 12) + self.assertEqual(image2.height(), 8) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(11, 7), image.get(13, 10)) + self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3)) + + image2 = tkinter.PhotoImage(master=self.root, file=testfile) + self.assertEqual(image2.width(), 16) + self.assertEqual(image2.height(), 16) + image2.read(testfile, from_coords=(2, 3, 14, 11), shrink=True) + self.assertEqual(image2.width(), 12) + self.assertEqual(image2.height(), 8) + self.assertEqual(image2.get(0, 0), image.get(2, 3)) + self.assertEqual(image2.get(11, 7), image.get(13, 10)) + self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3)) + + image2 = tkinter.PhotoImage(master=self.root) + image2.read(testfile, from_coords=(2, 3, 14, 11), to=(3, 6)) + self.assertEqual(image2.type(), 'photo') + self.assertEqual(image2.width(), 15) + self.assertEqual(image2.height(), 14) + self.assertEqual(image2.get(0+3, 0+6), image.get(2, 3)) + self.assertEqual(image2.get(11+3, 7+6), image.get(13, 10)) + self.assertEqual(image2.get(2+3, 4+6), image.get(2+2, 4+3)) + + def test_write(self): + filename = os_helper.TESTFN + import locale + if locale.getlocale()[0] is None: + # Tcl uses Latin1 in the C locale + filename = os_helper.TESTFN_ASCII + image = self.create() + self.addCleanup(os_helper.unlink, filename) + + image.write(filename) + image2 = tkinter.PhotoImage('::img::test2', master=self.root, + format='ppm', file=filename) + self.assertEqual(str(image2), '::img::test2') + self.assertEqual(image2.type(), 'photo') + self.assertEqual(image2.width(), 16) + self.assertEqual(image2.height(), 16) + self.assertEqual(image2.get(0, 0), image.get(0, 0)) + self.assertEqual(image2.get(4, 6), image.get(4, 6)) + + image.write(filename, format='gif', from_coords=(4, 6, 6, 9)) + image3 = tkinter.PhotoImage('::img::test3', master=self.root, + format='gif', file=filename) + self.assertEqual(str(image3), '::img::test3') + self.assertEqual(image3.type(), 'photo') + self.assertEqual(image3.width(), 2) + self.assertEqual(image3.height(), 3) + self.assertEqual(image3.get(0, 0), image.get(4, 6)) + self.assertEqual(image3.get(1, 2), image.get(5, 8)) + + image.write(filename, background='#ff0000') + image4 = tkinter.PhotoImage('::img::test4', master=self.root, + format='ppm', file=filename) + self.assertEqual(image4.get(0, 0), (255, 0, 0) if self.wantobjects else '255 0 0') + self.assertEqual(image4.get(4, 6), image.get(4, 6)) + + image.write(filename, grayscale=True) + image5 = tkinter.PhotoImage('::img::test5', master=self.root, + format='ppm', file=filename) + c = image5.get(4, 6) + if not self.wantobjects: + c = c.split() + self.assertTrue(c[0] == c[1] == c[2], c) + + def test_data(self): + image = self.create() + + data = image.data() + self.assertIsInstance(data, tuple) + for row in data: + self.assertIsInstance(row, str) + c = image.get(4, 6) + if not self.wantobjects: + c = tuple(map(int, c.split())) + self.assertEqual(data[6].split()[4], '#%02x%02x%02x' % c) + + data = image.data('ppm') + image2 = tkinter.PhotoImage('::img::test2', master=self.root, + format='ppm', data=data) + self.assertEqual(str(image2), '::img::test2') + self.assertEqual(image2.type(), 'photo') + self.assertEqual(image2.width(), 16) + self.assertEqual(image2.height(), 16) + self.assertEqual(image2.get(0, 0), image.get(0, 0)) + self.assertEqual(image2.get(4, 6), image.get(4, 6)) + + data = image.data(format='gif', from_coords=(4, 6, 6, 9)) + image3 = tkinter.PhotoImage('::img::test3', master=self.root, + format='gif', data=data) + self.assertEqual(str(image3), '::img::test3') + self.assertEqual(image3.type(), 'photo') + self.assertEqual(image3.width(), 2) + self.assertEqual(image3.height(), 3) + self.assertEqual(image3.get(0, 0), image.get(4, 6)) + self.assertEqual(image3.get(1, 2), image.get(5, 8)) + + data = image.data('ppm', background='#ff0000') + image4 = tkinter.PhotoImage('::img::test4', master=self.root, + format='ppm', data=data) + self.assertEqual(image4.get(0, 0), (255, 0, 0) if self.wantobjects else '255 0 0') + self.assertEqual(image4.get(4, 6), image.get(4, 6)) + + data = image.data('ppm', grayscale=True) + image5 = tkinter.PhotoImage('::img::test5', master=self.root, + format='ppm', data=data) + c = image5.get(4, 6) + if not self.wantobjects: + c = c.split() + self.assertTrue(c[0] == c[1] == c[2], c) + + + def test_transparency(self): + image = self.create() + self.assertIs(image.transparency_get(0, 0), True) + self.assertIs(image.transparency_get(4, 6), False) + image.transparency_set(4, 6, True) + self.assertIs(image.transparency_get(4, 6), True) + image.transparency_set(4, 6, False) + self.assertIs(image.transparency_get(4, 6), False) + self.assertRaises(tkinter.TclError, image.transparency_get, -1, 0) + self.assertRaises(tkinter.TclError, image.transparency_get, 16, 0) + self.assertRaises(tkinter.TclError, image.transparency_set, -1, 0, True) + self.assertRaises(tkinter.TclError, image.transparency_set, 16, 0, True) + self.assertRaises(TypeError, image.transparency_get, 0) + self.assertRaises(TypeError, image.transparency_get, 0, 0, 0) + self.assertRaises(TypeError, image.transparency_set, 0, 0) + self.assertRaises(TypeError, image.transparency_set, 0, 0, True, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tkinter/test_loadtk.py b/Lib/test/test_tkinter/test_loadtk.py new file mode 100644 index 00000000000..7cff654621e --- /dev/null +++ b/Lib/test/test_tkinter/test_loadtk.py @@ -0,0 +1,47 @@ +import os +import sys +import unittest +import test.support as test_support +from test.support import os_helper +from test.test_tkinter.support import setUpModule # noqa: F401 +from tkinter import Tcl, TclError + +test_support.requires('gui') + +class TkLoadTest(unittest.TestCase): + + @unittest.skipIf('DISPLAY' not in os.environ, 'No $DISPLAY set.') + def testLoadTk(self): + tcl = Tcl() + self.assertRaises(TclError,tcl.winfo_geometry) + tcl.loadtk() + self.assertEqual('1x1+0+0', tcl.winfo_geometry()) + tcl.destroy() + + def testLoadTkFailure(self): + old_display = None + if sys.platform.startswith(('win', 'darwin', 'cygwin')): + # no failure possible on windows? + + # XXX Maybe on tk older than 8.4.13 it would be possible, + # see tkinter.h. + return + with os_helper.EnvironmentVarGuard() as env: + if 'DISPLAY' in os.environ: + del env['DISPLAY'] + # on some platforms, deleting environment variables + # doesn't actually carry through to the process level + # because they don't support unsetenv + # If that's the case, abort. + with os.popen('echo $DISPLAY') as pipe: + display = pipe.read().strip() + if display: + return + + tcl = Tcl() + self.assertRaises(TclError, tcl.winfo_geometry) + self.assertRaises(TclError, tcl.loadtk) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tkinter/test_messagebox.py b/Lib/test/test_tkinter/test_messagebox.py new file mode 100644 index 00000000000..f29f0c7ba59 --- /dev/null +++ b/Lib/test/test_tkinter/test_messagebox.py @@ -0,0 +1,37 @@ +import unittest +import tkinter +from test.support import requires, swap_attr +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import AbstractDefaultRootTest +from tkinter.commondialog import Dialog +from tkinter.messagebox import showinfo + +requires('gui') + + +class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): + + def test_showinfo(self): + def test_callback(dialog, master): + nonlocal ismapped + master.update() + ismapped = master.winfo_ismapped() + raise ZeroDivisionError + + with swap_attr(Dialog, '_test_callback', test_callback): + ismapped = None + self.assertRaises(ZeroDivisionError, showinfo, "Spam", "Egg Information") + self.assertEqual(ismapped, False) + + root = tkinter.Tk() + ismapped = None + self.assertRaises(ZeroDivisionError, showinfo, "Spam", "Egg Information") + self.assertEqual(ismapped, True) + root.destroy() + + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, showinfo, "Spam", "Egg Information") + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py new file mode 100644 index 00000000000..1fe092a152e --- /dev/null +++ b/Lib/test/test_tkinter/test_misc.py @@ -0,0 +1,1982 @@ +import collections.abc +import functools +import os +import platform +import sys +import textwrap +import unittest +import weakref +import tkinter +from tkinter import TclError +import enum +from test import support +from test.support import os_helper +from test.support.script_helper import assert_python_ok +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest, + requires_tk, get_tk_patchlevel, + tcl_version) + +support.requires('gui') + +class MiscTest(AbstractTkTest, unittest.TestCase): + + def test_all(self): + self.assertIn("Widget", tkinter.__all__) + # Check that variables from tkinter.constants are also in tkinter.__all__ + self.assertIn("CASCADE", tkinter.__all__) + self.assertIsNotNone(tkinter.CASCADE) + # Check that sys, re, and constants are not in tkinter.__all__ + self.assertNotIn("re", tkinter.__all__) + self.assertNotIn("sys", tkinter.__all__) + self.assertNotIn("constants", tkinter.__all__) + # Check that an underscored functions is not in tkinter.__all__ + self.assertNotIn("_tkerror", tkinter.__all__) + # Check that wantobjects is not in tkinter.__all__ + self.assertNotIn("wantobjects", tkinter.__all__) + + def test_repr(self): + t = tkinter.Toplevel(self.root, name='top') + f = tkinter.Frame(t, name='child') + self.assertEqual(repr(f), '') + + def test_generated_names(self): + class Button2(tkinter.Button): + pass + + t = tkinter.Toplevel(self.root) + f = tkinter.Frame(t) + f2 = tkinter.Frame(t) + self.assertNotEqual(str(f), str(f2)) + b = tkinter.Button(f2) + b2 = Button2(f2) + for name in str(b).split('.') + str(b2).split('.'): + self.assertFalse(name.isidentifier(), msg=repr(name)) + b3 = tkinter.Button(f2) + b4 = Button2(f2) + self.assertEqual(len({str(b), str(b2), str(b3), str(b4)}), 4) + + def test_dealloc_in_wrong_thread(self): + # gh-83274: deallocating the interpreter in the wrong thread must not + # crash. + script = textwrap.dedent(""" + import threading + import tkinter + root = tkinter.Tk() + root.destroy() + # Let another thread drop the last reference. + ready = threading.Event() + t = threading.Thread(target=lambda obj: ready.wait(), args=(root,)) + t.start() + del root + ready.set() + t.join() + print('ok') + """) + rc, out, err = assert_python_ok('-c', script) + self.assertEqual(out.strip(), b'ok') + if not support.Py_GIL_DISABLED: + # On the free-threaded build the interpreter may instead be + # deallocated in its own thread (deferred reference counting), so + # the warning is not necessarily emitted. The crucial guarantee -- + # no crash -- is already checked by assert_python_ok() above. + self.assertIn(b'RuntimeWarning', err) + self.assertIn(b'gh-83274', err) + + @requires_tk(8, 6, 6) + def test_tk_busy(self): + root = self.root + f = tkinter.Frame(root, name='myframe') + f2 = tkinter.Frame(root) + f.pack() + f2.pack() + b = tkinter.Button(f) + b.pack() + f.tk_busy_hold() + with self.assertRaisesRegex(TclError, 'unknown option "-spam"'): + f.tk_busy_configure(spam='eggs') + with self.assertRaisesRegex(TclError, 'unknown option "-spam"'): + f.tk_busy_cget('spam') + with self.assertRaisesRegex(TclError, 'unknown option "-spam"'): + f.tk_busy_configure('spam') + self.assertIsInstance(f.tk_busy_configure(), dict) + + self.assertTrue(f.tk_busy_status()) + self.assertFalse(root.tk_busy_status()) + self.assertFalse(f2.tk_busy_status()) + self.assertFalse(b.tk_busy_status()) + self.assertIn(f, f.tk_busy_current()) + self.assertIn(f, f.tk_busy_current('*.m?f*me')) + self.assertNotIn(f, f.tk_busy_current('*spam')) + + f.tk_busy_forget() + self.assertFalse(f.tk_busy_status()) + self.assertFalse(f.tk_busy_current()) + errmsg = r"can(no|')t find busy window.*" + with self.assertRaisesRegex(TclError, errmsg): + f.tk_busy_configure() + with self.assertRaisesRegex(TclError, errmsg): + f.tk_busy_forget() + + @requires_tk(8, 6, 6) + def test_tk_busy_with_cursor(self): + root = self.root + if root._windowingsystem == 'aqua': + self.skipTest('the cursor option is not supported on OSX/Aqua') + f = tkinter.Frame(root, name='myframe') + f.pack() + f.tk_busy_hold(cursor='gumby') + + self.assertEqual(f.tk_busy_cget('cursor'), 'gumby') + f.tk_busy_configure(cursor='heart') + self.assertEqual(f.tk_busy_cget('cursor'), 'heart') + self.assertEqual(f.tk_busy_configure()['cursor'][4], 'heart') + self.assertEqual(f.tk_busy_configure('cursor')[4], 'heart') + + f.tk_busy_forget() + errmsg = r"can(no|')t find busy window.*" + with self.assertRaisesRegex(TclError, errmsg): + f.tk_busy_cget('cursor') + + def test_tk_setPalette(self): + root = self.root + root.tk_setPalette('black') + self.assertEqual(root['background'], 'black') + root.tk_setPalette('white') + self.assertEqual(root['background'], 'white') + self.assertRaisesRegex(tkinter.TclError, + '^unknown color name "spam"$', + root.tk_setPalette, 'spam') + + root.tk_setPalette(background='black') + self.assertEqual(root['background'], 'black') + root.tk_setPalette(background='blue', highlightColor='yellow') + self.assertEqual(root['background'], 'blue') + self.assertEqual(root['highlightcolor'], 'yellow') + root.tk_setPalette(background='yellow', highlightColor='blue') + self.assertEqual(root['background'], 'yellow') + self.assertEqual(root['highlightcolor'], 'blue') + self.assertRaisesRegex(tkinter.TclError, + '^unknown color name "spam"$', + root.tk_setPalette, background='spam') + self.assertRaisesRegex(tkinter.TclError, + '^must specify a background color$', + root.tk_setPalette, spam='white') + self.assertRaisesRegex(tkinter.TclError, + '^must specify a background color$', + root.tk_setPalette, highlightColor='blue') + + def test_after(self): + root = self.root + + def callback(start=0, step=1, *, end=0): + nonlocal count + count = start + step + end + + # Without function, sleeps for ms. + self.assertIsNone(root.after(1)) + + # Set up with callback with no args. + count = 0 + timer1 = root.after(0, callback) + self.assertIn(timer1, root.tk.call('after', 'info')) + (script, _) = root.tk.splitlist(root.tk.call('after', 'info', timer1)) + root.update() # Process all pending events. + self.assertEqual(count, 1) + with self.assertRaises(tkinter.TclError): + root.tk.call(script) + + # Set up with callback with args. + count = 0 + timer1 = root.after(0, callback, 42, 11) + root.update() # Process all pending events. + self.assertEqual(count, 53) + + # Cancel before called. + timer1 = root.after(1000, callback) + self.assertIn(timer1, root.tk.call('after', 'info')) + (script, _) = root.tk.splitlist(root.tk.call('after', 'info', timer1)) + root.after_cancel(timer1) # Cancel this event. + self.assertEqual(count, 53) + with self.assertRaises(tkinter.TclError): + root.tk.call(script) + + # Call with a callable class + count = 0 + timer1 = root.after(0, functools.partial(callback, 42, 11)) + root.update() # Process all pending events. + self.assertEqual(count, 53) + + # Set up with callback with keyword args. + count = 0 + timer1 = root.after(0, callback, 42, step=11, end=1) + root.update() # Process all pending events. + self.assertEqual(count, 54) + + def test_after_idle(self): + root = self.root + + def callback(start=0, step=1, *, end=0): + nonlocal count + count = start + step + end + + # Set up with callback with no args. + count = 0 + idle1 = root.after_idle(callback) + self.assertIn(idle1, root.tk.call('after', 'info')) + (script, _) = root.tk.splitlist(root.tk.call('after', 'info', idle1)) + root.update_idletasks() # Process all pending events. + self.assertEqual(count, 1) + with self.assertRaises(tkinter.TclError): + root.tk.call(script) + + # Set up with callback with args. + count = 0 + idle1 = root.after_idle(callback, 42, 11) + root.update_idletasks() # Process all pending events. + self.assertEqual(count, 53) + + # Cancel before called. + idle1 = root.after_idle(callback) + self.assertIn(idle1, root.tk.call('after', 'info')) + (script, _) = root.tk.splitlist(root.tk.call('after', 'info', idle1)) + root.after_cancel(idle1) # Cancel this event. + self.assertEqual(count, 53) + with self.assertRaises(tkinter.TclError): + root.tk.call(script) + + # Set up with callback with keyword args. + count = 0 + idle1 = root.after_idle(callback, 42, step=11, end=1) + root.update() # Process all pending events. + self.assertEqual(count, 54) + + def test_after_cancel(self): + root = self.root + + def callback(): + nonlocal count + count += 1 + + timer1 = root.after(5000, callback) + idle1 = root.after_idle(callback) + + # No value for id raises a ValueError. + with self.assertRaises(ValueError): + root.after_cancel(None) + + # Cancel timer event. + count = 0 + (script, _) = root.tk.splitlist(root.tk.call('after', 'info', timer1)) + root.tk.call(script) + self.assertEqual(count, 1) + root.after_cancel(timer1) + with self.assertRaises(tkinter.TclError): + root.tk.call(script) + self.assertEqual(count, 1) + with self.assertRaises(tkinter.TclError): + root.tk.call('after', 'info', timer1) + + # Cancel same event - nothing happens. + root.after_cancel(timer1) + + # Cancel idle event. + count = 0 + (script, _) = root.tk.splitlist(root.tk.call('after', 'info', idle1)) + root.tk.call(script) + self.assertEqual(count, 1) + root.after_cancel(idle1) + with self.assertRaises(tkinter.TclError): + root.tk.call(script) + self.assertEqual(count, 1) + with self.assertRaises(tkinter.TclError): + root.tk.call('after', 'info', idle1) + + def test_after_info(self): + root = self.root + + # No events. + self.assertEqual(root.after_info(), ()) + + # Add timer. + timer = root.after(1, lambda: 'break') + + # With no parameter, it returns a tuple of the event handler ids. + self.assertEqual(root.after_info(), (timer, )) + root.after_cancel(timer) + + timer1 = root.after(5000, lambda: 'break') + timer2 = root.after(5000, lambda: 'break') + idle1 = root.after_idle(lambda: 'break') + # Only contains new events and not 'timer'. + self.assertEqual(root.after_info(), (idle1, timer2, timer1)) + + # With a parameter returns a tuple of (script, type). + timer1_info = root.after_info(timer1) + self.assertEqual(len(timer1_info), 2) + self.assertEqual(timer1_info[1], 'timer') + idle1_info = root.after_info(idle1) + self.assertEqual(len(idle1_info), 2) + self.assertEqual(idle1_info[1], 'idle') + + root.after_cancel(timer1) + with self.assertRaises(tkinter.TclError): + root.after_info(timer1) + root.after_cancel(timer2) + with self.assertRaises(tkinter.TclError): + root.after_info(timer2) + root.after_cancel(idle1) + with self.assertRaises(tkinter.TclError): + root.after_info(idle1) + + # No events. + self.assertEqual(root.after_info(), ()) + + def test_clipboard(self): + root = self.root + root.clipboard_clear() + root.clipboard_append('Ùñî') + self.assertEqual(root.clipboard_get(), 'Ùñî') + root.clipboard_append('çōđě') + self.assertEqual(root.clipboard_get(), 'Ùñîçōđě') + root.clipboard_clear() + with self.assertRaises(tkinter.TclError): + root.clipboard_get() + + def test_clipboard_astral(self): + root = self.root + root.clipboard_clear() + root.clipboard_append('𝔘𝔫𝔦') + self.assertEqual(root.clipboard_get(), '𝔘𝔫𝔦') + root.clipboard_append('𝔠𝔬𝔡𝔢') + self.assertEqual(root.clipboard_get(), '𝔘𝔫𝔦𝔠𝔬𝔡𝔢') + root.clipboard_clear() + with self.assertRaises(tkinter.TclError): + root.clipboard_get() + + def test_getint(self): + self.assertEqual(self.root.getint('42'), 42) + self.assertEqual(self.root.getint(42), 42) + self.assertEqual(self.root.getint('-5'), -5) + self.assertRaises(ValueError, self.root.getint, 'spam') + + def test_getdouble(self): + self.assertEqual(self.root.getdouble('3.5'), 3.5) + self.assertEqual(self.root.getdouble(3), 3.0) + self.assertRaises(ValueError, self.root.getdouble, 'spam') + + def test_getvar(self): + self.root.setvar('test_var', 'hello') + self.assertEqual(self.root.getvar('test_var'), 'hello') + + def test_register(self): + result = [] + def callback(): + result.append(1) + return 'spam' + name = self.root.register(callback) + self.assertIsInstance(name, str) + self.assertEqual(self.root.tk.call(name), 'spam') + self.assertEqual(result, [1]) + self.root.deletecommand(name) + self.assertRaises(TclError, self.root.tk.call, name) + + def test_createcommand_no_leak(self): + # gh-80937: dropping the interpreter must release a command's callback, + # even without an explicit deletecommand(). + interp = tkinter.Tcl() + callback = lambda: '' + ref = weakref.ref(callback) + interp.tk.createcommand('cb', callback) + del callback, interp + support.gc_collect() + self.assertIsNone(ref()) + + def test_option(self): + self.addCleanup(self.root.option_clear) + self.root.option_add('*Button.background', 'red') + b = tkinter.Button(self.root) + self.assertEqual(b.option_get('background', 'Background'), 'red') + self.assertEqual(b.option_get('foreground', 'Foreground'), '') + self.root.option_clear() + self.assertEqual(b.option_get('background', 'Background'), '') + + def test_option_readfile(self): + self.addCleanup(self.root.option_clear) + self.addCleanup(os_helper.unlink, os_helper.TESTFN) + with open(os_helper.TESTFN, 'w') as f: + f.write('*Button.background: red\n') + self.root.option_readfile(os_helper.TESTFN) + b = tkinter.Button(self.root) + self.assertEqual(b.option_get('background', 'Background'), 'red') + self.assertRaises(TclError, self.root.option_readfile, + os_helper.TESTFN + '.nonexistent') + self.assertRaises(TypeError, self.root.option_readfile) + self.assertRaises(TypeError, self.root.option_readfile, 'a', 'b', 'c') + + def test_nametowidget(self): + b = tkinter.Button(self.root, name='btn') + self.assertIs(self.root.nametowidget('btn'), b) + self.assertIs(self.root.nametowidget(str(b)), b) + self.assertRaises(KeyError, self.root.nametowidget, '.nonexistent') + + def test_nametowidget_menu_clone(self): + # A menu used as a menubar or cascade is cloned by Tk under an + # auto-generated name (each path component is the original name + # prefixed with one or more '#' clone markers). nametowidget() + # maps such a name back to the original widget (gh-38464). + menubar = tkinter.Menu(self.root) + filemenu = tkinter.Menu(menubar, tearoff=0) + menubar.add_cascade(label='File', menu=filemenu) + submenu = tkinter.Menu(filemenu, tearoff=0) + filemenu.add_cascade(label='More', menu=submenu) + self.root['menu'] = menubar + self.root.update_idletasks() + + originals = {menubar, filemenu, submenu} + clones = [] + def collect(parent): + for name in self.root.tk.splitlist( + self.root.tk.call('winfo', 'children', parent)): + clones.append(name) + collect(name) + collect('.') + # Every menu (originals and clones) resolves to an original widget. + self.assertTrue(any('#' in name for name in clones)) + for name in clones: + self.assertIn(self.root.nametowidget(name), originals) + + def test_focus_methods(self): + f = tkinter.Frame(self.root, width=150, height=100) + f.pack() + self.root.wait_visibility() # needed on Windows + self.root.update_idletasks() + f.focus_force() + self.root.update() + # The window manager can take the focus away, and then focus_get() + # and focus_displayof() return None. + if self.root.focus_displayof() is not None: + self.assertIs(self.root.focus_get(), f) + self.assertIs(self.root.focus_displayof(), f) + self.assertIs(f.focus_lastfor(), f) + b = tkinter.Button(f) + b.pack() + self.root.update() + b.focus_set() + self.root.update() + if self.root.focus_displayof() is not None: + self.assertIs(self.root.focus_get(), b) + self.assertIs(f.focus_lastfor(), b) + + def test_focus_methods_unresolvable(self): + # The focus may be on a widget that tkinter did not create and so + # cannot map to an instance (e.g. a torn-off menu). The focus + # methods return None instead of raising KeyError (gh-88758). + menu = tkinter.Menu(self.root, tearoff=1) + menu.add_command(label='Hello') + tearoff = self.root.tk.call('tk::TearOffMenu', str(menu), 0, 0) + self.addCleanup(self.root.tk.call, 'destroy', tearoff) + self.root.update() + self.assertRaises(KeyError, self.root.nametowidget, tearoff) + + self.root.tk.call('focus', '-force', tearoff) + self.root.update() + self.assertIsNone(self.root.focus_get()) + self.assertIsNone(self.root.focus_displayof()) + + def test_grab(self): + f = tkinter.Frame(self.root) + f.pack() + self.root.wait_visibility() + self.assertIsNone(self.root.grab_current()) + self.assertIsNone(f.grab_status()) + f.grab_set() + self.assertEqual(f.grab_status(), 'local') + self.assertIs(self.root.grab_current(), f) + f.grab_release() + self.assertIsNone(f.grab_status()) + self.assertIsNone(self.root.grab_current()) + + def test_selection_own(self): + self.root.selection_own() + self.assertIs(self.root.selection_own_get(), self.root) + f = tkinter.Frame(self.root) + f.selection_own() + self.assertIs(self.root.selection_own_get(), f) + + def test_event_add_delete_info(self): + self.addCleanup(self.root.event_delete, '<>') + self.root.event_add('<>', '', '') + self.assertEqual(self.root.event_info('<>'), + ('', '')) + self.assertIn('<>', self.root.event_info()) + self.root.event_delete('<>', '') + self.assertEqual(self.root.event_info('<>'), + ('',)) + self.root.event_delete('<>') + self.assertEqual(self.root.event_info('<>'), ()) + self.assertRaises(TypeError, self.root.event_delete) + + def test_bell(self): + self.root.bell() # No exception. + self.root.bell(displayof=self.root) + + def test_tk_focusNext_focusPrev(self): + f = tkinter.Frame(self.root) + f.pack() + entries = [tkinter.Entry(f) for _ in range(3)] + for entry in entries: + entry.pack() + # tk_focusNext skips widgets that are not viewable. + entries[-1].wait_visibility() + self.assertIs(entries[0].tk_focusNext(), entries[1]) + self.assertIs(entries[1].tk_focusNext(), entries[2]) + self.assertIs(entries[2].tk_focusPrev(), entries[1]) + self.assertIs(entries[1].tk_focusPrev(), entries[0]) + self.assertRaises(TypeError, entries[0].tk_focusNext, 'x') + self.assertRaises(TypeError, entries[0].tk_focusPrev, 'x') + + def test_tk_strictMotif(self): + self.addCleanup(self.root.tk_strictMotif, False) + self.assertIs(self.root.tk_strictMotif(), False) + self.assertIs(self.root.tk_strictMotif(True), True) + self.assertIs(self.root.tk_strictMotif(), True) + self.assertIs(self.root.tk_strictMotif(False), False) + self.assertRaises(TypeError, self.root.tk_strictMotif, 1, 2) + + def test_tk_bisque(self): + # tk_bisque resets the color palette; use a separate root so that + # the shared one is not affected. + root = tkinter.Tk() + self.addCleanup(root.destroy) + root.tk_bisque() + self.assertEqual(root['background'], '#ffe4c4') + self.assertRaises(TypeError, root.tk_bisque, 'x') + + def test_wait_variable(self): + var = tkinter.StringVar(self.root) + self.assertEqual(self.root.waitvar, self.root.wait_variable) + self.root.after(1, var.set, 'done') + self.root.wait_variable(var) # Returns once the variable is set. + self.assertEqual(var.get(), 'done') + + def test_wait_window(self): + top = tkinter.Toplevel(self.root) + self.root.after(1, top.destroy) + self.root.wait_window(top) # Returns once the window is destroyed. + self.assertFalse(top.winfo_exists()) + + def test_tk_focusFollowsMouse(self): + self.root.tk_focusFollowsMouse() # No exception. + + def test_selection_handle(self): + f = tkinter.Frame(self.root) + def handler(offset, length): + return 'PAYLOAD'[int(offset):int(offset) + int(length)] + f.selection_handle(handler) + f.selection_own() + self.assertEqual(f.selection_get(), 'PAYLOAD') + + def test_grab_set_global(self): + # A successful global grab directs all events on the display to this + # application, so only the error paths are tested here. + self.assertRaises(TypeError, self.root.grab_set_global, 'extra') + with self.subTest('non-viewable window'): + if self.root._windowingsystem != 'x11': + # Grabbing a non-viewable window fails only on X11; elsewhere + # it would actually grab the whole display. + self.skipTest('only X11 fails the grab') + f = tkinter.Frame(self.root) # not yet viewable + self.assertRaisesRegex(TclError, 'grab failed', f.grab_set_global) + + def test_send(self): + if self.root._windowingsystem != 'x11': + self.skipTest('send is only supported on X11') + self.assertRaisesRegex(TclError, 'no application named', + self.root.send, 'no_such_interp_xyzzy', 'set x 1') + + def test_event_repr_defaults(self): + e = tkinter.Event() + e.serial = 12345 + e.num = '??' + e.height = '??' + e.keycode = '??' + e.state = 0 + e.time = 123456789 + e.width = '??' + e.x = '??' + e.y = '??' + e.char = '' + e.keysym = '??' + e.keysym_num = '??' + e.type = '100' + e.widget = '??' + e.x_root = '??' + e.y_root = '??' + e.delta = 0 + self.assertEqual(repr(e), '<100 event>') + + def test_event_repr(self): + e = tkinter.Event() + e.serial = 12345 + e.num = 3 + e.focus = True + e.height = 200 + e.keycode = 65 + e.state = 0x30405 + e.time = 123456789 + e.width = 300 + e.x = 10 + e.y = 20 + e.char = 'A' + e.send_event = True + e.keysym = 'Key-A' + e.keysym_num = ord('A') + e.type = tkinter.EventType.Configure + e.widget = '.text' + e.x_root = 1010 + e.y_root = 1020 + e.delta = -1 + self.assertEqual(repr(e), + "") + + def test_eventtype_enum(self): + class CheckedEventType(enum.StrEnum): + KeyPress = '2' + Key = KeyPress + KeyRelease = '3' + ButtonPress = '4' + Button = ButtonPress + ButtonRelease = '5' + Motion = '6' + Enter = '7' + Leave = '8' + FocusIn = '9' + FocusOut = '10' + Keymap = '11' # undocumented + Expose = '12' + GraphicsExpose = '13' # undocumented + NoExpose = '14' # undocumented + Visibility = '15' + Create = '16' + Destroy = '17' + Unmap = '18' + Map = '19' + MapRequest = '20' + Reparent = '21' + Configure = '22' + ConfigureRequest = '23' + Gravity = '24' + ResizeRequest = '25' + Circulate = '26' + CirculateRequest = '27' + Property = '28' + SelectionClear = '29' # undocumented + SelectionRequest = '30' # undocumented + Selection = '31' # undocumented + Colormap = '32' + ClientMessage = '33' # undocumented + Mapping = '34' # undocumented + VirtualEvent = '35' # undocumented + Activate = '36' + Deactivate = '37' + MouseWheel = '38' + enum._test_simple_enum(CheckedEventType, tkinter.EventType) + + def test_getboolean(self): + for v in 'true', 'yes', 'on', '1', 't', 'y', 1, True: + self.assertIs(self.root.getboolean(v), True) + for v in 'false', 'no', 'off', '0', 'f', 'n', 0, False: + self.assertIs(self.root.getboolean(v), False) + self.assertRaises(ValueError, self.root.getboolean, 'yea') + self.assertRaises(ValueError, self.root.getboolean, '') + self.assertRaises(TypeError, self.root.getboolean, None) + self.assertRaises(TypeError, self.root.getboolean, ()) + + def test_mainloop(self): + log = [] + def callback(): + log.append(1) + self.root.after(100, self.root.quit) + self.root.after(100, callback) + self.root.mainloop(1) + self.assertEqual(log, []) + self.root.mainloop(0) + self.assertEqual(log, [1]) + self.assertTrue(self.root.winfo_exists()) + + def test_info_patchlevel(self): + vi = self.root.info_patchlevel() + f = tkinter.Frame(self.root) + self.assertEqual(f.info_patchlevel(), vi) + # The following is almost a copy of tests for sys.version_info. + self.assertIsInstance(vi[:], tuple) + self.assertEqual(len(vi), 5) + self.assertIsInstance(vi[0], int) + self.assertIsInstance(vi[1], int) + self.assertIsInstance(vi[2], int) + self.assertIn(vi[3], ("alpha", "beta", "candidate", "final")) + self.assertIsInstance(vi[4], int) + self.assertIsInstance(vi.major, int) + self.assertIsInstance(vi.minor, int) + self.assertIsInstance(vi.micro, int) + self.assertIn(vi.releaselevel, ("alpha", "beta", "final")) + self.assertIsInstance(vi.serial, int) + self.assertEqual(vi[0], vi.major) + self.assertEqual(vi[1], vi.minor) + self.assertEqual(vi[2], vi.micro) + self.assertEqual(vi[3], vi.releaselevel) + self.assertEqual(vi[4], vi.serial) + self.assertTrue(vi > (1,0,0)) + if vi.releaselevel == 'final': + self.assertEqual(vi.serial, 0) + else: + self.assertEqual(vi.micro, 0) + self.assertStartsWith(str(vi), f'{vi.major}.{vi.minor}') + + def test_embedded_null(self): + widget = tkinter.Entry(self.root) + widget.insert(0, 'abc\0def') # ASCII-only + widget.selection_range(0, 'end') + self.assertEqual(widget.selection_get(), 'abc\x00def') + widget.insert(0, '\u20ac\0') # non-ASCII + widget.selection_range(0, 'end') + self.assertEqual(widget.selection_get(), '\u20ac\0abc\x00def') + + def test_iterable_protocol(self): + widget = tkinter.Entry(self.root) + self.assertNotIsSubclass(tkinter.Entry, collections.abc.Iterable) + self.assertNotIsSubclass(tkinter.Entry, collections.abc.Container) + self.assertNotIsInstance(widget, collections.abc.Iterable) + self.assertNotIsInstance(widget, collections.abc.Container) + with self.assertRaisesRegex(TypeError, 'is not iterable'): + iter(widget) + with self.assertRaisesRegex(TypeError, 'is not a container or iterable'): + widget in widget + + +class TkTest(AbstractTkTest, unittest.TestCase): + + def test_readprofile(self): + # gh-153333: profile scripts are decoded with their own coding cookie, + # not the locale encoding. Two cookies so no locale can mask the bug. + profiles = { + '.RpClass.py': ('latin-1', "self._rp_latin1 = 'caf\xe9'"), + '.rpbase.py': ('utf-8', "self._rp_utf8 = 'caf\xe9'"), + } + self.addCleanup(self.root.__dict__.pop, '_rp_latin1', None) + self.addCleanup(self.root.__dict__.pop, '_rp_utf8', None) + with (os_helper.temp_dir() as home, + os_helper.EnvironmentVarGuard() as env): + env['HOME'] = home + for filename, (encoding, body) in profiles.items(): + script = '# -*- coding: %s -*-\n%s\n' % (encoding, body) + with open(os.path.join(home, filename), 'wb') as f: + f.write(script.encode(encoding)) + self.root.readprofile('rpbase', 'RpClass') + self.assertEqual(self.root._rp_latin1, 'caf\xe9') + self.assertEqual(self.root._rp_utf8, 'caf\xe9') + + def test_className(self): + # The className argument sets the class of the root window. Tk + # title-cases it: the first letter is upper-cased, the rest lower-cased. + cases = [ + ('fooBAR', 'Foobar'), + ('éÉ', 'Éé'), # small and capital E WITH ACUTE + ] + if tcl_version >= (9, 0): + # small and capital DESERET LETTER LONG I (a non-BMP script) + cases.append(('\U00010428\U00010400', '\U00010400\U00010428')) + for className, klass in cases: + root = tkinter.Tk(className=className) + try: + self.assertEqual(root.winfo_class(), klass) + finally: + root.destroy() + if tcl_version < (9, 0): + # gh-126219: title-casing a non-BMP first letter crashed Tcl 8.x; + # such a class name is now rejected. + self.assertRaises(ValueError, tkinter.Tk, className='\U00010428') + + +class WinfoTest(AbstractTkTest, unittest.TestCase): + + def test_winfo_rgb(self): + + def assertApprox(col1, col2): + # A small amount of flexibility is required (bpo-45496) + # 33 is ~0.05% of 65535, which is a reasonable margin + for col1_channel, col2_channel in zip(col1, col2): + self.assertAlmostEqual(col1_channel, col2_channel, delta=33) + + root = self.root + rgb = root.winfo_rgb + + # Color name. + self.assertEqual(rgb('red'), (65535, 0, 0)) + self.assertEqual(rgb('dark slate blue'), (18504, 15677, 35723)) + # #RGB - extends each 4-bit hex value to be 16-bit. + self.assertEqual(rgb('#F0F'), (0xFFFF, 0x0000, 0xFFFF)) + # #RRGGBB - extends each 8-bit hex value to be 16-bit. + assertApprox(rgb('#4a3c8c'), (0x4a4a, 0x3c3c, 0x8c8c)) + # #RRRRGGGGBBBB + assertApprox(rgb('#dede14143939'), (0xdede, 0x1414, 0x3939)) + # Invalid string. + with self.assertRaises(tkinter.TclError): + rgb('#123456789a') + # RGB triplet is invalid input. + with self.assertRaises(tkinter.TclError): + rgb((111, 78, 55)) + + def test_winfo_pathname(self): + t = tkinter.Toplevel(self.root) + w = tkinter.Button(t) + wid = w.winfo_id() + self.assertIsInstance(wid, int) + self.assertEqual(self.root.winfo_pathname(hex(wid)), str(w)) + self.assertEqual(self.root.winfo_pathname(hex(wid), displayof=None), str(w)) + self.assertEqual(self.root.winfo_pathname(hex(wid), displayof=t), str(w)) + self.assertEqual(self.root.winfo_pathname(wid), str(w)) + self.assertEqual(self.root.winfo_pathname(wid, displayof=None), str(w)) + self.assertEqual(self.root.winfo_pathname(wid, displayof=t), str(w)) + + def test_winfo_class_name_parent(self): + f = tkinter.Frame(self.root) + b = tkinter.Button(f) + self.assertEqual(f.winfo_class(), 'Frame') + self.assertEqual(b.winfo_class(), 'Button') + self.assertEqual(b.winfo_name(), str(b).rsplit('.', 1)[-1]) + self.assertEqual(b.winfo_parent(), str(f)) + self.assertEqual(f.winfo_parent(), str(self.root)) + self.assertIs(f.winfo_toplevel(), self.root) + t = tkinter.Toplevel(self.root) + self.assertIs(tkinter.Button(t).winfo_toplevel(), t) + self.assertEqual(self.root.nametowidget(b.winfo_parent()), f) + + def test_winfo_children(self): + self.assertEqual(self.root.winfo_children(), []) + f = tkinter.Frame(self.root) + b = tkinter.Button(f) + self.assertEqual(self.root.winfo_children(), [f]) + self.assertEqual(f.winfo_children(), [b]) + + def test_winfo_visual_info(self): + f = tkinter.Frame(self.root) + self.assertIsInstance(f.winfo_depth(), int) + self.assertIsInstance(f.winfo_cells(), int) + self.assertIsInstance(f.winfo_visual(), str) + self.assertIsInstance(f.winfo_visualid(), str) + self.assertIsInstance(f.winfo_colormapfull(), bool) + visuals = self.root.winfo_visualsavailable() + self.assertIsInstance(visuals, list) + for name, depth in visuals: + self.assertIsInstance(name, str) + self.assertIsInstance(depth, int) + + def test_winfo_viewable(self): + f = tkinter.Frame(self.root) + self.assertFalse(f.winfo_viewable()) + f.pack() + f.wait_visibility() + self.root.update() + self.assertTrue(f.winfo_viewable()) + + def test_winfo_atom(self): + atom = self.root.winfo_atom('PRIMARY') + self.assertIsInstance(atom, int) + self.assertEqual(self.root.winfo_atomname(atom), 'PRIMARY') + self.assertEqual( + self.root.winfo_atomname(atom, displayof=self.root), 'PRIMARY') + self.assertEqual( + self.root.winfo_atom('PRIMARY', displayof=self.root), atom) + self.assertRaisesRegex(TclError, 'no atom exists', + self.root.winfo_atomname, 10 ** 9) + + def test_winfo_pointer(self): + self.assertIsInstance(self.root.winfo_pointerx(), int) + self.assertIsInstance(self.root.winfo_pointery(), int) + xy = self.root.winfo_pointerxy() + self.assertIsInstance(xy, tuple) + self.assertEqual(len(xy), 2) + self.assertTrue(all(isinstance(v, int) for v in xy)) + + def test_winfo_containing(self): + self.root.update() + # No window contains a point far off the screen. + self.assertIsNone(self.root.winfo_containing(-10000, -10000)) + self.assertIsNone( + self.root.winfo_containing(-10000, -10000, displayof=self.root)) + + def test_winfo_fpixels(self): + self.assertIsInstance(self.root.winfo_fpixels('1i'), float) + self.assertAlmostEqual(self.root.winfo_fpixels('1i'), + self.root.winfo_fpixels('72p')) + # Tk < 9 reports 'bad screen distance "spam"', Tk 9 reports + # 'expected screen distance ... but got "spam"'. + self.assertRaisesRegex(TclError, + r'(bad|expected) screen distance.*"spam"', + self.root.winfo_fpixels, 'spam') + + def test_winfo_screen(self): + for name in ('winfo_screenwidth', 'winfo_screenheight', + 'winfo_screenmmwidth', 'winfo_screenmmheight', + 'winfo_screencells', 'winfo_screendepth'): + value = getattr(self.root, name)() + self.assertIsInstance(value, int) + self.assertGreater(value, 0) + self.assertIsInstance(self.root.winfo_screenvisual(), str) + self.assertIsInstance(self.root.winfo_screen(), str) + self.assertIsInstance(self.root.winfo_server(), str) + + def test_winfo_vroot(self): + for name in ('winfo_vrootwidth', 'winfo_vrootheight', + 'winfo_vrootx', 'winfo_vrooty'): + self.assertIsInstance(getattr(self.root, name)(), int) + + def test_winfo_interps(self): + interps = self.root.winfo_interps() + self.assertIsInstance(interps, tuple) + # The registry of interpreters is only populated where "send" is + # supported (i.e. X11), so do not require this interpreter's name. + if self.root._windowingsystem == 'x11': + self.assertIn(self.root.tk.call('tk', 'appname'), interps) + self.assertEqual(self.root.winfo_interps(displayof=self.root), interps) + + +class WmTest(AbstractTkTest, unittest.TestCase): + + def test_wm_attribute(self): + w = self.root + attributes = w.wm_attributes(return_python_dict=True) + self.assertIsInstance(attributes, dict) + attributes2 = w.wm_attributes() + self.assertIsInstance(attributes2, tuple) + self.assertEqual(attributes2[::2], + tuple('-' + k for k in attributes)) + self.assertEqual(attributes2[1::2], tuple(attributes.values())) + # silently deprecated + attributes3 = w.wm_attributes(None) + if self.wantobjects: + self.assertEqual(attributes3, attributes2) + else: + self.assertIsInstance(attributes3, str) + + for name in attributes: + self.assertEqual(w.wm_attributes(name), attributes[name]) + # silently deprecated + for name in attributes: + self.assertEqual(w.wm_attributes('-' + name), attributes[name]) + + self.assertIn('alpha', attributes) + self.assertIn('fullscreen', attributes) + self.assertIn('topmost', attributes) + if w._windowingsystem == "win32": + self.assertIn('disabled', attributes) + self.assertIn('toolwindow', attributes) + self.assertIn('transparentcolor', attributes) + if w._windowingsystem == "aqua": + self.assertIn('modified', attributes) + self.assertIn('notify', attributes) + self.assertIn('titlepath', attributes) + self.assertIn('transparent', attributes) + if w._windowingsystem == "x11": + self.assertIn('type', attributes) + self.assertIn('zoomed', attributes) + + w.wm_attributes(alpha=0.5) + self.assertEqual(w.wm_attributes('alpha'), + 0.5 if self.wantobjects else '0.5') + w.wm_attributes(alpha=1.0) + self.assertEqual(w.wm_attributes('alpha'), + 1.0 if self.wantobjects else '1.0') + # silently deprecated + w.wm_attributes('-alpha', 0.5) + self.assertEqual(w.wm_attributes('alpha'), + 0.5 if self.wantobjects else '0.5') + w.wm_attributes(alpha=1.0) + self.assertEqual(w.wm_attributes('alpha'), + 1.0 if self.wantobjects else '1.0') + + def test_wm_iconbitmap(self): + t = tkinter.Toplevel(self.root) + patchlevel = get_tk_patchlevel(t) + + if ( + t._windowingsystem == 'aqua' + and sys.platform == 'darwin' + and platform.machine() == 'x86_64' + and platform.mac_ver()[0].startswith('26.') + ): + # https://github.com/python/cpython/issues/146531 + # Tk bug 4a2070f0d3a99aa412bc582d386d575ca2f37323 + # Not fixed as of Tk 8.6.18 and 9.0.4. + self.skipTest('wm iconbitmap hangs on macOS 26 Intel') + + self.assertEqual(t.wm_iconbitmap(), '') + t.wm_iconbitmap('hourglass') + bug = False + if t._windowingsystem == 'aqua': + # Tk bug 13ac26b35dc55f7c37f70b39d59d7ef3e63017c8. + if patchlevel < (8, 6, 17) or (9, 0) <= patchlevel < (9, 0, 2): + bug = True + if not bug: + self.assertEqual(t.wm_iconbitmap(), 'hourglass') + self.assertEqual(self.root.wm_iconbitmap(), '') + t.wm_iconbitmap('') + self.assertEqual(t.wm_iconbitmap(), '') + + if t._windowingsystem == 'win32': + t.wm_iconbitmap(default='hourglass') + self.assertEqual(t.wm_iconbitmap(), 'hourglass') + self.assertEqual(self.root.wm_iconbitmap(), '') + t.wm_iconbitmap(default='') + self.assertEqual(t.wm_iconbitmap(), '') + + t.destroy() + + def test_wm_iconphoto(self): + t = tkinter.Toplevel(self.root) + img = tkinter.PhotoImage(master=t, width=16, height=16) + t.wm_iconphoto(False, img) # No exception. + t.wm_iconphoto(True, img) + self.assertRaises(tkinter.TclError, t.wm_iconphoto, False, 'spam') + + def test_wm_title(self): + t = tkinter.Toplevel(self.root) + t.title('Hello') + self.assertEqual(t.title(), 'Hello') + self.assertEqual(t.wm_title(), 'Hello') + t.wm_title('Spam') + self.assertEqual(t.title(), 'Spam') + + def test_wm_geometry(self): + t = tkinter.Toplevel(self.root) + t.geometry('200x100+10+20') + t.update() + self.assertRegex(t.geometry(), r'^200x100\+-?\d+\+-?\d+$') + self.assertEqual(t.wm_geometry(), t.geometry()) + + def test_wm_minsize_maxsize(self): + t = tkinter.Toplevel(self.root) + # Use a width above the minimum enforced by some platforms (72 on Aqua). + t.minsize(150, 100) + self.assertEqual(t.minsize(), (150, 100)) + t.maxsize(500, 600) + self.assertEqual(t.maxsize(), (500, 600)) + + def test_wm_resizable(self): + t = tkinter.Toplevel(self.root) + t.resizable(False, True) + self.assertEqual(t.resizable(), (0, 1)) + self.assertRaisesRegex(TclError, 'expected boolean value', + t.resizable, 'spam', True) + + def test_wm_aspect(self): + t = tkinter.Toplevel(self.root) + self.assertEqual(t.aspect(), None) + t.aspect(1, 2, 3, 4) + self.assertEqual(t.aspect(), (1, 2, 3, 4)) + + def test_wm_grid(self): + t = tkinter.Toplevel(self.root) + t.wm_grid(10, 10, 5, 5) + self.assertEqual(t.wm_grid(), (10, 10, 5, 5)) + + def test_wm_positionfrom_sizefrom(self): + # These set X11 size hints and may be no-ops on other platforms. + t = tkinter.Toplevel(self.root) + t.positionfrom('user') + self.assertIn(t.positionfrom(), ('user', '')) + t.sizefrom('program') + self.assertIn(t.sizefrom(), ('program', '')) + + def test_wm_focusmodel(self): + t = tkinter.Toplevel(self.root) + self.assertEqual(t.focusmodel(), 'passive') + t.focusmodel('active') + self.assertEqual(t.focusmodel(), 'active') + self.assertRaises(TclError, t.focusmodel, 'spam') + + def test_wm_iconname(self): + # WM_ICON_NAME is an X11 property and may be a no-op elsewhere. + t = tkinter.Toplevel(self.root) + t.iconname('Icon') + self.assertIn(t.iconname(), ('Icon', '')) + + def test_wm_iconposition(self): + t = tkinter.Toplevel(self.root) + t.wm_iconposition(3, 4) # An X11 hint; may be a no-op elsewhere. + if t._windowingsystem == 'x11': + self.assertEqual(t.wm_iconposition(), (3, 4)) + + def test_wm_iconmask_iconwindow(self): + if self.root._windowingsystem != 'x11': + self.skipTest('iconmask and iconwindow are X11-specific') + t = tkinter.Toplevel(self.root) + t.wm_iconmask('gray50') # No exception. + icon = tkinter.Toplevel(self.root) + t.wm_iconwindow(icon) + self.assertEqual(str(t.wm_iconwindow()), str(icon)) + + def test_wm_colormapwindows(self): + if self.root._windowingsystem != 'x11': + self.skipTest('colormapwindows is X11-specific') + t = tkinter.Toplevel(self.root) + self.assertEqual(t.wm_colormapwindows(), []) + f = tkinter.Frame(t) + t.wm_colormapwindows(f) + self.assertEqual([str(w) for w in t.wm_colormapwindows()], [str(f)]) + + def test_wm_manage_forget(self): + f = tkinter.Frame(self.root) + self.root.wm_manage(f) # Make the frame a top-level window. + self.root.wm_forget(f) # Revert it; no exception either way. + + def test_wm_client_command(self): + t = tkinter.Toplevel(self.root) + t.client('myhost') + t.wm_command('myapp -x') + # WM_CLIENT_MACHINE and WM_COMMAND are X11 properties; elsewhere the + # setters may be no-ops and wm_command may return a split list. + if t._windowingsystem == 'x11': + self.assertEqual(t.client(), 'myhost') + self.assertEqual(t.wm_command(), 'myapp -x') + + def test_wm_overrideredirect(self): + t = tkinter.Toplevel(self.root) + self.assertFalse(t.overrideredirect()) + t.overrideredirect(True) + self.assertTrue(t.overrideredirect()) + + def test_wm_state(self): + t = tkinter.Toplevel(self.root) + t.update() + self.assertEqual(t.state(), 'normal') + t.withdraw() + self.assertEqual(t.state(), 'withdrawn') + t.deiconify() + t.update() + self.assertEqual(t.state(), 'normal') + self.assertRaises(TclError, t.state, 'spam') + + def test_wm_frame(self): + t = tkinter.Toplevel(self.root) + t.update() + self.assertIsInstance(t.frame(), str) + + def test_wm_group(self): + # The window group is an X11 concept and may be a no-op elsewhere. + t = tkinter.Toplevel(self.root) + t.group(self.root) + self.assertIn(t.group(), (str(self.root), '')) + + def test_wm_protocol(self): + t = tkinter.Toplevel(self.root) + self.assertIsInstance(t.protocol(), tuple) + t.protocol('WM_SAVE_YOURSELF', lambda: None) + self.assertIn('WM_SAVE_YOURSELF', t.protocol()) + # Querying a single protocol returns the bound command name. + self.assertTrue(t.protocol('WM_SAVE_YOURSELF')) + + def test_wm_transient(self): + t = tkinter.Toplevel(self.root) + self.assertEqual(t.transient(), '') + t.transient(self.root) + self.assertEqual(str(t.transient()), str(self.root)) + + +class EventTest(AbstractTkTest, unittest.TestCase): + + def test_focus(self): + f = tkinter.Frame(self.root, width=150, height=100) + f.pack() + self.root.wait_visibility() # needed on Windows + self.root.update_idletasks() + + events = [] + f.bind('', events.append) + + f.focus_force() + self.root.update() + # The window manager can take the focus away and give it back, + # which makes Tk generate additional focus events. + self.assertGreaterEqual(len(events), 1, events) + e = events[0] + self.assertIs(e.type, tkinter.EventType.FocusIn) + self.assertIs(e.widget, f) + self.assertIsInstance(e.serial, int) + self.assertEqual(e.time, '??') + self.assertIs(e.send_event, False) + self.assertNotHasAttr(e, 'focus') + self.assertEqual(e.num, '??') + self.assertEqual(e.state, '??') + self.assertEqual(e.char, '??') + self.assertEqual(e.keycode, '??') + self.assertEqual(e.keysym, '??') + self.assertEqual(e.keysym_num, '??') + self.assertEqual(e.width, '??') + self.assertEqual(e.height, '??') + self.assertEqual(e.x, '??') + self.assertEqual(e.y, '??') + self.assertEqual(e.x_root, '??') + self.assertEqual(e.y_root, '??') + self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, 'NotifyAncestor') + self.assertEqual(repr(e), '') + + def test_configure(self): + f = tkinter.Frame(self.root, width=150, height=100) + f.pack() + self.root.wait_visibility() # needed on Windows + self.root.update_idletasks() + + events = [] + f.bind('', events.append) + + f.configure(height=120, borderwidth=10) + self.assertEqual(len(events), 1, events) + e = events[0] + self.assertIs(e.type, tkinter.EventType.Configure) + self.assertIs(e.widget, f) + self.assertIsInstance(e.serial, int) + self.assertEqual(e.time, '??') + self.assertIs(e.send_event, False) + self.assertNotHasAttr(e, 'focus') + self.assertEqual(e.num, '??') + self.assertEqual(e.state, '??') + self.assertEqual(e.char, '??') + self.assertEqual(e.keycode, '??') + self.assertEqual(e.keysym, '??') + self.assertEqual(e.keysym_num, '??') + self.assertEqual(e.width, 150) + self.assertEqual(e.height, 100) + self.assertEqual(e.x, 0) + self.assertEqual(e.y, 0) + self.assertEqual(e.x_root, '??') + self.assertEqual(e.y_root, '??') + self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, '??') + self.assertEqual(repr(e), '') + + def test_event_generate_key_press(self): + f = tkinter.Frame(self.root, width=150, height=100) + f.pack() + self.root.wait_visibility() # needed on Windows + self.root.update_idletasks() + + events = [] + f.bind('', events.append) + f.focus_force() + + f.event_generate('') + self.assertEqual(len(events), 1, events) + e = events[0] + self.assertIs(e.type, tkinter.EventType.KeyPress) + self.assertIs(e.widget, f) + self.assertIsInstance(e.serial, int) + self.assertEqual(e.time, 0) + self.assertIs(e.send_event, False) + self.assertNotHasAttr(e, 'focus') + self.assertEqual(e.num, '??') + self.assertIsInstance(e.state, int) + self.assertNotEqual(e.state, 0) + self.assertEqual(e.char, 'z') + self.assertIsInstance(e.keycode, int) + self.assertNotEqual(e.keycode, 0) + self.assertEqual(e.keysym, 'z') + self.assertEqual(e.keysym_num, ord('z')) + self.assertEqual(e.width, '??') + self.assertEqual(e.height, '??') + self.assertEqual(e.x, -1 - f.winfo_rootx()) + self.assertEqual(e.y, -1 - f.winfo_rooty()) + self.assertEqual(e.x_root, -1) + self.assertEqual(e.y_root, -1) + self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, '??') + self.assertEqual(repr(e), + f"") + + def test_event_generate_enter(self): + f = tkinter.Frame(self.root, width=150, height=100) + f.pack() + self.root.wait_visibility() # needed on Windows + self.root.update_idletasks() + + events = [] + f.bind('', events.append) + + f.event_generate('', x=100, y=50) + self.assertEqual(len(events), 1, events) + e = events[0] + self.assertIs(e.type, tkinter.EventType.Enter) + self.assertIs(e.widget, f) + self.assertIsInstance(e.serial, int) + self.assertEqual(e.time, 0) + self.assertIs(e.send_event, False) + self.assertIs(e.focus, False) + self.assertEqual(e.num, '??') + self.assertEqual(e.state, 0) + self.assertEqual(e.char, '??') + self.assertEqual(e.keycode, '??') + self.assertEqual(e.keysym, '??') + self.assertEqual(e.keysym_num, '??') + self.assertEqual(e.width, '??') + self.assertEqual(e.height, '??') + self.assertEqual(e.x, 100) + self.assertEqual(e.y, 50) + self.assertEqual(e.x_root, 100 + f.winfo_rootx()) + self.assertEqual(e.y_root, 50 + f.winfo_rooty()) + self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, 'NotifyAncestor') + self.assertEqual(repr(e), '') + + f.event_generate('', x=100, y=50, detail='NotifyPointer') + self.assertEqual(len(events), 2, events) + e = events[1] + self.assertIs(e.type, tkinter.EventType.Enter) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, 'NotifyPointer') + + def test_event_generate_button_press(self): + f = tkinter.Frame(self.root, width=150, height=100) + f.pack() + self.root.wait_visibility() # needed on Windows + self.root.update_idletasks() + + events = [] + f.bind('', events.append) + f.focus_force() + + f.event_generate('', x=100, y=50) + self.assertEqual(len(events), 1, events) + e = events[0] + self.assertIs(e.type, tkinter.EventType.ButtonPress) + self.assertIs(e.widget, f) + self.assertIsInstance(e.serial, int) + self.assertEqual(e.time, 0) + self.assertIs(e.send_event, False) + self.assertNotHasAttr(e, 'focus') + self.assertEqual(e.num, 1) + self.assertEqual(e.state, 0) + self.assertEqual(e.char, '??') + self.assertEqual(e.keycode, '??') + self.assertEqual(e.keysym, '??') + self.assertEqual(e.keysym_num, '??') + self.assertEqual(e.width, '??') + self.assertEqual(e.height, '??') + self.assertEqual(e.x, 100) + self.assertEqual(e.y, 50) + self.assertEqual(e.x_root, f.winfo_rootx() + 100) + self.assertEqual(e.y_root, f.winfo_rooty() + 50) + self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, '??') + self.assertEqual(repr(e), '') + + def test_event_generate_motion(self): + f = tkinter.Frame(self.root, width=150, height=100) + f.pack() + self.root.wait_visibility() # needed on Windows + self.root.update_idletasks() + + events = [] + f.bind('', events.append) + f.focus_force() + + f.event_generate('', x=100, y=50) + self.assertEqual(len(events), 1, events) + e = events[0] + self.assertIs(e.type, tkinter.EventType.Motion) + self.assertIs(e.widget, f) + self.assertIsInstance(e.serial, int) + self.assertEqual(e.time, 0) + self.assertIs(e.send_event, False) + self.assertNotHasAttr(e, 'focus') + self.assertEqual(e.num, '??') + self.assertEqual(e.state, 0x100) + self.assertEqual(e.char, '??') + self.assertEqual(e.keycode, '??') + self.assertEqual(e.keysym, '??') + self.assertEqual(e.keysym_num, '??') + self.assertEqual(e.width, '??') + self.assertEqual(e.height, '??') + self.assertEqual(e.x, 100) + self.assertEqual(e.y, 50) + self.assertEqual(e.x_root, f.winfo_rootx() + 100) + self.assertEqual(e.y_root, f.winfo_rooty() + 50) + self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, '??') + self.assertEqual(repr(e), '') + + def test_event_generate_mouse_wheel(self): + f = tkinter.Frame(self.root, width=150, height=100) + f.pack() + self.root.wait_visibility() # needed on Windows + self.root.update_idletasks() + + events = [] + f.bind('', events.append) + f.focus_force() + + f.event_generate('', x=100, y=50, delta=-5) + self.assertEqual(len(events), 1, events) + e = events[0] + self.assertIs(e.type, tkinter.EventType.MouseWheel) + self.assertIs(e.widget, f) + self.assertIsInstance(e.serial, int) + self.assertIs(e.send_event, False) + self.assertNotHasAttr(e, 'focus') + self.assertEqual(e.time, 0) + self.assertEqual(e.num, '??') + self.assertEqual(e.state, 0) + self.assertEqual(e.char, '??') + self.assertEqual(e.keycode, '??') + self.assertEqual(e.keysym, '??') + self.assertEqual(e.keysym_num, '??') + self.assertEqual(e.width, '??') + self.assertEqual(e.height, '??') + self.assertEqual(e.x, 100) + self.assertEqual(e.y, 50) + self.assertEqual(e.x_root, f.winfo_rootx() + 100) + self.assertEqual(e.y_root, f.winfo_rooty() + 50) + self.assertEqual(e.delta, -5) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, '??') + self.assertEqual(repr(e), '') + + def test_event_generate_virtual_event(self): + f = tkinter.Frame(self.root, width=150, height=100) + f.pack() + self.root.wait_visibility() # needed on Windows + self.root.update_idletasks() + + events = [] + f.bind('<>', events.append) + f.focus_force() + + f.event_generate('<>', x=50) + self.assertEqual(len(events), 1, events) + e = events[0] + self.assertIs(e.type, tkinter.EventType.VirtualEvent) + self.assertIs(e.widget, f) + self.assertIsInstance(e.serial, int) + self.assertEqual(e.time, 0) + self.assertIs(e.send_event, False) + self.assertNotHasAttr(e, 'focus') + self.assertEqual(e.num, '??') + self.assertEqual(e.state, 0) + self.assertEqual(e.char, '??') + self.assertEqual(e.keycode, '??') + self.assertEqual(e.keysym, '??') + self.assertEqual(e.keysym_num, '??') + self.assertEqual(e.width, '??') + self.assertEqual(e.height, '??') + self.assertEqual(e.x, 50) + self.assertEqual(e.y, 0) + self.assertEqual(e.x_root, f.winfo_rootx() + 50) + self.assertEqual(e.y_root, -1) + self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, '') + self.assertEqual(e.detail, '??') + self.assertEqual(repr(e), + f"") + + f.event_generate('<>', data='spam') + self.assertEqual(len(events), 2, events) + e = events[1] + self.assertIs(e.type, tkinter.EventType.VirtualEvent) + self.assertEqual(e.user_data, 'spam') + self.assertEqual(e.detail, '??') + + +class BindTest(AbstractTkTest, unittest.TestCase): + + def setUp(self): + super().setUp() + root = self.root + self.frame = tkinter.Frame(self.root, class_='Test', + width=150, height=100) + self.frame.pack() + + def assertCommandExist(self, funcid): + self.assertEqual(_info_commands(self.root, funcid), (funcid,)) + + def assertCommandNotExist(self, funcid): + self.assertEqual(_info_commands(self.root, funcid), ()) + + def test_bind(self): + event = '' + f = self.frame + self.assertEqual(f.bind(), ()) + self.assertEqual(f.bind(event), '') + def test1(e): pass + def test2(e): pass + + funcid = f.bind(event, test1) + self.assertEqual(f.bind(), (event,)) + script = f.bind(event) + self.assertIn(funcid, script) + self.assertCommandExist(funcid) + + funcid2 = f.bind(event, test2, add=True) + script = f.bind(event) + self.assertIn(funcid, script) + self.assertIn(funcid2, script) + self.assertCommandExist(funcid) + self.assertCommandExist(funcid2) + + def test_unbind(self): + event = '' + f = self.frame + self.assertEqual(f.bind(), ()) + self.assertEqual(f.bind(event), '') + def test1(e): pass + def test2(e): pass + + funcid = f.bind(event, test1) + funcid2 = f.bind(event, test2, add=True) + + self.assertRaises(TypeError, f.unbind) + f.unbind(event) + self.assertEqual(f.bind(event), '') + self.assertEqual(f.bind(), ()) + + def test_unbind2(self): + f = self.frame + f.wait_visibility() + f.focus_force() + f.update_idletasks() + event = '' + self.assertEqual(f.bind(), ()) + self.assertEqual(f.bind(event), '') + def test1(e): events.append('a') + def test2(e): events.append('b') + def test3(e): events.append('c') + + funcid = f.bind(event, test1) + funcid2 = f.bind(event, test2, add=True) + funcid3 = f.bind(event, test3, add=True) + events = [] + f.event_generate(event) + self.assertEqual(events, ['a', 'b', 'c']) + + f.unbind(event, funcid2) + script = f.bind(event) + self.assertNotIn(funcid2, script) + self.assertIn(funcid, script) + self.assertIn(funcid3, script) + self.assertEqual(f.bind(), (event,)) + self.assertCommandNotExist(funcid2) + self.assertCommandExist(funcid) + self.assertCommandExist(funcid3) + events = [] + f.event_generate(event) + self.assertEqual(events, ['a', 'c']) + + f.unbind(event, funcid) + f.unbind(event, funcid3) + self.assertEqual(f.bind(event), '') + self.assertEqual(f.bind(), ()) + self.assertCommandNotExist(funcid) + self.assertCommandNotExist(funcid2) + self.assertCommandNotExist(funcid3) + events = [] + f.event_generate(event) + self.assertEqual(events, []) + + # non-idempotent + self.assertRaises(tkinter.TclError, f.unbind, event, funcid2) + + def test_bind_rebind(self): + event = '' + f = self.frame + self.assertEqual(f.bind(), ()) + self.assertEqual(f.bind(event), '') + def test1(e): pass + def test2(e): pass + def test3(e): pass + + funcid = f.bind(event, test1) + funcid2 = f.bind(event, test2, add=True) + script = f.bind(event) + self.assertIn(funcid2, script) + self.assertIn(funcid, script) + self.assertCommandExist(funcid) + self.assertCommandExist(funcid2) + + funcid3 = f.bind(event, test3) + script = f.bind(event) + self.assertNotIn(funcid, script) + self.assertNotIn(funcid2, script) + self.assertIn(funcid3, script) + self.assertCommandNotExist(funcid) + self.assertCommandNotExist(funcid2) + self.assertCommandExist(funcid3) + + def test_bind_class(self): + event = '' + bind_class = self.root.bind_class + unbind_class = self.root.unbind_class + self.assertRaises(TypeError, bind_class) + self.assertEqual(bind_class('Test'), ()) + self.assertEqual(bind_class('Test', event), '') + self.addCleanup(unbind_class, 'Test', event) + def test1(e): pass + def test2(e): pass + + funcid = bind_class('Test', event, test1) + self.assertEqual(bind_class('Test'), (event,)) + script = bind_class('Test', event) + self.assertIn(funcid, script) + self.assertCommandExist(funcid) + + funcid2 = bind_class('Test', event, test2, add=True) + script = bind_class('Test', event) + self.assertIn(funcid, script) + self.assertIn(funcid2, script) + self.assertCommandExist(funcid) + self.assertCommandExist(funcid2) + + def test_unbind_class(self): + event = '' + bind_class = self.root.bind_class + unbind_class = self.root.unbind_class + self.assertEqual(bind_class('Test'), ()) + self.assertEqual(bind_class('Test', event), '') + self.addCleanup(unbind_class, 'Test', event) + def test1(e): pass + def test2(e): pass + + funcid = bind_class('Test', event, test1) + funcid2 = bind_class('Test', event, test2, add=True) + + self.assertRaises(TypeError, unbind_class) + self.assertRaises(TypeError, unbind_class, 'Test') + unbind_class('Test', event) + self.assertEqual(bind_class('Test', event), '') + self.assertEqual(bind_class('Test'), ()) + self.assertCommandNotExist(funcid) + self.assertCommandNotExist(funcid2) + + unbind_class('Test', event) # idempotent + + def test_bind_class_rebind(self): + event = '' + bind_class = self.root.bind_class + unbind_class = self.root.unbind_class + self.assertEqual(bind_class('Test'), ()) + self.assertEqual(bind_class('Test', event), '') + self.addCleanup(unbind_class, 'Test', event) + def test1(e): pass + def test2(e): pass + def test3(e): pass + + funcid = bind_class('Test', event, test1) + funcid2 = bind_class('Test', event, test2, add=True) + script = bind_class('Test', event) + self.assertIn(funcid2, script) + self.assertIn(funcid, script) + self.assertCommandExist(funcid) + self.assertCommandExist(funcid2) + + funcid3 = bind_class('Test', event, test3) + script = bind_class('Test', event) + self.assertNotIn(funcid, script) + self.assertNotIn(funcid2, script) + self.assertIn(funcid3, script) + self.assertCommandNotExist(funcid) + self.assertCommandNotExist(funcid2) + self.assertCommandExist(funcid3) + + def test_bind_all(self): + event = '' + bind_all = self.root.bind_all + unbind_all = self.root.unbind_all + self.assertNotIn(event, bind_all()) + self.assertEqual(bind_all(event), '') + self.addCleanup(unbind_all, event) + def test1(e): pass + def test2(e): pass + + funcid = bind_all(event, test1) + self.assertIn(event, bind_all()) + script = bind_all(event) + self.assertIn(funcid, script) + self.assertCommandExist(funcid) + + funcid2 = bind_all(event, test2, add=True) + script = bind_all(event) + self.assertIn(funcid, script) + self.assertIn(funcid2, script) + self.assertCommandExist(funcid) + self.assertCommandExist(funcid2) + + def test_unbind_all(self): + event = '' + bind_all = self.root.bind_all + unbind_all = self.root.unbind_all + self.assertNotIn(event, bind_all()) + self.assertEqual(bind_all(event), '') + self.addCleanup(unbind_all, event) + def test1(e): pass + def test2(e): pass + + funcid = bind_all(event, test1) + funcid2 = bind_all(event, test2, add=True) + + unbind_all(event) + self.assertEqual(bind_all(event), '') + self.assertNotIn(event, bind_all()) + self.assertCommandNotExist(funcid) + self.assertCommandNotExist(funcid2) + + unbind_all(event) # idempotent + + def test_bind_all_rebind(self): + event = '' + bind_all = self.root.bind_all + unbind_all = self.root.unbind_all + self.assertNotIn(event, bind_all()) + self.assertEqual(bind_all(event), '') + self.addCleanup(unbind_all, event) + def test1(e): pass + def test2(e): pass + def test3(e): pass + + funcid = bind_all(event, test1) + funcid2 = bind_all(event, test2, add=True) + script = bind_all(event) + self.assertIn(funcid2, script) + self.assertIn(funcid, script) + self.assertCommandExist(funcid) + self.assertCommandExist(funcid2) + + funcid3 = bind_all(event, test3) + script = bind_all(event) + self.assertNotIn(funcid, script) + self.assertNotIn(funcid2, script) + self.assertIn(funcid3, script) + self.assertCommandNotExist(funcid) + self.assertCommandNotExist(funcid2) + self.assertCommandExist(funcid3) + + def _test_tag_bind(self, w): + tag = 'sel' + event = '' + w.pack() + self.assertRaises(TypeError, w.tag_bind) + tag_bind = w._tag_bind if isinstance(w, tkinter.Text) else w.tag_bind + if isinstance(w, tkinter.Text): + self.assertRaises(TypeError, w.tag_bind, tag) + self.assertRaises(TypeError, w.tag_bind, tag, event) + self.assertEqual(tag_bind(tag), ()) + self.assertEqual(tag_bind(tag, event), '') + def test1(e): pass + def test2(e): pass + + funcid = w.tag_bind(tag, event, test1) + self.assertEqual(tag_bind(tag), (event,)) + script = tag_bind(tag, event) + self.assertIn(funcid, script) + self.assertCommandExist(funcid) + + funcid2 = w.tag_bind(tag, event, test2, add=True) + script = tag_bind(tag, event) + self.assertIn(funcid, script) + self.assertIn(funcid2, script) + self.assertCommandExist(funcid) + self.assertCommandExist(funcid2) + + def _test_tag_unbind(self, w): + tag = 'sel' + event = '' + w.pack() + tag_bind = w._tag_bind if isinstance(w, tkinter.Text) else w.tag_bind + self.assertEqual(tag_bind(tag), ()) + self.assertEqual(tag_bind(tag, event), '') + def test1(e): pass + def test2(e): pass + + funcid = w.tag_bind(tag, event, test1) + funcid2 = w.tag_bind(tag, event, test2, add=True) + + self.assertRaises(TypeError, w.tag_unbind, tag) + w.tag_unbind(tag, event) + self.assertEqual(tag_bind(tag, event), '') + self.assertEqual(tag_bind(tag), ()) + + def _test_tag_bind_rebind(self, w): + tag = 'sel' + event = '' + w.pack() + tag_bind = w._tag_bind if isinstance(w, tkinter.Text) else w.tag_bind + self.assertEqual(tag_bind(tag), ()) + self.assertEqual(tag_bind(tag, event), '') + def test1(e): pass + def test2(e): pass + def test3(e): pass + + funcid = w.tag_bind(tag, event, test1) + funcid2 = w.tag_bind(tag, event, test2, add=True) + script = tag_bind(tag, event) + self.assertIn(funcid2, script) + self.assertIn(funcid, script) + self.assertCommandExist(funcid) + self.assertCommandExist(funcid2) + + funcid3 = w.tag_bind(tag, event, test3) + script = tag_bind(tag, event) + self.assertNotIn(funcid, script) + self.assertNotIn(funcid2, script) + self.assertIn(funcid3, script) + self.assertCommandExist(funcid3) + + def test_canvas_tag_bind(self): + c = tkinter.Canvas(self.frame) + self._test_tag_bind(c) + + def test_canvas_tag_unbind(self): + c = tkinter.Canvas(self.frame) + self._test_tag_unbind(c) + + def test_canvas_tag_bind_rebind(self): + c = tkinter.Canvas(self.frame) + self._test_tag_bind_rebind(c) + + def test_text_tag_bind(self): + t = tkinter.Text(self.frame) + self._test_tag_bind(t) + + def test_text_tag_unbind(self): + t = tkinter.Text(self.frame) + self._test_tag_unbind(t) + + def test_text_tag_bind_rebind(self): + t = tkinter.Text(self.frame) + self._test_tag_bind_rebind(t) + + def test_bindtags(self): + f = self.frame + self.assertEqual(self.root.bindtags(), ('.', 'Tk', 'all')) + self.assertEqual(f.bindtags(), (str(f), 'Test', '.', 'all')) + f.bindtags(('a', 'b c')) + self.assertEqual(f.bindtags(), ('a', 'b c')) + + def test_bind_events(self): + event = '' + root = self.root + t = tkinter.Toplevel(root) + f = tkinter.Frame(t, class_='Test', width=150, height=100) + f.pack() + root.wait_visibility() # needed on Windows + root.update_idletasks() + self.addCleanup(root.unbind_class, 'Test', event) + self.addCleanup(root.unbind_class, 'Toplevel', event) + self.addCleanup(root.unbind_class, 'tag', event) + self.addCleanup(root.unbind_class, 'tag2', event) + self.addCleanup(root.unbind_all, event) + def test(what): + return lambda e: events.append((what, e.widget)) + + root.bind_all(event, test('all')) + root.bind_class('Test', event, test('frame class')) + root.bind_class('Toplevel', event, test('toplevel class')) + root.bind_class('tag', event, test('tag')) + root.bind_class('tag2', event, test('tag2')) + f.bind(event, test('frame')) + t.bind(event, test('toplevel')) + + events = [] + f.event_generate(event) + self.assertEqual(events, [ + ('frame', f), + ('frame class', f), + ('toplevel', f), + ('all', f), + ]) + + events = [] + t.event_generate(event) + self.assertEqual(events, [ + ('toplevel', t), + ('toplevel class', t), + ('all', t), + ]) + + f.bindtags(('tag', 'tag3')) + events = [] + f.event_generate(event) + self.assertEqual(events, [('tag', f)]) + + +class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): + + def test_default_root(self): + self.assertIs(tkinter._support_default_root, True) + self.assertIsNone(tkinter._default_root) + root = tkinter.Tk() + root2 = tkinter.Tk() + root3 = tkinter.Tk() + self.assertIs(tkinter._default_root, root) + root2.destroy() + self.assertIs(tkinter._default_root, root) + root.destroy() + self.assertIsNone(tkinter._default_root) + root3.destroy() + self.assertIsNone(tkinter._default_root) + + def test_no_default_root(self): + self.assertIs(tkinter._support_default_root, True) + self.assertIsNone(tkinter._default_root) + root = tkinter.Tk() + self.assertIs(tkinter._default_root, root) + tkinter.NoDefaultRoot() + self.assertIs(tkinter._support_default_root, False) + self.assertNotHasAttr(tkinter, '_default_root') + # repeated call is no-op + tkinter.NoDefaultRoot() + self.assertIs(tkinter._support_default_root, False) + self.assertNotHasAttr(tkinter, '_default_root') + root.destroy() + self.assertIs(tkinter._support_default_root, False) + self.assertNotHasAttr(tkinter, '_default_root') + root = tkinter.Tk() + self.assertIs(tkinter._support_default_root, False) + self.assertNotHasAttr(tkinter, '_default_root') + root.destroy() + + def test_getboolean(self): + self.assertRaises(RuntimeError, tkinter.getboolean, '1') + root = tkinter.Tk() + self.assertIs(tkinter.getboolean('1'), True) + self.assertRaises(ValueError, tkinter.getboolean, 'yea') + root.destroy() + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, tkinter.getboolean, '1') + + def test_mainloop(self): + self.assertRaises(RuntimeError, tkinter.mainloop) + root = tkinter.Tk() + root.after_idle(root.quit) + tkinter.mainloop() + root.destroy() + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, tkinter.mainloop) + + +def _info_commands(widget, pattern=None): + return widget.tk.splitlist(widget.tk.call('info', 'commands', pattern)) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tkinter/test_scrolledtext.py b/Lib/test/test_tkinter/test_scrolledtext.py new file mode 100644 index 00000000000..913e7d8aab6 --- /dev/null +++ b/Lib/test/test_tkinter/test_scrolledtext.py @@ -0,0 +1,65 @@ +import unittest +import tkinter +from tkinter.scrolledtext import ScrolledText +from test.support import requires +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import AbstractTkTest + +requires('gui') + + +class ScrolledTextTest(AbstractTkTest, unittest.TestCase): + + def create(self, **kwargs): + st = ScrolledText(self.root, **kwargs) + self.addCleanup(st.destroy) + return st + + def test_create(self): + st = self.create(background='red', height=5) + # It is a Text widget held in a Frame together with a Scrollbar. + self.assertIsInstance(st, tkinter.Text) + self.assertIsInstance(st.frame, tkinter.Frame) + self.assertIsInstance(st.vbar, tkinter.Scrollbar) + self.assertEqual(st.winfo_parent(), str(st.frame)) + # str() returns the frame, so that geometry managers manage it. + self.assertEqual(str(st), str(st.frame)) + # Keyword options configure the Text. + self.assertEqual(str(st['background']), 'red') + self.assertEqual(st['height'], 5 if self.wantobjects else '5') + + def test_text_methods(self): + st = self.create() + st.insert('1.0', 'hello\nworld') + self.assertEqual(st.get('1.0', 'end-1c'), 'hello\nworld') + self.assertEqual(st.index('end-1c'), '2.5') + st.delete('1.0', 'end') + self.assertEqual(st.get('1.0', 'end-1c'), '') + + def test_geometry_methods(self): + st = self.create() + # configure is not redirected; it configures the Text. + st.configure(height=8) + self.assertEqual(st['height'], 8 if self.wantobjects else '8') + # Pack, Grid and Place methods are redirected to the frame. + st.pack() + self.root.update() + self.assertEqual(st.frame.winfo_manager(), 'pack') + self.assertEqual(st.pack_info(), st.frame.pack_info()) + st.pack_forget() + self.assertEqual(st.frame.winfo_manager(), '') + + def test_scrollbar(self): + st = self.create(height=5) + st.pack() + st.insert('1.0', '\n'.join(map(str, range(100)))) + self.root.update() + # The scrollbar tracks the text view. + self.assertEqual(st.vbar.get(), st.yview()) + st.yview_moveto(1.0) + self.root.update() + self.assertEqual(st.vbar.get()[1], 1.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tkinter/test_simpledialog.py b/Lib/test/test_tkinter/test_simpledialog.py new file mode 100644 index 00000000000..1cb03074d89 --- /dev/null +++ b/Lib/test/test_tkinter/test_simpledialog.py @@ -0,0 +1,279 @@ +import unittest +import tkinter +from tkinter import messagebox +from test.support import requires, swap_attr +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import (AbstractDefaultRootTest, + AbstractDialogTest, AbstractTkTest) +from tkinter.simpledialog import (Dialog, SimpleDialog, + askinteger, askfloat, askstring, + _QueryInteger, _QueryFloat, _QueryString) + +requires('gui') + + +class SimpleDialogTest(AbstractTkTest, unittest.TestCase): + # The root window is not hidden here (cf. AbstractDialogTest): SimpleDialog + # makes its window transient for the master unconditionally, and a + # transient of a hidden window is never mapped on Windows, so that go() + # would block in wait_visibility(). + # SimpleDialog's modal loop is in go(); its bindings are exercised here by + # generating events on the constructed dialog, without entering the loop. + + def create(self, **kw): + kw.setdefault('text', 'Question?') + kw.setdefault('buttons', ['Yes', 'No']) + kw.setdefault('default', 0) + kw.setdefault('cancel', 1) + d = SimpleDialog(self.root, **kw) + self.addCleanup(lambda: d.root.winfo_exists() and d.root.destroy()) + return d + + def test_message(self): + # The text is shown in a message widget. + d = self.create(text='Hello?') + self.assertEqual(d.message.winfo_class(), 'Message') + self.assertEqual(str(d.message.cget('text')), 'Hello?') + + def test_class_name(self): + # class_ sets the Tk class of the dialog window. + d = self.create(class_='MyDialog') + self.assertEqual(d.root.winfo_class(), 'MyDialog') + + def test_button(self): + # Pressing a button records its index. + d = self.create(buttons=['Yes', 'No']) + d.frame.winfo_children()[1].invoke() # "No" + self.assertEqual(d.num, 1) + + def test_default_button(self): + # The default button is drawn with a raised border. + d = self.create(buttons=['Yes', 'No'], default=0) + self.assertEqual(str(d.frame.winfo_children()[0].cget('relief')), 'ridge') + + def test_return_activates_default(self): + # invokes the default button. + d = self.create() # default 0 + self.require_mapped(d.root) + d.root.update() + d.root.focus_force() + d.root.event_generate('') + d.root.update() + self.assertEqual(d.num, 0) + + def test_return_no_default(self): + # With no default button, rings the bell and leaves the dialog + # open instead of activating a button. + d = self.create(default=None) + self.require_mapped(d.root) + bells = [] + with swap_attr(d.root, 'bell', lambda *a, **k: bells.append(True)): + d.root.focus_force() + d.root.event_generate('') + d.root.update() + self.assertTrue(bells) # rang the bell + self.assertIsNone(d.num) + self.assertTrue(d.root.winfo_exists()) + + def test_wm_delete_cancels(self): + # Closing the window through the window manager records the cancel index. + d = self.create() # cancel 1 + d.wm_delete_window() + self.assertEqual(d.num, 1) + + def test_wm_delete_no_cancel(self): + # With no cancel index, closing the window through the window manager + # rings the bell and leaves the dialog open instead of recording an + # index. + d = self.create(default=None, cancel=None) + d.root.update() + bells = [] + with swap_attr(d.root, 'bell', lambda *a, **k: bells.append(True)): + d.wm_delete_window() + d.root.update() + self.assertTrue(bells) # rang the bell + self.assertIsNone(d.num) + self.assertTrue(d.root.winfo_exists()) + + def test_go(self): + # go() runs the modal loop and returns the chosen button's index. + d = self.create() + d.root.after(1, lambda: d.done(0)) + self.assertEqual(d.go(), 0) + + +class DialogTest(AbstractDialogTest, unittest.TestCase): + # Dialog is a base class for custom dialogs; exercise it via _QueryInteger. + + def open(self, **kw): + with swap_attr(Dialog, 'wait_window', staticmethod(lambda w: None)): + d = _QueryInteger('Title', 'Prompt', parent=self.root, **kw) + self.addCleanup(lambda: d.winfo_exists() and d.destroy()) + return d + + def buttons(self, d): + # Map the button box's buttons by their label. + return {str(b.cget('text')): b + for frame in d.winfo_children() + for b in frame.winfo_children() + if b.winfo_class() == 'Button'} + + def test_background(self): + # The classic dialog keeps the default Toplevel background. + d = self.open() + ref = tkinter.Toplevel(self.root) + self.addCleanup(ref.destroy) + self.assertEqual(str(d.cget('background')), str(ref.cget('background'))) + + def test_buttons(self): + # The button box has OK (the default) and Cancel buttons. + buttons = self.buttons(self.open()) + self.assertEqual(set(buttons), {'OK', 'Cancel'}) + self.assertEqual(str(buttons['OK'].cget('default')), 'active') + + def test_ok(self): + # The OK button validates the entry and stores the result. + d = self.open() + d.entry.insert(0, '42') + self.buttons(d)['OK'].invoke() + self.assertEqual(d.result, 42) + self.assertFalse(d.winfo_exists()) # The dialog closed. + + def test_cancel(self): + # The Cancel button closes the dialog without a result. + d = self.open() + self.buttons(d)['Cancel'].invoke() + self.assertIsNone(d.result) + self.assertFalse(d.winfo_exists()) + + +class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): + + def test_askinteger(self): + @staticmethod + def mock_wait_window(w): + nonlocal ismapped + ismapped = w.master.winfo_ismapped() + w.destroy() + + with swap_attr(Dialog, 'wait_window', mock_wait_window): + ismapped = None + askinteger("Go To Line", "Line number") + self.assertEqual(ismapped, False) + + root = tkinter.Tk() + ismapped = None + askinteger("Go To Line", "Line number") + self.assertEqual(ismapped, True) + root.destroy() + + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number") + + +class QueryDialogTest(AbstractDialogTest, unittest.TestCase): + # The query dialogs are modal: their __init__ blocks in wait_window(). + # Mock that out so the dialog stays alive and can be driven with generated + # events, exercising the / bindings and the validation. + + def open(self, query, **kw): + with swap_attr(Dialog, 'wait_window', staticmethod(lambda w: None)): + d = query("Title", "Prompt", parent=self.root, **kw) + self.addCleanup(lambda: d.winfo_exists() and d.destroy()) + d.focus_force() + d.update() + return d + + def enter(self, d, value, key=''): + d.entry.delete(0, 'end') + d.entry.insert(0, value) + d.event_generate(key) + d.update() + + def test_initialvalue(self): + # The entry is pre-filled with the initial value, which is accepted. + d = self.open(_QueryInteger, initialvalue=42) + self.assertEqual(d.entry.get(), '42') + d.event_generate('') + d.update() + self.assertEqual(d.result, 42) + + def test_show(self): + # _QueryString hides the entered text when show is given. + d = self.open(_QueryString, show='*') + self.assertEqual(str(d.entry.cget('show')), '*') + + def test_return_accepts(self): + for query, value, expected in [ + (_QueryInteger, '42', 42), + (_QueryFloat, '1.5', 1.5), + (_QueryString, 'spam', 'spam'), + ]: + with self.subTest(query=query.__name__): + d = self.open(query) + self.enter(d, value) + self.assertEqual(d.result, expected) + self.assertFalse(d.winfo_exists()) # The dialog closed. + + def test_escape_cancels(self): + d = self.open(_QueryString) + self.enter(d, 'spam', '') + self.assertIsNone(d.result) + self.assertFalse(d.winfo_exists()) + + def test_invalid_value(self): + warnings = [] + d = self.open(_QueryInteger) + with swap_attr(messagebox, 'showwarning', + lambda *a, **k: warnings.append(a)): + self.enter(d, 'not a number') + self.assertIsNone(d.result) + self.assertTrue(d.winfo_exists()) # The dialog stays open. + self.assertTrue(warnings) + + def test_out_of_range(self): + warnings = [] + d = self.open(_QueryInteger, minvalue=10, maxvalue=20) + with swap_attr(messagebox, 'showwarning', + lambda *a, **k: warnings.append(a)): + self.enter(d, '5') # Below the minimum. + self.assertIsNone(d.result) + self.enter(d, '25') # Above the maximum. + self.assertIsNone(d.result) + self.assertTrue(d.winfo_exists()) + self.assertEqual(len(warnings), 2) + + def test_boundary_values_accepted(self): + # The min/max checks are inclusive: a value equal to a bound passes. + d = self.open(_QueryInteger, minvalue=10, maxvalue=20) + self.enter(d, '10') # Exactly the minimum. + self.assertEqual(d.result, 10) + self.assertFalse(d.winfo_exists()) + + d = self.open(_QueryInteger, minvalue=10, maxvalue=20) + self.enter(d, '20') # Exactly the maximum. + self.assertEqual(d.result, 20) + self.assertFalse(d.winfo_exists()) + + def run_ask(self, ask, value, **kw): + # Drive a modal ask* function: enter a value and accept it. + def accept(d): + d.entry.delete(0, 'end') + d.entry.insert(0, value) + d.ok() + with swap_attr(Dialog, 'wait_window', staticmethod(accept)): + return ask('Title', 'Prompt', parent=self.root, **kw) + + def test_ask_functions(self): + self.assertEqual(self.run_ask(askinteger, '42'), 42) + self.assertEqual(self.run_ask(askfloat, '1.5'), 1.5) + self.assertEqual(self.run_ask(askstring, 'spam'), 'spam') + + def test_ask_cancelled(self): + # A cancelled ask* returns None. + with swap_attr(Dialog, 'wait_window', staticmethod(lambda d: d.cancel())): + self.assertIsNone(askstring('Title', 'Prompt', parent=self.root)) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tkinter/test_text.py b/Lib/test/test_tkinter/test_text.py new file mode 100644 index 00000000000..6379a6ca984 --- /dev/null +++ b/Lib/test/test_tkinter/test_text.py @@ -0,0 +1,747 @@ +import unittest +import tkinter +from tkinter import TclError +from test.support import requires +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import AbstractTkTest, requires_tk + +requires('gui') + +class TextTest(AbstractTkTest, unittest.TestCase): + + def setUp(self): + super().setUp() + self.text = tkinter.Text(self.root) + self.text.pack() + + def test_debug(self): + text = self.text + olddebug = text.debug() + try: + text.debug(False) + self.assertIs(text.debug(), False) + text.debug(True) + self.assertIs(text.debug(), True) + finally: + text.debug(olddebug) + self.assertEqual(text.debug(), olddebug) + + def test_index(self): + text = self.text + text.insert('1.0', 'Lorem ipsum\ndolor sit amet') + self.assertEqual(text.index('1.0'), '1.0') + self.assertEqual(text.index('1.end'), '1.11') + self.assertEqual(text.index('end'), '3.0') + self.assertEqual(text.index('2.5'), '2.5') + # Index past the end of a line is clamped to its end. + self.assertEqual(text.index('1.100'), '1.11') + self.assertRaises(TclError, text.index, 'invalid') + self.assertRaises(TypeError, text.index) + self.assertRaises(TypeError, text.index, '1.0', '2.5') + + def test_compare(self): + text = self.text + text.insert('1.0', 'Lorem ipsum\ndolor sit amet') + self.assertIs(text.compare('1.0', '<', '2.0'), True) + self.assertIs(text.compare('2.0', '<', '1.0'), False) + self.assertIs(text.compare('1.5', '==', '1.5'), True) + self.assertIs(text.compare('1.5', '!=', '1.5'), False) + self.assertIs(text.compare('2.0', '>=', '2.0'), True) + self.assertIs(text.compare('1.0', '<=', 'end'), True) + self.assertRaises(TclError, text.compare, '1.0', 'invalid', '2.0') + self.assertRaises(TypeError, text.compare, '1.0', '<') + self.assertRaises(TypeError, text.compare, '1.0', '<', '2.0', '3.0') + + def test_insert_get_delete(self): + text = self.text + text.insert('1.0', 'Lorem ipsum') + self.assertEqual(text.get('1.0', 'end'), 'Lorem ipsum\n') + self.assertEqual(text.get('1.0', '1.5'), 'Lorem') + self.assertEqual(text.get('1.6'), 'i') # single character + + # insert before an existing index + text.insert('1.0', '*** ') + self.assertEqual(text.get('1.0', 'end'), '*** Lorem ipsum\n') + + text.delete('1.0', '1.4') + self.assertEqual(text.get('1.0', 'end'), 'Lorem ipsum\n') + text.delete('1.5') # delete a single character + self.assertEqual(text.get('1.0', 'end'), 'Loremipsum\n') + self.assertRaises(TypeError, text.get) + self.assertRaises(TypeError, text.get, '1.0', '1.5', 'end') + self.assertRaises(TypeError, text.delete, '1.0', '1.5', 'end') + + def test_insert_with_tags(self): + text = self.text + text.insert('1.0', 'hello', 'a', ' ', ('a', 'b'), 'world', 'b') + self.assertEqual(text.get('1.0', 'end'), 'hello world\n') + self.assertEqual([str(i) for i in text.tag_ranges('a')], + ['1.0', '1.6']) + self.assertEqual([str(i) for i in text.tag_ranges('b')], + ['1.5', '1.11']) + + def test_replace(self): + text = self.text + text.insert('1.0', 'Lorem ipsum') + text.replace('1.0', '1.5', 'Hello') + self.assertEqual(text.get('1.0', 'end'), 'Hello ipsum\n') + text.replace('1.6', 'end - 1 char', 'world') + self.assertEqual(text.get('1.0', 'end'), 'Hello world\n') + self.assertRaises(TclError, text.replace, 'invalid', '1.5', 'x') + # The first index must not be after the second. + self.assertRaises(TclError, text.replace, '1.5', '1.0', 'x') + self.assertRaises(TypeError, text.replace, '1.0', '1.5') + + def test_mark(self): + text = self.text + text.insert('1.0', 'Lorem ipsum\ndolor sit amet') + # 'insert' and 'current' marks always exist. + self.assertIn('insert', text.mark_names()) + self.assertIn('current', text.mark_names()) + + text.mark_set('here', '1.5') + self.assertIn('here', text.mark_names()) + self.assertEqual(text.index('here'), '1.5') + text.mark_set('here', '2.3') + self.assertEqual(text.index('here'), '2.3') + + text.mark_unset('here') + self.assertNotIn('here', text.mark_names()) + + # Unsetting a non-existent mark is not an error. + text.mark_unset('nonexistent') + self.assertRaises(TclError, text.mark_set, 'm', 'invalid') + self.assertRaises(TypeError, text.mark_set, 'm') + self.assertRaises(TypeError, text.mark_set, 'm', '1.0', '2.0') + + def test_mark_gravity(self): + text = self.text + text.insert('1.0', 'Lorem ipsum') + text.mark_set('here', '1.5') + # The default gravity is right. + self.assertEqual(text.mark_gravity('here'), 'right') + text.mark_gravity('here', 'left') + self.assertEqual(text.mark_gravity('here'), 'left') + # With left gravity the mark stays before inserted text. + text.insert('here', 'XXX') + self.assertEqual(text.index('here'), '1.5') + # With right gravity the mark moves after inserted text. + text.mark_gravity('here', 'right') + text.insert('here', 'YYY') + self.assertEqual(text.index('here'), '1.8') + + self.assertRaises(TclError, text.mark_gravity, 'nonexistent') + self.assertRaises(TclError, text.mark_gravity, 'here', 'invalid') + self.assertRaises(TypeError, text.mark_gravity) + self.assertRaises(TypeError, text.mark_gravity, 'here', 'left', 'extra') + + def test_mark_next_previous(self): + text = self.text + text.insert('1.0', 'Lorem ipsum') + # Keep the builtin 'insert' and 'current' marks at 1.0 so they do + # not interfere with the queries below. + text.mark_set('insert', '1.0') + text.mark_set('current', '1.0') + text.mark_set('m1', '1.3') + text.mark_set('m2', '1.7') + # mark_next finds a mark at a position at or after the index, + # mark_previous finds one at a position strictly before it. + self.assertEqual(text.mark_next('1.1'), 'm1') + self.assertEqual(text.mark_next('1.3'), 'm1') + self.assertEqual(text.mark_next('1.4'), 'm2') + self.assertIsNone(text.mark_next('1.8')) + self.assertEqual(text.mark_previous('1.4'), 'm1') + self.assertEqual(text.mark_previous('1.7'), 'm1') + self.assertEqual(text.mark_previous('end'), 'm2') + self.assertIsNone(text.mark_previous('1.0')) + + self.assertRaises(TclError, text.mark_next, 'invalid') + self.assertRaises(TclError, text.mark_previous, 'invalid') + self.assertRaises(TypeError, text.mark_next) + self.assertRaises(TypeError, text.mark_previous) + self.assertRaises(TypeError, text.mark_next, '1.0', '2.0') + self.assertRaises(TypeError, text.mark_previous, '1.0', '2.0') + + def test_tag_add_remove_ranges(self): + text = self.text + text.insert('1.0', 'Lorem ipsum\ndolor sit amet') + self.assertEqual(text.tag_ranges('sel'), ()) + + text.tag_add('big', '1.0', '1.5') + self.assertEqual([str(i) for i in text.tag_ranges('big')], + ['1.0', '1.5']) + # Add a second, disjoint range. + text.tag_add('big', '2.0', '2.5') + self.assertEqual([str(i) for i in text.tag_ranges('big')], + ['1.0', '1.5', '2.0', '2.5']) + + text.tag_remove('big', '1.0', '1.3') + self.assertEqual([str(i) for i in text.tag_ranges('big')], + ['1.3', '1.5', '2.0', '2.5']) + + # tag_ranges of an undefined tag is empty, not an error. + self.assertEqual(text.tag_ranges('nonexistent'), ()) + self.assertRaises(TclError, text.tag_add, 'big', 'invalid') + self.assertRaises(TypeError, text.tag_add, 'big') + self.assertRaises(TclError, text.tag_remove, 'big', 'invalid') + self.assertRaises(TypeError, text.tag_remove, 'big', '1.0', '2.0', '3.0') + self.assertRaises(TypeError, text.tag_ranges, 'big', 'extra') + + def test_tag_names(self): + text = self.text + text.insert('1.0', 'Lorem ipsum') + text.tag_add('a', '1.0', '1.5') + text.tag_add('b', '1.3', '1.8') + # 'sel' always exists; order reflects priority (creation order). + self.assertEqual(set(text.tag_names()), {'sel', 'a', 'b'}) + # Tags applied to the character at the given index. + self.assertEqual(set(text.tag_names('1.4')), {'a', 'b'}) + self.assertEqual(set(text.tag_names('1.0')), {'a'}) + self.assertEqual(set(text.tag_names('1.10')), set()) + self.assertRaises(TclError, text.tag_names, 'invalid') + self.assertRaises(TypeError, text.tag_names, '1.0', '2.0') + + def test_tag_nextrange_prevrange(self): + text = self.text + text.insert('1.0', 'Lorem ipsum dolor') + text.tag_add('a', '1.0', '1.5') + text.tag_add('a', '1.12', '1.17') + + self.assertEqual([str(i) for i in text.tag_nextrange('a', '1.0')], + ['1.0', '1.5']) + self.assertEqual([str(i) for i in text.tag_nextrange('a', '1.5')], + ['1.12', '1.17']) + self.assertEqual(text.tag_nextrange('a', '1.17'), ()) + + self.assertEqual([str(i) for i in text.tag_prevrange('a', 'end')], + ['1.12', '1.17']) + self.assertEqual([str(i) for i in text.tag_prevrange('a', '1.12')], + ['1.0', '1.5']) + self.assertEqual(text.tag_prevrange('a', '1.0'), ()) + + # An undefined tag has no ranges, but the index must be valid. + self.assertEqual(text.tag_nextrange('nonexistent', '1.0'), ()) + self.assertRaises(TclError, text.tag_nextrange, 'a', 'invalid') + self.assertRaises(TclError, text.tag_prevrange, 'a', 'invalid') + self.assertRaises(TypeError, text.tag_nextrange, 'a') + self.assertRaises(TypeError, text.tag_prevrange, 'a') + self.assertRaises(TypeError, text.tag_nextrange, 'a', '1.0', '2.0', '3.0') + self.assertRaises(TypeError, text.tag_prevrange, 'a', '1.0', '2.0', '3.0') + + def test_tag_delete(self): + text = self.text + text.insert('1.0', 'Lorem ipsum') + text.tag_add('a', '1.0', '1.5') + text.tag_add('b', '1.6', '1.11') + self.assertEqual(set(text.tag_names()), {'sel', 'a', 'b'}) + text.tag_delete('a', 'b') + self.assertEqual(set(text.tag_names()), {'sel'}) + self.assertEqual(text.tag_ranges('a'), ()) + + def test_tag_raise_lower(self): + text = self.text + text.tag_configure('a', foreground='red') + text.tag_configure('b', foreground='green') + text.tag_configure('c', foreground='blue') + # Creation order is lowest-to-highest priority; tag_names lists + # them in increasing priority order. + self.assertEqual(text.tag_names(), ('sel', 'a', 'b', 'c')) + text.tag_raise('a') + self.assertEqual(text.tag_names(), ('sel', 'b', 'c', 'a')) + text.tag_lower('a') + self.assertEqual(text.tag_names(), ('a', 'sel', 'b', 'c')) + text.tag_raise('a', 'b') + self.assertEqual(text.tag_names(), ('sel', 'b', 'a', 'c')) + text.tag_lower('c', 'b') + self.assertEqual(text.tag_names(), ('sel', 'c', 'b', 'a')) + + self.assertRaises(TclError, text.tag_raise, 'nonexistent') + self.assertRaises(TclError, text.tag_lower, 'nonexistent') + self.assertRaises(TclError, text.tag_raise, 'a', 'nonexistent') + self.assertRaises(TypeError, text.tag_raise) + self.assertRaises(TypeError, text.tag_raise, 'a', 'b', 'c') + self.assertRaises(TypeError, text.tag_lower, 'a', 'b', 'c') + + def test_tag_cget_configure(self): + text = self.text + text.tag_configure('a', foreground='red', underline=True) + self.assertEqual(str(text.tag_cget('a', 'foreground')), 'red') + self.assertIn(text.tag_cget('a', 'underline'), (1, '1', True)) + # tag_cget normalizes the option name (no leading hyphen needed). + self.assertEqual(str(text.tag_cget('a', '-foreground')), 'red') + # configure() query returns the full option spec. + self.assertEqual(text.tag_configure('a', 'foreground')[-1], 'red') + text.tag_configure('a', foreground='blue') + self.assertEqual(str(text.tag_cget('a', 'foreground')), 'blue') + + self.assertRaises(TclError, text.tag_cget, 'nonexistent', + 'foreground') + self.assertRaises(TypeError, text.tag_cget, 'a') + self.assertRaises(TypeError, text.tag_cget, 'a', 'foreground', 'extra') + + def test_edit_modified(self): + text = self.text + self.assertEqual(text.edit_modified(), 0) + text.insert('1.0', 'spam') + self.assertEqual(text.edit_modified(), 1) + text.edit_modified(False) + self.assertEqual(text.edit_modified(), 0) + text.edit_modified(True) + self.assertEqual(text.edit_modified(), 1) + + def test_edit_undo_redo(self): + text = self.text + text.configure(undo=True) + text.insert('1.0', 'spam') + text.edit_separator() + text.insert('end', ' eggs') + self.assertEqual(text.get('1.0', 'end'), 'spam eggs\n') + + text.edit_undo() + self.assertEqual(text.get('1.0', 'end'), 'spam\n') + text.edit_redo() + self.assertEqual(text.get('1.0', 'end'), 'spam eggs\n') + + # An empty undo stack raises. + text.edit_reset() + self.assertRaises(TclError, text.edit_undo) + + def test_dump(self): + text = self.text + text.insert('1.0', 'hello') + text.tag_add('a', '1.0', '1.3') + text.mark_set('here', '1.2') + + dump = text.dump('1.0', 'end') + self.assertIsInstance(dump, list) + for item in dump: + self.assertIsInstance(item, tuple) + self.assertEqual(len(item), 3) + kinds = {item[0] for item in dump} + self.assertIn('text', kinds) + self.assertIn('tagon', kinds) + self.assertIn('mark', kinds) + + # Filtering by kind. + text_only = text.dump('1.0', 'end', text=True) + self.assertEqual({item[0] for item in text_only}, {'text'}) + self.assertEqual(''.join(item[1] for item in text_only), 'hello\n') + + # The command callback receives each triple and suppresses the result. + collected = [] + result = text.dump('1.0', 'end', text=True, + command=lambda *a: collected.append(a)) + self.assertIsNone(result) + self.assertEqual(''.join(key_value_index[1] + for key_value_index in collected), 'hello\n') + + self.assertRaises(TclError, text.dump, 'invalid') + self.assertRaises(TypeError, text.dump) + self.assertRaises(TypeError, text.dump, '1.0', 'end', None, 'extra') + + def test_image(self): + text = self.text + image = tkinter.PhotoImage(master=self.root, width=10, height=10) + text.insert('1.0', 'AB') + name = text.image_create('1.1', image=image) + self.assertIsInstance(name, str) + self.assertIn(name, self.root.splitlist(text.image_names())) + self.assertEqual(str(text.image_cget(name, 'image')), str(image)) + + text.image_configure(name, align='top') + self.assertEqual(str(text.image_cget(name, 'align')), 'top') + + # An embedded image occupies a single index position. + self.assertEqual(text.index('end - 1 char'), '1.3') + + # Either a name or an image is required, and the index must be valid. + self.assertRaises(TclError, text.image_create, '1.0') + self.assertRaises(TclError, text.image_create, 'invalid', + image=image) + self.assertRaises(TypeError, text.image_cget, name) + self.assertRaises(TypeError, text.image_cget, name, 'image', 'extra') + + def test_window(self): + text = self.text + button = tkinter.Button(text, text='ok') + text.insert('1.0', 'AB') + text.window_create('1.1', window=button) + self.assertEqual(self.root.splitlist(text.window_names()), + (str(button),)) + self.assertEqual(text.window_cget('1.1', 'window'), str(button)) + + text.window_configure('1.1', padx=5) + self.assertEqual(text.window_cget('1.1', 'padx'), 5) + + # No embedded window at this index, and the index must be valid. + self.assertRaises(TclError, text.window_cget, '1.0', 'window') + self.assertRaises(TclError, text.window_cget, 'invalid', 'window') + self.assertRaises(TypeError, text.window_cget, '1.1') + self.assertRaises(TypeError, text.window_cget, '1.1', 'window', 'extra') + button.destroy() + + def test_tag_configure(self): + text = self.text + tag = 'a' + getint = text.tk.getint + getboolean = text.tk.getboolean + + # Color options. + for opt in ('foreground', 'background'): + text.tag_configure(tag, **{opt: '#ff0000'}) + self.assertEqual(str(text.tag_cget(tag, opt)), '#ff0000') + + # Stipple (bitmap) options. + for opt in ('bgstipple', 'fgstipple'): + text.tag_configure(tag, **{opt: 'gray50'}) + self.assertEqual(str(text.tag_cget(tag, opt)), 'gray50') + + # Enumerated options. + for opt, value in (('justify', 'center'), ('wrap', 'word'), + ('relief', 'raised'), ('tabstyle', 'wordprocessor'), + ('offset', '5')): + text.tag_configure(tag, **{opt: value}) + self.assertEqual(str(text.tag_cget(tag, 'justify')), 'center') + self.assertEqual(str(text.tag_cget(tag, 'wrap')), 'word') + self.assertEqual(str(text.tag_cget(tag, 'relief')), 'raised') + self.assertEqual(str(text.tag_cget(tag, 'tabstyle')), 'wordprocessor') + + # Boolean options. + for opt in ('underline', 'overstrike', 'elide'): + text.tag_configure(tag, **{opt: True}) + self.assertIs(getboolean(text.tag_cget(tag, opt)), True) + text.tag_configure(tag, **{opt: False}) + self.assertIs(getboolean(text.tag_cget(tag, opt)), False) + + # Screen-distance (pixel) options. + for opt in ('borderwidth', 'lmargin1', 'lmargin2', 'rmargin', + 'spacing1', 'spacing2', 'spacing3'): + text.tag_configure(tag, **{opt: 7}) + self.assertEqual(getint(text.tag_cget(tag, opt)), 7) + + # Other options. + text.tag_configure(tag, font='Helvetica 12') + self.assertEqual(str(text.tag_cget(tag, 'font')), 'Helvetica 12') + text.tag_configure(tag, tabs=(10.2, '1i')) + self.assertEqual([str(x) for x in text.tag_ranges('sel')], []) + + self.assertRaises(TclError, text.tag_cget, tag, 'spam') + + @requires_tk(8, 6, 6) + def test_tag_configure_colors(self): + # Tag color options added in Tk 8.6.6. + text = self.text + tag = 'a' + for opt in ('selectforeground', 'selectbackground', + 'lmargincolor', 'rmargincolor', + 'underlinefg', 'overstrikefg'): + text.tag_configure(tag, **{opt: '#00ff00'}) + self.assertEqual(str(text.tag_cget(tag, opt)), '#00ff00') + + def test_tag_configure_query(self): + text = self.text + tag = 'a' + # Querying all options returns a dict keyed by option name. + cnf = text.tag_configure(tag) + self.assertIsInstance(cnf, dict) + self.assertIn('foreground', cnf) + # The value is the full 5-tuple option specification. + self.assertEqual(len(cnf['foreground']), 5) + + # Querying a single option returns its specification. + spec = text.tag_configure(tag, 'foreground') + self.assertEqual(spec[0], 'foreground') + self.assertEqual(spec[-1], '') # unset by default + + # Setting via keyword arguments and via a dict are equivalent. + text.tag_configure(tag, foreground='red') + self.assertEqual(str(text.tag_configure(tag, 'foreground')[-1]), 'red') + text.tag_configure(tag, {'foreground': 'blue'}) + self.assertEqual(str(text.tag_cget(tag, 'foreground')), 'blue') + + # tag_config is an alias of tag_configure. + self.assertEqual(text.tag_config, text.tag_configure) + + def test_image_configure(self): + text = self.text + image = tkinter.PhotoImage(master=self.root, width=10, height=10) + text.insert('1.0', 'AB') + name = text.image_create('1.1', image=image, name='img') + self.assertEqual(name, 'img') + + self.assertEqual(str(text.image_cget(name, 'name')), 'img') + self.assertEqual(str(text.image_cget(name, 'image')), str(image)) + for value in ('top', 'center', 'bottom', 'baseline'): + text.image_configure(name, align=value) + self.assertEqual(str(text.image_cget(name, 'align')), value) + text.image_configure(name, padx=3, pady=4) + self.assertEqual(text.tk.getint(text.image_cget(name, 'padx')), 3) + self.assertEqual(text.tk.getint(text.image_cget(name, 'pady')), 4) + + # Querying returns the full option set. + cnf = text.image_configure(name) + self.assertIsInstance(cnf, dict) + self.assertIn('align', cnf) + self.assertRaises(TclError, text.image_cget, name, 'spam') + + def test_window_configure(self): + text = self.text + button = tkinter.Button(text, text='ok') + text.insert('1.0', 'AB') + text.window_create('1.1', window=button) + + self.assertEqual(text.window_cget('1.1', 'window'), str(button)) + for value in ('top', 'center', 'bottom', 'baseline'): + text.window_configure('1.1', align=value) + self.assertEqual(str(text.window_cget('1.1', 'align')), value) + text.window_configure('1.1', padx=3, pady=4) + self.assertEqual(text.tk.getint(text.window_cget('1.1', 'padx')), 3) + self.assertEqual(text.tk.getint(text.window_cget('1.1', 'pady')), 4) + text.window_configure('1.1', stretch=True) + self.assertIs(text.tk.getboolean(text.window_cget('1.1', 'stretch')), + True) + + cnf = text.window_configure('1.1') + self.assertIsInstance(cnf, dict) + self.assertIn('stretch', cnf) + self.assertRaises(TclError, text.window_cget, '1.1', 'spam') + self.assertEqual(text.window_config, text.window_configure) + button.destroy() + + def test_peer(self): + text = self.text + text.insert('1.0', 'Lorem ipsum') + self.assertEqual(text.peer_names(), ()) + + text.peer_create('.peer1') + names = self.root.splitlist(text.tk.call('winfo', 'children', '.')) + self.assertIn('.peer1', [str(n) for n in names]) + self.assertEqual([str(p) for p in text.peer_names()], ['.peer1']) + # Peers share content. + self.assertEqual(text.tk.call('.peer1', 'get', '1.0', 'end'), + 'Lorem ipsum\n') + text.tk.call('destroy', '.peer1') + + def test_bbox(self): + text = self.text + text.insert('1.0', 'hello') + text.update() + bbox = text.bbox('1.0') + self.assertIsInstance(bbox, tuple) + self.assertEqual(len(bbox), 4) + for v in bbox: + self.assertIsInstance(v, int) + # A character that is not displayed has no bounding box. + self.assertIsNone(text.bbox('end')) + self.assertRaises(TclError, text.bbox, 'invalid') + self.assertRaises(TypeError, text.bbox) + self.assertRaises(TypeError, text.bbox, '1.0', '2.0') + + def test_dlineinfo(self): + text = self.text + text.insert('1.0', 'hello\nworld') + text.update() + info = text.dlineinfo('1.0') + self.assertIsInstance(info, tuple) + self.assertEqual(len(info), 5) + for v in info: + self.assertIsInstance(v, int) + self.assertRaises(TclError, text.dlineinfo, 'invalid') + self.assertRaises(TypeError, text.dlineinfo) + self.assertRaises(TypeError, text.dlineinfo, '1.0', '2.0') + + def test_see(self): + text = self.text + text.insert('1.0', '\n'.join('line %d' % i for i in range(200))) + text.update() + # Initially the last line is not visible. + self.assertIsNone(text.bbox('200.0')) + text.see('200.0') + text.update() + self.assertIsNotNone(text.bbox('200.0')) + self.assertRaises(TclError, text.see, 'invalid') + self.assertRaises(TypeError, text.see) + self.assertRaises(TypeError, text.see, '1.0', '2.0') + + # yview_pickplace is a deprecated way to make an index visible. + text.yview_pickplace('1.0') + text.update() + self.assertIsNotNone(text.bbox('1.0')) + + def test_search(self): + text = self.text + + # pattern and index are obligatory arguments. + self.assertRaises(TclError, text.search, None, '1.0') + self.assertRaises(TclError, text.search, 'a', None) + self.assertRaises(TclError, text.search, None, None) + + # Invalid text index. + self.assertRaises(TclError, text.search, '', 0) + self.assertRaises(TclError, text.search, '', '') + self.assertRaises(TclError, text.search, '', 'invalid') + self.assertRaises(TclError, text.search, '', '1.0', 0) + self.assertRaises(TclError, text.search, '', '1.0', '') + self.assertRaises(TclError, text.search, '', '1.0', 'invalid') + + text.insert('1.0', + 'This is a test. This is only a test.\n' + 'Another line.\n' + 'Yet another line.\n' + '64-bit') + + self.assertEqual(text.search('test', '1.0'), '1.10') + self.assertEqual(text.search('test', '1.0', 'end'), '1.10') + self.assertEqual(text.search('test', '1.0', '1.10'), '') + self.assertEqual(text.search('test', '1.11'), '1.31') + self.assertEqual(text.search('test', '1.32', 'end'), '') + self.assertEqual(text.search('test', '1.32'), '1.10') + + self.assertEqual(text.search('', '1.0'), '1.0') # empty pattern + self.assertEqual(text.search('nonexistent', '1.0'), '') + self.assertEqual(text.search('-bit', '1.0'), '4.2') # starts with a hyphen + + self.assertEqual(text.search('line', '3.0'), '3.12') + self.assertEqual(text.search('line', '3.0', forwards=True), '3.12') + self.assertEqual(text.search('line', '3.0', backwards=True), '2.8') + self.assertEqual(text.search('line', '3.0', forwards=True, backwards=True), '2.8') + + self.assertEqual(text.search('t.', '1.0'), '1.13') + self.assertEqual(text.search('t.', '1.0', exact=True), '1.13') + self.assertEqual(text.search('t.', '1.0', regexp=True), '1.10') + self.assertEqual(text.search('t.', '1.0', exact=True, regexp=True), '1.10') + + self.assertEqual(text.search('TEST', '1.0'), '') + self.assertEqual(text.search('TEST', '1.0', nocase=True), '1.10') + + self.assertEqual(text.search('.*line', '1.0', regexp=True), '2.0') + self.assertEqual(text.search('.*line', '1.0', regexp=True, nolinestop=True), '1.0') + + self.assertEqual(text.search('test', '1.0', '1.13'), '1.10') + self.assertEqual(text.search('test', '1.0', '1.13', strictlimits=True), '') + self.assertEqual(text.search('test', '1.0', '1.14', strictlimits=True), '1.10') + + var = tkinter.Variable(self.root) + self.assertEqual(text.search('test', '1.0', count=var), '1.10') + self.assertEqual(var.get(), 4 if self.wantobjects else '4') + + # TODO: Add test for elide=True + + def test_search_all(self): + text = self.text + + # pattern and index are obligatory arguments. + self.assertRaises(TclError, text.search_all, None, '1.0') + self.assertRaises(TclError, text.search_all, 'a', None) + self.assertRaises(TclError, text.search_all, None, None) + + # Keyword-only arguments + self.assertRaises(TypeError, text.search_all, 'a', '1.0', 'end', None) + + # Invalid text index. + self.assertRaises(TclError, text.search_all, '', 0) + self.assertRaises(TclError, text.search_all, '', '') + self.assertRaises(TclError, text.search_all, '', 'invalid') + self.assertRaises(TclError, text.search_all, '', '1.0', 0) + self.assertRaises(TclError, text.search_all, '', '1.0', '') + self.assertRaises(TclError, text.search_all, '', '1.0', 'invalid') + + def eq(res, expected): + self.assertIsInstance(res, tuple) + self.assertEqual([str(i) for i in res], expected) + + text.insert('1.0', 'ababa\naba\n64-bit') + + eq(text.search_all('aba', '1.0'), ['1.0', '2.0']) + eq(text.search_all('aba', '1.0', 'end'), ['1.0', '2.0']) + eq(text.search_all('aba', '1.1', 'end'), ['1.2', '2.0']) + eq(text.search_all('aba', '1.1'), ['1.2', '2.0', '1.0']) + + res = text.search_all('', '1.0') # empty pattern + eq(res[:5], ['1.0', '1.1', '1.2', '1.3', '1.4']) + eq(res[-5:], ['3.2', '3.3', '3.4', '3.5', '3.6']) + eq(text.search_all('nonexistent', '1.0'), []) + eq(text.search_all('-bit', '1.0'), ['3.2']) # starts with a hyphen + + eq(text.search_all('aba', '1.0', 'end', forwards=True), ['1.0', '2.0']) + eq(text.search_all('aba', 'end', '1.0', backwards=True), ['2.0', '1.2']) + + eq(text.search_all('aba', '1.0', overlap=True), ['1.0', '1.2', '2.0']) + eq(text.search_all('aba', 'end', '1.0', overlap=True, backwards=True), ['2.0', '1.2', '1.0']) + + eq(text.search_all('aba', '1.0', exact=True), ['1.0', '2.0']) + eq(text.search_all('a.a', '1.0', exact=True), []) + eq(text.search_all('a.a', '1.0', regexp=True), ['1.0', '2.0']) + + eq(text.search_all('ABA', '1.0'), []) + eq(text.search_all('ABA', '1.0', nocase=True), ['1.0', '2.0']) + + eq(text.search_all('a.a', '1.0', regexp=True), ['1.0', '2.0']) + eq(text.search_all('a.a', '1.0', regexp=True, nolinestop=True), ['1.0', '1.4']) + + eq(text.search_all('aba', '1.0', '2.2'), ['1.0', '2.0']) + eq(text.search_all('aba', '1.0', '2.2', strictlimits=True), ['1.0']) + eq(text.search_all('aba', '1.0', '2.3', strictlimits=True), ['1.0', '2.0']) + + var = tkinter.Variable(self.root) + eq(text.search_all('aba', '1.0', count=var), ['1.0', '2.0']) + self.assertEqual(var.get(), (3, 3) if self.wantobjects else '3 3') + + # TODO: Add test for elide=True + + def test_count(self): + text = self.text + text.insert('1.0', + 'Lorem ipsum dolor sit amet,\n' + 'consectetur adipiscing elit,\n' + 'sed do eiusmod tempor incididunt\n' + 'ut labore et dolore magna aliqua.') + + options = ('chars', 'indices', 'lines', + 'displaychars', 'displayindices', 'displaylines', + 'xpixels', 'ypixels') + self.assertEqual(len(text.count('1.0', 'end', *options, return_ints=True)), 8) + self.assertEqual(len(text.count('1.0', 'end', *options)), 8) + self.assertEqual(text.count('1.0', 'end', 'chars', 'lines', return_ints=True), + (124, 4)) + self.assertEqual(text.count('1.3', '4.5', 'chars', 'lines'), (92, 3)) + self.assertEqual(text.count('4.5', '1.3', 'chars', 'lines', return_ints=True), + (-92, -3)) + self.assertEqual(text.count('4.5', '1.3', 'chars', 'lines'), (-92, -3)) + self.assertEqual(text.count('1.3', '1.3', 'chars', 'lines', return_ints=True), + (0, 0)) + self.assertEqual(text.count('1.3', '1.3', 'chars', 'lines'), (0, 0)) + self.assertEqual(text.count('1.0', 'end', 'lines', return_ints=True), 4) + self.assertEqual(text.count('1.0', 'end', 'lines'), (4,)) + self.assertEqual(text.count('end', '1.0', 'lines', return_ints=True), -4) + self.assertEqual(text.count('end', '1.0', 'lines'), (-4,)) + self.assertEqual(text.count('1.3', '1.5', 'lines', return_ints=True), 0) + self.assertEqual(text.count('1.3', '1.5', 'lines'), None) + self.assertEqual(text.count('1.3', '1.3', 'lines', return_ints=True), 0) + self.assertEqual(text.count('1.3', '1.3', 'lines'), None) + # Count 'indices' by default. + self.assertEqual(text.count('1.0', 'end', return_ints=True), 124) + self.assertEqual(text.count('1.0', 'end'), (124,)) + self.assertEqual(text.count('1.0', 'end', 'indices', return_ints=True), 124) + self.assertEqual(text.count('1.0', 'end', 'indices'), (124,)) + self.assertRaises(TclError, text.count, '1.0', 'end', 'spam') + self.assertRaises(TclError, text.count, '1.0', 'end', '-lines') + + self.assertIsInstance(text.count('1.3', '1.5', 'ypixels', return_ints=True), int) + self.assertIsInstance(text.count('1.3', '1.5', 'ypixels'), tuple) + self.assertIsInstance(text.count('1.3', '1.5', 'update', 'ypixels', return_ints=True), int) + self.assertIsInstance(text.count('1.3', '1.5', 'update', 'ypixels'), int) + self.assertEqual(text.count('1.3', '1.3', 'update', 'ypixels', return_ints=True), 0) + self.assertEqual(text.count('1.3', '1.3', 'update', 'ypixels'), None) + self.assertEqual(text.count('1.3', '1.5', 'update', 'indices', return_ints=True), 2) + self.assertEqual(text.count('1.3', '1.5', 'update', 'indices'), 2) + self.assertEqual(text.count('1.3', '1.3', 'update', 'indices', return_ints=True), 0) + self.assertEqual(text.count('1.3', '1.3', 'update', 'indices'), None) + self.assertEqual(text.count('1.3', '1.5', 'update', return_ints=True), 2) + self.assertEqual(text.count('1.3', '1.5', 'update'), (2,)) + self.assertEqual(text.count('1.3', '1.3', 'update', return_ints=True), 0) + self.assertEqual(text.count('1.3', '1.3', 'update'), None) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tkinter/test_tkinter_pipe.py b/Lib/test/test_tkinter/test_tkinter_pipe.py new file mode 100644 index 00000000000..775d0ff5e19 --- /dev/null +++ b/Lib/test/test_tkinter/test_tkinter_pipe.py @@ -0,0 +1,55 @@ +# test_tkinter_pipe.py +import unittest +import subprocess +import sys +from test import support + + +@unittest.skipUnless(support.has_subprocess_support, "test requires subprocess") +class TkinterPipeTest(unittest.TestCase): + + def test_tkinter_pipe_buffered(self): + args = [sys.executable, "-i"] + proc = subprocess.Popen(args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + proc.stdin.write(b"import tkinter\n") + proc.stdin.write(b"interpreter = tkinter.Tcl()\n") + proc.stdin.write(b"print('hello')\n") + proc.stdin.write(b"print('goodbye')\n") + proc.stdin.write(b"quit()\n") + stdout, stderr = proc.communicate(timeout=support.SHORT_TIMEOUT) + stdout = stdout.decode() + self.assertEqual(stdout.split(), ['hello', 'goodbye']) + + def test_tkinter_pipe_unbuffered(self): + args = [sys.executable, "-i", "-u"] + proc = subprocess.Popen(args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + proc.stdin.write(b"import tkinter\n") + proc.stdin.write(b"interpreter = tkinter.Tcl()\n") + + proc.stdin.write(b"print('hello')\n") + proc.stdin.flush() + stdout = proc.stdout.readline() + stdout = stdout.decode() + self.assertEqual(stdout.strip(), 'hello') + + proc.stdin.write(b"print('hello again')\n") + proc.stdin.flush() + stdout = proc.stdout.readline() + stdout = stdout.decode() + self.assertEqual(stdout.strip(), 'hello again') + + proc.stdin.write(b"print('goodbye')\n") + proc.stdin.write(b"quit()\n") + stdout, stderr = proc.communicate(timeout=support.SHORT_TIMEOUT) + stdout = stdout.decode() + self.assertEqual(stdout.strip(), 'goodbye') + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tkinter/test_variables.py b/Lib/test/test_tkinter/test_variables.py new file mode 100644 index 00000000000..5ee4e9467dd --- /dev/null +++ b/Lib/test/test_tkinter/test_variables.py @@ -0,0 +1,363 @@ +import unittest +from test import support + +import gc +import tkinter +from tkinter import (Variable, StringVar, IntVar, DoubleVar, BooleanVar, Tcl, + TclError) +from test.support import ALWAYS_EQ +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import AbstractDefaultRootTest, tcl_version + + +class Var(Variable): + + _default = "default" + side_effect = False + + def set(self, value): + self.side_effect = True + super().set(value) + + +class TestBase(unittest.TestCase): + + def setUp(self): + self.root = Tcl() + + def tearDown(self): + del self.root + + +class TestVariable(TestBase): + + def info_exists(self, *args): + return self.root.getboolean(self.root.call("info", "exists", *args)) + + def test_default(self): + v = Variable(self.root) + self.assertEqual("", v.get()) + self.assertRegex(str(v), r"^PY_VAR(\d+)$") + + def test_name_and_value(self): + v = Variable(self.root, "sample string", "varname") + self.assertEqual("sample string", v.get()) + self.assertEqual("varname", str(v)) + + def test___del__(self): + self.assertFalse(self.info_exists("varname")) + v = Variable(self.root, "sample string", "varname") + self.assertTrue(self.info_exists("varname")) + del v + support.gc_collect() # For PyPy or other GCs. + self.assertFalse(self.info_exists("varname")) + + def test_dont_unset_not_existing(self): + self.assertFalse(self.info_exists("varname")) + v1 = Variable(self.root, name="name") + v2 = Variable(self.root, name="name") + del v1 + support.gc_collect() # For PyPy or other GCs. + self.assertFalse(self.info_exists("name")) + # shouldn't raise exception + del v2 + support.gc_collect() # For PyPy or other GCs. + self.assertFalse(self.info_exists("name")) + + def test_equality(self): + # values doesn't matter, only class and name are checked + v1 = Variable(self.root, name="abc") + v2 = Variable(self.root, name="abc") + self.assertIsNot(v1, v2) + self.assertEqual(v1, v2) + + v3 = Variable(self.root, name="cba") + self.assertNotEqual(v1, v3) + + v4 = StringVar(self.root, name="abc") + self.assertEqual(str(v1), str(v4)) + self.assertNotEqual(v1, v4) + + V = type('Variable', (), {}) + self.assertNotEqual(v1, V()) + + self.assertNotEqual(v1, object()) + self.assertEqual(v1, ALWAYS_EQ) + + root2 = tkinter.Tk() + self.addCleanup(root2.destroy) + v5 = Variable(root2, name="abc") + self.assertEqual(str(v1), str(v5)) + self.assertNotEqual(v1, v5) + + def test_invalid_name(self): + with self.assertRaises(TypeError): + Variable(self.root, name=123) + + def test_null_in_name(self): + with self.assertRaises(ValueError): + Variable(self.root, name='var\x00name') + with self.assertRaises(ValueError): + self.root.globalsetvar('var\x00name', "value") + with self.assertRaises(ValueError): + self.root.globalsetvar(b'var\x00name', "value") + with self.assertRaises(ValueError): + self.root.setvar('var\x00name', "value") + with self.assertRaises(ValueError): + self.root.setvar(b'var\x00name', "value") + + def test_initialize(self): + v = Var(self.root) + self.assertFalse(v.side_effect) + v.set("value") + self.assertTrue(v.side_effect) + self.assertEqual(Variable.initialize, Variable.set) + + def test_trace_old(self): + if tcl_version >= (9, 0): + self.skipTest('requires Tcl version < 9.0') + # Old interface + v = Variable(self.root) + vname = str(v) + self.assertEqual(v.trace, v.trace_variable) + trace = [] + def read_tracer(*args): + trace.append(('read',) + args) + def write_tracer(*args): + trace.append(('write',) + args) + with self.assertWarns(DeprecationWarning) as cm: + cb1 = v.trace_variable('r', read_tracer) + self.assertEqual(cm.filename, __file__) + with self.assertWarns(DeprecationWarning): + cb2 = v.trace_variable('wu', write_tracer) + with self.assertWarns(DeprecationWarning) as cm: + self.assertEqual(sorted(v.trace_vinfo()), [('r', cb1), ('wu', cb2)]) + self.assertEqual(cm.filename, __file__) + self.assertEqual(trace, []) + + v.set('spam') + self.assertEqual(trace, [('write', vname, '', 'w')]) + + trace = [] + v.get() + self.assertEqual(trace, [('read', vname, '', 'r')]) + + trace = [] + with self.assertWarns(DeprecationWarning): + info = sorted(v.trace_vinfo()) + with self.assertWarns(DeprecationWarning): + v.trace_vdelete('w', cb1) # Wrong mode + with self.assertWarns(DeprecationWarning): + self.assertEqual(sorted(v.trace_vinfo()), info) + with self.assertRaises(TclError): + with self.assertWarns(DeprecationWarning): + v.trace_vdelete('r', 'spam') # Wrong command name + with self.assertWarns(DeprecationWarning): + self.assertEqual(sorted(v.trace_vinfo()), info) + with self.assertWarns(DeprecationWarning): + v.trace_vdelete('r', (cb1, 43)) # Wrong arguments + with self.assertWarns(DeprecationWarning): + self.assertEqual(sorted(v.trace_vinfo()), info) + v.get() + self.assertEqual(trace, [('read', vname, '', 'r')]) + + trace = [] + with self.assertWarns(DeprecationWarning) as cm: + v.trace_vdelete('r', cb1) + self.assertEqual(cm.filename, __file__) + with self.assertWarns(DeprecationWarning): + self.assertEqual(v.trace_vinfo(), [('wu', cb2)]) + v.get() + self.assertEqual(trace, []) + + trace = [] + del write_tracer + gc.collect() + v.set('eggs') + self.assertEqual(trace, [('write', vname, '', 'w')]) + + trace = [] + del v + gc.collect() + self.assertEqual(trace, [('write', vname, '', 'u')]) + + def test_trace(self): + v = Variable(self.root) + vname = str(v) + trace = [] + def read_tracer(*args): + trace.append(('read',) + args) + def write_tracer(*args): + trace.append(('write',) + args) + tr1 = v.trace_add('read', read_tracer) + tr2 = v.trace_add(['write', 'unset'], write_tracer) + self.assertEqual(sorted(v.trace_info()), [ + (('read',), tr1), + (('write', 'unset'), tr2)]) + self.assertEqual(trace, []) + + v.set('spam') + self.assertEqual(trace, [('write', vname, '', 'write')]) + + trace = [] + v.get() + self.assertEqual(trace, [('read', vname, '', 'read')]) + + trace = [] + info = sorted(v.trace_info()) + v.trace_remove('write', tr1) # Wrong mode + self.assertEqual(sorted(v.trace_info()), info) + with self.assertRaises(TclError): + v.trace_remove('read', 'spam') # Wrong command name + self.assertEqual(sorted(v.trace_info()), info) + v.get() + self.assertEqual(trace, [('read', vname, '', 'read')]) + + trace = [] + v.trace_remove('read', tr1) + self.assertEqual(v.trace_info(), [(('write', 'unset'), tr2)]) + v.get() + self.assertEqual(trace, []) + + trace = [] + del write_tracer + gc.collect() + v.set('eggs') + self.assertEqual(trace, [('write', vname, '', 'write')]) + + trace = [] + del v + gc.collect() + self.assertEqual(trace, [('write', vname, '', 'unset')]) + + +class TestStringVar(TestBase): + + def test_default(self): + v = StringVar(self.root) + self.assertEqual("", v.get()) + + def test_get(self): + v = StringVar(self.root, "abc", "name") + self.assertEqual("abc", v.get()) + self.root.globalsetvar("name", "value") + self.assertEqual("value", v.get()) + + def test_get_null(self): + v = StringVar(self.root, "abc\x00def", "name") + self.assertEqual("abc\x00def", v.get()) + self.root.globalsetvar("name", "val\x00ue") + self.assertEqual("val\x00ue", v.get()) + + +class TestIntVar(TestBase): + + def test_default(self): + v = IntVar(self.root) + self.assertEqual(0, v.get()) + + def test_get(self): + v = IntVar(self.root, 123, "name") + self.assertEqual(123, v.get()) + self.root.globalsetvar("name", "345") + self.assertEqual(345, v.get()) + self.root.globalsetvar("name", "876.5") + self.assertEqual(876, v.get()) + + def test_invalid_value(self): + v = IntVar(self.root, name="name") + self.root.globalsetvar("name", "value") + with self.assertRaises((ValueError, TclError)): + v.get() + + +class TestDoubleVar(TestBase): + + def test_default(self): + v = DoubleVar(self.root) + self.assertEqual(0.0, v.get()) + + def test_get(self): + v = DoubleVar(self.root, 1.23, "name") + self.assertAlmostEqual(1.23, v.get()) + self.root.globalsetvar("name", "3.45") + self.assertAlmostEqual(3.45, v.get()) + + def test_get_from_int(self): + v = DoubleVar(self.root, 1.23, "name") + self.assertAlmostEqual(1.23, v.get()) + self.root.globalsetvar("name", "3.45") + self.assertAlmostEqual(3.45, v.get()) + self.root.globalsetvar("name", "456") + self.assertAlmostEqual(456, v.get()) + + def test_invalid_value(self): + v = DoubleVar(self.root, name="name") + self.root.globalsetvar("name", "value") + with self.assertRaises((ValueError, TclError)): + v.get() + + +class TestBooleanVar(TestBase): + + def test_default(self): + v = BooleanVar(self.root) + self.assertIs(v.get(), False) + + def test_get(self): + v = BooleanVar(self.root, True, "name") + self.assertIs(v.get(), True) + self.root.globalsetvar("name", "0") + self.assertIs(v.get(), False) + self.root.globalsetvar("name", 42 if self.root.wantobjects() else 1) + self.assertIs(v.get(), True) + self.root.globalsetvar("name", 0) + self.assertIs(v.get(), False) + self.root.globalsetvar("name", "on") + self.assertIs(v.get(), True) + + def test_set(self): + true = 1 if self.root.wantobjects() else "1" + false = 0 if self.root.wantobjects() else "0" + v = BooleanVar(self.root, name="name") + v.set(True) + self.assertEqual(self.root.globalgetvar("name"), true) + v.set("0") + self.assertEqual(self.root.globalgetvar("name"), false) + v.set(42) + self.assertEqual(self.root.globalgetvar("name"), true) + v.set(0) + self.assertEqual(self.root.globalgetvar("name"), false) + v.set("on") + self.assertEqual(self.root.globalgetvar("name"), true) + self.assertEqual(BooleanVar.initialize, BooleanVar.set) + + def test_invalid_value_domain(self): + false = 0 if self.root.wantobjects() else "0" + v = BooleanVar(self.root, name="name") + with self.assertRaises(TclError): + v.set("value") + self.assertEqual(self.root.globalgetvar("name"), false) + self.root.globalsetvar("name", "value") + with self.assertRaises(ValueError): + v.get() + self.root.globalsetvar("name", "1.0") + with self.assertRaises(ValueError): + v.get() + + +class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): + + def test_variable(self): + self.assertRaises(RuntimeError, Variable) + root = tkinter.Tk() + v = Variable() + v.set("value") + self.assertEqual(v.get(), "value") + root.destroy() + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, Variable) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_tkinter/test_widgets.py b/Lib/test/test_tkinter/test_widgets.py new file mode 100644 index 00000000000..14c06ee913d --- /dev/null +++ b/Lib/test/test_tkinter/test_widgets.py @@ -0,0 +1,2644 @@ +import unittest +import tkinter +from tkinter import TclError +import os +from test.support import requires + +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import (requires_tk, tk_version, + get_tk_patchlevel, widget_eq, + wait_until_mapped, + AbstractDefaultRootTest) + +from test.test_tkinter.widget_tests import ( + add_configure_tests, + AbstractWidgetTest, + StandardOptionsTests, + IntegerSizeTests, + PixelSizeTests) + +requires('gui') + + +EXPECTED_SCREEN_DISTANCE_ERRMSG = '(bad|expected) screen distance (but got )?"{}"' +EXPECTED_SCREEN_DISTANCE_OR_EMPTY_ERRMSG = '(bad|expected) screen distance (or "" but got )?"{}"' + +def float_round(x): + return float(round(x)) + +class AbstractToplevelTest(AbstractWidgetTest, PixelSizeTests): + _no_round = {'padx', 'pady'} + if tk_version < (8, 7): + _clipped = {'highlightthickness'} + else: + _clipped = {'borderwidth', 'height', 'highlightthickness', 'padx', + 'pady', 'width'} + + def test_configure_class(self): + widget = self.create() + self.assertEqual(widget['class'], + widget.__class__.__name__.title()) + self.checkInvalidParam(widget, 'class', 'Foo', + errmsg="can't modify -class option after widget is created") + widget2 = self.create(class_='Foo') + self.assertEqual(widget2['class'], 'Foo') + + def test_configure_colormap(self): + widget = self.create() + self.assertEqual(widget['colormap'], '') + self.checkInvalidParam(widget, 'colormap', 'new', + errmsg="can't modify -colormap option after widget is created") + widget2 = self.create(colormap='new') + self.assertEqual(widget2['colormap'], 'new') + + def test_configure_container(self): + widget = self.create() + self.assertEqual(widget['container'], 0 if self.wantobjects else '0') + self.checkInvalidParam(widget, 'container', 1, + errmsg="can't modify -container option after widget is created") + widget2 = self.create(container=True) + self.assertEqual(widget2['container'], 1 if self.wantobjects else '1') + + def test_configure_visual(self): + widget = self.create() + self.assertEqual(widget['visual'], '') + self.checkInvalidParam(widget, 'visual', 'default', + errmsg="can't modify -visual option after widget is created") + widget2 = self.create(visual='default') + self.assertEqual(widget2['visual'], 'default') + + +@add_configure_tests(StandardOptionsTests) +class ToplevelTest(AbstractToplevelTest, unittest.TestCase): + OPTIONS = ( + 'background', 'backgroundimage', 'borderwidth', + 'class', 'colormap', 'container', 'cursor', 'height', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'menu', 'padx', 'pady', 'relief', 'screen', + 'takefocus', 'tile', 'use', 'visual', 'width', + ) + + def create(self, **kwargs): + return tkinter.Toplevel(self.root, **kwargs) + + def test_configure_menu(self): + widget = self.create() + menu = tkinter.Menu(self.root) + self.checkParam(widget, 'menu', menu, eq=widget_eq) + self.checkParam(widget, 'menu', '') + + def test_configure_screen(self): + widget = self.create() + if widget._windowingsystem != 'x11': + self.skipTest('Not using Tk for X11') + self.assertEqual(widget['screen'], '') + try: + display = os.environ['DISPLAY'] + except KeyError: + self.skipTest('No $DISPLAY set.') + self.checkInvalidParam(widget, 'screen', display, + errmsg="can't modify -screen option after widget is created") + widget2 = self.create(screen=display) + self.assertEqual(widget2['screen'], display) + + def test_configure_use(self): + widget = self.create() + self.assertEqual(widget['use'], '') + parent = self.create(container=True) + wid = hex(parent.winfo_id()) + with self.subTest(wid=wid): + widget2 = self.create(use=wid) + self.assertEqual(widget2['use'], wid) + + +@add_configure_tests(StandardOptionsTests) +class FrameTest(AbstractToplevelTest, unittest.TestCase): + OPTIONS = ( + 'background', 'backgroundimage', 'borderwidth', + 'class', 'colormap', 'container', 'cursor', 'height', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'padx', 'pady', 'relief', 'takefocus', 'tile', 'visual', 'width', + ) + + def create(self, **kwargs): + return tkinter.Frame(self.root, **kwargs) + + +@add_configure_tests(StandardOptionsTests) +class LabelFrameTest(AbstractToplevelTest, unittest.TestCase): + OPTIONS = ( + 'background', 'borderwidth', + 'class', 'colormap', 'container', 'cursor', + 'font', 'foreground', 'height', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'labelanchor', 'labelwidget', 'padx', 'pady', 'relief', + 'takefocus', 'text', 'visual', 'width', + ) + + def create(self, **kwargs): + return tkinter.LabelFrame(self.root, **kwargs) + + def test_configure_labelanchor(self): + widget = self.create() + self.checkEnumParam(widget, 'labelanchor', + 'e', 'en', 'es', 'n', 'ne', 'nw', + 's', 'se', 'sw', 'w', 'wn', 'ws') + self.checkInvalidParam(widget, 'labelanchor', 'center') + + def test_configure_labelwidget(self): + widget = self.create() + label = tkinter.Label(self.root, text='Mupp', name='foo') + self.checkParam(widget, 'labelwidget', label, expected='.foo') + label.destroy() + +# Label, Button, Checkbutton, Radiobutton, MenuButton +class AbstractLabelTest(AbstractWidgetTest, IntegerSizeTests): + _rounds_pixels = False + if tk_version < (8, 7): + _clipped = {} + elif tk_version < (9, 0): + _clipped = {'borderwidth', 'height', 'highlightthickness', 'padx', 'pady', 'width'} + else: + _clipped = {'borderwidth', 'height', 'highlightthickness', + 'insertborderwidth', 'padx', 'pady', 'width'} + + def setUp(self): + super().setUp() + if tk_version[:2] == (9, 0) and get_tk_patchlevel(self.root) < (9, 0, 2): + self._clipped = self._clipped - {'height', 'width'} + + +@add_configure_tests(StandardOptionsTests) +class LabelTest(AbstractLabelTest, unittest.TestCase): + OPTIONS = ( + 'activebackground', 'activeforeground', 'anchor', + 'background', 'bitmap', 'borderwidth', 'compound', 'cursor', + 'disabledforeground', 'font', 'foreground', 'height', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'image', 'justify', 'padx', 'pady', 'relief', 'state', + 'takefocus', 'text', 'textvariable', + 'underline', 'width', 'wraplength', + ) + + def create(self, **kwargs): + return tkinter.Label(self.root, **kwargs) + + +@add_configure_tests(StandardOptionsTests) +class ButtonTest(AbstractLabelTest, unittest.TestCase): + OPTIONS = ( + 'activebackground', 'activeforeground', 'anchor', + 'background', 'bitmap', 'borderwidth', + 'command', 'compound', 'cursor', 'default', + 'disabledforeground', 'font', 'foreground', 'height', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'image', 'justify', 'overrelief', 'padx', 'pady', 'relief', + 'repeatdelay', 'repeatinterval', + 'state', 'takefocus', 'text', 'textvariable', + 'underline', 'width', 'wraplength') + if tk_version < (8, 7): + _clipped = {} + else: + _clipped = {'borderwidth', 'height', 'highlightthickness', + 'padx', 'pady', 'width'} + + def create(self, **kwargs): + return tkinter.Button(self.root, **kwargs) + + def test_configure_default(self): + widget = self.create() + self.checkEnumParam(widget, 'default', 'active', 'disabled', 'normal') + + def test_invoke(self): + success = [] + widget = self.create(command=lambda: success.append(1)) + widget.pack() + widget.invoke() + self.assertEqual(success, [1]) + # invoke does nothing for a disabled button. + widget.configure(state='disabled') + widget.invoke() + self.assertEqual(success, [1]) + + def test_flash(self): + widget = self.create() + widget.pack() + widget.update_idletasks() + widget.flash() # No exception. + + +@add_configure_tests(StandardOptionsTests) +class CheckbuttonTest(AbstractLabelTest, unittest.TestCase): + OPTIONS = ( + 'activebackground', 'activeforeground', 'anchor', + 'background', 'bitmap', 'borderwidth', + 'command', 'compound', 'cursor', + 'disabledforeground', 'font', 'foreground', 'height', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'image', 'indicatoron', 'justify', + 'offrelief', 'offvalue', 'onvalue', 'overrelief', + 'padx', 'pady', 'relief', 'selectcolor', 'selectimage', 'state', + 'takefocus', 'text', 'textvariable', + 'tristateimage', 'tristatevalue', + 'underline', 'variable', 'width', 'wraplength', + ) + + def create(self, **kwargs): + return tkinter.Checkbutton(self.root, **kwargs) + + + def test_configure_offvalue(self): + widget = self.create() + self.checkParams(widget, 'offvalue', 1, 2.3, '', 'any string') + + def test_configure_onvalue(self): + widget = self.create() + self.checkParams(widget, 'onvalue', 1, 2.3, '', 'any string') + + def test_unique_variables(self): + frames = [] + buttons = [] + for i in range(2): + f = tkinter.Frame(self.root) + f.pack() + frames.append(f) + for j in 'AB': + b = tkinter.Checkbutton(f, text=j) + b.pack() + buttons.append(b) + variables = [str(b['variable']) for b in buttons] + self.assertEqual(len(set(variables)), 4, variables) + + def test_same_name(self): + f1 = tkinter.Frame(self.root) + f2 = tkinter.Frame(self.root) + b1 = tkinter.Checkbutton(f1, name='test', text='Test1') + b2 = tkinter.Checkbutton(f2, name='test', text='Test2') + + v = tkinter.IntVar(self.root, name='test') + b1.select() + self.assertEqual(v.get(), 1) + b2.deselect() + self.assertEqual(v.get(), 0) + + def test_invoke(self): + success = [] + v = tkinter.IntVar(self.root) + widget = self.create(variable=v, onvalue=1, offvalue=0, + command=lambda: success.append(v.get())) + widget.pack() + widget.invoke() + self.assertEqual(v.get(), 1) + self.assertEqual(success, [1]) + widget.invoke() + self.assertEqual(v.get(), 0) + self.assertEqual(success, [1, 0]) + # A disabled checkbutton is not toggled and its command is not called. + widget.configure(state='disabled') + widget.invoke() + self.assertEqual(v.get(), 0) + self.assertEqual(success, [1, 0]) + + def test_toggle(self): + v = tkinter.IntVar(self.root) + widget = self.create(variable=v, onvalue=1, offvalue=0) + self.assertEqual(v.get(), 0) + widget.toggle() + self.assertEqual(v.get(), 1) + widget.toggle() + self.assertEqual(v.get(), 0) + + def test_flash(self): + widget = self.create() + widget.pack() + widget.update_idletasks() + widget.flash() # No exception. + +@add_configure_tests(StandardOptionsTests) +class RadiobuttonTest(AbstractLabelTest, unittest.TestCase): + OPTIONS = ( + 'activebackground', 'activeforeground', 'anchor', + 'background', 'bitmap', 'borderwidth', + 'command', 'compound', 'cursor', + 'disabledforeground', 'font', 'foreground', 'height', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'image', 'indicatoron', 'justify', 'offrelief', 'overrelief', + 'padx', 'pady', 'relief', 'selectcolor', 'selectimage', 'state', + 'takefocus', 'text', 'textvariable', + 'tristateimage', 'tristatevalue', + 'underline', 'value', 'variable', 'width', 'wraplength', + ) + + def create(self, **kwargs): + return tkinter.Radiobutton(self.root, **kwargs) + + def test_configure_value(self): + widget = self.create() + self.checkParams(widget, 'value', 1, 2.3, '', 'any string') + + def test_invoke(self): + success = [] + v = tkinter.StringVar(self.root) + widget = self.create(variable=v, value='on', + command=lambda: success.append(v.get())) + widget.pack() + widget.invoke() + self.assertEqual(v.get(), 'on') + self.assertEqual(success, ['on']) + # invoke does nothing for a disabled radiobutton. + v.set('') + widget.configure(state='disabled') + widget.invoke() + self.assertEqual(v.get(), '') + self.assertEqual(success, ['on']) + + def test_flash(self): + widget = self.create() + widget.pack() + widget.update_idletasks() + widget.flash() # No exception. + + +@add_configure_tests(StandardOptionsTests) +class MenubuttonTest(AbstractLabelTest, unittest.TestCase): + OPTIONS = ( + 'activebackground', 'activeforeground', 'anchor', + 'background', 'bitmap', 'borderwidth', + 'compound', 'cursor', 'direction', + 'disabledforeground', 'font', 'foreground', 'height', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'image', 'indicatoron', 'justify', 'menu', + 'padx', 'pady', 'relief', 'state', + 'takefocus', 'text', 'textvariable', + 'underline', 'width', 'wraplength', + ) + _rounds_pixels = (tk_version < (9, 0)) + if tk_version < (8, 7): + _clipped = {'highlightthickness', 'padx', 'pady'} + elif tk_version < (9, 0): + _clipped = {'borderwidth', 'highlightthickness', 'padx', 'pady'} + else: + _clipped = {'borderwidth', 'highlightthickness', 'insertborderwidth', 'padx', 'pady'} + + def setUp(self): + super().setUp() + if tk_version[:2] == (9, 0) and get_tk_patchlevel(self.root) < (9, 0, 1): + self._clipped = self._clipped - {'borderwidth'} + + def create(self, **kwargs): + return tkinter.Menubutton(self.root, **kwargs) + + def test_configure_direction(self): + widget = self.create() + self.checkEnumParam(widget, 'direction', + 'above', 'below', 'flush', 'left', 'right') + + def test_configure_height(self): + widget = self.create() + if tk_version < (8, 7) or (tk_version[:2] == (9, 0) and get_tk_patchlevel(self.root) < (9, 0, 1)): + conv = str + else: + conv = False + self.checkIntegerParam(widget, 'height', 100, -100, 0, conv=conv) + + def test_configure_image(self): + widget = self.create() + image = tkinter.PhotoImage(master=self.root, name='image1') + self.checkParam(widget, 'image', image, conv=str) + if tk_version < (8, 7): + errmsg = 'image "spam" doesn\'t exist' + else: + errmsg = 'image "spam" does not exist' + with self.assertRaises(tkinter.TclError) as cm: + widget['image'] = 'spam' + if errmsg is not None: + self.assertEqual(str(cm.exception), errmsg) + with self.assertRaises(tkinter.TclError) as cm: + widget.configure({'image': 'spam'}) + if errmsg is not None: + self.assertEqual(str(cm.exception), errmsg) + + def test_configure_menu(self): + widget = self.create() + menu = tkinter.Menu(widget, name='menu') + self.checkParam(widget, 'menu', menu, eq=widget_eq) + menu.destroy() + + def test_configure_width(self): + widget = self.create() + if tk_version < (8, 7) or (tk_version[:2] == (9, 0) and get_tk_patchlevel(self.root) < (9, 0, 1)): + conv = str + else: + conv = False + self.checkIntegerParam(widget, 'width', 402, -402, 0, conv=conv) + + +class OptionMenuTest(MenubuttonTest, unittest.TestCase): + + def create(self, default='b', values=('a', 'b', 'c'), **kwargs): + return tkinter.OptionMenu(self.root, None, default, *values, **kwargs) + + def test_bad_kwarg(self): + with self.assertRaisesRegex(TclError, r"^unknown option -image$"): + tkinter.OptionMenu(self.root, None, 'b', image='') + + def test_specify_name(self): + widget = tkinter.OptionMenu(self.root, None, ':)', name="option_menu") + self.assertEqual(str(widget), ".option_menu") + self.assertIs(self.root.children["option_menu"], widget) + +@add_configure_tests(IntegerSizeTests, StandardOptionsTests) +class EntryTest(AbstractWidgetTest, unittest.TestCase): + if tk_version < (8, 7): + _clipped = {'highlightthickness'} + else: + _clipped = {'borderwidth', 'highlightthickness', 'insertborderwidth', + 'insertwidth', 'selectborderwidth'} + + OPTIONS = ( + 'background', 'borderwidth', 'cursor', + 'disabledbackground', 'disabledforeground', + 'exportselection', 'font', 'foreground', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'insertbackground', 'insertborderwidth', + 'insertofftime', 'insertontime', 'insertwidth', + 'invalidcommand', 'justify', 'placeholder', 'placeholderforeground', + 'readonlybackground', 'relief', + 'selectbackground', 'selectborderwidth', 'selectforeground', + 'show', 'state', 'takefocus', 'textvariable', + 'validate', 'validatecommand', 'width', 'xscrollcommand', + ) + + def create(self, **kwargs): + return tkinter.Entry(self.root, **kwargs) + + def test_configure_disabledbackground(self): + widget = self.create() + self.checkColorParam(widget, 'disabledbackground') + + def test_configure_insertborderwidth(self): + widget = self.create(insertwidth=100) + self.checkPixelsParam(widget, 'insertborderwidth', + 0, 1.3, 2.6, 6, -2, '10p') + # insertborderwidth is bounded above by a half of insertwidth. + expected = 100 // 2 if tk_version < (8, 7) else 60 + self.checkParam(widget, 'insertborderwidth', 60, expected=expected) + + def test_configure_insertwidth(self): + widget = self.create() + self.checkPixelsParam(widget, 'insertwidth', 1.3, 3.6, 0.9, '10p') + if tk_version < (8, 7): + self.checkParam(widget, 'insertwidth', 0, expected=2) + self.checkParam(widget, 'insertwidth', 0.1, expected=2) + self.checkParam(widget, 'insertwidth', -2, expected=2) + else: + self.checkPixelsParam(widget, 'insertwidth', 0, 0.1, -2) + + def test_configure_invalidcommand(self): + widget = self.create() + self.checkCommandParam(widget, 'invalidcommand') + self.checkCommandParam(widget, 'invcmd') + + def test_configure_readonlybackground(self): + widget = self.create() + self.checkColorParam(widget, 'readonlybackground') + + def test_configure_show(self): + widget = self.create() + self.checkParam(widget, 'show', '*') + self.checkParam(widget, 'show', '') + self.checkParam(widget, 'show', ' ') + + def test_configure_state(self): + widget = self.create() + self.checkEnumParam(widget, 'state', + 'disabled', 'normal', 'readonly') + + def test_configure_validate(self): + widget = self.create() + self.checkEnumParam(widget, 'validate', + 'all', 'key', 'focus', 'focusin', 'focusout', 'none') + + def test_configure_validatecommand(self): + widget = self.create() + self.checkCommandParam(widget, 'validatecommand') + self.checkCommandParam(widget, 'vcmd') + + def test_selection_methods(self): + widget = self.create() + widget.insert(0, '12345') + self.assertFalse(widget.selection_present()) + widget.selection_range(0, 'end') + self.assertEqual(widget.selection_get(), '12345') + self.assertTrue(widget.selection_present()) + widget.selection_from(1) + widget.selection_to(2) + self.assertEqual(widget.selection_get(), '2') + widget.selection_range(3, 4) + self.assertEqual(widget.selection_get(), '4') + widget.selection_clear() + self.assertFalse(widget.selection_present()) + widget.selection_range(0, 'end') + widget.selection_adjust(4) + self.assertEqual(widget.selection_get(), '1234') + widget.selection_adjust(1) + self.assertEqual(widget.selection_get(), '234') + widget.selection_adjust(5) + self.assertEqual(widget.selection_get(), '2345') + widget.selection_adjust(0) + self.assertEqual(widget.selection_get(), '12345') + widget.selection_adjust(0) + + def test_delete(self): + widget = self.create() + widget.insert(0, 'abcdef') + widget.delete(1, 3) + self.assertEqual(widget.get(), 'adef') + widget.delete(1) + self.assertEqual(widget.get(), 'aef') + widget.delete(0, 'end') + self.assertEqual(widget.get(), '') + self.assertRaisesRegex(TclError, r'bad (entry|spinbox) index "xyz"', + widget.delete, 'xyz') + self.assertRaises(TypeError, widget.delete) + + def test_icursor(self): + widget = self.create() + widget.insert(0, 'abcdef') + widget.icursor(3) + widget.insert('insert', 'XYZ') + self.assertEqual(widget.get(), 'abcXYZdef') + self.assertRaisesRegex(TclError, r'bad (entry|spinbox) index "xyz"', + widget.icursor, 'xyz') + self.assertRaises(TypeError, widget.icursor) + + def test_select_aliases(self): + # The select_* methods are aliases of the selection_* methods. + widget = self.create() + widget.insert(0, '12345') + self.assertFalse(widget.select_present()) + widget.select_range(0, 'end') + self.assertTrue(widget.select_present()) + self.assertEqual(widget.selection_get(), '12345') + widget.select_from(1) + widget.select_to(3) + self.assertEqual(widget.selection_get(), '23') + widget.select_adjust(4) + self.assertEqual(widget.selection_get(), '234') + widget.select_clear() + self.assertFalse(widget.select_present()) + self.assertRaisesRegex(TclError, 'bad entry index "xyz"', + widget.select_range, 'xyz', 'end') + + +@add_configure_tests(StandardOptionsTests) +class SpinboxTest(EntryTest, unittest.TestCase): + OPTIONS = ( + 'activebackground', 'background', 'borderwidth', + 'buttonbackground', 'buttoncursor', 'buttondownrelief', 'buttonuprelief', + 'command', 'cursor', 'disabledbackground', 'disabledforeground', + 'exportselection', 'font', 'foreground', 'format', 'from', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'increment', + 'insertbackground', 'insertborderwidth', + 'insertofftime', 'insertontime', 'insertwidth', + 'invalidcommand', 'justify', 'placeholder', 'placeholderforeground', + 'relief', 'readonlybackground', 'repeatdelay', 'repeatinterval', + 'selectbackground', 'selectborderwidth', 'selectforeground', + 'state', 'takefocus', 'textvariable', 'to', + 'validate', 'validatecommand', 'values', + 'width', 'wrap', 'xscrollcommand', + ) + + def create(self, **kwargs): + return tkinter.Spinbox(self.root, **kwargs) + + test_configure_show = None + + def test_configure_buttonbackground(self): + widget = self.create() + self.checkColorParam(widget, 'buttonbackground') + + def test_configure_buttoncursor(self): + widget = self.create() + self.checkCursorParam(widget, 'buttoncursor') + + def test_configure_buttondownrelief(self): + widget = self.create() + self.checkReliefParam(widget, 'buttondownrelief') + + def test_configure_buttonuprelief(self): + widget = self.create() + self.checkReliefParam(widget, 'buttonuprelief') + + def test_configure_format(self): + widget = self.create() + self.checkParam(widget, 'format', '%2f') + self.checkParam(widget, 'format', '%2.2f') + self.checkParam(widget, 'format', '%.2f') + self.checkParam(widget, 'format', '%2.f') + self.checkInvalidParam(widget, 'format', '%2e-1f') + self.checkInvalidParam(widget, 'format', '2.2') + self.checkInvalidParam(widget, 'format', '%2.-2f') + self.checkParam(widget, 'format', '%-2.02f') + self.checkParam(widget, 'format', '% 2.02f') + self.checkParam(widget, 'format', '% -2.200f') + self.checkParam(widget, 'format', '%09.200f') + self.checkInvalidParam(widget, 'format', '%d') + + def test_configure_from(self): + widget = self.create() + self.checkParam(widget, 'to', 100.0) + self.checkFloatParam(widget, 'from', -10, 10.2, 11.7) + if tk_version >= (8, 7): + self.checkFloatParam(widget, 'from', 200, expected=100) + else: + self.checkInvalidParam( + widget, 'from', 200, + errmsg='-to value must be greater than -from value') + + def test_configure_increment(self): + widget = self.create() + self.checkFloatParam(widget, 'increment', -1, 1, 10.2, 12.8, 0) + + def test_configure_to(self): + widget = self.create() + self.checkParam(widget, 'from', -100.0) + self.checkFloatParam(widget, 'to', -10, 10.2, 11.7) + if tk_version >= (8, 7): + self.checkFloatParam(widget, 'to', -200, expected=-100) + else: + self.checkInvalidParam( + widget, 'to', -200, + errmsg='-to value must be greater than -from value') + + def test_configure_values(self): + # XXX + widget = self.create() + self.assertEqual(widget['values'], '') + if tk_version < (8, 7) or (tk_version[:2] == (9, 0) and get_tk_patchlevel(self.root) < (9, 0, 1)): + expected = 'mon tue wed thur' + else: + expected = ('mon', 'tue', 'wed', 'thur') + self.checkParam(widget, 'values', 'mon tue wed thur', + expected=expected) + self.checkParam(widget, 'values', ('mon', 'tue', 'wed', 'thur'), + expected=expected) + + if tk_version < (8, 7) or (tk_version[:2] == (9, 0) and get_tk_patchlevel(self.root) < (9, 0, 1)): + expected = '42 3.14 {} {any string}' + else: + expected = (42, 3.14, '', 'any string') + self.checkParam(widget, 'values', (42, 3.14, '', 'any string'), + expected=expected) + + self.checkParam(widget, 'values', '') + + def test_configure_wrap(self): + widget = self.create() + self.checkBooleanParam(widget, 'wrap') + + def test_bbox(self): + widget = self.create() + self.assertIsBoundingBox(widget.bbox(0)) + self.assertRaises(tkinter.TclError, widget.bbox, 'noindex') + self.assertRaises(tkinter.TclError, widget.bbox, None) + self.assertRaises(TypeError, widget.bbox) + self.assertRaises(TypeError, widget.bbox, 0, 1) + + def test_selection_methods(self): + widget = self.create() + widget.insert(0, '12345') + self.assertFalse(widget.selection_present()) + widget.selection_range(0, 'end') + self.assertEqual(widget.selection_get(), '12345') + self.assertTrue(widget.selection_present()) + widget.selection_from(1) + widget.selection_to(2) + self.assertEqual(widget.selection_get(), '2') + widget.selection_range(3, 4) + self.assertEqual(widget.selection_get(), '4') + widget.selection_clear() + self.assertFalse(widget.selection_present()) + widget.selection_range(0, 'end') + widget.selection_adjust(4) + self.assertEqual(widget.selection_get(), '1234') + widget.selection_adjust(1) + self.assertEqual(widget.selection_get(), '234') + widget.selection_adjust(5) + self.assertEqual(widget.selection_get(), '2345') + widget.selection_adjust(0) + self.assertEqual(widget.selection_get(), '12345') + + def test_selection_element(self): + widget = self.create() + self.assertEqual(widget.selection_element(), "none") + widget.selection_element("buttonup") + self.assertEqual(widget.selection_element(), "buttonup") + widget.selection_element("buttondown") + self.assertEqual(widget.selection_element(), "buttondown") + + # Spinbox has no select_* aliases, unlike Entry. + test_select_aliases = None + + def test_invoke(self): + widget = self.create(from_=0, to=10) + widget.delete(0, 'end') + widget.insert(0, '5') + widget.invoke('buttonup') + self.assertEqual(widget.get(), '6') + widget.invoke('buttondown') + self.assertEqual(widget.get(), '5') + self.assertRaisesRegex(TclError, 'bad element "spam"', + widget.invoke, 'spam') + + def test_identify(self): + widget = self.create() + widget.pack() + # Identifying the element under a point requires the widget to be + # mapped with a real size. + if wait_until_mapped(widget): + self.assertIn(widget.identify(5, 5), + ('entry', 'buttonup', 'buttondown', 'none')) + self.assertRaises(TclError, widget.identify, 'a', 'b') + + def test_scan(self): + widget = self.create() + widget.insert(0, 'a' * 100) + widget.pack() + widget.update_idletasks() + self.assertEqual(widget.scan_mark(10), ()) + self.assertEqual(widget.scan_dragto(0), ()) + self.assertRaises(TypeError, widget.scan_mark) + + +@add_configure_tests(StandardOptionsTests) +class TextTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'autoseparators', 'background', 'blockcursor', 'borderwidth', + 'cursor', 'endline', 'exportselection', + 'font', 'foreground', 'height', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'inactiveselectbackground', 'insertbackground', 'insertborderwidth', + 'insertofftime', 'insertontime', 'insertunfocussed', 'insertwidth', + 'maxundo', 'padx', 'pady', 'relief', + 'selectbackground', 'selectborderwidth', 'selectforeground', + 'setgrid', 'spacing1', 'spacing2', 'spacing3', 'startline', 'state', + 'tabs', 'tabstyle', 'takefocus', 'undo', 'width', 'wrap', + 'xscrollcommand', 'yscrollcommand', + ) + _no_round = {'selectborderwidth'} + if tk_version < (9, 0): + _clipped = {'highlightthickness', 'spacing1', 'spacing2', 'spacing3'} + else: + _clipped = {'borderwidth', 'height', 'highlightthickness', + 'insertborderwidth', 'insertwidth', 'padx', 'pady', + 'selectborderwidth', 'spacing1', 'spacing2', 'spacing3'} + + def setUp(self): + super().setUp() + if tk_version[:2] == (9, 0) and get_tk_patchlevel(self.root) < (9, 0, 2): + self._clipped = self._clipped - {'borderwidth', 'height', 'padx', 'pady'} + if tk_version[:2] == (9, 0) and get_tk_patchlevel(self.root) < (9, 0, 1): + self._clipped = self._clipped - {'insertborderwidth', 'insertwidth', 'selectborderwidth'} + + def create(self, **kwargs): + return tkinter.Text(self.root, **kwargs) + + def test_configure_autoseparators(self): + widget = self.create() + self.checkBooleanParam(widget, 'autoseparators') + + def test_configure_blockcursor(self): + widget = self.create() + self.checkBooleanParam(widget, 'blockcursor') + + def test_configure_endline(self): + widget = self.create() + text = '\n'.join('Line %d' for i in range(100)) + widget.insert('end', text) + self.checkParam(widget, 'endline', 200, expected='') + self.checkParam(widget, 'endline', -10, expected='') + self.checkInvalidParam(widget, 'endline', 'spam', + errmsg='expected integer but got "spam"') + self.checkParam(widget, 'endline', 50) + self.checkParam(widget, 'startline', 15) + self.checkInvalidParam(widget, 'endline', 10, + errmsg='-startline must be less than or equal to -endline') + + def test_configure_height(self): + widget = self.create() + self.checkPixelsParam(widget, 'height', 100, 101.2, 102.6, '3c') + if tk_version < (9, 0): + self.checkParam(widget, 'height', 0, expected=1) + self.checkParam(widget, 'height', -100, expected=1) + else: + self.checkPixelsParam(widget, 'height', 0, -100) + + def test_configure_maxundo(self): + widget = self.create() + self.checkIntegerParam(widget, 'maxundo', 0, 5, -1) + + def test_configure_inactiveselectbackground(self): + widget = self.create() + self.checkColorParam(widget, 'inactiveselectbackground') + + @requires_tk(8, 6) + def test_configure_insertunfocussed(self): + widget = self.create() + self.checkEnumParam(widget, 'insertunfocussed', + 'hollow', 'none', 'solid') + + def test_configure_spacing1(self): + widget = self.create() + self.checkPixelsParam(widget, 'spacing1', 20, 21.4, 22.6, -5, '0.5c') + + def test_configure_spacing2(self): + widget = self.create() + self.checkPixelsParam(widget, 'spacing2', 5, 6.4, 7.6, -1, '0.1c') + + def test_configure_spacing3(self): + widget = self.create() + self.checkPixelsParam(widget, 'spacing3', 20, 21.4, 22.6, -10, '0.5c') + + def test_configure_startline(self): + widget = self.create() + text = '\n'.join('Line %d' for i in range(100)) + widget.insert('end', text) + self.checkParam(widget, 'startline', 200, expected='') + self.checkParam(widget, 'startline', -10, expected='') + self.checkInvalidParam(widget, 'startline', 'spam', + errmsg='expected integer but got "spam"') + self.checkParam(widget, 'startline', 10) + self.checkParam(widget, 'endline', 50) + self.checkInvalidParam(widget, 'startline', 70, + errmsg='-startline must be less than or equal to -endline') + + def test_configure_state(self): + widget = self.create() + self.checkEnumParam(widget, 'state', 'disabled', 'normal') + + def test_configure_tabs(self): + widget = self.create() + self.checkParam(widget, 'tabs', (10.2, 20.7, '1i', '2i')) + self.checkParam(widget, 'tabs', '10.2 20.7 1i 2i', + expected=(10.2, 20.7, '1i', '2i') + if get_tk_patchlevel(self.root) >= (8, 6, 14) + else ('10.2', '20.7', '1i', '2i')) + self.checkParam(widget, 'tabs', '2c left 4c 6c center', + expected=('2c', 'left', '4c', '6c', 'center')) + self.checkInvalidParam(widget, 'tabs', 'spam', + errmsg=EXPECTED_SCREEN_DISTANCE_ERRMSG.format('spam')) + + def test_configure_tabstyle(self): + widget = self.create() + self.checkEnumParam(widget, 'tabstyle', 'tabular', 'wordprocessor') + + def test_configure_undo(self): + widget = self.create() + self.checkBooleanParam(widget, 'undo') + + def test_configure_width(self): + widget = self.create() + self.checkIntegerParam(widget, 'width', 402) + self.checkParam(widget, 'width', -402, expected=1) + self.checkParam(widget, 'width', 0, expected=1) + + def test_configure_wrap(self): + widget = self.create() + self.checkEnumParam(widget, 'wrap', 'char', 'none', 'word') + + def test_bbox(self): + widget = self.create() + self.assertIsBoundingBox(widget.bbox('1.1')) + self.assertIsNone(widget.bbox('end')) + self.assertRaises(tkinter.TclError, widget.bbox, 'noindex') + self.assertRaises(tkinter.TclError, widget.bbox, None) + self.assertRaises(TypeError, widget.bbox) + self.assertRaises(TypeError, widget.bbox, '1.1', 'end') + + +@add_configure_tests(PixelSizeTests, StandardOptionsTests) +class CanvasTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'background', 'borderwidth', + 'closeenough', 'confine', 'cursor', 'height', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'insertbackground', 'insertborderwidth', + 'insertofftime', 'insertontime', 'insertwidth', + 'offset', 'relief', 'scrollregion', + 'selectbackground', 'selectborderwidth', 'selectforeground', + 'state', 'takefocus', + 'xscrollcommand', 'xscrollincrement', + 'yscrollcommand', 'yscrollincrement', 'width', + ) + if tk_version < (8, 7): + _clipped = {'highlightthickness'} + else: + _clipped = {'borderwidth', 'height', 'highlightthickness', + 'insertborderwidth', 'insertwidth', 'selectborderwidth', + 'width', 'xscrollincrement', 'yscrollincrement'} + _stringify = True + + def setUp(self): + super().setUp() + if tk_version[:2] == (9, 0) and get_tk_patchlevel(self.root) < (9, 0, 1): + self._rounds_pixels = True + self._no_round = {'borderwidth', 'height', 'highlightthickness', + 'width', 'xscrollincrement', 'yscrollincrement'} + self._clipped = self._clipped - {'insertborderwidth', 'insertwidth', 'selectborderwidth'} + + def create(self, **kwargs): + return tkinter.Canvas(self.root, **kwargs) + + def test_configure_closeenough(self): + widget = self.create() + self.checkFloatParam(widget, 'closeenough', 24, 2.4, 3.6, -3, + conv=float) + + def test_configure_confine(self): + widget = self.create() + self.checkBooleanParam(widget, 'confine') + + def test_configure_offset(self): + widget = self.create() + self.assertEqual(widget['offset'], '0,0') + self.checkParams(widget, 'offset', + 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center') + self.checkParam(widget, 'offset', '10,20') + self.checkParam(widget, 'offset', '#5,6') + self.checkInvalidParam(widget, 'offset', 'spam') + + def test_configure_scrollregion(self): + widget = self.create() + self.checkParam(widget, 'scrollregion', '0 0 200 150') + self.checkParam(widget, 'scrollregion', (0, 0, 200, 150), + expected='0 0 200 150') + self.checkParam(widget, 'scrollregion', '') + self.checkInvalidParam(widget, 'scrollregion', 'spam', + errmsg='bad scrollRegion "spam"') + self.checkInvalidParam(widget, 'scrollregion', (0, 0, 200, 'spam')) + self.checkInvalidParam(widget, 'scrollregion', (0, 0, 200)) + self.checkInvalidParam(widget, 'scrollregion', (0, 0, 200, 150, 0)) + + def test_configure_state(self): + widget = self.create() + self.checkEnumParam(widget, 'state', 'disabled', 'normal', + errmsg='bad state value "{}": must be normal or disabled') + + def test_configure_xscrollincrement(self): + widget = self.create() + self.checkPixelsParam(widget, 'xscrollincrement', + 40, 0, 41.2, 43.6, -40, '0.5i') + + def test_configure_yscrollincrement(self): + widget = self.create() + self.checkPixelsParam(widget, 'yscrollincrement', + 10, 0, 11.2, 13.6, -10, '0.1i') + + def _test_option_joinstyle(self, c, factory): + for joinstyle in 'bevel', 'miter', 'round': + i = factory(joinstyle=joinstyle) + self.assertEqual(c.itemcget(i, 'joinstyle'), joinstyle) + self.assertRaises(TclError, factory, joinstyle='spam') + + def _test_option_smooth(self, c, factory): + for smooth in 1, True, '1', 'true', 'yes', 'on': + i = factory(smooth=smooth) + self.assertEqual(c.itemcget(i, 'smooth'), 'true') + for smooth in 0, False, '0', 'false', 'no', 'off': + i = factory(smooth=smooth) + self.assertEqual(c.itemcget(i, 'smooth'), '0') + i = factory(smooth=True, splinestep=30) + self.assertEqual(c.itemcget(i, 'smooth'), 'true') + self.assertEqual(c.itemcget(i, 'splinestep'), '30') + i = factory(smooth='raw', splinestep=30) + self.assertEqual(c.itemcget(i, 'smooth'), 'raw') + self.assertEqual(c.itemcget(i, 'splinestep'), '30') + self.assertRaises(TclError, factory, smooth='spam') + + def test_create_rectangle(self): + c = self.create() + i1 = c.create_rectangle(20, 30, 60, 10) + self.assertEqual(c.coords(i1), [20.0, 10.0, 60.0, 30.0]) + self.assertEqual(c.bbox(i1), (19, 9, 61, 31)) + + i2 = c.create_rectangle([21, 31, 61, 11]) + self.assertEqual(c.coords(i2), [21.0, 11.0, 61.0, 31.0]) + self.assertEqual(c.bbox(i2), (20, 10, 62, 32)) + + i3 = c.create_rectangle((22, 32), (62, 12)) + self.assertEqual(c.coords(i3), [22.0, 12.0, 62.0, 32.0]) + self.assertEqual(c.bbox(i3), (21, 11, 63, 33)) + + i4 = c.create_rectangle([(23, 33), (63, 13)]) + self.assertEqual(c.coords(i4), [23.0, 13.0, 63.0, 33.0]) + self.assertEqual(c.bbox(i4), (22, 12, 64, 34)) + + self.assertRaises(TclError, c.create_rectangle, 20, 30, 60) + self.assertRaises(TclError, c.create_rectangle, [20, 30, 60]) + self.assertRaises(TclError, c.create_rectangle, 20, 30, 40, 50, 60, 10) + self.assertRaises(TclError, c.create_rectangle, [20, 30, 40, 50, 60, 10]) + self.assertRaises(TclError, c.create_rectangle, 20, 30) + self.assertRaises(TclError, c.create_rectangle, [20, 30]) + self.assertRaises(IndexError, c.create_rectangle) + self.assertRaises(IndexError, c.create_rectangle, []) + + def test_create_line(self): + c = self.create() + i1 = c.create_line(20, 30, 40, 50, 60, 10) + self.assertEqual(c.coords(i1), [20.0, 30.0, 40.0, 50.0, 60.0, 10.0]) + self.assertEqual(c.bbox(i1), (18, 8, 62, 52)) + self.assertEqual(c.itemcget(i1, 'arrow'), 'none') + self.assertEqual(c.itemcget(i1, 'arrowshape'), '8 10 3') + self.assertEqual(c.itemcget(i1, 'capstyle'), 'butt') + self.assertEqual(c.itemcget(i1, 'joinstyle'), 'round') + self.assertEqual(c.itemcget(i1, 'smooth'), '0') + self.assertEqual(c.itemcget(i1, 'splinestep'), '12') + + i2 = c.create_line([21, 31, 41, 51, 61, 11]) + self.assertEqual(c.coords(i2), [21.0, 31.0, 41.0, 51.0, 61.0, 11.0]) + self.assertEqual(c.bbox(i2), (19, 9, 63, 53)) + + i3 = c.create_line((22, 32), (42, 52), (62, 12)) + self.assertEqual(c.coords(i3), [22.0, 32.0, 42.0, 52.0, 62.0, 12.0]) + self.assertEqual(c.bbox(i3), (20, 10, 64, 54)) + + i4 = c.create_line([(23, 33), (43, 53), (63, 13)]) + self.assertEqual(c.coords(i4), [23.0, 33.0, 43.0, 53.0, 63.0, 13.0]) + self.assertEqual(c.bbox(i4), (21, 11, 65, 55)) + + self.assertRaises(TclError, c.create_line, 20, 30, 60) + self.assertRaises(TclError, c.create_line, [20, 30, 60]) + self.assertRaises(TclError, c.create_line, 20, 30) + self.assertRaises(TclError, c.create_line, [20, 30]) + self.assertRaises(IndexError, c.create_line) + self.assertRaises(IndexError, c.create_line, []) + + for arrow in 'none', 'first', 'last', 'both': + i = c.create_line(20, 30, 60, 10, arrow=arrow) + self.assertEqual(c.itemcget(i, 'arrow'), arrow) + i = c.create_line(20, 30, 60, 10, arrow='first', arrowshape=[10, 15, 5]) + self.assertEqual(c.itemcget(i, 'arrowshape'), '10 15 5') + self.assertRaises(TclError, c.create_line, 20, 30, 60, 10, arrow='spam') + + for capstyle in 'butt', 'projecting', 'round': + i = c.create_line(20, 30, 60, 10, capstyle=capstyle) + self.assertEqual(c.itemcget(i, 'capstyle'), capstyle) + self.assertRaises(TclError, c.create_line, 20, 30, 60, 10, capstyle='spam') + + self._test_option_joinstyle(c, + lambda **kwargs: c.create_line(20, 30, 40, 50, 60, 10, **kwargs)) + self._test_option_smooth(c, + lambda **kwargs: c.create_line(20, 30, 60, 10, **kwargs)) + + def test_create_polygon(self): + c = self.create() + # In Tk < 8.7 polygons are filled, but has no outline by default. + # This affects its size, so always explicitly specify outline. + i1 = c.create_polygon(20, 30, 40, 50, 60, 10, outline='red') + self.assertEqual(c.coords(i1), [20.0, 30.0, 40.0, 50.0, 60.0, 10.0]) + self.assertEqual(c.bbox(i1), (18, 8, 62, 52)) + self.assertEqual(c.itemcget(i1, 'joinstyle'), 'round') + self.assertEqual(c.itemcget(i1, 'smooth'), '0') + self.assertEqual(c.itemcget(i1, 'splinestep'), '12') + + i2 = c.create_polygon([21, 31, 41, 51, 61, 11], outline='red') + self.assertEqual(c.coords(i2), [21.0, 31.0, 41.0, 51.0, 61.0, 11.0]) + self.assertEqual(c.bbox(i2), (19, 9, 63, 53)) + + i3 = c.create_polygon((22, 32), (42, 52), (62, 12), outline='red') + self.assertEqual(c.coords(i3), [22.0, 32.0, 42.0, 52.0, 62.0, 12.0]) + self.assertEqual(c.bbox(i3), (20, 10, 64, 54)) + + i4 = c.create_polygon([(23, 33), (43, 53), (63, 13)], outline='red') + self.assertEqual(c.coords(i4), [23.0, 33.0, 43.0, 53.0, 63.0, 13.0]) + self.assertEqual(c.bbox(i4), (21, 11, 65, 55)) + + self.assertRaises(TclError, c.create_polygon, 20, 30, 60) + self.assertRaises(TclError, c.create_polygon, [20, 30, 60]) + self.assertRaises(IndexError, c.create_polygon) + self.assertRaises(IndexError, c.create_polygon, []) + + self._test_option_joinstyle(c, + lambda **kwargs: c.create_polygon(20, 30, 40, 50, 60, 10, **kwargs)) + self._test_option_smooth(c, + lambda **kwargs: c.create_polygon(20, 30, 40, 50, 60, 10, **kwargs)) + + def test_create_arc(self): + c = self.create() + i = c.create_arc(10, 20, 30, 40) + self.assertEqual(c.coords(i), [10.0, 20.0, 30.0, 40.0]) + self.assertEqual(c.itemcget(i, 'style'), 'pieslice') + self.assertEqual(float(c.itemcget(i, 'start')), 0.0) + self.assertEqual(float(c.itemcget(i, 'extent')), 90.0) + + for style in 'pieslice', 'chord', 'arc': + i = c.create_arc(10, 20, 30, 40, style=style) + self.assertEqual(c.itemcget(i, 'style'), style) + self.assertRaises(TclError, c.create_arc, 10, 20, 30, 40, style='spam') + + i = c.create_arc(10, 20, 30, 40, start=45, extent=120, + outline='red', fill='blue', width=3) + self.assertEqual(float(c.itemcget(i, 'start')), 45.0) + self.assertEqual(float(c.itemcget(i, 'extent')), 120.0) + self.assertEqual(str(c.itemcget(i, 'outline')), 'red') + self.assertEqual(str(c.itemcget(i, 'fill')), 'blue') + self.assertEqual(float(c.itemcget(i, 'width')), 3.0) + self.assertRaises(TclError, c.create_arc, 10, 20, 30, 40, extent='spam') + + def test_create_oval(self): + c = self.create() + i = c.create_oval(10, 20, 30, 40) + self.assertEqual(c.coords(i), [10.0, 20.0, 30.0, 40.0]) + self.assertEqual(c.bbox(i), (9, 19, 31, 41)) + self.assertEqual(c.itemcget(i, 'stipple'), '') + + i = c.create_oval(10, 20, 30, 40, fill='red', outline='blue', width=2) + self.assertEqual(str(c.itemcget(i, 'fill')), 'red') + self.assertEqual(str(c.itemcget(i, 'outline')), 'blue') + self.assertEqual(float(c.itemcget(i, 'width')), 2.0) + self.assertRaises(TclError, c.create_oval, 10, 20, 30, 40, width='spam') + + def test_create_bitmap(self): + c = self.create() + i = c.create_bitmap(10, 20, bitmap='gray50') + self.assertEqual(c.coords(i), [10.0, 20.0]) + self.assertEqual(c.itemcget(i, 'bitmap'), 'gray50') + self.assertEqual(c.itemcget(i, 'anchor'), 'center') + + for anchor in 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center': + i = c.create_bitmap(10, 20, bitmap='gray50', anchor=anchor) + self.assertEqual(c.itemcget(i, 'anchor'), anchor) + self.assertRaises(TclError, c.create_bitmap, 10, 20, + bitmap='gray50', anchor='spam') + + i = c.create_bitmap(10, 20, bitmap='gray50', + foreground='red', background='blue') + self.assertEqual(str(c.itemcget(i, 'foreground')), 'red') + self.assertEqual(str(c.itemcget(i, 'background')), 'blue') + if c._windowingsystem != 'aqua': + # Aqua resolves bitmaps lazily and does not report a bad name here. + self.assertRaises(TclError, c.create_bitmap, 10, 20, bitmap='spam') + + def test_create_image(self): + c = self.create() + image = tkinter.PhotoImage(master=self.root, width=10, height=10) + i = c.create_image(10, 20, image=image) + self.assertEqual(c.coords(i), [10.0, 20.0]) + self.assertEqual(str(c.itemcget(i, 'image')), str(image)) + self.assertEqual(c.itemcget(i, 'anchor'), 'center') + + for anchor in 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center': + i = c.create_image(10, 20, image=image, anchor=anchor) + self.assertEqual(c.itemcget(i, 'anchor'), anchor) + self.assertRaises(TclError, c.create_image, 10, 20, + image=image, anchor='spam') + + def test_create_text(self): + c = self.create() + i = c.create_text(10, 20, text='Hello') + self.assertEqual(c.coords(i), [10.0, 20.0]) + self.assertEqual(c.itemcget(i, 'text'), 'Hello') + self.assertEqual(c.itemcget(i, 'anchor'), 'center') + self.assertEqual(c.itemcget(i, 'justify'), 'left') + + for justify in 'left', 'right', 'center': + i = c.create_text(10, 20, text='Hello', justify=justify) + self.assertEqual(c.itemcget(i, 'justify'), justify) + self.assertRaises(TclError, c.create_text, 10, 20, justify='spam') + + for anchor in 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center': + i = c.create_text(10, 20, text='Hello', anchor=anchor) + self.assertEqual(c.itemcget(i, 'anchor'), anchor) + self.assertRaises(TclError, c.create_text, 10, 20, anchor='spam') + + i = c.create_text(10, 20, text='Hello', fill='red', angle=45, + font='TkFixedFont') + self.assertEqual(str(c.itemcget(i, 'fill')), 'red') + self.assertEqual(float(c.itemcget(i, 'angle')), 45.0) + self.assertEqual(str(c.itemcget(i, 'font')), 'TkFixedFont') + self.assertRaises(TclError, c.create_text, 10, 20, angle='spam') + + def test_create_window(self): + c = self.create() + button = tkinter.Button(c, text='ok') + i = c.create_window(10, 20, window=button) + self.assertEqual(c.coords(i), [10.0, 20.0]) + self.assertEqual(c.itemcget(i, 'window'), str(button)) + self.assertEqual(c.itemcget(i, 'anchor'), 'center') + + for anchor in 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center': + i = c.create_window(10, 20, window=tkinter.Button(c), anchor=anchor) + self.assertEqual(c.itemcget(i, 'anchor'), anchor) + self.assertRaises(TclError, c.create_window, 10, 20, + window=button, anchor='spam') + + i = c.create_window(10, 20, window=tkinter.Button(c), + width=30, height=40) + self.assertEqual(int(c.itemcget(i, 'width')), 30) + self.assertEqual(int(c.itemcget(i, 'height')), 40) + self.assertRaises(TclError, c.create_window, 10, 20, + window=button, width='spam') + + def test_coords(self): + c = self.create() + i = c.create_line(20, 30, 40, 50, 60, 10, tags='x') + self.assertEqual(c.coords(i), [20.0, 30.0, 40.0, 50.0, 60.0, 10.0]) + self.assertEqual(c.coords('x'), [20.0, 30.0, 40.0, 50.0, 60.0, 10.0]) + self.assertEqual(c.bbox(i), (18, 8, 62, 52)) + + c.coords(i, 50, 60, 70, 80, 90, 40) + self.assertEqual(c.coords(i), [50.0, 60.0, 70.0, 80.0, 90.0, 40.0]) + self.assertEqual(c.bbox(i), (48, 38, 92, 82)) + + c.coords(i, [21, 31, 41, 51, 61, 11]) + self.assertEqual(c.coords(i), [21.0, 31.0, 41.0, 51.0, 61.0, 11.0]) + + c.coords(i, (22, 32), (42, 52), (62, 12)) + self.assertEqual(c.coords(i), [22.0, 32.0, 42.0, 52.0, 62.0, 12.0]) + + c.coords(i, [(23, 33), (43, 53), (63, 13)]) + self.assertEqual(c.coords(i), [23.0, 33.0, 43.0, 53.0, 63.0, 13.0]) + + c.coords(i, 20, 30, 60, 10) + self.assertEqual(c.coords(i), [20.0, 30.0, 60.0, 10.0]) + self.assertEqual(c.bbox(i), (18, 8, 62, 32)) + + self.assertRaises(TclError, c.coords, i, 20, 30, 60) + self.assertRaises(TclError, c.coords, i, [20, 30, 60]) + self.assertRaises(TclError, c.coords, i, 20, 30) + self.assertRaises(TclError, c.coords, i, [20, 30]) + + c.coords(i, '20', '30c', '60i', '10p') + coords = c.coords(i) + self.assertIsInstance(coords, list) + self.assertEqual(len(coords), 4) + self.assertEqual(coords[0], 20) + for i in range(4): + self.assertIsInstance(coords[i], float) + + @requires_tk(8, 6) + def test_moveto(self): + widget = self.create() + i1 = widget.create_rectangle(1, 1, 20, 20, tags='group') + i2 = widget.create_rectangle(30, 30, 50, 70, tags='group') + x1, y1, _, _ = widget.bbox(i1) + x2, y2, _, _ = widget.bbox(i2) + widget.moveto('group', 200, 100) + x1_2, y1_2, _, _ = widget.bbox(i1) + x2_2, y2_2, _, _ = widget.bbox(i2) + self.assertEqual(x1_2, 200) + self.assertEqual(y1_2, 100) + self.assertEqual(x2 - x1, x2_2 - x1_2) + self.assertEqual(y2 - y1, y2_2 - y1_2) + widget.tag_lower(i2, i1) + widget.moveto('group', y=50) + x1_3, y1_3, _, _ = widget.bbox(i1) + x2_3, y2_3, _, _ = widget.bbox(i2) + self.assertEqual(y2_3, 50) + self.assertEqual(x2_3, x2_2) + self.assertEqual(x2_2 - x1_2, x2_3 - x1_3) + self.assertEqual(y2_2 - y1_2, y2_3 - y1_3) + + def test_create_items(self): + c = self.create() + image = tkinter.PhotoImage(master=self.root, width=10, height=10) + button = tkinter.Button(c, text='ok') + items = { + 'arc': c.create_arc(10, 20, 30, 40), + 'bitmap': c.create_bitmap(10, 20, bitmap='gray50'), + 'image': c.create_image(10, 20, image=image), + 'line': c.create_line(10, 20, 30, 40), + 'oval': c.create_oval(10, 20, 30, 40), + 'polygon': c.create_polygon(10, 20, 30, 40, 50, 20), + 'rectangle': c.create_rectangle(10, 20, 30, 40), + 'text': c.create_text(10, 20, text='hello'), + 'window': c.create_window(10, 20, window=button), + } + for itemtype, item in items.items(): + self.assertIsInstance(item, int) + self.assertEqual(c.type(item), itemtype) + # All items are reported by find_all in creation (stacking) order. + self.assertEqual(c.find_all(), tuple(sorted(items.values()))) + + # No coordinates at all is an IndexError; a bad number is a TclError. + self.assertRaises(IndexError, c.create_arc) + self.assertRaises(TclError, c.create_arc, 1, 2, 3) + self.assertRaises(TclError, c.create_oval, 1, 2) + + def test_type(self): + c = self.create() + rect = c.create_rectangle(10, 20, 30, 40) + self.assertEqual(c.type(rect), 'rectangle') + # An unmatched tag or id is not an error. + self.assertIsNone(c.type('nonexistent')) + self.assertIsNone(c.type(9999)) + self.assertRaises(TypeError, c.type) + self.assertRaises(TypeError, c.type, rect, 'extra') + + def test_bbox(self): + c = self.create() + rect = c.create_rectangle(10, 10, 30, 30) + bbox = c.bbox(rect) + self.assertIsInstance(bbox, tuple) + self.assertEqual(len(bbox), 4) + for v in bbox: + self.assertIsInstance(v, int) + # The bounding box encloses the item (with a small margin). + self.assertEqual(bbox, (9, 9, 31, 31)) + # bbox over several items is their union. + rect2 = c.create_rectangle(50, 50, 70, 70) + self.assertEqual(c.bbox(rect, rect2), (9, 9, 71, 71)) + # An unmatched tag has no bounding box. + self.assertIsNone(c.bbox('nonexistent')) + # At least one tag or id is required. + self.assertRaises(TclError, c.bbox) + + def test_canvasx_canvasy(self): + c = self.create(borderwidth=0, highlightthickness=0) + self.assertEqual(c.canvasx(0), 0.0) + self.assertEqual(c.canvasx(10), 10.0) + self.assertIsInstance(c.canvasx(10), float) + # gridspacing rounds to the nearest multiple. + self.assertEqual(c.canvasx(13, 5), 15.0) + self.assertEqual(c.canvasy(0), 0.0) + self.assertEqual(c.canvasy(10), 10.0) + self.assertRaises(TypeError, c.canvasx) + self.assertRaises(TypeError, c.canvasx, 0, 5, 1) + self.assertRaises(TypeError, c.canvasy) + self.assertRaises(TypeError, c.canvasy, 0, 5, 1) + self.assertRaises(TclError, c.canvasx, 'spam') + self.assertRaises(TclError, c.canvasy, 'spam') + + def test_move(self): + c = self.create() + rect = c.create_rectangle(10, 10, 30, 30) + c.move(rect, 5, 7) + self.assertEqual(c.coords(rect), [15.0, 17.0, 35.0, 37.0]) + c.move(rect, -5, -7) + self.assertEqual(c.coords(rect), [10.0, 10.0, 30.0, 30.0]) + # move() takes variable arguments; bad or missing values reach Tcl. + self.assertRaises(TclError, c.move, rect, 'spam', 0) + self.assertRaises(TclError, c.move, rect) + + def test_scale(self): + c = self.create() + rect = c.create_rectangle(10, 10, 30, 30) + c.scale(rect, 0, 0, 2, 2) + self.assertEqual(c.coords(rect), [20.0, 20.0, 60.0, 60.0]) + c.scale(rect, 0, 0, 0.5, 0.5) + self.assertEqual(c.coords(rect), [10.0, 10.0, 30.0, 30.0]) + self.assertRaises(TclError, c.scale, rect, 0, 0, 'spam', 2) + self.assertRaises(TclError, c.scale, rect, 0, 0) # missing factors + + def test_delete(self): + c = self.create() + r1 = c.create_rectangle(10, 10, 30, 30) + r2 = c.create_rectangle(50, 50, 70, 70) + r3 = c.create_rectangle(90, 90, 110, 110) + self.assertEqual(c.find_all(), (r1, r2, r3)) + c.delete(r2) + self.assertEqual(c.find_all(), (r1, r3)) + # Deleting a non-existent item is not an error. + c.delete(9999) + c.delete('all') + self.assertEqual(c.find_all(), ()) + + def test_find(self): + c = self.create() + r1 = c.create_rectangle(10, 10, 30, 30) + r2 = c.create_rectangle(50, 50, 70, 70) + r3 = c.create_rectangle(100, 100, 120, 120) + + self.assertEqual(c.find_all(), (r1, r2, r3)) + # find_above/find_below return the single adjacent item. + self.assertEqual(c.find_above(r1), (r2,)) + self.assertEqual(c.find_below(r3), (r2,)) + self.assertEqual(c.find_above(r3), ()) # nothing above the top item + self.assertEqual(c.find_withtag(r2), (r2,)) + self.assertEqual(c.find_closest(60, 60), (r2,)) + self.assertEqual(c.find_enclosed(0, 0, 80, 80), (r1, r2)) + self.assertEqual(c.find_overlapping(0, 0, 20, 20), (r1,)) + # An unmatched query returns an empty tuple. + self.assertEqual(c.find_withtag('nonexistent'), ()) + for result in (c.find_all(), c.find_withtag(r1)): + self.assertIsInstance(result, tuple) + + self.assertRaises(TclError, c.find_closest, 'spam', 0) + self.assertRaises(TclError, c.find_enclosed, 0, 0, 'spam', 0) + self.assertRaises(TclError, c.find_overlapping, 0, 0, 'spam', 0) + self.assertRaises(TypeError, c.find_withtag) + self.assertRaises(TypeError, c.find_withtag, r1, 'extra') + self.assertRaises(TypeError, c.find_above) + self.assertRaises(TypeError, c.find_above, r1, 'extra') + self.assertRaises(TypeError, c.find_below) + self.assertRaises(TypeError, c.find_closest) + self.assertRaises(TypeError, c.find_closest, 0, 0, 1, 2, 3) + self.assertRaises(TypeError, c.find_enclosed, 0, 0, 1) + self.assertRaises(TypeError, c.find_enclosed, 0, 0, 1, 2, 3) + + def test_addtag_gettags_dtag(self): + c = self.create() + r1 = c.create_rectangle(10, 10, 30, 30) + r2 = c.create_rectangle(50, 50, 70, 70) + + self.assertEqual(c.gettags(r1), ()) + c.addtag_withtag('spam', r1) + self.assertEqual(c.gettags(r1), ('spam',)) + self.assertEqual(c.find_withtag('spam'), (r1,)) + + c.addtag_all('all') + self.assertIn('all', c.gettags(r1)) + self.assertIn('all', c.gettags(r2)) + + c.addtag_above('above1', r1) + self.assertIn('above1', c.gettags(r2)) + c.addtag_below('below2', r2) + self.assertIn('below2', c.gettags(r1)) + + c.addtag_enclosed('enc', 0, 0, 40, 40) + self.assertEqual(c.find_withtag('enc'), (r1,)) + c.addtag_overlapping('ov', 0, 0, 20, 20) + self.assertEqual(c.find_withtag('ov'), (r1,)) + c.addtag_closest('close', 60, 60) + self.assertEqual(c.find_withtag('close'), (r2,)) + + # gettags of an unmatched tag is empty. + self.assertEqual(c.gettags('nonexistent'), ()) + + # dtag removes a tag from an item. + c.dtag(r1, 'spam') + self.assertNotIn('spam', c.gettags(r1)) + + self.assertRaises(TypeError, c.addtag_withtag, 'tag') + self.assertRaises(TypeError, c.addtag_withtag, 'tag', r1, 'extra') + self.assertRaises(TypeError, c.addtag_all) + self.assertRaises(TypeError, c.addtag_enclosed, 'tag', 0, 0, 1) + self.assertRaises(TypeError, c.addtag_enclosed, 'tag', 0, 0, 1, 2, 3) + self.assertRaises(TclError, c.addtag_closest, 'tag', 'spam', 0) + self.assertRaises(TclError, c.addtag_enclosed, 'tag', 0, 0, 'spam', 0) + self.assertRaises(TclError, c.gettags) # needs an item + + def test_itemconfigure(self): + c = self.create() + rect = c.create_rectangle(10, 10, 30, 30) + c.itemconfigure(rect, fill='red', width=3) + self.assertEqual(str(c.itemcget(rect, 'fill')), 'red') + self.assertEqual(float(c.itemcget(rect, 'width')), 3.0) + + # Querying all options returns a dict; a single one returns its spec. + cnf = c.itemconfigure(rect) + self.assertIsInstance(cnf, dict) + self.assertIn('fill', cnf) + self.assertEqual(c.itemconfigure(rect, 'fill')[-1], 'red') + + # itemconfig is an alias of itemconfigure. + self.assertEqual(c.itemconfig, c.itemconfigure) + + self.assertRaises(TclError, c.itemcget, rect, 'badoption') + self.assertRaises(TclError, c.itemconfigure, rect, badoption='x') + self.assertRaises(TypeError, c.itemcget, rect) + self.assertRaises(TypeError, c.itemcget, rect, 'fill', 'extra') + + def test_tag_raise_lower(self): + c = self.create() + r1 = c.create_rectangle(10, 10, 30, 30) + r2 = c.create_rectangle(50, 50, 70, 70) + r3 = c.create_rectangle(90, 90, 110, 110) + self.assertEqual(c.find_all(), (r1, r2, r3)) + + c.tag_raise(r1) + self.assertEqual(c.find_all(), (r2, r3, r1)) + c.tag_lower(r1) + self.assertEqual(c.find_all(), (r1, r2, r3)) + # Raise above / lower below a specific item. + c.tag_raise(r1, r2) + self.assertEqual(c.find_all(), (r2, r1, r3)) + c.tag_lower(r3, r2) + self.assertEqual(c.find_all(), (r3, r2, r1)) + + # lower/lift are aliases of tag_lower/tag_raise. + self.assertEqual(c.lower, c.tag_lower) + self.assertEqual(c.lift, c.tag_raise) + + # raise/lower need at least an item; an unmatched tag is not an error. + self.assertRaises(TclError, c.tag_raise) + self.assertRaises(TclError, c.tag_lower) + self.assertIsNone(c.tag_raise('nonexistent')) + + def test_text_item(self): + c = self.create() + item = c.create_text(10, 10, text='Hello') + self.assertEqual(c.index(item, 'end'), 5) + self.assertIsInstance(c.index(item, 'end'), int) + + c.insert(item, 'end', ' world') + self.assertEqual(c.itemcget(item, 'text'), 'Hello world') + self.assertEqual(c.index(item, 'end'), 11) + c.insert(item, 0, '>> ') + self.assertEqual(c.itemcget(item, 'text'), '>> Hello world') + + c.dchars(item, 0, 2) + self.assertEqual(c.itemcget(item, 'text'), 'Hello world') + c.icursor(item, 3) + + # index requires an indexable (text) item and a valid index. + self.assertRaises(TclError, c.index, item, 'badspec') + self.assertRaises(TclError, c.index, item) # missing index + self.assertRaises(TclError, c.dchars, item, 'badspec', 'end') + rect = c.create_rectangle(10, 10, 30, 30) + self.assertRaises(TclError, c.index, rect, 'end') + + def test_select(self): + c = self.create() + item = c.create_text(10, 10, text='Hello world') + self.assertIsNone(c.select_item()) + + c.select_from(item, 0) + c.select_to(item, 4) + self.assertEqual(int(c.select_item()), item) + c.select_adjust(item, 6) + + c.select_clear() + self.assertIsNone(c.select_item()) + self.assertRaises(TypeError, c.select_from, item) + self.assertRaises(TypeError, c.select_from, item, 0, 'extra') + # A bad index reaches Tcl; selection applies only to text items. + self.assertRaises(TclError, c.select_from, item, 'badspec') + rect = c.create_rectangle(10, 10, 30, 30) + self.assertRaises(TclError, c.select_from, rect, 0) + + def test_focus(self): + c = self.create() + item = c.create_text(10, 10, text='Hello') + # Only text items can take the focus. + c.focus(item) + self.assertEqual(int(c.focus()), item) + c.focus('') + self.assertIn(c.focus(), ('', None)) + + def test_scan(self): + c = self.create() + c.create_rectangle(10, 10, 300, 300) + c.scan_mark(0, 0) + c.scan_dragto(5, 5) # default gain=10 + c.scan_dragto(5, 5, 1) + self.assertRaises(TypeError, c.scan_mark) + self.assertRaises(TypeError, c.scan_mark, 0, 0, 0) + self.assertRaises(TclError, c.scan_mark, 'spam', 0) + + def test_postscript(self): + c = self.create() + c.create_rectangle(10, 10, 30, 30, fill='black') + ps = c.postscript() + self.assertIsInstance(ps, str) + self.assertStartsWith(ps, '%!PS-Adobe') + self.assertRaises(TclError, c.postscript, badoption='spam') + + def assertCommandExist(self, widget, funcid): + self.assertEqual( + widget.tk.splitlist(widget.tk.call('info', 'commands', funcid)), + (funcid,)) + + def assertCommandNotExist(self, widget, funcid): + self.assertEqual( + widget.tk.splitlist(widget.tk.call('info', 'commands', funcid)), + ()) + + def test_tag_bind(self): + c = self.create() + c.pack() + item = c.create_rectangle(20, 20, 100, 100, fill='red') + self.assertEqual(c.tag_bind(item), ()) + self.assertEqual(c.tag_bind(item, ''), '') + + events = [] + def test1(e): events.append('a') + def test2(e): events.append('b') + + funcid = c.tag_bind(item, '', test1) + self.assertEqual(c.tag_bind(item), ('',)) + script = c.tag_bind(item, '') + self.assertIn(funcid, script) + self.assertCommandExist(c, funcid) + + funcid2 = c.tag_bind(item, '', test2, add=True) + script = c.tag_bind(item, '') + self.assertIn(funcid, script) + self.assertIn(funcid2, script) + self.assertCommandExist(c, funcid) + self.assertCommandExist(c, funcid2) + + c.wait_visibility() + c.focus_force() + c.update() + c.event_generate('', x=50, y=50) + c.update() + self.assertEqual(events, ['a', 'b']) + + # Binding to a tag applies to all items carrying it. + c.addtag_withtag('spam', item) + events.clear() + c.tag_bind('spam', '', test1) + c.event_generate('', x=50, y=50) + c.update() + self.assertEqual(events, ['a']) + + def test_tag_unbind(self): + c = self.create() + c.pack() + item = c.create_rectangle(20, 20, 100, 100, fill='red') + + events = [] + def test1(e): events.append('a') + def test2(e): events.append('b') + + funcid = c.tag_bind(item, '', test1) + funcid2 = c.tag_bind(item, '', test2, add=True) + c.wait_visibility() + c.focus_force() + c.update() + c.event_generate('', x=50, y=50) + c.update() + self.assertEqual(events, ['a', 'b']) + + # Removing one function leaves the other in place. + c.tag_unbind(item, '', funcid) + script = c.tag_bind(item, '') + self.assertNotIn(funcid, script) + self.assertIn(funcid2, script) + self.assertCommandNotExist(c, funcid) + self.assertCommandExist(c, funcid2) + events.clear() + c.event_generate('', x=50, y=50) + c.update() + self.assertEqual(events, ['b']) + + # Without a funcid all bindings for the sequence are removed. + c.tag_unbind(item, '') + self.assertEqual(c.tag_bind(item, ''), '') + self.assertEqual(c.tag_bind(item), ()) + events.clear() + c.event_generate('', x=50, y=50) + c.update() + self.assertEqual(events, []) + + self.assertRaises(TypeError, c.tag_unbind, item) + + +@add_configure_tests(IntegerSizeTests, StandardOptionsTests) +class ListboxTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'activestyle', 'background', 'borderwidth', 'cursor', + 'disabledforeground', 'exportselection', + 'font', 'foreground', 'height', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'justify', 'listvariable', 'relief', + 'selectbackground', 'selectborderwidth', 'selectforeground', + 'selectmode', 'setgrid', 'state', + 'takefocus', 'width', 'xscrollcommand', 'yscrollcommand', + ) + if tk_version < (8, 7): + _clipped = {'highlightthickness'} + else: + _clipped = {'borderwidth', 'highlightthickness', 'selectborderwidth'} + + def create(self, **kwargs): + return tkinter.Listbox(self.root, **kwargs) + + def test_configure_activestyle(self): + widget = self.create() + self.checkEnumParam(widget, 'activestyle', + 'dotbox', 'none', 'underline') + + test_configure_justify = requires_tk(8, 6, 5)(StandardOptionsTests.test_configure_justify) + + def test_configure_listvariable(self): + widget = self.create() + var = tkinter.DoubleVar(self.root) + self.checkVariableParam(widget, 'listvariable', var) + + def test_configure_selectmode(self): + widget = self.create() + self.checkParam(widget, 'selectmode', 'single') + self.checkParam(widget, 'selectmode', 'browse') + self.checkParam(widget, 'selectmode', 'multiple') + self.checkParam(widget, 'selectmode', 'extended') + + def test_configure_state(self): + widget = self.create() + self.checkEnumParam(widget, 'state', 'disabled', 'normal') + + def test_itemconfigure(self): + widget = self.create() + with self.assertRaisesRegex(TclError, 'item number "0" out of range'): + widget.itemconfigure(0) + colors = 'red orange yellow green blue white violet'.split() + widget.insert('end', *colors) + for i, color in enumerate(colors): + widget.itemconfigure(i, background=color) + with self.assertRaises(TypeError): + widget.itemconfigure() + with self.assertRaisesRegex(TclError, 'bad listbox index "red"'): + widget.itemconfigure('red') + if get_tk_patchlevel(self.root) >= (8, 6, 14): + prefix = ('background', '', '', '') + else: + prefix = ('background', 'background', 'Background', '') + self.assertEqual(widget.itemconfigure(0, 'background'), + (*prefix, 'red')) + self.assertEqual(widget.itemconfigure('end', 'background'), + (*prefix, 'violet')) + self.assertEqual(widget.itemconfigure('@0,0', 'background'), + (*prefix, 'red')) + + d = widget.itemconfigure(0) + self.assertIsInstance(d, dict) + for k, v in d.items(): + self.assertIn(len(v), (2, 5)) + if len(v) == 5: + self.assertEqual(v, widget.itemconfigure(0, k)) + self.assertEqual(v[4], widget.itemcget(0, k)) + + def check_itemconfigure(self, name, value): + widget = self.create() + widget.insert('end', 'a', 'b', 'c', 'd') + widget.itemconfigure(0, **{name: value}) + self.assertEqual(widget.itemconfigure(0, name)[4], value) + self.assertEqual(widget.itemcget(0, name), value) + with self.assertRaisesRegex(TclError, 'unknown color name "spam"'): + widget.itemconfigure(0, **{name: 'spam'}) + + def test_itemconfigure_background(self): + self.check_itemconfigure('background', '#ff0000') + + def test_itemconfigure_bg(self): + self.check_itemconfigure('bg', '#ff0000') + + def test_itemconfigure_fg(self): + self.check_itemconfigure('fg', '#110022') + + def test_itemconfigure_foreground(self): + self.check_itemconfigure('foreground', '#110022') + + def test_itemconfigure_selectbackground(self): + self.check_itemconfigure('selectbackground', '#110022') + + def test_itemconfigure_selectforeground(self): + self.check_itemconfigure('selectforeground', '#654321') + + def test_box(self): + lb = self.create() + lb.insert(0, *('el%d' % i for i in range(8))) + lb.pack() + self.assertIsBoundingBox(lb.bbox(0)) + self.assertIsNone(lb.bbox(-1)) + self.assertIsNone(lb.bbox(10)) + self.assertRaises(TclError, lb.bbox, 'noindex') + self.assertRaises(TclError, lb.bbox, None) + self.assertRaises(TypeError, lb.bbox) + self.assertRaises(TypeError, lb.bbox, 0, 1) + + def test_curselection(self): + lb = self.create() + lb.insert(0, *('el%d' % i for i in range(8))) + lb.selection_clear(0, tkinter.END) + lb.selection_set(2, 4) + lb.selection_set(6) + self.assertEqual(lb.curselection(), (2, 3, 4, 6)) + self.assertRaises(TypeError, lb.curselection, 0) + + def test_get(self): + lb = self.create() + lb.insert(0, *('el%d' % i for i in range(8))) + self.assertEqual(lb.get(0), 'el0') + self.assertEqual(lb.get(3), 'el3') + self.assertEqual(lb.get('end'), 'el7') + self.assertEqual(lb.get(8), '') + self.assertEqual(lb.get(-1), '') + self.assertEqual(lb.get(3, 5), ('el3', 'el4', 'el5')) + self.assertEqual(lb.get(5, 'end'), ('el5', 'el6', 'el7')) + self.assertEqual(lb.get(5, 0), ()) + self.assertEqual(lb.get(0, 0), ('el0',)) + self.assertRaises(TclError, lb.get, 'noindex') + self.assertRaises(TclError, lb.get, None) + self.assertRaises(TypeError, lb.get) + self.assertRaises(TclError, lb.get, 'end', 'noindex') + self.assertRaises(TypeError, lb.get, 1, 2, 3) + self.assertRaises(TclError, lb.get, 2.4) + + def test_size(self): + lb = self.create() + self.assertEqual(lb.size(), 0) + lb.insert(0, *('el%d' % i for i in range(8))) + self.assertEqual(lb.size(), 8) + lb.delete(0, 2) + self.assertEqual(lb.size(), 5) + self.assertRaises(TypeError, lb.size, 0) + + def test_delete(self): + lb = self.create() + lb.insert(0, *('el%d' % i for i in range(8))) + lb.delete(0) + self.assertEqual(lb.get(0, 'end'), + ('el1', 'el2', 'el3', 'el4', 'el5', 'el6', 'el7')) + lb.delete(2, 4) + self.assertEqual(lb.get(0, 'end'), ('el1', 'el2', 'el6', 'el7')) + lb.delete(0, 'end') + self.assertEqual(lb.size(), 0) + self.assertRaises(TclError, lb.delete, 'noindex') + self.assertRaises(TypeError, lb.delete) + + def test_index(self): + lb = self.create() + lb.insert(0, *('el%d' % i for i in range(8))) + self.assertEqual(lb.index(3), 3) + self.assertEqual(lb.index('end'), 8) # the number of elements + lb.activate(4) + self.assertEqual(lb.index('active'), 4) + lb.selection_anchor(2) + self.assertEqual(lb.index('anchor'), 2) + self.assertRaisesRegex(TclError, 'bad listbox index "spam"', + lb.index, 'spam') + + def test_nearest(self): + lb = self.create() + lb.insert(0, *('el%d' % i for i in range(8))) + lb.pack() + lb.wait_visibility() + lb.update() + # Derive the line height from the first item, which is always + # displayed (bbox() returns None for items that are not). + x, y, w, h = lb.bbox(0) + self.assertEqual(lb.nearest(y + h // 2), 0) + self.assertEqual(lb.nearest(y + 3 * h + h // 2), 3) + self.assertRaises(TclError, lb.nearest, 'spam') + self.assertRaises(TypeError, lb.nearest) + + def test_see(self): + lb = self.create(height=5) + lb.insert(0, *('el%d' % i for i in range(20))) + lb.pack() + lb.update_idletasks() + lb.see('end') + lb.update_idletasks() + self.assertEqual(lb.yview()[1], 1.0) + lb.see(0) + lb.update_idletasks() + self.assertEqual(lb.yview()[0], 0.0) + self.assertRaises(TclError, lb.see, 'spam') + + def test_activate(self): + lb = self.create() + lb.insert(0, *('el%d' % i for i in range(8))) + lb.activate(3) + self.assertEqual(lb.index('active'), 3) + lb.activate('end') + self.assertEqual(lb.index('active'), 7) + self.assertRaises(TclError, lb.activate, 'spam') + self.assertRaises(TypeError, lb.activate) + + def test_selection(self): + lb = self.create() + lb.insert(0, *('el%d' % i for i in range(8))) + self.assertEqual(lb.curselection(), ()) + self.assertFalse(lb.selection_includes(0)) + + lb.selection_set(2, 4) + lb.selection_set(6) + self.assertEqual(lb.curselection(), (2, 3, 4, 6)) + self.assertTrue(lb.selection_includes(3)) + self.assertFalse(lb.selection_includes(5)) + + lb.selection_clear(3, 4) + self.assertEqual(lb.curselection(), (2, 6)) + + lb.selection_anchor(5) + self.assertEqual(lb.index('anchor'), 5) + + # select_* are aliases of the selection_* methods. + lb.select_clear(0, 'end') + self.assertEqual(lb.curselection(), ()) + lb.select_set(1) + self.assertTrue(lb.select_includes(1)) + lb.select_anchor(1) + self.assertEqual(lb.index('anchor'), 1) + + self.assertRaisesRegex(TclError, 'bad listbox index "spam"', + lb.selection_includes, 'spam') + + def test_selection_event(self): + # Keyboard navigation changes the selection and fires the + # <> virtual event. + lb = self.create(selectmode='browse', exportselection=False) + lb.insert(0, *('el%d' % i for i in range(5))) + lb.pack() + self.require_mapped(lb) + events = [] + lb.bind('<>', lambda e: events.append(lb.curselection())) + lb.focus_force() + lb.activate(0) + lb.event_generate('') + lb.event_generate('') + lb.update() + self.assertEqual(events, [(1,), (2,)]) + self.assertEqual(lb.curselection(), (2,)) + + +@add_configure_tests(PixelSizeTests, StandardOptionsTests) +class ScaleTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'activebackground', 'background', 'bigincrement', 'borderwidth', + 'command', 'cursor', 'digits', 'font', 'foreground', 'from', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'label', 'length', 'orient', 'relief', + 'repeatdelay', 'repeatinterval', + 'resolution', 'showvalue', 'sliderlength', 'sliderrelief', 'state', + 'takefocus', 'tickinterval', 'to', 'troughcolor', 'variable', 'width', + ) + _clipped = {'highlightthickness'} + default_orient = 'vertical' + + def create(self, **kwargs): + return tkinter.Scale(self.root, **kwargs) + + def test_configure_bigincrement(self): + widget = self.create() + self.checkFloatParam(widget, 'bigincrement', 12.4, 23.6, -5) + + def test_configure_digits(self): + widget = self.create() + self.checkIntegerParam(widget, 'digits', 5, 0) + + def test_configure_from(self): + widget = self.create() + conv = float if get_tk_patchlevel(self.root) >= (8, 6, 10) else float_round + self.checkFloatParam(widget, 'from', 100, 14.9, 15.1, conv=conv) + + def test_configure_label(self): + widget = self.create() + self.checkParam(widget, 'label', 'any string') + self.checkParam(widget, 'label', '') + + def test_configure_length(self): + widget = self.create() + self.checkPixelsParam(widget, 'length', 130, 131.2, 135.6, '5i') + + def test_configure_resolution(self): + widget = self.create() + self.checkFloatParam(widget, 'resolution', 4.2, 0, 6.7, -2) + + def test_configure_showvalue(self): + widget = self.create() + self.checkBooleanParam(widget, 'showvalue') + + def test_configure_sliderlength(self): + widget = self.create() + self.checkPixelsParam(widget, 'sliderlength', + 10, 11.2, 15.6, -3, '3m') + + def test_configure_sliderrelief(self): + widget = self.create() + self.checkReliefParam(widget, 'sliderrelief') + + def test_configure_tickinterval(self): + widget = self.create() + self.checkFloatParam(widget, 'tickinterval', 1, 4.3, 7.6, 0, + conv=float_round) + self.checkParam(widget, 'tickinterval', -2, expected=2, + conv=float_round) + + def test_configure_to(self): + widget = self.create() + self.checkFloatParam(widget, 'to', 300, 14.9, 15.1, -10, + conv=float_round) + + def test_identify(self): + widget = self.create() + widget.pack() + # Probe a point on the trough centreline (Scale.coords()) rather than + # a fixed pixel: (5, 5) lies outside the trough and always identifies + # as '', so it would not actually exercise identify(). + if wait_until_mapped(widget): + x, y = widget.coords() + self.assertIn(widget.identify(int(x), int(y)), + ('slider', 'trough1', 'trough2')) + self.assertRaises(TclError, widget.identify, 'a', 'b') + + +@add_configure_tests(PixelSizeTests, StandardOptionsTests) +class ScrollbarTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'activebackground', 'activerelief', + 'background', 'borderwidth', + 'command', 'cursor', 'elementborderwidth', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'jump', 'orient', 'relief', + 'repeatdelay', 'repeatinterval', + 'takefocus', 'troughcolor', 'width', + ) + if tk_version < (8, 7): + _clipped = {'highlightthickness'} + elif tk_version < (9, 0): + _clipped = {'borderwidth', 'elementborderwidth', 'highlightthickness'} + else: + _clipped = {'borderwidth', 'elementborderwidth', 'highlightthickness', 'width'} + _clipped_to_default = {'elementborderwidth'} + _stringify = True + default_orient = 'vertical' + + def create(self, **kwargs): + return tkinter.Scrollbar(self.root, **kwargs) + + def test_configure_elementborderwidth(self): + widget = self.create() + self.checkPixelsParam(widget, 'elementborderwidth', 4.3, 5.6, -2, '1m') + + def test_configure_orient(self): + widget = self.create() + self.checkEnumParam(widget, 'orient', 'vertical', 'horizontal', + fullname='orientation', allow_empty=True) + + def test_activate(self): + sb = self.create() + for e in ('arrow1', 'slider', 'arrow2'): + sb.activate(e) + self.assertEqual(sb.activate(), e) + sb.activate('') + self.assertIsNone(sb.activate()) + self.assertRaises(TypeError, sb.activate, 'arrow1', 'arrow2') + + def test_set(self): + sb = self.create() + sb.set(0.2, 0.4) + self.assertEqual(sb.get(), (0.2, 0.4)) + self.assertRaises(TclError, sb.set, 'abc', 'def') + self.assertRaises(TclError, sb.set, 0.6, 'def') + self.assertRaises(TclError, sb.set, 0.6, None) + self.assertRaises(TypeError, sb.set, 0.6) + self.assertRaises(TypeError, sb.set, 0.6, 0.7, 0.8) + + def test_fraction(self): + sb = self.create() + sb.pack(fill='y', expand=True) + sb.update_idletasks() + self.assertIsInstance(sb.fraction(0, 0), float) + f = sb.fraction(0, 1000) + self.assertIsInstance(f, float) + self.assertGreaterEqual(f, 0.0) + self.assertLessEqual(f, 1.0) + self.assertRaises(TclError, sb.fraction, 'a', 'b') + self.assertRaises(TypeError, sb.fraction, 0) + + def test_delta(self): + sb = self.create() + sb.pack(fill='y', expand=True) + sb.update_idletasks() + self.assertIsInstance(sb.delta(0, 10), float) + self.assertRaises(TclError, sb.delta, 'a', 'b') + self.assertRaises(TypeError, sb.delta, 0) + + def test_identify(self): + sb = self.create() + sb.pack(fill='y', expand=True) + # Identifying the element under a point requires the widget to be + # mapped with a real size. + if wait_until_mapped(sb): + self.assertIn(sb.identify(5, 5), + ('arrow1', 'arrow2', 'slider', 'trough1', 'trough2')) + self.assertRaises(TclError, sb.identify, 'a', 'b') + + +@add_configure_tests(PixelSizeTests, StandardOptionsTests) +class PanedWindowTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'background', 'borderwidth', 'cursor', + 'handlepad', 'handlesize', 'height', + 'opaqueresize', 'orient', + 'proxybackground', 'proxyborderwidth', 'proxyrelief', + 'relief', + 'sashcursor', 'sashpad', 'sashrelief', 'sashwidth', + 'showhandle', 'width', + ) + _no_round = {'handlesize', 'height', 'proxyborderwidth', 'sashwidth', + 'selectborderwidth', 'width'} + _clipped = {} + default_orient = 'horizontal' + + def create(self, **kwargs): + return tkinter.PanedWindow(self.root, **kwargs) + + def test_configure_handlepad(self): + widget = self.create() + self.checkPixelsParam(widget, 'handlepad', 5, 6.4, 7.6, -3, '1m') + + def test_configure_handlesize(self): + widget = self.create() + self.checkPixelsParam(widget, 'handlesize', 8, 9.4, 10.6, -3, '2m') + + def test_configure_opaqueresize(self): + widget = self.create() + self.checkBooleanParam(widget, 'opaqueresize') + + @requires_tk(8, 6, 5) + def test_configure_proxybackground(self): + widget = self.create() + self.checkColorParam(widget, 'proxybackground') + + @requires_tk(8, 6, 5) + def test_configure_proxyborderwidth(self): + widget = self.create() + self.checkPixelsParam(widget, 'proxyborderwidth', + 0, 1.3, 2.9, 6, -2, '10p') + + @requires_tk(8, 6, 5) + def test_configure_proxyrelief(self): + widget = self.create() + self.checkReliefParam(widget, 'proxyrelief', + allow_empty=(tk_version >= (8, 7))) + + def test_configure_sashcursor(self): + widget = self.create() + self.checkCursorParam(widget, 'sashcursor') + + def test_configure_sashpad(self): + widget = self.create() + self.checkPixelsParam(widget, 'sashpad', 8, 1.3, 2.6, -2, '2m') + + def test_configure_sashrelief(self): + widget = self.create() + self.checkReliefParam(widget, 'sashrelief') + + def test_configure_sashwidth(self): + widget = self.create() + self.checkPixelsParam(widget, 'sashwidth', 10, 11.1, 15.6, -3, '1m') + + def test_configure_showhandle(self): + widget = self.create() + self.checkBooleanParam(widget, 'showhandle') + + def create2(self): + p = self.create() + b = tkinter.Button(p) + c = tkinter.Button(p) + p.add(b) + p.add(c) + return p, b, c + + def test_panes(self): + p, b, c = self.create2() + self.assertEqual([str(x) for x in p.panes()], [str(b), str(c)]) + + def test_remove(self): + p, b, c = self.create2() + p.remove(b) + self.assertEqual([str(x) for x in p.panes()], [str(c)]) + p.forget(c) # forget is an alias of remove. + self.assertEqual(p.panes(), ()) + + def test_sash(self): + p, b, c = self.create2() + p.configure(width=200, height=50) + p.pack() + p.update() + x, y = p.sash_coord(0) + self.assertIsInstance(x, int) + self.assertIsInstance(y, int) + p.sash_place(0, 120, 0) + p.update() + self.assertEqual(p.sash_coord(0)[0], 120) + p.sash_mark(0) # No exception. + self.assertRaises(TclError, p.sash_coord, 5) + + def test_proxy(self): + p, b, c = self.create2() + p.configure(width=200, height=50) + p.pack() + p.update() + p.proxy_place(100, 10) + p.update() + self.assertEqual(p.proxy_coord()[0], 100) + p.proxy_forget() + p.update() + + def test_identify(self): + p, b, c = self.create2() + p.configure(width=200, height=50) + p.pack() + # Locating the sash requires the widget to be mapped with a real + # size; the rest of the checks do not. + if wait_until_mapped(p): + x, y = p.sash_coord(0) + # A point over the sash reports the sash. + self.assertIn('sash', p.identify(x + 1, y + 5)) + # A point over a pane reports nothing. + self.assertFalse(p.identify(2, 2)) + self.assertRaises(TclError, p.identify, 'a', 'b') + + def test_add_options(self): + p = self.create() + b = tkinter.Button(p) + p.add(b, minsize=40, padx=3, sticky='ns') + self.assertEqual(p.panecget(b, 'minsize'), + 40 if self.wantobjects else '40') + self.assertEqual(p.panecget(b, 'padx'), + 3 if self.wantobjects else '3') + self.assertEqual(p.panecget(b, 'sticky'), 'ns') + self.assertRaisesRegex(TclError, 'unknown option "-spam"', + p.add, tkinter.Button(p), spam='x') + self.assertRaisesRegex(TclError, 'bad window path name "spam"', + p.add, 'spam') + + def test_paneconfigure_errors(self): + p, b, c = self.create2() + self.assertRaisesRegex(TclError, 'unknown option "-spam"', + p.paneconfigure, b, spam='x') + self.assertRaises(TclError, p.panecget, b, 'spam') + self.assertRaises(TclError, p.paneconfigure, 'spam') + + def test_paneconfigure(self): + p, b, c = self.create2() + self.assertRaises(TypeError, p.paneconfigure) + d = p.paneconfigure(b) + self.assertIsInstance(d, dict) + for k, v in d.items(): + self.assertEqual(len(v), 5) + self.assertEqual(v, p.paneconfigure(b, k)) + self.assertEqual(v[4], p.panecget(b, k)) + + def check_paneconfigure(self, p, b, name, value, expected): + if not self.wantobjects: + expected = str(expected) + p.paneconfigure(b, **{name: value}) + self.assertEqual(p.paneconfigure(b, name)[4], expected) + self.assertEqual(p.panecget(b, name), expected) + + def check_paneconfigure_bad(self, p, b, name, msg): + with self.assertRaisesRegex(TclError, msg): + p.paneconfigure(b, **{name: 'badValue'}) + + def test_paneconfigure_after(self): + p, b, c = self.create2() + self.check_paneconfigure(p, b, 'after', c, str(c)) + self.check_paneconfigure_bad(p, b, 'after', + 'bad window path name "badValue"') + + def test_paneconfigure_before(self): + p, b, c = self.create2() + self.check_paneconfigure(p, b, 'before', c, str(c)) + self.check_paneconfigure_bad(p, b, 'before', + 'bad window path name "badValue"') + + def test_paneconfigure_height(self): + p, b, c = self.create2() + self.check_paneconfigure(p, b, 'height', 10, 10) + self.check_paneconfigure_bad(p, b, 'height', + EXPECTED_SCREEN_DISTANCE_OR_EMPTY_ERRMSG.format('badValue')) + + def test_paneconfigure_hide(self): + p, b, c = self.create2() + self.check_paneconfigure(p, b, 'hide', False, 0) + self.check_paneconfigure_bad(p, b, 'hide', + 'expected boolean value but got "badValue"') + + def test_paneconfigure_minsize(self): + p, b, c = self.create2() + self.check_paneconfigure(p, b, 'minsize', 10, 10) + self.check_paneconfigure_bad(p, b, 'minsize', + EXPECTED_SCREEN_DISTANCE_ERRMSG.format('badValue')) + + def test_paneconfigure_padx(self): + p, b, c = self.create2() + self.check_paneconfigure(p, b, 'padx', 1.3, 1 if tk_version < (9, 0) else 1.3) + self.check_paneconfigure_bad(p, b, 'padx', + EXPECTED_SCREEN_DISTANCE_ERRMSG.format('badValue')) + + def test_paneconfigure_pady(self): + p, b, c = self.create2() + self.check_paneconfigure(p, b, 'pady', 1.3, 1 if tk_version < (9, 0) else 1.3) + self.check_paneconfigure_bad(p, b, 'pady', + EXPECTED_SCREEN_DISTANCE_ERRMSG.format('badValue')) + + def test_paneconfigure_sticky(self): + p, b, c = self.create2() + self.check_paneconfigure(p, b, 'sticky', 'nsew', 'nesw') + self.check_paneconfigure_bad(p, b, 'sticky', + 'bad stickyness value "badValue": must ' + 'be a string containing zero or more of ' + 'n, e, s, and w') + + def test_paneconfigure_stretch(self): + p, b, c = self.create2() + self.check_paneconfigure(p, b, 'stretch', 'alw', 'always') + self.check_paneconfigure_bad(p, b, 'stretch', + 'bad stretch "badValue": must be ' + 'always, first, last, middle, or never') + + def test_paneconfigure_width(self): + p, b, c = self.create2() + self.check_paneconfigure(p, b, 'width', 10, 10) + self.check_paneconfigure_bad(p, b, 'width', + EXPECTED_SCREEN_DISTANCE_OR_EMPTY_ERRMSG.format('badValue')) + + +@add_configure_tests(StandardOptionsTests) +class MenuTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'activebackground', 'activeborderwidth', 'activeforeground', + 'activerelief', 'background', 'borderwidth', 'cursor', + 'disabledforeground', 'font', 'foreground', + 'postcommand', 'relief', 'selectcolor', 'takefocus', + 'tearoff', 'tearoffcommand', 'title', 'type', + ) + _rounds_pixels = False + _clipped = {} + + def create(self, **kwargs): + return tkinter.Menu(self.root, **kwargs) + + def test_indexcommand_none(self): + widget = self.create() + i = widget.index('none') + self.assertIsNone(i) + + test_configure_activerelief = requires_tk(8, 7)(StandardOptionsTests.test_configure_activerelief) + + def test_configure_postcommand(self): + widget = self.create() + self.checkCommandParam(widget, 'postcommand') + + def test_configure_tearoff(self): + widget = self.create() + self.checkBooleanParam(widget, 'tearoff') + + def test_configure_tearoffcommand(self): + widget = self.create() + self.checkCommandParam(widget, 'tearoffcommand') + + def test_configure_title(self): + widget = self.create() + self.checkParam(widget, 'title', 'any string') + + def test_configure_type(self): + widget = self.create() + values = ('normal', 'tearoff', 'menubar') + self.checkEnumParam(widget, 'type', *values, + allow_empty=tk_version < (8, 7), + sort=tk_version >= (8, 7)) + + def test_entryconfigure(self): + m1 = self.create() + m1.add_command(label='test') + self.assertRaises(TypeError, m1.entryconfigure) + with self.assertRaisesRegex(TclError, 'bad menu entry index "foo"'): + m1.entryconfigure('foo') + d = m1.entryconfigure(1) + self.assertIsInstance(d, dict) + for k, v in d.items(): + self.assertIsInstance(k, str) + self.assertIsInstance(v, tuple) + self.assertEqual(len(v), 5) + self.assertEqual(v[0], k) + self.assertEqual(m1.entrycget(1, k), v[4]) + m1.destroy() + + def test_entryconfigure_label(self): + m1 = self.create() + m1.add_command(label='test') + self.assertEqual(m1.entrycget(1, 'label'), 'test') + m1.entryconfigure(1, label='changed') + self.assertEqual(m1.entrycget(1, 'label'), 'changed') + + def test_entryconfigure_variable(self): + m1 = self.create() + v1 = tkinter.BooleanVar(self.root) + v2 = tkinter.BooleanVar(self.root) + m1.add_checkbutton(variable=v1, onvalue=True, offvalue=False, + label='Nonsense') + self.assertEqual(str(m1.entrycget(1, 'variable')), str(v1)) + m1.entryconfigure(1, variable=v2) + self.assertEqual(str(m1.entrycget(1, 'variable')), str(v2)) + + def test_add(self): + m = self.create(tearoff=False) + m.add_command(label='Command') + m.add_checkbutton(label='Checkbutton') + m.add_radiobutton(label='Radiobutton') + m.add_separator() + m.add_cascade(label='Cascade', menu=tkinter.Menu(m, tearoff=False)) + self.assertEqual(m.index('end'), 4) + self.assertEqual([m.type(i) for i in range(5)], + ['command', 'checkbutton', 'radiobutton', + 'separator', 'cascade']) + self.assertEqual(m.entrycget(0, 'label'), 'Command') + self.assertRaisesRegex(TclError, 'bad menu entry type "spam"', + m.add, 'spam') + + def test_insert(self): + m = self.create(tearoff=False) + m.add_command(label='A') + m.add_command(label='C') + m.insert_command(1, label='B') + m.insert_separator(0) + m.insert_checkbutton('end', label='D') + m.insert_radiobutton(0, label='top') + m.insert_cascade(2, label='sub', + menu=tkinter.Menu(m, tearoff=False)) + self.assertEqual( + [m.type(i) for i in range(m.index('end') + 1)], + ['radiobutton', 'separator', 'cascade', 'command', + 'command', 'command', 'checkbutton']) + self.assertEqual( + [m.entrycget(i, 'label') for i in (3, 4, 5)], + ['A', 'B', 'C']) + self.assertRaisesRegex(TclError, 'bad menu entry type "spam"', + m.insert, 0, 'spam') + self.assertRaisesRegex(TclError, 'bad menu entry index "spam"', + m.insert_command, 'spam', label='z') + + def test_delete(self): + m = self.create(tearoff=False) + commands = [] + for label in 'ABCDE': + m.add_command(label=label, + command=lambda label=label: commands.append(label)) + # The Tcl command for a deleted item is cleaned up. + funcid = str(m.entrycget(2, 'command')) + self.assertEqual( + m.tk.splitlist(m.tk.call('info', 'commands', funcid)), (funcid,)) + + m.delete(2) # Delete a single item ('C'). + self.assertEqual([m.entrycget(i, 'label') for i in range(4)], + ['A', 'B', 'D', 'E']) + self.assertEqual( + m.tk.splitlist(m.tk.call('info', 'commands', funcid)), ()) + + m.delete(1, 2) # Delete a range ('B' and 'D'). + self.assertEqual([m.entrycget(i, 'label') for i in range(2)], + ['A', 'E']) + self.assertRaises(TypeError, m.delete) + + def test_index(self): + m = self.create(tearoff=False) + self.assertIsNone(m.index('end')) + m.add_command(label='First') + m.add_command(label='Second') + self.assertEqual(m.index('end'), 1) + self.assertEqual(m.index('last'), 1) + self.assertEqual(m.index('Second'), 1) + self.assertEqual(m.index(0), 0) + # 'active' and 'none' map to None when no item is active. + self.assertIsNone(m.index('active')) + self.assertIsNone(m.index('none')) + self.assertRaisesRegex(TclError, 'bad menu entry index "spam"', + m.index, 'spam') + + def test_invoke(self): + m = self.create(tearoff=False) + commands = [] + m.add_command(label='Command', + command=lambda: commands.append('invoked')) + var = tkinter.IntVar(self.root) + m.add_checkbutton(label='Check', variable=var, + onvalue=1, offvalue=0) + rvar = tkinter.StringVar(self.root) + m.add_radiobutton(label='Radio', variable=rvar, value='on') + + m.invoke(0) + self.assertEqual(commands, ['invoked']) + m.invoke(1) + self.assertEqual(var.get(), 1) + m.invoke(1) + self.assertEqual(var.get(), 0) + m.invoke(2) + self.assertEqual(rvar.get(), 'on') + self.assertRaisesRegex(TclError, 'bad menu entry index "spam"', + m.invoke, 'spam') + + def test_xposition_yposition(self): + m = self.create(tearoff=False) + m.add_command(label='First') + m.add_command(label='Second') + m.update_idletasks() + self.assertIsInstance(m.xposition(0), int) + y0 = m.yposition(0) + y1 = m.yposition(1) + self.assertIsInstance(y0, int) + self.assertLess(y0, y1) + # An out-of-range index gives the position past the last item. + self.assertEqual(m.xposition('end'), m.xposition(1)) + self.assertRaisesRegex(TclError, 'bad menu entry index "spam"', + m.xposition, 'spam') + self.assertRaisesRegex(TclError, 'bad menu entry index "spam"', + m.yposition, 'spam') + + def test_post_unpost(self): + m = self.create(tearoff=False) + if m._windowingsystem != 'x11': + # Posting a menu is modal on Windows and uses a native, unmapped + # menu on Aqua, so it cannot be tested synchronously there. + self.skipTest('menu posting is not testable on this platform') + m.add_command(label='First') + m.add_command(label='Second') + self.assertFalse(m.winfo_ismapped()) + + m.post(0, 0) + m.update() + self.assertTrue(m.winfo_ismapped()) + m.unpost() + m.update() + self.assertFalse(m.winfo_ismapped()) + + m.tk_popup(0, 0) + m.update() + self.assertTrue(m.winfo_ismapped()) + m.unpost() + m.update() + self.assertFalse(m.winfo_ismapped()) + + def check_entry_option(self, m, index, option, value, expected=None): + if expected is None: + expected = value + m.entryconfigure(index, **{option: value}) + self.assertEqual(str(m.entrycget(index, option)), str(expected)) + self.assertEqual(str(m.entryconfigure(index, option)[4]), str(expected)) + + def test_entry_options(self): + m = self.create(tearoff=False) + m.add_command(label='Command') + self.check_entry_option(m, 0, 'accelerator', 'Ctrl+O') + self.check_entry_option(m, 0, 'underline', 2) + self.check_entry_option(m, 0, 'state', 'disabled') + self.check_entry_option(m, 0, 'background', 'red') + self.check_entry_option(m, 0, 'foreground', 'blue') + self.check_entry_option(m, 0, 'columnbreak', 1) + self.check_entry_option(m, 0, 'hidemargin', 1) + + m.add_checkbutton(label='Checkbutton') + self.check_entry_option(m, 1, 'onvalue', 'Y') + self.check_entry_option(m, 1, 'offvalue', 'N') + self.check_entry_option(m, 1, 'indicatoron', 0) + + m.add_radiobutton(label='Radiobutton') + self.check_entry_option(m, 2, 'value', 'V') + self.check_entry_option(m, 2, 'selectcolor', 'green') + + def test_entry_options_invalid(self): + m = self.create(tearoff=False) + m.add_command(label='Command') + self.assertRaisesRegex(TclError, 'unknown option "-spam"', + m.entrycget, 0, 'spam') + self.assertRaisesRegex(TclError, 'unknown option "-spam"', + m.entryconfigure, 0, spam='x') + self.assertRaisesRegex(TclError, 'bad state "spam"', + m.entryconfigure, 0, state='spam') + # Tk < 9 reports "expected integer but got ...", while Tk 9, where + # underline accepts an index, reports "bad index ...". + self.assertRaisesRegex(TclError, + r'(expected integer but got|bad index) "spam"', + m.entryconfigure, 0, underline='spam') + self.assertRaisesRegex(TclError, 'unknown color name "spam"', + m.entryconfigure, 0, background='spam') + self.assertRaisesRegex(TclError, 'expected boolean value but got "spam"', + m.entryconfigure, 0, columnbreak='spam') + # onvalue applies only to checkbutton and radiobutton entries. + self.assertRaisesRegex(TclError, 'unknown option "-onvalue"', + m.entrycget, 0, 'onvalue') + + +@add_configure_tests(PixelSizeTests, StandardOptionsTests) +class MessageTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'anchor', 'aspect', 'background', 'borderwidth', + 'cursor', 'font', 'foreground', + 'highlightbackground', 'highlightcolor', 'highlightthickness', + 'justify', 'padx', 'pady', 'relief', + 'takefocus', 'text', 'textvariable', 'width', + ) + _no_round = {'padx', 'pady'} + if tk_version < (8, 7): + _clipped = {'highlightthickness'} + else: + _clipped = {'borderwidth', 'highlightthickness', 'padx', 'pady', 'width'} + _clipped_to_default = {'padx', 'pady'} + + def create(self, **kwargs): + return tkinter.Message(self.root, **kwargs) + + def test_configure_aspect(self): + widget = self.create() + self.checkIntegerParam(widget, 'aspect', 250, 0, -300) + + +class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): + + def test_frame(self): + self._test_widget(tkinter.Frame) + + def test_label(self): + self._test_widget(tkinter.Label) + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_tkinter/widget_tests.py b/Lib/test/test_tkinter/widget_tests.py new file mode 100644 index 00000000000..014906cba29 --- /dev/null +++ b/Lib/test/test_tkinter/widget_tests.py @@ -0,0 +1,628 @@ +# Common tests for test_tkinter/test_widgets.py and test_ttk/test_widgets.py + +import re +import tkinter +from test.test_tkinter.support import (AbstractTkTest, requires_tk, tk_version, + pixels_conv, tcl_obj_eq) +import test.support + +_sentinel = object() + +# Abbreviated option names accepted by Tk in addition to the full names. +_OPTION_ALIASES = { + 'bd': 'borderwidth', + 'bg': 'background', + 'bgimg': 'backgroundimage', + 'fg': 'foreground', + 'invcmd': 'invalidcommand', + 'vcmd': 'validatecommand', +} + +# Options which accept all values allowed by Tk_GetPixels +# borderwidth = bd + +class AbstractWidgetTest(AbstractTkTest): + _default_pixels = '' if tk_version >= (9, 0) else -1 # Value for unset pixel options. + _rounds_pixels = (tk_version < (9, 0)) # True if some pixel options are rounded. + _no_round = set() # Pixel options which are not rounded nonetheless + _stringify = False # Whether to convert tuples to strings + _allow_empty_justify = False + _clipped_to_default = set() + + @property + def scaling(self): + try: + return self._scaling + except AttributeError: + self._scaling = float(self.root.call('tk', 'scaling')) + return self._scaling + + def _str(self, value): + if not self._stringify and self.wantobjects and tk_version >= (8, 6): + return value + if isinstance(value, tuple): + return ' '.join(map(self._str, value)) + return str(value) + + def assertEqual2(self, actual, expected, msg=None, eq=object.__eq__): + if eq(actual, expected): + return + self.assertEqual(actual, expected, msg) + + def checkParam(self, widget, name, value, *, expected=_sentinel, + conv=False, eq=None): + widget[name] = value + if expected is _sentinel: + expected = value + if name in self._clipped: + if not isinstance(expected, str) and expected < 0: + if tk_version >= (8, 7) and name in self._clipped_to_default: + expected = self._default_pixels + else: + expected = 0 + if conv: + expected = conv(expected) + if self._stringify or not self.wantobjects: + if isinstance(expected, tuple): + expected = tkinter._join(expected) + else: + expected = str(expected) + if eq is None: + eq = tcl_obj_eq + self.assertEqual2(widget[name], expected, eq=eq) + self.assertEqual2(widget.cget(name), expected, eq=eq) + t = widget.configure(name) + self.assertEqual(len(t), 5) + self.assertEqual2(t[4], expected, eq=eq) + + def checkInvalidParam(self, widget, name, value, errmsg=None): + orig = widget[name] + if errmsg is not None: + errmsg = errmsg.format(re.escape(str(value))) + errmsg = fr'\A{errmsg}\z' + with self.assertRaisesRegex(tkinter.TclError, errmsg or ''): + widget[name] = value + self.assertEqual(widget[name], orig) + with self.assertRaisesRegex(tkinter.TclError, errmsg or ''): + widget.configure({name: value}) + self.assertEqual(widget[name], orig) + + def checkParams(self, widget, name, *values, **kwargs): + for value in values: + self.checkParam(widget, name, value, **kwargs) + + def checkIntegerParam(self, widget, name, *values, **kwargs): + self.checkParams(widget, name, *values, **kwargs) + errmsg = 'expected integer but got "{}"' + self.checkInvalidParam(widget, name, '', errmsg=errmsg) + self.checkInvalidParam(widget, name, '10p', errmsg=errmsg) + self.checkInvalidParam(widget, name, 3.2, errmsg=errmsg) + + def checkFloatParam(self, widget, name, *values, conv=float, **kwargs): + for value in values: + self.checkParam(widget, name, value, conv=conv, **kwargs) + errmsg = 'expected floating-point number but got "{}"' + self.checkInvalidParam(widget, name, '', errmsg=errmsg) + self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg) + + def checkBooleanParam(self, widget, name): + for value in (False, 0, 'false', 'no', 'off'): + self.checkParam(widget, name, value, expected=0) + for value in (True, 1, 'true', 'yes', 'on'): + self.checkParam(widget, name, value, expected=1) + errmsg = 'expected boolean value but got "{}"' + self.checkInvalidParam(widget, name, '', errmsg=errmsg) + self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg) + + def checkColorParam(self, widget, name, *, allow_empty=None, **kwargs): + self.checkParams(widget, name, + '#ff0000', '#00ff00', '#0000ff', '#123456', + 'red', 'green', 'blue', 'white', 'black', 'grey', + **kwargs) + self.checkInvalidParam(widget, name, 'spam', + errmsg='unknown color name "spam"') + + def checkCursorParam(self, widget, name, **kwargs): + self.checkParams(widget, name, 'arrow', 'watch', 'cross', '',**kwargs) + self.checkParam(widget, name, 'none') + self.checkInvalidParam(widget, name, 'spam', + errmsg='bad cursor spec "spam"') + + def checkCommandParam(self, widget, name): + def command(*args): + pass + widget[name] = command + self.assertTrue(widget[name]) + self.checkParams(widget, name, '') + + def checkEnumParam(self, widget, name, *values, + errmsg=None, allow_empty=False, fullname=None, + sort=False, **kwargs): + self.checkParams(widget, name, *values, **kwargs) + if errmsg is None: + if sort: + if values[-1]: + values = tuple(sorted(values)) + else: + values = tuple(sorted(values[:-1])) + ('',) + errmsg2 = ' %s "{}": must be %s%s or %s' % ( + fullname or name, + ', '.join(values[:-1]), + ',' if len(values) > 2 else '', + values[-1] or '""') + if '' not in values and not allow_empty: + self.checkInvalidParam(widget, name, '', + errmsg='ambiguous' + errmsg2) + errmsg = 'bad' + errmsg2 + self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg) + + def checkPixelsParam(self, widget, name, *values, conv=None, **kwargs): + if conv is None: + if self._rounds_pixels and name not in self._no_round: + conv = round + alow_neg = tk_version < (9, 1) + for value in values: + expected = _sentinel + conv1 = conv + if isinstance(value, str): + if not getattr(self, '_converts_pixels', True): + conv1 = str + if conv1 and conv1 is not str: + expected = pixels_conv(value) * self.scaling + conv1 = round + elif not alow_neg and isinstance(value, (int, float)) and value < 0: + self.checkInvalidParam(widget, name, value) + continue + self.checkParam(widget, name, value, expected=expected, + conv=conv1, **kwargs) + errmsg = '(bad|expected) screen distance ((or "" )?but got )?"{}"' + self.checkInvalidParam(widget, name, '6x', errmsg=errmsg) + self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg) + + def checkReliefParam(self, widget, name, *, allow_empty=False): + values = ('flat', 'groove', 'raised', 'ridge', 'solid', 'sunken') + if allow_empty: + values += ('',) + self.checkParams(widget, name, *values) + errmsg = 'bad relief "{}": must be %s, or %s' % ( + ', '.join(values[:-1]), + values[-1] or '""') + if tk_version < (8, 6): + errmsg = None + self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg) + + def checkImageParam(self, widget, name): + image = tkinter.PhotoImage(master=self.root, name='image1') + self.checkParam(widget, name, image, conv=str) + if tk_version < (8, 7): + errmsg = 'image "spam" doesn\'t exist' + else: + errmsg = 'image "spam" does not exist' + self.checkInvalidParam(widget, name, 'spam', + errmsg=errmsg) + widget[name] = '' + + def checkVariableParam(self, widget, name, var): + self.checkParam(widget, name, var, conv=str) + + def assertIsBoundingBox(self, bbox): + self.assertIsNotNone(bbox) + self.assertIsInstance(bbox, tuple) + if len(bbox) != 4: + self.fail('Invalid bounding box: %r' % (bbox,)) + for item in bbox: + if not isinstance(item, int): + self.fail('Invalid bounding box: %r' % (bbox,)) + break + + + def test_keys(self): + widget = self.create() + keys = widget.keys() + self.assertEqual(sorted(keys), sorted(widget.configure())) + for k in keys: + widget[k] + # Test if OPTIONS contains all keys + if test.support.verbose: + keys = set(keys) + expected = set(self.OPTIONS) + for k in sorted(keys - expected): + if not (k in _OPTION_ALIASES and + _OPTION_ALIASES[k] in keys and + _OPTION_ALIASES[k] in expected): + print('%s.OPTIONS doesn\'t contain "%s"' % + (self.__class__.__name__, k)) + + def test_options_in_docstring(self): + # Every option in OPTIONS must be listed in the docstring of the + # __init__ method (see gh-78335). Options reported by keys() but + # missing from the docstring are only printed in verbose mode, as + # some of them depend on the Tk version. + widget = self.create() + doc = type(widget).__init__.__doc__ + if doc is None: + self.skipTest('docstrings are not available (run with -OO)') + # Look at the option list only, not the leading description. + start = doc.find('Valid option names') + if start < 0: + start = doc.find('OPTIONS') + if start < 0: + self.skipTest('the __init__ docstring does not list options') + documented = set(re.findall(r'[a-z][a-z0-9]+', doc[start:])) + def is_documented(option): + return (option in documented or + _OPTION_ALIASES.get(option) in documented) + missing = sorted(o for o in self.OPTIONS if not is_documented(o)) + self.assertEqual(missing, [], + '%s options missing from the __init__ docstring: %s' + % (type(widget).__name__, missing)) + if test.support.verbose: + for key in sorted(set(widget.keys())): + if not is_documented(key): + print('%s.__init__ docstring doesn\'t contain "%s"' + % (type(widget).__name__, key)) + +class PixelOptionsTests: + """Standard options that accept all formats acceptable to Tk_GetPixels. + + In addition to numbers, these options can be set with distances + specified as a string consisting of a number followed by a single + character giving the unit of distance. The allowed units are: + millimeters ('m'), centimeters ('c'), inches ('i') or points ('p'). + In Tk 9 a cget call for one of these options returns a Tcl_Obj of + type "pixels", whose string representation is the distance string + passed to configure. + """ + PIXEL_OPTIONS = ('activeborderwidth', 'borderwidth', 'highlightthickness', + 'insertborderwidth', 'insertwidth', 'padx', 'pady', 'selectborderwidth') + + def test_configure_activeborderwidth(self): + widget = self.create() + self.checkPixelsParam(widget, 'activeborderwidth', + 0, 1.3, 2.9, 6, -2, '10p') + + def test_configure_borderwidth(self): + widget = self.create() + self.checkPixelsParam(widget, 'borderwidth', + 0, 1.3, 2.6, 6, -2, '10p') + + if 'bd' in self.OPTIONS: + self.checkPixelsParam(widget, 'bd', 0, 1.3, 2.6, 6, '10p') + self.checkParam(widget, 'bd', -2, expected=expected) + + def test_configure_highlightthickness(self): + widget = self.create() + self.checkPixelsParam(widget, 'highlightthickness', + 0, 1.3, 2.6, 6, -2, '10p') + + def test_configure_insertborderwidth(self): + widget = self.create() + self.checkPixelsParam(widget, 'insertborderwidth', 0, 1.3, 2.6, 6, -2, '10p') + + def test_configure_insertwidth(self): + widget = self.create() + self.checkPixelsParam(widget, 'insertwidth', 1.3, 2.6, -2, '10p') + + def test_configure_padx(self): + widget = self.create() + self.checkPixelsParam(widget, 'padx', 3, 4.4, 5.6, -2, '12m') + + def test_configure_pady(self): + widget = self.create() + self.checkPixelsParam(widget, 'pady', 3, 4.4, 5.6, -2, '12m') + + def test_configure_selectborderwidth(self): + widget = self.create() + self.checkPixelsParam(widget, 'selectborderwidth', 1.3, 2.6, -2, '10p') + + +class StandardOptionsTests(PixelOptionsTests): + + STANDARD_OPTIONS = ( 'activebackground', 'activeforeground', + 'anchor', 'background', 'bitmap', 'compound', 'cursor', + 'disabledforeground', 'exportselection', 'font', 'foreground', + 'highlightbackground', 'highlightcolor', 'image', + 'insertbackground', 'insertofftime', 'insertontime', 'jump', + 'justify', 'orient', 'relief', 'repeatdelay', 'repeatinterval', + 'selectbackground', 'selectforeground', 'setgrid', 'takefocus', + 'text', 'textvariable', 'troughcolor', 'underline', 'wraplength', + 'xscrollcommand', 'yscrollcommand', ) + PixelOptionsTests.PIXEL_OPTIONS + + def test_configure_activebackground(self): + widget = self.create() + self.checkColorParam(widget, 'activebackground') + + def test_configure_activeforeground(self): + widget = self.create() + self.checkColorParam(widget, 'activeforeground') + + def test_configure_activerelief(self): + widget = self.create() + self.checkReliefParam(widget, 'activerelief') + + def test_configure_anchor(self): + widget = self.create() + self.checkEnumParam(widget, 'anchor', + 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center') + + def test_configure_background(self): + widget = self.create() + self.checkColorParam(widget, 'background') + if 'bg' in self.OPTIONS: + self.checkColorParam(widget, 'bg') + + @requires_tk(8, 7) + def test_configure_backgroundimage(self): + widget = self.create() + self.checkImageParam(widget, 'backgroundimage') + + def test_configure_bitmap(self): + widget = self.create() + self.checkParam(widget, 'bitmap', 'questhead') + self.checkParam(widget, 'bitmap', 'gray50') + filename = test.support.findfile('python.xbm', subdir='tkinterdata') + self.checkParam(widget, 'bitmap', '@' + filename) + # Cocoa Tk widgets don't detect invalid -bitmap values + # See https://core.tcl.tk/tk/info/31cd33dbf0 + if not ('aqua' in self.root.tk.call('tk', 'windowingsystem') and + 'AppKit' in self.root.winfo_server()): + self.checkInvalidParam(widget, 'bitmap', 'spam', + errmsg='bitmap "spam" not defined') + + def test_configure_compound(self): + widget = self.create() + self.checkEnumParam(widget, 'compound', + 'bottom', 'center', 'left', 'none', 'right', 'top') + + def test_configure_cursor(self): + widget = self.create() + self.checkCursorParam(widget, 'cursor') + + def test_configure_disabledforeground(self): + widget = self.create() + self.checkColorParam(widget, 'disabledforeground') + + def test_configure_exportselection(self): + widget = self.create() + self.checkBooleanParam(widget, 'exportselection') + + def test_configure_font(self): + widget = self.create() + self.checkParam(widget, 'font', + '-Adobe-Helvetica-Medium-R-Normal--*-120-*-*-*-*-*-*') + is_ttk = widget.__class__.__module__ == 'tkinter.ttk' + if not is_ttk: + errmsg = 'font "" does ?n[o\']t exist' + self.checkInvalidParam(widget, 'font', '', errmsg=errmsg) + + def test_configure_foreground(self): + widget = self.create() + self.checkColorParam(widget, 'foreground') + if 'fg' in self.OPTIONS: + self.checkColorParam(widget, 'fg') + + def test_configure_highlightbackground(self): + widget = self.create() + self.checkColorParam(widget, 'highlightbackground') + + def test_configure_highlightcolor(self): + widget = self.create() + self.checkColorParam(widget, 'highlightcolor') + + def test_configure_image(self): + widget = self.create() + self.checkImageParam(widget, 'image') + + def test_configure_insertbackground(self): + widget = self.create() + self.checkColorParam(widget, 'insertbackground') + + def test_configure_insertofftime(self): + widget = self.create() + self.checkIntegerParam(widget, 'insertofftime', 100) + + def test_configure_insertontime(self): + widget = self.create() + self.checkIntegerParam(widget, 'insertontime', 100) + + def test_configure_jump(self): + widget = self.create() + self.checkBooleanParam(widget, 'jump') + + def test_configure_justify(self): + widget = self.create() + values = ('left', 'right', 'center') + if self._allow_empty_justify: + values += ('',) + self.checkEnumParam(widget, 'justify', *values, + fullname='justification') + + def test_configure_orient(self): + widget = self.create() + self.assertEqual(str(widget['orient']), self.default_orient) + self.checkEnumParam(widget, 'orient', 'horizontal', 'vertical') + + @requires_tk(8, 7) + def test_configure_placeholder(self): + widget = self.create() + self.checkParam(widget, 'placeholder', 'xxx') + + @requires_tk(8, 7) + def test_configure_placeholderforeground(self): + widget = self.create() + self.checkColorParam(widget, 'placeholderforeground') + + def test_configure_relief(self): + widget = self.create() + self.checkReliefParam(widget, 'relief') + + def test_configure_repeatdelay(self): + widget = self.create() + self.checkIntegerParam(widget, 'repeatdelay', -500, 500) + + def test_configure_repeatinterval(self): + widget = self.create() + self.checkIntegerParam(widget, 'repeatinterval', -500, 500) + + def test_configure_selectbackground(self): + widget = self.create() + self.checkColorParam(widget, 'selectbackground') + + def test_configure_selectforeground(self): + widget = self.create() + self.checkColorParam(widget, 'selectforeground') + + def test_configure_setgrid(self): + widget = self.create() + self.checkBooleanParam(widget, 'setgrid') + + def test_configure_state(self): + widget = self.create() + self.checkEnumParam(widget, 'state', 'active', 'disabled', 'normal') + + def test_configure_takefocus(self): + widget = self.create() + self.checkParams(widget, 'takefocus', '0', '1', '') + + def test_configure_text(self): + widget = self.create() + self.checkParams(widget, 'text', '', 'any string') + + def test_configure_textvariable(self): + widget = self.create() + var = tkinter.StringVar(self.root) + self.checkVariableParam(widget, 'textvariable', var) + + @requires_tk(8, 7) + def test_configure_tile(self): + widget = self.create() + self.checkBooleanParam(widget, 'tile') + + def test_configure_troughcolor(self): + widget = self.create() + self.checkColorParam(widget, 'troughcolor') + + def test_configure_underline(self): + widget = self.create() + self.checkParams(widget, 'underline', 0, 1, 10) + if tk_version >= (8, 7): + is_ttk = widget.__class__.__module__ == 'tkinter.ttk' + self.checkParam(widget, 'underline', '', + expected='' if is_ttk else self._default_pixels) + self.checkParam(widget, 'underline', '5+2', + expected='5+2' if is_ttk else 7) + self.checkParam(widget, 'underline', '5-2', + expected='5-2' if is_ttk else 3) + self.checkParam(widget, 'underline', 'end', expected='end') + self.checkParam(widget, 'underline', 'end-2', expected='end-2') + errmsg = (r'bad index "{}": must be integer\?\[\+-\]integer\?, ' + r'end\?\[\+-\]integer\?, or ""') + else: + errmsg = 'expected integer but got "{}"' + self.checkInvalidParam(widget, 'underline', '', errmsg=errmsg) + self.checkInvalidParam(widget, 'underline', '10p', errmsg=errmsg) + self.checkInvalidParam(widget, 'underline', 3.2, errmsg=errmsg) + + def test_configure_wraplength(self): + widget = self.create() + self.checkPixelsParam(widget, 'wraplength', 100) + + def test_configure_xscrollcommand(self): + widget = self.create() + self.checkCommandParam(widget, 'xscrollcommand') + + def test_configure_yscrollcommand(self): + widget = self.create() + self.checkCommandParam(widget, 'yscrollcommand') + + # non-standard but common options + + def test_configure_command(self): + widget = self.create() + self.checkCommandParam(widget, 'command') + + def test_configure_indicatoron(self): + widget = self.create() + self.checkBooleanParam(widget, 'indicatoron') + + def test_configure_offrelief(self): + widget = self.create() + self.checkReliefParam(widget, 'offrelief') + + def test_configure_overrelief(self): + widget = self.create() + self.checkReliefParam(widget, 'overrelief', + allow_empty=(tk_version >= (8, 7))) + + def test_configure_selectcolor(self): + widget = self.create() + self.checkColorParam(widget, 'selectcolor') + + def test_configure_selectimage(self): + widget = self.create() + self.checkImageParam(widget, 'selectimage') + + def test_configure_tristateimage(self): + widget = self.create() + self.checkImageParam(widget, 'tristateimage') + + def test_configure_tristatevalue(self): + widget = self.create() + self.checkParam(widget, 'tristatevalue', 'unknowable') + + def test_configure_variable(self): + widget = self.create() + var = tkinter.DoubleVar(self.root) + self.checkVariableParam(widget, 'variable', var) + + +class IntegerSizeTests: + """ Tests widgets which only accept integral width and height.""" + def test_configure_height(self): + widget = self.create() + self.checkIntegerParam(widget, 'height', 100, -100, 0) + + def test_configure_width(self): + widget = self.create() + self.checkIntegerParam(widget, 'width', 402, -402, 0) + + +class PixelSizeTests: + """ Tests widgets which accept screen distances for width and height.""" + def test_configure_height(self): + widget = self.create() + self.checkPixelsParam(widget, 'height', 100, 101.2, 102.6, -100, 0, '3c') + + def test_configure_width(self): + widget = self.create() + self.checkPixelsParam(widget, 'width', 402, 403.4, 404.6, -402, 0, '5i') + + +def add_configure_tests(*source_classes): + # This decorator adds test_configure_xxx methods from source classes for + # every xxx option in the OPTIONS class attribute if they are not defined + # explicitly. + def decorator(cls): + for option in cls.OPTIONS: + methodname = 'test_configure_' + option + if not hasattr(cls, methodname): + for source_class in source_classes: + if hasattr(source_class, methodname): + setattr(cls, methodname, + getattr(source_class, methodname)) + break + else: + def test(self, option=option): + widget = self.create() + widget[option] + raise AssertionError('Option "%s" is not tested in %s' % + (option, cls.__name__)) + test.__name__ = methodname + setattr(cls, methodname, test) + return cls + return decorator + +def setUpModule(): + if test.support.verbose: + tcl = tkinter.Tcl() + print('patchlevel =', tcl.call('info', 'patchlevel'), flush=True) diff --git a/Lib/test/test_tomllib/burntsushi.py b/Lib/test/test_tomllib/burntsushi.py index 71228c65369..0ec50eb1a98 100644 --- a/Lib/test/test_tomllib/burntsushi.py +++ b/Lib/test/test_tomllib/burntsushi.py @@ -7,19 +7,8 @@ import datetime from typing import Any -# Aliases for converting TOML compliance format [1] to BurntSushi format [2] -# [1] https://github.com/toml-lang/compliance/blob/db7c3211fda30ff9ddb10292f4aeda7e2e10abc4/docs/json-encoding.md # noqa: E501 -# [2] https://github.com/BurntSushi/toml-test/blob/4634fdf3a6ecd6aaea5f4cdcd98b2733c2694993/README.md # noqa: E501 -_aliases = { - "boolean": "bool", - "offset datetime": "datetime", - "local datetime": "datetime-local", - "local date": "date-local", - "local time": "time-local", -} - - -def convert(obj): # noqa: C901 + +def convert(obj): if isinstance(obj, str): return {"type": "string", "value": obj} elif isinstance(obj, bool): @@ -53,31 +42,25 @@ def convert(obj): # noqa: C901 def normalize(obj: Any) -> Any: """Normalize test objects. - This normalizes primitive values (e.g. floats), and also converts from - TOML compliance format [1] to BurntSushi format [2]. - - [1] https://github.com/toml-lang/compliance/blob/db7c3211fda30ff9ddb10292f4aeda7e2e10abc4/docs/json-encoding.md # noqa: E501 - [2] https://github.com/BurntSushi/toml-test/blob/4634fdf3a6ecd6aaea5f4cdcd98b2733c2694993/README.md # noqa: E501 - """ + This normalizes primitive values (e.g. floats).""" if isinstance(obj, list): return [normalize(item) for item in obj] if isinstance(obj, dict): if "type" in obj and "value" in obj: type_ = obj["type"] - norm_type = _aliases.get(type_, type_) value = obj["value"] - if norm_type == "float": + if type_ == "float": norm_value = _normalize_float_str(value) - elif norm_type in {"datetime", "datetime-local"}: + elif type_ in {"datetime", "datetime-local"}: norm_value = _normalize_datetime_str(value) - elif norm_type == "time-local": + elif type_ == "time-local": norm_value = _normalize_localtime_str(value) else: norm_value = value - if norm_type == "array": + if type_ == "array": return [normalize(item) for item in value] - return {"type": norm_type, "value": norm_value} + return {"type": type_, "value": norm_value} return {k: normalize(v) for k, v in obj.items()} raise AssertionError("Burntsushi fixtures should be dicts/lists only") diff --git a/Lib/test/test_tomllib/data/valid/dates-and-times/datetimes.json b/Lib/test/test_tomllib/data/valid/dates-and-times/datetimes.json index 99aca873480..09a7c083d14 100644 --- a/Lib/test/test_tomllib/data/valid/dates-and-times/datetimes.json +++ b/Lib/test/test_tomllib/data/valid/dates-and-times/datetimes.json @@ -1,4 +1,5 @@ { "local-dt": {"type":"datetime-local","value":"1988-10-27t01:01:01"}, + "local-dt-no-seconds": {"type":"datetime-local","value":"2025-04-18t20:05:00"}, "zulu-dt": {"type":"datetime","value":"1988-10-27t01:01:01z"} } diff --git a/Lib/test/test_tomllib/data/valid/dates-and-times/datetimes.toml b/Lib/test/test_tomllib/data/valid/dates-and-times/datetimes.toml index cf84159de46..5dc4b318256 100644 --- a/Lib/test/test_tomllib/data/valid/dates-and-times/datetimes.toml +++ b/Lib/test/test_tomllib/data/valid/dates-and-times/datetimes.toml @@ -1,2 +1,3 @@ local-dt=1988-10-27t01:01:01 +local-dt-no-seconds=2025-04-18T20:05 zulu-dt=1988-10-27t01:01:01z diff --git a/Lib/test/test_tomllib/data/valid/dates-and-times/localtime.json b/Lib/test/test_tomllib/data/valid/dates-and-times/localtime.json index 4d96abcbc79..1f66348b237 100644 --- a/Lib/test/test_tomllib/data/valid/dates-and-times/localtime.json +++ b/Lib/test/test_tomllib/data/valid/dates-and-times/localtime.json @@ -1,2 +1,4 @@ {"t": - {"type":"time-local","value":"00:00:00.999999"}} + {"type":"time-local","value":"00:00:00.999999"}, +"t2": + {"type":"time-local","value":"00:00:00"}} diff --git a/Lib/test/test_tomllib/data/valid/dates-and-times/localtime.toml b/Lib/test/test_tomllib/data/valid/dates-and-times/localtime.toml index 87547c1cf3b..6579b30c94f 100644 --- a/Lib/test/test_tomllib/data/valid/dates-and-times/localtime.toml +++ b/Lib/test/test_tomllib/data/valid/dates-and-times/localtime.toml @@ -1 +1,2 @@ -t=00:00:00.99999999999999 \ No newline at end of file +t=00:00:00.99999999999999 +t2=00:00 \ No newline at end of file diff --git a/Lib/test/test_tomllib/data/valid/inline-table/empty-inline-table.json b/Lib/test/test_tomllib/data/valid/inline-table/empty-inline-table.json new file mode 100644 index 00000000000..2655cfd3ab0 --- /dev/null +++ b/Lib/test/test_tomllib/data/valid/inline-table/empty-inline-table.json @@ -0,0 +1 @@ +{"empty": {}} \ No newline at end of file diff --git a/Lib/test/test_tomllib/data/valid/inline-table/empty-inline-table.toml b/Lib/test/test_tomllib/data/valid/inline-table/empty-inline-table.toml new file mode 100644 index 00000000000..d2d15ab9e08 --- /dev/null +++ b/Lib/test/test_tomllib/data/valid/inline-table/empty-inline-table.toml @@ -0,0 +1 @@ +empty ={ }#nothing here diff --git a/Lib/test/test_tomllib/data/valid/inline-table/multiline-inline-table.json b/Lib/test/test_tomllib/data/valid/inline-table/multiline-inline-table.json new file mode 100644 index 00000000000..d253884fbac --- /dev/null +++ b/Lib/test/test_tomllib/data/valid/inline-table/multiline-inline-table.json @@ -0,0 +1,35 @@ +{ + "multiline": { + "a": { + "type": "integer", + "value": "1" + }, + "b": { + "type": "integer", + "value": "2" + }, + "c": [ + { + "type": "integer", + "value": "1" + }, + { + "type": "integer", + "value": "2" + }, + { + "type": "integer", + "value": "3" + } + ], + "d": { + "type": "integer", + "value": "3" + }, + "e": { + "type": "integer", + "value": "4" + }, + "f": {} + } +} \ No newline at end of file diff --git a/Lib/test/test_tomllib/data/valid/inline-table/multiline-inline-table.toml b/Lib/test/test_tomllib/data/valid/inline-table/multiline-inline-table.toml new file mode 100644 index 00000000000..6a98a08a576 --- /dev/null +++ b/Lib/test/test_tomllib/data/valid/inline-table/multiline-inline-table.toml @@ -0,0 +1,12 @@ +multiline = { + "a" = 1, "b" = 2, + c = [ + 1, + 2, + 3, + ],# comment + d = 3, + e = 4, f = { + # comment + }, +} diff --git a/Lib/test/test_tomllib/data/valid/multiline-basic-str/replacements.json b/Lib/test/test_tomllib/data/valid/multiline-basic-str/replacements.json new file mode 100644 index 00000000000..699f556248d --- /dev/null +++ b/Lib/test/test_tomllib/data/valid/multiline-basic-str/replacements.json @@ -0,0 +1,6 @@ +{ + "escape": {"type":"string","value":"\u001B"}, + "tab": {"type":"string","value":"\t"}, + "upper-j": {"type":"string","value":"J"}, + "upper-j-2": {"type":"string","value":"J"} +} diff --git a/Lib/test/test_tomllib/data/valid/multiline-basic-str/replacements.toml b/Lib/test/test_tomllib/data/valid/multiline-basic-str/replacements.toml new file mode 100644 index 00000000000..fa5647e5938 --- /dev/null +++ b/Lib/test/test_tomllib/data/valid/multiline-basic-str/replacements.toml @@ -0,0 +1,4 @@ +escape = "\e" +tab = "\x09" +upper-j = "\x4a" +upper-j-2 = "\x4A" diff --git a/Lib/test/test_tomllib/test_data.py b/Lib/test/test_tomllib/test_data.py index 3483d93022b..9db1a37466e 100644 --- a/Lib/test/test_tomllib/test_data.py +++ b/Lib/test/test_tomllib/test_data.py @@ -8,12 +8,6 @@ from . import burntsushi, tomllib - -class MissingFile: - def __init__(self, path: Path): - self.path = path - - DATA_DIR = Path(__file__).parent / "data" VALID_FILES = tuple((DATA_DIR / "valid").glob("**/*.toml")) @@ -22,10 +16,7 @@ def __init__(self, path: Path): _expected_files = [] for p in VALID_FILES: json_path = p.with_suffix(".json") - try: - text = json.loads(json_path.read_bytes().decode()) - except FileNotFoundError: - text = MissingFile(json_path) + text = json.loads(json_path.read_bytes().decode()) _expected_files.append(text) VALID_FILES_EXPECTED = tuple(_expected_files) @@ -49,14 +40,6 @@ def test_invalid(self): def test_valid(self): for valid, expected in zip(VALID_FILES, VALID_FILES_EXPECTED): with self.subTest(msg=valid.stem): - if isinstance(expected, MissingFile): - # For a poor man's xfail, assert that this is one of the - # test cases where expected data is known to be missing. - assert valid.stem in { - "qa-array-inline-nested-1000", - "qa-table-inline-nested-1000", - } - continue toml_str = valid.read_bytes().decode() actual = tomllib.loads(toml_str) actual = burntsushi.convert(actual) diff --git a/Lib/test/test_tomllib/test_misc.py b/Lib/test/test_tomllib/test_misc.py index 85526ed8015..af7ab91bba7 100644 --- a/Lib/test/test_tomllib/test_misc.py +++ b/Lib/test/test_tomllib/test_misc.py @@ -9,8 +9,10 @@ from pathlib import Path import sys import tempfile +import textwrap import unittest from test import support +from test.support.script_helper import assert_python_ok from . import tomllib @@ -137,3 +139,20 @@ def test_types_import(self): never imported by tests. """ importlib.import_module(f"{tomllib.__name__}._types") + + def test_lazy_import(self): + # Test the TOML file can be parsed without importing regular + # expressions (tomllib._re) + code = textwrap.dedent(""" + import sys, tomllib, textwrap + document = textwrap.dedent(''' + [metadata] + key = "text" + array = ["array", "of", "text"] + booleans = [true, false] + ''') + tomllib.loads(document) + print("lazy import?", 'tomllib._re' not in sys.modules) + """) + proc = assert_python_ok("-c", code) + self.assertIn(b"lazy import? True", proc.out) diff --git a/Lib/test/test_ttk/__init__.py b/Lib/test/test_ttk/__init__.py new file mode 100644 index 00000000000..7a47f44ca38 --- /dev/null +++ b/Lib/test/test_ttk/__init__.py @@ -0,0 +1,53 @@ +import os.path +import unittest +from test import support +from test.support import import_helper + + +if support.check_sanitizer(address=True, memory=True): + raise unittest.SkipTest("Tests involving libX11 can SEGFAULT on ASAN/MSAN builds") + +# Skip this test if _tkinter wasn't built. +import_helper.import_module('_tkinter') + +# Skip test if tk cannot be initialized. +support.requires('gui') + + +import tkinter +from _tkinter import TclError +from tkinter import ttk + + +class TestModule(unittest.TestCase): + def test_deprecated__version__(self): + with self.assertWarnsRegex( + DeprecationWarning, + "'__version__' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(ttk, "__version__") + self.assertEqual(cm.filename, __file__) + + +def setUpModule(): + wantobjects = support.get_resource_value('wantobjects') + if wantobjects is not None: + unittest.enterModuleContext( + support.swap_attr(tkinter, 'wantobjects', int(wantobjects))) + root = None + try: + root = tkinter.Tk() + button = ttk.Button(root) + button.destroy() + del button + except TclError as msg: + # assuming ttk is not available + raise unittest.SkipTest("ttk not available: %s" % msg) + finally: + if root is not None: + root.destroy() + del root + + +def load_tests(*args): + return support.load_package_tests(os.path.dirname(__file__), *args) diff --git a/Lib/test/test_ttk/__main__.py b/Lib/test/test_ttk/__main__.py new file mode 100644 index 00000000000..40a23a297ec --- /dev/null +++ b/Lib/test/test_ttk/__main__.py @@ -0,0 +1,4 @@ +from . import load_tests +import unittest + +unittest.main() diff --git a/Lib/test/test_ttk/test_extensions.py b/Lib/test/test_ttk/test_extensions.py new file mode 100644 index 00000000000..f6ca5e97610 --- /dev/null +++ b/Lib/test/test_ttk/test_extensions.py @@ -0,0 +1,404 @@ +import sys +import unittest +import tkinter +from tkinter import ttk +from test.support import requires, gc_collect +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest, + get_tk_patchlevel, widget_eq) + +requires('gui') + +from test.test_ttk import test_widgets + + +class LabeledScaleTest(AbstractTkTest, unittest.TestCase): + + def tearDown(self): + self.root.update_idletasks() + super().tearDown() + + def test_widget_destroy(self): + # automatically created variable + x = ttk.LabeledScale(self.root) + var = x._variable._name + x.destroy() + gc_collect() # For PyPy or other GCs. + self.assertRaises(tkinter.TclError, x.tk.globalgetvar, var) + + # manually created variable + myvar = tkinter.DoubleVar(self.root) + name = myvar._name + x = ttk.LabeledScale(self.root, variable=myvar) + x.destroy() + if self.wantobjects: + self.assertEqual(x.tk.globalgetvar(name), myvar.get()) + else: + self.assertEqual(float(x.tk.globalgetvar(name)), myvar.get()) + del myvar + gc_collect() # For PyPy or other GCs. + self.assertRaises(tkinter.TclError, x.tk.globalgetvar, name) + + # checking that the tracing callback is properly removed + myvar = tkinter.IntVar(self.root) + # LabeledScale will start tracing myvar + x = ttk.LabeledScale(self.root, variable=myvar) + x.destroy() + # Unless the tracing callback was removed, creating a new + # LabeledScale with the same var will cause an error now. This + # happens because the variable will be set to (possibly) a new + # value which causes the tracing callback to be called and then + # it tries calling instance attributes not yet defined. + ttk.LabeledScale(self.root, variable=myvar) + if hasattr(sys, 'last_exc'): + self.assertNotEqual(type(sys.last_exc), tkinter.TclError) + elif hasattr(sys, 'last_type'): + self.assertNotEqual(sys.last_type, tkinter.TclError) + + def test_initialization(self): + # master passing + master = tkinter.Frame(self.root) + x = ttk.LabeledScale(master) + self.assertEqual(x.master, master) + x.destroy() + + # variable initialization/passing + passed_expected = (('0', 0), (0, 0), (10, 10), + (-1, -1), (sys.maxsize + 1, sys.maxsize + 1), + (2.5, 2), ('2.5', 2)) + for pair in passed_expected: + x = ttk.LabeledScale(self.root, from_=pair[0]) + self.assertEqual(x.value, pair[1]) + x.destroy() + x = ttk.LabeledScale(self.root, from_=None) + self.assertRaises((ValueError, tkinter.TclError), x._variable.get) + x.destroy() + # variable should have its default value set to the from_ value + myvar = tkinter.DoubleVar(self.root, value=20) + x = ttk.LabeledScale(self.root, variable=myvar) + self.assertEqual(x.value, 0) + x.destroy() + # check that it is really using a DoubleVar + x = ttk.LabeledScale(self.root, variable=myvar, from_=0.5) + self.assertEqual(x.value, 0.5) + self.assertEqual(x._variable._name, myvar._name) + x.destroy() + + # widget positionment + def check_positions(scale, scale_pos, label, label_pos): + self.assertEqual(scale.pack_info()['side'], scale_pos) + self.assertEqual(label.place_info()['anchor'], label_pos) + x = ttk.LabeledScale(self.root, compound='top') + check_positions(x.scale, 'bottom', x.label, 'n') + x.destroy() + x = ttk.LabeledScale(self.root, compound='bottom') + check_positions(x.scale, 'top', x.label, 's') + x.destroy() + # invert default positions + x = ttk.LabeledScale(self.root, compound='unknown') + check_positions(x.scale, 'top', x.label, 's') + x.destroy() + x = ttk.LabeledScale(self.root) # take default positions + check_positions(x.scale, 'bottom', x.label, 'n') + x.destroy() + + # extra, and invalid, kwargs + self.assertRaises(tkinter.TclError, ttk.LabeledScale, master, a='b') + + + def test_horizontal_range(self): + lscale = ttk.LabeledScale(self.root, from_=0, to=10) + lscale.pack() + self.require_mapped(lscale) + + linfo_1 = lscale.label.place_info() + prev_xcoord = lscale.scale.coords()[0] + self.assertEqual(prev_xcoord, int(linfo_1['x'])) + # change range to: from -5 to 5. This should change the x coord of + # the scale widget, since 0 is at the middle of the new + # range. + lscale.scale.configure(from_=-5, to=5) + # The following update is needed since the test doesn't use mainloop, + # at the same time this shouldn't affect test outcome + lscale.update() + curr_xcoord = lscale.scale.coords()[0] + self.assertNotEqual(prev_xcoord, curr_xcoord) + # the label widget should have been repositioned too + linfo_2 = lscale.label.place_info() + self.assertEqual(lscale.label['text'], 0 if self.wantobjects else '0') + self.assertEqual(curr_xcoord, int(linfo_2['x'])) + # change the range back + lscale.scale.configure(from_=0, to=10) + self.assertNotEqual(prev_xcoord, curr_xcoord) + self.assertEqual(prev_xcoord, int(linfo_1['x'])) + + lscale.destroy() + + + def test_variable_change(self): + x = ttk.LabeledScale(self.root) + x.pack() + self.require_mapped(x) + + curr_xcoord = x.scale.coords()[0] + newval = x.value + 1 + x.value = newval + # The following update is needed since the test doesn't use mainloop, + # at the same time this shouldn't affect test outcome + x.update() + self.assertEqual(x.value, newval) + self.assertEqual(x.label['text'], + newval if self.wantobjects else str(newval)) + self.assertEqual(float(x.scale.get()), newval) + self.assertGreater(x.scale.coords()[0], curr_xcoord) + self.assertEqual(x.scale.coords()[0], + int(x.label.place_info()['x'])) + + # value outside range + if self.wantobjects: + conv = lambda x: x + else: + conv = int + x.value = conv(x.scale['to']) + 1 # no changes shouldn't happen + x.update() + self.assertEqual(x.value, newval) + self.assertEqual(conv(x.label['text']), newval) + self.assertEqual(float(x.scale.get()), newval) + self.assertEqual(x.scale.coords()[0], + int(x.label.place_info()['x'])) + + # non-integer value + x.value = newval = newval + 1.5 + x.update() + self.assertEqual(x.value, int(newval)) + self.assertEqual(conv(x.label['text']), int(newval)) + self.assertEqual(float(x.scale.get()), newval) + + x.destroy() + + + def test_resize(self): + x = ttk.LabeledScale(self.root) + x.pack(expand=True, fill='both') + gc_collect() # For PyPy or other GCs. + self.require_mapped(x) + + width, height = x.master.winfo_width(), x.master.winfo_height() + width_new, height_new = width * 2, height * 2 + + x.value = 3 + x.update() + x.master.wm_geometry("%dx%d" % (width_new, height_new)) + self.assertEqual(int(x.label.place_info()['x']), + x.scale.coords()[0]) + + # Reset geometry + x.master.wm_geometry("%dx%d" % (width, height)) + x.destroy() + + +class OptionMenuTest(test_widgets.MenubuttonTest, unittest.TestCase): + + # OptionMenu documents only its own options, not the inherited + # Menubutton options (like the classic tkinter.OptionMenu). + test_options_in_docstring = None + + def setUp(self): + super().setUp() + self.textvar = tkinter.StringVar(self.root) + + def tearDown(self): + del self.textvar + super().tearDown() + + def create(self, default='b', values=('a', 'b', 'c'), **kwargs): + return ttk.OptionMenu(self.root, self.textvar, default, *values, **kwargs) + + def test_bad_kwarg(self): + with self.assertRaisesRegex(tkinter.TclError, r"^unknown option -image$"): + ttk.OptionMenu(self.root, self.textvar, 'b', image='') + + def test_configure_class(self): + # Unlike a plain Menubutton, OptionMenu does not accept a class at + # construction, so only the read-only nature of the option is tested. + widget = self.create() + self.assertEqual(widget['class'], '') + errmsg = 'attempt to change read-only option' + if get_tk_patchlevel(self.root) < (8, 6, 0, 'beta', 3): + errmsg = 'Attempt to change read-only option' + self.checkInvalidParam(widget, 'class', 'Foo', errmsg=errmsg) + + def test_configure_style(self): + # Like Menubutton, but OptionMenu does not accept a class at + # construction, so the custom-class part of the standard test is omitted. + widget = self.create() + self.assertEqual(widget['style'], '') + self.checkInvalidParam(widget, 'style', 'Foo', + errmsg='Layout Foo not found') + style = ttk.Style(self.root) + style.configure('Custom.TMenubutton') + self.checkParam(widget, 'style', 'Custom.TMenubutton') + + def test_configure_menu(self): + # OptionMenu manages its own menu; ['menu'] returns that Menu widget. + widget = self.create() + self.assertIsInstance(widget['menu'], tkinter.Menu) + self.checkParam(widget, 'menu', widget['menu'], eq=widget_eq) + + def test_configure_text(self): + # The displayed text is governed by the textvariable. + widget = self.create() + self.textvar.set('a') + self.assertEqual(widget['text'], 'a') + self.textvar.set('c') + self.assertEqual(widget['text'], 'c') + + def test_widget_destroy(self): + var = tkinter.StringVar(self.root) + optmenu = ttk.OptionMenu(self.root, var) + name = var._name + optmenu.update_idletasks() + optmenu.destroy() + self.assertEqual(optmenu.tk.globalgetvar(name), var.get()) + del var + gc_collect() # For PyPy or other GCs. + self.assertRaises(tkinter.TclError, optmenu.tk.globalgetvar, name) + + + def test_initialization(self): + self.assertRaises(tkinter.TclError, + ttk.OptionMenu, self.root, self.textvar, invalid='thing') + + optmenu = ttk.OptionMenu(self.root, self.textvar, 'b', 'a', 'b') + self.assertEqual(optmenu._variable.get(), 'b') + + self.assertTrue(optmenu['menu']) + self.assertTrue(optmenu['textvariable']) + + optmenu.destroy() + + + def test_menu(self): + items = ('a', 'b', 'c') + default = 'a' + optmenu = ttk.OptionMenu(self.root, self.textvar, default, *items) + found_default = False + for i in range(len(items)): + value = optmenu['menu'].entrycget(i, 'value') + self.assertEqual(value, items[i]) + if value == default: + found_default = True + self.assertTrue(found_default) + optmenu.destroy() + + # default shouldn't be in menu if it is not part of values + default = 'd' + optmenu = ttk.OptionMenu(self.root, self.textvar, default, *items) + curr = None + i = 0 + while True: + last, curr = curr, optmenu['menu'].entryconfigure(i, 'value') + if last == curr: + # no more menu entries + break + self.assertNotEqual(curr, default) + i += 1 + self.assertEqual(i, len(items)) + + # check that variable is updated correctly + optmenu.pack() + gc_collect() # For PyPy or other GCs. + optmenu['menu'].invoke(0) + self.assertEqual(optmenu._variable.get(), items[0]) + + # changing to an invalid index shouldn't change the variable + self.assertRaises(tkinter.TclError, optmenu['menu'].invoke, -1) + self.assertEqual(optmenu._variable.get(), items[0]) + + optmenu.destroy() + + # specifying a callback + success = [] + def cb_test(item): + self.assertEqual(item, items[1]) + success.append(True) + optmenu = ttk.OptionMenu(self.root, self.textvar, 'a', command=cb_test, + *items) + optmenu['menu'].invoke(1) + if not success: + self.fail("Menu callback not invoked") + + optmenu.destroy() + + def test_set_menu(self): + optmenu = ttk.OptionMenu(self.root, self.textvar, 'a', 'a', 'b', 'c') + menu = optmenu['menu'] + self.assertEqual(menu.index('end'), 2) + + # set_menu rebuilds the menu with new values and an optional default. + optmenu.set_menu('y', 'x', 'y', 'z') + self.assertEqual(self.textvar.get(), 'y') + self.assertEqual([menu.entrycget(i, 'label') for i in range(3)], + ['x', 'y', 'z']) + + # Without a default the variable is left unchanged. + optmenu.set_menu(None, 'p', 'q') + self.assertEqual(self.textvar.get(), 'y') + self.assertEqual([menu.entrycget(i, 'label') for i in range(2)], + ['p', 'q']) + optmenu.destroy() + + def test_unique_radiobuttons(self): + # check that radiobuttons are unique across instances (bpo25684) + items = ('a', 'b', 'c') + default = 'a' + optmenu = ttk.OptionMenu(self.root, self.textvar, default, *items) + textvar2 = tkinter.StringVar(self.root) + optmenu2 = ttk.OptionMenu(self.root, textvar2, default, *items) + optmenu.pack() + optmenu2.pack() + optmenu['menu'].invoke(1) + optmenu2['menu'].invoke(2) + optmenu_stringvar_name = optmenu['menu'].entrycget(0, 'variable') + optmenu2_stringvar_name = optmenu2['menu'].entrycget(0, 'variable') + self.assertNotEqual(optmenu_stringvar_name, + optmenu2_stringvar_name) + self.assertEqual(self.root.tk.globalgetvar(optmenu_stringvar_name), + items[1]) + self.assertEqual(self.root.tk.globalgetvar(optmenu2_stringvar_name), + items[2]) + + optmenu.destroy() + optmenu2.destroy() + + def test_trace_variable(self): + # prior to bpo45160, tracing a variable would cause the callback to be made twice + success = [] + items = ('a', 'b', 'c') + textvar = tkinter.StringVar(self.root) + def cb_test(*args): + success.append(textvar.get()) + optmenu = ttk.OptionMenu(self.root, textvar, "a", *items) + optmenu.pack() + cb_name = textvar.trace_add("write", cb_test) + optmenu['menu'].invoke(1) + self.assertEqual(success, ['b']) + self.assertEqual(textvar.get(), 'b') + textvar.trace_remove("write", cb_name) + optmenu.destroy() + + def test_specify_name(self): + textvar = tkinter.StringVar(self.root) + widget = ttk.OptionMenu(self.root, textvar, ":)", name="option_menu_ex") + self.assertEqual(str(widget), ".option_menu_ex") + self.assertIs(self.root.children["option_menu_ex"], widget) + + +class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): + + def test_labeledscale(self): + self._test_widget(ttk.LabeledScale) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_ttk/test_style.py b/Lib/test/test_ttk/test_style.py new file mode 100644 index 00000000000..56d8a5931fd --- /dev/null +++ b/Lib/test/test_ttk/test_style.py @@ -0,0 +1,464 @@ +import unittest +import sys +import tkinter +from tkinter import ttk +from tkinter import TclError +from test import support +from test.support import requires +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import AbstractTkTest, get_tk_patchlevel + +requires('gui') + +CLASS_NAMES = [ + '.', 'ComboboxPopdownFrame', 'Heading', + 'Horizontal.TProgressbar', 'Horizontal.TScale', 'Item', 'Sash', + 'TButton', 'TCheckbutton', 'TCombobox', 'TEntry', + 'TLabelframe', 'TLabelframe.Label', 'TMenubutton', + 'TNotebook', 'TNotebook.Tab', 'Toolbutton', 'TProgressbar', + 'TRadiobutton', 'Treeview', 'TScale', 'TScrollbar', 'TSpinbox', + 'Vertical.TProgressbar', 'Vertical.TScale' +] + +class StyleTest(AbstractTkTest, unittest.TestCase): + + def setUp(self): + super().setUp() + self.style = ttk.Style(self.root) + + + def test_configure(self): + style = self.style + style.configure('TButton', background='yellow') + self.assertEqual(style.configure('TButton', 'background'), + 'yellow') + self.assertIsInstance(style.configure('TButton'), dict) + + + def test_map(self): + style = self.style + + # Single state + for states in ['active'], [('active',)]: + with self.subTest(states=states): + style.map('TButton', background=[(*states, 'white')]) + expected = [('active', 'white')] + self.assertEqual(style.map('TButton', 'background'), expected) + m = style.map('TButton') + self.assertIsInstance(m, dict) + self.assertEqual(m['background'], expected) + + # Multiple states + for states in ['pressed', '!disabled'], ['pressed !disabled'], [('pressed', '!disabled')]: + with self.subTest(states=states): + style.map('TButton', background=[(*states, 'black')]) + expected = [('pressed', '!disabled', 'black')] + self.assertEqual(style.map('TButton', 'background'), expected) + m = style.map('TButton') + self.assertIsInstance(m, dict) + self.assertEqual(m['background'], expected) + + # Default state + for states in [], [''], [()]: + with self.subTest(states=states): + style.map('TButton', background=[(*states, 'grey')]) + expected = [('grey',)] + self.assertEqual(style.map('TButton', 'background'), expected) + m = style.map('TButton') + self.assertIsInstance(m, dict) + self.assertEqual(m['background'], expected) + + + def test_lookup(self): + style = self.style + style.configure('TButton', background='yellow') + style.map('TButton', background=[('active', 'background', 'blue')]) + + self.assertEqual(style.lookup('TButton', 'background'), 'yellow') + self.assertEqual(style.lookup('TButton', 'background', + ['active', 'background']), 'blue') + self.assertEqual(style.lookup('TButton', 'optionnotdefined', + default='iknewit'), 'iknewit') + + + def test_layout(self): + style = self.style + self.assertRaises(tkinter.TclError, style.layout, 'NotALayout') + tv_style = style.layout('Treeview') + + # "erase" Treeview layout + style.layout('Treeview', '') + self.assertEqual(style.layout('Treeview'), + [('null', {'sticky': 'nswe'})] + ) + + # restore layout + style.layout('Treeview', tv_style) + self.assertEqual(style.layout('Treeview'), tv_style) + + # should return a list + self.assertIsInstance(style.layout('TButton'), list) + + # correct layout, but "option" doesn't exist as option + self.assertRaises(tkinter.TclError, style.layout, 'Treeview', + [('name', {'option': 'inexistent'})]) + + + def test_theme_use(self): + self.assertRaises(tkinter.TclError, self.style.theme_use, + 'nonexistingname') + + curr_theme = self.style.theme_use() + new_theme = None + for theme in self.style.theme_names(): + if theme != curr_theme: + new_theme = theme + self.style.theme_use(theme) + break + else: + # just one theme available, can't go on with tests + return + + self.assertFalse(curr_theme == new_theme) + self.assertFalse(new_theme != self.style.theme_use()) + + self.style.theme_use(curr_theme) + + def test_theme_settings(self): + style = self.style + theme = style.theme_use() + style.theme_settings(theme, { + 'Test.TLabel': { + 'configure': {'foreground': 'red', 'background': 'blue'}, + 'map': {'foreground': [('active', 'green')]}, + }, + }) + self.assertEqual(style.lookup('Test.TLabel', 'foreground'), 'red') + self.assertEqual(style.lookup('Test.TLabel', 'background'), 'blue') + self.assertEqual(style.map('Test.TLabel', 'foreground'), + [('active', 'green')]) + self.assertRaises(tkinter.TclError, style.theme_settings, + 'nonexistingname', {}) + + def test_configure_custom_copy(self): + style = self.style + + curr_theme = self.style.theme_use() + self.addCleanup(self.style.theme_use, curr_theme) + for theme in self.style.theme_names(): + self.style.theme_use(theme) + for name in CLASS_NAMES: + default = style.configure(name) + if not default: + continue + with self.subTest(theme=theme, name=name): + if support.verbose >= 2: + print('configure', theme, name, default) + if (theme in ('vista', 'xpnative') + and sys.getwindowsversion()[:2] == (6, 1)): + # Fails on the Windows 7 buildbot + continue + newname = f'C.{name}' + self.assertEqual(style.configure(newname), None) + style.configure(newname, **default) + self.assertEqual(style.configure(newname), default) + for key, value in default.items(): + self.assertEqual(style.configure(newname, key), value) + + + def test_map_custom_copy(self): + style = self.style + + curr_theme = self.style.theme_use() + self.addCleanup(self.style.theme_use, curr_theme) + for theme in self.style.theme_names(): + self.style.theme_use(theme) + for name in CLASS_NAMES: + default = style.map(name) + if not default: + continue + with self.subTest(theme=theme, name=name): + if support.verbose >= 2: + print('map', theme, name, default) + if (theme in ('vista', 'xpnative') + and sys.getwindowsversion()[:2] == (6, 1)): + # Fails on the Windows 7 buildbot + continue + newname = f'C.{name}' + self.assertEqual(style.map(newname), {}) + style.map(newname, **default) + if theme == 'alt' and name == '.' and get_tk_patchlevel(self.root) < (8, 6, 1): + default['embossed'] = [('disabled', '1')] + self.assertEqual(style.map(newname), default) + for key, value in default.items(): + self.assertEqual(style.map(newname, key), value) + + def test_element_options(self): + style = self.style + element_names = style.element_names() + self.assertNotIsInstance(element_names, str) + for name in element_names: + self.assertIsInstance(name, str) + element_options = style.element_options(name) + self.assertNotIsInstance(element_options, str) + for optname in element_options: + self.assertIsInstance(optname, str) + + def test_element_create_errors(self): + style = self.style + with self.assertRaises(TypeError): + style.element_create('plain.newelem') + with self.assertRaisesRegex(TclError, 'No such element type spam'): + style.element_create('plain.newelem', 'spam') + + def test_element_create_from(self): + style = self.style + style.element_create('plain.background', 'from', 'default') + self.assertIn('plain.background', style.element_names()) + style.element_create('plain.arrow', 'from', 'default', 'rightarrow') + self.assertIn('plain.arrow', style.element_names()) + + def test_element_create_from_errors(self): + style = self.style + with self.assertRaises(IndexError): + style.element_create('plain.newelem', 'from') + with self.assertRaisesRegex(TclError, + 'theme "spam" (does not|doesn\'t) exist'): + style.element_create('plain.newelem', 'from', 'spam') + + def test_element_create_image(self): + style = self.style + image = tkinter.PhotoImage(master=self.root, width=12, height=10) + style.element_create('block', 'image', image) + self.assertIn('block', style.element_names()) + + style.layout('TestLabel1', [('block', {'sticky': 'news'})]) + a = ttk.Label(self.root, style='TestLabel1') + a.pack(expand=True, fill='both') + self.assertEqual(a.winfo_reqwidth(), 12) + self.assertEqual(a.winfo_reqheight(), 10) + + imgfile = support.findfile('python.xbm', subdir='tkinterdata') + img1 = tkinter.BitmapImage(master=self.root, file=imgfile, + foreground='yellow', background='blue') + img2 = tkinter.BitmapImage(master=self.root, file=imgfile, + foreground='blue', background='yellow') + img3 = tkinter.BitmapImage(master=self.root, file=imgfile, + foreground='white', background='black') + style.element_create('TestButton.button', 'image', + img1, ('pressed', img2), ('active', img3), + border=(2, 4), sticky='we') + self.assertIn('TestButton.button', style.element_names()) + + style.layout('TestButton', [('TestButton.button', {'sticky': 'news'})]) + b = ttk.Button(self.root, style='TestButton') + b.pack(expand=True, fill='both') + self.assertEqual(b.winfo_reqwidth(), 16) + self.assertEqual(b.winfo_reqheight(), 16) + + def test_element_create_image_errors(self): + style = self.style + image = tkinter.PhotoImage(master=self.root, width=10, height=10) + with self.assertRaises(IndexError): + style.element_create('block2', 'image') + with self.assertRaises(TypeError): + style.element_create('block2', 'image', image, 1) + with self.assertRaises(ValueError): + style.element_create('block2', 'image', image, ()) + with self.assertRaisesRegex(TclError, 'Invalid state name'): + style.element_create('block2', 'image', image, ('spam', image)) + with self.assertRaisesRegex(TclError, 'Invalid state name'): + style.element_create('block2', 'image', image, (1, image)) + with self.assertRaises(TypeError): + style.element_create('block2', 'image', image, ('pressed', 1, image)) + with self.assertRaises(TypeError): + style.element_create('block2', 'image', image, (1, 'selected', image)) + with self.assertRaisesRegex(TclError, 'bad option'): + style.element_create('block2', 'image', image, spam=1) + + def test_element_create_vsapi_1(self): + style = self.style + if 'xpnative' not in style.theme_names(): + self.skipTest("requires 'xpnative' theme") + style.element_create('smallclose', 'vsapi', 'WINDOW', 19, [ + ('disabled', 4), + ('pressed', 3), + ('active', 2), + ('', 1)]) + style.layout('CloseButton', + [('CloseButton.smallclose', {'sticky': 'news'})]) + b = ttk.Button(self.root, style='CloseButton') + b.pack(expand=True, fill='both') + self.assertEqual(b.winfo_reqwidth(), 13) + self.assertEqual(b.winfo_reqheight(), 13) + + def test_element_create_vsapi_2(self): + style = self.style + if 'xpnative' not in style.theme_names(): + self.skipTest("requires 'xpnative' theme") + style.element_create('pin', 'vsapi', 'EXPLORERBAR', 3, [ + ('pressed', '!selected', 3), + ('active', '!selected', 2), + ('pressed', 'selected', 6), + ('active', 'selected', 5), + ('selected', 4), + ('', 1)]) + style.layout('Explorer.Pin', + [('Explorer.Pin.pin', {'sticky': 'news'})]) + pin = ttk.Checkbutton(self.root, style='Explorer.Pin') + pin.pack(expand=True, fill='both') + self.assertEqual(pin.winfo_reqwidth(), 16) + self.assertEqual(pin.winfo_reqheight(), 16) + + def test_element_create_vsapi_3(self): + style = self.style + if 'xpnative' not in style.theme_names(): + self.skipTest("requires 'xpnative' theme") + style.element_create('headerclose', 'vsapi', 'EXPLORERBAR', 2, [ + ('pressed', 3), + ('active', 2), + ('', 1)]) + style.layout('Explorer.CloseButton', + [('Explorer.CloseButton.headerclose', {'sticky': 'news'})]) + b = ttk.Button(self.root, style='Explorer.CloseButton') + b.pack(expand=True, fill='both') + self.assertEqual(b.winfo_reqwidth(), 16) + self.assertEqual(b.winfo_reqheight(), 16) + + def test_theme_create(self): + style = self.style + curr_theme = style.theme_use() + curr_layout = style.layout('TLabel') + style.theme_create('testtheme1') + self.assertIn('testtheme1', style.theme_names()) + + style.theme_create('testtheme2', settings={ + 'elem' : {'element create': ['from', 'default'],}, + 'TLabel' : { + 'configure': {'padding': 10}, + 'layout': [('elem', {'sticky': 'we'})], + }, + }) + self.assertIn('testtheme2', style.theme_names()) + + style.theme_create('testtheme3', 'testtheme2') + self.assertIn('testtheme3', style.theme_names()) + + style.theme_use('testtheme1') + self.assertEqual(style.element_names(), ()) + self.assertEqual(style.layout('TLabel'), curr_layout) + + style.theme_use('testtheme2') + self.assertEqual(style.element_names(), ('elem',)) + self.assertEqual(style.lookup('TLabel', 'padding'), '10') + self.assertEqual(style.layout('TLabel'), [('elem', {'sticky': 'we'})]) + + style.theme_use('testtheme3') + self.assertEqual(style.element_names(), ()) + self.assertEqual(style.lookup('TLabel', 'padding'), '') + self.assertEqual(style.layout('TLabel'), [('elem', {'sticky': 'we'})]) + + style.theme_use(curr_theme) + + def test_theme_create_image(self): + style = self.style + curr_theme = style.theme_use() + image = tkinter.PhotoImage(master=self.root, width=10, height=10) + new_theme = 'testtheme4' + style.theme_create(new_theme, settings={ + 'block' : { + 'element create': ['image', image, {'width': 120, 'height': 100}], + }, + 'TestWidget.block2' : { + 'element create': ['image', image], + }, + 'TestWidget' : { + 'configure': { + 'anchor': 'left', + 'padding': (3, 0, 0, 2), + 'foreground': 'yellow', + }, + 'map': { + 'foreground': [ + ('pressed', 'red'), + ('active', 'disabled', 'blue'), + ], + }, + 'layout': [ + ('TestWidget.block', {'sticky': 'we', 'side': 'left'}), + ('TestWidget.border', { + 'sticky': 'nsw', + 'border': 1, + 'children': [ + ('TestWidget.block2', {'sticky': 'nswe'}) + ] + }) + ], + }, + }) + + style.theme_use(new_theme) + self.assertIn('block', style.element_names()) + self.assertEqual(style.lookup('TestWidget', 'anchor'), 'left') + self.assertEqual(style.lookup('TestWidget', 'padding'), '3 0 0 2') + self.assertEqual(style.lookup('TestWidget', 'foreground'), 'yellow') + self.assertEqual(style.lookup('TestWidget', 'foreground', + ['active']), 'yellow') + self.assertEqual(style.lookup('TestWidget', 'foreground', + ['active', 'pressed']), 'red') + self.assertEqual(style.lookup('TestWidget', 'foreground', + ['active', 'disabled']), 'blue') + self.assertEqual(style.layout('TestWidget'), + [ + ('TestWidget.block', {'side': 'left', 'sticky': 'we'}), + ('TestWidget.border', { + 'sticky': 'nsw', + 'border': '1', + 'children': [('TestWidget.block2', {'sticky': 'nswe'})] + }) + ]) + + b = ttk.Label(self.root, style='TestWidget') + b.pack(expand=True, fill='both') + # The exact width varies with the Tk version and display scaling. + self.assertGreater(b.winfo_reqwidth(), 130) + self.assertEqual(b.winfo_reqheight(), 100) + + style.theme_use(curr_theme) + + def test_theme_create_vsapi(self): + style = self.style + if 'xpnative' not in style.theme_names(): + self.skipTest("requires 'xpnative' theme") + curr_theme = style.theme_use() + new_theme = 'testtheme5' + style.theme_create(new_theme, settings={ + 'pin' : { + 'element create': ['vsapi', 'EXPLORERBAR', 3, [ + ('pressed', '!selected', 3), + ('active', '!selected', 2), + ('pressed', 'selected', 6), + ('active', 'selected', 5), + ('selected', 4), + ('', 1)]], + }, + 'Explorer.Pin' : { + 'layout': [('Explorer.Pin.pin', {'sticky': 'news'})], + }, + }) + + style.theme_use(new_theme) + self.assertIn('pin', style.element_names()) + self.assertEqual(style.layout('Explorer.Pin'), + [('Explorer.Pin.pin', {'sticky': 'nswe'})]) + + pin = ttk.Checkbutton(self.root, style='Explorer.Pin') + pin.pack(expand=True, fill='both') + self.assertEqual(pin.winfo_reqwidth(), 16) + self.assertEqual(pin.winfo_reqheight(), 16) + + style.theme_use(curr_theme) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_ttk/test_widgets.py b/Lib/test/test_ttk/test_widgets.py new file mode 100644 index 00000000000..216f5304bba --- /dev/null +++ b/Lib/test/test_ttk/test_widgets.py @@ -0,0 +1,2202 @@ +import unittest +import tkinter +from tkinter import ttk, TclError +from test.support import requires, gc_collect +import sys + +from test.test_ttk_textonly import MockTclObj +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import ( + AbstractTkTest, requires_tk, tk_version, get_tk_patchlevel, + simulate_mouse_click, wait_until_mapped, AbstractDefaultRootTest) +from test.test_tkinter.widget_tests import (add_configure_tests, + AbstractWidgetTest, StandardOptionsTests, IntegerSizeTests, PixelSizeTests) + +requires('gui') + + +class StandardTtkOptionsTests(StandardOptionsTests): + + def test_configure_class(self): + widget = self.create() + self.assertEqual(widget['class'], '') + errmsg='attempt to change read-only option' + if get_tk_patchlevel(self.root) < (8, 6, 0, 'beta', 3): + errmsg='Attempt to change read-only option' + self.checkInvalidParam(widget, 'class', 'Foo', errmsg=errmsg) + widget2 = self.create(class_='Foo') + self.assertEqual(widget2['class'], 'Foo') + + def test_configure_padding(self): + widget = self.create() + if get_tk_patchlevel(self.root) < (8, 6, 14): + def padding_conv(value): + self.assertIsInstance(value, tuple) + return tuple(map(str, value)) + else: + padding_conv = None + self.checkParam(widget, 'padding', 0, expected=(0,), conv=padding_conv) + self.checkParam(widget, 'padding', 5, expected=(5,), conv=padding_conv) + self.checkParam(widget, 'padding', (5, 6), + expected=(5, 6), conv=padding_conv) + self.checkParam(widget, 'padding', (5, 6, 7), + expected=(5, 6, 7), conv=padding_conv) + self.checkParam(widget, 'padding', (5, 6, 7, 8), + expected=(5, 6, 7, 8), conv=padding_conv) + self.checkParam(widget, 'padding', ('5p', '6p', '7p', '8p')) + self.checkParam(widget, 'padding', (), expected='') + + def test_configure_state(self): + widget = self.create() + self.checkParams(widget, 'state', 'active', 'disabled', 'readonly') + + def test_configure_style(self): + widget = self.create() + self.assertEqual(widget['style'], '') + errmsg = 'Layout Foo not found' + if hasattr(self, 'default_orient'): + errmsg = ('Layout %s.Foo not found' % + getattr(self, 'default_orient').title()) + self.checkInvalidParam(widget, 'style', 'Foo', + errmsg=errmsg) + widget2 = self.create(class_='Foo') + self.assertEqual(widget2['class'], 'Foo') + # XXX + + def test_configure_relief(self): + widget = self.create() + self.checkReliefParam(widget, 'relief', + allow_empty=(tk_version >= (8, 7))) + + +class WidgetTest(AbstractTkTest, unittest.TestCase): + """Tests methods available in every ttk widget.""" + + def setUp(self): + super().setUp() + self.widget = ttk.Button(self.root, width=0, text="Text") + self.widget.pack() + + def test_identify(self): + # Identifying the element under a point requires the widget to be + # mapped with a real size; the rest of the checks do not. + if wait_until_mapped(self.widget): + self.assertEqual(self.widget.identify( + int(self.widget.winfo_width() / 2), + int(self.widget.winfo_height() / 2) + ), "label") + self.assertEqual(self.widget.identify(-1, -1), "") + + self.assertRaises(tkinter.TclError, self.widget.identify, None, 5) + self.assertRaises(tkinter.TclError, self.widget.identify, 5, None) + self.assertRaises(tkinter.TclError, self.widget.identify, 5, '') + + def test_widget_state(self): + # XXX not sure about the portability of all these tests + self.assertEqual(self.widget.state(), ()) + self.assertEqual(self.widget.instate(['!disabled']), True) + + # changing from !disabled to disabled + self.assertEqual(self.widget.state(['disabled']), ('!disabled', )) + # no state change + self.assertEqual(self.widget.state(['disabled']), ()) + # change back to !disable but also active + self.assertEqual(self.widget.state(['!disabled', 'active']), + ('!active', 'disabled')) + # no state changes, again + self.assertEqual(self.widget.state(['!disabled', 'active']), ()) + self.assertEqual(self.widget.state(['active', '!disabled']), ()) + + def test_cb(arg1, **kw): + return arg1, kw + self.assertEqual(self.widget.instate(['!disabled'], + test_cb, "hi", **{"msg": "there"}), + ('hi', {'msg': 'there'})) + + # attempt to set invalid statespec + currstate = self.widget.state() + self.assertRaises(tkinter.TclError, self.widget.instate, + ['badstate']) + self.assertRaises(tkinter.TclError, self.widget.instate, + ['disabled', 'badstate']) + # verify that widget didn't change its state + self.assertEqual(currstate, self.widget.state()) + + # ensuring that passing None as state doesn't modify current state + self.widget.state(['active', '!disabled']) + self.assertEqual(self.widget.state(), ('active', )) + + +class AbstractToplevelTest(AbstractWidgetTest, PixelSizeTests): + _rounds_pixels = False + _clipped = {} + + +@add_configure_tests(StandardTtkOptionsTests) +class FrameTest(AbstractToplevelTest, unittest.TestCase): + OPTIONS = ( + 'borderwidth', 'class', 'cursor', 'height', + 'padding', 'relief', 'style', 'takefocus', + 'width', + ) + + def create(self, **kwargs): + return ttk.Frame(self.root, **kwargs) + + +@add_configure_tests(StandardTtkOptionsTests) +class LabelFrameTest(AbstractToplevelTest, unittest.TestCase): + OPTIONS = ( + 'borderwidth', 'class', 'cursor', 'height', + 'labelanchor', 'labelwidget', + 'padding', 'relief', 'style', 'takefocus', + 'text', 'underline', 'width', + ) + + def create(self, **kwargs): + return ttk.LabelFrame(self.root, **kwargs) + + def test_configure_labelanchor(self): + widget = self.create() + self.checkEnumParam(widget, 'labelanchor', + 'e', 'en', 'es', 'n', 'ne', 'nw', 's', 'se', 'sw', 'w', 'wn', 'ws', + errmsg='Bad label anchor specification {}') + self.checkInvalidParam(widget, 'labelanchor', 'center') + + def test_configure_labelwidget(self): + widget = self.create() + label = ttk.Label(self.root, text='Mupp', name='foo') + self.checkParam(widget, 'labelwidget', label, expected='.foo') + label.destroy() + + +class AbstractLabelTest(AbstractWidgetTest): + _allow_empty_justify = True + _rounds_pixels = False + _clipped = {} + + def checkImageParam(self, widget, name): + image = tkinter.PhotoImage(master=self.root, name='image1') + image2 = tkinter.PhotoImage(master=self.root, name='image2') + self.checkParam(widget, name, image, expected=('image1',)) + self.checkParam(widget, name, 'image1', expected=('image1',)) + self.checkParam(widget, name, (image,), expected=('image1',)) + self.checkParam(widget, name, (image, 'active', image2), + expected=('image1', 'active', 'image2')) + self.checkParam(widget, name, 'image1 active image2', + expected=('image1', 'active', 'image2')) + if tk_version < (8, 7): + errmsg = 'image "spam" doesn\'t exist' + else: + errmsg = 'image "spam" does not exist' + self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg) + + def test_configure_compound(self): + values = ('none', 'text', 'image', 'center', 'top', 'bottom', 'left', 'right') + if tk_version >= (8, 7): + values += ('',) + widget = self.create() + self.checkEnumParam(widget, 'compound', *values, allow_empty=True) + + test_configure_justify = requires_tk(8, 7)(StandardOptionsTests.test_configure_justify) + + def test_configure_width(self): + widget = self.create() + self.checkParams(widget, 'width', 402, -402, 0) + + +@add_configure_tests(StandardTtkOptionsTests) +class LabelTest(AbstractLabelTest, unittest.TestCase): + OPTIONS = ( + 'anchor', 'background', 'borderwidth', + 'class', 'compound', 'cursor', 'font', 'foreground', + 'image', 'justify', 'padding', 'relief', 'state', 'style', + 'takefocus', 'text', 'textvariable', + 'underline', 'width', 'wraplength', + ) + _conv_pixels = False + _allow_empty_justify = tk_version >= (8, 7) + + def create(self, **kwargs): + return ttk.Label(self.root, **kwargs) + + def test_configure_anchor(self): + widget = self.create() + values = ('n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center') + if get_tk_patchlevel(self.root) >= (9, 0, 5): + values += ('',) + self.checkEnumParam(widget, 'anchor', *values) + + test_configure_justify = StandardOptionsTests.test_configure_justify + + +@add_configure_tests(StandardTtkOptionsTests) +class ButtonTest(AbstractLabelTest, unittest.TestCase): + OPTIONS = ( + 'class', 'command', 'compound', 'cursor', 'default', + 'image', 'justify', 'padding', 'state', 'style', + 'takefocus', 'text', 'textvariable', + 'underline', 'width', + ) + + def create(self, **kwargs): + return ttk.Button(self.root, **kwargs) + + def test_configure_default(self): + widget = self.create() + values = ('normal', 'active', 'disabled') + self.checkEnumParam(widget, 'default', *values, + sort=tk_version >= (8, 7)) + + def test_invoke(self): + success = [] + btn = ttk.Button(self.root, command=lambda: success.append(1)) + btn.invoke() + self.assertTrue(success) + + +@add_configure_tests(StandardTtkOptionsTests) +class CheckbuttonTest(AbstractLabelTest, unittest.TestCase): + OPTIONS = ( + 'class', 'command', 'compound', 'cursor', + 'image', 'justify', + 'offvalue', 'onvalue', + 'padding', 'state', 'style', + 'takefocus', 'text', 'textvariable', + 'underline', 'variable', 'width', + ) + + def create(self, **kwargs): + return ttk.Checkbutton(self.root, **kwargs) + + def test_configure_offvalue(self): + widget = self.create() + self.checkParams(widget, 'offvalue', 1, 2.3, '', 'any string') + + def test_configure_onvalue(self): + widget = self.create() + self.checkParams(widget, 'onvalue', 1, 2.3, '', 'any string') + + def test_invoke(self): + success = [] + def cb_test(): + success.append(1) + return "cb test called" + + cbtn = ttk.Checkbutton(self.root, command=cb_test) + # the variable automatically created by ttk.Checkbutton is actually + # undefined till we invoke the Checkbutton + self.assertEqual(cbtn.state(), ('alternate', )) + self.assertRaises(tkinter.TclError, cbtn.tk.globalgetvar, + cbtn['variable']) + + res = cbtn.invoke() + self.assertEqual(res, "cb test called") + self.assertEqual(cbtn['onvalue'], + cbtn.tk.globalgetvar(cbtn['variable'])) + self.assertTrue(success) + + cbtn['command'] = '' + res = cbtn.invoke() + if tk_version >= (8, 7) and self.wantobjects: + self.assertEqual(res, ()) + else: + self.assertEqual(str(res), '') + self.assertLessEqual(len(success), 1) + self.assertEqual(cbtn['offvalue'], + cbtn.tk.globalgetvar(cbtn['variable'])) + + def test_unique_variables(self): + frames = [] + buttons = [] + for i in range(2): + f = ttk.Frame(self.root) + f.pack() + frames.append(f) + for j in 'AB': + b = ttk.Checkbutton(f, text=j) + b.pack() + buttons.append(b) + variables = [str(b['variable']) for b in buttons] + self.assertEqual(len(set(variables)), 4, variables) + + def test_unique_variables2(self): + buttons = [] + f = ttk.Frame(self.root) + f.pack() + f = ttk.Frame(self.root) + f.pack() + for j in 'AB': + b = tkinter.Checkbutton(f, text=j) + b.pack() + buttons.append(b) + # Should be larger than the number of all previously created + # tkinter.Checkbutton widgets: + for j in range(100): + b = ttk.Checkbutton(f, text=str(j)) + b.pack() + buttons.append(b) + names = [str(b) for b in buttons] + self.assertEqual(len(set(names)), len(buttons), names) + variables = [str(b['variable']) for b in buttons] + self.assertEqual(len(set(variables)), len(buttons), variables) + + +@add_configure_tests(IntegerSizeTests, StandardTtkOptionsTests) +class EntryTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'background', 'class', 'cursor', + 'exportselection', 'font', 'foreground', + 'invalidcommand', 'justify', + 'placeholder', 'placeholderforeground', + 'show', 'state', 'style', 'takefocus', 'textvariable', + 'validate', 'validatecommand', 'width', 'xscrollcommand', + ) + _rounds_pixels = False + _clipped = {} + # bpo-27313: macOS Tk/Tcl may or may not report 'Entry.field'. + IDENTIFY_AS = {'Entry.field', 'textarea'} + + def setUp(self): + super().setUp() + self.entry = self.create() + self._allow_empty_justify = get_tk_patchlevel(self.root) >= (9, 0, 5) + + def create(self, **kwargs): + return ttk.Entry(self.root, **kwargs) + + def _arrow_x(self, widget, y, arrow): + # Return an x coordinate on the up/down *arrow* element in row y, + # found by scanning inward from the right edge of the widget. A + # fixed inset such as width - 5 lands in the arrow element's + # right-hand border, which scales with the display scaling, so at a + # high DPI identify() there returns 'field' instead of the arrow. + self.assertTrue(wait_until_mapped(widget, full_size=True)) + width = widget.winfo_width() + for x in range(width - 1, width // 2, -1): + if widget.identify(x, y).endswith(arrow): + return x + self.fail(f'no {arrow} found in row {y}') + + def test_configure_invalidcommand(self): + widget = self.create() + self.checkCommandParam(widget, 'invalidcommand') + + def test_configure_show(self): + widget = self.create() + self.checkParam(widget, 'show', '*') + self.checkParam(widget, 'show', '') + self.checkParam(widget, 'show', ' ') + + def test_configure_validate(self): + widget = self.create() + self.checkEnumParam(widget, 'validate', + 'all', 'key', 'focus', 'focusin', 'focusout', 'none') + + def test_configure_validatecommand(self): + widget = self.create() + self.checkCommandParam(widget, 'validatecommand') + + def test_bbox(self): + self.assertIsBoundingBox(self.entry.bbox(0)) + self.assertRaises(tkinter.TclError, self.entry.bbox, 'noindex') + self.assertRaises(tkinter.TclError, self.entry.bbox, None) + + def test_identify(self): + if (tk_version >= (9, 0) and sys.platform == 'darwin' + and isinstance(self.entry, ttk.Combobox)): + self.skipTest('Test does not work on macOS Tk 9.') + # https://core.tcl-lang.org/tk/tktview/8b49e9cfa6 + self.entry.pack() + + # Identifying the element under a point requires the widget to be + # mapped with a real size; the rest of the checks do not. + if wait_until_mapped(self.entry): + # Probe the centre of the widget: a fixed pixel such as (5, 5) + # lands in the field at a normal scaling but in the surrounding + # padding at a high DPI. + x = self.entry.winfo_width() // 2 + y = self.entry.winfo_height() // 2 + self.assertIn(self.entry.identify(x, y), self.IDENTIFY_AS) + self.assertEqual(self.entry.identify(-1, -1), "") + + self.assertRaises(tkinter.TclError, self.entry.identify, None, 5) + self.assertRaises(tkinter.TclError, self.entry.identify, 5, None) + self.assertRaises(tkinter.TclError, self.entry.identify, 5, '') + + def test_validation_options(self): + success = [] + test_invalid = lambda: success.append(True) + + self.entry['validate'] = 'none' + self.entry['validatecommand'] = lambda: False + + self.entry['invalidcommand'] = test_invalid + self.entry.validate() + self.assertTrue(success) + + self.entry['invalidcommand'] = '' + self.entry.validate() + self.assertEqual(len(success), 1) + + self.entry['invalidcommand'] = test_invalid + self.entry['validatecommand'] = lambda: True + self.entry.validate() + self.assertEqual(len(success), 1) + + self.entry['validatecommand'] = '' + self.entry.validate() + self.assertEqual(len(success), 1) + + self.entry['validatecommand'] = True + self.assertRaises(tkinter.TclError, self.entry.validate) + + def test_validation(self): + validation = [] + def validate(to_insert): + if not 'a' <= to_insert.lower() <= 'z': + validation.append(False) + return False + validation.append(True) + return True + + self.entry['validate'] = 'key' + self.entry['validatecommand'] = self.entry.register(validate), '%S' + + self.entry.insert('end', 1) + self.entry.insert('end', 'a') + self.assertEqual(validation, [False, True]) + self.assertEqual(self.entry.get(), 'a') + + def test_revalidation(self): + def validate(content): + for letter in content: + if not 'a' <= letter.lower() <= 'z': + return False + return True + + self.entry['validatecommand'] = self.entry.register(validate), '%P' + + self.entry.insert('end', 'avocado') + self.assertEqual(self.entry.validate(), True) + self.assertEqual(self.entry.state(), ()) + + self.entry.delete(0, 'end') + self.assertEqual(self.entry.get(), '') + + self.entry.insert('end', 'a1b') + self.assertEqual(self.entry.validate(), False) + self.assertEqual(self.entry.state(), ('invalid', )) + + self.entry.delete(1) + self.assertEqual(self.entry.validate(), True) + self.assertEqual(self.entry.state(), ()) + + +@add_configure_tests(IntegerSizeTests, StandardTtkOptionsTests) +class ComboboxTest(EntryTest, unittest.TestCase): + OPTIONS = ( + 'background', 'class', 'cursor', 'exportselection', + 'font', 'foreground', 'height', 'invalidcommand', + 'justify', 'placeholder', 'placeholderforeground', 'postcommand', + 'show', 'state', 'style', + 'takefocus', 'textvariable', + 'validate', 'validatecommand', 'values', + 'width', 'xscrollcommand', + ) + IDENTIFY_AS = {'Combobox.button', 'textarea'} + + def setUp(self): + super().setUp() + self.combo = self.create() + + def create(self, **kwargs): + return ttk.Combobox(self.root, **kwargs) + + def test_configure_height(self): + widget = self.create() + self.checkParams(widget, 'height', 100, 101.2, 102.6, -100, 0, '1i') + + def _show_drop_down_listbox(self): + y = 5 + if sys.platform != 'darwin': # there's no down arrow on macOS + x = self._arrow_x(self.combo, y, 'downarrow') + else: + x = self.combo.winfo_width() - 5 + self.combo.event_generate('', x=x, y=y) + self.combo.event_generate('', x=x, y=y) + + def test_virtual_event(self): + if (tk_version >= (9, 0) and sys.platform == 'darwin' + and isinstance(self.entry, ttk.Combobox)): + self.skipTest('Test does not work on macOS Tk 9.') + # https://core.tcl-lang.org/tk/tktview/8b49e9cfa6 + success = [] + + self.combo['values'] = [1] + self.combo.bind('<>', + lambda evt: success.append(True)) + self.combo.pack() + self.require_mapped(self.combo) + + height = self.combo.winfo_height() + self._show_drop_down_listbox() + self.combo.update() + self.combo.event_generate('') + self.combo.update() + + self.assertTrue(success) + + def test_configure_postcommand(self): + if (tk_version >= (9, 0) and sys.platform == 'darwin' + and isinstance(self.entry, ttk.Combobox)): + self.skipTest('Test does not work on macOS Tk 9.') + # https://core.tcl-lang.org/tk/tktview/8b49e9cfa6 + success = [] + + self.combo['postcommand'] = lambda: success.append(True) + self.combo.pack() + self.require_mapped(self.combo) + + self._show_drop_down_listbox() + self.assertTrue(success) + + # testing postcommand removal + self.combo['postcommand'] = '' + self._show_drop_down_listbox() + self.assertEqual(len(success), 1) + + def test_configure_values(self): + def check_get_current(getval, currval): + self.assertEqual(self.combo.get(), getval) + self.assertEqual(self.combo.current(), currval) + + self.assertIn(self.combo['values'], ((), '')) + check_get_current('', -1) + + self.checkParam(self.combo, 'values', 'mon tue wed thur', + expected=('mon', 'tue', 'wed', 'thur')) + self.checkParam(self.combo, 'values', ('mon', 'tue', 'wed', 'thur')) + self.checkParam(self.combo, 'values', (42, 3.14, '', 'any string')) + self.checkParam(self.combo, 'values', '') + + self.combo['values'] = ['a', 1, 'c'] + + self.combo.set('c') + check_get_current('c', 2) + + self.combo.current(0) + check_get_current('a', 0) + + self.combo.set('d') + check_get_current('d', -1) + + # testing values with empty string + self.combo.set('') + self.combo['values'] = (1, 2, '', 3) + check_get_current('', 2) + + # testing values with empty string set through configure + self.combo.configure(values=[1, '', 2]) + self.assertEqual(self.combo['values'], + ('1', '', '2') if self.wantobjects else + '1 {} 2') + + # testing values with spaces + self.combo['values'] = ['a b', 'a\tb', 'a\nb'] + self.assertEqual(self.combo['values'], + ('a b', 'a\tb', 'a\nb') if self.wantobjects else + '{a b} {a\tb} {a\nb}') + + # testing values with special characters + self.combo['values'] = [r'a\tb', '"a"', '} {'] + self.assertEqual(self.combo['values'], + (r'a\tb', '"a"', '} {') if self.wantobjects else + r'a\\tb {"a"} \}\ \{') + + # out of range + self.assertRaises(tkinter.TclError, self.combo.current, + len(self.combo['values'])) + # it expects an integer (or something that can be converted to int) + self.assertRaises(tkinter.TclError, self.combo.current, '') + + # testing creating combobox with empty string in values + combo2 = ttk.Combobox(self.root, values=[1, 2, '']) + self.assertEqual(combo2['values'], + ('1', '2', '') if self.wantobjects else '1 2 {}') + combo2.destroy() + + +@add_configure_tests(IntegerSizeTests, StandardTtkOptionsTests) +class PanedWindowTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'class', 'cursor', 'height', + 'orient', 'style', 'takefocus', 'width', + ) + _rounds_pixels = False + _clipped = {} + + def setUp(self): + super().setUp() + self.paned = self.create() + + def create(self, **kwargs): + return ttk.PanedWindow(self.root, **kwargs) + + def test_configure_orient(self): + widget = self.create() + self.assertEqual(str(widget['orient']), 'vertical') + errmsg='attempt to change read-only option' + if get_tk_patchlevel(self.root) < (8, 6, 0, 'beta', 3): + errmsg='Attempt to change read-only option' + self.checkInvalidParam(widget, 'orient', 'horizontal', + errmsg=errmsg) + widget2 = self.create(orient='horizontal') + self.assertEqual(str(widget2['orient']), 'horizontal') + + def test_add(self): + # attempt to add a child that is not a direct child of the paned window + label = ttk.Label(self.paned) + child = ttk.Label(label) + self.assertRaises(tkinter.TclError, self.paned.add, child) + label.destroy() + child.destroy() + # another attempt + label = ttk.Label(self.root) + child = ttk.Label(label) + self.assertRaises(tkinter.TclError, self.paned.add, child) + child.destroy() + label.destroy() + + good_child = ttk.Label(self.root) + self.paned.add(good_child) + # re-adding a child is not accepted + self.assertRaises(tkinter.TclError, self.paned.add, good_child) + + other_child = ttk.Label(self.paned) + self.paned.add(other_child) + self.assertEqual(self.paned.pane(0), self.paned.pane(1)) + self.assertRaises(tkinter.TclError, self.paned.pane, 2) + good_child.destroy() + other_child.destroy() + self.assertRaises(tkinter.TclError, self.paned.pane, 0) + + def test_forget(self): + self.assertRaises(tkinter.TclError, self.paned.forget, None) + self.assertRaises(tkinter.TclError, self.paned.forget, 0) + + self.paned.add(ttk.Label(self.root)) + self.paned.forget(0) + self.assertRaises(tkinter.TclError, self.paned.forget, 0) + + def test_insert(self): + self.assertRaises(tkinter.TclError, self.paned.insert, None, 0) + self.assertRaises(tkinter.TclError, self.paned.insert, 0, None) + self.assertRaises(tkinter.TclError, self.paned.insert, 0, 0) + + child = ttk.Label(self.root) + child2 = ttk.Label(self.root) + child3 = ttk.Label(self.root) + + if tk_version >= (8, 7): + self.paned.insert(0, child) + self.assertEqual(self.paned.panes(), (str(child),)) + self.paned.forget(0) + else: + self.assertRaises(tkinter.TclError, self.paned.insert, 0, child) + + self.assertEqual(self.paned.panes(), ()) + self.paned.insert('end', child2) + self.paned.insert(0, child) + self.assertEqual(self.paned.panes(), (str(child), str(child2))) + + self.paned.insert(0, child2) + self.assertEqual(self.paned.panes(), (str(child2), str(child))) + + self.paned.insert('end', child3) + self.assertEqual(self.paned.panes(), + (str(child2), str(child), str(child3))) + + # reinserting a child should move it to its current position + panes = self.paned.panes() + self.paned.insert('end', child3) + self.assertEqual(panes, self.paned.panes()) + + # moving child3 to child2 position should result in child2 ending up + # in previous child position and child ending up in previous child3 + # position + self.paned.insert(child2, child3) + self.assertEqual(self.paned.panes(), + (str(child3), str(child2), str(child))) + + def test_pane(self): + self.assertRaises(tkinter.TclError, self.paned.pane, 0) + + child = ttk.Label(self.root) + self.paned.add(child) + self.assertIsInstance(self.paned.pane(0), dict) + self.assertEqual(self.paned.pane(0, weight=None), + 0 if self.wantobjects else '0') + # newer form for querying a single option + self.assertEqual(self.paned.pane(0, 'weight'), + 0 if self.wantobjects else '0') + self.assertEqual(self.paned.pane(0), self.paned.pane(str(child))) + + self.assertRaises(tkinter.TclError, self.paned.pane, 0, + badoption='somevalue') + + def test_sashpos(self): + self.assertRaises(tkinter.TclError, self.paned.sashpos, None) + self.assertRaises(tkinter.TclError, self.paned.sashpos, '') + self.assertRaises(tkinter.TclError, self.paned.sashpos, 0) + + child = ttk.Label(self.paned, text='a') + self.paned.add(child, weight=1) + self.assertRaises(tkinter.TclError, self.paned.sashpos, 0) + child2 = ttk.Label(self.paned, text='b') + self.paned.add(child2) + self.assertRaises(tkinter.TclError, self.paned.sashpos, 1) + + self.paned.pack(expand=True, fill='both') + + curr_pos = self.paned.sashpos(0) + self.paned.sashpos(0, 1000) + self.assertNotEqual(curr_pos, self.paned.sashpos(0)) + self.assertIsInstance(self.paned.sashpos(0), int) + + +@add_configure_tests(StandardTtkOptionsTests) +class RadiobuttonTest(AbstractLabelTest, unittest.TestCase): + OPTIONS = ( + 'class', 'command', 'compound', 'cursor', + 'image', 'justify', + 'padding', 'state', 'style', + 'takefocus', 'text', 'textvariable', + 'underline', 'value', 'variable', 'width', + ) + + def create(self, **kwargs): + return ttk.Radiobutton(self.root, **kwargs) + + def test_configure_value(self): + widget = self.create() + self.checkParams(widget, 'value', 1, 2.3, '', 'any string') + + def test_configure_invoke(self): + success = [] + def cb_test(): + success.append(1) + return "cb test called" + + myvar = tkinter.IntVar(self.root) + cbtn = ttk.Radiobutton(self.root, command=cb_test, + variable=myvar, value=0) + cbtn2 = ttk.Radiobutton(self.root, command=cb_test, + variable=myvar, value=1) + + if self.wantobjects: + conv = lambda x: x + else: + conv = int + + res = cbtn.invoke() + self.assertEqual(res, "cb test called") + self.assertEqual(conv(cbtn['value']), myvar.get()) + self.assertEqual(myvar.get(), + conv(cbtn.tk.globalgetvar(cbtn['variable']))) + self.assertTrue(success) + + cbtn2['command'] = '' + res = cbtn2.invoke() + if tk_version >= (8, 7) and self.wantobjects: + self.assertEqual(res, ()) + else: + self.assertEqual(str(res), '') + self.assertLessEqual(len(success), 1) + self.assertEqual(conv(cbtn2['value']), myvar.get()) + self.assertEqual(myvar.get(), + conv(cbtn.tk.globalgetvar(cbtn['variable']))) + + self.assertEqual(str(cbtn['variable']), str(cbtn2['variable'])) + + +@add_configure_tests(StandardTtkOptionsTests) +class MenubuttonTest(AbstractLabelTest, unittest.TestCase): + OPTIONS = ( + 'class', 'compound', 'cursor', 'direction', + 'image', 'justify', 'menu', 'padding', 'state', 'style', + 'takefocus', 'text', 'textvariable', + 'underline', 'width', + ) + + def create(self, **kwargs): + return ttk.Menubutton(self.root, **kwargs) + + def test_configure_direction(self): + widget = self.create() + values = ('above', 'below', 'left', 'right', 'flush') + self.checkEnumParam(widget, 'direction', *values, + sort=tk_version >= (8, 7)) + + def test_configure_menu(self): + widget = self.create() + menu = tkinter.Menu(widget, name='menu') + self.checkParam(widget, 'menu', menu, conv=str) + menu.destroy() + + +@add_configure_tests(StandardTtkOptionsTests) +class ScaleTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'class', 'command', 'cursor', 'from', 'length', + 'orient', 'state', 'style', 'takefocus', 'to', 'value', 'variable', + ) + _rounds_pixels = False + _clipped = {} + default_orient = 'horizontal' + + def setUp(self): + super().setUp() + self.scale = self.create() + self.scale.pack() + self.scale.update() + + def create(self, **kwargs): + return ttk.Scale(self.root, **kwargs) + + def test_configure_from(self): + widget = self.create() + self.checkFloatParam(widget, 'from', 100, 14.9, 15.1, conv=False) + + def test_configure_length(self): + widget = self.create() + self.checkPixelsParam(widget, 'length', 130, 131.2, 135.6, '5i') + + test_configure_state = requires_tk(8, 6, 9)(StandardTtkOptionsTests.test_configure_state) + + def test_configure_to(self): + widget = self.create() + self.checkFloatParam(widget, 'to', 300, 14.9, 15.1, -10, conv=False) + + def test_configure_value(self): + widget = self.create() + self.checkFloatParam(widget, 'value', 300, 14.9, 15.1, -10, conv=False) + + def test_custom_event(self): + failure = [1, 1, 1] # will need to be empty + + funcid = self.scale.bind('<>', lambda evt: failure.pop()) + + self.scale['from'] = 10 + self.scale['from_'] = 10 + self.scale['to'] = 3 + + self.assertFalse(failure) + + failure = [1, 1, 1] + self.scale.configure(from_=2, to=5) + self.scale.configure(from_=0, to=-2) + self.scale.configure(to=10) + + self.assertFalse(failure) + + def test_get(self): + if self.wantobjects: + conv = lambda x: x + else: + conv = float + + # Reading the value at the far edge needs the realized width. + if wait_until_mapped(self.scale): + scale_width = self.scale.winfo_width() + self.assertEqual(self.scale.get(scale_width, 0), self.scale['to']) + + self.assertEqual(conv(self.scale.get(0, 0)), conv(self.scale['from'])) + self.assertEqual(self.scale.get(), self.scale['value']) + self.scale['value'] = 30 + self.assertEqual(self.scale.get(), self.scale['value']) + + self.assertRaises(tkinter.TclError, self.scale.get, '', 0) + self.assertRaises(tkinter.TclError, self.scale.get, 0, '') + + def test_set(self): + if self.wantobjects: + conv = lambda x: x + else: + conv = float + + # set restricts the max/min values according to the current range + max = conv(self.scale['to']) + new_max = max + 10 + self.scale.set(new_max) + self.assertEqual(conv(self.scale.get()), max) + min = conv(self.scale['from']) + self.scale.set(min - 1) + self.assertEqual(conv(self.scale.get()), min) + + # changing directly the variable doesn't impose this limitation tho + var = tkinter.DoubleVar(self.root) + self.scale['variable'] = var + var.set(max + 5) + self.assertEqual(conv(self.scale.get()), var.get()) + self.assertEqual(conv(self.scale.get()), max + 5) + del var + gc_collect() # For PyPy or other GCs. + + # the same happens with the value option + self.scale['value'] = max + 10 + self.assertEqual(conv(self.scale.get()), max + 10) + self.assertEqual(conv(self.scale.get()), conv(self.scale['value'])) + + # nevertheless, note that the max/min values we can get specifying + # x, y coords are the ones according to the current range + self.assertEqual(conv(self.scale.get(0, 0)), min) + # Reading the value at the far edge needs the realized width. + if wait_until_mapped(self.scale): + self.assertEqual( + conv(self.scale.get(self.scale.winfo_width(), 0)), max) + + self.assertRaises(tkinter.TclError, self.scale.set, None) + + +@add_configure_tests(StandardTtkOptionsTests) +class ProgressbarTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'anchor', 'class', 'cursor', 'font', 'foreground', 'justify', + 'orient', 'length', + 'mode', 'maximum', 'phase', 'text', 'wraplength', + 'style', 'takefocus', 'value', 'variable', + ) + _rounds_pixels = False + _clipped = {} + _allow_empty_justify = True + default_orient = 'horizontal' + + def create(self, **kwargs): + return ttk.Progressbar(self.root, **kwargs) + + @requires_tk(8, 7) + def test_configure_anchor(self): + widget = self.create() + self.checkEnumParam(widget, 'anchor', + 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center', '') + + test_configure_font = requires_tk(8, 7)(StandardOptionsTests.test_configure_font) + test_configure_foreground = requires_tk(8, 7)(StandardOptionsTests.test_configure_foreground) + test_configure_justify = requires_tk(8, 7)(StandardTtkOptionsTests.test_configure_justify) + + def test_configure_length(self): + widget = self.create() + self.checkPixelsParam(widget, 'length', 100.1, 56.7, '2i') + + def test_configure_maximum(self): + widget = self.create() + self.checkFloatParam(widget, 'maximum', 150.2, 77.7, 0, -10, conv=False) + + def test_configure_mode(self): + widget = self.create() + self.checkEnumParam(widget, 'mode', 'determinate', 'indeterminate') + + def test_configure_phase(self): + # XXX + pass + + test_configure_text = requires_tk(8, 7)(StandardOptionsTests.test_configure_text) + + def test_configure_value(self): + widget = self.create() + self.checkFloatParam(widget, 'value', 150.2, 77.7, 0, -10, + conv=False) + + test_configure_wraplength = requires_tk(8, 7)(StandardOptionsTests.test_configure_wraplength) + + def test_step(self): + widget = self.create(maximum=100, mode='determinate') + self.assertEqual(float(widget['value']), 0.0) + widget.step() # The default increment is 1.0. + self.assertEqual(float(widget['value']), 1.0) + widget.step(5) + self.assertEqual(float(widget['value']), 6.0) + widget.step(-2) + self.assertEqual(float(widget['value']), 4.0) + + def test_start_stop(self): + widget = self.create(maximum=100, mode='determinate') + widget.pack() + widget.start() # Schedule autoincrement; no exception. + widget.update() + widget.stop() # Cancel it. + # After stopping, the value no longer changes. + value = float(widget['value']) + widget.update() + self.assertEqual(float(widget['value']), value) + + +@unittest.skipIf(sys.platform == 'darwin', + 'ttk.Scrollbar is special on MacOSX') +@add_configure_tests(StandardTtkOptionsTests) +class ScrollbarTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'class', 'command', 'cursor', 'orient', 'style', 'takefocus', + ) + _rounds_pixels = False + _clipped = {} + default_orient = 'vertical' + + def create(self, **kwargs): + return ttk.Scrollbar(self.root, **kwargs) + + +@add_configure_tests(StandardTtkOptionsTests) +class NotebookTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'class', 'cursor', 'height', 'padding', 'style', 'takefocus', 'width', + ) + _rounds_pixels = (tk_version < (9,0)) + _converts_pixels = False + _clipped = {} + + def setUp(self): + super().setUp() + self.nb = self.create(padding=0) + self.child1 = ttk.Label(self.root) + self.child2 = ttk.Label(self.root) + self.nb.add(self.child1, text='a') + self.nb.add(self.child2, text='b') + + def create(self, **kwargs): + return ttk.Notebook(self.root, **kwargs) + + def test_configure_height(self): + widget = self.create() + if get_tk_patchlevel(self.root) < (8, 6, 15): + self.checkIntegerParam(widget, 'height', 402, -402, 0) + else: + self.checkPixelsParam(widget, 'height', '10c', 402, -402, 0) + + def test_configure_width(self): + widget = self.create() + if get_tk_patchlevel(self.root) < (8, 6, 15): + self.checkIntegerParam(widget, 'width', 402, -402, 0) + else: + self.checkPixelsParam(widget, 'width', '10c', 402, -402, 0) + + def test_tab_identifiers(self): + self.nb.forget(0) + self.nb.hide(self.child2) + self.assertRaises(tkinter.TclError, self.nb.tab, self.child1) + self.assertEqual(self.nb.index('end'), 1) + self.nb.add(self.child2) + self.assertEqual(self.nb.index('end'), 1) + self.nb.select(self.child2) + + self.assertTrue(self.nb.tab('current')) + self.nb.add(self.child1, text='a') + + self.nb.pack() + self.nb.update() + # Address a tab by a point taken from Tk's own geometry rather than a + # fixed pixel, which can land on a different tab or miss the tabs + # entirely (e.g. on Aqua the tabs are inset) as the display scaling + # changes. + x, y = self._tab_point(self.nb.index('current')) + self.assertEqual(self.nb.tab(f'@{x},{y}'), self.nb.tab('current')) + + x, y = self._tab_point(self.nb.index(self.child1)) + self.assertEqual(self.nb.tab(f'@{x},{y}', text=None), 'a') + + def test_add_and_hidden(self): + self.assertRaises(tkinter.TclError, self.nb.hide, -1) + self.assertRaises(tkinter.TclError, self.nb.hide, 'hi') + self.assertRaises(tkinter.TclError, self.nb.hide, None) + self.assertRaises(tkinter.TclError, self.nb.add, None) + self.assertRaises(tkinter.TclError, self.nb.add, ttk.Label(self.root), + unknown='option') + + tabs = self.nb.tabs() + self.nb.hide(self.child1) + self.nb.add(self.child1) + self.assertEqual(self.nb.tabs(), tabs) + + child = ttk.Label(self.root) + self.nb.add(child, text='c') + tabs = self.nb.tabs() + + curr = self.nb.index('current') + # verify that the tab gets re-added at its previous position + child2_index = self.nb.index(self.child2) + self.nb.hide(self.child2) + self.nb.add(self.child2) + self.assertEqual(self.nb.tabs(), tabs) + self.assertEqual(self.nb.index(self.child2), child2_index) + self.assertEqual(str(self.child2), self.nb.tabs()[child2_index]) + # but the tab next to it (not hidden) is the one selected now + self.assertEqual(self.nb.index('current'), curr + 1) + + def test_forget(self): + self.assertRaises(tkinter.TclError, self.nb.forget, -1) + self.assertRaises(tkinter.TclError, self.nb.forget, 'hi') + self.assertRaises(tkinter.TclError, self.nb.forget, None) + + tabs = self.nb.tabs() + child1_index = self.nb.index(self.child1) + self.nb.forget(self.child1) + self.assertNotIn(str(self.child1), self.nb.tabs()) + self.assertEqual(len(tabs) - 1, len(self.nb.tabs())) + + self.nb.add(self.child1) + self.assertEqual(self.nb.index(self.child1), 1) + self.assertNotEqual(child1_index, self.nb.index(self.child1)) + + def test_index(self): + self.assertRaises(tkinter.TclError, self.nb.index, -1) + self.assertRaises(tkinter.TclError, self.nb.index, None) + + self.assertIsInstance(self.nb.index('end'), int) + self.assertEqual(self.nb.index(self.child1), 0) + self.assertEqual(self.nb.index(self.child2), 1) + self.assertEqual(self.nb.index('end'), 2) + + def test_insert(self): + # moving tabs + tabs = self.nb.tabs() + self.nb.insert(1, tabs[0]) + self.assertEqual(self.nb.tabs(), (tabs[1], tabs[0])) + self.nb.insert(self.child1, self.child2) + self.assertEqual(self.nb.tabs(), tabs) + self.nb.insert('end', self.child1) + self.assertEqual(self.nb.tabs(), (tabs[1], tabs[0])) + self.nb.insert('end', 0) + self.assertEqual(self.nb.tabs(), tabs) + # bad moves + self.assertRaises(tkinter.TclError, self.nb.insert, 2, tabs[0]) + self.assertRaises(tkinter.TclError, self.nb.insert, -1, tabs[0]) + + # new tab + child3 = ttk.Label(self.root) + self.nb.insert(1, child3) + self.assertEqual(self.nb.tabs(), (tabs[0], str(child3), tabs[1])) + self.nb.forget(child3) + self.assertEqual(self.nb.tabs(), tabs) + self.nb.insert(self.child1, child3) + self.assertEqual(self.nb.tabs(), (str(child3), ) + tabs) + self.nb.forget(child3) + if tk_version >= (8, 7): + self.nb.insert(2, child3) + self.assertEqual(self.nb.tabs(), (*tabs, str(child3))) + else: + self.assertRaises(tkinter.TclError, self.nb.insert, 2, child3) + self.assertRaises(tkinter.TclError, self.nb.insert, -1, child3) + + # bad inserts + self.assertRaises(tkinter.TclError, self.nb.insert, 'end', None) + self.assertRaises(tkinter.TclError, self.nb.insert, None, 0) + self.assertRaises(tkinter.TclError, self.nb.insert, None, None) + + def test_select(self): + self.nb.pack() + self.nb.update() + + success = [] + tab_changed = [] + + self.child1.bind('', lambda evt: success.append(True)) + self.nb.bind('<>', + lambda evt: tab_changed.append(True)) + + self.assertEqual(self.nb.select(), str(self.child1)) + self.nb.select(self.child2) + self.assertTrue(success) + self.assertEqual(self.nb.select(), str(self.child2)) + + self.nb.update() + self.assertTrue(tab_changed) + + def test_tab(self): + self.assertRaises(tkinter.TclError, self.nb.tab, -1) + self.assertRaises(tkinter.TclError, self.nb.tab, 'notab') + self.assertRaises(tkinter.TclError, self.nb.tab, None) + + self.assertIsInstance(self.nb.tab(self.child1), dict) + self.assertEqual(self.nb.tab(self.child1, text=None), 'a') + # newer form for querying a single option + self.assertEqual(self.nb.tab(self.child1, 'text'), 'a') + self.nb.tab(self.child1, text='abc') + self.assertEqual(self.nb.tab(self.child1, text=None), 'abc') + self.assertEqual(self.nb.tab(self.child1, 'text'), 'abc') + + def test_configure_tabs(self): + self.assertEqual(len(self.nb.tabs()), 2) + + self.nb.forget(self.child1) + self.nb.forget(self.child2) + + self.assertEqual(self.nb.tabs(), ()) + + def _tab_point(self, index): + # Return a window coordinate that really lies on tab *index*, taken + # from Tk's own geometry (ttk::notebook index @x,y), so that it is + # correct regardless of the theme, platform and display scaling. A + # fixed pixel such as (5, 5) can land on a different tab element + # depending on the DPI, or miss the tabs entirely (e.g. on Aqua the + # tabs are inset), which would make a click there test nothing. + self.assertTrue(wait_until_mapped(self.nb, full_size=True)) + w, h = self.nb.winfo_width(), self.nb.winfo_height() + # The selected pane fills the widget except for the tab strip, so skip + # that large tab-free region instead of querying every pixel in it. + pane = self.nb.nametowidget(self.nb.select()) + px, py = pane.winfo_x(), pane.winfo_y() + pw, ph = pane.winfo_width(), pane.winfo_height() + for y in range(h): + inside_y = py <= y < py + ph + for x in range(w): + if inside_y and px <= x < px + pw: + continue + try: + if self.nb.index(f'@{x},{y}') == index: + return x, y + except TclError: + continue + self.fail(f'no point found on tab {index}') + + def test_traversal(self): + self.nb.pack() + self.nb.update() + + # A mouse click selects the tab it lands on. + self.nb.select(1) + self.assertEqual(self.nb.select(), str(self.child2)) + simulate_mouse_click(self.nb, *self._tab_point(0)) + self.assertEqual(self.nb.select(), str(self.child1)) + + # Control-Tab and Shift-Control-Tab traverse the tabs. + self.nb.focus_force() + self.nb.event_generate('') + self.assertEqual(self.nb.select(), str(self.child2)) + self.nb.focus_force() + self.nb.event_generate('') + self.assertEqual(self.nb.select(), str(self.child1)) + self.nb.focus_force() + self.nb.event_generate('') + self.assertEqual(self.nb.select(), str(self.child2)) + + self.nb.tab(self.child1, text='a', underline=0) + self.nb.tab(self.child2, text='e', underline=0) + self.nb.enable_traversal() + self.nb.focus_force() + + # A click still selects the tab it lands on after enable_traversal(). + self.nb.select(1) + self.assertEqual(self.nb.select(), str(self.child2)) + simulate_mouse_click(self.nb, *self._tab_point(0)) + self.assertEqual(self.nb.select(), str(self.child1)) + + # Mnemonics traverse the tabs (macOS uses region-dependent + # Emacs-style shortcuts, so use the regular arrow keys there). + if sys.platform == 'darwin': + begin = '' + end = '' + else: + begin = '' + end = '' + self.nb.event_generate(end) + self.assertEqual(self.nb.select(), str(self.child2)) + self.nb.event_generate(begin) + self.assertEqual(self.nb.select(), str(self.child1)) + + +@add_configure_tests(IntegerSizeTests, StandardTtkOptionsTests) +class SpinboxTest(EntryTest, unittest.TestCase): + OPTIONS = ( + 'background', 'class', 'command', 'cursor', 'exportselection', + 'font', 'foreground', 'format', 'from', 'increment', + 'invalidcommand', 'justify', + 'placeholder', 'placeholderforeground', + 'show', 'state', 'style', + 'takefocus', 'textvariable', 'to', 'validate', 'validatecommand', + 'values', 'width', 'wrap', 'xscrollcommand', + ) + IDENTIFY_AS = {'Spinbox.field', 'textarea'} + + def setUp(self): + super().setUp() + self.spin = self.create() + self.spin.pack() + + def create(self, **kwargs): + return ttk.Spinbox(self.root, **kwargs) + + def _click_increment_arrow(self): + self.require_mapped(self.spin) + y = self.spin.winfo_height()//2 - 5 + x = self._arrow_x(self.spin, y, 'uparrow') + self.spin.event_generate('', x=x, y=y) + self.spin.event_generate('', x=x, y=y) + self.spin.update_idletasks() + + def _click_decrement_arrow(self): + self.require_mapped(self.spin) + y = self.spin.winfo_height()//2 + 4 + x = self._arrow_x(self.spin, y, 'downarrow') + self.spin.event_generate('', x=x, y=y) + self.spin.event_generate('', x=x, y=y) + self.spin.update_idletasks() + + def test_configure_command(self): + success = [] + + self.spin['command'] = lambda: success.append(True) + self.spin.update() + self._click_increment_arrow() + self.spin.update() + self.assertTrue(success) + + self._click_decrement_arrow() + self.assertEqual(len(success), 2) + + # testing postcommand removal + self.spin['command'] = '' + self.spin.update_idletasks() + self._click_increment_arrow() + self._click_decrement_arrow() + self.spin.update() + self.assertEqual(len(success), 2) + + def test_increment_decrement_events(self): + # Clicking the arrows fires the <> and <> + # virtual events. + events = [] + self.spin.bind('<>', lambda e: events.append('increment')) + self.spin.bind('<>', lambda e: events.append('decrement')) + self.spin.update() + self._click_increment_arrow() + self.spin.update() + self._click_decrement_arrow() + self.spin.update() + self.assertEqual(events, ['increment', 'decrement']) + + def test_configure_to(self): + self.spin['from'] = 0 + self.spin['to'] = 5 + self.spin.set(4) + self.spin.update() + self._click_increment_arrow() # 5 + + self.assertEqual(self.spin.get(), '5') + + self._click_increment_arrow() # 5 + self.assertEqual(self.spin.get(), '5') + + def test_configure_from(self): + self.spin['from'] = 1 + self.spin['to'] = 10 + self.spin.set(2) + self.spin.update() + self._click_decrement_arrow() # 1 + self.assertEqual(self.spin.get(), '1') + self._click_decrement_arrow() # 1 + self.assertEqual(self.spin.get(), '1') + + def test_configure_increment(self): + self.spin['from'] = 0 + self.spin['to'] = 10 + self.spin['increment'] = 4 + self.spin.set(1) + self.spin.update() + + self._click_increment_arrow() # 5 + self.assertEqual(self.spin.get(), '5') + self.spin['increment'] = 2 + self.spin.update() + self._click_decrement_arrow() # 3 + self.assertEqual(self.spin.get(), '3') + + def test_configure_format(self): + self.spin.set(1) + self.spin['format'] = '%10.3f' + self.spin.update() + self._click_increment_arrow() + value = self.spin.get() + + self.assertEqual(len(value), 10) + self.assertEqual(value.index('.'), 6) + + self.spin['format'] = '' + self.spin.update() + self._click_increment_arrow() + value = self.spin.get() + self.assertTrue('.' not in value) + self.assertEqual(len(value), 1) + + def test_configure_wrap(self): + self.spin['to'] = 10 + self.spin['from'] = 1 + self.spin.set(1) + self.spin['wrap'] = True + self.spin.update() + + self._click_decrement_arrow() + self.assertEqual(self.spin.get(), '10') + + self._click_increment_arrow() + self.assertEqual(self.spin.get(), '1') + + self.spin['wrap'] = False + self.spin.update() + + self._click_decrement_arrow() + self.assertEqual(self.spin.get(), '1') + + def test_configure_values(self): + self.assertEqual(self.spin['values'], '') + self.checkParam(self.spin, 'values', 'mon tue wed thur', + expected=('mon', 'tue', 'wed', 'thur')) + self.checkParam(self.spin, 'values', ('mon', 'tue', 'wed', 'thur')) + self.checkParam(self.spin, 'values', (42, 3.14, '', 'any string')) + self.checkParam(self.spin, 'values', '') + + self.spin['values'] = ['a', 1, 'c'] + + # test incrementing / decrementing values + self.spin.set('a') + self.spin.update() + self._click_increment_arrow() + self.assertEqual(self.spin.get(), '1') + + self._click_decrement_arrow() + self.assertEqual(self.spin.get(), 'a') + + # testing values with empty string set through configure + self.spin.configure(values=[1, '', 2]) + self.assertEqual(self.spin['values'], + ('1', '', '2') if self.wantobjects else + '1 {} 2') + + # testing values with spaces + self.spin['values'] = ['a b', 'a\tb', 'a\nb'] + self.assertEqual(self.spin['values'], + ('a b', 'a\tb', 'a\nb') if self.wantobjects else + '{a b} {a\tb} {a\nb}') + + # testing values with special characters + self.spin['values'] = [r'a\tb', '"a"', '} {'] + self.assertEqual(self.spin['values'], + (r'a\tb', '"a"', '} {') if self.wantobjects else + r'a\\tb {"a"} \}\ \{') + + # testing creating spinbox with empty string in values + spin2 = ttk.Spinbox(self.root, values=[1, 2, '']) + self.assertEqual(spin2['values'], + ('1', '2', '') if self.wantobjects else '1 2 {}') + spin2.destroy() + + +@add_configure_tests(StandardTtkOptionsTests) +class TreeviewTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'class', 'columns', 'cursor', 'displaycolumns', + 'height', 'padding', 'selectmode', 'selecttype', 'show', 'striped', + 'style', 'takefocus', 'titlecolumns', 'titleitems', + 'xscrollcommand', 'yscrollcommand', + ) + _rounds_pixels = False + _clipped = {} + + def setUp(self): + super().setUp() + self.tv = self.create(padding=0) + + def create(self, **kwargs): + return ttk.Treeview(self.root, **kwargs) + + def test_configure_columns(self): + widget = self.create() + self.checkParam(widget, 'columns', 'a b c', + expected=('a', 'b', 'c')) + self.checkParam(widget, 'columns', ('a', 'b', 'c')) + self.checkParam(widget, 'columns', '', + expected=() if tk_version >= (8, 7) else '') + + def test_configure_displaycolumns(self): + widget = self.create() + widget['columns'] = ('a', 'b', 'c') + self.checkParam(widget, 'displaycolumns', 'b a c', + expected=('b', 'a', 'c')) + self.checkParam(widget, 'displaycolumns', ('b', 'a', 'c')) + self.checkParam(widget, 'displaycolumns', '#all', + expected=('#all',)) + self.checkParam(widget, 'displaycolumns', (2, 1, 0)) + self.checkInvalidParam(widget, 'displaycolumns', ('a', 'b', 'd'), + errmsg='Invalid column index "?d"?') + errmsg = 'Column index "?{}"? out of bounds' + self.checkInvalidParam(widget, 'displaycolumns', (1, 2, 3), + errmsg=errmsg.format(3)) + self.checkInvalidParam(widget, 'displaycolumns', (1, -2), + errmsg=errmsg.format(-2)) + + def test_configure_height(self): + widget = self.create() + self.checkPixelsParam(widget, 'height', 100, -100, 0, '3c', + conv=False) + self.checkPixelsParam(widget, 'height', 101.2, 102.6, '3c', + conv=False) + + def test_configure_selectmode(self): + widget = self.create() + if tk_version >= (9, 1): + self.checkEnumParam(widget, 'selectmode', + 'none', 'single', 'browse', 'extended', + 'multiple') + else: + self.checkEnumParam(widget, 'selectmode', + 'none', 'browse', 'extended') + + @requires_tk(8, 7) + def test_configure_selecttype(self): + widget = self.create() + self.checkEnumParam(widget, 'selecttype', 'item', 'cell') + + def test_configure_show(self): + widget = self.create() + self.checkParam(widget, 'show', 'tree headings', + expected=('tree', 'headings')) + self.checkParam(widget, 'show', ('tree', 'headings')) + self.checkParam(widget, 'show', ('headings', 'tree')) + self.checkParam(widget, 'show', 'tree', expected=('tree',)) + self.checkParam(widget, 'show', 'headings', expected=('headings',)) + + @requires_tk(8, 7) + def test_configure_striped(self): + widget = self.create() + self.checkBooleanParam(widget, 'striped') + + @requires_tk(8, 7) + def test_configure_titlecolumns(self): + widget = self.create() + self.checkIntegerParam(widget, 'titlecolumns', 0, 1, 5) + self.checkInvalidParam(widget, 'titlecolumns', -2) + + @requires_tk(8, 7) + def test_configure_titleitems(self): + widget = self.create() + self.checkIntegerParam(widget, 'titleitems', 0, 1, 5) + self.checkInvalidParam(widget, 'titleitems', -2) + + def test_bbox(self): + self.tv.pack() + self.assertEqual(self.tv.bbox(''), '') + self.tv.update() + + item_id = self.tv.insert('', 'end') + children = self.tv.get_children() + self.assertTrue(children) + + bbox = self.tv.bbox(children[0]) + self.assertIsBoundingBox(bbox) + + # compare width in bboxes + self.tv['columns'] = ['test'] + self.tv.column('test', width=50) + bbox_column0 = self.tv.bbox(children[0], 0) + root_width = self.tv.column('#0', width=None) + if not self.wantobjects: + root_width = int(root_width) + self.assertEqual(bbox_column0[0], bbox[0] + root_width) + + # verify that bbox of a closed item is the empty string + child1 = self.tv.insert(item_id, 'end') + self.assertEqual(self.tv.bbox(child1), '') + + def test_children(self): + # no children yet, should get an empty tuple + self.assertEqual(self.tv.get_children(), ()) + + item_id = self.tv.insert('', 'end') + self.assertIsInstance(self.tv.get_children(), tuple) + self.assertEqual(self.tv.get_children()[0], item_id) + + # add item_id and child3 as children of child2 + child2 = self.tv.insert('', 'end') + child3 = self.tv.insert('', 'end') + self.tv.set_children(child2, item_id, child3) + self.assertEqual(self.tv.get_children(child2), (item_id, child3)) + + # child3 has child2 as parent, thus trying to set child2 as a children + # of child3 should result in an error + self.assertRaises(tkinter.TclError, + self.tv.set_children, child3, child2) + + # remove child2 children + self.tv.set_children(child2) + self.assertEqual(self.tv.get_children(child2), ()) + + # remove root's children + self.tv.set_children('') + self.assertEqual(self.tv.get_children(), ()) + + def test_column(self): + # return a dict with all options/values + self.assertIsInstance(self.tv.column('#0'), dict) + # return a single value of the given option + if self.wantobjects: + self.assertIsInstance(self.tv.column('#0', width=None), int) + # set a new value for an option + self.tv.column('#0', width=10) + # testing new way to get option value + self.assertEqual(self.tv.column('#0', 'width'), + 10 if self.wantobjects else '10') + self.assertEqual(self.tv.column('#0', width=None), + 10 if self.wantobjects else '10') + # check read-only option + self.assertRaises(tkinter.TclError, self.tv.column, '#0', id='X') + + self.assertRaises(tkinter.TclError, self.tv.column, 'invalid') + invalid_kws = [ + {'unknown_option': 'some value'}, {'stretch': 'wrong'}, + {'anchor': 'wrong'}, {'width': 'wrong'}, {'minwidth': 'wrong'} + ] + for kw in invalid_kws: + self.assertRaises(tkinter.TclError, self.tv.column, '#0', + **kw) + + def test_delete(self): + self.assertRaises(tkinter.TclError, self.tv.delete, '#0') + + item_id = self.tv.insert('', 'end') + item2 = self.tv.insert(item_id, 'end') + self.assertEqual(self.tv.get_children(), (item_id, )) + self.assertEqual(self.tv.get_children(item_id), (item2, )) + + self.tv.delete(item_id) + self.assertFalse(self.tv.get_children()) + + # reattach should fail + self.assertRaises(tkinter.TclError, + self.tv.reattach, item_id, '', 'end') + + # test multiple item delete + item1 = self.tv.insert('', 'end') + item2 = self.tv.insert('', 'end') + self.assertEqual(self.tv.get_children(), (item1, item2)) + + self.tv.delete(item1, item2) + self.assertFalse(self.tv.get_children()) + + def test_detach_reattach(self): + item_id = self.tv.insert('', 'end') + item2 = self.tv.insert(item_id, 'end') + + # calling detach without items is valid, although it does nothing + prev = self.tv.get_children() + self.tv.detach() # this should do nothing + self.assertEqual(prev, self.tv.get_children()) + + self.assertEqual(self.tv.get_children(), (item_id, )) + self.assertEqual(self.tv.get_children(item_id), (item2, )) + + # detach item with children + self.tv.detach(item_id) + self.assertFalse(self.tv.get_children()) + + # reattach item with children + self.tv.reattach(item_id, '', 'end') + self.assertEqual(self.tv.get_children(), (item_id, )) + self.assertEqual(self.tv.get_children(item_id), (item2, )) + + # move a children to the root + self.tv.move(item2, '', 'end') + self.assertEqual(self.tv.get_children(), (item_id, item2)) + self.assertEqual(self.tv.get_children(item_id), ()) + + # bad values + self.assertRaises(tkinter.TclError, + self.tv.reattach, 'nonexistent', '', 'end') + self.assertRaises(tkinter.TclError, + self.tv.detach, 'nonexistent') + self.assertRaises(tkinter.TclError, + self.tv.reattach, item2, 'otherparent', 'end') + self.assertRaises(tkinter.TclError, + self.tv.reattach, item2, '', 'invalid') + + # multiple detach + self.tv.detach(item_id, item2) + self.assertEqual(self.tv.get_children(), ()) + self.assertEqual(self.tv.get_children(item_id), ()) + + def test_exists(self): + self.assertIs(self.tv.exists('something'), False) + self.assertIs(self.tv.exists(''), True) + self.assertIs(self.tv.exists({}), False) + + # the following will make a tk.call equivalent to + # tk.call(treeview, "exists") which should result in an error + # in the tcl interpreter since tk requires an item. + self.assertRaises(tkinter.TclError, self.tv.exists, None) + + def test_parent(self): + a = self.tv.insert('', 'end') + b = self.tv.insert(a, 'end') + self.assertEqual(self.tv.parent(b), a) + self.assertEqual(self.tv.parent(a), '') + self.assertRaises(tkinter.TclError, self.tv.parent, 'nonexistent') + + def test_next_prev(self): + a = self.tv.insert('', 'end') + b = self.tv.insert('', 'end') + c = self.tv.insert('', 'end') + self.assertEqual(self.tv.next(a), b) + self.assertEqual(self.tv.next(b), c) + self.assertEqual(self.tv.next(c), '') + self.assertEqual(self.tv.prev(c), b) + self.assertEqual(self.tv.prev(b), a) + self.assertEqual(self.tv.prev(a), '') + self.assertRaises(tkinter.TclError, self.tv.next, 'nonexistent') + self.assertRaises(tkinter.TclError, self.tv.prev, 'nonexistent') + + def test_see(self): + a = self.tv.insert('', 'end') + b = self.tv.insert(a, 'end') + # see() opens all of the item's ancestors. + self.assertFalse(self.tv.tk.getboolean(self.tv.item(a, 'open'))) + self.tv.see(b) + self.assertTrue(self.tv.tk.getboolean(self.tv.item(a, 'open'))) + self.assertRaises(tkinter.TclError, self.tv.see, 'nonexistent') + + def test_identify_element(self): + self.tv.pack() + self.tv.wait_visibility() + parent = self.tv.insert('', 'end', text='parent') + self.tv.insert(parent, 'end', text='child') + self.tv.update() + x, y, w, h = self.tv.bbox(parent) + # The Treeitem.indicator element is packed at the left of the row in + # the Item layout on every platform and theme. + element = self.tv.identify_element(x + 8, y + h // 2) + self.assertRegex(element, r'.*indicator\z') + # The empty string is returned outside the widget. + self.assertEqual(self.tv.identify_element(-1, -1), '') + self.assertRaises(tkinter.TclError, self.tv.identify_element, None, 5) + + def test_focus(self): + # nothing is focused right now + self.assertEqual(self.tv.focus(), '') + + item1 = self.tv.insert('', 'end') + self.tv.focus(item1) + self.assertEqual(self.tv.focus(), item1) + + self.tv.delete(item1) + self.assertEqual(self.tv.focus(), '') + + # try focusing inexistent item + self.assertRaises(tkinter.TclError, self.tv.focus, 'hi') + + def test_heading(self): + # check a dict is returned + self.assertIsInstance(self.tv.heading('#0'), dict) + + # check a value is returned + self.tv.heading('#0', text='hi') + self.assertEqual(self.tv.heading('#0', 'text'), 'hi') + self.assertEqual(self.tv.heading('#0', text=None), 'hi') + + # invalid option + self.assertRaises(tkinter.TclError, self.tv.heading, '#0', + background=None) + # invalid value + self.assertRaises(tkinter.TclError, self.tv.heading, '#0', + anchor=1) + + def test_heading_callback(self): + def simulate_heading_click(x, y): + if tk_version >= (8, 6): + self.assertEqual(self.tv.identify_column(x), '#0') + self.assertEqual(self.tv.identify_region(x, y), 'heading') + simulate_mouse_click(self.tv, x, y) + self.tv.update() + + success = [] # no success for now + + self.tv.pack() + self.tv.heading('#0', command=lambda: success.append(True)) + self.tv.column('#0', width=100) + self.tv.update() + + # assuming that the coords (5, 5) fall into heading #0 + simulate_heading_click(5, 5) + if not success: + self.fail("The command associated to the treeview heading wasn't " + "invoked.") + + success = [] + commands = self.tv.master._tclCommands + self.tv.heading('#0', command=str(self.tv.heading('#0', command=None))) + self.assertEqual(commands, self.tv.master._tclCommands) + # Click elsewhere first, so the second heading click is not reported + # as a double click (which does not invoke the command). + simulate_mouse_click(self.tv, 5, 50) + simulate_heading_click(5, 5) + if not success: + self.fail("The command associated to the treeview heading wasn't " + "invoked.") + + # XXX The following raises an error in a tcl interpreter, but not in + # Python + #self.tv.heading('#0', command='I dont exist') + #simulate_heading_click(5, 5) + + def test_index(self): + # item 'what' doesn't exist + self.assertRaises(tkinter.TclError, self.tv.index, 'what') + + self.assertEqual(self.tv.index(''), 0) + + item1 = self.tv.insert('', 'end') + item2 = self.tv.insert('', 'end') + c1 = self.tv.insert(item1, 'end') + c2 = self.tv.insert(item1, 'end') + self.assertEqual(self.tv.index(item1), 0) + self.assertEqual(self.tv.index(c1), 0) + self.assertEqual(self.tv.index(c2), 1) + self.assertEqual(self.tv.index(item2), 1) + + self.tv.move(item2, '', 0) + self.assertEqual(self.tv.index(item2), 0) + self.assertEqual(self.tv.index(item1), 1) + + # check that index still works even after its parent and siblings + # have been detached + self.tv.detach(item1) + self.assertEqual(self.tv.index(c2), 1) + self.tv.detach(c1) + self.assertEqual(self.tv.index(c2), 0) + + # but it fails after item has been deleted + self.tv.delete(item1) + self.assertRaises(tkinter.TclError, self.tv.index, c2) + + def test_insert_item(self): + # parent 'none' doesn't exist + self.assertRaises(tkinter.TclError, self.tv.insert, 'none', 'end') + + # open values + self.assertRaises(tkinter.TclError, self.tv.insert, '', 'end', + open='') + self.assertRaises(tkinter.TclError, self.tv.insert, '', 'end', + open='please') + self.assertFalse(self.tv.delete(self.tv.insert('', 'end', open=True))) + self.assertFalse(self.tv.delete(self.tv.insert('', 'end', open=False))) + + # invalid index + self.assertRaises(tkinter.TclError, self.tv.insert, '', 'middle') + + # trying to duplicate item id is invalid + itemid = self.tv.insert('', 'end', 'first-item') + self.assertEqual(itemid, 'first-item') + self.assertRaises(tkinter.TclError, self.tv.insert, '', 'end', + 'first-item') + self.assertRaises(tkinter.TclError, self.tv.insert, '', 'end', + MockTclObj('first-item')) + + # unicode values + value = '\xe1ba' + item = self.tv.insert('', 'end', values=(value, )) + self.assertEqual(self.tv.item(item, 'values'), + (value,) if self.wantobjects else value) + self.assertEqual(self.tv.item(item, values=None), + (value,) if self.wantobjects else value) + + self.tv.item(item, values=self.root.splitlist(self.tv.item(item, values=None))) + self.assertEqual(self.tv.item(item, values=None), + (value,) if self.wantobjects else value) + + self.assertIsInstance(self.tv.item(item), dict) + + # erase item values + self.tv.item(item, values='') + self.assertFalse(self.tv.item(item, values=None)) + + # item tags + item = self.tv.insert('', 'end', tags=[1, 2, value]) + self.assertEqual(self.tv.item(item, tags=None), + ('1', '2', value) if self.wantobjects else + '1 2 %s' % value) + self.tv.item(item, tags=[]) + self.assertFalse(self.tv.item(item, tags=None)) + self.tv.item(item, tags=(1, 2)) + self.assertEqual(self.tv.item(item, tags=None), + ('1', '2') if self.wantobjects else '1 2') + + # values with spaces + item = self.tv.insert('', 'end', values=('a b c', + '%s %s' % (value, value))) + self.assertEqual(self.tv.item(item, values=None), + ('a b c', '%s %s' % (value, value)) if self.wantobjects else + '{a b c} {%s %s}' % (value, value)) + + # text + self.assertEqual(self.tv.item( + self.tv.insert('', 'end', text="Label here"), text=None), + "Label here") + self.assertEqual(self.tv.item( + self.tv.insert('', 'end', text=value), text=None), + value) + + # test for values which are not None + keep_type = self.wantobjects and tk_version >= (9, 1) + itemid = self.tv.insert('', 'end', 0) + self.assertEqual(itemid, 0 if keep_type else '0') + itemid = self.tv.insert('', 'end', 0.0) + self.assertEqual(itemid, 0.0 if keep_type else '0.0') + # this is because False resolves to 0 and element with 0 iid is already present + self.assertRaises(tkinter.TclError, self.tv.insert, '', 'end', False) + self.assertRaises(tkinter.TclError, self.tv.insert, '', 'end', '') + + def test_selection(self): + self.assertRaises(TypeError, self.tv.selection, 'spam') + # item 'none' doesn't exist + self.assertRaises(tkinter.TclError, self.tv.selection_set, 'none') + self.assertRaises(tkinter.TclError, self.tv.selection_add, 'none') + self.assertRaises(tkinter.TclError, self.tv.selection_remove, 'none') + self.assertRaises(tkinter.TclError, self.tv.selection_toggle, 'none') + + item1 = self.tv.insert('', 'end') + item2 = self.tv.insert('', 'end') + c1 = self.tv.insert(item1, 'end') + c2 = self.tv.insert(item1, 'end') + c3 = self.tv.insert(item1, 'end') + self.assertEqual(self.tv.selection(), ()) + + self.tv.selection_set(c1, item2) + self.assertEqual(self.tv.selection(), (c1, item2)) + self.tv.selection_set(c2) + self.assertEqual(self.tv.selection(), (c2,)) + + self.tv.selection_add(c1, item2) + self.assertEqual(self.tv.selection(), (c1, c2, item2)) + self.tv.selection_add(item1) + self.assertEqual(self.tv.selection(), (item1, c1, c2, item2)) + self.tv.selection_add() + self.assertEqual(self.tv.selection(), (item1, c1, c2, item2)) + + self.tv.selection_remove(item1, c3) + self.assertEqual(self.tv.selection(), (c1, c2, item2)) + self.tv.selection_remove(c2) + self.assertEqual(self.tv.selection(), (c1, item2)) + self.tv.selection_remove() + self.assertEqual(self.tv.selection(), (c1, item2)) + + self.tv.selection_toggle(c1, c3) + self.assertEqual(self.tv.selection(), (c3, item2)) + self.tv.selection_toggle(item2) + self.assertEqual(self.tv.selection(), (c3,)) + self.tv.selection_toggle() + self.assertEqual(self.tv.selection(), (c3,)) + + self.tv.insert('', 'end', id='with spaces') + self.tv.selection_set('with spaces') + self.assertEqual(self.tv.selection(), ('with spaces',)) + + self.tv.insert('', 'end', id='{brace') + self.tv.selection_set('{brace') + self.assertEqual(self.tv.selection(), ('{brace',)) + + self.tv.insert('', 'end', id='unicode\u20ac') + self.tv.selection_set('unicode\u20ac') + self.assertEqual(self.tv.selection(), ('unicode\u20ac',)) + + self.tv.insert('', 'end', id=b'bytes\xe2\x82\xac') + self.tv.selection_set(b'bytes\xe2\x82\xac') + self.assertEqual(self.tv.selection(), + (b'bytes\xe2\x82\xac',) + if self.wantobjects and tk_version >= (9, 1) + else ('bytes\xe2\x82\xac',)) + + self.tv.selection_set() + self.assertEqual(self.tv.selection(), ()) + + # Old interface + self.tv.selection_set((c1, item2)) + self.assertEqual(self.tv.selection(), (c1, item2)) + self.tv.selection_add((c1, item1)) + self.assertEqual(self.tv.selection(), (item1, c1, item2)) + self.tv.selection_remove((item1, c3)) + self.assertEqual(self.tv.selection(), (c1, item2)) + self.tv.selection_toggle((c1, c3)) + self.assertEqual(self.tv.selection(), (c3, item2)) + + def test_virtual_events(self): + # Keyboard navigation fires the <>, <> + # and <> virtual events. + parent = self.tv.insert('', 'end') + self.tv.insert(parent, 'end') + item2 = self.tv.insert('', 'end') + self.tv.pack() + self.require_mapped(self.tv) + selects, opens, closes = [], [], [] + self.tv.bind('<>', + lambda e: selects.append(self.tv.selection())) + self.tv.bind('<>', lambda e: opens.append(self.tv.focus())) + self.tv.bind('<>', lambda e: closes.append(self.tv.focus())) + self.tv.focus(parent) + self.tv.selection_set(parent) + self.tv.update() + + # Force the focus right before generating the event: the window + # manager can take it back. + self.tv.focus_force() + self.tv.event_generate('') # Open the focused parent. + self.tv.update() + self.assertEqual(opens, [parent]) + + self.tv.focus_force() + self.tv.event_generate('') # Close it again. + self.tv.update() + self.assertEqual(closes, [parent]) + + self.tv.focus_force() + self.tv.event_generate('') # Move the selection. + self.tv.update() + self.assertEqual(self.tv.selection(), (item2,)) + self.assertIn((item2,), selects) + + def test_set(self): + self.tv['columns'] = ['A', 'B'] + item = self.tv.insert('', 'end', values=['a', 'b']) + values = self.tv.set(item) + if tk_version >= (9, 1): + self.assertEqual(values.pop('#0'), '') + self.assertEqual(values, {'A': 'a', 'B': 'b'}) + + self.tv.set(item, 'B', 'a') + self.assertEqual(self.tv.item(item, values=None), + ('a', 'a') if self.wantobjects else 'a a') + + self.tv['columns'] = ['B'] + values = self.tv.set(item) + values.pop('#0', None) + self.assertEqual(values, {'B': 'a'}) + + self.tv.set(item, 'B', 'b') + self.assertEqual(self.tv.set(item, column='B'), 'b') + self.assertEqual(self.tv.item(item, values=None), + ('b', 'a') if self.wantobjects else 'b a') + + self.tv.set(item, 'B', 123) + self.assertEqual(self.tv.set(item, 'B'), + 123 if self.wantobjects else '123') + self.assertEqual(self.tv.item(item, values=None), + (123, 'a') if self.wantobjects else '123 a') + values = self.tv.set(item) + values.pop('#0', None) + self.assertEqual(values, + {'B': 123} if self.wantobjects else {'B': '123'}) + + # inexistent column + self.assertRaises(tkinter.TclError, self.tv.set, item, 'A') + self.assertRaises(tkinter.TclError, self.tv.set, item, 'A', 'b') + + # inexistent item + self.assertRaises(tkinter.TclError, self.tv.set, 'notme') + + def test_tag_bind(self): + events = [] + item1 = self.tv.insert('', 'end', tags=['call']) + item2 = self.tv.insert('', 'end', tags=['call']) + self.tv.tag_bind('call', '', + lambda evt: events.append(1)) + self.tv.tag_bind('call', '', + lambda evt: events.append(2)) + + self.tv.pack() + self.tv.update() + + # Find the y coordinate of each item. Scan the whole height of the + # widget rather than a fixed pixel range, since the row height grows + # with the display scaling. + pos_y = set() + found = set() + for y in range(self.tv.winfo_height()): + if len(found) == 2: # item1 and item2 already found + break + item_id = self.tv.identify_row(y) + if item_id and item_id not in found: + pos_y.add(y) + found.add(item_id) + + self.assertEqual(len(pos_y), 2) # item1 and item2 y pos + for y in pos_y: + simulate_mouse_click(self.tv, 0, y) + + # by now there should be 4 things in the events list, since each + # item had a bind for two events that were simulated above + self.assertEqual(len(events), 4) + for evt in zip(events[::2], events[1::2]): + self.assertEqual(evt, (1, 2)) + + def test_tag_configure(self): + # Just testing parameter passing for now + self.assertRaises(TypeError, self.tv.tag_configure) + self.assertRaises(tkinter.TclError, self.tv.tag_configure, + 'test', sky='blue') + self.tv.tag_configure('test', foreground='blue') + self.assertEqual(str(self.tv.tag_configure('test', 'foreground')), + 'blue') + self.assertEqual(str(self.tv.tag_configure('test', foreground=None)), + 'blue') + self.assertIsInstance(self.tv.tag_configure('test'), dict) + + def test_tag_has(self): + item1 = self.tv.insert('', 'end', text='Item 1', tags=['tag1']) + item2 = self.tv.insert('', 'end', text='Item 2', tags=['tag2']) + self.assertRaises(TypeError, self.tv.tag_has) + self.assertRaises(TclError, self.tv.tag_has, 'tag1', 'non-existing') + self.assertTrue(self.tv.tag_has('tag1', item1)) + self.assertFalse(self.tv.tag_has('tag1', item2)) + self.assertFalse(self.tv.tag_has('tag2', item1)) + self.assertTrue(self.tv.tag_has('tag2', item2)) + self.assertFalse(self.tv.tag_has('tag3', item1)) + self.assertFalse(self.tv.tag_has('tag3', item2)) + self.assertEqual(self.tv.tag_has('tag1'), (item1,)) + self.assertEqual(self.tv.tag_has('tag2'), (item2,)) + self.assertEqual(self.tv.tag_has('tag3'), ()) + + +@add_configure_tests(StandardTtkOptionsTests) +class SeparatorTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'class', 'cursor', 'orient', 'style', 'takefocus', + # 'state'? + ) + _rounds_pixels = False + _clipped = {} + default_orient = 'horizontal' + + def create(self, **kwargs): + return ttk.Separator(self.root, **kwargs) + + +@add_configure_tests(StandardTtkOptionsTests) +class SizegripTest(AbstractWidgetTest, unittest.TestCase): + OPTIONS = ( + 'class', 'cursor', 'style', 'takefocus', + # 'state'? + ) + _rounds_pixels = False + _clipped = {} + + def create(self, **kwargs): + return ttk.Sizegrip(self.root, **kwargs) + + +class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): + + def test_frame(self): + self._test_widget(ttk.Frame) + + def test_label(self): + self._test_widget(ttk.Label) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_ttk_textonly.py b/Lib/test/test_ttk_textonly.py new file mode 100644 index 00000000000..e6525c4d6c6 --- /dev/null +++ b/Lib/test/test_ttk_textonly.py @@ -0,0 +1,481 @@ +from test.support import import_helper + +# Skip this test if _tkinter does not exist. +import_helper.import_module('_tkinter') + +import unittest +from tkinter import ttk + + +class MockTkApp: + + def splitlist(self, arg): + if isinstance(arg, tuple): + return arg + return arg.split(':') + + def wantobjects(self): + return True + + +class MockTclObj(object): + typename = 'test' + + def __init__(self, val): + self.val = val + + def __str__(self): + return str(self.val) + + +class MockStateSpec(object): + typename = 'StateSpec' + + def __init__(self, *args): + self.val = args + + def __str__(self): + return ' '.join(self.val) + + +class InternalFunctionsTest(unittest.TestCase): + + def test_format_optdict(self): + def check_against(fmt_opts, result): + for i in range(0, len(fmt_opts), 2): + self.assertEqual(result.pop(fmt_opts[i]), fmt_opts[i + 1]) + if result: + self.fail("result still got elements: %s" % result) + + # passing an empty dict should return an empty object (tuple here) + self.assertFalse(ttk._format_optdict({})) + + # check list formatting + check_against( + ttk._format_optdict({'fg': 'blue', 'padding': [1, 2, 3, 4]}), + {'-fg': 'blue', '-padding': '1 2 3 4'}) + + # check tuple formatting (same as list) + check_against( + ttk._format_optdict({'test': (1, 2, '', 0)}), + {'-test': '1 2 {} 0'}) + + # check untouched values + check_against( + ttk._format_optdict({'test': {'left': 'as is'}}), + {'-test': {'left': 'as is'}}) + + # check script formatting + check_against( + ttk._format_optdict( + {'test': [1, -1, '', '2m', 0], 'test2': 3, + 'test3': '', 'test4': 'abc def', + 'test5': '"abc"', 'test6': '{}', + 'test7': '} -spam {'}, script=True), + {'-test': '{1 -1 {} 2m 0}', '-test2': '3', + '-test3': '{}', '-test4': '{abc def}', + '-test5': '{"abc"}', '-test6': r'\{\}', + '-test7': r'\}\ -spam\ \{'}) + + opts = {'αβγ': True, 'á': False} + orig_opts = opts.copy() + # check if giving unicode keys is fine + check_against(ttk._format_optdict(opts), {'-αβγ': True, '-á': False}) + # opts should remain unchanged + self.assertEqual(opts, orig_opts) + + # passing values with spaces inside a tuple/list + check_against( + ttk._format_optdict( + {'option': ('one two', 'three')}), + {'-option': '{one two} three'}) + check_against( + ttk._format_optdict( + {'option': ('one\ttwo', 'three')}), + {'-option': '{one\ttwo} three'}) + + # passing empty strings inside a tuple/list + check_against( + ttk._format_optdict( + {'option': ('', 'one')}), + {'-option': '{} one'}) + + # passing values with braces inside a tuple/list + check_against( + ttk._format_optdict( + {'option': ('one} {two', 'three')}), + {'-option': r'one\}\ \{two three'}) + + # passing quoted strings inside a tuple/list + check_against( + ttk._format_optdict( + {'option': ('"one"', 'two')}), + {'-option': '{"one"} two'}) + check_against( + ttk._format_optdict( + {'option': ('{one}', 'two')}), + {'-option': r'\{one\} two'}) + + # ignore an option + amount_opts = len(ttk._format_optdict(opts, ignore=('á'))) / 2 + self.assertEqual(amount_opts, len(opts) - 1) + + # ignore non-existing options + amount_opts = len(ttk._format_optdict(opts, ignore=('á', 'b'))) / 2 + self.assertEqual(amount_opts, len(opts) - 1) + + # ignore every option + self.assertFalse(ttk._format_optdict(opts, ignore=list(opts.keys()))) + + + def test_format_mapdict(self): + opts = {'a': [('b', 'c', 'val'), ('d', 'otherval'), ('', 'single')]} + result = ttk._format_mapdict(opts) + self.assertEqual(len(result), len(list(opts.keys())) * 2) + self.assertEqual(result, ('-a', '{b c} val d otherval {} single')) + self.assertEqual(ttk._format_mapdict(opts, script=True), + ('-a', '{{b c} val d otherval {} single}')) + + self.assertEqual(ttk._format_mapdict({2: []}), ('-2', '')) + + opts = {'üñíćódè': [('á', 'vãl')]} + result = ttk._format_mapdict(opts) + self.assertEqual(result, ('-üñíćódè', 'á vãl')) + + self.assertEqual(ttk._format_mapdict({'opt': [('value',)]}), + ('-opt', '{} value')) + + # empty states + valid = {'opt': [('', '', 'hi')]} + self.assertEqual(ttk._format_mapdict(valid), ('-opt', '{ } hi')) + + # when passing multiple states, they all must be strings + invalid = {'opt': [(1, 2, 'valid val')]} + self.assertRaises(TypeError, ttk._format_mapdict, invalid) + invalid = {'opt': [([1], '2', 'valid val')]} + self.assertRaises(TypeError, ttk._format_mapdict, invalid) + # but when passing a single state, it can be anything + valid = {'opt': [[1, 'value']]} + self.assertEqual(ttk._format_mapdict(valid), ('-opt', '1 value')) + # special attention to single states which evaluate to False + for stateval in (None, 0, False, '', set()): # just some samples + valid = {'opt': [(stateval, 'value')]} + self.assertEqual(ttk._format_mapdict(valid), + ('-opt', '{} value')) + + # values must be iterable + opts = {'a': None} + self.assertRaises(TypeError, ttk._format_mapdict, opts) + + + def test_format_elemcreate(self): + self.assertTrue(ttk._format_elemcreate(None), (None, ())) + + ## Testing type = image + # image type expects at least an image name, so this should raise + # IndexError since it tries to access the index 0 of an empty tuple + self.assertRaises(IndexError, ttk._format_elemcreate, 'image') + + # don't format returned values as a tcl script + # minimum acceptable for image type + self.assertEqual(ttk._format_elemcreate('image', False, 'test'), + ("test", ())) + # specifying a state spec + self.assertEqual(ttk._format_elemcreate('image', False, 'test', + ('', 'a')), ("test {} a", ())) + # state spec with multiple states + self.assertEqual(ttk._format_elemcreate('image', False, 'test', + ('a', 'b', 'c')), ("test {a b} c", ())) + # state spec and options + self.assertEqual(ttk._format_elemcreate('image', False, 'test', + ('a', 'b'), a='x'), ("test a b", ("-a", "x"))) + # format returned values as a tcl script + # state spec with multiple states and an option with a multivalue + self.assertEqual(ttk._format_elemcreate('image', True, 'test', + ('a', 'b', 'c', 'd'), x=[2, 3]), ("{test {a b c} d}", "-x {2 3}")) + + ## Testing type = vsapi + # vsapi type expects at least a class name and a part_id, so this + # should raise a ValueError since it tries to get two elements from + # an empty tuple + self.assertRaises(ValueError, ttk._format_elemcreate, 'vsapi') + + # don't format returned values as a tcl script + # minimum acceptable for vsapi + self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b'), + ('a', 'b', ('', 1), ())) + # now with a state spec with multiple states + self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b', + [('a', 'b', 'c')]), ('a', 'b', ('a b', 'c'), ())) + # state spec and option + self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b', + [('a', 'b')], opt='x'), ('a', 'b', ('a', 'b'), ("-opt", "x"))) + # format returned values as a tcl script + # state spec with a multivalue and an option + self.assertEqual(ttk._format_elemcreate('vsapi', True, 'a', 'b', + opt='x'), ("a b {{} 1}", "-opt x")) + self.assertEqual(ttk._format_elemcreate('vsapi', True, 'a', 'b', + [('a', 'b', [1, 2])], opt='x'), ("a b {{a b} {1 2}}", "-opt x")) + + # Testing type = from + # from type expects at least a type name + self.assertRaises(IndexError, ttk._format_elemcreate, 'from') + + self.assertEqual(ttk._format_elemcreate('from', False, 'a'), + ('a', ())) + self.assertEqual(ttk._format_elemcreate('from', False, 'a', 'b'), + ('a', ('b',))) + self.assertEqual(ttk._format_elemcreate('from', True, 'a', 'b'), + ('a', 'b')) + + + def test_format_layoutlist(self): + def sample(indent=0, indent_size=2): + return ttk._format_layoutlist( + [('a', {'other': [1, 2, 3], 'children': + [('b', {'children': + [('c', {'children': + [('d', {'nice': 'opt'})], 'something': (1, 2) + })] + })] + })], indent=indent, indent_size=indent_size)[0] + + def sample_expected(indent=0, indent_size=2): + spaces = lambda amount=0: ' ' * (amount + indent) + return ( + "%sa -other {1 2 3} -children {\n" + "%sb -children {\n" + "%sc -something {1 2} -children {\n" + "%sd -nice opt\n" + "%s}\n" + "%s}\n" + "%s}" % (spaces(), spaces(indent_size), + spaces(2 * indent_size), spaces(3 * indent_size), + spaces(2 * indent_size), spaces(indent_size), spaces())) + + # empty layout + self.assertEqual(ttk._format_layoutlist([])[0], '') + + # _format_layoutlist always expects the second item (in every item) + # to act like a dict (except when the value evaluates to False). + self.assertRaises(AttributeError, + ttk._format_layoutlist, [('a', 'b')]) + + smallest = ttk._format_layoutlist([('a', None)], indent=0) + self.assertEqual(smallest, + ttk._format_layoutlist([('a', '')], indent=0)) + self.assertEqual(smallest[0], 'a') + + # testing indentation levels + self.assertEqual(sample(), sample_expected()) + for i in range(4): + self.assertEqual(sample(i), sample_expected(i)) + self.assertEqual(sample(i, i), sample_expected(i, i)) + + # invalid layout format, different kind of exceptions will be + # raised by internal functions + + # plain wrong format + self.assertRaises(ValueError, ttk._format_layoutlist, + ['bad', 'format']) + # will try to use iteritems in the 'bad' string + self.assertRaises(AttributeError, ttk._format_layoutlist, + [('name', 'bad')]) + # bad children formatting + self.assertRaises(ValueError, ttk._format_layoutlist, + [('name', {'children': {'a': None}})]) + + + def test_script_from_settings(self): + # empty options + self.assertFalse(ttk._script_from_settings({'name': + {'configure': None, 'map': None, 'element create': None}})) + + # empty layout + self.assertEqual( + ttk._script_from_settings({'name': {'layout': None}}), + "ttk::style layout name {\nnull\n}") + + configdict = {'αβγ': True, 'á': False} + self.assertTrue( + ttk._script_from_settings({'name': {'configure': configdict}})) + + mapdict = {'üñíćódè': [('á', 'vãl')]} + self.assertTrue( + ttk._script_from_settings({'name': {'map': mapdict}})) + + # invalid image element + self.assertRaises(IndexError, + ttk._script_from_settings, {'name': {'element create': ['image']}}) + + # minimal valid image + self.assertTrue(ttk._script_from_settings({'name': + {'element create': ['image', 'name']}})) + + image = {'thing': {'element create': + ['image', 'name', ('state1', 'state2', 'val')]}} + self.assertEqual(ttk._script_from_settings(image), + "ttk::style element create thing image {name {state1 state2} val} ") + + image['thing']['element create'].append({'opt': 30}) + self.assertEqual(ttk._script_from_settings(image), + "ttk::style element create thing image {name {state1 state2} val} " + "-opt 30") + + image['thing']['element create'][-1]['opt'] = [MockTclObj(3), + MockTclObj('2m')] + self.assertEqual(ttk._script_from_settings(image), + "ttk::style element create thing image {name {state1 state2} val} " + "-opt {3 2m}") + + vsapi = {'pin': {'element create': + ['vsapi', 'EXPLORERBAR', 3, [ + ('pressed', '!selected', 3), + ('active', '!selected', 2), + ('pressed', 'selected', 6), + ('active', 'selected', 5), + ('selected', 4), + ('', 1)]]}} + self.assertEqual(ttk._script_from_settings(vsapi), + "ttk::style element create pin vsapi EXPLORERBAR 3 {" + "{pressed !selected} 3 " + "{active !selected} 2 " + "{pressed selected} 6 " + "{active selected} 5 " + "selected 4 " + "{} 1} ") + + def test_tclobj_to_py(self): + self.assertEqual( + ttk._tclobj_to_py((MockStateSpec('a', 'b'), 'val')), + [('a', 'b', 'val')]) + self.assertEqual( + ttk._tclobj_to_py([MockTclObj('1'), 2, MockTclObj('3m')]), + [1, 2, '3m']) + + + def test_list_from_statespec(self): + def test_it(sspec, value, res_value, states): + self.assertEqual(ttk._list_from_statespec( + (sspec, value)), [states + (res_value, )]) + + states_even = tuple('state%d' % i for i in range(6)) + statespec = MockStateSpec(*states_even) + test_it(statespec, 'val', 'val', states_even) + test_it(statespec, MockTclObj('val'), 'val', states_even) + + states_odd = tuple('state%d' % i for i in range(5)) + statespec = MockStateSpec(*states_odd) + test_it(statespec, 'val', 'val', states_odd) + + test_it(('a', 'b', 'c'), MockTclObj('val'), 'val', ('a', 'b', 'c')) + + + def test_list_from_layouttuple(self): + tk = MockTkApp() + + # empty layout tuple + self.assertFalse(ttk._list_from_layouttuple(tk, ())) + + # shortest layout tuple + self.assertEqual(ttk._list_from_layouttuple(tk, ('name', )), + [('name', {})]) + + # not so interesting ltuple + sample_ltuple = ('name', '-option', 'value') + self.assertEqual(ttk._list_from_layouttuple(tk, sample_ltuple), + [('name', {'option': 'value'})]) + + # empty children + self.assertEqual(ttk._list_from_layouttuple(tk, + ('something', '-children', ())), + [('something', {'children': []})] + ) + + # more interesting ltuple + ltuple = ( + 'name', '-option', 'niceone', '-children', ( + ('otherone', '-children', ( + ('child', )), '-otheropt', 'othervalue' + ) + ) + ) + self.assertEqual(ttk._list_from_layouttuple(tk, ltuple), + [('name', {'option': 'niceone', 'children': + [('otherone', {'otheropt': 'othervalue', 'children': + [('child', {})] + })] + })] + ) + + # bad tuples + self.assertRaises(ValueError, ttk._list_from_layouttuple, tk, + ('name', 'no_minus')) + self.assertRaises(ValueError, ttk._list_from_layouttuple, tk, + ('name', 'no_minus', 'value')) + self.assertRaises(ValueError, ttk._list_from_layouttuple, tk, + ('something', '-children')) # no children + + + def test_val_or_dict(self): + def func(res, opt=None, val=None): + if opt is None: + return res + if val is None: + return "test val" + return (opt, val) + + tk = MockTkApp() + tk.call = func + + self.assertEqual(ttk._val_or_dict(tk, {}, '-test:3'), + {'test': '3'}) + self.assertEqual(ttk._val_or_dict(tk, {}, ('-test', 3)), + {'test': 3}) + + self.assertEqual(ttk._val_or_dict(tk, {'test': None}, 'x:y'), + 'test val') + + self.assertEqual(ttk._val_or_dict(tk, {'test': 3}, 'x:y'), + {'test': 3}) + + + def test_convert_stringval(self): + tests = ( + (0, 0), ('09', 9), ('a', 'a'), ('áÚ', 'áÚ'), ([], '[]'), + (None, 'None') + ) + for orig, expected in tests: + self.assertEqual(ttk._convert_stringval(orig), expected) + + +class TclObjsToPyTest(unittest.TestCase): + + def test_unicode(self): + adict = {'opt': 'välúè'} + self.assertEqual(ttk.tclobjs_to_py(adict), {'opt': 'välúè'}) + + adict['opt'] = MockTclObj(adict['opt']) + self.assertEqual(ttk.tclobjs_to_py(adict), {'opt': 'välúè'}) + + def test_multivalues(self): + adict = {'opt': [1, 2, 3, 4]} + self.assertEqual(ttk.tclobjs_to_py(adict), {'opt': [1, 2, 3, 4]}) + + adict['opt'] = [1, 'xm', 3] + self.assertEqual(ttk.tclobjs_to_py(adict), {'opt': [1, 'xm', 3]}) + + adict['opt'] = (MockStateSpec('a', 'b'), 'válũè') + self.assertEqual(ttk.tclobjs_to_py(adict), + {'opt': [('a', 'b', 'válũè')]}) + + self.assertEqual(ttk.tclobjs_to_py({'x': ['y z']}), + {'x': ['y z']}) + + def test_nosplit(self): + self.assertEqual(ttk.tclobjs_to_py({'text': 'some text'}), + {'text': 'some text'}) + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index 12d2eed8741..c49ce9cdb6f 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -577,6 +577,28 @@ def test_no_animation_calls_update_at_exit(self): s.update.assert_not_called() s.update.assert_called_once() + def test_update_is_not_reentrant(self): + # ondrag(goto) reenters _update() while cv.update() processes events; + # without a guard this recurses without bound (gh-50966). + s = turtle.TurtleScreen(cv=unittest.mock.MagicMock()) + depth = max_depth = 0 + + def reenter(): + nonlocal depth, max_depth + depth += 1 + max_depth = max(max_depth, depth) + if depth < 50: + s._update() # as an event handler would + depth -= 1 + + s.cv.update.reset_mock() # ignore calls made during construction + s.cv.update.side_effect = reenter + s._update() + # cv.update() runs once; reentrant calls only flush idle tasks. + self.assertEqual(s.cv.update.call_count, 1) + self.assertEqual(max_depth, 1) + self.assertTrue(s.cv.update_idletasks.called) + class TestTurtle(unittest.TestCase): def setUp(self): diff --git a/Lib/test/test_type_aliases.py b/Lib/test/test_type_aliases.py index 7deca5779ea..05bc6a80888 100644 --- a/Lib/test/test_type_aliases.py +++ b/Lib/test/test_type_aliases.py @@ -8,6 +8,11 @@ Callable, TypeAliasType, TypeVar, TypeVarTuple, ParamSpec, Unpack, get_args, ) +type GlobalTypeAlias = int + +def get_type_alias(): + type TypeAliasInFunc = str + return TypeAliasInFunc class TypeParamsInvalidTest(unittest.TestCase): def test_name_collisions(self): @@ -70,6 +75,8 @@ def inner[B](self): class TypeParamsAliasValueTest(unittest.TestCase): + type TypeAliasInClass = dict + def test_alias_value_01(self): type TA1 = int @@ -142,6 +149,38 @@ def test_subscripting(self): self.assertIs(specialized2.__origin__, VeryGeneric) self.assertEqual(specialized2.__args__, (int, str, float, [bool, range])) + def test___name__(self): + type TypeAliasLocal = GlobalTypeAlias + + self.assertEqual(GlobalTypeAlias.__name__, 'GlobalTypeAlias') + self.assertEqual(get_type_alias().__name__, 'TypeAliasInFunc') + self.assertEqual(self.TypeAliasInClass.__name__, 'TypeAliasInClass') + self.assertEqual(TypeAliasLocal.__name__, 'TypeAliasLocal') + + with self.assertRaisesRegex( + AttributeError, + "readonly attribute", + ): + setattr(TypeAliasLocal, '__name__', 'TA') + + def test___qualname__(self): + type TypeAliasLocal = GlobalTypeAlias + + self.assertEqual(GlobalTypeAlias.__qualname__, + 'GlobalTypeAlias') + self.assertEqual(get_type_alias().__qualname__, + 'get_type_alias..TypeAliasInFunc') + self.assertEqual(self.TypeAliasInClass.__qualname__, + 'TypeParamsAliasValueTest.TypeAliasInClass') + self.assertEqual(TypeAliasLocal.__qualname__, + 'TypeParamsAliasValueTest.test___qualname__..TypeAliasLocal') + + with self.assertRaisesRegex( + AttributeError, + "readonly attribute", + ): + setattr(TypeAliasLocal, '__qualname__', 'TA') + def test_repr(self): type Simple = int self.assertEqual(repr(Simple), Simple.__qualname__) @@ -195,15 +234,25 @@ class TypeAliasConstructorTest(unittest.TestCase): def test_basic(self): TA = TypeAliasType("TA", int) self.assertEqual(TA.__name__, "TA") + self.assertEqual(TA.__qualname__, "TA") self.assertIs(TA.__value__, int) self.assertEqual(TA.__type_params__, ()) self.assertEqual(TA.__module__, __name__) + def test_with_qualname(self): + TA = TypeAliasType("TA", str, qualname="Class.TA") + self.assertEqual(TA.__name__, "TA") + self.assertEqual(TA.__qualname__, "Class.TA") + self.assertIs(TA.__value__, str) + self.assertEqual(TA.__type_params__, ()) + self.assertEqual(TA.__module__, __name__) + def test_attributes_with_exec(self): ns = {} exec("type TA = int", ns, ns) TA = ns["TA"] self.assertEqual(TA.__name__, "TA") + self.assertEqual(TA.__qualname__, "TA") self.assertIs(TA.__value__, int) self.assertEqual(TA.__type_params__, ()) self.assertIs(TA.__module__, None) @@ -212,6 +261,7 @@ def test_generic(self): T = TypeVar("T") TA = TypeAliasType("TA", list[T], type_params=(T,)) self.assertEqual(TA.__name__, "TA") + self.assertEqual(TA.__qualname__, "TA") self.assertEqual(TA.__value__, list[T]) self.assertEqual(TA.__type_params__, (T,)) self.assertEqual(TA.__module__, __name__) @@ -220,6 +270,7 @@ def test_generic(self): def test_not_generic(self): TA = TypeAliasType("TA", list[int], type_params=()) self.assertEqual(TA.__name__, "TA") + self.assertEqual(TA.__qualname__, "TA") self.assertEqual(TA.__value__, list[int]) self.assertEqual(TA.__type_params__, ()) self.assertEqual(TA.__module__, __name__) @@ -270,8 +321,9 @@ def test_expects_type_like(self): TypeAliasType("A", int, type_params=(T, 2)) def test_keywords(self): - TA = TypeAliasType(name="TA", value=int) + TA = TypeAliasType(name="TA", value=int, type_params=(), qualname=None) self.assertEqual(TA.__name__, "TA") + self.assertEqual(TA.__qualname__, "TA") self.assertIs(TA.__value__, int) self.assertEqual(TA.__type_params__, ()) self.assertEqual(TA.__module__, __name__) @@ -285,6 +337,8 @@ def test_errors(self): TypeAliasType("TA", list, ()) with self.assertRaises(TypeError): TypeAliasType("TA", list, type_params=42) + with self.assertRaises(TypeError): + TypeAliasType("TA", list, qualname=range(5)) class TypeAliasTypeTest(unittest.TestCase): @@ -318,6 +372,8 @@ def test_module(self): mod_generics_cache.__name__) self.assertEqual(mod_generics_cache.OldStyle.__module__, mod_generics_cache.__name__) + Alias.__module__ = "ham.spam.eggs" + self.assertEqual(Alias.__module__, "ham.spam.eggs") def test_unpack(self): type Alias = tuple[int, int] diff --git a/Lib/test/test_type_cache.py b/Lib/test/test_type_cache.py index 7469a1047f8..849a2afd8ed 100644 --- a/Lib/test/test_type_cache.py +++ b/Lib/test/test_type_cache.py @@ -1,9 +1,10 @@ """ Tests for the internal type cache in CPython. """ +import collections.abc import dis import unittest import warnings from test import support -from test.support import import_helper, requires_specialization, requires_specialization_ft +from test.support import import_helper, requires_specialization try: from sys import _clear_type_cache except ImportError: @@ -114,6 +115,25 @@ class HolderSub(Holder): Holder.set_value() HolderSub.value + def test_abc_register_invalidates_subclass_versions(self): + class Parent: + pass + + class Child(Parent): + pass + + type_assign_version(Parent) + type_assign_version(Child) + parent_version = type_get_version(Parent) + child_version = type_get_version(Child) + if parent_version == 0 or child_version == 0: + self.skipTest("Could not assign valid type versions") + + collections.abc.Mapping.register(Parent) + + self.assertEqual(type_get_version(Parent), 0) + self.assertEqual(type_get_version(Child), 0) + @support.cpython_only class TypeCacheWithSpecializationTests(unittest.TestCase): def tearDown(self): @@ -219,7 +239,7 @@ def store_bar_2(type_): self._check_specialization(store_bar_2, B(), "STORE_ATTR", should_specialize=False) - @requires_specialization_ft + @requires_specialization def test_class_call_specialization_user_type(self): class F: def __init__(self): diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index d3a0ec531f0..93e17de7534 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -13,6 +13,7 @@ import pickle import re import sys +import warnings from unittest import TestCase, main from unittest.mock import patch from copy import copy, deepcopy @@ -28,32 +29,31 @@ from typing import assert_type, cast, runtime_checkable from typing import get_type_hints from typing import get_origin, get_args, get_protocol_members -from typing import override +from typing import override, disjoint_base from typing import is_typeddict, is_protocol from typing import reveal_type from typing import dataclass_transform -from typing import no_type_check, no_type_check_decorator +from typing import no_type_check from typing import Type -from typing import NamedTuple, NotRequired, Required, ReadOnly, TypedDict +from typing import NamedTuple, NotRequired, Required, ReadOnly, TypedDict, NoExtraItems from typing import IO, TextIO, BinaryIO from typing import Pattern, Match from typing import Annotated, ForwardRef from typing import Self, LiteralString from typing import TypeAlias from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs -from typing import TypeGuard, TypeIs, NoDefault +from typing import TypeForm, TypeGuard, TypeIs, NoDefault import abc import textwrap import typing import weakref -import warnings import types from test.support import ( - captured_stderr, cpython_only, infinite_recursion, requires_docstrings, import_helper, run_code, - EqualToForwardRef, - exceeds_recursion_limit, run_with_limited_c_stack, - skip_wasi_stack_overflow, skip_emscripten_stack_overflow, + captured_stderr, cpython_only, requires_docstrings, import_helper, run_code, + subTests, EqualToForwardRef, + exceeds_recursion_limit, skip_if_huge_c_stack, skip_wasi_stack_overflow, + skip_emscripten_stack_overflow, ) from test.typinganndata import ( ann_module695, mod_generics_cache, _typed_dict_helper, @@ -784,7 +784,7 @@ def test_typevartuple_none(self): self.assertIs(U_None.__default__, None) self.assertIs(U_None.has_default(), True) - class X[**Ts]: ... + class X[*Ts]: ... Ts, = X.__type_params__ self.assertIs(Ts.__default__, NoDefault) self.assertIs(Ts.has_default(), False) @@ -1293,6 +1293,62 @@ def test_cannot_call_instance(self): with self.assertRaises(TypeError): Ts() + def test_default_variance(self): + Ts = TypeVarTuple('Ts') + self.assertIs(Ts.__covariant__, False) + self.assertIs(Ts.__contravariant__, False) + self.assertIs(Ts.__infer_variance__, False) + self.assertIsNone(Ts.__bound__) + + def test_covariant(self): + Ts_co = TypeVarTuple('Ts_co', covariant=True) + self.assertIs(Ts_co.__covariant__, True) + self.assertIs(Ts_co.__contravariant__, False) + self.assertIs(Ts_co.__infer_variance__, False) + + def test_contravariant(self): + Ts_contra = TypeVarTuple('Ts_contra', contravariant=True) + self.assertIs(Ts_contra.__covariant__, False) + self.assertIs(Ts_contra.__contravariant__, True) + self.assertIs(Ts_contra.__infer_variance__, False) + + def test_infer_variance(self): + Ts = TypeVarTuple('Ts', infer_variance=True) + self.assertIs(Ts.__covariant__, False) + self.assertIs(Ts.__contravariant__, False) + self.assertIs(Ts.__infer_variance__, True) + + def test_bound(self): + Ts_bound = TypeVarTuple('Ts_bound', bound=int) + self.assertIs(Ts_bound.__bound__, int) + Ts_tuple_bound = TypeVarTuple('Ts_tuple_bound', bound=(int, str)) + self.assertEqual(Ts_tuple_bound.__bound__, (int, str)) + obj = object() + Ts_object = TypeVarTuple('Ts_object', bound=obj) + self.assertIs(Ts_object.__bound__, obj) + Ts_no_bound = TypeVarTuple('Ts_no_bound') + self.assertIsNone(Ts_no_bound.__bound__) + + def test_no_bivariant(self): + with self.assertRaises(ValueError): + TypeVarTuple('Ts', covariant=True, contravariant=True) + + def test_cannot_combine_explicit_and_infer(self): + with self.assertRaises(ValueError): + TypeVarTuple('Ts', covariant=True, infer_variance=True) + with self.assertRaises(ValueError): + TypeVarTuple('Ts', contravariant=True, infer_variance=True) + + def test_repr_with_variance(self): + Ts = TypeVarTuple('Ts') + self.assertEqual(repr(Ts), '~Ts') + Ts_co = TypeVarTuple('Ts_co', covariant=True) + self.assertEqual(repr(Ts_co), '+Ts_co') + Ts_contra = TypeVarTuple('Ts_contra', contravariant=True) + self.assertEqual(repr(Ts_contra), '-Ts_contra') + Ts_infer = TypeVarTuple('Ts_infer', infer_variance=True) + self.assertEqual(repr(Ts_infer), 'Ts_infer') + def test_unpacked_typevartuple_is_equal_to_itself(self): Ts = TypeVarTuple('Ts') self.assertEqual((*Ts,)[0], (*Ts,)[0]) @@ -1621,7 +1677,10 @@ def func1(*args: *Ts): pass self.assertEqual(gth(func1), {'args': Unpack[Ts]}) def func2(*args: *tuple[int, str]): pass - self.assertEqual(gth(func2), {'args': Unpack[tuple[int, str]]}) + hint = gth(func2)['args'] + self.assertIsInstance(hint, types.GenericAlias) + self.assertEqual(hint.__args__[0], int) + self.assertIs(hint.__unpacked__, True) class CustomVariadic(Generic[*Ts]): pass @@ -1636,7 +1695,10 @@ def func1(*args: '*Ts'): pass {'args': Unpack[Ts]}) def func2(*args: '*tuple[int, str]'): pass - self.assertEqual(gth(func2), {'args': Unpack[tuple[int, str]]}) + hint = gth(func2)['args'] + self.assertIsInstance(hint, types.GenericAlias) + self.assertEqual(hint.__args__[0], int) + self.assertIs(hint.__unpacked__, True) class CustomVariadic(Generic[*Ts]): pass @@ -3890,8 +3952,8 @@ def meth(self): pass self.assertIsNot(get_protocol_members(PR), P.__protocol_attrs__) acceptable_extra_attrs = { - '_is_protocol', '_is_runtime_protocol', '__parameters__', - '__init__', '__annotations__', '__subclasshook__', '__annotate__', + '_is_protocol', '_is_runtime_protocol', '__typing_is_deprecated_inherited_runtime_protocol__', + '__parameters__', '__init__', '__annotations__', '__subclasshook__', '__annotate__', '__annotations_cache__', '__annotate_func__', } self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs) @@ -3983,6 +4045,7 @@ class C: pass def test_defining_generic_protocols(self): T = TypeVar('T') + T2 = TypeVar('T2') S = TypeVar('S') @runtime_checkable @@ -3992,17 +4055,26 @@ def meth(self): pass class P(PR[int, T], Protocol[T]): y = 1 + self.assertEqual(P.__parameters__, (T,)) + with self.assertRaises(TypeError): PR[int] with self.assertRaises(TypeError): P[int, str] + with self.assertRaisesRegex( + TypeError, + re.escape('Some type variables (~S) are not listed in Protocol[~T, ~T2]'), + ): + class ExtraTypeVars(P[S], Protocol[T, T2]): ... class C(PR[int, T]): pass + self.assertEqual(C.__parameters__, (T,)) self.assertIsInstance(C[str](), C) def test_defining_generic_protocols_old_style(self): T = TypeVar('T') + T2 = TypeVar('T2') S = TypeVar('S') @runtime_checkable @@ -4021,9 +4093,19 @@ class P(PR[int, str], Protocol): class P1(Protocol, Generic[T]): def bar(self, x: T) -> str: ... + self.assertEqual(P1.__parameters__, (T,)) + class P2(Generic[T], Protocol): def bar(self, x: T) -> str: ... + self.assertEqual(P2.__parameters__, (T,)) + + msg = re.escape('Some type variables (~S) are not listed in Protocol[~T, ~T2]') + with self.assertRaisesRegex(TypeError, msg): + class ExtraTypeVars(P1[S], Protocol[T, T2]): ... + with self.assertRaisesRegex(TypeError, msg): + class ExtraTypeVars(P2[S], Protocol[T, T2]): ... + @runtime_checkable class PSub(P1[str], Protocol): x = 1 @@ -4036,6 +4118,28 @@ def bar(self, x: str) -> str: self.assertIsInstance(Test(), PSub) + def test_protocol_parameter_order(self): + # https://github.com/python/cpython/issues/137191 + T1 = TypeVar("T1") + T2 = TypeVar("T2", default=object) + + class A(Protocol[T1]): ... + + class B0(A[T2], Generic[T1, T2]): ... + self.assertEqual(B0.__parameters__, (T1, T2)) + + class B1(A[T2], Protocol, Generic[T1, T2]): ... + self.assertEqual(B1.__parameters__, (T1, T2)) + + class B2(A[T2], Protocol[T1, T2]): ... + self.assertEqual(B2.__parameters__, (T1, T2)) + + class B3[T1, T2](A[T2], Protocol): + @staticmethod + def get_typeparams(): + return (T1, T2) + self.assertEqual(B3.__parameters__, B3.get_typeparams()) + def test_pep695_generic_protocol_callable_members(self): @runtime_checkable class Foo[T](Protocol): @@ -4421,6 +4525,70 @@ class P(Protocol): with self.assertRaisesRegex(TypeError, "@runtime_checkable"): isinstance(1, P) + @subTests(['check_obj', 'check_func'], ([42, isinstance], [frozenset, issubclass])) + def test_inherited_runtime_protocol_deprecated(self, check_obj, check_func): + """See GH-132604.""" + + class BareProto(Protocol): + """I am not runtime-checkable.""" + + @runtime_checkable + class RCProto1(Protocol): + """I am runtime-checkable.""" + + class InheritedRCProto1(RCProto1, Protocol): + """I am accidentally runtime-checkable (by inheritance).""" + + @runtime_checkable + class RCProto2(InheritedRCProto1, Protocol): + """Explicit RC -> inherited RC -> explicit RC.""" + def spam(self): ... + + @runtime_checkable + class RCProto3(BareProto, Protocol): + """Not RC -> explicit RC.""" + + class InheritedRCProto2(RCProto3, Protocol): + """Not RC -> explicit RC -> inherited RC.""" + def eggs(self): ... + + class InheritedRCProto3(RCProto2, Protocol): + """Explicit RC -> inherited RC -> explicit RC -> inherited RC.""" + + class Concrete1(BareProto): + pass + + class Concrete2(InheritedRCProto2): + pass + + class Concrete3(InheritedRCProto3): + pass + + depr_message_re = ( + r" isn't explicitly decorated " + r"with @runtime_checkable but it is used in issubclass\(\) or " + r"isinstance\(\). Instance and class checks can only be used with " + r"@runtime_checkable protocols. This will raise a TypeError in Python 3.20." + ) + + for inherited_runtime_proto in InheritedRCProto1, InheritedRCProto2, InheritedRCProto3: + with self.assertWarnsRegex(DeprecationWarning, depr_message_re): + check_func(check_obj, inherited_runtime_proto) + + # Don't warn for explicitly checkable protocols and concrete implementations. + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + + for checkable in RCProto1, RCProto2, RCProto3, Concrete1, Concrete2, Concrete3: + check_func(check_obj, checkable) + + # Don't warn for uncheckable protocols. + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + + with self.assertRaises(TypeError): # Self-test. Protocol below can't be runtime-checkable. + check_func(check_obj, BareProto) + def test_super_call_init(self): class P(Protocol): x: int @@ -4932,7 +5100,7 @@ class MM2(collections.abc.MutableMapping, MutableMapping[str, str]): self.assertEqual(MM2.__bases__, (collections.abc.MutableMapping, Generic)) @cpython_only - @run_with_limited_c_stack() + @skip_if_huge_c_stack() @skip_wasi_stack_overflow() @skip_emscripten_stack_overflow() def test_parameters_deep_recursion(self): @@ -5821,6 +5989,7 @@ def test_subclass_special_form(self): Final[int], Literal[1, 2], Concatenate[int, ParamSpec("P")], + TypeForm[int], TypeGuard[int], TypeIs[range], ): @@ -5887,6 +6056,22 @@ class A: with self.assertRaises(TypeError): a[int] + def test_parameter_added_after_parameters_cached(self): + # gh-155752: GenericAlias parameters are cached before substitution, so + # an argument can gain __typing_subst__ after the tuple is calculated. + class Parameter: + pass + + first = Parameter() + first.__typing_subst__ = lambda value: value + late = Parameter() + alias = types.GenericAlias(dict, (first, late)) + self.assertEqual(alias.__parameters__, (first,)) + late.__typing_subst__ = lambda value: value + + with self.assertRaisesRegex(TypeError, "not found in __parameters__"): + alias[0] + def test_return_non_tuple_while_unpacking(self): # GH-138497: GenericAlias objects didn't ensure that __typing_subst__ actually # returned a tuple @@ -6380,62 +6565,8 @@ class F: for clazz in [C, D, E, F]: self.assertEqual(get_type_hints(clazz), expected_result) - def test_meta_no_type_check(self): - depr_msg = ( - "'typing.no_type_check_decorator' is deprecated " - "and slated for removal in Python 3.15" - ) - with self.assertWarnsRegex(DeprecationWarning, depr_msg): - @no_type_check_decorator - def magic_decorator(func): - return func - - self.assertEqual(magic_decorator.__name__, 'magic_decorator') - - @magic_decorator - def foo(a: 'whatevers') -> {}: - pass - - @magic_decorator - class C: - def foo(a: 'whatevers') -> {}: - pass - - self.assertEqual(foo.__name__, 'foo') - th = get_type_hints(foo) - self.assertEqual(th, {}) - cth = get_type_hints(C.foo) - self.assertEqual(cth, {}) - ith = get_type_hints(C().foo) - self.assertEqual(ith, {}) - class InternalsTests(BaseTestCase): - def test_deprecation_for_no_type_params_passed_to__evaluate(self): - with self.assertWarnsRegex( - DeprecationWarning, - ( - "Failing to pass a value to the 'type_params' parameter " - "of 'typing._eval_type' is deprecated" - ) - ) as cm: - self.assertEqual(typing._eval_type(list["int"], globals(), {}), list[int]) - - self.assertEqual(cm.filename, __file__) - - f = ForwardRef("int") - - with self.assertWarnsRegex( - DeprecationWarning, - ( - "Failing to pass a value to the 'type_params' parameter " - "of 'typing.ForwardRef._evaluate' is deprecated" - ) - ) as cm: - self.assertIs(f._evaluate(globals(), {}, recursive_guard=frozenset()), int) - - self.assertEqual(cm.filename, __file__) - def test_collect_parameters(self): typing = import_helper.import_fresh_module("typing") with self.assertWarnsRegex( @@ -7236,6 +7367,24 @@ def add_right(self, node: 'Node[T]' = None): right_hints = get_type_hints(t.add_right, globals(), locals()) self.assertEqual(right_hints['node'], Node[T]) + def test_get_type_hints_preserve_generic_alias_subclasses(self): + # https://github.com/python/cpython/issues/130870 + # A real world example of this is `collections.abc.Callable`. When parameterized, + # the result is a subclass of `types.GenericAlias`. + class MyAlias(types.GenericAlias): + pass + + class MyClass: + def __class_getitem__(cls, args): + return MyAlias(cls, args) + + # Using a forward reference is important, otherwise it works as expected. + # `y` tests that the `GenericAlias` subclass is preserved when stripping `Annotated`. + def func(x: MyClass['int'], y: MyClass[Annotated[int, ...]]): ... + + assert isinstance(get_type_hints(func)['x'], MyAlias) + assert isinstance(get_type_hints(func)['y'], MyAlias) + def test_stringified_typeddict(self): ns = run_code( """ @@ -7249,6 +7398,13 @@ class TD[UniqueT](TypedDict): self.assertEqual(TD.__annotations__, {'a': EqualToForwardRef('UniqueT', owner=TD, module=TD.__module__)}) self.assertEqual(get_type_hints(TD), {'a': TD.__type_params__[0]}) + def test_get_type_hints_order(self): + """Ensure that the order of function annotations matches the order they're defined""" + def f(positional: int, /, normal: str, *args: bytes, kwarg: list, **kwargs: bool) -> tuple: + pass + + self.assertEqual(list(gth(f)), ["positional", "normal", "args", "kwarg", "kwargs", "return"]) + class GetUtilitiesTestCase(TestCase): def test_get_origin(self): @@ -7339,6 +7495,7 @@ class C(Generic[T]): pass self.assertEqual(get_args(Required[int]), (int,)) self.assertEqual(get_args(NotRequired[int]), (int,)) self.assertEqual(get_args(TypeAlias), ()) + self.assertEqual(get_args(TypeForm[int]), (int,)) self.assertEqual(get_args(TypeGuard[int]), (int,)) self.assertEqual(get_args(TypeIs[range]), (range,)) Ts = TypeVarTuple('Ts') @@ -7576,14 +7733,23 @@ def test_mutablesequence(self): self.assertNotIsInstance((), typing.MutableSequence) def test_bytestring(self): + previous_typing_module = sys.modules.pop("typing", None) + self.addCleanup(sys.modules.__setitem__, "typing", previous_typing_module) + with self.assertWarns(DeprecationWarning): - self.assertIsInstance(b'', typing.ByteString) + from typing import ByteString with self.assertWarns(DeprecationWarning): - self.assertIsInstance(bytearray(b''), typing.ByteString) + self.assertIsInstance(b'', ByteString) with self.assertWarns(DeprecationWarning): - class Foo(typing.ByteString): ... + self.assertIsInstance(bytearray(b''), ByteString) with self.assertWarns(DeprecationWarning): - class Bar(typing.ByteString, typing.Awaitable): ... + self.assertIsSubclass(bytes, ByteString) + with self.assertWarns(DeprecationWarning): + self.assertIsSubclass(bytearray, ByteString) + with self.assertWarns(DeprecationWarning): + class Foo(ByteString): ... + with self.assertWarns(DeprecationWarning): + class Bar(ByteString, typing.Awaitable): ... def test_list(self): self.assertIsSubclass(list, typing.List) @@ -8326,78 +8492,13 @@ class Group(NamedTuple): self.assertIs(type(a), Group) self.assertEqual(a, (1, [2])) - def test_namedtuple_keyword_usage(self): - with self.assertWarnsRegex( - DeprecationWarning, - "Creating NamedTuple classes using keyword arguments is deprecated" - ): - LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int) - - nick = LocalEmployee('Nick', 25) - self.assertIsInstance(nick, tuple) - self.assertEqual(nick.name, 'Nick') - self.assertEqual(LocalEmployee.__name__, 'LocalEmployee') - self.assertEqual(LocalEmployee._fields, ('name', 'age')) - self.assertEqual(LocalEmployee.__annotations__, dict(name=str, age=int)) - - with self.assertRaisesRegex( - TypeError, - "Either list of fields or keywords can be provided to NamedTuple, not both" - ): - NamedTuple('Name', [('x', int)], y=str) - - with self.assertRaisesRegex( - TypeError, - "Either list of fields or keywords can be provided to NamedTuple, not both" - ): - NamedTuple('Name', [], y=str) - - with self.assertRaisesRegex( - TypeError, - ( - r"Cannot pass `None` as the 'fields' parameter " - r"and also specify fields using keyword arguments" - ) - ): - NamedTuple('Name', None, x=int) - - def test_namedtuple_special_keyword_names(self): - with self.assertWarnsRegex( - DeprecationWarning, - "Creating NamedTuple classes using keyword arguments is deprecated" - ): - NT = NamedTuple("NT", cls=type, self=object, typename=str, fields=list) - - self.assertEqual(NT.__name__, 'NT') - self.assertEqual(NT._fields, ('cls', 'self', 'typename', 'fields')) - a = NT(cls=str, self=42, typename='foo', fields=[('bar', tuple)]) - self.assertEqual(a.cls, str) - self.assertEqual(a.self, 42) - self.assertEqual(a.typename, 'foo') - self.assertEqual(a.fields, [('bar', tuple)]) - def test_empty_namedtuple(self): - expected_warning = re.escape( - "Failing to pass a value for the 'fields' parameter is deprecated " - "and will be disallowed in Python 3.15. " - "To create a NamedTuple class with 0 fields " - "using the functional syntax, " - "pass an empty list, e.g. `NT1 = NamedTuple('NT1', [])`." - ) - with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"): - NT1 = NamedTuple('NT1') + with self.assertRaisesRegex(TypeError, "missing.*required.*argument"): + BAD = NamedTuple('BAD') - expected_warning = re.escape( - "Passing `None` as the 'fields' parameter is deprecated " - "and will be disallowed in Python 3.15. " - "To create a NamedTuple class with 0 fields " - "using the functional syntax, " - "pass an empty list, e.g. `NT2 = NamedTuple('NT2', [])`." - ) - with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"): - NT2 = NamedTuple('NT2', None) - - NT3 = NamedTuple('NT2', []) + NT1 = NamedTuple('NT1', {}) + NT2 = NamedTuple('NT2', ()) + NT3 = NamedTuple('NT3', []) class CNT(NamedTuple): pass # empty body @@ -8412,16 +8513,18 @@ class CNT(NamedTuple): def test_namedtuple_errors(self): with self.assertRaises(TypeError): NamedTuple.__new__() + with self.assertRaisesRegex(TypeError, "object is not iterable"): + NamedTuple('Name', None) with self.assertRaisesRegex( TypeError, - "missing 1 required positional argument" + "missing 2 required positional arguments" ): NamedTuple() with self.assertRaisesRegex( TypeError, - "takes from 1 to 2 positional arguments but 3 were given" + "takes 2 positional arguments but 3 were given" ): NamedTuple('Emp', [('name', str)], None) @@ -8433,10 +8536,22 @@ def test_namedtuple_errors(self): with self.assertRaisesRegex( TypeError, - "missing 1 required positional argument: 'typename'" + "got some positional-only arguments passed as keyword arguments" ): NamedTuple(typename='Emp', name=str, id=int) + with self.assertRaisesRegex( + TypeError, + "got an unexpected keyword argument" + ): + NamedTuple('Name', [('x', int)], y=str) + + with self.assertRaisesRegex( + TypeError, + "got an unexpected keyword argument" + ): + NamedTuple('Name', [], y=str) + def test_copy_and_pickle(self): global Emp # pickle wants to reference the class by name Emp = NamedTuple('Emp', [('name', str), ('cool', int)]) @@ -8567,6 +8682,15 @@ class ThisWontWorkEither(NamedTuple): def name(self): return __class__.__name__ + def test_named_tuple_non_sequence_input(self): + field_names = ["x", "y"] + field_values = [int, int] + Point = NamedTuple("Point", zip(field_names, field_values)) + p = Point(1, 2) + self.assertEqual(p.x, 1) + self.assertEqual(p.y, 2) + self.assertEqual(repr(p),"Point(x=1, y=2)") + class TypedDictTests(BaseTestCase): def test_basics_functional_syntax(self): @@ -8937,6 +9061,32 @@ class ChildWithInlineAndOptional(Untotal, Inline): class Wrong(*bases): pass + def test_closed_values(self): + class Implicit(TypedDict): ... + class ExplicitTrue(TypedDict, closed=True): ... + class ExplicitFalse(TypedDict, closed=False): ... + + self.assertIsNone(Implicit.__closed__) + self.assertIs(ExplicitTrue.__closed__, True) + self.assertIs(ExplicitFalse.__closed__, False) + + def test_extra_items_class_arg(self): + class TD(TypedDict, extra_items=int): + a: str + + self.assertIs(TD.__extra_items__, int) + self.assertEqual(TD.__annotations__, {'a': str}) + self.assertEqual(TD.__required_keys__, frozenset({'a'})) + self.assertEqual(TD.__optional_keys__, frozenset()) + + class NoExtra(TypedDict): + a: str + + self.assertIs(NoExtra.__extra_items__, NoExtraItems) + self.assertEqual(NoExtra.__annotations__, {'a': str}) + self.assertEqual(NoExtra.__required_keys__, frozenset({'a'})) + self.assertEqual(NoExtra.__optional_keys__, frozenset()) + def test_is_typeddict(self): self.assertIs(is_typeddict(Point2D), True) self.assertIs(is_typeddict(Union[str, int]), False) @@ -9180,39 +9330,27 @@ class MultipleGenericBases(GenericParent[int], GenericParent[float]): self.assertEqual(CallTypedDict.__orig_bases__, (TypedDict,)) def test_zero_fields_typeddicts(self): - T1 = TypedDict("T1", {}) + T1a = TypedDict("T1a", {}) + T1b = TypedDict("T1b", []) + T1c = TypedDict("T1c", ()) class T2(TypedDict): pass class T3[tvar](TypedDict): pass S = TypeVar("S") class T4(TypedDict, Generic[S]): pass - expected_warning = re.escape( - "Failing to pass a value for the 'fields' parameter is deprecated " - "and will be disallowed in Python 3.15. " - "To create a TypedDict class with 0 fields " - "using the functional syntax, " - "pass an empty dictionary, e.g. `T5 = TypedDict('T5', {})`." - ) - with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"): - T5 = TypedDict('T5') - - expected_warning = re.escape( - "Passing `None` as the 'fields' parameter is deprecated " - "and will be disallowed in Python 3.15. " - "To create a TypedDict class with 0 fields " - "using the functional syntax, " - "pass an empty dictionary, e.g. `T6 = TypedDict('T6', {})`." - ) - with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"): - T6 = TypedDict('T6', None) - - for klass in T1, T2, T3, T4, T5, T6: + for klass in T1a, T1b, T1c, T2, T3, T4: with self.subTest(klass=klass.__name__): self.assertEqual(klass.__annotations__, {}) self.assertEqual(klass.__required_keys__, set()) self.assertEqual(klass.__optional_keys__, set()) self.assertIsInstance(klass(), dict) + def test_errors(self): + with self.assertRaisesRegex(TypeError, "missing 1 required.*argument"): + TypedDict('TD') + with self.assertRaisesRegex(TypeError, "object is not iterable"): + TypedDict('TD', None) + def test_readonly_inheritance(self): class Base1(TypedDict): a: ReadOnly[int] @@ -9276,6 +9414,71 @@ class AllTheThings(TypedDict): }, ) + def test_closed_inheritance(self): + class Base(TypedDict, extra_items=ReadOnly[Union[str, None]]): + a: int + + self.assertEqual(Base.__required_keys__, frozenset({"a"})) + self.assertEqual(Base.__optional_keys__, frozenset({})) + self.assertEqual(Base.__readonly_keys__, frozenset({})) + self.assertEqual(Base.__mutable_keys__, frozenset({"a"})) + self.assertEqual(Base.__annotations__, {"a": int}) + self.assertEqual(Base.__extra_items__, ReadOnly[Union[str, None]]) + self.assertIsNone(Base.__closed__) + + class Child(Base, extra_items=int): + a: str + + self.assertEqual(Child.__required_keys__, frozenset({'a'})) + self.assertEqual(Child.__optional_keys__, frozenset({})) + self.assertEqual(Child.__readonly_keys__, frozenset({})) + self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) + self.assertEqual(Child.__annotations__, {"a": str}) + self.assertIs(Child.__extra_items__, int) + self.assertIsNone(Child.__closed__) + + class GrandChild(Child, closed=True): + a: float + + self.assertEqual(GrandChild.__required_keys__, frozenset({'a'})) + self.assertEqual(GrandChild.__optional_keys__, frozenset({})) + self.assertEqual(GrandChild.__readonly_keys__, frozenset({})) + self.assertEqual(GrandChild.__mutable_keys__, frozenset({'a'})) + self.assertEqual(GrandChild.__annotations__, {"a": float}) + self.assertIs(GrandChild.__extra_items__, NoExtraItems) + self.assertIs(GrandChild.__closed__, True) + + class GrandGrandChild(GrandChild): + ... + self.assertEqual(GrandGrandChild.__required_keys__, frozenset({'a'})) + self.assertEqual(GrandGrandChild.__optional_keys__, frozenset({})) + self.assertEqual(GrandGrandChild.__readonly_keys__, frozenset({})) + self.assertEqual(GrandGrandChild.__mutable_keys__, frozenset({'a'})) + self.assertEqual(GrandGrandChild.__annotations__, {"a": float}) + self.assertIs(GrandGrandChild.__extra_items__, NoExtraItems) + self.assertIsNone(GrandGrandChild.__closed__) + + def test_implicit_extra_items(self): + class Base(TypedDict): + a: int + + self.assertIs(Base.__extra_items__, NoExtraItems) + self.assertIsNone(Base.__closed__) + + class ChildA(Base, closed=True): + ... + + self.assertEqual(ChildA.__extra_items__, NoExtraItems) + self.assertIs(ChildA.__closed__, True) + + def test_cannot_combine_closed_and_extra_items(self): + with self.assertRaisesRegex( + TypeError, + "Cannot combine closed=True and extra_items" + ): + class TD(TypedDict, closed=True, extra_items=range): + x: str + def test_annotations(self): # _type_check is applied with self.assertRaisesRegex(TypeError, "Plain typing.Final is not valid as type argument"): @@ -9505,6 +9708,12 @@ class A(typing.Match): class B(typing.Pattern): pass + def test_typed_dict_signature(self): + self.assertListEqual( + list(inspect.signature(TypedDict).parameters), + ['typename', 'fields', 'total', 'closed', 'extra_items'] + ) + class AnnotatedTests(BaseTestCase): @@ -10380,6 +10589,17 @@ def test_paramspec_in_nested_generics(self): self.assertEqual(G2[[int, str], float], list[C]) self.assertEqual(G3[[int, str], float], list[C] | int) + def test_paramspec_bound(self): + P = ParamSpec('P', bound=[int, str]) + self.assertEqual(P.__bound__, [int, str]) + P2 = ParamSpec('P2', bound=(int, str)) + self.assertEqual(P2.__bound__, (int, str)) + obj = object() + P3 = ParamSpec('P3', bound=obj) + self.assertIs(P3.__bound__, obj) + P4 = ParamSpec('P4') + self.assertIs(P4.__bound__, None) + def test_paramspec_gets_copied(self): # bpo-46581 P = ParamSpec('P') @@ -10587,6 +10807,72 @@ def test_no_isinstance(self): issubclass(int, TypeIs) +class TypeFormTests(BaseTestCase): + def test_basics(self): + TypeForm[int] # OK + self.assertEqual(TypeForm[int], TypeForm[int]) + + def foo(arg) -> TypeForm[int]: ... + self.assertEqual(gth(foo), {'return': TypeForm[int]}) + + with self.assertRaises(TypeError): + TypeForm[int, str] + + def test_repr(self): + self.assertEqual(repr(TypeForm), 'typing.TypeForm') + cv = TypeForm[int] + self.assertEqual(repr(cv), 'typing.TypeForm[int]') + cv = TypeForm[Employee] + self.assertEqual(repr(cv), 'typing.TypeForm[%s.Employee]' % __name__) + cv = TypeForm[tuple[int]] + self.assertEqual(repr(cv), 'typing.TypeForm[tuple[int]]') + + def test_cannot_subclass(self): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): + class C(type(TypeForm)): + pass + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): + class D(type(TypeForm[int])): + pass + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.TypeForm'): + class E(TypeForm): + pass + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.TypeForm\[int\]'): + class F(TypeForm[int]): + pass + + def test_call(self): + objs = [ + 1, + "int", + int, + tuple[int, str], + Tuple[int, str], + ] + for obj in objs: + with self.subTest(obj=obj): + self.assertIs(TypeForm(obj), obj) + + with self.assertRaises(TypeError): + TypeForm() + with self.assertRaises(TypeError): + TypeForm("too", "many") + + def test_cannot_init_type(self): + with self.assertRaises(TypeError): + type(TypeForm)() + with self.assertRaises(TypeError): + type(TypeForm[Optional[int]])() + + def test_no_isinstance(self): + with self.assertRaises(TypeError): + isinstance(1, TypeForm[int]) + with self.assertRaises(TypeError): + issubclass(int, TypeForm) + + SpecialAttrsP = typing.ParamSpec('SpecialAttrsP') SpecialAttrsT = typing.TypeVar('SpecialAttrsT', int, float, complex) @@ -10594,6 +10880,10 @@ def test_no_isinstance(self): class SpecialAttrsTests(BaseTestCase): def test_special_attrs(self): + with warnings.catch_warnings( + action='ignore', category=DeprecationWarning + ): + typing_ByteString = typing.ByteString cls_to_check = { # ABC classes typing.AbstractSet: 'AbstractSet', @@ -10602,7 +10892,7 @@ def test_special_attrs(self): typing.AsyncIterable: 'AsyncIterable', typing.AsyncIterator: 'AsyncIterator', typing.Awaitable: 'Awaitable', - typing.ByteString: 'ByteString', + typing_ByteString: 'ByteString', typing.Callable: 'Callable', typing.ChainMap: 'ChainMap', typing.Collection: 'Collection', @@ -10684,6 +10974,7 @@ def test_special_attrs(self): typing.Never: 'Never', typing.Optional: 'Optional', typing.TypeAlias: 'TypeAlias', + typing.TypeForm: 'TypeForm', typing.TypeGuard: 'TypeGuard', typing.TypeIs: 'TypeIs', typing.TypeVar: 'TypeVar', @@ -10698,6 +10989,7 @@ def test_special_attrs(self): typing.Literal[1, 2]: 'Literal', typing.Literal[True, 2]: 'Literal', typing.Optional[Any]: 'Union', + typing.TypeForm[Any]: 'TypeForm', typing.TypeGuard[Any]: 'TypeGuard', typing.TypeIs[Any]: 'TypeIs', typing.Union[Any]: 'Any', @@ -10780,6 +11072,18 @@ def bar(self): self.assertNotIn('__magic__', dir_items) +class DisjointBaseTests(BaseTestCase): + def test_disjoint_base_unmodified(self): + class C: ... + self.assertIs(C, disjoint_base(C)) + + def test_dunder_disjoint_base(self): + @disjoint_base + class C: ... + + self.assertIs(C.__disjoint_base__, True) + + class RevealTypeTests(BaseTestCase): def test_reveal_type(self): obj = object() @@ -10959,7 +11263,8 @@ def test_all_exported_names(self): # there's a few types and metaclasses that aren't exported not k.endswith(('Meta', '_contra', '_co')) and not k.upper() == k and - # but export all things that have __module__ == 'typing' + k not in {"ByteString"} and + # but export all other things that have __module__ == 'typing' getattr(v, '__module__', None) == typing.__name__ ) } diff --git a/Lib/test/test_unittest/test_assertions.py b/Lib/test/test_unittest/test_assertions.py index 1dec947ea76..df95e558949 100644 --- a/Lib/test/test_unittest/test_assertions.py +++ b/Lib/test/test_unittest/test_assertions.py @@ -406,11 +406,12 @@ def testAssertWarnsRegex(self): # test warning raised but with wrong message def raise_wrong_message(): warnings.warn('foo') - self.assertMessagesCM('assertWarnsRegex', (UserWarning, 'regex'), - raise_wrong_message, - ['^"regex" does not match "foo"$', '^oops$', - '^"regex" does not match "foo"$', - '^"regex" does not match "foo" : oops$']) + with self.assertWarnsRegex(UserWarning, 'foo'): + self.assertMessagesCM('assertWarnsRegex', (UserWarning, 'regex'), + raise_wrong_message, + ['^"regex" does not match "foo"$', '^oops$', + '^"regex" does not match "foo"$', + '^"regex" does not match "foo" : oops$']) if __name__ == "__main__": diff --git a/Lib/test/test_unittest/test_case.py b/Lib/test/test_unittest/test_case.py index d66cab146af..83b42918e1e 100644 --- a/Lib/test/test_unittest/test_case.py +++ b/Lib/test/test_unittest/test_case.py @@ -1631,11 +1631,11 @@ def testAssertRaisesRegexNoExceptionType(self): self.assertRaisesRegex((ValueError, object), 'expect') def testAssertWarnsCallable(self): - def _runtime_warn(): - warnings.warn("foo", RuntimeWarning) + def _runtime_warn(categories=(RuntimeWarning,)): + for category in categories: + warnings.warn("foo", category) # Success when the right warning is triggered, even several times - self.assertWarns(RuntimeWarning, _runtime_warn) - self.assertWarns(RuntimeWarning, _runtime_warn) + self.assertWarns(RuntimeWarning, _runtime_warn, (RuntimeWarning, RuntimeWarning)) # A tuple of warning classes is accepted self.assertWarns((DeprecationWarning, RuntimeWarning), _runtime_warn) # *args and **kwargs also work @@ -1648,22 +1648,35 @@ def _runtime_warn(): with self.assertRaises(TypeError): self.assertWarns(RuntimeWarning, None) # Failure when another warning is triggered - with warnings.catch_warnings(): + with warnings.catch_warnings(record=True) as log: # Force default filter (in case tests are run with -We) warnings.simplefilter("default", RuntimeWarning) with self.assertRaises(self.failureException): - self.assertWarns(DeprecationWarning, _runtime_warn) + self.assertWarns(DeprecationWarning, _runtime_warn, + (RuntimeWarning, RuntimeWarning)) + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) # Filters for other warnings are not modified with warnings.catch_warnings(): warnings.simplefilter("error", RuntimeWarning) with self.assertRaises(RuntimeWarning): self.assertWarns(DeprecationWarning, _runtime_warn) + # Warnings that do not match the category are not swallowed. + with self.assertWarns(RuntimeWarning): + with self.assertRaises(self.failureException): + self.assertWarns(DeprecationWarning, _runtime_warn) + with self.assertWarns(RuntimeWarning): + self.assertWarns(DeprecationWarning, _runtime_warn, + (RuntimeWarning, DeprecationWarning)) + with self.assertWarns(RuntimeWarning): + self.assertWarns(DeprecationWarning, _runtime_warn, + (DeprecationWarning, RuntimeWarning)) def testAssertWarnsContext(self): # Believe it or not, it is preferable to duplicate all tests above, # to make sure the __warningregistry__ $@ is circumvented correctly. - def _runtime_warn(): - warnings.warn("foo", RuntimeWarning) + def _runtime_warn(category=RuntimeWarning): + warnings.warn("foo", category) _runtime_warn_lineno = inspect.getsourcelines(_runtime_warn)[1] with self.assertWarns(RuntimeWarning) as cm: _runtime_warn() @@ -1694,18 +1707,58 @@ def _runtime_warn(): with self.assertWarns(RuntimeWarning, foobar=42): pass # Failure when another warning is triggered - with warnings.catch_warnings(): + with warnings.catch_warnings(record=True) as log: # Force default filter (in case tests are run with -We) warnings.simplefilter("default", RuntimeWarning) with self.assertRaises(self.failureException): with self.assertWarns(DeprecationWarning): _runtime_warn() + _runtime_warn() + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) + with warnings.catch_warnings(record=True) as log: + # Force default filter (in case tests are run with -We) + warnings.simplefilter("error", RuntimeWarning) + warnings.filterwarnings("default", category=RuntimeWarning, + module=__name__) + with self.assertRaises(self.failureException): + with self.assertWarns(DeprecationWarning): + _runtime_warn() + _runtime_warn() + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) # Filters for other warnings are not modified with warnings.catch_warnings(): warnings.simplefilter("error", RuntimeWarning) with self.assertRaises(RuntimeWarning): with self.assertWarns(DeprecationWarning): _runtime_warn() + # Warnings that do not match the category are not swallowed. + with self.assertWarns(RuntimeWarning): + with self.assertRaises(self.failureException): + with self.assertWarns(DeprecationWarning): + _runtime_warn() + with self.assertWarns(RuntimeWarning): + with self.assertWarns(DeprecationWarning): + _runtime_warn() + _runtime_warn(DeprecationWarning) + with self.assertWarns(RuntimeWarning): + with self.assertWarns(DeprecationWarning): + _runtime_warn(DeprecationWarning) + _runtime_warn() + # Filters by module name work for other warnings. + with warnings.catch_warnings(record=True) as log: + warnings.filterwarnings("error", category=RuntimeWarning) + warnings.filterwarnings("default", category=RuntimeWarning, + module=re.escape(__name__)) + warnings.filterwarnings("error", category=RuntimeWarning, + module='test_case') + with self.assertWarns(DeprecationWarning): + _runtime_warn(DeprecationWarning) + _runtime_warn() + _runtime_warn() + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) def testAssertWarnsNoExceptionType(self): with self.assertRaises(TypeError): @@ -1722,8 +1775,9 @@ def testAssertWarnsNoExceptionType(self): self.assertWarns((UserWarning, Exception)) def testAssertWarnsRegexCallable(self): - def _runtime_warn(msg): - warnings.warn(msg, RuntimeWarning) + def _runtime_warn(*msgs): + for msg in msgs: + warnings.warn(msg, RuntimeWarning) self.assertWarnsRegex(RuntimeWarning, "o+", _runtime_warn, "foox") # Failure when no warning is triggered @@ -1734,16 +1788,26 @@ def _runtime_warn(msg): with self.assertRaises(TypeError): self.assertWarnsRegex(RuntimeWarning, "o+", None) # Failure when another warning is triggered - with warnings.catch_warnings(): + with warnings.catch_warnings(record=True) as log: # Force default filter (in case tests are run with -We) warnings.simplefilter("default", RuntimeWarning) with self.assertRaises(self.failureException): self.assertWarnsRegex(DeprecationWarning, "o+", - _runtime_warn, "foox") - # Failure when message doesn't match - with self.assertRaises(self.failureException): + _runtime_warn, "foox", "foox") + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) + # Failure when message doesn't match. + # Warnings that do not match the regex are not swallowed. + with self.assertWarnsRegex(RuntimeWarning, "ar"): + with self.assertRaises(self.failureException): + self.assertWarnsRegex(RuntimeWarning, "o+", + _runtime_warn, "barz") + with self.assertWarnsRegex(RuntimeWarning, "ar"): + self.assertWarnsRegex(RuntimeWarning, "o+", + _runtime_warn, "barz", "foox") + with self.assertWarnsRegex(RuntimeWarning, "ar"): self.assertWarnsRegex(RuntimeWarning, "o+", - _runtime_warn, "barz") + _runtime_warn, "foox", "barz") # A little trickier: we ask RuntimeWarnings to be raised, and then # check for some of them. It is implementation-defined whether # non-matching RuntimeWarnings are simply re-raised, or produce a @@ -1778,16 +1842,29 @@ def _runtime_warn(msg): with self.assertWarnsRegex(RuntimeWarning, 'o+', foobar=42): pass # Failure when another warning is triggered - with warnings.catch_warnings(): + with warnings.catch_warnings(record=True) as log: # Force default filter (in case tests are run with -We) warnings.simplefilter("default", RuntimeWarning) with self.assertRaises(self.failureException): with self.assertWarnsRegex(DeprecationWarning, "o+"): _runtime_warn("foox") - # Failure when message doesn't match - with self.assertRaises(self.failureException): + _runtime_warn("foox") + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) + # Failure when message doesn't match. + # Warnings that do not match the regex are not swallowed. + with self.assertWarnsRegex(RuntimeWarning, "ar"): + with self.assertRaises(self.failureException): + with self.assertWarnsRegex(RuntimeWarning, "o+"): + _runtime_warn("barz") + with self.assertWarnsRegex(RuntimeWarning, "ar"): with self.assertWarnsRegex(RuntimeWarning, "o+"): _runtime_warn("barz") + _runtime_warn("foox") + with self.assertWarnsRegex(RuntimeWarning, "ar"): + with self.assertWarnsRegex(RuntimeWarning, "o+"): + _runtime_warn("foox") + _runtime_warn("barz") # A little trickier: we ask RuntimeWarnings to be raised, and then # check for some of them. It is implementation-defined whether # non-matching RuntimeWarnings are simply re-raised, or produce a @@ -1797,6 +1874,19 @@ def _runtime_warn(msg): with self.assertRaises((RuntimeWarning, self.failureException)): with self.assertWarnsRegex(RuntimeWarning, "o+"): _runtime_warn("barz") + # Filters by module name work for warnings with other message. + with warnings.catch_warnings(record=True) as log: + warnings.filterwarnings("error", category=RuntimeWarning) + warnings.filterwarnings("default", category=RuntimeWarning, + module=re.escape(__name__)) + warnings.filterwarnings("error", category=RuntimeWarning, + module='test_case') + with self.assertWarnsRegex(RuntimeWarning, "ar"): + _runtime_warn("bar") + _runtime_warn("foox") + _runtime_warn("foox") + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) def testAssertWarnsRegexNoExceptionType(self): with self.assertRaises(TypeError): @@ -1920,6 +2010,22 @@ def testAssertLogsUnexpectedException(self): with self.assertLogs(): raise ZeroDivisionError("Unexpected") + def testAssertLogsWithFormatter(self): + # Check alternative formats will be respected + format = "[No.1: the larch] %(levelname)s:%(name)s:%(message)s" + formatter = logging.Formatter(format) + with self.assertNoStderr(): + with self.assertLogs() as cm: + log_foo.info("1") + log_foobar.debug("2") + self.assertEqual(cm.output, ["INFO:foo:1"]) + self.assertLogRecords(cm.records, [{'name': 'foo'}]) + with self.assertLogs(formatter=formatter) as cm: + log_foo.info("1") + log_foobar.debug("2") + self.assertEqual(cm.output, ["[No.1: the larch] INFO:foo:1"]) + self.assertLogRecords(cm.records, [{'name': 'foo'}]) + def testAssertNoLogsDefault(self): with self.assertRaises(self.failureException) as cm: with self.assertNoLogs(): diff --git a/Lib/test/test_unittest/test_loader.py b/Lib/test/test_unittest/test_loader.py index cdff6d1a20c..f4e50a3dccb 100644 --- a/Lib/test/test_unittest/test_loader.py +++ b/Lib/test/test_unittest/test_loader.py @@ -1,3 +1,4 @@ +import abc import functools import sys import types @@ -98,6 +99,22 @@ def test_loadTestsFromTestCase__from_FunctionTestCase(self): self.assertIsInstance(suite, loader.suiteClass) self.assertEqual(list(suite), []) + # "Do not load any tests from a TestCase-derived class that is an abstract + # base class." + def test_loadTestsFromTestCase__from_abc_TestCase(self): + class FooBase(unittest.TestCase, metaclass=abc.ABCMeta): + @abc.abstractmethod + def test(self): ... + class Foo(FooBase): + def test(self): pass + + empty_suite = unittest.TestSuite() + + loader = unittest.TestLoader() + suite = loader.loadTestsFromTestCase(Foo) + self.assertEqual(loader.loadTestsFromTestCase(FooBase), empty_suite) + self.assertEqual(list(suite), [Foo('test')]) + ################################################################ ### /Tests for TestLoader.loadTestsFromTestCase @@ -252,6 +269,24 @@ def load_tests(loader, tests, pattern): self.assertRaisesRegex(TypeError, "some failure", test.m) + # Check that loadTestsFromModule skips abstract base classes derived from + # TestCase, which can't be instantiated. + def test_loadTestsFromModule__skip_abc_TestCase(self): + m = types.ModuleType('m') + class MyTestCaseBase(unittest.TestCase, metaclass=abc.ABCMeta): + @abc.abstractmethod + def test(self): + ... + class MyTestCase(MyTestCaseBase): + def test(self): + pass + m.testcase_1 = MyTestCaseBase + m.testcase_2 = MyTestCase + loader = unittest.TestLoader() + suite = loader.loadTestsFromModule(m) + expected = [loader.suiteClass([MyTestCase('test')])] + self.assertEqual(list(suite), expected) + ################################################################ ### /Tests for TestLoader.loadTestsFromModule() @@ -1052,6 +1087,22 @@ def test_loadTestsFromNames__module_not_loaded(self): if module_name in sys.modules: del sys.modules[module_name] + # "The specifier should not refer to a test method in a TestCase-derived + # subclass that is an abstract base class" + def test_loadTestsFromNames__testmethod_in_abc_TestCase(self): + m = types.ModuleType('m') + class Foo(unittest.TestCase, metaclass=abc.ABCMeta): + @abc.abstractmethod + def test_1(self): ... + def test_2(self): pass + m.Foo = Foo + + loader = unittest.TestLoader() + for name in 'Foo.test_1', 'Foo.test_2': + with self.subTest(name=name), self.assertRaisesRegex(TypeError, + "Cannot instantiate abstract test case Foo"): + loader.loadTestsFromNames([name], m) + ################################################################ ### /Tests for TestLoader.loadTestsFromNames() diff --git a/Lib/test/test_unittest/test_result.py b/Lib/test/test_unittest/test_result.py index 9ac4c52449c..3f44e617303 100644 --- a/Lib/test/test_unittest/test_result.py +++ b/Lib/test/test_unittest/test_result.py @@ -1282,14 +1282,22 @@ def setUpModule(): suite(result) expected_out = '\nStdout:\ndo cleanup2\ndo cleanup1\n' self.assertEqual(stdout.getvalue(), expected_out) - self.assertEqual(len(result.errors), 1) + self.assertEqual(len(result.errors), 2) description = 'tearDownModule (Module)' test_case, formatted_exc = result.errors[0] self.assertEqual(test_case.description, description) self.assertIn('ValueError: bad cleanup2', formatted_exc) + self.assertNotIn('ExceptionGroup', formatted_exc) self.assertNotIn('TypeError', formatted_exc) self.assertIn(expected_out, formatted_exc) + test_case, formatted_exc = result.errors[1] + self.assertEqual(test_case.description, description) + self.assertIn('TypeError: bad cleanup1', formatted_exc) + self.assertNotIn('ExceptionGroup', formatted_exc) + self.assertNotIn('ValueError', formatted_exc) + self.assertIn(expected_out, formatted_exc) + def testBufferSetUpModule_DoModuleCleanups(self): with captured_stdout() as stdout: result = unittest.TestResult() @@ -1313,22 +1321,34 @@ def setUpModule(): suite(result) expected_out = '\nStdout:\nset up module\ndo cleanup2\ndo cleanup1\n' self.assertEqual(stdout.getvalue(), expected_out) - self.assertEqual(len(result.errors), 2) + self.assertEqual(len(result.errors), 3) description = 'setUpModule (Module)' test_case, formatted_exc = result.errors[0] self.assertEqual(test_case.description, description) self.assertIn('ZeroDivisionError: division by zero', formatted_exc) + self.assertNotIn('ExceptionGroup', formatted_exc) self.assertNotIn('ValueError', formatted_exc) self.assertNotIn('TypeError', formatted_exc) self.assertIn('\nStdout:\nset up module\n', formatted_exc) + test_case, formatted_exc = result.errors[1] self.assertIn(expected_out, formatted_exc) self.assertEqual(test_case.description, description) self.assertIn('ValueError: bad cleanup2', formatted_exc) + self.assertNotIn('ExceptionGroup', formatted_exc) self.assertNotIn('ZeroDivisionError', formatted_exc) self.assertNotIn('TypeError', formatted_exc) self.assertIn(expected_out, formatted_exc) + test_case, formatted_exc = result.errors[2] + self.assertIn(expected_out, formatted_exc) + self.assertEqual(test_case.description, description) + self.assertIn('TypeError: bad cleanup1', formatted_exc) + self.assertNotIn('ExceptionGroup', formatted_exc) + self.assertNotIn('ZeroDivisionError', formatted_exc) + self.assertNotIn('ValueError', formatted_exc) + self.assertIn(expected_out, formatted_exc) + def testBufferTearDownModule_DoModuleCleanups(self): with captured_stdout() as stdout: result = unittest.TestResult() @@ -1355,21 +1375,32 @@ def tearDownModule(): suite(result) expected_out = '\nStdout:\ntear down module\ndo cleanup2\ndo cleanup1\n' self.assertEqual(stdout.getvalue(), expected_out) - self.assertEqual(len(result.errors), 2) + self.assertEqual(len(result.errors), 3) description = 'tearDownModule (Module)' test_case, formatted_exc = result.errors[0] self.assertEqual(test_case.description, description) self.assertIn('ZeroDivisionError: division by zero', formatted_exc) + self.assertNotIn('ExceptionGroup', formatted_exc) self.assertNotIn('ValueError', formatted_exc) self.assertNotIn('TypeError', formatted_exc) self.assertIn('\nStdout:\ntear down module\n', formatted_exc) + test_case, formatted_exc = result.errors[1] self.assertEqual(test_case.description, description) self.assertIn('ValueError: bad cleanup2', formatted_exc) + self.assertNotIn('ExceptionGroup', formatted_exc) self.assertNotIn('ZeroDivisionError', formatted_exc) self.assertNotIn('TypeError', formatted_exc) self.assertIn(expected_out, formatted_exc) + test_case, formatted_exc = result.errors[2] + self.assertEqual(test_case.description, description) + self.assertIn('TypeError: bad cleanup1', formatted_exc) + self.assertNotIn('ExceptionGroup', formatted_exc) + self.assertNotIn('ZeroDivisionError', formatted_exc) + self.assertNotIn('ValueError', formatted_exc) + self.assertIn(expected_out, formatted_exc) + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_unittest/test_runner.py b/Lib/test/test_unittest/test_runner.py index 4d3cfd60b8d..a47e2ebb59d 100644 --- a/Lib/test/test_unittest/test_runner.py +++ b/Lib/test/test_unittest/test_runner.py @@ -13,6 +13,7 @@ LoggingResult, ResultWithNoStartTestRunStopTestRun, ) +from test.support.testcase import ExceptionIsLikeMixin def resultFactory(*_): @@ -604,7 +605,7 @@ class EmptyTest(unittest.TestCase): @support.force_not_colorized_test_class -class TestModuleCleanUp(unittest.TestCase): +class TestModuleCleanUp(ExceptionIsLikeMixin, unittest.TestCase): def test_add_and_do_ModuleCleanup(self): module_cleanups = [] @@ -646,11 +647,50 @@ class Module(object): [(module_cleanup_good, (1, 2, 3), dict(four='hello', five='goodbye')), (module_cleanup_bad, (), {})]) - with self.assertRaises(CustomError) as e: + with self.assertRaises(Exception) as e: unittest.case.doModuleCleanups() - self.assertEqual(str(e.exception), 'CleanUpExc') + self.assertExceptionIsLike(e.exception, + ExceptionGroup('module cleanup failed', + [CustomError('CleanUpExc')])) self.assertEqual(unittest.case._module_cleanups, []) + def test_doModuleCleanup_with_multiple_errors_in_addModuleCleanup(self): + def module_cleanup_bad1(): + raise TypeError('CleanUpExc1') + + def module_cleanup_bad2(): + raise ValueError('CleanUpExc2') + + class Module: + unittest.addModuleCleanup(module_cleanup_bad1) + unittest.addModuleCleanup(module_cleanup_bad2) + with self.assertRaises(ExceptionGroup) as e: + unittest.case.doModuleCleanups() + self.assertExceptionIsLike(e.exception, + ExceptionGroup('module cleanup failed', [ + ValueError('CleanUpExc2'), + TypeError('CleanUpExc1'), + ])) + + def test_doModuleCleanup_with_exception_group_in_addModuleCleanup(self): + def module_cleanup_bad(): + raise ExceptionGroup('CleanUpExc', [ + ValueError('CleanUpExc2'), + TypeError('CleanUpExc1'), + ]) + + class Module: + unittest.addModuleCleanup(module_cleanup_bad) + with self.assertRaises(ExceptionGroup) as e: + unittest.case.doModuleCleanups() + self.assertExceptionIsLike(e.exception, + ExceptionGroup('module cleanup failed', [ + ExceptionGroup('CleanUpExc', [ + ValueError('CleanUpExc2'), + TypeError('CleanUpExc1'), + ]), + ])) + def test_addModuleCleanup_arg_errors(self): cleanups = [] def cleanup(*args, **kwargs): @@ -871,9 +911,11 @@ def tearDownClass(cls): ordering = [] blowUp = True suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest) - with self.assertRaises(CustomError) as cm: + with self.assertRaises(Exception) as cm: suite.debug() - self.assertEqual(str(cm.exception), 'CleanUpExc') + self.assertExceptionIsLike(cm.exception, + ExceptionGroup('module cleanup failed', + [CustomError('CleanUpExc')])) self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test', 'tearDownClass', 'tearDownModule', 'cleanup_exc']) self.assertEqual(unittest.case._module_cleanups, []) diff --git a/Lib/test/test_unittest/test_util.py b/Lib/test/test_unittest/test_util.py index d590a333930..09ce09b91b7 100644 --- a/Lib/test/test_unittest/test_util.py +++ b/Lib/test/test_unittest/test_util.py @@ -26,6 +26,39 @@ def test_sorted_list_difference(self): self.assertEqual(sorted_list_difference([2], [1, 1]), ([2], [1])) self.assertEqual(sorted_list_difference([1, 2], [1, 1]), ([2], [])) + def test_sorted_list_difference_tail_deduplication(self): + # Tail deduplication when one list is exhausted before the other. + # These exercise the except-IndexError path in sorted_list_difference. + self.assertEqual(sorted_list_difference([], [0, 0]), ([], [0])) + self.assertEqual(sorted_list_difference([0, 0], []), ([0], [])) + self.assertEqual(sorted_list_difference([], [1, 1, 2, 2]), ([], [1, 2])) + self.assertEqual(sorted_list_difference([1, 1, 2, 2], []), ([1, 2], [])) + # One list exhausts mid-way, leaving duplicated tail in the other. + self.assertEqual(sorted_list_difference([1], [1, 2, 2, 3, 3]), ([], [2, 3])) + self.assertEqual(sorted_list_difference([1, 2, 2, 3, 3], [1]), ([2, 3], [])) + + def test_sorted_list_difference_strings(self): + self.assertEqual( + sorted_list_difference(['a', 'b'], ['b', 'c']), + (['a'], ['c'])) + self.assertEqual( + sorted_list_difference([], ['a', 'a', 'b']), + ([], ['a', 'b'])) + self.assertEqual( + sorted_list_difference(['a', 'a', 'b'], []), + (['a', 'b'], [])) + + def test_sorted_list_difference_unhashable(self): + self.assertEqual( + sorted_list_difference([[1], [2]], [[2], [3]]), + ([[1]], [[3]])) + self.assertEqual( + sorted_list_difference([], [[0], [0]]), + ([], [[0]])) + self.assertEqual( + sorted_list_difference([[0], [0]], []), + ([[0]], [])) + def test_unorderable_list_difference(self): self.assertEqual(unorderable_list_difference([], []), ([], [])) self.assertEqual(unorderable_list_difference([1, 2], []), ([2, 1], [])) diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py index 35e4652a87b..9bc04fc8e0a 100644 --- a/Lib/test/test_unparse.py +++ b/Lib/test/test_unparse.py @@ -216,6 +216,15 @@ def test_tstrings(self): self.check_ast_roundtrip('t""') self.check_ast_roundtrip("t'{(lambda x: x)}'") self.check_ast_roundtrip("t'{t'{x}'}'") + self.check_ast_roundtrip( + r"""t'''{( + 1, # Force lexer metadata reconstruction. + "\"#")}'''""" + ) + self.check_ast_roundtrip( + r'''t"""Value: {value =\ +}"""''' + ) def test_tstring_with_nonsensical_str_field(self): # `value` suggests that the original code is `t'{test1}`, but `str` suggests otherwise @@ -403,6 +412,11 @@ def test_set_comprehension(self): def test_dict_comprehension(self): self.check_ast_roundtrip("{x: x*x for x in range(10)}") + def test_dict_comprehension_unpacking(self): + self.check_ast_roundtrip("{**x for x in ()}") + self.check_ast_roundtrip("{**x for x in range(10)}") + self.check_ast_roundtrip("[*x for x in ()]") + def test_class_decorators(self): self.check_ast_roundtrip(class_decorator) @@ -679,6 +693,24 @@ def test_unparse_interactive_integrity_3(self): self.check_src_roundtrip(src, out, mode='single') + @test.support.subTests('case', [ + "case 'a string':", + "case True:", + "case _:", + "case [*_]:", + "case tuple():", + "case cls(arg, more=arg2):", + "case tuple() | list():", + "case [tuple() as obj, _, {'a': a, **more}]:", + "case -2:", + "case +2:", + "case 2 + 3j:", + "case +2 - 3j:", + ]) + def test_unparse_match(self, case): + src = 'match x:\n ' + case + '\n pass' + self.check_src_roundtrip(src) + class CosmeticTestCase(ASTTestCase): """Test if there are cosmetic issues caused by unnecessary additions""" diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index 1e5f79998e7..ab59727a8fd 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -807,6 +807,61 @@ def test_short_content_raises_ContentTooShortError_without_reporthook(self): self.unfakehttp() +class urlcleanup_Tests(unittest.TestCase, FakeHTTPMixin): + """Test urllib.request.urlcleanup()""" + + def setUp(self): + self.addCleanup(urllib.request.urlcleanup) + + def urlretrieve(self): + self.fakehttp(b'HTTP/1.1 200 OK\r\n\r\ndata') + try: + filename, headers = urllib.request.urlretrieve( + support.TEST_HTTP_URL) + finally: + self.unfakehttp() + self.addCleanup(os_helper.unlink, filename) + return filename + + def fake_urlopen(self, data): + self.fakehttp(b'HTTP/1.1 200 OK\r\n\r\n' + data) + try: + with urllib.request.urlopen(support.TEST_HTTP_URL) as fp: + return fp.read() + finally: + self.unfakehttp() + + def test_temporary_files(self): + filename = self.urlretrieve() + self.assertTrue(os.path.exists(filename)) + + urllib.request.urlcleanup() + self.assertFalse(os.path.exists(filename)) + + # A file created after the cleanup is not deleted. + os_helper.create_empty_file(filename) + urllib.request.urlcleanup() + self.assertTrue(os.path.exists(filename)) + + def test_opener(self): + # The implicitly created opener supports http. + self.assertEqual(self.fake_urlopen(b'first'), b'first') + + # An installed opener replaces it and supports only its handlers. + opener = urllib.request.OpenerDirector() + opener.add_handler(urllib.request.DataHandler()) + opener.add_handler(urllib.request.UnknownHandler()) + urllib.request.install_opener(opener) + with urllib.request.urlopen('data:,hello') as fp: + self.assertEqual(fp.read(), b'hello') + with self.assertRaises(urllib.error.URLError): + self.fake_urlopen(b'') + + # urlcleanup() resets the opener. + urllib.request.urlcleanup() + self.assertEqual(self.fake_urlopen(b'second'), b'second') + + class QuotingTests(unittest.TestCase): r"""Tests for urllib.quote() and urllib.quote_plus() diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index eeea9cda2f4..7efbc81a160 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -270,6 +270,50 @@ def test_password_manager_default_port(self): self.assertEqual(find_user_pass("i", "http://j.example.com:80"), (None, None)) + def test_password_manager_scheme(self): + mgr = urllib.request.HTTPPasswordMgr() + mgr.add_password( + "realm", "https://example.com/", "user", "password") + + self.assertEqual( + mgr.find_user_password("realm", "https://example.com/"), + ("user", "password")) + self.assertEqual( + mgr.find_user_password("realm", "http://example.com/"), + (None, None)) + # Support an authority without a scheme. + self.assertEqual( + mgr.find_user_password("realm", "example.com"), + ("user", "password")) + # An authority without a scheme continues to match any scheme. + mgr.add_password( + "realm", "schemeless.example.com", "user", "password") + for scheme in "http", "https": + with self.subTest(scheme=scheme): + self.assertEqual( + mgr.find_user_password( + "realm", f"{scheme}://schemeless.example.com/"), + ("user", "password")) + + # A network-path reference also has no scheme. + mgr.add_password( + "realm", "//network-path.example.com/", "user", "password") + self.assertEqual( + mgr.find_user_password( + "realm", "https://network-path.example.com/"), + ("user", "password")) + + def test_password_manager_reduced_uri(self): + mgr = urllib.request.HTTPPasswordMgr() + + self.assertEqual( + mgr.reduce_uri("http://example.com/path"), + ("example.com:80", "/path")) + self.assertTrue( + mgr.is_suburi( + ("example.com", "/path"), + ("example.com", "/path/subpath"))) + class MockOpener: addheaders = [] @@ -577,6 +621,23 @@ class NonHandler(object): self.assertRaises(TypeError, OpenerDirector().add_handler, NonHandler()) + def test_no_protocol_methods(self): + # test the case that methods starts with handler type without the protocol + # like open*() or _open*(). + # These methods should be ignored + + o = OpenerDirector() + meth_spec = [ + ["open"], + ["_open"], + ["error"] + ] + + add_ordered_mock_handlers(o, meth_spec) + + self.assertEqual(len(o.handle_open), 0) + self.assertEqual(len(o.handle_error), 0) + def test_badly_named_methods(self): # test work-around for three methods that accidentally follow the # naming conventions for handler methods @@ -1808,6 +1869,18 @@ def test_basic_prior_auth_auto_send(self): # expect request to be sent with auth header self.assertTrue(http_handler.has_auth_header) + def test_basic_prior_auth_different_scheme(self): + pwd_manager = HTTPPasswordMgrWithPriorAuth() + auth_handler = HTTPBasicAuthHandler(pwd_manager) + auth_handler.add_password( + None, "https://example.com/", "user", "password", + is_authenticated=True) + + request = Request("http://example.com/") + auth_handler.http_request(request) + + self.assertFalse(request.has_header("Authorization")) + def test_basic_prior_auth_send_after_first_success(self): # Auto send auth header after authentication is successful once diff --git a/Lib/test/test_urllib2net.py b/Lib/test/test_urllib2net.py index d015267cefd..17db686942f 100644 --- a/Lib/test/test_urllib2net.py +++ b/Lib/test/test_urllib2net.py @@ -259,27 +259,6 @@ def test_custom_headers(self): opener.open(request) self.assertEqual(request.get_header('User-agent'),'Test-Agent') - @unittest.skip('XXX: http://www.imdb.com is gone') - def test_sites_no_connection_close(self): - # Some sites do not send Connection: close header. - # Verify that those work properly. (#issue12576) - - URL = 'http://www.imdb.com' # mangles Connection:close - - with socket_helper.transient_internet(URL): - try: - with urllib.request.urlopen(URL) as res: - pass - except ValueError: - self.fail("urlopen failed for site not sending \ - Connection:close") - else: - self.assertTrue(res) - - req = urllib.request.urlopen(URL) - res = req.read() - self.assertTrue(res) - def _test_urls(self, urls, handlers, retry=True): import time import logging diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py index 98f0b190ee7..a5b7966c778 100644 --- a/Lib/test/test_urlparse.py +++ b/Lib/test/test_urlparse.py @@ -1,7 +1,10 @@ +import copy +import pickle import sys import unicodedata import unittest import urllib.parse +from urllib.parse import urldefrag, urlparse, urlsplit, urlunparse, urlunsplit from test import support RFC1808_BASE = "http://a/b/c/d;p?q#f" @@ -107,19 +110,46 @@ class UrlParseTestCase(unittest.TestCase): def checkRoundtrips(self, url, parsed, split, url2=None): if url2 is None: url2 = url - result = urllib.parse.urlparse(url) + self.checkRoundtrips1(url, parsed, split, missing_as_none=True) + empty = url[:0] + parsed = tuple(x or empty for x in parsed) + split = tuple(x or empty for x in split) + self.checkRoundtrips1(url, parsed, split, url2, missing_as_none=False) + + result = urlparse(url, missing_as_none=True) + self.assertEqual(urlunparse(result, keep_empty=False), url2) + self.assertEqual(urlunparse(tuple(result), keep_empty=False), url2) + result = urlparse(url, missing_as_none=False) + with self.assertRaises(ValueError): + urlunparse(result, keep_empty=True) + urlunparse(tuple(result), keep_empty=True) + + result = urlsplit(url, missing_as_none=True) + self.assertEqual(urlunsplit(result, keep_empty=False), url2) + self.assertEqual(urlunsplit(tuple(result), keep_empty=False), url2) + result = urlsplit(url, missing_as_none=False) + with self.assertRaises(ValueError): + urlunsplit(result, keep_empty=True) + urlunsplit(tuple(result), keep_empty=True) + + def checkRoundtrips1(self, url, parsed, split, url2=None, *, missing_as_none): + if url2 is None: + url2 = url + result = urlparse(url, missing_as_none=missing_as_none) self.assertSequenceEqual(result, parsed) t = (result.scheme, result.netloc, result.path, result.params, result.query, result.fragment) self.assertSequenceEqual(t, parsed) # put it back together and it should be the same - result2 = urllib.parse.urlunparse(result) - self.assertSequenceEqual(result2, url2) - self.assertSequenceEqual(result2, result.geturl()) + result2 = urlunparse(result) + self.assertEqual(result2, url2) + self.assertEqual(result2, result.geturl()) + self.assertEqual(urlunparse(result, keep_empty=missing_as_none), url2) + self.assertEqual(urlunparse(tuple(result), keep_empty=missing_as_none), result2) # the result of geturl() is a fixpoint; we can always parse it # again to get the same result: - result3 = urllib.parse.urlparse(result.geturl()) + result3 = urlparse(result.geturl(), missing_as_none=missing_as_none) self.assertEqual(result3.geturl(), result.geturl()) self.assertSequenceEqual(result3, result) self.assertEqual(result3.scheme, result.scheme) @@ -134,17 +164,18 @@ def checkRoundtrips(self, url, parsed, split, url2=None): self.assertEqual(result3.port, result.port) # check the roundtrip using urlsplit() as well - result = urllib.parse.urlsplit(url) + result = urlsplit(url, missing_as_none=missing_as_none) self.assertSequenceEqual(result, split) t = (result.scheme, result.netloc, result.path, - result.query, result.fragment) + result.query, result.fragment) self.assertSequenceEqual(t, split) - result2 = urllib.parse.urlunsplit(result) - self.assertSequenceEqual(result2, url2) - self.assertSequenceEqual(result2, result.geturl()) + result2 = urlunsplit(result) + self.assertEqual(result2, url2) + self.assertEqual(result2, result.geturl()) + self.assertEqual(urlunsplit(tuple(result), keep_empty=missing_as_none), result2) # check the fixpoint property of re-parsing the result of geturl() - result3 = urllib.parse.urlsplit(result.geturl()) + result3 = urlsplit(result.geturl(), missing_as_none=missing_as_none) self.assertEqual(result3.geturl(), result.geturl()) self.assertSequenceEqual(result3, result) self.assertEqual(result3.scheme, result.scheme) @@ -177,94 +208,94 @@ def test_qs(self, orig, expect): @support.subTests('bytes', (False, True)) @support.subTests('url,parsed,split', [ ('path/to/file', - ('', '', 'path/to/file', '', '', ''), - ('', '', 'path/to/file', '', '')), + (None, None, 'path/to/file', None, None, None), + (None, None, 'path/to/file', None, None)), ('/path/to/file', - ('', '', '/path/to/file', '', '', ''), - ('', '', '/path/to/file', '', '')), + (None, None, '/path/to/file', None, None, None), + (None, None, '/path/to/file', None, None)), ('//path/to/file', - ('', 'path', '/to/file', '', '', ''), - ('', 'path', '/to/file', '', '')), + (None, 'path', '/to/file', None, None, None), + (None, 'path', '/to/file', None, None)), ('////path/to/file', - ('', '', '//path/to/file', '', '', ''), - ('', '', '//path/to/file', '', '')), + (None, '', '//path/to/file', None, None, None), + (None, '', '//path/to/file', None, None)), ('/////path/to/file', - ('', '', '///path/to/file', '', '', ''), - ('', '', '///path/to/file', '', '')), + (None, '', '///path/to/file', None, None, None), + (None, '', '///path/to/file', None, None)), ('scheme:path/to/file', - ('scheme', '', 'path/to/file', '', '', ''), - ('scheme', '', 'path/to/file', '', '')), + ('scheme', None, 'path/to/file', None, None, None), + ('scheme', None, 'path/to/file', None, None)), ('scheme:/path/to/file', - ('scheme', '', '/path/to/file', '', '', ''), - ('scheme', '', '/path/to/file', '', '')), + ('scheme', None, '/path/to/file', None, None, None), + ('scheme', None, '/path/to/file', None, None)), ('scheme://path/to/file', - ('scheme', 'path', '/to/file', '', '', ''), - ('scheme', 'path', '/to/file', '', '')), + ('scheme', 'path', '/to/file', None, None, None), + ('scheme', 'path', '/to/file', None, None)), ('scheme:////path/to/file', - ('scheme', '', '//path/to/file', '', '', ''), - ('scheme', '', '//path/to/file', '', '')), + ('scheme', '', '//path/to/file', None, None, None), + ('scheme', '', '//path/to/file', None, None)), ('scheme://///path/to/file', - ('scheme', '', '///path/to/file', '', '', ''), - ('scheme', '', '///path/to/file', '', '')), + ('scheme', '', '///path/to/file', None, None, None), + ('scheme', '', '///path/to/file', None, None)), ('file:tmp/junk.txt', - ('file', '', 'tmp/junk.txt', '', '', ''), - ('file', '', 'tmp/junk.txt', '', '')), + ('file', None, 'tmp/junk.txt', None, None, None), + ('file', None, 'tmp/junk.txt', None, None)), ('file:///tmp/junk.txt', - ('file', '', '/tmp/junk.txt', '', '', ''), - ('file', '', '/tmp/junk.txt', '', '')), + ('file', '', '/tmp/junk.txt', None, None, None), + ('file', '', '/tmp/junk.txt', None, None)), ('file:////tmp/junk.txt', - ('file', '', '//tmp/junk.txt', '', '', ''), - ('file', '', '//tmp/junk.txt', '', '')), + ('file', '', '//tmp/junk.txt', None, None, None), + ('file', '', '//tmp/junk.txt', None, None)), ('file://///tmp/junk.txt', - ('file', '', '///tmp/junk.txt', '', '', ''), - ('file', '', '///tmp/junk.txt', '', '')), + ('file', '', '///tmp/junk.txt', None, None, None), + ('file', '', '///tmp/junk.txt', None, None)), ('http:tmp/junk.txt', - ('http', '', 'tmp/junk.txt', '', '', ''), - ('http', '', 'tmp/junk.txt', '', '')), + ('http', None, 'tmp/junk.txt', None, None, None), + ('http', None, 'tmp/junk.txt', None, None)), ('http://example.com/tmp/junk.txt', - ('http', 'example.com', '/tmp/junk.txt', '', '', ''), - ('http', 'example.com', '/tmp/junk.txt', '', '')), + ('http', 'example.com', '/tmp/junk.txt', None, None, None), + ('http', 'example.com', '/tmp/junk.txt', None, None)), ('http:///example.com/tmp/junk.txt', - ('http', '', '/example.com/tmp/junk.txt', '', '', ''), - ('http', '', '/example.com/tmp/junk.txt', '', '')), + ('http', '', '/example.com/tmp/junk.txt', None, None, None), + ('http', '', '/example.com/tmp/junk.txt', None, None)), ('http:////example.com/tmp/junk.txt', - ('http', '', '//example.com/tmp/junk.txt', '', '', ''), - ('http', '', '//example.com/tmp/junk.txt', '', '')), + ('http', '', '//example.com/tmp/junk.txt', None, None, None), + ('http', '', '//example.com/tmp/junk.txt', None, None)), ('imap://mail.python.org/mbox1', - ('imap', 'mail.python.org', '/mbox1', '', '', ''), - ('imap', 'mail.python.org', '/mbox1', '', '')), + ('imap', 'mail.python.org', '/mbox1', None, None, None), + ('imap', 'mail.python.org', '/mbox1', None, None)), ('mms://wms.sys.hinet.net/cts/Drama/09006251100.asf', ('mms', 'wms.sys.hinet.net', '/cts/Drama/09006251100.asf', - '', '', ''), + None, None, None), ('mms', 'wms.sys.hinet.net', '/cts/Drama/09006251100.asf', - '', '')), + None, None)), ('nfs://server/path/to/file.txt', - ('nfs', 'server', '/path/to/file.txt', '', '', ''), - ('nfs', 'server', '/path/to/file.txt', '', '')), + ('nfs', 'server', '/path/to/file.txt', None, None, None), + ('nfs', 'server', '/path/to/file.txt', None, None)), ('svn+ssh://svn.zope.org/repos/main/ZConfig/trunk/', ('svn+ssh', 'svn.zope.org', '/repos/main/ZConfig/trunk/', - '', '', ''), + None, None, None), ('svn+ssh', 'svn.zope.org', '/repos/main/ZConfig/trunk/', - '', '')), + None, None)), ('git+ssh://git@github.com/user/project.git', ('git+ssh', 'git@github.com','/user/project.git', - '','',''), + None,None,None), ('git+ssh', 'git@github.com','/user/project.git', - '', '')), + None, None)), ('itms-services://?action=download-manifest&url=https://example.com/app', - ('itms-services', '', '', '', - 'action=download-manifest&url=https://example.com/app', ''), + ('itms-services', '', '', None, + 'action=download-manifest&url=https://example.com/app', None), ('itms-services', '', '', - 'action=download-manifest&url=https://example.com/app', '')), + 'action=download-manifest&url=https://example.com/app', None)), ('+scheme:path/to/file', - ('', '', '+scheme:path/to/file', '', '', ''), - ('', '', '+scheme:path/to/file', '', '')), + (None, None, '+scheme:path/to/file', None, None, None), + (None, None, '+scheme:path/to/file', None, None)), ('sch_me:path/to/file', - ('', '', 'sch_me:path/to/file', '', '', ''), - ('', '', 'sch_me:path/to/file', '', '')), + (None, None, 'sch_me:path/to/file', None, None, None), + (None, None, 'sch_me:path/to/file', None, None)), ('schème:path/to/file', - ('', '', 'schème:path/to/file', '', '', ''), - ('', '', 'schème:path/to/file', '', '')), + (None, None, 'schème:path/to/file', None, None, None), + (None, None, 'schème:path/to/file', None, None)), ]) def test_roundtrips(self, bytes, url, parsed, split): if bytes: @@ -279,24 +310,24 @@ def test_roundtrips(self, bytes, url, parsed, split): @support.subTests('url,url2,parsed,split', [ ('///path/to/file', '/path/to/file', - ('', '', '/path/to/file', '', '', ''), - ('', '', '/path/to/file', '', '')), + (None, '', '/path/to/file', None, None, None), + (None, '', '/path/to/file', None, None)), ('scheme:///path/to/file', 'scheme:/path/to/file', - ('scheme', '', '/path/to/file', '', '', ''), - ('scheme', '', '/path/to/file', '', '')), + ('scheme', '', '/path/to/file', None, None, None), + ('scheme', '', '/path/to/file', None, None)), ('file:/tmp/junk.txt', 'file:///tmp/junk.txt', - ('file', '', '/tmp/junk.txt', '', '', ''), - ('file', '', '/tmp/junk.txt', '', '')), + ('file', None, '/tmp/junk.txt', None, None, None), + ('file', None, '/tmp/junk.txt', None, None)), ('http:/tmp/junk.txt', 'http:///tmp/junk.txt', - ('http', '', '/tmp/junk.txt', '', '', ''), - ('http', '', '/tmp/junk.txt', '', '')), + ('http', None, '/tmp/junk.txt', None, None, None), + ('http', None, '/tmp/junk.txt', None, None)), ('https:/tmp/junk.txt', 'https:///tmp/junk.txt', - ('https', '', '/tmp/junk.txt', '', '', ''), - ('https', '', '/tmp/junk.txt', '', '')), + ('https', None, '/tmp/junk.txt', None, None, None), + ('https', None, '/tmp/junk.txt', None, None)), ]) def test_roundtrips_normalization(self, bytes, url, url2, parsed, split): if bytes: @@ -310,17 +341,17 @@ def test_roundtrips_normalization(self, bytes, url, url2, parsed, split): @support.subTests('scheme', ('http', 'https')) @support.subTests('url,parsed,split', [ ('://www.python.org', - ('www.python.org', '', '', '', ''), - ('www.python.org', '', '', '')), + ('www.python.org', '', None, None, None), + ('www.python.org', '', None, None)), ('://www.python.org#abc', - ('www.python.org', '', '', '', 'abc'), - ('www.python.org', '', '', 'abc')), + ('www.python.org', '', None, None, 'abc'), + ('www.python.org', '', None, 'abc')), ('://www.python.org?q=abc', - ('www.python.org', '', '', 'q=abc', ''), - ('www.python.org', '', 'q=abc', '')), + ('www.python.org', '', None, 'q=abc', None), + ('www.python.org', '', 'q=abc', None)), ('://www.python.org/#abc', - ('www.python.org', '/', '', '', 'abc'), - ('www.python.org', '/', '', 'abc')), + ('www.python.org', '/', None, None, 'abc'), + ('www.python.org', '/', None, 'abc')), ('://a/b/c/d;p?q#f', ('a', '/b/c/d', 'p', 'q', 'f'), ('a', '/b/c/d;p', 'q', 'f')), @@ -342,16 +373,21 @@ def test_http_roundtrips(self, bytes, scheme, url, parsed, split): def checkJoin(self, base, relurl, expected, *, relroundtrip=True): with self.subTest(base=base, relurl=relurl): self.assertEqual(urllib.parse.urljoin(base, relurl), expected) - baseb = base.encode('ascii') - relurlb = relurl.encode('ascii') - expectedb = expected.encode('ascii') + baseb = str_encode(base) + relurlb = str_encode(relurl) + expectedb = str_encode(expected) self.assertEqual(urllib.parse.urljoin(baseb, relurlb), expectedb) if relroundtrip: - relurl = urllib.parse.urlunsplit(urllib.parse.urlsplit(relurl)) - self.assertEqual(urllib.parse.urljoin(base, relurl), expected) - relurlb = urllib.parse.urlunsplit(urllib.parse.urlsplit(relurlb)) - self.assertEqual(urllib.parse.urljoin(baseb, relurlb), expectedb) + relurl2 = urlunsplit(urlsplit(relurl)) + self.assertEqual(urllib.parse.urljoin(base, relurl2), expected) + relurlb2 = urlunsplit(urlsplit(relurlb)) + self.assertEqual(urllib.parse.urljoin(baseb, relurlb2), expectedb) + + relurl3 = urlunsplit(urlsplit(relurl, missing_as_none=True)) + self.assertEqual(urllib.parse.urljoin(base, relurl3), expected) + relurlb3 = urlunsplit(urlsplit(relurlb, missing_as_none=True)) + self.assertEqual(urllib.parse.urljoin(baseb, relurlb3), expectedb) @support.subTests('bytes', (False, True)) @support.subTests('u', ['Python', './Python','x-newscheme://foo.com/stuff','x://y','x:/y','x:/','/',]) @@ -387,7 +423,7 @@ def test_RFC1808(self): self.checkJoin(RFC1808_BASE, '../../g', 'http://a/g') # "abnormal" cases from RFC 1808: - self.checkJoin(RFC1808_BASE, '', 'http://a/b/c/d;p?q#f') + self.checkJoin(RFC1808_BASE, None, 'http://a/b/c/d;p?q#f') self.checkJoin(RFC1808_BASE, 'g.', 'http://a/b/c/g.') self.checkJoin(RFC1808_BASE, '.g', 'http://a/b/c/.g') self.checkJoin(RFC1808_BASE, 'g..', 'http://a/b/c/g..') @@ -411,8 +447,10 @@ def test_RFC1808(self): def test_RFC2368(self): # Issue 11467: path that starts with a number is not parsed correctly - self.assertEqual(urllib.parse.urlparse('mailto:1337@example.org'), + self.assertEqual(urlparse('mailto:1337@example.org'), ('mailto', '', '1337@example.org', '', '', '')) + self.assertEqual(urlparse('mailto:1337@example.org', missing_as_none=True), + ('mailto', None, '1337@example.org', None, None, None)) def test_RFC2396(self): # cases from RFC 2396 @@ -741,18 +779,18 @@ def test_RFC2732_invalid(self, bytes, invalid_url): @support.subTests('bytes', (False, True)) @support.subTests('url,defrag,frag', [ ('http://python.org#frag', 'http://python.org', 'frag'), - ('http://python.org', 'http://python.org', ''), + ('http://python.org', 'http://python.org', None), ('http://python.org/#frag', 'http://python.org/', 'frag'), - ('http://python.org/', 'http://python.org/', ''), + ('http://python.org/', 'http://python.org/', None), ('http://python.org/?q#frag', 'http://python.org/?q', 'frag'), - ('http://python.org/?q', 'http://python.org/?q', ''), + ('http://python.org/?q', 'http://python.org/?q', None), ('http://python.org/p#frag', 'http://python.org/p', 'frag'), - ('http://python.org/p?q', 'http://python.org/p?q', ''), + ('http://python.org/p?q', 'http://python.org/p?q', None), (RFC1808_BASE, 'http://a/b/c/d;p?q', 'f'), - (RFC2396_BASE, 'http://a/b/c/d;p?q', ''), + (RFC2396_BASE, 'http://a/b/c/d;p?q', None), ('http://a/b/c;p?q#f', 'http://a/b/c;p?q', 'f'), ('http://a/b/c;p?q#', 'http://a/b/c;p?q', ''), - ('http://a/b/c;p?q', 'http://a/b/c;p?q', ''), + ('http://a/b/c;p?q', 'http://a/b/c;p?q', None), ('http://a/b/c;p?#f', 'http://a/b/c;p?', 'f'), ('http://a/b/c;p#f', 'http://a/b/c;p', 'f'), ('http://a/b/c;?q#f', 'http://a/b/c;?q', 'f'), @@ -764,14 +802,19 @@ def test_RFC2732_invalid(self, bytes, invalid_url): ('//a/b/c;p?q#f', '//a/b/c;p?q', 'f'), ('://a/b/c;p?q#f', '://a/b/c;p?q', 'f'), ]) - def test_urldefrag(self, bytes, url, defrag, frag): + @support.subTests('missing_as_none', (False, True)) + def test_urldefrag(self, bytes, url, defrag, frag, missing_as_none): if bytes: url = str_encode(url) defrag = str_encode(defrag) frag = str_encode(frag) - result = urllib.parse.urldefrag(url) - hash = '#' if isinstance(url, str) else b'#' - self.assertEqual(result.geturl(), url.rstrip(hash)) + result = urllib.parse.urldefrag(url, missing_as_none=missing_as_none) + if not missing_as_none: + hash = '#' if isinstance(url, str) else b'#' + url = url.rstrip(hash) + if frag is None: + frag = url[:0] + self.assertEqual(result.geturl(), url) self.assertEqual(result, (defrag, frag)) self.assertEqual(result.url, defrag) self.assertEqual(result.fragment, frag) @@ -1001,26 +1044,27 @@ def test_attributes_bad_scheme(self, bytes, parse, scheme): if not url.isascii(): self.skipTest('non-ASCII bytes') url = url.encode("ascii") - p = parse(url) - self.assertEqual(p.scheme, b"" if bytes else "") + p = parse(url, missing_as_none=True) + self.assertIsNone(p.scheme) - def test_attributes_without_netloc(self): + @support.subTests('missing_as_none', (False, True)) + def test_attributes_without_netloc(self, missing_as_none): # This example is straight from RFC 3261. It looks like it # should allow the username, hostname, and port to be filled # in, but doesn't. Since it's a URI and doesn't use the # scheme://netloc syntax, the netloc and related attributes # should be left empty. uri = "sip:alice@atlanta.com;maddr=239.255.255.1;ttl=15" - p = urllib.parse.urlsplit(uri) - self.assertEqual(p.netloc, "") + p = urllib.parse.urlsplit(uri, missing_as_none=missing_as_none) + self.assertEqual(p.netloc, None if missing_as_none else "") self.assertEqual(p.username, None) self.assertEqual(p.password, None) self.assertEqual(p.hostname, None) self.assertEqual(p.port, None) self.assertEqual(p.geturl(), uri) - p = urllib.parse.urlparse(uri) - self.assertEqual(p.netloc, "") + p = urllib.parse.urlparse(uri, missing_as_none=missing_as_none) + self.assertEqual(p.netloc, None if missing_as_none else "") self.assertEqual(p.username, None) self.assertEqual(p.password, None) self.assertEqual(p.hostname, None) @@ -1029,16 +1073,16 @@ def test_attributes_without_netloc(self): # You guessed it, repeating the test with bytes input uri = b"sip:alice@atlanta.com;maddr=239.255.255.1;ttl=15" - p = urllib.parse.urlsplit(uri) - self.assertEqual(p.netloc, b"") + p = urllib.parse.urlsplit(uri, missing_as_none=missing_as_none) + self.assertEqual(p.netloc, None if missing_as_none else b"") self.assertEqual(p.username, None) self.assertEqual(p.password, None) self.assertEqual(p.hostname, None) self.assertEqual(p.port, None) self.assertEqual(p.geturl(), uri) - p = urllib.parse.urlparse(uri) - self.assertEqual(p.netloc, b"") + p = urllib.parse.urlparse(uri, missing_as_none=missing_as_none) + self.assertEqual(p.netloc, None if missing_as_none else b"") self.assertEqual(p.username, None) self.assertEqual(p.password, None) self.assertEqual(p.hostname, None) @@ -1052,67 +1096,86 @@ def test_noslash(self): self.assertEqual(urllib.parse.urlparse(b"http://example.com?blahblah=/foo"), (b'http', b'example.com', b'', b'', b'blahblah=/foo', b'')) - def test_withoutscheme(self): + @support.subTests('missing_as_none', (False, True)) + def test_withoutscheme(self, missing_as_none): # Test urlparse without scheme # Issue 754016: urlparse goes wrong with IP:port without scheme # RFC 1808 specifies that netloc should start with //, urlparse expects # the same, otherwise it classifies the portion of url as path. - self.assertEqual(urllib.parse.urlparse("path"), - ('','','path','','','')) - self.assertEqual(urllib.parse.urlparse("//www.python.org:80"), - ('','www.python.org:80','','','','')) - self.assertEqual(urllib.parse.urlparse("http://www.python.org:80"), - ('http','www.python.org:80','','','','')) + none = None if missing_as_none else '' + self.assertEqual(urlparse("path", missing_as_none=missing_as_none), + (none, none, 'path', none, none, none)) + self.assertEqual(urlparse("//www.python.org:80", missing_as_none=missing_as_none), + (none, 'www.python.org:80', '', none, none, none)) + self.assertEqual(urlparse("http://www.python.org:80", missing_as_none=missing_as_none), + ('http', 'www.python.org:80', '', none, none, none)) # Repeat for bytes input - self.assertEqual(urllib.parse.urlparse(b"path"), - (b'',b'',b'path',b'',b'',b'')) - self.assertEqual(urllib.parse.urlparse(b"//www.python.org:80"), - (b'',b'www.python.org:80',b'',b'',b'',b'')) - self.assertEqual(urllib.parse.urlparse(b"http://www.python.org:80"), - (b'http',b'www.python.org:80',b'',b'',b'',b'')) - - def test_portseparator(self): + none = None if missing_as_none else b'' + self.assertEqual(urlparse(b"path", missing_as_none=missing_as_none), + (none, none, b'path', none, none, none)) + self.assertEqual(urlparse(b"//www.python.org:80", missing_as_none=missing_as_none), + (none, b'www.python.org:80', b'', none, none, none)) + self.assertEqual(urlparse(b"http://www.python.org:80", missing_as_none=missing_as_none), + (b'http', b'www.python.org:80', b'', none, none, none)) + + @support.subTests('missing_as_none', (False, True)) + def test_portseparator(self, missing_as_none): # Issue 754016 makes changes for port separator ':' from scheme separator - self.assertEqual(urllib.parse.urlparse("http:80"), ('http','','80','','','')) - self.assertEqual(urllib.parse.urlparse("https:80"), ('https','','80','','','')) - self.assertEqual(urllib.parse.urlparse("path:80"), ('path','','80','','','')) - self.assertEqual(urllib.parse.urlparse("http:"),('http','','','','','')) - self.assertEqual(urllib.parse.urlparse("https:"),('https','','','','','')) - self.assertEqual(urllib.parse.urlparse("http://www.python.org:80"), - ('http','www.python.org:80','','','','')) + none = None if missing_as_none else '' + self.assertEqual(urlparse("http:80", missing_as_none=missing_as_none), + ('http', none, '80', none, none, none)) + self.assertEqual(urlparse("https:80", missing_as_none=missing_as_none), + ('https', none, '80', none, none, none)) + self.assertEqual(urlparse("path:80", missing_as_none=missing_as_none), + ('path', none, '80', none, none, none)) + self.assertEqual(urlparse("http:", missing_as_none=missing_as_none), + ('http', none, '', none, none, none)) + self.assertEqual(urlparse("https:", missing_as_none=missing_as_none), + ('https', none, '', none, none, none)) + self.assertEqual(urlparse("http://www.python.org:80", missing_as_none=missing_as_none), + ('http', 'www.python.org:80', '', none, none, none)) # As usual, need to check bytes input as well - self.assertEqual(urllib.parse.urlparse(b"http:80"), (b'http',b'',b'80',b'',b'',b'')) - self.assertEqual(urllib.parse.urlparse(b"https:80"), (b'https',b'',b'80',b'',b'',b'')) - self.assertEqual(urllib.parse.urlparse(b"path:80"), (b'path',b'',b'80',b'',b'',b'')) - self.assertEqual(urllib.parse.urlparse(b"http:"),(b'http',b'',b'',b'',b'',b'')) - self.assertEqual(urllib.parse.urlparse(b"https:"),(b'https',b'',b'',b'',b'',b'')) - self.assertEqual(urllib.parse.urlparse(b"http://www.python.org:80"), - (b'http',b'www.python.org:80',b'',b'',b'',b'')) + none = None if missing_as_none else b'' + self.assertEqual(urlparse(b"http:80", missing_as_none=missing_as_none), + (b'http', none, b'80', none, none, none)) + self.assertEqual(urlparse(b"https:80", missing_as_none=missing_as_none), + (b'https', none, b'80', none, none, none)) + self.assertEqual(urlparse(b"path:80", missing_as_none=missing_as_none), + (b'path', none, b'80', none, none, none)) + self.assertEqual(urlparse(b"http:", missing_as_none=missing_as_none), + (b'http', none, b'', none, none, none)) + self.assertEqual(urlparse(b"https:", missing_as_none=missing_as_none), + (b'https', none, b'', none, none, none)) + self.assertEqual(urlparse(b"http://www.python.org:80", missing_as_none=missing_as_none), + (b'http', b'www.python.org:80', b'', none, none, none)) def test_usingsys(self): # Issue 3314: sys module is used in the error self.assertRaises(TypeError, urllib.parse.urlencode, "foo") - def test_anyscheme(self): + @support.subTests('missing_as_none', (False, True)) + def test_anyscheme(self, missing_as_none): # Issue 7904: s3://foo.com/stuff has netloc "foo.com". - self.assertEqual(urllib.parse.urlparse("s3://foo.com/stuff"), - ('s3', 'foo.com', '/stuff', '', '', '')) - self.assertEqual(urllib.parse.urlparse("x-newscheme://foo.com/stuff"), - ('x-newscheme', 'foo.com', '/stuff', '', '', '')) - self.assertEqual(urllib.parse.urlparse("x-newscheme://foo.com/stuff?query#fragment"), - ('x-newscheme', 'foo.com', '/stuff', '', 'query', 'fragment')) - self.assertEqual(urllib.parse.urlparse("x-newscheme://foo.com/stuff?query"), - ('x-newscheme', 'foo.com', '/stuff', '', 'query', '')) + none = None if missing_as_none else '' + self.assertEqual(urlparse("s3://foo.com/stuff", missing_as_none=missing_as_none), + ('s3', 'foo.com', '/stuff', none, none, none)) + self.assertEqual(urlparse("x-newscheme://foo.com/stuff", missing_as_none=missing_as_none), + ('x-newscheme', 'foo.com', '/stuff', none, none, none)) + self.assertEqual(urlparse("x-newscheme://foo.com/stuff?query#fragment", missing_as_none=missing_as_none), + ('x-newscheme', 'foo.com', '/stuff', none, 'query', 'fragment')) + self.assertEqual(urlparse("x-newscheme://foo.com/stuff?query", missing_as_none=missing_as_none), + ('x-newscheme', 'foo.com', '/stuff', none, 'query', none)) # And for bytes... - self.assertEqual(urllib.parse.urlparse(b"s3://foo.com/stuff"), - (b's3', b'foo.com', b'/stuff', b'', b'', b'')) - self.assertEqual(urllib.parse.urlparse(b"x-newscheme://foo.com/stuff"), - (b'x-newscheme', b'foo.com', b'/stuff', b'', b'', b'')) - self.assertEqual(urllib.parse.urlparse(b"x-newscheme://foo.com/stuff?query#fragment"), - (b'x-newscheme', b'foo.com', b'/stuff', b'', b'query', b'fragment')) - self.assertEqual(urllib.parse.urlparse(b"x-newscheme://foo.com/stuff?query"), - (b'x-newscheme', b'foo.com', b'/stuff', b'', b'query', b'')) + none = None if missing_as_none else b'' + self.assertEqual(urlparse(b"s3://foo.com/stuff", missing_as_none=missing_as_none), + (b's3', b'foo.com', b'/stuff', none, none, none)) + self.assertEqual(urlparse(b"x-newscheme://foo.com/stuff", missing_as_none=missing_as_none), + (b'x-newscheme', b'foo.com', b'/stuff', none, none, none)) + self.assertEqual(urlparse(b"x-newscheme://foo.com/stuff?query#fragment", missing_as_none=missing_as_none), + (b'x-newscheme', b'foo.com', b'/stuff', none, b'query', b'fragment')) + self.assertEqual(urlparse(b"x-newscheme://foo.com/stuff?query", missing_as_none=missing_as_none), + (b'x-newscheme', b'foo.com', b'/stuff', none, b'query', none)) @support.subTests('func', (urllib.parse.urlparse, urllib.parse.urlsplit)) def test_default_scheme(self, func): @@ -1125,8 +1188,11 @@ def test_default_scheme(self, func): self.assertEqual(func("path", scheme="ftp").scheme, "ftp") self.assertEqual(func(b"path", scheme=b"ftp").scheme, b"ftp") self.assertEqual(func("path").scheme, "") + self.assertEqual(func("path", missing_as_none=True).scheme, None) self.assertEqual(func(b"path").scheme, b"") + self.assertEqual(func(b"path", missing_as_none=True).scheme, None) self.assertEqual(func(b"path", "").scheme, b"") + self.assertEqual(func(b"path", "", missing_as_none=True).scheme, b"") @support.subTests('url,attr,expected_frag', ( ("http:#frag", "path", "frag"), @@ -1151,9 +1217,16 @@ def test_parse_fragments(self, url, attr, expected_frag, func): "#" + expected_frag) self.assertEqual(func(url, "", False).fragment, "") + result = func(url, allow_fragments=False, missing_as_none=True) + self.assertIsNone(result.fragment) + self.assertTrue( + getattr(result, attr).endswith("#" + expected_frag)) + self.assertIsNone(func(url, "", False, missing_as_none=True).fragment) + result = func(url, allow_fragments=True) self.assertEqual(result.fragment, expected_frag) - self.assertNotEndsWith(getattr(result, attr), expected_frag) + self.assertFalse( + getattr(result, attr).endswith(expected_frag)) self.assertEqual(func(url, "", True).fragment, expected_frag) self.assertEqual(func(url).fragment, expected_frag) @@ -1182,19 +1255,10 @@ def test_mixed_types_rejected(self): with self.assertRaisesRegex(TypeError, "Cannot mix str"): urllib.parse.urljoin(b"http://python.org", "http://python.org") - @support.subTests('result_type', [ - urllib.parse.DefragResult, - urllib.parse.SplitResult, - urllib.parse.ParseResult, - ]) - def test_result_pairs(self, result_type): - # Check encoding and decoding between result pairs - str_type = result_type - num_args = len(str_type._fields) + def _check_result_type(self, str_type, str_args): bytes_type = str_type._encoded_counterpart self.assertIs(bytes_type._decoded_counterpart, str_type) - str_args = ('',) * num_args - bytes_args = (b'',) * num_args + bytes_args = tuple_encode(str_args) str_result = str_type(*str_args) bytes_result = bytes_type(*bytes_args) encoding = 'ascii' @@ -1213,6 +1277,169 @@ def test_result_pairs(self, result_type): self.assertEqual(str_result.encode(encoding), bytes_result) self.assertEqual(str_result.encode(encoding, errors), bytes_args) self.assertEqual(str_result.encode(encoding, errors), bytes_result) + for result in str_result, bytes_result: + self.assertEqual(copy.copy(result), result) + self.assertEqual(copy.deepcopy(result), result) + self.assertEqual(copy.replace(result), result) + self.assertEqual(result._replace(), result) + + def test_result_pairs__(self): + # Check encoding and decoding between result pairs + self._check_result_type(urllib.parse.DefragResult, ('', '')) + self._check_result_type(urllib.parse.DefragResult, ('', None)) + self._check_result_type(urllib.parse.SplitResult, ('', '', '', '', '')) + self._check_result_type(urllib.parse.SplitResult, (None, None, '', None, None)) + self._check_result_type(urllib.parse.ParseResult, ('', '', '', '', '', '')) + self._check_result_type(urllib.parse.ParseResult, (None, None, '', None, None, None)) + + def test_result_encoding_decoding(self): + def check(str_result, bytes_result): + self.assertEqual(str_result.encode(), bytes_result) + self.assertEqual(str_result.encode().geturl(), bytes_result.geturl()) + self.assertEqual(bytes_result.decode(), str_result) + self.assertEqual(bytes_result.decode().geturl(), str_result.geturl()) + + url = 'http://example.com/?#' + burl = url.encode() + for func in urldefrag, urlsplit, urlparse: + check(func(url, missing_as_none=True), func(burl, missing_as_none=True)) + check(func(url), func(burl)) + + def test_result_copying(self): + def check(result): + result2 = copy.copy(result) + self.assertEqual(result2, result) + self.assertEqual(result2.geturl(), result.geturl()) + result2 = copy.deepcopy(result) + self.assertEqual(result2, result) + self.assertEqual(result2.geturl(), result.geturl()) + result2 = copy.replace(result) + self.assertEqual(result2, result) + self.assertEqual(result2.geturl(), result.geturl()) + result2 = result._replace() + self.assertEqual(result2, result) + self.assertEqual(result2.geturl(), result.geturl()) + + url = 'http://example.com/?#' + burl = url.encode() + for func in urldefrag, urlsplit, urlparse: + check(func(url)) + check(func(url, missing_as_none=True)) + check(func(burl)) + check(func(burl, missing_as_none=True)) + + def test_result_pickling(self): + def check(result): + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + pickled = pickle.dumps(result, proto) + result2 = pickle.loads(pickled) + self.assertEqual(result2, result) + self.assertEqual(result2.geturl(), result.geturl()) + + url = 'http://example.com/?#' + burl = url.encode() + for func in urldefrag, urlsplit, urlparse: + check(func(url)) + check(func(url, missing_as_none=True)) + check(func(burl)) + check(func(burl, missing_as_none=True)) + + def test_result_compat_unpickling(self): + def check(result, pickles): + for pickled in pickles: + result2 = pickle.loads(pickled) + self.assertEqual(result2, result) + self.assertEqual(result2.geturl(), result.geturl()) + + url = 'http://example.com/?#' + burl = url.encode() + # Pre-3.15 data. + check(urldefrag(url), ( + b'ccopy_reg\n_reconstructor\n(curlparse\nDefragResult\nc__builtin__\ntuple\n(Vhttp://example.com/?\nV\nttR.', + b'ccopy_reg\n_reconstructor\n(curlparse\nDefragResult\nc__builtin__\ntuple\n(X\x14\x00\x00\x00http://example.com/?X\x00\x00\x00\x00ttR.', + b'\x80\x02curlparse\nDefragResult\nX\x14\x00\x00\x00http://example.com/?X\x00\x00\x00\x00\x86\x81.', + b'\x80\x03curllib.parse\nDefragResult\nX\x14\x00\x00\x00http://example.com/?X\x00\x00\x00\x00\x86\x81.', + b'\x80\x04\x958\x00\x00\x00\x00\x00\x00\x00\x8c\x0curllib.parse\x8c\x0cDefragResult\x93\x8c\x14http://example.com/?\x8c\x00\x86\x81.', + )) + check(urldefrag(burl), ( + b'ccopy_reg\n_reconstructor\n(curlparse\nDefragResultBytes\nc__builtin__\ntuple\n(c_codecs\nencode\n(Vhttp://example.com/?\nVlatin1\ntRc__builtin__\nbytes\n(tRttR.', + b'ccopy_reg\n_reconstructor\n(curlparse\nDefragResultBytes\nc__builtin__\ntuple\n(c_codecs\nencode\n(X\x14\x00\x00\x00http://example.com/?X\x06\x00\x00\x00latin1tRc__builtin__\nbytes\n)RttR.', + b'\x80\x02curlparse\nDefragResultBytes\nc_codecs\nencode\nX\x14\x00\x00\x00http://example.com/?X\x06\x00\x00\x00latin1\x86Rc__builtin__\nbytes\n)R\x86\x81.', + b'\x80\x03curllib.parse\nDefragResultBytes\nC\x14http://example.com/?C\x00\x86\x81.', + b'\x80\x04\x95=\x00\x00\x00\x00\x00\x00\x00\x8c\x0curllib.parse\x8c\x11DefragResultBytes\x93C\x14http://example.com/?C\x00\x86\x81.', + )) + check(urlsplit(url), ( + b'ccopy_reg\n_reconstructor\n(curlparse\nSplitResult\nc__builtin__\ntuple\n(Vhttp\nVexample.com\nV/\nV\np0\ng0\nttR.', + b'ccopy_reg\n_reconstructor\n(curlparse\nSplitResult\nc__builtin__\ntuple\n(X\x04\x00\x00\x00httpX\x0b\x00\x00\x00example.comX\x01\x00\x00\x00/X\x00\x00\x00\x00q\x00h\x00ttR.', + b'\x80\x02curlparse\nSplitResult\n(X\x04\x00\x00\x00httpX\x0b\x00\x00\x00example.comX\x01\x00\x00\x00/X\x00\x00\x00\x00q\x00h\x00t\x81.', + b'\x80\x03curllib.parse\nSplitResult\n(X\x04\x00\x00\x00httpX\x0b\x00\x00\x00example.comX\x01\x00\x00\x00/X\x00\x00\x00\x00q\x00h\x00t\x81.', + b'\x80\x04\x95;\x00\x00\x00\x00\x00\x00\x00\x8c\x0curllib.parse\x8c\x0bSplitResult\x93(\x8c\x04http\x8c\x0bexample.com\x8c\x01/\x8c\x00\x94h\x00t\x81.', + )) + check(urlsplit(burl), ( + b'ccopy_reg\n_reconstructor\n(curlparse\nSplitResultBytes\nc__builtin__\ntuple\n(c_codecs\nencode\np0\n(Vhttp\nVlatin1\np1\ntRg0\n(Vexample.com\ng1\ntRg0\n(V/\ng1\ntRc__builtin__\nbytes\n(tRp2\ng2\nttR.', + b'ccopy_reg\n_reconstructor\n(curlparse\nSplitResultBytes\nc__builtin__\ntuple\n(c_codecs\nencode\nq\x00(X\x04\x00\x00\x00httpX\x06\x00\x00\x00latin1q\x01tRh\x00(X\x0b\x00\x00\x00example.comh\x01tRh\x00(X\x01\x00\x00\x00/h\x01tRc__builtin__\nbytes\n)Rq\x02h\x02ttR.', + b'\x80\x02curlparse\nSplitResultBytes\n(c_codecs\nencode\nq\x00X\x04\x00\x00\x00httpX\x06\x00\x00\x00latin1q\x01\x86Rh\x00X\x0b\x00\x00\x00example.comh\x01\x86Rh\x00X\x01\x00\x00\x00/h\x01\x86Rc__builtin__\nbytes\n)Rq\x02h\x02t\x81.', + b'\x80\x03curllib.parse\nSplitResultBytes\n(C\x04httpC\x0bexample.comC\x01/C\x00q\x00h\x00t\x81.', + b'\x80\x04\x95@\x00\x00\x00\x00\x00\x00\x00\x8c\x0curllib.parse\x8c\x10SplitResultBytes\x93(C\x04httpC\x0bexample.comC\x01/C\x00\x94h\x00t\x81.', + )) + check(urlparse(url), ( + b'ccopy_reg\n_reconstructor\n(curlparse\nParseResult\nc__builtin__\ntuple\n(Vhttp\nVexample.com\nV/\nV\np0\ng0\ng0\nttR.', + b'ccopy_reg\n_reconstructor\n(curlparse\nParseResult\nc__builtin__\ntuple\n(X\x04\x00\x00\x00httpX\x0b\x00\x00\x00example.comX\x01\x00\x00\x00/X\x00\x00\x00\x00q\x00h\x00h\x00ttR.', + b'\x80\x02curlparse\nParseResult\n(X\x04\x00\x00\x00httpX\x0b\x00\x00\x00example.comX\x01\x00\x00\x00/X\x00\x00\x00\x00q\x00h\x00h\x00t\x81.', + b'\x80\x03curllib.parse\nParseResult\n(X\x04\x00\x00\x00httpX\x0b\x00\x00\x00example.comX\x01\x00\x00\x00/X\x00\x00\x00\x00q\x00h\x00h\x00t\x81.', + b'\x80\x04\x95=\x00\x00\x00\x00\x00\x00\x00\x8c\x0curllib.parse\x8c\x0bParseResult\x93(\x8c\x04http\x8c\x0bexample.com\x8c\x01/\x8c\x00\x94h\x00h\x00t\x81.', + )) + check(urlparse(burl), ( + b'ccopy_reg\n_reconstructor\n(curlparse\nParseResultBytes\nc__builtin__\ntuple\n(c_codecs\nencode\np0\n(Vhttp\nVlatin1\np1\ntRg0\n(Vexample.com\ng1\ntRg0\n(V/\ng1\ntRc__builtin__\nbytes\n(tRp2\ng2\ng2\nttR.', + b'ccopy_reg\n_reconstructor\n(curlparse\nParseResultBytes\nc__builtin__\ntuple\n(c_codecs\nencode\nq\x00(X\x04\x00\x00\x00httpX\x06\x00\x00\x00latin1q\x01tRh\x00(X\x0b\x00\x00\x00example.comh\x01tRh\x00(X\x01\x00\x00\x00/h\x01tRc__builtin__\nbytes\n)Rq\x02h\x02h\x02ttR.', + b'\x80\x02curlparse\nParseResultBytes\n(c_codecs\nencode\nq\x00X\x04\x00\x00\x00httpX\x06\x00\x00\x00latin1q\x01\x86Rh\x00X\x0b\x00\x00\x00example.comh\x01\x86Rh\x00X\x01\x00\x00\x00/h\x01\x86Rc__builtin__\nbytes\n)Rq\x02h\x02h\x02t\x81.', + b'\x80\x03curllib.parse\nParseResultBytes\n(C\x04httpC\x0bexample.comC\x01/C\x00q\x00h\x00h\x00t\x81.', + b'\x80\x04\x95B\x00\x00\x00\x00\x00\x00\x00\x8c\x0curllib.parse\x8c\x10ParseResultBytes\x93(C\x04httpC\x0bexample.comC\x01/C\x00\x94h\x00h\x00t\x81.', + )) + + # 3.15 data with missing_as_none=True. + check(urldefrag(url, missing_as_none=True), ( + b'ccopy_reg\n_reconstructor\n(curlparse\nDefragResult\nc__builtin__\ntuple\n(Vhttp://example.com/?\nV\nttR(N(dV_keep_empty\nI01\nstb.', + b'ccopy_reg\n_reconstructor\n(curlparse\nDefragResult\nc__builtin__\ntuple\n(X\x14\x00\x00\x00http://example.com/?X\x00\x00\x00\x00ttR(N}X\x0b\x00\x00\x00_keep_emptyI01\nstb.', + b'\x80\x02curlparse\nDefragResult\nX\x14\x00\x00\x00http://example.com/?X\x00\x00\x00\x00\x86\x81N}X\x0b\x00\x00\x00_keep_empty\x88s\x86b.', + b'\x80\x03curllib.parse\nDefragResult\nX\x14\x00\x00\x00http://example.com/?X\x00\x00\x00\x00\x86\x81N}X\x0b\x00\x00\x00_keep_empty\x88s\x86b.', + b'\x80\x04\x95K\x00\x00\x00\x00\x00\x00\x00\x8c\x0curllib.parse\x8c\x0cDefragResult\x93\x8c\x14http://example.com/?\x8c\x00\x86\x81N}\x8c\x0b_keep_empty\x88s\x86b.', + )) + check(urldefrag(burl, missing_as_none=True), ( + b'ccopy_reg\n_reconstructor\n(curlparse\nDefragResultBytes\nc__builtin__\ntuple\n(c_codecs\nencode\n(Vhttp://example.com/?\nVlatin1\ntRc__builtin__\nbytes\n(tRttR(N(dV_keep_empty\nI01\nstb.', + b'ccopy_reg\n_reconstructor\n(curlparse\nDefragResultBytes\nc__builtin__\ntuple\n(c_codecs\nencode\n(X\x14\x00\x00\x00http://example.com/?X\x06\x00\x00\x00latin1tRc__builtin__\nbytes\n)RttR(N}X\x0b\x00\x00\x00_keep_emptyI01\nstb.', + b'\x80\x02curlparse\nDefragResultBytes\nc_codecs\nencode\nX\x14\x00\x00\x00http://example.com/?X\x06\x00\x00\x00latin1\x86Rc__builtin__\nbytes\n)R\x86\x81N}X\x0b\x00\x00\x00_keep_empty\x88s\x86b.', + b'\x80\x03curllib.parse\nDefragResultBytes\nC\x14http://example.com/?C\x00\x86\x81N}X\x0b\x00\x00\x00_keep_empty\x88s\x86b.', + b'\x80\x04\x95P\x00\x00\x00\x00\x00\x00\x00\x8c\x0curllib.parse\x8c\x11DefragResultBytes\x93C\x14http://example.com/?C\x00\x86\x81N}\x8c\x0b_keep_empty\x88s\x86b.', + )) + check(urlsplit(url, missing_as_none=True), ( + b'ccopy_reg\n_reconstructor\n(curlparse\nSplitResult\nc__builtin__\ntuple\n(Vhttp\nVexample.com\nV/\nV\np0\ng0\nttR(N(dV_keep_empty\nI01\nstb.', + b'ccopy_reg\n_reconstructor\n(curlparse\nSplitResult\nc__builtin__\ntuple\n(X\x04\x00\x00\x00httpX\x0b\x00\x00\x00example.comX\x01\x00\x00\x00/X\x00\x00\x00\x00q\x00h\x00ttR(N}X\x0b\x00\x00\x00_keep_emptyI01\nstb.', + b'\x80\x02curlparse\nSplitResult\n(X\x04\x00\x00\x00httpX\x0b\x00\x00\x00example.comX\x01\x00\x00\x00/X\x00\x00\x00\x00q\x00h\x00t\x81N}X\x0b\x00\x00\x00_keep_empty\x88s\x86b.', + b'\x80\x03curllib.parse\nSplitResult\n(X\x04\x00\x00\x00httpX\x0b\x00\x00\x00example.comX\x01\x00\x00\x00/X\x00\x00\x00\x00q\x00h\x00t\x81N}X\x0b\x00\x00\x00_keep_empty\x88s\x86b.', + b'\x80\x04\x95N\x00\x00\x00\x00\x00\x00\x00\x8c\x0curllib.parse\x8c\x0bSplitResult\x93(\x8c\x04http\x8c\x0bexample.com\x8c\x01/\x8c\x00\x94h\x00t\x81N}\x8c\x0b_keep_empty\x88s\x86b.', + )) + check(urlsplit(burl, missing_as_none=True), ( + b'ccopy_reg\n_reconstructor\n(curlparse\nSplitResultBytes\nc__builtin__\ntuple\n(c_codecs\nencode\np0\n(Vhttp\nVlatin1\np1\ntRg0\n(Vexample.com\ng1\ntRg0\n(V/\ng1\ntRc__builtin__\nbytes\n(tRp2\ng2\nttR(N(dV_keep_empty\nI01\nstb.', + b'ccopy_reg\n_reconstructor\n(curlparse\nSplitResultBytes\nc__builtin__\ntuple\n(c_codecs\nencode\nq\x00(X\x04\x00\x00\x00httpX\x06\x00\x00\x00latin1q\x01tRh\x00(X\x0b\x00\x00\x00example.comh\x01tRh\x00(X\x01\x00\x00\x00/h\x01tRc__builtin__\nbytes\n)Rq\x02h\x02ttR(N}X\x0b\x00\x00\x00_keep_emptyI01\nstb.', + b'\x80\x02curlparse\nSplitResultBytes\n(c_codecs\nencode\nq\x00X\x04\x00\x00\x00httpX\x06\x00\x00\x00latin1q\x01\x86Rh\x00X\x0b\x00\x00\x00example.comh\x01\x86Rh\x00X\x01\x00\x00\x00/h\x01\x86Rc__builtin__\nbytes\n)Rq\x02h\x02t\x81N}X\x0b\x00\x00\x00_keep_empty\x88s\x86b.', + b'\x80\x03curllib.parse\nSplitResultBytes\n(C\x04httpC\x0bexample.comC\x01/C\x00q\x00h\x00t\x81N}X\x0b\x00\x00\x00_keep_empty\x88s\x86b.', + b'\x80\x04\x95S\x00\x00\x00\x00\x00\x00\x00\x8c\x0curllib.parse\x8c\x10SplitResultBytes\x93(C\x04httpC\x0bexample.comC\x01/C\x00\x94h\x00t\x81N}\x8c\x0b_keep_empty\x88s\x86b.', + )) + check(urlparse(url, missing_as_none=True), ( + b'ccopy_reg\n_reconstructor\n(curlparse\nParseResult\nc__builtin__\ntuple\n(Vhttp\nVexample.com\nV/\nNV\np0\ng0\nttR(N(dV_keep_empty\nI01\nstb.', + b'ccopy_reg\n_reconstructor\n(curlparse\nParseResult\nc__builtin__\ntuple\n(X\x04\x00\x00\x00httpX\x0b\x00\x00\x00example.comX\x01\x00\x00\x00/NX\x00\x00\x00\x00q\x00h\x00ttR(N}X\x0b\x00\x00\x00_keep_emptyI01\nstb.', + b'\x80\x02curlparse\nParseResult\n(X\x04\x00\x00\x00httpX\x0b\x00\x00\x00example.comX\x01\x00\x00\x00/NX\x00\x00\x00\x00q\x00h\x00t\x81N}X\x0b\x00\x00\x00_keep_empty\x88s\x86b.', + b'\x80\x03curllib.parse\nParseResult\n(X\x04\x00\x00\x00httpX\x0b\x00\x00\x00example.comX\x01\x00\x00\x00/NX\x00\x00\x00\x00q\x00h\x00t\x81N}X\x0b\x00\x00\x00_keep_empty\x88s\x86b.', + b'\x80\x04\x95O\x00\x00\x00\x00\x00\x00\x00\x8c\x0curllib.parse\x8c\x0bParseResult\x93(\x8c\x04http\x8c\x0bexample.com\x8c\x01/N\x8c\x00\x94h\x00t\x81N}\x8c\x0b_keep_empty\x88s\x86b.', + )) + check(urlparse(burl, missing_as_none=True), ( + b'ccopy_reg\n_reconstructor\n(curlparse\nParseResultBytes\nc__builtin__\ntuple\n(c_codecs\nencode\np0\n(Vhttp\nVlatin1\np1\ntRg0\n(Vexample.com\ng1\ntRg0\n(V/\ng1\ntRNc__builtin__\nbytes\n(tRp2\ng2\nttR(N(dV_keep_empty\nI01\nstb.', + b'ccopy_reg\n_reconstructor\n(curlparse\nParseResultBytes\nc__builtin__\ntuple\n(c_codecs\nencode\nq\x00(X\x04\x00\x00\x00httpX\x06\x00\x00\x00latin1q\x01tRh\x00(X\x0b\x00\x00\x00example.comh\x01tRh\x00(X\x01\x00\x00\x00/h\x01tRNc__builtin__\nbytes\n)Rq\x02h\x02ttR(N}X\x0b\x00\x00\x00_keep_emptyI01\nstb.', + b'\x80\x02curlparse\nParseResultBytes\n(c_codecs\nencode\nq\x00X\x04\x00\x00\x00httpX\x06\x00\x00\x00latin1q\x01\x86Rh\x00X\x0b\x00\x00\x00example.comh\x01\x86Rh\x00X\x01\x00\x00\x00/h\x01\x86RNc__builtin__\nbytes\n)Rq\x02h\x02t\x81N}X\x0b\x00\x00\x00_keep_empty\x88s\x86b.', + b'\x80\x03curllib.parse\nParseResultBytes\n(C\x04httpC\x0bexample.comC\x01/NC\x00q\x00h\x00t\x81N}X\x0b\x00\x00\x00_keep_empty\x88s\x86b.', + b'\x80\x04\x95T\x00\x00\x00\x00\x00\x00\x00\x8c\x0curllib.parse\x8c\x10ParseResultBytes\x93(C\x04httpC\x0bexample.comC\x01/NC\x00\x94h\x00t\x81N}\x8c\x0b_keep_empty\x88s\x86b.', + )) def test_parse_qs_encoding(self): result = urllib.parse.parse_qs("key=\u0141%E9", encoding="latin-1") @@ -1462,6 +1689,11 @@ def test_telurl_params(self): self.assertEqual(p1.path, '+1-201-555-0123') self.assertEqual(p1.params, '') + p1 = urllib.parse.urlparse('tel:+1-201-555-0123', missing_as_none=True) + self.assertEqual(p1.scheme, 'tel') + self.assertEqual(p1.path, '+1-201-555-0123') + self.assertEqual(p1.params, None) + p1 = urllib.parse.urlparse('tel:7042;phone-context=example.com') self.assertEqual(p1.scheme, 'tel') self.assertEqual(p1.path, '7042') @@ -1689,63 +1921,63 @@ def test_splittype_deprecation(self): urllib.parse.splittype('') self.assertEqual(str(cm.warning), 'urllib.parse.splittype() is deprecated as of 3.8, ' - 'use urllib.parse.urlparse() instead') + 'use urllib.parse.urlsplit() instead') def test_splithost_deprecation(self): with self.assertWarns(DeprecationWarning) as cm: urllib.parse.splithost('') self.assertEqual(str(cm.warning), 'urllib.parse.splithost() is deprecated as of 3.8, ' - 'use urllib.parse.urlparse() instead') + 'use urllib.parse.urlsplit() instead') def test_splituser_deprecation(self): with self.assertWarns(DeprecationWarning) as cm: urllib.parse.splituser('') self.assertEqual(str(cm.warning), 'urllib.parse.splituser() is deprecated as of 3.8, ' - 'use urllib.parse.urlparse() instead') + 'use urllib.parse.urlsplit() instead') def test_splitpasswd_deprecation(self): with self.assertWarns(DeprecationWarning) as cm: urllib.parse.splitpasswd('') self.assertEqual(str(cm.warning), 'urllib.parse.splitpasswd() is deprecated as of 3.8, ' - 'use urllib.parse.urlparse() instead') + 'use urllib.parse.urlsplit() instead') def test_splitport_deprecation(self): with self.assertWarns(DeprecationWarning) as cm: urllib.parse.splitport('') self.assertEqual(str(cm.warning), 'urllib.parse.splitport() is deprecated as of 3.8, ' - 'use urllib.parse.urlparse() instead') + 'use urllib.parse.urlsplit() instead') def test_splitnport_deprecation(self): with self.assertWarns(DeprecationWarning) as cm: urllib.parse.splitnport('') self.assertEqual(str(cm.warning), 'urllib.parse.splitnport() is deprecated as of 3.8, ' - 'use urllib.parse.urlparse() instead') + 'use urllib.parse.urlsplit() instead') def test_splitquery_deprecation(self): with self.assertWarns(DeprecationWarning) as cm: urllib.parse.splitquery('') self.assertEqual(str(cm.warning), 'urllib.parse.splitquery() is deprecated as of 3.8, ' - 'use urllib.parse.urlparse() instead') + 'use urllib.parse.urlsplit() instead') def test_splittag_deprecation(self): with self.assertWarns(DeprecationWarning) as cm: urllib.parse.splittag('') self.assertEqual(str(cm.warning), 'urllib.parse.splittag() is deprecated as of 3.8, ' - 'use urllib.parse.urlparse() instead') + 'use urllib.parse.urlsplit() instead') def test_splitattr_deprecation(self): with self.assertWarns(DeprecationWarning) as cm: urllib.parse.splitattr('') self.assertEqual(str(cm.warning), 'urllib.parse.splitattr() is deprecated as of 3.8, ' - 'use urllib.parse.urlparse() instead') + 'use urllib.parse.urlsplit() instead') def test_splitvalue_deprecation(self): with self.assertWarns(DeprecationWarning) as cm: @@ -1762,6 +1994,8 @@ def test_to_bytes_deprecation(self): def str_encode(s): + if s is None: + return None return s.encode('ascii') def tuple_encode(t): diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py old mode 100644 new mode 100755 diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py index bfbcf112b0b..82f14ca968f 100644 --- a/Lib/test/test_webbrowser.py +++ b/Lib/test/test_webbrowser.py @@ -5,9 +5,10 @@ import subprocess import sys import unittest +import warnings import webbrowser -from functools import partial from test import support +from test.support import force_not_colorized_test_class from test.support import import_helper from test.support import is_apple_mobile from test.support import os_helper @@ -335,6 +336,87 @@ def close(self): return None +@unittest.skipUnless(sys.platform == "darwin", "macOS specific test") +@requires_subprocess() +class MacOSTest(unittest.TestCase): + + def setUp(self): + env = self.enterContext(os_helper.EnvironmentVarGuard()) + env.unset("BROWSER") + + def test_default(self): + browser = webbrowser.get() + self.assertIsInstance(browser, webbrowser.MacOS) + self.assertEqual(browser.name, 'default') + + def test_default_http_open(self): + # http/https URLs use /usr/bin/open directly — no bundle ID needed. + browser = webbrowser.MacOS('default') + with mock.patch('subprocess.run') as mock_run: + mock_run.return_value = mock.Mock(returncode=0) + result = browser.open(URL) + mock_run.assert_called_once_with( + ['/usr/bin/open', URL], + stderr=subprocess.DEVNULL, + ) + self.assertTrue(result) + + def test_default_non_http_uses_bundle_id(self): + # Non-http(s) URLs (e.g. file://) must be routed through the browser + # via -b to prevent OS file handler dispatch. + file_url = 'file:///tmp/test.html' + browser = webbrowser.MacOS('default') + with mock.patch('webbrowser._macos_default_browser_bundle_id', + return_value='com.google.Chrome'), \ + mock.patch('subprocess.run') as mock_run: + mock_run.return_value = mock.Mock(returncode=0) + result = browser.open(file_url) + mock_run.assert_called_once_with( + ['/usr/bin/open', '-b', 'com.google.Chrome', file_url], + stderr=subprocess.DEVNULL, + ) + self.assertTrue(result) + + def test_named_known_browser_uses_bundle_id(self): + # Named browsers with a known bundle ID use /usr/bin/open -b. + browser = webbrowser.MacOS('safari') + with mock.patch('subprocess.run') as mock_run: + mock_run.return_value = mock.Mock(returncode=0) + result = browser.open(URL) + mock_run.assert_called_once_with( + ['/usr/bin/open', '-b', 'com.apple.Safari', URL], + stderr=subprocess.DEVNULL, + ) + self.assertTrue(result) + + def test_named_unknown_browser_falls_back_to_dash_a(self): + # Named browsers not in the bundle ID map fall back to -a. + browser = webbrowser.MacOS('lynx') + with mock.patch('subprocess.run') as mock_run: + mock_run.return_value = mock.Mock(returncode=0) + browser.open(URL) + mock_run.assert_called_once_with( + ['/usr/bin/open', '-a', 'lynx', URL], + stderr=subprocess.DEVNULL, + ) + + def test_open_failure(self): + browser = webbrowser.MacOS('default') + with mock.patch('subprocess.run') as mock_run: + mock_run.return_value = mock.Mock(returncode=1) + result = browser.open(URL) + self.assertFalse(result) + + +@unittest.skipUnless(sys.platform == "darwin", "macOS specific test") +@requires_subprocess() +class MacOSXOSAScriptDeprecationTest(unittest.TestCase): + + def test_deprecation_warning(self): + with self.assertWarns(DeprecationWarning): + webbrowser.MacOSXOSAScript('default') + + @unittest.skipUnless(sys.platform == "darwin", "macOS specific test") @requires_subprocess() class MacOSXOSAScriptTest(unittest.TestCase): @@ -345,17 +427,14 @@ def setUp(self): env.unset("BROWSER") support.patch(self, os, "popen", self.mock_popen) + self.enterContext(warnings.catch_warnings()) + warnings.simplefilter("ignore", DeprecationWarning) self.browser = webbrowser.MacOSXOSAScript("default") def mock_popen(self, cmd, mode): self.popen_pipe = MockPopenPipe(cmd, mode) return self.popen_pipe - def test_default(self): - browser = webbrowser.get() - assert isinstance(browser, webbrowser.MacOSXOSAScript) - self.assertEqual(browser.name, "default") - def test_default_open(self): url = "https://python.org" self.browser.open(url) @@ -381,7 +460,9 @@ def test_default_browser_lookup(self): self.assertIn(f'open location "{url}"', script) def test_explicit_browser(self): - browser = webbrowser.MacOSXOSAScript("safari") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + browser = webbrowser.MacOSXOSAScript("safari") browser.open("https://python.org") script = self.popen_pipe.pipe.getvalue() self.assertIn('tell application "safari"', script) @@ -527,6 +608,7 @@ def test_environment_preferred(self): self.assertEqual(webbrowser.get().name, sys.executable) +@force_not_colorized_test_class class CliTest(unittest.TestCase): def test_parse_args(self): for command, url, new_win in [ diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index d84e7647adb..45704ed9063 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -1,6 +1,6 @@ from unittest import mock from test import support -from test.support import socket_helper, control_characters_c0 +from test.support import force_not_colorized, socket_helper, control_characters_c0 from test.test_httpservers import NoLogRequestHandler from unittest import TestCase from wsgiref.util import setup_testing_defaults @@ -66,6 +66,26 @@ def header_app(environ, start_response): ]).encode('iso-8859-1')] +def input_app(func_name, *args): + def app(e,s): + req = getattr(e['wsgi.input'], func_name)(*args) + s("200 OK", [("Content-Type", "text/plain; charset=utf-8")]) + if type(req) is list: + resp = b";".join(req) + else: + resp = req + return [resp] + return app + + +def errors_app(func_name, *args): + def app(e,s): + getattr(e['wsgi.errors'], func_name)(*args) + s("200 OK", [("Content-Type", "text/plain; charset=utf-8")]) + return [b"data"] + return app + + def run_amock(app=hello_app, data=b"GET / HTTP/1.0\n\n"): server = make_server("", 80, app, MockServer, MockHandler) inp = BufferedReader(BytesIO(data)) @@ -109,7 +129,7 @@ def check_hello(self, out, has_length=True): sys.version.split()[0]) self.assertEqual(out, ("HTTP/1.0 200 OK\r\n" - "Server: WSGIServer/0.2 " + pyver +"\r\n" + "Server: WSGIServer " + pyver + "\r\n" "Content-Type: text/plain\r\n" "Date: Mon, 05 Jun 2006 18:49:54 GMT\r\n" + (has_length and "Content-Length: 13\r\n" or "") + @@ -192,6 +212,96 @@ def bad_app(e,s): err.splitlines()[-2], "AssertionError" ) + def test_wsgi_input_read(self): + bad_app = input_app("read") + good_app = input_app("read", 5) + + out, err = run_amock(validator(bad_app)) + self.assertEndsWith(out, + b"A server error occurred. Please contact the administrator." + ) + + self.assertEqual( + err.splitlines()[-2], "AssertionError" + ) + + out, err = run_amock(validator(good_app), b"GET / HTTP/1.0\n\nTest 1\nTest 2\n") + self.assertEndsWith(out, b"Test ") + + def test_wsgi_input_readlines(self): + bad_app = input_app("readlines", 3, 5) + good_app = input_app("readlines", 1) + + out, err = run_amock(validator(bad_app)) + self.assertEndsWith(out, + b"A server error occurred. Please contact the administrator." + ) + self.assertEqual( + err.splitlines()[-2], "AssertionError" + ) + out, err = run_amock(validator(good_app), b"GET / HTTP/1.0\n\nTest Line 1\nTest Line 2\n") + self.assertEndsWith(out, b"Test Line 1\n") + + def test_wsgi_input_readline(self): + bad_app = input_app("readline", 3, 4) + good_app = input_app("readline", 2) + + out, err = run_amock(validator(bad_app)) + self.assertEndsWith(out, + b"A server error occurred. Please contact the administrator." + ) + self.assertEqual( + err.splitlines()[-2], "AssertionError" + ) + + out, err = run_amock(validator(good_app), b"GET / HTTP/1.0\n\nTest 1\nTest 2\n") + self.assertEndsWith(out, b"Te") + + def test_wsgi_input_close(self): + app = input_app("close") + out, err = run_amock(validator(app), b"GET / HTTP/1.0\n\nTest 1\nTest 2\n") + self.assertEqual(err.splitlines()[-2], 'AssertionError: input.close() must not be called') + self.assertEndsWith(out, b"A server error occurred. Please contact the administrator.") + + def test_wsgi_input_iter(self): + def app(e,s): + req = [] + for line in e['wsgi.input']: + req.append(line) + s("200 OK", [('Content-Type', 'text/plain; charser=utf-8')]) + return [b';'.join(req)] + + out, err = run_amock(validator(app), b"GET / HTTP/1.0\n\nTest 1\nTest 2\n") + self.assertEndsWith(out, b"Test 1\n;Test 2\n") + + def test_wsgi_errors_write(self): + bad_app = errors_app("write", b"Test") + good_app = errors_app("write", "Test") + + out, err = run_amock(validator(bad_app), b"GET / HTTP/1.0\n\n") + self.assertEqual(err.splitlines()[-2], 'AssertionError') + + out, err = run_amock(validator(good_app), b"GET / HTTP/1.0\n\n") + self.assertStartsWith(err, "Test") + + def test_wsgi_errors_writelines(self): + bad_app = errors_app("writelines", [1, "Test"]) + good_app = errors_app("writelines", ["Test", "Test"]) + + out, err = run_amock(validator(bad_app), b"GET / HTTP/1.0\n\n") + self.assertEqual(err.splitlines()[-2], 'AssertionError') + + out, err = run_amock(validator(good_app), b"GET / HTTP/1.0\n\n") + self.assertStartsWith(err, "TestTest") + + def test_wsgi_errors_close(self): + app = errors_app("close") + + out, err = run_amock(validator(app), b"GET / HTTP/1.0\n\n") + self.assertEqual(err.splitlines()[-2], + 'AssertionError: errors.close() must not be called') + + @force_not_colorized def test_bytes_validation(self): def app(e, s): s("200 OK", [ @@ -206,7 +316,7 @@ def app(e, s): pyver = py + b"/" + ver self.assertEqual( b"HTTP/1.0 200 OK\r\n" - b"Server: WSGIServer/0.2 "+ pyver + b"\r\n" + b"Server: WSGIServer " + pyver + b"\r\n" b"Content-Type: text/plain; charset=utf-8\r\n" b"Date: Wed, 24 Dec 2008 13:29:32 GMT\r\n" b"\r\n" @@ -879,5 +989,17 @@ def testRaisesControlCharacters(self): base.start_response('200 OK', [('x', c0)]) +class TestModule(unittest.TestCase): + def test_deprecated__version__(self): + from wsgiref import simple_server + + with self.assertWarnsRegex( + DeprecationWarning, + "'__version__' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(simple_server, "__version__") + self.assertEqual(cm.filename, __file__) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_xml.py b/Lib/test/test_xml.py new file mode 100644 index 00000000000..3a8b9204816 --- /dev/null +++ b/Lib/test/test_xml.py @@ -0,0 +1,43 @@ +import xml +import unittest + + +class TestUtils(unittest.TestCase): + + def test_is_valid_name(self): + is_valid_name = xml.is_valid_name + self.assertFalse(is_valid_name('')) + self.assertTrue(is_valid_name('name')) + self.assertTrue(is_valid_name('NAME')) + self.assertTrue(is_valid_name('name0:-._·')) + self.assertTrue(is_valid_name('_')) + self.assertTrue(is_valid_name(':')) + self.assertTrue(is_valid_name('Ñàḿĕ')) + self.assertTrue(is_valid_name('\U000EFFFF')) + self.assertFalse(is_valid_name('0')) + self.assertFalse(is_valid_name('-')) + self.assertFalse(is_valid_name('.')) + self.assertFalse(is_valid_name('·')) + self.assertFalse(is_valid_name('na me')) + for c in '<>/!?=\x00\x01\x7f\ud800\udfff\ufffe\uffff\U000F0000': + self.assertFalse(is_valid_name('name' + c)) + + def test_is_valid_text(self): + is_valid_text = xml.is_valid_text + self.assertTrue(is_valid_text('')) + self.assertTrue(is_valid_text('!0Aa_~ \r\n\t\x85\xa0')) + self.assertTrue(is_valid_text('\ud7ff\ue000\ufffd\U00010000\U0010ffff')) + self.assertFalse(is_valid_text('\x00')) + self.assertFalse(is_valid_text('\x01')) + self.assertFalse(is_valid_text('\x1f')) + self.assertTrue(is_valid_text('\x7f')) + self.assertTrue(is_valid_text('\x80')) + self.assertTrue(is_valid_text('\x9f')) + self.assertFalse(is_valid_text('\ud800')) + self.assertFalse(is_valid_text('\udfff')) + self.assertFalse(is_valid_text('\ufffe')) + self.assertFalse(is_valid_text('\uffff')) + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 0b89c251a4c..1baa0dcf634 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -381,6 +381,19 @@ def test_simpleops(self): self.serialize_check(element, '') + def test_positional_only_parameter(self): + # Test Element positional-only parameters (gh-144846). + + # 'tag' is positional-only + with self.assertRaises(TypeError): + ET.Element(tag='fail') + + # 'tag' and 'attrib' as kwarg/attribute names + e = ET.Element('e', attrib={'attrib': 'foo'}, tag='bar') + self.assertEqual(e.tag, 'e') + self.assertEqual(e.get('attrib'), 'foo') + self.assertEqual(e.get('tag'), 'bar') + def test_cdata(self): # Test CDATA handling (etc). @@ -484,6 +497,28 @@ def test_attrib(self): self.assertEqual(ET.tostring(elem), b'') + def test_subelement_positional_only_parameter(self): + # Test SubElement positional-only parameters (gh-144270). + parent = ET.Element('parent') + + # 'parent' and 'tag' are positional-only + with self.assertRaises(TypeError): + ET.SubElement(parent=parent, tag='fail') + with self.assertRaises(TypeError): + ET.SubElement(parent, tag='fail') + + # 'attrib' can be passed as keyword + sub1 = ET.SubElement(parent, 'sub1', attrib={'key': 'value'}) + self.assertEqual(sub1.get('key'), 'value') + + # 'tag' and 'parent' as kwargs become XML attributes, not func params + sub2 = ET.SubElement(parent, 'sub2', attrib={'attrib': 'foo'}, + tag='bar', parent='baz') + self.assertEqual(sub2.tag, 'sub2') + self.assertEqual(sub2.get('attrib'), 'foo') + self.assertEqual(sub2.get('tag'), 'bar') + self.assertEqual(sub2.get('parent'), 'baz') + def test_makeelement(self): # Test makeelement handling. @@ -1279,6 +1314,12 @@ def test_processinginstruction_serialization(self): # no processing instructions in text serialization self.assertEqual(ET.tostring(pi, method='text'), b'') + def test_empty_attribute_serialization(self): + # empty attrs only work in html + elem = ET.Element('tag', attrib={'attr': None}) + self.assertRaises(TypeError, ET.tostring, elem) + self.assertEqual(ET.tostring(elem, method='html'), b'') + @support.subTests('tag', ("script", "style", "xmp", "iframe", "noembed", "noframes")) def test_html_cdata_elems_serialization(self, tag): # content of raw text elements is not escaped in html @@ -1520,12 +1561,27 @@ def test_resource_warnings_failed_iteration(self): action, elem = next(it) self.assertEqual((action, elem.tag), ('end', 'document')) with warnings_helper.check_no_resource_warning(self): + with self.assertRaises(ET.ParseError) as cm: + next(it) + self.assertEqual(str(cm.exception), + 'junk after document element: line 1, column 12') + it.close() + del cm, it + gc_collect() + + it = ET.iterparse(TESTFN) + action, elem = next(it) + self.assertEqual((action, elem.tag), ('end', 'document')) + with self.assertWarns(ResourceWarning) as wm: with self.assertRaises(ET.ParseError) as cm: next(it) self.assertEqual(str(cm.exception), 'junk after document element: line 1, column 12') del cm, it gc_collect() + self.assertIn('unclosed iterparse iterator', str(wm.warning)) + self.assertIn(repr(TESTFN), str(wm.warning)) + self.assertEqual(wm.filename, __file__) def test_resource_warnings_exhausted(self): it = ET.iterparse(SIMPLE_XMLFILE) @@ -2794,6 +2850,17 @@ def test_pickle_issue18997(self): self.assertEqual(e2.tag, 'group') self.assertEqual(e2[0].tag, 'dogs') + def test_remove_errors(self): + e = ET.Element('tag') + with self.assertRaisesRegex(ValueError, + r" not in "): + e.remove(ET.Element('subtag')) + with self.assertRaisesRegex(TypeError, + r".*\bElement, not type"): + e.remove(ET.Element) + with self.assertRaisesRegex(TypeError, + r".*\bElement, not int"): + e.remove(1) class BadElementTest(ElementTestCase, unittest.TestCase): @@ -3182,7 +3249,7 @@ def __deepcopy__(self, memo): [a.tag, b.tag, a.tag, b.tag]) @unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON; stack overflow on Windows") - @support.skip_if_unlimited_stack_size + @support.skip_if_huge_c_stack(500_000) @support.skip_emscripten_stack_overflow() @support.skip_wasi_stack_overflow() def test_deeply_nested_deepcopy(self): @@ -3413,6 +3480,37 @@ def test_find_xpath(self): self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()-0]') self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()+1]') + def test_find_xpath_index_no_quadratic_complexity(self): + class CountingElement(ET.Element): + findall_calls = 0 + def findall(self, *args, **kwargs): + type(self).findall_calls += 1 + return super().findall(*args, **kwargs) + + def work(n, pattern): + root = CountingElement("root") + for _ in range(n): + ET.SubElement(root, "a") + CountingElement.findall_calls = 0 + root.findall(pattern) + return CountingElement.findall_calls + + for pattern in [".//a[1]", ".//a[last()]"]: + w1 = work(1024, pattern) + w2 = work(2048, pattern) + w3 = work(4096, pattern) + + self.assertGreater(w1, 0) + r1 = w2 / w1 + r2 = w3 / w2 + # Doubling N must not ~double the parent.findall calls. + # Linear-in-N call counts indicate the cache is missing. + self.assertLess( + max(r1, r2), 1.5, + msg=f"Possible quadratic behavior on {pattern!r}: " + f"calls={w1, w2, w3} ratios={r1, r2}", + ) + def test_findall(self): e = ET.XML(SAMPLE_XML) e[2] = ET.XML(SAMPLE_SECTION) @@ -4846,6 +4944,19 @@ def get_option(config, option_name, default=None): # -------------------------------------------------------------------- + +class TestModule(unittest.TestCase): + def test_deprecated_version(self): + with self.assertWarnsRegex( + DeprecationWarning, + "'VERSION' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(ET, "VERSION") + self.assertEqual(cm.filename, __file__) + + +# -------------------------------------------------------------------- + def setUpModule(module=None): # When invoked without a module, runs the Python ET tests by loading pyET. # Otherwise, uses the given module as the ET. diff --git a/Lib/test/test_zipfile/__main__.py b/Lib/test/test_zipfile/__main__.py index e25ac946edf..90da74ade38 100644 --- a/Lib/test/test_zipfile/__main__.py +++ b/Lib/test/test_zipfile/__main__.py @@ -1,6 +1,6 @@ import unittest -from . import load_tests # noqa: F401 +from . import load_tests if __name__ == "__main__": diff --git a/Lib/test/test_zipfile/_path/test_path.py b/Lib/test/test_zipfile/_path/test_path.py index 351d9eefeb0..e7931b6f394 100644 --- a/Lib/test/test_zipfile/_path/test_path.py +++ b/Lib/test/test_zipfile/_path/test_path.py @@ -317,7 +317,7 @@ def test_mutability(self, alpharep): HUGE_ZIPFILE_NUM_ENTRIES = 2**13 def huge_zipfile(self): - """Create a read-only zipfile with a huge number of entries entries.""" + """Create a read-only zipfile with a huge number of entries.""" strm = io.BytesIO() zf = zipfile.ZipFile(strm, "w") for entry in map(str, range(self.HUGE_ZIPFILE_NUM_ENTRIES)): diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index f9ad28ea04d..8f226826524 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -1,6 +1,7 @@ import _pyio import array import contextlib +import errno import importlib.util import io import itertools @@ -20,14 +21,15 @@ from random import randint, random, randbytes from test import archiver_tests -from test.support import script_helper, os_helper +from test.support import script_helper from test.support import ( findfile, requires_zlib, requires_bz2, requires_lzma, requires_zstd, captured_stdout, captured_stderr, requires_subprocess, cpython_only ) from test.support.os_helper import ( - TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath + TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath, + with_source_date_epoch, without_source_date_epoch, ) from test.support.import_helper import ensure_lazy_imports @@ -654,6 +656,12 @@ def test_add_file_after_2107(self): os.utime(TESTFN, (ts, ts)) except OverflowError: self.skipTest('Host fs cannot set timestamp to required value.') + except OSError as exc: + # Some file systems (e.g. UFS and ZFS on illumos) do not + # support timestamps that do not fit in 32 bits. + if exc.errno != errno.EOVERFLOW: + raise + self.skipTest('Host fs cannot set timestamp to required value.') mtime_ns = os.stat(TESTFN).st_mtime_ns if mtime_ns != (4386268800 * 10**9): @@ -1880,29 +1888,24 @@ def test_writestr_extended_local_header_issue1202(self): zinfo.flag_bits |= zipfile._MASK_USE_DATA_DESCRIPTOR # Include an extended local header. orig_zip.writestr(zinfo, data) + @with_source_date_epoch(epoch=1735715999) def test_write_with_source_date_epoch(self): - with os_helper.EnvironmentVarGuard() as env: - # Set the SOURCE_DATE_EPOCH environment variable to a specific timestamp - env['SOURCE_DATE_EPOCH'] = "1735715999" - - with zipfile.ZipFile(TESTFN, "w") as zf: - zf.writestr("test_source_date_epoch.txt", "Testing SOURCE_DATE_EPOCH") + with zipfile.ZipFile(TESTFN, "w") as zf: + zf.writestr("test_source_date_epoch.txt", "Testing SOURCE_DATE_EPOCH") - with zipfile.ZipFile(TESTFN, "r") as zf: - zip_info = zf.getinfo("test_source_date_epoch.txt") - expected_utc = (2025, 1, 1, 7, 19, 58) - self.assertEqual(zip_info.date_time, expected_utc) + with zipfile.ZipFile(TESTFN, "r") as zf: + zip_info = zf.getinfo("test_source_date_epoch.txt") + expected_utc = (2025, 1, 1, 7, 19, 58) + self.assertEqual(zip_info.date_time, expected_utc) + @without_source_date_epoch def test_write_without_source_date_epoch(self): - with os_helper.EnvironmentVarGuard() as env: - del env['SOURCE_DATE_EPOCH'] - - with zipfile.ZipFile(TESTFN, "w") as zf: - zf.writestr("test_no_source_date_epoch.txt", "Testing without SOURCE_DATE_EPOCH") + with zipfile.ZipFile(TESTFN, "w") as zf: + zf.writestr("test_no_source_date_epoch.txt", "Testing without SOURCE_DATE_EPOCH") - with zipfile.ZipFile(TESTFN, "r") as zf: - zip_info = zf.getinfo("test_no_source_date_epoch.txt") - self.assertTimestampAlmostEqual(time.localtime(), zip_info.date_time, tolerance=2) + with zipfile.ZipFile(TESTFN, "r") as zf: + zip_info = zf.getinfo("test_no_source_date_epoch.txt") + self.assertTimestampAlmostEqual(time.localtime(), zip_info.date_time, tolerance=2) def assertTimestampAlmostEqual(self, time1, time2, tolerance): import datetime @@ -2720,6 +2723,115 @@ def tearDown(self): unlink(TESTFN2) +class AbstractBoundedDecompressTests: + # ZipExtFile._read1() bounds the output of each decompress() call so that a + # small member declaring a large uncompressed size cannot expand into one + # unbounded read. + def test_read1_output_is_bounded(self): + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", compression=self.compression) as zf: + zf.writestr("big", b"\0" * (4 * 1024 * 1024)) + with zipfile.ZipFile(io.BytesIO(buf.getvalue())) as zf: + with zf.open("big") as f: + self.assertLessEqual(len(f._read1(100)), f.MIN_READ_SIZE) + + +class StoredBoundedDecompressTests(AbstractBoundedDecompressTests, + unittest.TestCase): + compression = zipfile.ZIP_STORED + + +@requires_zlib() +class DeflateBoundedDecompressTests(AbstractBoundedDecompressTests, + unittest.TestCase): + compression = zipfile.ZIP_DEFLATED + + +@requires_bz2() +class Bzip2BoundedDecompressTests(AbstractBoundedDecompressTests, + unittest.TestCase): + compression = zipfile.ZIP_BZIP2 + + +@requires_lzma() +class LzmaBoundedDecompressTests(AbstractBoundedDecompressTests, + unittest.TestCase): + compression = zipfile.ZIP_LZMA + + +@requires_zstd() +class ZstdBoundedDecompressTests(AbstractBoundedDecompressTests, + unittest.TestCase): + compression = zipfile.ZIP_ZSTANDARD + + +class MonkeypatchedDecompressorTests(unittest.TestCase): + # Some third-party projects monkey-patch _get_decompressor() to add + # additional compression schemes. This can break at any time as the + # internal compressor objects change. + # To protect users, we try to keep this case working. + # See also: GH-156002 and GH-113767. + COMPRESSION = 99 + + class Compressor: + """Compressor with only the original BZ2Compressor API""" + def compress(self, data): + return data.swapcase() + + def flush(self): + return b'' + + class Decompressor: + """Decompressor with only the 3.3+ BZ2Decompressor API""" + eof = False + + def decompress(self, data): + return data.swapcase() + + def setUp(self): + orig_check_compression = zipfile._check_compression + orig_get_compressor = zipfile._get_compressor + orig_get_decompressor = zipfile._get_decompressor + + def check_compression(compression): + if compression != self.COMPRESSION: + orig_check_compression(compression) + + def get_compressor(compress_type, compresslevel=None): + if compress_type == self.COMPRESSION: + return self.Compressor() + return orig_get_compressor(compress_type, compresslevel) + + def get_decompressor(compress_type): + if compress_type == self.COMPRESSION: + return self.Decompressor() + return orig_get_decompressor(compress_type) + + self.enterContext(mock.patch.object( + zipfile, '_check_compression', check_compression)) + self.enterContext(mock.patch.object( + zipfile, '_get_compressor', get_compressor)) + self.enterContext(mock.patch.object( + zipfile, '_get_decompressor', get_decompressor)) + + def test_roundtrip_monkeypatched_decompressor(self): + data = bytes(range(256)) * 8 + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", compression=self.COMPRESSION) as zf: + zf.writestr("member", data) + self.assertIn(data.swapcase(), buf.getvalue()) + with zipfile.ZipFile(io.BytesIO(buf.getvalue())) as zf: + self.assertEqual(zf.read("member"), data) + with zf.open("member") as f: + self.assertEqual(f.read(100), data[:100]) + self.assertEqual(f.read1(100), data[100:200]) + f.seek(-100, os.SEEK_END) + self.assertEqual(f.read(), data[-100:]) + # Rewinding past the read buffer re-creates the decompressor. + f.seek(0) + self.assertEqual(f.read(), data) + + class AbstractBadCrcTests: def test_testzip_with_bad_crc(self): """Tests that files with bad CRCs return their name from testzip.""" diff --git a/Lib/test/test_zoneinfo/data/update_test_data.py b/Lib/test/test_zoneinfo/data/update_test_data.py index f531ab316a1..cf1b1eff4fe 100644 --- a/Lib/test/test_zoneinfo/data/update_test_data.py +++ b/Lib/test/test_zoneinfo/data/update_test_data.py @@ -120,3 +120,6 @@ def update_test_data(fname: str = "zoneinfo_data.json") -> None: if __name__ == "__main__": update_test_data() + + print("Remember to update the HAS_TZDATA_PKG version requirement in " + "test_zoneinfo.py!") diff --git a/Lib/test/test_zoneinfo/data/zoneinfo_data.json b/Lib/test/test_zoneinfo/data/zoneinfo_data.json index ec4414a0cde..6c9bf09e12f 100644 --- a/Lib/test/test_zoneinfo/data/zoneinfo_data.json +++ b/Lib/test/test_zoneinfo/data/zoneinfo_data.json @@ -1,190 +1,218 @@ { "data": { "Africa/Abidjan": [ - "{Wp48S^xk9=GL@E0stWa761SMbT8$j-~f{VGF<>F7KxBg5R*{Ksocg8-YYVul=v7vZzaHN", - "uC=da5UI2rH18c!OnjV{y4u(+A!!VBKmY&$ORw>7UO^(500B;v0RR91bXh%WvBYQl0ssI2", - "00dcD" + "{Wp48S^xk9=GL@E0stWa761SMbT8$j-~f{VGF<>F7KxBg5R*{Ksocg8-", + "YYVul=v7vZzaHNuC=da5UI2rH18c!OnjV{y4u(+A!!VBKmY&$ORw>7UO^(500B;v0RR91b", + "Xh%WvBYQl0ssI200dcD" ], "Africa/Casablanca": [ - "{Wp48S^xk9=GL@E0stWa761SMbT8$j;0b&Kz+C_;7KxBg5R*{N&yjMUR~;C-fDaSOU;q-~", - "FqW+4{YBjbcw}`a!dW>b)R2-0a+uwf`P3{_Y@HuCz}S$J$ZJ>R_V<~|Fk>sgX4=%0vUrh-", - "lt@YP^Wrus;j?`Th#xRPzf<<~Hp4DH^gZX>d{+WOp~HNu8!{uWu}&XphAd{j1;rB4|9?R!", - "pqruAFUMt8#*WcrVS{;kLlY(cJRV$w?d2car%Rs>q9BgTU4", - "Ht-tQKZ7Z`9QqOb?R#b%z?rk>!CkH7jy3wja4NG2q)H}fNRKg8v{);Em;K3Cncf4C6&Oaj", - "V+DbX%o4+)CV3+e!Lm6dutu(0BQpH1T?W(~cQtKV*^_Pdx!LirjpTs?Bmt@vktjLq4;)O!", - "rrly=c*rwTwMJFd0I57`hgkc?=nyI4RZf9W$6DCWugmf&)wk^tWH17owj=#PGH7Xv-?9$j", - "njwDlkOE+BFNR9YXEmBpO;rqEw=e2IR-8^(W;8ma?M3JVd($2T>IW+0tk|Gm8>ftukRQ9J", - "8k3brzqMnVyjsLI-CKneFa)Lxvp_aq40f}0J3VVoWL5rox", - "`Kptivcp}o5xA^@>qNI%?zo=Yj4AMV?kbAA)j(1%)+Pp)bSn+7Yk`M{oE}L-Z!G6OMr5G+h", - "p)$3Lg{ono{4cN>Vr&>L4kXH;_VnBL5U!LgzqE%P7QQ*tue}O`3(TZ0`aKn&~8trOQ-rBXCp)f@P6RMO4l0+;b|5-pk9_ryNh}Zc*v%mvz_#", - "yd6fjB0g9{MmMnu8bG%#C~ugXK^S^k@?ab#", - "O|aE>dDTt4s4n69(~@t~!wniV%g7khFx~I*4>Y|V$4j5%KPF*-FyKIi@!Ho&", - "x8QQsksYt8)D+W)Ni!=G`ogSu^vLL-l#7A7=iIAKL2SuZk9F}NfNk86VI)9WZE?%2wC-ya", - "F~z#Qsq)LH0|_D8^5fU8X%GeQ4TB>R-dlziA&tZe&1ada208!$nk`7bOFO2S00Gb)R2-", + "0a+uwf`P3{_Y@HuCz}S$J$ZJ>R_V<~|Fk>sgX4=%0vUrh-lt@YP^Wrus;j?`Th#xRPzf<<", + "~Hp4DH^gZX>d{+WOp~HNu8!{uWu}&XphAd{j1;rB4|9?R!pqruAFUMt8#*WcrVS{;kLlY(", + "cJRV$w?d2car%Rs>q9BgTU4Ht-tQKZ7Z`9QqOb?R#b%z?rk", + ">!CkH7jy3wja4NG2q)H}fNRKg8v{);Em;K3Cncf4C6&OajV+DbX%o4+)CV3+e!Lm6dutu(", + "0BQpH1T?W(~cQtKV*^_Pdx!LirjpTs?Bmt@vktjLq4;)O!rrly=c*rwTwMJFd0I57`hgkc", + "?=nyI4RZf9W$6DCWugmf&)wk^tWH17owj=#PGH7Xv-", + "?9$jnjwDlkOE+BFNR9YXEmBpO;rqEw=e2IR-", + "8^(W;8ma?M3JVd($2T>IW+0tk|Gm8>ftukRQ9J8k3brzqMnVyjsLI-CKneFa)Lxvp_aq40f}0J3VVoWL5rox`Kptivcp}o5", + "xA^@>qNI%?zo=Yj4AMV?kbAA)j(1%)+Pp)bSn+7Yk`M{oE}L-Z!G6OMr5G+hp)$", + "3Lg{ono{4cN>Vr&>L4kXH;_VnBL5U!LgzqE%P7QQ*tue}O`3(TZ0`aKn&~8trOQ-rBXCp)f@P6RMO4l0+;b|5-", + "pk9_ryNh}Zc*v%mvz_#yd6fjB0g9{MmMnu8bG%#C~ugXK^S^k@?ab#O|aE>dDTt4s4n69(~@t~!wni", + "V%g7khFx~I*4>Y|V$4j5%KPF*-", + "FyKIi@!Ho&x8QQsksYt8)D+W)Ni!=G`ogSu^vLL-", + "l#7A7=iIAKL2SuZk9F}NfNk86VI)9WZE?%2wC-yaF~z#Qsq)LH0|_D8^5fU8X%GeQ4TB>R", + "-dlziA&tZe&1ada208!$nk`7bOFO2S00GH)lhRYu$%jt%!jv+I", "JxhE=%W1?wYb!37Rb?(rgwFIAQI{L#8r*zy!$TMtER_1(vn(Zix^{AVB1(jwr$iL6h0Z!2", "8Gb~UW@0~e512{Z%8}Qzdnjl~wJ1{c2>`Z@1A~t&lyL{p{eM{5)QGf7Mo5FW9==mlyXJt2", - "UwpntR7H0eSq!(aYq#aqUz&RM*tvuMI)AsM?K3-dV3-TT{t)!Iy#JTo=tXkzAM9~j2YbiO", - "ls3(H8Dc>Y|D1aqL51vjLbpYG;GvGTQB4bXuJ%mA;(B4eUpu$$@zv2vVcq-Y)VKbzp^tei", - "uzy}R{LuvDjpuVb`79O+CBmg{Wx!bvx$eu4zRE&", - "PehMb=&G<9$>iZ|bFE)0=4I?KLFGBC0I(0_svgw0%FiMsT%koo*!nEYc6GY@QnU}&4Isg;", - "l=|khi(!VaiSE2=Ny`&&tpi~~;{$uN}%f|7mBhAy;s3YT^sy!$eG~?`9mNJC9@4Bac_p^BZh)Yd_rWW5qh-?tKY(>5VHO", - "L*iT8P@wCavLj^yYbnDR+4ukhS+xPrpl)iqB?u)bj9a2aW==g6G3lCJd>(+Blfr)~^40F4f>cRZ^UF;RibfZ>0m73hR", - "C{$vTfC(STN`g7(B<=Z2556{}0`?p&|Akkst!4Xy4OT;A@c$XTUI3FRRjy*KA7uC56FD)z", - "^X{WV*sr(w!c$W357o!&eLO2wTDNOyw@gf(&R<t;=-Tu1TV{>%8ZVATC9tjD8|(&`$9YHvZ9bVe#>w", - "|8c;Tg|xE&)`*}LwM*E}q}q8^Qja%p`_U)*5DdLI9O@!e=3jFjOCrCq28b_bb;s>%D#iJB", - "CWJi{JH!Js;6nfayos$kq^OEX00HO-lokL0!mqm{vBYQl0ssI200dcD" + "UwpntR7H0eSq!(aYq#aqUz&RM*tvuMI)AsM?K3-dV3-", + "TT{t)!Iy#JTo=tXkzAM9~j2YbiOls3(H8Dc>Y|D1aqL51vjLbpYG;GvGTQB4bXuJ%mA;(B", + "4eUpu$$@zv2vVcq-", + "Y)VKbzp^teiuzy}R{LuvDjpuVb`79O+CBmg{Wx!", + "bvx$eu4zRE&PehMb=&G<9$>iZ|bFE)0=4I?KLFGBC0I(0_svgw0%FiMsT%koo*!nEYc6GY", + "@QnU}&4Isg;l=|khi(!VaiSE2=Ny`&&tpi~~;{$uMiozmmC;M)^ZwTGGtj|3E=aVrD5Bqa^;RV?wWs=hsohk2ql1XcqaOW|", + "E4urOCR0a_;Id)cJ-GU5w3iJ10Aox!FH|8SHoiD_{JMEEOV0O?MDsDaI;Y>v=$rgN_q~t`", + ";bL0p0cr+3oR<~L*q!A1N=YxYRoqaQo%ua#07hqE;U_X@B)L3Jd;2oN%}66Gz+0zO^<`*Jt_si5v;Qg-", + "3q})ae)#%Dl>20mc9;6%A4(VObs;0JpQAGD7ADLJ?Rqiv#V5ZljDomsklGi_^8gtLkmRF>EstlBz)cI_>MG*?Z%S", + "T;72Q8^Ku7t$MI_A1{5SEE>mII;1T8kfJf_FgrXNwx!AelhM`I@+>J+gg!uCyW&Vjnd0r~", + "xZpKPsHdS4@$h%w6Eq6>tdfnmQLWvk`|e2>DM-E4Qm9Nc-", + "(V_Gh@)x^Nj3H9p`fLm1IA_eaBVtRg`l1J@5pcFi$-", + "31XWMxJ!;}xqC^I};&9=Az*&VH*lfN0M|Gx-9!ypV67!-KSwt=VI-*jej4FC~WiMTU+-", + "L<2+d_ew|{n~!VJuGlSJ0d_=`@?_jC3h)r_?|J_;aJ|`U^>F6N?3pcY069b8yy#(^>pP8L", + "*@Vg?IIMosq$u-00HX=q!s`G%_Rg?vBYQl0ssI200dcD" ], "America/Santiago": [ - "{Wp48S^xk9=GL@E0stWa761SMbT8$j;0fRZ<6QtM7KxBg84(fsEAUJ$J{f-TXlPEUec5Ee", - "n+hsD4lC(QYax=JdSpoyje8%VM`GW}{bJ8@y$A8O&*$pw{(f~Os#}2w", - "eX6^Rgi$IT%n^V^85L>$_c7{cB^#ogV=rHBJGiz-RQNFGK?gdPi|q)j`&8)}KJ{qo6dixa", - "9@yYyVg+%lo0nO+Tw0-w2hJ%mafyWL)|", - ")?W6Bi%FWuGPA1Dru$XR4SZANsAthU2EoKHF6oEtKq`rwP", - "(VNegnI_NI%;ma$)wj{k!@KFB30Yo)IOrl>)$)D|+(5h&+%2vuwGuy^@S8FT^s21V5};>VA9Iu;?8bHz#r<;JtfZDI1(FT@edh0#", - "MYW$A1qkMGIwTZqqdYNE3gl#zp&NbL9Mp=voqN|;?gqR&4$)1`znddtEyuKS*^nMMD=0^>", - "7^z6-C4P67UWOXuMBubP>j6i~03aR@jD^-Y`JSYu#Yp0P8dLLJ0QOPE8=BoiuRX59YW7xg", - "WiexjHX%&0?`ZQCdxCdL^qd1v@kOjQKaWo2Y1++~LcA%FTq?5o%}fX1-RIvlB)1#iTNomGnUL=nM!>Ix|AGtON7!F1O?53kqlC2o-`ZGw*+s", - "NM$^9znsIJMwlgscE`|O3|;BRgsQMYm~`uv+nvuv`nigRa}X=BX=A5Sw$)WEklF7&c>_~$", - "zJ(m--bqXgiN^w-U=BJH9C0Qro(x90zo@rK;&TJ$nI@&k$ORgOb2s%gWbc}ok_27)Eoku~Fq|B-Ps+4J_", - "HPJMLJ2^_)cOU$p&3kNAlrV!)%~6r$BJ>OOi~=-<6byle{?zd4J{NG}o8tw|+#ZNLcpNwk", - "TuPE~sbJB8_RZb2DopStO+Wwux~F#S59zm%00I98;S&G=b(j+6vBYQl0ssI200dcD" + "{Wp48S^xk9=GL@E0stWa761SMbT8$j;0e+L++6@F7KxBg84(fsEAUJ$9Yj;p>Mq3>*@-", + "8>razI(EFyOtYzTyCY`3kVX~k`hSJt^$bT@V;iGRbT`JI3V)lBnui#lA$wIsfE8sL=RFVi", + "Wi5>;Y=+h5(#B8hW33CVCYO92MtEmXQtd@", + "E+3y)$%o~XozOC|QgFl*-mp9+(`s0w4O4a$v!xt`G&ZI1uE4c`JE-t5WW%1*91J-", + "tr^_>U@@l^R_i@%gJ)q{kYX|w-", + "m{DbN=Z+Zey_(<=ZlU4e**~fkShDtX&9_W>qj>RuJZ-ko&-nY}Eq_?SFX8N<9wT-TH*xbF0XT;L+siI1gPQrnKc|85", + "UZaq6@7JAVeu%)Aa6I>6D;F*N)|f6jhs01q_UU%gc_Ek#>SbTrf6i;OsAQ>8&urTeX|Ier", + "uSV{o||MjbXV?)AHzh1+v~odHV<_+-", + "M0v=QR6~y%&K00{%grZ=+5)JNlFg!9Q}2rv{$OC*D{_yiUd$SL!S)f@K#pe<#CBb&}BH0p", + "%w(SzGJjGX`q+ybM!e4GEP>b&S_4lnbBR!*5>ZIxYP?QK2DIny}eL_od7Ll|WdneS(sbh~", + "!}|r_VlzIOB|lsf+rf*B~wqYWzu*", + "k-", + "19B1stj0gO@Kd^97k>cVnNvEEaMwZ6=ObszLNnyDGMW0?9>PhVq$r$BCx^ZdJ#fO^fC1JG", + "oW>FS5oC!pr7=y}O@ecL6T+R$E20#GvlwVfswl?qnCEe|@!hI*Ooyk8~vfN}h@~NGp9$00", + "H<1(-Qyy=a@C3vBYQl0ssI200dcD" ], "Asia/Tokyo": [ - "{Wp48S^xk9=GL@E0stWa761SMbT8$j-~luMgIxeB7KxBg5R*;y?l4Rl4neXH3cv!OtfK@h", - "KZzauI)S!FSDREPhhBS6Fb$&Vv#7%;?Te|>pF^0HBr&z_Tk<%vMW_QqjevRZOp8XVFgP<8", - "TkT#`9H&0Ua;gT1#rZLV0HqbAKK;_z@nO;6t0LpF^0HBr&z_Tk<%vMW_QqjevRZOp8XVFgP<8TkT#`9H&0Ua;gT1#rZLV0HqbAKK;_z@", + "nO;6t0L}hOdk<>TdUa07R(LPI6@!GU$ty4=mwqHG-XVe*n(Yvgdlr+FqIU18!osi)48t~eWX8)&L", - "G)Ud^0zz@*AF+2r7E}Nf9Y72K~o-T%}D&z%}#7g2br?oH6ZiYH^%>J3D)TPKV(JY*bwjuw5=DsPB@~CrROZeN", - "x>A*H&CHrWt0`EP`m!F%waepl#|w#&`XgVc?~2M3uw$fGX~tf_Il!q#Aa<*8xlzQ2+7r6Z", - "^;Laa9F(WB_O&Dy2r>~@kSi16W{=6+i5GV=Uq~KX*~&HUN4oz7*O(gXIr}sDVcD`Ikgw#|", - "50ssal8s)Qy;?YGCf;*UKKKN!T4!Kqy_G;7PfQapugqvVBKy12v3TVH^L2", - "0?#5*VP~MOYfe$h`*L!7@tiW|_^X1N%<}`7YahiUYtMu5XwmOf3?dr+@zXHwW`z}ZDqZlT", - "<2Cs(<1%M!i6o&VK89BY0J7HPIo;O62s=|IbV^@y$N&#=>i^F00FcHoDl#3", - "Mdv&xvBYQl0ssI200dcD" + "{Wp48S^xk9=GL@E0stWa761SMbT8$j;0TQa5M2N&7KxBg5R*_t3)o}P`_Ull(nK1~U=~g{", + "!2#v7ta-VPM|>+w;XP~5#XzS!mmq>y(@${PQsHdkBYiJixTuCQ2N?55>HhxKTEufgeVl}_", + "Sq_FUIXFF$kI6!-", + "eKVzudw1Zyg8;a%MynCz&_3Cl@%Zs*_JRo#I#Ss92V)!~{AwVung!|wK?4JFyFv#=nfhNI", + "6Czr|3VjINV7V6#eFr=Gl1UDdL9MQRWZ`RiZET+fl#=kxZ_-", + "G@=#VCQEvCZVdj9;e?rY8ynpo&34DPa3", + "KI_Sc_0#?r^44F}Rzb_ChM7PPrq3@FkqqjWqgvOzPq+vkb;DT&2YMC|tC3lu6Ihz2prrFP", + "vOU@6I3$XU@68kFFWx8fx?jt@frXW`;AZso*plI73>*wENVoc+2cm`jgoe6(B`w`{BdxVOWD1rqVTCN+Y4kRvHLYXlr)vU4G7S$p~Ia_n|G>=3N5dDB8cS6IGd-ckfc7", + "8#^71HtN_BO2#F%&%Ll+IhR1DlEUG@+#_Yl8AKa1US?{&+yOwA=WA=!z-", + "B9t2>Tn9Wu2*8Ydrbage(I4dkD_vcP(|c-", + "LhGRkSn{W*nk?RJQN!j1RSZ}L6e<&UGMisk>nLH5JiLNgOSC7c&Mn1+Pit*Gz;s=M?_q!v", + "DZ@dJctlzl2>W(rvs{(CERM)RT0xuqDAl{)$*ztEhe=?|;3u&!6O1c88W}SFHDBBHh%rrh", + "U6=f6kjYt!N19xk*s_9IpbeS;(cPoTPX!vr00FE9ju8L=r}(KdvBYQl0ssI200dcD" ], "Europe/Dublin": [ - "{Wp48S^xk9=GL@E0stWa761SMbT8$j;0>b$_+0=h7KxBg5R*;&J77#T_U2R5sleVWFDmK~", - "Kzj5oh@`QKHvW^6V{jU-w>qg1tSt0c^vh;?qAqA0%t?;#S~6U8Qi", - "v&f1s9IH#g$m1k1a#3+lylw4mwT4QnEUUQdwg+xnEcBlgu31bAVabn41OMZVLGz6NDwG%X", - "uQar!b>GI{qSahE`AG}$kRWbuI~JCt;38)Xwbb~Qggs55t+MAHIxgDxzTJ;2xXx99+qCy4", - "45kC#v_l8fx|G&jlVvaciR<-wwf22l%4(t@S6tnX39#_K(4S0fu$FUs$isud9IKzCXB78NkARYq@9Dc0TGkhz);NtM_SSzEffN", - "l{2^*CKGdp52h!52A)6q9fUSltXF{T*Ehc9Q7u8!W7pE(Fv$D$cKUAt6wY=DA1mGgxC*VX", - "q_If3G#FY6-Voj`fIKk`0}Cc72_SD{v>468LV{pyBI33^p0E?}RwDA6Pkq--C~0jF&Z@Pv", - "!dx_1SN_)jwz@P$(oK%P!Tk9?fRjK88yxhxlcFtTjjZ$DYssSsa#ufYrR+}}nKS+r384o~", - "!Uw$nwTbF~qgRsgr0N#d@KIinx%hQB(SJyjJtDtIy(%mDm}ZBGN}dV6K~om|=U", - "VGkbciQ=^$_14|gT21!YQ)@y*Rd0i_lS6gtPBE9+ah%WIJPwzUTjIr+J1XckkmA!6WE16%", - "CVAl{Dn&-)=G$Bjh?bh0$Xt1UDcgXJjXzzojuw0>paV~?Sa`VN3FysqFxTzfKVAu*ucq#+m=|KSSMvp_#@-lwd+q*ue", - "FQ^5<|<0R-u4qYMbRqzSn&", - "Q7jSuvc%b+EZc%>nI(+&0Tl1Y>a6v4`uNFD-7$QrhHgS7Wnv~rDgfH;rQw3+m`LJxoM4v#", - "gK@?|B{RHJ*VxZgk#!p<_&-sjxOda0YaiJ1UnG41VPv(Et%ElzKRMcO$AfgU+Xnwg5p2_+", - "NrnZ1WfEj^fmHd^sx@%JWKkh#zaK0ox%rdP)zUmGZZnqmZ_9L=%6R8ibJH0bOT$AGhDo6{", - "fJ?;_U;D|^>5by2ul@i4Zf()InfFN}00EQ=q#FPL>RM>svBYQl0ssI200dcD" + "{Wp48S^xk9=GL@E0stWa761SMbT8$j;0>b$_gw%g7KxBg5R*;&J77#T_U2R5sleVWFDmK~", + "Kzj5oh@`QKHvW^6V{jU-", + "w>qg1tSt0c^vh;?qAqA0%t?;#S~6U8Qiv&f1s9IH#g$m1k1a#3+lylw4mwT4QnEUUQdwg+", + "xnEcBlgu31bAVabn41OMZVLGz6NDwG%XuQar!b>GI{qSahE`AG}$kRWbuI~JCt;38)Xwbb", + "~Qggs55t+MAHIxgDxzTJ;2xXx99+qCy445kC#v_l8fx|G&jlVvaciR<-", + "wwf22l%4(t@S6tnX39#_K(4S0fu$FUs$isud9IKzCXB78NkARYq@9Dc0TGkhz);NtM_SSzEffNl{2^*CKGdp52h!52A)6q9fUSltXF{T", + "*Ehc9Q7u8!W7pE(Fv$D$cKUAt6wY=DA1mGgxC*VXq_If3G#FY6-", + "Voj`fIKk`0}Cc72_SD{v>468LV{pyBI33^p0E?}RwDA6Pkq--C~0jF&Z@Pv!dx_1SN_)jw", + "z@P$(oK%P!Tk9?fRjK88yxhxlcFtTjjZ$DYssSsa#ufYrR+}}nKS+r384o~!Uw$nwTbF~q", + "gRsgr0N#d@KIinx%hQB(SJyjJtDtIy(%mDm}ZBGN}dV6K~om|=UVGkbciQ=^$_", + "14|gT21!YQ)@y*Rd0i_lS6gtPBE9+ah%WIJPwzUTjIr+J1XckkmA!6WE16%CVAl{Dj@)nt", + "QW>5WCgG;uYjCg(j>qCs(*}h9IWV8w>LXsNq+C>vLWc", + ">tZt7asixk*XDd^+o!O=O52B7aPqt&HfgmRCd4-+ky?P-", + "XP?5Np6dxH29G;c3@torNWCs!ufT_DK<~j-", + "UgqAdf5G4ZdP`bECDkw8*?qvrtnMfoJT^Wq|8_~4;n!EAt=*dv)-", + "0yF8uKXAiZ~3d|7Z>ZO|t~YD2@;`6Qg{xlf8Q&;*xB-Ys_R2Cp{y((HBw7kN@@CtfA<<$B", + "fGc2BhK{XBpaQiJGWmO!j9XN7n0YhjD8vjw0{?{0zZm5js?lNS*8*{rU&d|`Rl_i(p2nHf}VB&&P+m6k^", + "y|9Wu}iApF2OdyoJC2VV0^9?>qd00ENOKUU4S;*h>v*ep0", - "xTi1cK2{aY*|2D*-~K<;-{_W+r@NvZ7-|NZv($ek_C%VfP0xjWeZP#CPXD`IKkakjh(kUd", - "&H)m;^Q(jGjIyiyrcUMtOP)u3A>sw6ux;Bmp3x$4QvQKMx5TrCx_!$srWQuXNs&`9=^IY1", - "yc&C31!sQh7P=Mk*#6x8Z@5^%ehR8UW$OWw0KMw}P1ycI^", - "4eh12oBUOV?S>n*d!+EM@>x#9PZD12iD=zaC;7`8dTfkU_6d}OZvSFSbGgXeKw}XyX@D=(", - ")D0!^DBGr8pXWBT$S-yhLP>Z3ys^VW3}RQ6{NGGVJG6vf*MH93vvNW6yLjie1;{4tVhg-KnSf|G`!", - "Z;j$7gJ1ows~RD=@n7I6aFd8rOR_7Y?E-$clI%1o5gA@O!KPa^(8^iFFeFykI-+z>E$mvp", - "E_h`vbHPjqkLs`Dn-0FV`R@z|h!S(Lb;M&|Exr!biY`%bfp$6`hK;GDhdP|^Q", - "*Ty*}1d41K>H2B{jrjE9aFK>yAQJBX9CD%-384S;0fw`PlprHGS`^b$oS-`I4VH7ji8ou-", - "g|060jfb1XcxiInT0oOoeR7#%e5Ug5#KW)nVSRvLHNe$SQHM@2)`S9L7>RL@Qx%fmm7?3u7P5TywFQ}C@S(pq}|", - "eLPT{C^{<0Q?uU&kSVd%!~8q3;Z0s3OqzF`$HRkePL5Ywgiwn{R(zi+jmOBFrVpW;)@UsU#%$8BcV#h@}m$#!Fglo&bwb78aYqOG_W7h{eb(+39&-mk4EIXq_", - "_`30=8sfA3=!3TO_TyS5X22~?6nKngZ|bq=grdq=9X)3xAkA42L!~rmS)n3w-~;lgz%Fhn", - "(?rXdp2ho~9?wmVs2JwVt~?@FVD%`tN69{(i3oQa;O0|FpV7KxBg5R*;+c_2(RlxFoAjepiN+t(&A", + "khpF;7sO(r>4o7M4zZVa$XY~v@-", + "@YI68ej+V4yBoGpi5ZYu;Zw>N;+Vkw)1TIVA4;Vn*JV=%f`o+fC9=H@gz8U6N-", + "M+jj4KAFBq5|dk<~RQE?jJV`tG;f@gAn7I{P&H39;;isJ!fMa`+wG", + "KkWXbi?P7&ivzm}BJ9guJ&D1=Ci_?iEC=T+}Ht)wPz7azBV$vs`Fg0TYba5j@iBgmJ}w{Wg", + "C;WaVQ5{ZjAIYY-lC8|yfnqplNF0_6r6Jo}h*-", + "^e6n7IHY9#^nyJM($bLr;XKyoo4e=k4E_rXAXU|8;BGkQ^kNAtK@OYtPw+AVo@wAk+F{Xd", + "YfNlG<<-FnlW4;LdIej=F0WFr~-c^-", + "M)7hP!$diHGAim;v4c$Ui^z<`A!+F8RhS5DXaZomO|PHR8B%eNWlI^Y;%XH23*BQ9Pv!0dNOS", + "JW;D6iM}OHVD9S;=_EGP=j^CP6mQ~WcEd%i!wnK!|`afKPHZ{nSFK{L!7$MM299!v3kGuq;oa;+;@@n1r@0Kc%yl18x$nR#Kzi@?+E(y", + "7o2rS3FClOOT`Amt}UyGfYZ7QWYqqCA(t;kD}FGa+vi>Zi)j}a2lrvx_};$<>-", + "h&pt_ePq>W5KHZPr_P0CVzjF(&G-", + "bL>Xi>@Jv$b)=hLcP>(ngUOdeI5{4_W6pSrkv%rnFAW~Xq0%BpP=^5An)E2@X9`)$o<{(F", + "VhGYMIW)@T&B7JZ06+RhK~>g?V;J{)+)M6xch^XnnI^Q0TC;zLSj(LMJ&sOV*Qj&~gGAP}", + "&ujCT8Fj?*_GQ!H)00D>!#~T0u=fLlPvBYQl0ssI200dcD" ], "Europe/London": [ - "{Wp48S^xk9=GL@E0stWa761SMbT8$j;0`|pJ6!-O7KxBg5R*;$9DqzW!kQs3DZt(=0_!m1", - "4wvE`6N%Vj#u6PS_3S?~(2)&xn8}2}3Wr#kG8n2!x8>$E$lF&~Y#_H6bu6(BiwblJ>;-Fs", - "gA$Y$*?=X)n1pFkKn}F~`>=4)+LLQk?L*P!bhAm0;`N~z3QbUIyVrm%kOZ(n1JJsm0pyb8", - "!GV{d*C!9KXv;4vD4Q>-k#+x(!V5L@w5M>v2V5a`B>t(|B", - "|Fqr4^-{S*%Ep~ojUtx_CRbSQ(uFwu2=KH)Q@EBs@ZqRXn4mU;B!68;;IQs3Ub=n&UU%*m", - "k&zwD36&JSwsN(%k&x?H+tN^6)23c`I0=5^N_R0~1>tsFZ`^`3z~rXSXT&qcwa#n!%+Z#P", - "PG}(D^_CCILXnF|GKwabBh*xFS?4rwGo2vtJUwzrbv_$5PO+`?$l{H-jGB@X%S!OAhw;D4", - "XFycN3!XqQ&EorJOD3>~^U%Luw!jF<;6_q-f-S|6{cQDfZ2(4Xf1MMLr1=SA=MwVf2%Pp%VP;jn)|5Tf!-DbUGn%I-rkYaH7?$$O!t)wwClAisr3eUoeB^~T=U*_P~Y2*KdnO87>B!19sV=xZ5", - "yApq26RxgqA|*tmsvtL#OhcF(C<0EGWHP)BFl?h)_*7!{LoJiv%RsOs!q->n+DcV%9~B@RbC_1G_1g6`Yd~8|%-=2l~oGN!~TVv2Bnk>7wW8L@^?vX$f3AiT)(4nrCuTm9%(XC6Nai", - "E(;}7&=YZagjAN$O-cN;1u{dTkElmB0GT$|Wa)QMmKrx<|LCJ9qlUoFsUbD^H^6_8(w<0{", - "ftj&O1~p_%lh5z;zNV&sP$E$lF&~Y#_H6bu6(BiwblJ>;-", + "FsgA$Y$*?=X)n1pFkKn}F~`>=4)+LLQk?L*P!bhAm0;`N~z3QbUIyVrm%kOZ(n1JJsm0py", + "b8!GV{d*C!9KXv;4vD4Q>-", + "k#+x(!V5L@w5M>v2V5a`B>t(|B|Fqr4^-", + "{S*%Ep~ojUtx_CRbSQ(uFwu2=KH)Q@EBs@ZqRXn4mU;B!68;;IQs3Ub=n&UU%*mk&zwD36", + "&JSwsN(%k&x?H+tN^6)23c`I0=5^N_R0~1>tsFZ`^`3z~rXSXT&qcwa#n#k^5sFT7HKtyC", + "9A0oluNf^P4yk-d_|mUd;T0;~y@2Cu@Xd;IRT#++Y*Z6*0%M4`z`3a2vnHc-", + "B(#xp*94c8t_y=lV~#Q=Whn&p<+Qzs`pL<^`L`BrF9ohSDa^SnosMt{zpFXpUjqumClb$D", + "^}s!&5JM`)L?H!Gzj6q@G0mVG?P#xu;u1DMoWN$14T", + "b>^F1?&ymdJ4TS`!aAO1#Myz0fU6<#?jJoc&X-", + "z&$D#Zj!Dg3w**{JNc(J;D0DrI1K1-", + "iv3~i7vFT?5yqP3rSI@jsz*k!0bE*~Pkn+g>u6Z;Se-", + ")c$&;tI=8t;#10nr-)VFCx@^bf}Ak2a$g+MN&Pv7{W+wBYjvrBy)wwx-", + "|SlQynn=O3ULjr$jM*QQAezq5|DqO63Zf+jql$4rQ2m|lPqGyA<>cV6bIA", + "o~*{`bgCh=A>WU}#b$t&5MPj$~|esT>8v-", + "C$_n!^iqI5;Q?X=YtG#wS0iS@Z+K+>Pw@;#vN~2NVaNcNiR~iPI$HEp0CZblbg!9944qJf", + "e^U%kyd74NW@4bR+8mK^RfSa}|2+8EsMDCnOSZ@D)kcJx`Cx~hD", + "N7PV9xa~1y#{E$Hy#KX4zE=~k$7%Vm?00000LN&_Vg>>9w00Gqs&>R2&ZK8hovBYQl0ssI", + "200dcD" ], "Pacific/Kiritimati": [ - "{Wp48S^xk9=GL@E0stWa761SMbT8$j-~jCaVO;+", - "NF2>iK{8KMUf+)<-)VxXbLxD(alL}N$AT-ogNbJSMMYeX+Z{jS)b8TK^PB=FxyBxzfmFto", - "eo0R`a(%NO?#aEH9|?Cv00000NIsFh6BW2800DjO0RR918Pu^`vBYQl0ssI200dcD" + "{Wp48S^xk9=GL@E0stWa761SMbT8$j-~itMU0nbw7KxBg5R*{Ky3@w34e-", + "emlEe%!1&HoVz@2)g91$Qvf%e=}%J+k5^yTOu{Q*OrV;`agAU{YYC1>Bq#Lo)Ct}OFqAaP", + "SYvN>0#bjna^XrQl~da<70IbZ?+00000VC(mK^UzXX00DX60RR91ZYU6cvBYQl0ssI200d", + "cD" ], "UTC": [ - "{Wp48S^xk9=GL@E0stWa761SMbT8$j-~e#|9bEt_7KxBg5R*|3h1|xhHLji!C57qW6L*|H", - "pEErm00000ygu;I+>V)?00B92fhY-(AGY&-0RR9100dcD" + "{Wp48S^xk9=GL@E0stWa761SMbT8$j-", + "~e#|9bEt_7KxBg5R*|3h1|xhHLji!C57qW6L*|HpEErm00000ygu;I+>V)?00B92fhY-", + "(AGY&-0RR9100dcD" ] }, "metadata": { - "version": "2020a" + "version": "2025b" } } \ No newline at end of file diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index 111b9d92283..41ba92342ff 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -29,7 +29,7 @@ try: importlib.metadata.metadata("tzdata") - HAS_TZDATA_PKG = True + HAS_TZDATA_PKG = (importlib.metadata.version("tzdata") == "2025.3") except importlib.metadata.PackageNotFoundError: HAS_TZDATA_PKG = False @@ -316,6 +316,18 @@ def test_unambiguous(self): self.assertEqual(dt.utcoffset(), offset.utcoffset, dt) self.assertEqual(dt.dst(), offset.dst, dt) + def test_datetime_subclass_negative_components(self): + class MinusOneDateTime(datetime): + hour = minute = second = -1 + + zi = self.zone_from_key("UTC") + dt = MinusOneDateTime(2024, 1, 1, tzinfo=zi) + + self.assertEqual(dt.utcoffset(), ZERO) + self.assertEqual(dt.dst(), ZERO) + self.assertEqual(dt.tzname(), "UTC") + self.assertEqual(zi.fromutc(dt), datetime(2024, 1, 1, tzinfo=zi)) + def test_folds_and_gaps(self): test_cases = [] for key in self.zones(): @@ -1009,14 +1021,14 @@ def populate_tzstr_header(cls): cls._tzif_header = bytes(out) - def zone_from_tzstr(self, tzstr): + def zone_from_tzstr(self, tzstr, encoding="ascii"): """Creates a zoneinfo file following a POSIX rule.""" zonefile = io.BytesIO(self._tzif_header) zonefile.seek(0, 2) # Write the footer zonefile.write(b"\x0A") - zonefile.write(tzstr.encode("ascii")) + zonefile.write(tzstr.encode(encoding)) zonefile.write(b"\x0A") zonefile.seek(0) @@ -1111,6 +1123,9 @@ def test_extreme_tzstr(self): "AAA4BBB,J1/2,J1/14", "AAA4BBB,J20/2,J365/2", "AAA4BBB,J365/2,J365/14", + # Leading-zero day-of-year + "AAA4BBB,J001/2,J065/2", + "AAA4BBB,001/2,065/2", # Extreme transition hour "AAA4BBB,J60/167,J300/2", "AAA4BBB,J60/+167,J300/2", @@ -1142,9 +1157,21 @@ def test_extreme_tzstr(self): def test_invalid_tzstr(self): invalid_tzstrs = [ "PST8PDT", # DST but no transition specified + # gh-152212: the std offset is required (POSIX TZ grammar) + "AAA", + "A", + "AA", + "B", "+11", # Unquoted alphanumeric "GMT,M3.2.0/2,M11.1.0/3", # Transition rule but no DST "GMT0+11,M3.2.0/2,M11.1.0/3", # Unquoted alphanumeric in DST + # Unquoted abbreviation with embedded or leading whitespace + "AB C3", + " A B 3", + "AAA4BB B,J60/2,J300/2", # Embedded whitespace in DST + # Empty quoted abbreviation + "<>5", + "AAA4<>,M3.2.0/2,M11.1.0/3", "PST8PDT,M3.2.0/2", # Only one transition rule # Invalid offset hours "AAA168", @@ -1184,6 +1211,11 @@ def test_invalid_tzstr(self): # Invalid weekday "AAA4BBB,M1.1.7/2,M2.1.1/2", "AAA4BBB,M1.1.1/2,M2.1.7/2", + # Invalid Mm.w.d separator + "AAA4BBB,M3.2X0,M11.1.0", + "AAA4BBB,M3.2.0,M11.1X0", + "AAA4BBB,M3.2-0,M11.1.0/3", + "AAA4BBB,M3.2.0/2,M11.1:0", # Invalid numeric offset "AAA4BBB,-1/2,20/2", "AAA4BBB,1/2,-1/2", @@ -1192,6 +1224,15 @@ def test_invalid_tzstr(self): # Invalid julian offset "AAA4BBB,J0/2,J20/2", "AAA4BBB,J20/2,J366/2", + # gh-152847: non-digit day-of-year + "AAA4BBB,J1_0,J300/2", + "AAA4BBB,J60/2,J30_0/2", + "AAA4BBB,1_0,J300/2", + "AAA4BBB,J+1,J300/2", + "AAA4BBB,J 1,J300/2", + "AAA4BBB, 1,J300/2", + "AAA4BBB,J0001,J300/2", + "AAA4BBB,0001,J300/2", # Invalid transition time "AAA4BBB,J60/2/3,J300/2", "AAA4BBB,J60/2,J300/2/3", @@ -1222,6 +1263,24 @@ def test_invalid_tzstr(self): with self.assertRaisesRegex(ValueError, tzstr_regex): self.zone_from_tzstr(invalid_tzstr) + def test_invalid_tzstr_non_ascii_abbr(self): + tzstr = "ABÀC3" + if self.module is py_zoneinfo: + expected = re.escape(tzstr) + else: + expected = re.escape(repr(tzstr.encode("utf-8"))) + with self.assertRaisesRegex(ValueError, expected): + self.zone_from_tzstr(tzstr, encoding="utf-8") + + def test_invalid_tzstr_non_ascii_dst_date(self): + tzstr = "AAA4BBB,J١,J300/2" + if self.module is py_zoneinfo: + expected = re.escape(tzstr) + else: + expected = re.escape(repr(tzstr.encode("utf-8"))) + with self.assertRaisesRegex(ValueError, expected): + self.zone_from_tzstr(tzstr, encoding="utf-8") + @classmethod def _populate_test_cases(cls): # This method uses a somewhat unusual style in that it populates the @@ -2055,6 +2114,21 @@ def test_exclude_posixrules(self): actual = self.module.available_timezones() self.assertEqual(actual, expected) + def test_exclude_localtime(self): + expected = { + "America/New_York", + "Europe/London", + } + + tree = list(expected) + ["localtime"] + + with tempfile.TemporaryDirectory() as td: + for key in tree: + self.touch_zone(key, td) + + with self.tzpath_context([td]): + actual = self.module.available_timezones() + self.assertEqual(actual, expected) class CTestModule(TestModule): module = c_zoneinfo @@ -2296,8 +2370,8 @@ def _America_Los_Angeles(): ] def _America_Santiago(): - LMT = ZoneOffset("LMT", timedelta(seconds=-16966), ZERO) - SMT = ZoneOffset("SMT", timedelta(seconds=-16966), ZERO) + LMT = ZoneOffset("LMT", timedelta(seconds=-16965), ZERO) + SMT = ZoneOffset("SMT", timedelta(seconds=-16965), ZERO) N05 = ZoneOffset("-05", timedelta(seconds=-18000), ZERO) N04 = ZoneOffset("-04", timedelta(seconds=-14400), ZERO) N03 = ZoneOffset("-03", timedelta(seconds=-10800), ONE_H) @@ -2332,8 +2406,8 @@ def _Australia_Sydney(): return [ ZoneTransition(datetime(1895, 2, 1), LMT, AEST), - ZoneTransition(datetime(1917, 1, 1, 0, 1), AEST, AEDT), - ZoneTransition(datetime(1917, 3, 25, 2), AEDT, AEST), + ZoneTransition(datetime(1917, 1, 1, 2), AEST, AEDT), + ZoneTransition(datetime(1917, 3, 25, 3), AEDT, AEST), ZoneTransition(datetime(2012, 4, 1, 3), AEDT, AEST), ZoneTransition(datetime(2012, 10, 7, 2), AEST, AEDT), ZoneTransition(datetime(2040, 4, 1, 3), AEDT, AEST), @@ -2341,7 +2415,7 @@ def _Australia_Sydney(): ] def _Europe_Dublin(): - LMT = ZoneOffset("LMT", timedelta(seconds=-1500), ZERO) + LMT = ZoneOffset("LMT", timedelta(seconds=-1521), ZERO) DMT = ZoneOffset("DMT", timedelta(seconds=-1521), ZERO) IST_0 = ZoneOffset("IST", timedelta(seconds=2079), ONE_H) GMT_0 = ZoneOffset("GMT", ZERO, ZERO) diff --git a/Lib/timeit.py b/Lib/timeit.py index 0451f66bdf1..01bdfd901e3 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -6,35 +6,6 @@ Library usage: see the Timer class. -Command line usage: - python timeit.py [-n N] [-r N] [-s S] [-p] [-h] [--] [statement] - -Options: - -n/--number N: how many times to execute 'statement' (default: see below) - -r/--repeat N: how many times to repeat the timer (default 5) - -s/--setup S: statement to be executed once initially (default 'pass'). - Execution time of this setup statement is NOT timed. - -p/--process: use time.process_time() (default is time.perf_counter()) - -v/--verbose: print raw timing results; repeat for more digits precision - -u/--unit: set the output time unit (nsec, usec, msec, or sec) - -h/--help: print this usage message and exit - --: separate options from statement, use when statement starts with - - statement: statement to be timed (default 'pass') - -A multi-line statement may be given by specifying each line as a -separate argument; indented lines are possible by enclosing an -argument in quotes and using leading spaces. Multiple -s options are -treated similarly. - -If -n is not given, a suitable number of loops is calculated by trying -increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the -total time is at least 0.2 seconds. - -Note: there is a certain baseline overhead associated with executing a -pass statement. It differs between versions. The code here doesn't try -to hide it, but you should be aware of it. The baseline overhead can be -measured by invoking the program without arguments. - Classes: Timer @@ -57,6 +28,7 @@ default_number = 1000000 default_repeat = 5 default_timer = time.perf_counter +default_target_time = 0.2 _globals = globals @@ -133,7 +105,7 @@ def __init__(self, stmt="pass", setup="pass", timer=default_timer, exec(code, global_ns, local_ns) self.inner = local_ns["inner"] - def print_exc(self, file=None): + def print_exc(self, file=None, **kwargs): """Helper to print a traceback from the timed code. Typical use: @@ -149,6 +121,11 @@ def print_exc(self, file=None): The optional file argument directs where the traceback is sent; it defaults to sys.stderr. + + The optional colorize keyword argument controls whether the + traceback is colorized; it defaults to False for programmatic + usage. When used from the command line, this is automatically + set based on terminal capabilities. """ import linecache, traceback if self.src is not None: @@ -158,7 +135,8 @@ def print_exc(self, file=None): dummy_src_name) # else the source is already stored somewhere else - traceback.print_exc(file=file) + kwargs['colorize'] = kwargs.get('colorize', False) + traceback.print_exc(file=file, **kwargs) def timeit(self, number=default_number): """Time 'number' executions of the main statement. @@ -206,12 +184,13 @@ def repeat(self, repeat=default_repeat, number=default_number): r.append(t) return r - def autorange(self, callback=None): - """Return the number of loops and time taken so that total time >= 0.2. + def autorange(self, callback=None, target_time=default_target_time): + """Return the number of loops and time taken so that + total time >= target_time (default is 0.2 seconds). Calls the timeit method with increasing numbers from the sequence - 1, 2, 5, 10, 20, 50, ... until the time taken is at least 0.2 - second. Returns (number, time_taken). + 1, 2, 5, 10, 20, 50, ... until the target time is reached. + Returns (number, time_taken). If *callback* is given and is not None, it will be called after each trial with two arguments: ``callback(number, time_taken)``. @@ -223,7 +202,7 @@ def autorange(self, callback=None): time_taken = self.timeit(number) if callback: callback(number, time_taken) - if time_taken >= 0.2: + if time_taken >= target_time: return (number, time_taken) i *= 10 @@ -257,53 +236,114 @@ def main(args=None, *, _wrap_timer=None): is not None, it must be a callable that accepts a timer function and returns another timer function (used for unit testing). """ + import argparse if args is None: args = sys.argv[1:] - import getopt + import _colorize + colorize = _colorize.can_colorize() + theme = _colorize.get_theme(force_color=colorize).timeit + reset = theme.reset + + epilog = """\ +A multi-line statement may be given by specifying each line as a +separate argument; indented lines are possible by enclosing an +argument in quotes and using leading spaces. Multiple `-s` options are +treated similarly. + +If `-n` is not given, a suitable number of loops is calculated by trying +increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the +total time is at least `--target-time` seconds. + +Note: there is a certain baseline overhead associated with executing a +pass statement. It differs between versions. The code here doesn't try +to hide it, but you should be aware of it. The baseline overhead can be +measured by invoking the program without arguments.""" + + parser = argparse.ArgumentParser( + prog="python -m timeit", + description="""\ +Tool for measuring execution time of small code snippets. + +This module avoids a number of common traps for measuring execution +times. See also Tim Peters' introduction to the Algorithms chapter in +the Python Cookbook, published by O'Reilly. + +Library usage: see the `Timer` class.""", + epilog=epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "-n", + "--number", + type=int, + default=0, + help="how many times to execute 'statement' (default: see below)", + ) + parser.add_argument( + "-r", + "--repeat", + type=int, + default=default_repeat, + help="how many times to repeat the timer (default %(default)s)", + ) + parser.add_argument( + "-s", + "--setup", + action="append", + default=[], + help="statement to be executed once initially. " + "Execution time of this setup statement is NOT timed. " + "(default 'pass')", + ) + parser.add_argument( + "-p", + "--process", + action="store_true", + help="use `time.process_time()` (default is `time.perf_counter()`)", + ) + parser.add_argument( + "-t", + "--target-time", + type=float, + default=default_target_time, + help="if `--number` is 0 the code will run until it takes " + "at least this many seconds (default %(default)s)", + ) + parser.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="print raw timing results; repeat for more digits precision", + ) + parser.add_argument( + "-u", + "--unit", + default=None, + choices=["nsec", "usec", "msec", "sec"], + help="set the output time unit", + ) + parser.add_argument( + "statement", + nargs="*", + default=["pass"], + help="statement to be timed (default 'pass')", + ) try: - opts, args = getopt.getopt(args, "n:u:s:r:pvh", - ["number=", "setup=", "repeat=", - "process", "verbose", "unit=", "help"]) - except getopt.error as err: - print(err) - print("use -h/--help for command line help") - return 2 - - timer = default_timer - stmt = "\n".join(args) or "pass" - number = 0 # auto-determine - setup = [] - repeat = default_repeat - verbose = 0 - time_unit = None + ns = parser.parse_args(args) + except SystemExit as e: + return e.code + + timer = time.process_time if ns.process else default_timer + stmt = "\n".join(ns.statement) or "pass" + number = ns.number + target_time = ns.target_time + setup = "\n".join(ns.setup) or "pass" + repeat = max(ns.repeat, 1) + verbose = ns.verbose + time_unit = ns.unit units = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.0} - precision = 3 - for o, a in opts: - if o in ("-n", "--number"): - number = int(a) - if o in ("-s", "--setup"): - setup.append(a) - if o in ("-u", "--unit"): - if a in units: - time_unit = a - else: - print("Unrecognized unit. Please select nsec, usec, msec, or sec.", - file=sys.stderr) - return 2 - if o in ("-r", "--repeat"): - repeat = int(a) - if repeat <= 0: - repeat = 1 - if o in ("-p", "--process"): - timer = time.process_time - if o in ("-v", "--verbose"): - if verbose: - precision += 1 - verbose += 1 - if o in ("-h", "--help"): - print(__doc__, end="") - return 0 - setup = "\n".join(setup) or "pass" + precision = 3 + max(verbose - 1, 0) # Include the current directory, so that local imports work (sys.path # contains the directory of this script, rather than the current @@ -315,18 +355,21 @@ def main(args=None, *, _wrap_timer=None): t = Timer(stmt, setup, timer) if number == 0: - # determine number so that 0.2 <= total time < 2.0 + # determine number so that total time >= target_time callback = None if verbose: def callback(number, time_taken): - msg = "{num} loop{s} -> {secs:.{prec}g} sec" - plural = (number != 1) - print(msg.format(num=number, s='s' if plural else '', - secs=time_taken, prec=precision)) + s = "" if number == 1 else "s" + print( + f"{number} loop{s} " + f"{theme.punctuation}-> " + f"{theme.timing}{time_taken:.{precision}g} sec{reset}" + ) + try: - number, _ = t.autorange(callback) + number, _ = t.autorange(callback, target_time) except: - t.print_exc() + t.print_exc(colorize=colorize) return 1 if verbose: @@ -335,7 +378,7 @@ def callback(number, time_taken): try: raw_timings = t.repeat(repeat, number) except: - t.print_exc() + t.print_exc(colorize=colorize) return 1 def format_time(dt): @@ -353,24 +396,34 @@ def format_time(dt): return "%.*g %s" % (precision, dt / scale, unit) if verbose: - print("raw times: %s" % ", ".join(map(format_time, raw_timings))) + raw = f"{theme.punctuation}, ".join( + f"{theme.timing}{t}" for t in map(format_time, raw_timings) + ) + print(f"raw times: {raw}{reset}") print() timings = [dt / number for dt in raw_timings] - best = min(timings) - print("%d loop%s, best of %d: %s per loop" - % (number, 's' if number != 1 else '', - repeat, format_time(best))) - best = min(timings) worst = max(timings) + s = "" if number == 1 else "s" + print( + f"{number} loop{s}, best of {repeat}: " + f"{theme.best}{format_time(best)}{reset} " + f"{theme.per_loop}per loop{reset}" + ) + if worst >= best * 4: import warnings - warnings.warn_explicit("The test results are likely unreliable. " - "The worst time (%s) was more than four times " - "slower than the best time (%s)." - % (format_time(worst), format_time(best)), - UserWarning, '', 0) + + print(file=sys.stderr) + warnings.warn_explicit( + f"{theme.warning}The test results are likely unreliable. " + f"The {theme.warning_worst}worst time ({format_time(worst)})" + f"{theme.warning} was more than four times slower than the " + f"{theme.warning_best}best time ({format_time(best)})" + f"{theme.warning}.{reset}", + UserWarning, "", 0, + ) return None diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 01ea2ab62cf..63b08b3e82d 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -3,7 +3,7 @@ Tkinter provides classes which allow the display, positioning and control of widgets. Toplevel widgets are Tk and Toplevel. Other widgets are Frame, Label, Entry, Text, Canvas, Button, Radiobutton, -Checkbutton, Scale, Listbox, Scrollbar, OptionMenu, Spinbox +Checkbutton, Scale, Listbox, Scrollbar, OptionMenu, Spinbox, LabelFrame and PanedWindow. Properties of the widgets are specified with keyword arguments. @@ -255,6 +255,10 @@ class Event: type - type of the event as a number widget - widget in which the event occurred delta - delta of wheel movement (MouseWheel) + detail - certain fixed strings (see Tcl/Tk documentation) + (Enter, Leave, FocusIn, FocusOut, ConfigureRequest) + user_data - data string which was passed to event_generate() or empty + string (VirtualEvent) """ def __repr__(self): @@ -501,12 +505,14 @@ def trace_variable(self, mode, callback): Return the name of the callback. This deprecated method wraps a deprecated Tcl method removed - in Tcl 9.0. Use trace_add() instead. + in Tcl 9.0 and will be removed in Python 3.17. Use trace_add() + instead. """ import warnings warnings.warn( - "trace_variable() is deprecated and not supported with Tcl 9; " - "use trace_add() instead.", + "trace_variable() is deprecated and will be removed in Python " + "3.17; use trace_add() instead. It is not supported with " + "Tcl 9.", DeprecationWarning, stacklevel=2) cbname = self._register(callback) self._tk.call("trace", "variable", self._name, mode, cbname) @@ -521,12 +527,14 @@ def trace_vdelete(self, mode, cbname): CBNAME is the name of the callback returned from trace_variable or trace. This deprecated method wraps a deprecated Tcl method removed - in Tcl 9.0. Use trace_remove() instead. + in Tcl 9.0 and will be removed in Python 3.17. Use trace_remove() + instead. """ import warnings warnings.warn( - "trace_vdelete() is deprecated and not supported with Tcl 9; " - "use trace_remove() instead.", + "trace_vdelete() is deprecated and will be removed in Python " + "3.17; use trace_remove() instead. It is not supported with " + "Tcl 9.", DeprecationWarning, stacklevel=2) self._tk.call("trace", "vdelete", self._name, mode, cbname) cbname = self._tk.splitlist(cbname)[0] @@ -544,12 +552,14 @@ def trace_vinfo(self): """Return all trace callback information. This deprecated method wraps a deprecated Tcl method removed - in Tcl 9.0. Use trace_info() instead. + in Tcl 9.0 and will be removed in Python 3.17. Use trace_info() + instead. """ import warnings warnings.warn( - "trace_vinfo() is deprecated and not supported with Tcl 9; " - "use trace_info() instead.", + "trace_vinfo() is deprecated and will be removed in Python " + "3.17; use trace_info() instead. It is not supported with " + "Tcl 9.", DeprecationWarning, stacklevel=2) return [self._tk.splitlist(x) for x in self._tk.splitlist( self._tk.call("trace", "vinfo", self._name))] @@ -817,7 +827,10 @@ def focus_get(self): the focus.""" name = self.tk.call('focus') if name == 'none' or not name: return None - return self._nametowidget(name) + try: + return self._nametowidget(name) + except KeyError: + return None def focus_displayof(self): """Return the widget which has currently the focus on the @@ -826,14 +839,20 @@ def focus_displayof(self): Return None if the application does not have the focus.""" name = self.tk.call('focus', '-displayof', self._w) if name == 'none' or not name: return None - return self._nametowidget(name) + try: + return self._nametowidget(name) + except KeyError: + return None def focus_lastfor(self): """Return the widget which would have the focus if top level for this widget gets the focus from the window manager.""" name = self.tk.call('focus', '-lastfor', self._w) if name == 'none' or not name: return None - return self._nametowidget(name) + try: + return self._nametowidget(name) + except KeyError: + return None def tk_focusFollowsMouse(self): """The widget under mouse will get automatically focus. Can not @@ -1236,7 +1255,10 @@ def winfo_containing(self, rootX, rootY, displayof=0): + self._displayof(displayof) + (rootX, rootY) name = self.tk.call(args) if not name: return None - return self._nametowidget(name) + try: + return self._nametowidget(name) + except KeyError: + return None def winfo_depth(self): """Return the number of bits per pixel.""" @@ -1499,13 +1521,26 @@ def bindtags(self, tagList=None): else: self.tk.call('bindtags', self._w, tagList) - def _bind(self, what, sequence, func, add, needcleanup=1): + def _delete_bind_commands(self, *what): + lines = self.tk.call(what).split('\n') + p = re.compile(r'if \{"\[([^ ]+) .*\]" == "break"\} break') + for line in lines: + m = p.fullmatch(line) + if m: + funcid = m[1] + try: + self.deletecommand(funcid) + except TclError: + pass + + def _bind(self, what, sequence, func, add): """Internal function.""" if isinstance(func, str): self.tk.call(what + (sequence, func)) elif func: - funcid = self._register(func, self._substitute, - needcleanup) + if not add: + self._delete_bind_commands(*what, sequence) + funcid = self._register(func, self._substitute, needcleanup=True) cmd = ('%sif {"[%s %s]" == "break"} break\n' % (add and '+' or '', @@ -1538,7 +1573,7 @@ def bind(self, sequence=None, func=None, add=None): for pressing A and the Alt key (KeyPress can be omitted). An event pattern can also be a virtual event of the form <> where AString can be arbitrary. This - event can be generated by event_generate. + event can be generated by event_generate(). If events are concatenated they must appear shortly after each other. @@ -1571,6 +1606,7 @@ def unbind(self, sequence, funcid=None): def _unbind(self, what, funcid=None): if funcid is None: + self._delete_bind_commands(*what) self.tk.call(*what, '') else: lines = self.tk.call(what).split('\n') @@ -1587,7 +1623,7 @@ def bind_all(self, sequence=None, func=None, add=None): An additional boolean parameter ADD specifies whether FUNC will be called additionally to the other bound function or whether it will replace the previous function. See bind for the return value.""" - return self._root()._bind(('bind', 'all'), sequence, func, add, True) + return self._root()._bind(('bind', 'all'), sequence, func, add) def unbind_all(self, sequence): """Unbind for all widgets for event SEQUENCE all functions.""" @@ -1601,7 +1637,7 @@ def bind_class(self, className, sequence=None, func=None, add=None): whether it will replace the previous function. See bind for the return value.""" - return self._root()._bind(('bind', className), sequence, func, add, True) + return self._root()._bind(('bind', className), sequence, func, add) def unbind_class(self, className, sequence): """Unbind for all widgets with bindtag CLASSNAME for event SEQUENCE @@ -1688,7 +1724,16 @@ def nametowidget(self, name): for n in name: if not n: break - w = w.children[n] + try: + w = w.children[n] + except KeyError: + # Menu clones (a menu used as a menubar or a cascade) get + # auto-generated names where each path component is the + # original name prefixed with one or more '#' clone markers. + # Map such a name back to the original widget. + if not n.startswith('#'): + raise + w = w.children[n.rsplit('#', 1)[-1]] return w @@ -1723,7 +1768,7 @@ def _root(self): w = self while w.master is not None: w = w.master return w - _subst_format = ('%#', '%b', '%f', '%h', '%k', + _subst_format = ('%#', '%b', '%d', '%f', '%h', '%k', '%s', '%t', '%w', '%x', '%y', '%A', '%E', '%K', '%N', '%W', '%T', '%X', '%Y', '%D') _subst_format_str = " ".join(_subst_format) @@ -1744,11 +1789,14 @@ def getint_event(s): if any(isinstance(s, tuple) for s in args): args = [s[0] if isinstance(s, tuple) and len(s) == 1 else s for s in args] - nsign, b, f, h, k, s, t, w, x, y, A, E, K, N, W, T, X, Y, D = args - # Missing: (a, c, d, m, o, v, B, R) + nsign, b, d, f, h, k, s, t, w, x, y, A, E, K, N, W, T, X, Y, D = args + # Missing: (a, c, m, o, v, B, R) e = Event() # serial field: valid for all events # number of button: ButtonPress and ButtonRelease events only + # detail: for Enter, Leave, FocusIn, FocusOut and ConfigureRequest + # events certain fixed strings (see Tcl/Tk documentation) + # user_data: data string from a virtual event or an empty string # height field: Configure, ConfigureRequest, Create, # ResizeRequest, and Expose events only # keycode field: KeyPress and KeyRelease events only @@ -1762,6 +1810,12 @@ def getint_event(s): # KeyRelease, and Motion events e.serial = getint(nsign) e.num = getint_event(b) + if T == EventType.VirtualEvent: + e.user_data = d + e.detail = '??' + else: + e.user_data = '??' + e.detail = d try: e.focus = getboolean(f) except TclError: pass e.height = getint_event(h) @@ -1848,6 +1902,7 @@ def cget(self, key): return self.tk.call(self._w, 'cget', '-' + key) __getitem__ = cget + __iter__ = None # prevent using __getitem__ for iteration def __setitem__(self, key, value): self.configure({key: value}) @@ -1884,9 +1939,21 @@ def pack_propagate(self, flag=_noarg_): propagate = pack_propagate - def pack_slaves(self): + def pack_content(self): """Returns a list of all of the content widgets in the packing order for this container.""" + try: + res = self.tk.call('pack', 'content', self._w) + except TclError: + if self.info_patchlevel() >= (8, 6): + raise + res = self.tk.call('pack', 'slaves', self._w) + return [self._nametowidget(x) for x in self.tk.splitlist(res)] + + content = pack_content + + def pack_slaves(self): + """Synonym for pack_content().""" return [self._nametowidget(x) for x in self.tk.splitlist( self.tk.call('pack', 'slaves', self._w))] @@ -1894,9 +1961,19 @@ def pack_slaves(self): slaves = pack_slaves # Place method that applies to the container widget - def place_slaves(self): + def place_content(self): """Returns a list of all the content widgets for which this widget is the container.""" + try: + res = self.tk.call('place', 'content', self._w) + except TclError: + if self.info_patchlevel() >= (8, 6): + raise + res = self.tk.call('place', 'slaves', self._w) + return [self._nametowidget(x) for x in self.tk.splitlist(res)] + + def place_slaves(self): + """Synonym for place_content().""" return [self._nametowidget(x) for x in self.tk.splitlist( self.tk.call( @@ -2017,7 +2094,7 @@ def grid_size(self): size = grid_size - def grid_slaves(self, row=None, column=None): + def grid_content(self, row=None, column=None): """Returns a list of the content widgets. If no arguments are supplied, a list of all of the content in this @@ -2026,6 +2103,21 @@ def grid_slaves(self, row=None, column=None): column is returned. """ args = () + if row is not None: + args = args + ('-row', row) + if column is not None: + args = args + ('-column', column) + try: + res = self.tk.call('grid', 'content', self._w, *args) + except TclError: + if self.info_patchlevel() >= (8, 6): + raise + res = self.tk.call('grid', 'slaves', self._w, *args) + return [self._nametowidget(x) for x in self.tk.splitlist(res)] + + def grid_slaves(self, row=None, column=None): + """Synonym for grid_content().""" + args = () if row is not None: args = args + ('-row', row) if column is not None: @@ -2344,7 +2436,7 @@ def wm_iconphoto(self, default=False, *args): # new in Tk 8.5 def wm_iconposition(self, x=None, y=None): """Set the position of the icon of this widget to X and Y. Return - a tuple of the current values of X and X if None is given.""" + a tuple of the current values of X and Y if None is given.""" return self._getints(self.tk.call( 'wm', 'iconposition', self._w, x, y)) @@ -2547,11 +2639,13 @@ def readprofile(self, baseName, className): if os.path.isfile(class_tcl): self.tk.call('source', class_tcl) if os.path.isfile(class_py): - exec(open(class_py).read(), dir) + with open(class_py, 'rb') as f: + exec(f.read(), dir) if os.path.isfile(base_tcl): self.tk.call('source', base_tcl) if os.path.isfile(base_py): - exec(open(base_py).read(), dir) + with open(base_py, 'rb') as f: + exec(f.read(), dir) def report_callback_exception(self, exc, val, tb): """Report callback exception on sys.stderr. @@ -2640,6 +2734,7 @@ def pack_info(self): info = pack_info propagate = pack_propagate = Misc.pack_propagate + content = pack_content = Misc.pack_content slaves = pack_slaves = Misc.pack_slaves @@ -2697,6 +2792,7 @@ def place_info(self): return d info = place_info + content = place_content = Misc.place_content slaves = place_slaves = Misc.place_slaves @@ -2752,6 +2848,7 @@ def grid_info(self): propagate = grid_propagate = Misc.grid_propagate rowconfigure = grid_rowconfigure = Misc.grid_rowconfigure size = grid_size = Misc.grid_size + content = grid_content = Misc.grid_content slaves = grid_slaves = Misc.grid_slaves @@ -2834,10 +2931,11 @@ class Toplevel(BaseWidget, Wm): def __init__(self, master=None, cnf={}, **kw): """Construct a toplevel widget with the parent MASTER. - Valid option names: background, bd, bg, borderwidth, class, - colormap, container, cursor, height, highlightbackground, - highlightcolor, highlightthickness, menu, relief, screen, takefocus, - use, visual, width.""" + Valid option names: background, backgroundimage (Tk 9.0+), bd, bg, + bgimg (Tk 9.0+), borderwidth, class, colormap, container, + cursor, height, highlightbackground, highlightcolor, + highlightthickness, menu, padx, pady, relief, screen, + takefocus, tile (Tk 9.0+), use, visual, width.""" if kw: cnf = _cnfmerge((cnf, kw)) extra = () @@ -2957,7 +3055,7 @@ def addtag_withtag(self, newtag, tagOrId): """Add tag NEWTAG to all items with TAGORID.""" self.addtag(newtag, 'withtag', tagOrId) - def bbox(self, *args): + def bbox(self, *args): # overrides Misc.bbox """Return a tuple of X1,Y1,X2,Y2 coordinates for a rectangle which encloses all items with tags specified as arguments.""" return self._getints( @@ -3096,7 +3194,7 @@ def find_withtag(self, tagOrId): """Return all items with TAGORID.""" return self.find('withtag', tagOrId) - def focus(self, *args): + def focus(self, *args): # overrides Misc.focus """Set focus to the first item specified in ARGS.""" return self.tk.call((self._w, 'focus') + args) @@ -3142,7 +3240,7 @@ def tag_lower(self, *args): (optional below another item).""" self.tk.call((self._w, 'lower') + args) - lower = tag_lower + lower = tag_lower # overrides Misc.lower def move(self, *args): """Move an item TAGORID given in ARGS.""" @@ -3170,7 +3268,7 @@ def tag_raise(self, *args): (optional above another item).""" self.tk.call((self._w, 'raise') + args) - lift = tkraise = tag_raise + lift = tkraise = tag_raise # overrides Misc.tkraise def scale(self, *args): """Scale item TAGORID with XORIGIN, YORIGIN, XSCALE, YSCALE.""" @@ -3220,12 +3318,13 @@ def __init__(self, master=None, cnf={}, **kw): """Construct a checkbutton widget with the parent MASTER. Valid option names: activebackground, activeforeground, anchor, - background, bd, bg, bitmap, borderwidth, command, cursor, - disabledforeground, fg, font, foreground, height, - highlightbackground, highlightcolor, highlightthickness, image, - indicatoron, justify, offvalue, onvalue, padx, pady, relief, - selectcolor, selectimage, state, takefocus, text, textvariable, - underline, variable, width, wraplength.""" + background, bd, bg, bitmap, borderwidth, command, compound, + cursor, disabledforeground, fg, font, foreground, height, + highlightbackground, highlightcolor, highlightthickness, + image, indicatoron, justify, offrelief, offvalue, onvalue, + overrelief, padx, pady, relief, selectcolor, selectimage, + state, takefocus, text, textvariable, tristateimage, + tristatevalue, underline, variable, width, wraplength.""" Widget.__init__(self, master, 'checkbutton', cnf, kw) def _setup(self, master, cnf): @@ -3269,13 +3368,15 @@ def __init__(self, master=None, cnf={}, **kw): """Construct an entry widget with the parent MASTER. Valid option names: background, bd, bg, borderwidth, cursor, - exportselection, fg, font, foreground, highlightbackground, - highlightcolor, highlightthickness, insertbackground, - insertborderwidth, insertofftime, insertontime, insertwidth, - invalidcommand, invcmd, justify, relief, selectbackground, - selectborderwidth, selectforeground, show, state, takefocus, - textvariable, validate, validatecommand, vcmd, width, - xscrollcommand.""" + disabledbackground, disabledforeground, exportselection, fg, + font, foreground, highlightbackground, highlightcolor, + highlightthickness, insertbackground, insertborderwidth, + insertofftime, insertontime, insertwidth, invalidcommand, + invcmd, justify, locale (Tk 9.1+), placeholder (Tk 9.0+), + placeholderforeground (Tk 9.0+), readonlybackground, relief, + selectbackground, selectborderwidth, selectforeground, show, + state, takefocus, textvariable, validate, validatecommand, + vcmd, width, xscrollcommand.""" Widget.__init__(self, master, 'entry', cnf, kw) def delete(self, first, last=None): @@ -3315,7 +3416,7 @@ def selection_adjust(self, index): select_adjust = selection_adjust - def selection_clear(self): + def selection_clear(self): # overrides Misc.selection_clear """Clear the selection if it is in this widget.""" self.tk.call(self._w, 'selection', 'clear') @@ -3354,9 +3455,11 @@ class Frame(Widget): def __init__(self, master=None, cnf={}, **kw): """Construct a frame widget with the parent MASTER. - Valid option names: background, bd, bg, borderwidth, class, - colormap, container, cursor, height, highlightbackground, - highlightcolor, highlightthickness, relief, takefocus, visual, width.""" + Valid option names: background, backgroundimage (Tk 9.0+), bd, bg, + bgimg (Tk 9.0+), borderwidth, class, colormap, container, + cursor, height, highlightbackground, highlightcolor, + highlightthickness, padx, pady, relief, takefocus, tile (Tk + 9.0+), visual, width.""" cnf = _cnfmerge((cnf, kw)) extra = () if 'class_' in cnf: @@ -3386,7 +3489,8 @@ def __init__(self, master=None, cnf={}, **kw): WIDGET-SPECIFIC OPTIONS - height, state, width + compound, height, state, + textangle (Tk 9.1+), width """ Widget.__init__(self, master, 'label', cnf, kw) @@ -3398,18 +3502,21 @@ class Listbox(Widget, XView, YView): def __init__(self, master=None, cnf={}, **kw): """Construct a listbox widget with the parent MASTER. - Valid option names: background, bd, bg, borderwidth, cursor, - exportselection, fg, font, foreground, height, highlightbackground, - highlightcolor, highlightthickness, relief, selectbackground, - selectborderwidth, selectforeground, selectmode, setgrid, takefocus, - width, xscrollcommand, yscrollcommand, listvariable.""" + Valid option names: activestyle, background, bd, bg, borderwidth, + cursor, disabledforeground, exportselection, fg, font, + foreground, height, highlightbackground, highlightcolor, + highlightthickness, inactiveselectbackground (Tk 9.1+), + inactiveselectforeground (Tk 9.1+), justify, listvariable, + relief, selectbackground, selectborderwidth, selectforeground, + selectmode, setgrid, state, takefocus, width, xscrollcommand, + yscrollcommand.""" Widget.__init__(self, master, 'listbox', cnf, kw) def activate(self, index): """Activate item identified by INDEX.""" self.tk.call(self._w, 'activate', index) - def bbox(self, index): + def bbox(self, index): # overrides Misc.bbox """Return a tuple of X1,Y1,X2,Y2 coordinates for a rectangle which encloses the item identified by the given index.""" return self._getints(self.tk.call(self._w, 'bbox', index)) or None @@ -3465,7 +3572,7 @@ def selection_anchor(self, index): select_anchor = selection_anchor - def selection_clear(self, first, last=None): + def selection_clear(self, first, last=None): # overrides Misc.selection_clear """Clear the selection from FIRST to LAST (included).""" self.tk.call(self._w, 'selection', 'clear', first, last) @@ -3486,7 +3593,7 @@ def selection_set(self, first, last=None): select_set = selection_set - def size(self): + def size(self): # overrides Misc.size """Return the number of elements in the listbox.""" return self.tk.getint(self.tk.call(self._w, 'size')) @@ -3512,7 +3619,8 @@ def __init__(self, master=None, cnf={}, **kw): """Construct menu widget with the parent MASTER. Valid option names: activebackground, activeborderwidth, - activeforeground, background, bd, bg, borderwidth, cursor, + activeforeground, activerelief (Tk 9.0+), background, bd, bg, + borderwidth, cursor, disabledforeground, fg, font, foreground, postcommand, relief, selectcolor, takefocus, tearoff, tearoffcommand, title, type.""" Widget.__init__(self, master, 'menu', cnf, kw) @@ -3642,6 +3750,14 @@ class Menubutton(Widget): """Menubutton widget, obsolete since Tk8.0.""" def __init__(self, master=None, cnf={}, **kw): + """Construct a menubutton widget with the parent MASTER. + + Valid option names: activebackground, activeforeground, anchor, + background, bd, bg, bitmap, borderwidth, compound, cursor, + direction, disabledforeground, fg, font, foreground, height, + highlightbackground, highlightcolor, highlightthickness, + image, indicatoron, justify, menu, padx, pady, relief, state, + takefocus, text, textvariable, underline, width, wraplength.""" Widget.__init__(self, master, 'menubutton', cnf, kw) @@ -3649,6 +3765,12 @@ class Message(Widget): """Message widget to display multiline text. Obsolete since Label does it too.""" def __init__(self, master=None, cnf={}, **kw): + """Construct a message widget with the parent MASTER. + + Valid option names: anchor, aspect, background, bd, bg, borderwidth, + cursor, fg, font, foreground, highlightbackground, + highlightcolor, highlightthickness, justify, padx, pady, + relief, takefocus, text, textvariable, width.""" Widget.__init__(self, master, 'message', cnf, kw) @@ -3659,12 +3781,13 @@ def __init__(self, master=None, cnf={}, **kw): """Construct a radiobutton widget with the parent MASTER. Valid option names: activebackground, activeforeground, anchor, - background, bd, bg, bitmap, borderwidth, command, cursor, - disabledforeground, fg, font, foreground, height, - highlightbackground, highlightcolor, highlightthickness, image, - indicatoron, justify, padx, pady, relief, selectcolor, selectimage, - state, takefocus, text, textvariable, underline, value, variable, - width, wraplength.""" + background, bd, bg, bitmap, borderwidth, command, compound, + cursor, disabledforeground, fg, font, foreground, height, + highlightbackground, highlightcolor, highlightthickness, + image, indicatoron, justify, offrelief, overrelief, padx, + pady, relief, selectcolor, selectimage, state, takefocus, + text, textvariable, tristateimage, tristatevalue, underline, + value, variable, width, wraplength.""" Widget.__init__(self, master, 'radiobutton', cnf, kw) def deselect(self): @@ -3795,14 +3918,16 @@ def __init__(self, master=None, cnf={}, **kw): WIDGET-SPECIFIC OPTIONS - autoseparators, height, maxundo, - spacing1, spacing2, spacing3, - state, tabs, undo, width, wrap, + autoseparators, blockcursor, endline, + height, inactiveselectbackground, + insertunfocussed, locale (Tk 9.1+), maxundo, + spacing1, spacing2, spacing3, startline, + state, tabs, tabstyle, undo, width, wrap, """ Widget.__init__(self, master, 'text', cnf, kw) - def bbox(self, index): + def bbox(self, index): # overrides Misc.bbox """Return a tuple of (x,y,width,height) which gives the bounding box of the visible part of the character at the given index.""" return self._getints( @@ -3987,7 +4112,7 @@ def image_create(self, index, cnf={}, **kw): self._w, "image", "create", index, *self._options(cnf, kw)) - def image_names(self): + def image_names(self): # overrides Misc.image_names """Return all names of embedded images in this widget.""" return self.tk.call(self._w, "image", "names") @@ -4059,8 +4184,9 @@ def scan_dragto(self, x, y): self.tk.call(self._w, 'scan', 'dragto', x, y) def search(self, pattern, index, stopindex=None, - forwards=None, backwards=None, exact=None, - regexp=None, nocase=None, count=None, elide=None): + forwards=None, backwards=None, exact=None, + regexp=None, nocase=None, count=None, + elide=None, *, nolinestop=None, strictlimits=None): """Search PATTERN beginning from INDEX until STOPINDEX. Return the index of the first character of a match or an empty string.""" @@ -4072,12 +4198,39 @@ def search(self, pattern, index, stopindex=None, if nocase: args.append('-nocase') if elide: args.append('-elide') if count: args.append('-count'); args.append(count) + if nolinestop: args.append('-nolinestop') + if strictlimits: args.append('-strictlimits') if pattern and pattern[0] == '-': args.append('--') args.append(pattern) args.append(index) - if stopindex: args.append(stopindex) + if stopindex is not None: args.append(stopindex) return str(self.tk.call(tuple(args))) + def search_all(self, pattern, index, stopindex=None, *, + forwards=None, backwards=None, exact=None, + regexp=None, nocase=None, count=None, + elide=None, nolinestop=None, overlap=None, + strictlimits=None): + """Search all occurrences of PATTERN from INDEX to STOPINDEX. + Return a tuple of indices where matches begin.""" + args = [self._w, 'search', '-all'] + if forwards: args.append('-forwards') + if backwards: args.append('-backwards') + if exact: args.append('-exact') + if regexp: args.append('-regexp') + if nocase: args.append('-nocase') + if elide: args.append('-elide') + if count: args.append('-count'); args.append(count) + if nolinestop: args.append('-nolinestop') + if overlap: args.append('-overlap') + if strictlimits: args.append('-strictlimits') + if pattern and pattern[0] == '-': args.append('--') + args.append(pattern) + args.append(index) + if stopindex is not None: args.append(stopindex) + result = self.tk.call(tuple(args)) + return self.tk.splitlist(result) + def see(self, index): """Scroll such that the character at INDEX is visible.""" self.tk.call(self._w, 'see', index) @@ -4291,6 +4444,8 @@ def __setitem__(self, key, value): def __getitem__(self, key): return self.tk.call(self.name, 'configure', '-'+key) + __iter__ = None # prevent using __getitem__ for iteration + def configure(self, **kw): """Configure the image.""" res = () @@ -4625,14 +4780,17 @@ def __init__(self, master=None, cnf={}, **kw): buttondownrelief, buttonuprelief, command, disabledbackground, disabledforeground, format, from, - invalidcommand, increment, + invalidcommand, invcmd, increment, + locale (Tk 9.1+), + placeholder (Tk 9.0+), + placeholderforeground (Tk 9.0+), readonlybackground, state, to, - validate, validatecommand values, + validate, validatecommand, vcmd, values, width, wrap, """ Widget.__init__(self, master, 'spinbox', cnf, kw) - def bbox(self, index): + def bbox(self, index): # overrides Misc.bbox """Return a tuple of X1,Y1,X2,Y2 coordinates for a rectangle which encloses the character given by index. @@ -4741,7 +4899,7 @@ def selection_adjust(self, index): """ return self.selection("adjust", index) - def selection_clear(self): + def selection_clear(self): # overrides Misc.selection_clear """Clear the selection If the selection isn't in this widget then the @@ -4816,8 +4974,9 @@ def __init__(self, master=None, cnf={}, **kw): WIDGET-SPECIFIC OPTIONS handlepad, handlesize, opaqueresize, - sashcursor, sashpad, sashrelief, - sashwidth, showhandle, + proxybackground, proxyborderwidth, + proxyrelief, sashcursor, sashpad, + sashrelief, sashwidth, showhandle, """ Widget.__init__(self, master, 'panedwindow', cnf, kw) @@ -4838,7 +4997,7 @@ def remove(self, child): """ self.tk.call(self._w, 'forget', child) - forget = remove + forget = remove # overrides Pack.forget def identify(self, x, y): """Identify the panedwindow component at point x, y diff --git a/Lib/tkinter/colorchooser.py b/Lib/tkinter/colorchooser.py index e2fb69dba92..4085bde1637 100644 --- a/Lib/tkinter/colorchooser.py +++ b/Lib/tkinter/colorchooser.py @@ -1,3 +1,5 @@ +"""Interface to the native Tk color selection dialog.""" + # tk common color chooser dialogue # # this module provides an interface to the native color dialogue diff --git a/Lib/tkinter/commondialog.py b/Lib/tkinter/commondialog.py index 86f5387e001..408cedc345d 100644 --- a/Lib/tkinter/commondialog.py +++ b/Lib/tkinter/commondialog.py @@ -1,3 +1,5 @@ +"""Base class for the Tk common dialogs.""" + # base class for tk common dialogues # # this module provides a base class for accessing the common diff --git a/Lib/tkinter/dialog.py b/Lib/tkinter/dialog.py index 36ae6c277cb..ef005af0d5a 100644 --- a/Lib/tkinter/dialog.py +++ b/Lib/tkinter/dialog.py @@ -1,5 +1,7 @@ # dialog.py -- Tkinter interface to the tk_dialog script. +"""Classic Tk dialog box, wrapping the tk_dialog script.""" + from tkinter import _cnfmerge, Widget, TclError, Button, Pack __all__ = ["Dialog"] @@ -8,6 +10,8 @@ class Dialog(Widget): + """A modal dialog box built from the classic (non-themed) Tk widgets.""" + def __init__(self, master=None, cnf={}, **kw): cnf = _cnfmerge((cnf, kw)) self.widgetName = '__dialog__' @@ -21,7 +25,8 @@ def __init__(self, master=None, cnf={}, **kw): try: Widget.destroy(self) except TclError: pass - def destroy(self): pass + def destroy(self): + """Do nothing; the dialog window is already destroyed.""" def _test(): diff --git a/Lib/tkinter/font.py b/Lib/tkinter/font.py index 3e24e28ef58..2bbdbd56043 100644 --- a/Lib/tkinter/font.py +++ b/Lib/tkinter/font.py @@ -1,3 +1,5 @@ +"""Utilities to help work with fonts in Tkinter.""" + # Tkinter font wrapper # # written by Fredrik Lundh, February 1998 @@ -6,7 +8,6 @@ import itertools import tkinter -__version__ = "0.9" __all__ = ["NORMAL", "ROMAN", "BOLD", "ITALIC", "nametofont", "Font", "families", "names"] @@ -72,8 +73,15 @@ def __init__(self, root=None, font=None, name=None, exists=False, root = tkinter._get_default_root('use font') tk = getattr(root, 'tk', root) if font: - # get actual settings corresponding to the given font - font = tk.splitlist(tk.call("font", "actual", font)) + # start from the settings of the given font + try: + # a named font: copy its options, preserving the size, + # which can be negative (specified in pixels) + font = tk.splitlist(tk.call("font", "configure", font)) + except tkinter.TclError: + # a font description: resolve it ("font configure" only + # accepts a font name); this loses a size in pixels + font = tk.splitlist(tk.call("font", "actual", font)) else: font = self._set(options) if not name: @@ -115,6 +123,8 @@ def __getitem__(self, key): def __setitem__(self, key, value): self.configure(**{key: value}) + __iter__ = None # prevent using __getitem__ for iteration + def __del__(self): try: if self.delete_font: @@ -124,7 +134,7 @@ def __del__(self): def copy(self): "Return a distinct copy of the current font" - return Font(self._tk, **self.actual()) + return Font(self._tk, self.name) def actual(self, option=None, displayof=None): "Return actual font attributes" @@ -198,6 +208,15 @@ def names(root=None): return root.tk.splitlist(root.tk.call("font", "names")) +def __getattr__(name): + if name == "__version__": + from warnings import _deprecated + + _deprecated("__version__", remove=(3, 20)) + return "0.9" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + # -------------------------------------------------------------------- # test stuff diff --git a/Lib/tkinter/messagebox.py b/Lib/tkinter/messagebox.py index 5f0343b660c..f6848142a9a 100644 --- a/Lib/tkinter/messagebox.py +++ b/Lib/tkinter/messagebox.py @@ -1,3 +1,5 @@ +"""Interface to the standard Tk message boxes.""" + # tk common message boxes # # this module provides an interface to the native message boxes @@ -99,24 +101,24 @@ def showerror(title=None, message=None, **options): def askquestion(title=None, message=None, **options): - "Ask a question" + "Ask a question; return the symbolic name of the selected button" return _show(title, message, QUESTION, YESNO, **options) def askokcancel(title=None, message=None, **options): - "Ask if operation should proceed; return true if the answer is ok" + "Ask if operation should proceed; return True if the answer is ok" s = _show(title, message, QUESTION, OKCANCEL, **options) return s == OK def askyesno(title=None, message=None, **options): - "Ask a question; return true if the answer is yes" + "Ask a question; return True if the answer is yes" s = _show(title, message, QUESTION, YESNO, **options) return s == YES def askyesnocancel(title=None, message=None, **options): - "Ask a question; return true if the answer is yes, None if cancelled." + "Ask a question; return True if the answer is yes, None if cancelled" s = _show(title, message, QUESTION, YESNOCANCEL, **options) # s might be a Tcl index object, so convert it to a string s = str(s) @@ -126,7 +128,7 @@ def askyesnocancel(title=None, message=None, **options): def askretrycancel(title=None, message=None, **options): - "Ask if operation should be retried; return true if the answer is yes" + "Ask if operation should be retried; return True if the answer is retry" s = _show(title, message, WARNING, RETRYCANCEL, **options) return s == RETRY diff --git a/Lib/tkinter/simpledialog.py b/Lib/tkinter/simpledialog.py index 6e5b025a9f9..b9710a98ed0 100644 --- a/Lib/tkinter/simpledialog.py +++ b/Lib/tkinter/simpledialog.py @@ -8,7 +8,7 @@ # fredrik@pythonware.com # http://www.pythonware.com # -"""This modules handles dialog boxes. +"""This module handles dialog boxes. It contains the following public symbols: @@ -23,9 +23,12 @@ askstring -- get a string from the user """ -from tkinter import * +from tkinter import Button, Entry, Frame, Label, Message, Tk, Toplevel from tkinter import _get_temp_root, _destroy_temp_root from tkinter import messagebox +from tkinter.constants import ACTIVE, BOTH, END, LEFT, RIDGE, W, E + +__all__ = ["SimpleDialog", "Dialog", "askinteger", "askfloat", "askstring"] class SimpleDialog: diff --git a/Lib/tkinter/ttk.py b/Lib/tkinter/ttk.py index ef2b91dfbb6..4ca8d12dc28 100644 --- a/Lib/tkinter/ttk.py +++ b/Lib/tkinter/ttk.py @@ -12,8 +12,6 @@ of the widgets appearance lies at Themes. """ -__version__ = "0.3.1" - __author__ = "Guilherme Polo " __all__ = ["Button", "Checkbutton", "Combobox", "Entry", "Frame", "Label", @@ -581,8 +579,8 @@ def __init__(self, master=None, **kw): STANDARD OPTIONS - class, compound, cursor, image, state, style, takefocus, - text, textvariable, underline, width + class, compound, cursor, image, justify (Tk 9.0+), padding, + state, style, takefocus, text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS @@ -604,8 +602,8 @@ def __init__(self, master=None, **kw): STANDARD OPTIONS - class, compound, cursor, image, state, style, takefocus, - text, textvariable, underline, width + class, compound, cursor, image, justify (Tk 9.0+), padding, + state, style, takefocus, text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS @@ -638,8 +636,10 @@ def __init__(self, master=None, widget=None, **kw): WIDGET-SPECIFIC OPTIONS - exportselection, invalidcommand, justify, show, state, - textvariable, validate, validatecommand, width + background, exportselection, font, foreground, invalidcommand, + justify, locale (Tk 9.1+), placeholder (Tk 9.0+), + placeholderforeground (Tk 9.0+), show, state, textvariable, + validate, validatecommand, width VALIDATION MODES @@ -648,7 +648,7 @@ def __init__(self, master=None, widget=None, **kw): Widget.__init__(self, master, widget or "ttk::entry", kw) - def bbox(self, index): + def bbox(self, index): # overrides Misc.bbox """Return a tuple of (x, y, width, height) which describes the bounding box of the character given by index.""" return self._getints(self.tk.call(self._w, "bbox", index)) @@ -676,12 +676,14 @@ def __init__(self, master=None, **kw): STANDARD OPTIONS - class, cursor, style, takefocus + class, cursor, style, takefocus, xscrollcommand WIDGET-SPECIFIC OPTIONS - exportselection, justify, height, postcommand, state, - textvariable, values, width + background, exportselection, font, foreground, height, + invalidcommand, justify, locale (Tk 9.1+), placeholder (Tk 9.0+), + placeholderforeground (Tk 9.0+), postcommand, show, state, + textvariable, validate, validatecommand, values, width """ Entry.__init__(self, master, "ttk::combobox", **kw) @@ -730,13 +732,13 @@ def __init__(self, master=None, **kw): STANDARD OPTIONS - class, compound, cursor, image, style, takefocus, text, - textvariable, underline, width + class, compound, cursor, image, state, style, takefocus, + text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS - anchor, background, font, foreground, justify, padding, - relief, text, wraplength + anchor, background, borderwidth, font, foreground, justify, + padding, relief, text, textangle (Tk 9.1+), wraplength """ Widget.__init__(self, master, "ttk::label", kw) @@ -754,8 +756,9 @@ def __init__(self, master=None, **kw): class, cursor, style, takefocus WIDGET-SPECIFIC OPTIONS - labelanchor, text, underline, padding, labelwidget, width, - height + + borderwidth, height, labelanchor, labelwidget, padding, + relief, text, underline, width """ Widget.__init__(self, master, "ttk::labelframe", kw) @@ -771,8 +774,8 @@ def __init__(self, master=None, **kw): STANDARD OPTIONS - class, compound, cursor, image, state, style, takefocus, - text, textvariable, underline, width + class, compound, cursor, image, justify (Tk 9.0+), padding, + state, style, takefocus, text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS @@ -826,7 +829,7 @@ def add(self, child, **kw): self.tk.call(self._w, "add", child, *(_format_optdict(kw))) - def forget(self, tab_id): + def forget(self, tab_id): # overrides Pack.forget """Removes the tab specified by tab_id, unmaps and unmanages the associated window.""" self.tk.call(self._w, "forget", tab_id) @@ -985,7 +988,9 @@ def __init__(self, master=None, **kw): STANDARD OPTIONS - class, cursor, style, takefocus + anchor (Tk 9.0+), class, cursor, font (Tk 9.0+), + foreground (Tk 9.0+), justify (Tk 9.0+), style, takefocus, + text (Tk 9.0+), wraplength (Tk 9.0+) WIDGET-SPECIFIC OPTIONS @@ -1024,8 +1029,8 @@ def __init__(self, master=None, **kw): STANDARD OPTIONS - class, compound, cursor, image, state, style, takefocus, - text, textvariable, underline, width + class, compound, cursor, image, justify (Tk 9.0+), padding, + state, style, takefocus, text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS @@ -1052,7 +1057,7 @@ def __init__(self, master=None, **kw): STANDARD OPTIONS - class, cursor, style, takefocus + class, cursor, state, style, takefocus WIDGET-SPECIFIC OPTIONS @@ -1149,7 +1154,10 @@ def __init__(self, master=None, **kw): WIDGET-SPECIFIC OPTIONS - to, from_, increment, values, wrap, format, command + background, command, exportselection, font, foreground, + format, from_, increment, justify, locale (Tk 9.1+), + placeholder (Tk 9.0+), placeholderforeground (Tk 9.0+), show, + state, textvariable, to, values, width, wrap """ Entry.__init__(self, master, "ttk::spinbox", **kw) @@ -1176,7 +1184,10 @@ def __init__(self, master=None, **kw): WIDGET-SPECIFIC OPTIONS - columns, displaycolumns, height, padding, selectmode, show + columns, displaycolumns, headingheight (Tk 9.1+), height, + padding, rowheight (Tk 9.1+), selectmode, selecttype (Tk 9.0+), + show, striped (Tk 9.0+), titlecolumns (Tk 9.0+), + titleitems (Tk 9.0+) ITEM OPTIONS @@ -1189,7 +1200,7 @@ def __init__(self, master=None, **kw): Widget.__init__(self, master, "ttk::treeview", kw) - def bbox(self, item, column=None): + def bbox(self, item, column=None): # overrides Misc.bbox """Returns the bounding box (relative to the treeview widget's window) of the specified item in the form x y width height. @@ -1248,7 +1259,7 @@ def exists(self, item): return self.tk.getboolean(self.tk.call(self._w, "exists", item)) - def focus(self, item=None): + def focus(self, item=None): # overrides Misc.focus """If item is specified, sets the focus item to item. Otherwise, returns the current focus item, or '' if there is none.""" return self.tk.call(self._w, "focus", item) @@ -1472,8 +1483,8 @@ def tag_configure(self, tagname, option=None, **kw): def tag_has(self, tagname, item=None): - """If item is specified, returns 1 or 0 depending on whether the - specified item has the given tagname. Otherwise, returns a list of + """If item is specified, returns True if the specified item has the + given tagname, False otherwise. Otherwise, returns a list of all items which have the specified tag. * Availability: Tk 8.6""" @@ -1648,3 +1659,12 @@ def destroy(self): except AttributeError: pass super().destroy() + + +def __getattr__(name): + if name == "__version__": + from warnings import _deprecated + + _deprecated("__version__", remove=(3, 20)) + return "0.3.1" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/tomllib/_parser.py b/Lib/tomllib/_parser.py index 84e70495dd0..b8993480800 100644 --- a/Lib/tomllib/_parser.py +++ b/Lib/tomllib/_parser.py @@ -4,8 +4,11 @@ from __future__ import annotations +# Defer loading regular expressions until we actually need them in +# parse_value(). +__lazy_modules__ = ["tomllib._re"] + import sys -from types import MappingProxyType from ._re import ( RE_DATETIME, @@ -16,6 +19,9 @@ match_to_number, ) +if sys.version_info < (3, 15): + from types import MappingProxyType as frozendict + TYPE_CHECKING = False if TYPE_CHECKING: from collections.abc import Iterable @@ -30,35 +36,36 @@ # and RecursionError. MAX_KEY_PARTS: Final = sys.getrecursionlimit() -ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127)) +ASCII_CTRL: Final = frozenset(chr(i) for i in range(32)) | frozenset(chr(127)) # Neither of these sets include quotation mark or backslash. They are # currently handled as separate cases in the parser functions. -ILLEGAL_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t") -ILLEGAL_MULTILINE_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t\n") +ILLEGAL_BASIC_STR_CHARS: Final = ASCII_CTRL - frozenset("\t") +ILLEGAL_MULTILINE_BASIC_STR_CHARS: Final = ASCII_CTRL - frozenset("\t\n") -ILLEGAL_LITERAL_STR_CHARS = ILLEGAL_BASIC_STR_CHARS -ILLEGAL_MULTILINE_LITERAL_STR_CHARS = ILLEGAL_MULTILINE_BASIC_STR_CHARS +ILLEGAL_LITERAL_STR_CHARS: Final = ILLEGAL_BASIC_STR_CHARS +ILLEGAL_MULTILINE_LITERAL_STR_CHARS: Final = ILLEGAL_MULTILINE_BASIC_STR_CHARS -ILLEGAL_COMMENT_CHARS = ILLEGAL_BASIC_STR_CHARS +ILLEGAL_COMMENT_CHARS: Final = ILLEGAL_BASIC_STR_CHARS -TOML_WS = frozenset(" \t") -TOML_WS_AND_NEWLINE = TOML_WS | frozenset("\n") -BARE_KEY_CHARS = frozenset( +TOML_WS: Final = frozenset(" \t") +TOML_WS_AND_NEWLINE: Final = TOML_WS | frozenset("\n") +BARE_KEY_CHARS: Final = frozenset( "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" "-_" ) -KEY_INITIAL_CHARS = BARE_KEY_CHARS | frozenset("\"'") -HEXDIGIT_CHARS = frozenset("abcdef" "ABCDEF" "0123456789") +KEY_INITIAL_CHARS: Final = BARE_KEY_CHARS | frozenset("\"'") +HEXDIGIT_CHARS: Final = frozenset("abcdef" "ABCDEF" "0123456789") -BASIC_STR_ESCAPE_REPLACEMENTS = MappingProxyType( +BASIC_STR_ESCAPE_REPLACEMENTS: Final = frozendict( { "\\b": "\u0008", # backspace "\\t": "\u0009", # tab - "\\n": "\u000A", # linefeed - "\\f": "\u000C", # form feed - "\\r": "\u000D", # carriage return + "\\n": "\u000a", # linefeed + "\\f": "\u000c", # form feed + "\\r": "\u000d", # carriage return + "\\e": "\u001b", # escape '\\"': "\u0022", # quote - "\\\\": "\u005C", # backslash + "\\\\": "\u005c", # backslash } ) @@ -141,7 +148,7 @@ def load(fp: IO[bytes], /, *, parse_float: ParseFloat = float) -> dict[str, Any] return loads(s, parse_float=parse_float) -def loads(s: str, /, *, parse_float: ParseFloat = float) -> dict[str, Any]: # noqa: C901 +def loads(s: str, /, *, parse_float: ParseFloat = float) -> dict[str, Any]: """Parse TOML from a string.""" # The spec allows converting "\r\n" to "\n", even in string @@ -216,10 +223,10 @@ class Flags: """Flags that map to parsed keys/namespaces.""" # Marks an immutable namespace (inline array or inline table). - FROZEN = 0 + FROZEN: Final = 0 # Marks a nest that has been explicitly created and can no longer # be opened using the "[table]" syntax. - EXPLICIT_NEST = 1 + EXPLICIT_NEST: Final = 1 def __init__(self) -> None: self._flags: dict[str, dict[Any, Any]] = {} @@ -265,8 +272,8 @@ def is_(self, key: Key, flag: int) -> bool: cont = inner_cont["nested"] key_stem = key[-1] if key_stem in cont: - cont = cont[key_stem] - return flag in cont["flags"] or flag in cont["recursive_flags"] + inner_cont = cont[key_stem] + return flag in inner_cont["flags"] or flag in inner_cont["recursive_flags"] return False @@ -527,7 +534,7 @@ def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos nested_dict = NestedDict() flags = Flags() - pos = skip_chars(src, pos, TOML_WS) + pos = skip_comments_and_array_ws(src, pos) if src.startswith("}", pos): return pos + 1, nested_dict.dict while True: @@ -542,16 +549,18 @@ def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos if key_stem in nest: raise TOMLDecodeError(f"Duplicate inline table key {key_stem!r}", src, pos) nest[key_stem] = value - pos = skip_chars(src, pos, TOML_WS) + pos = skip_comments_and_array_ws(src, pos) c = src[pos : pos + 1] if c == "}": return pos + 1, nested_dict.dict if c != ",": raise TOMLDecodeError("Unclosed inline table", src, pos) + pos += 1 + pos = skip_comments_and_array_ws(src, pos) + if src.startswith("}", pos): + return pos + 1, nested_dict.dict if isinstance(value, (dict, list)): flags.set(key, Flags.FROZEN, recursive=True) - pos += 1 - pos = skip_chars(src, pos, TOML_WS) def parse_basic_str_escape( @@ -573,6 +582,8 @@ def parse_basic_str_escape( pos += 1 pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE) return pos, "" + if escape_id == "\\x": + return parse_hex_char(src, pos, 2) if escape_id == "\\u": return parse_hex_char(src, pos, 4) if escape_id == "\\U": @@ -672,7 +683,7 @@ def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> tuple[Pos, str]: pos += 1 -def parse_value( # noqa: C901 +def parse_value( src: str, pos: Pos, parse_float: ParseFloat ) -> tuple[Pos, Any]: try: diff --git a/Lib/tomllib/_re.py b/Lib/tomllib/_re.py index eb8beb19747..fc374ed63d3 100644 --- a/Lib/tomllib/_re.py +++ b/Lib/tomllib/_re.py @@ -10,16 +10,20 @@ TYPE_CHECKING = False if TYPE_CHECKING: - from typing import Any + from typing import Any, Final from ._types import ParseFloat -# E.g. -# - 00:32:00.999999 -# - 00:32:00 -_TIME_RE_STR = r"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?" +_TIME_RE_STR: Final = r""" +([01][0-9]|2[0-3]) # hours +:([0-5][0-9]) # minutes +(?: + :([0-5][0-9]) # optional seconds + (?:\.([0-9]{1,6})[0-9]*)? # optional fractions of a second +)? +""" -RE_NUMBER = re.compile( +RE_NUMBER: Final = re.compile( r""" 0 (?: @@ -38,8 +42,8 @@ """, flags=re.VERBOSE, ) -RE_LOCALTIME = re.compile(_TIME_RE_STR) -RE_DATETIME = re.compile( +RE_LOCALTIME: Final = re.compile(_TIME_RE_STR, flags=re.VERBOSE) +RE_DATETIME: Final = re.compile( rf""" ([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27 (?: @@ -74,7 +78,8 @@ def match_to_datetime(match: re.Match[str]) -> datetime | date: year, month, day = int(year_str), int(month_str), int(day_str) if hour_str is None: return date(year, month, day) - hour, minute, sec = int(hour_str), int(minute_str), int(sec_str) + hour, minute = int(hour_str), int(minute_str) + sec = int(sec_str) if sec_str else 0 micros = int(micros_str.ljust(6, "0")) if micros_str else 0 if offset_sign_str: tz: tzinfo | None = cached_tz( @@ -103,8 +108,9 @@ def cached_tz(hour_str: str, minute_str: str, sign_str: str) -> timezone: def match_to_localtime(match: re.Match[str]) -> time: hour_str, minute_str, sec_str, micros_str = match.groups() + sec = int(sec_str) if sec_str else 0 micros = int(micros_str.ljust(6, "0")) if micros_str else 0 - return time(int(hour_str), int(minute_str), int(sec_str), micros) + return time(int(hour_str), int(minute_str), sec, micros) def match_to_number(match: re.Match[str], parse_float: ParseFloat) -> Any: diff --git a/Lib/trace.py b/Lib/trace.py index cf8817f4383..43ec201c469 100644 --- a/Lib/trace.py +++ b/Lib/trace.py @@ -604,7 +604,7 @@ def results(self): def main(): import argparse - parser = argparse.ArgumentParser(color=True) + parser = argparse.ArgumentParser() parser.add_argument('--version', action='version', version='trace 2.0') grp = parser.add_argument_group('Main options', @@ -612,27 +612,27 @@ def main(): grp.add_argument('-c', '--count', action='store_true', help='Count the number of times each line is executed and write ' - 'the counts to .cover for each module executed, in ' - 'the module\'s directory. See also --coverdir, --file, ' - '--no-report below.') + 'the counts to `.cover` for each module executed, in ' + 'the module\'s directory. See also `--coverdir`, `--file`, ' + '`--no-report` below.') grp.add_argument('-t', '--trace', action='store_true', - help='Print each line to sys.stdout before it is executed') + help='Print each line to `sys.stdout` before it is executed') grp.add_argument('-l', '--listfuncs', action='store_true', help='Keep track of which functions are executed at least once ' - 'and write the results to sys.stdout after the program exits. ' - 'Cannot be specified alongside --trace or --count.') + 'and write the results to `sys.stdout` after the program exits. ' + 'Cannot be specified alongside `--trace` or `--count`.') grp.add_argument('-T', '--trackcalls', action='store_true', help='Keep track of caller/called pairs and write the results to ' - 'sys.stdout after the program exits.') + '`sys.stdout` after the program exits.') grp = parser.add_argument_group('Modifiers') _grp = grp.add_mutually_exclusive_group() _grp.add_argument('-r', '--report', action='store_true', help='Generate a report from a counts file; does not execute any ' - 'code. --file must specify the results file to read, which ' - 'must have been created in a previous run with --count ' - '--file=FILE') + 'code. `--file` must specify the results file to read, which ' + 'must have been created in a previous run with `--count` ' + '`--file=FILE`') _grp.add_argument('-R', '--no-report', action='store_true', help='Do not generate the coverage report files. ' 'Useful if you want to accumulate over several runs.') @@ -641,14 +641,14 @@ def main(): help='File to accumulate counts over several runs') grp.add_argument('-C', '--coverdir', help='Directory where the report files go. The coverage report ' - 'for . will be written to file ' - '//.cover') + 'for `.` will be written to file ' + '`//.cover`') grp.add_argument('-m', '--missing', action='store_true', help='Annotate executable lines that were not executed with ' '">>>>>> "') grp.add_argument('-s', '--summary', action='store_true', - help='Write a brief summary for each file to sys.stdout. ' - 'Can only be used with --count or --report') + help='Write a brief summary for each file to `sys.stdout`. ' + 'Can only be used with `--count` or `--report`') grp.add_argument('-g', '--timing', action='store_true', help='Prefix each line with the time since the program started. ' 'Only used while tracing') @@ -661,7 +661,7 @@ def main(): 'module names.') grp.add_argument('--ignore-dir', action='append', default=[], help='Ignore files in the given directory ' - '(multiple directories can be joined by os.pathsep).') + '(multiple directories can be joined by `os.pathsep`).') parser.add_argument('--module', action='store_true', default=False, help='Trace a module. ') @@ -721,7 +721,6 @@ def parse_ignore_dir(s): '__package__': mod_spec.parent, '__loader__': mod_spec.loader, '__spec__': mod_spec, - '__cached__': None, } else: sys.argv = [opts.progname, *opts.arguments] @@ -734,7 +733,6 @@ def parse_ignore_dir(s): '__file__': opts.progname, '__name__': '__main__', '__package__': None, - '__cached__': None, } t.runctx(code, globs, globs) except OSError as err: diff --git a/Lib/turtle.py b/Lib/turtle.py index e5ce2c0a03c..73c3e8bd116 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -22,80 +22,17 @@ # 3. This notice may not be removed or altered from any source distribution. """ -Turtle graphics is a popular way for introducing programming to -kids. It was part of the original Logo programming language developed -by Wally Feurzig and Seymour Papert in 1966. - -Imagine a robotic turtle starting at (0, 0) in the x-y plane. After an ``import turtle``, give it -the command turtle.forward(15), and it moves (on-screen!) 15 pixels in -the direction it is facing, drawing a line as it moves. Give it the -command turtle.right(25), and it rotates in-place 25 degrees clockwise. - -By combining together these and similar commands, intricate shapes and -pictures can easily be drawn. - ------ turtle.py - -This module is an extended reimplementation of turtle.py from the -Python standard distribution up to Python 2.5. (See: https://www.python.org) - -It tries to keep the merits of turtle.py and to be (nearly) 100% -compatible with it. This means in the first place to enable the -learning programmer to use all the commands, classes and methods -interactively when using the module from within IDLE run with -the -n switch. - -Roughly it has the following features added: - -- Better animation of the turtle movements, especially of turning the - turtle. So the turtles can more easily be used as a visual feedback - instrument by the (beginning) programmer. - -- Different turtle shapes, image files as turtle shapes, user defined - and user controllable turtle shapes, among them compound - (multicolored) shapes. Turtle shapes can be stretched and tilted, which - makes turtles very versatile geometrical objects. - -- Fine control over turtle movement and screen updates via delay(), - and enhanced tracer() and speed() methods. - -- Aliases for the most commonly used commands, like fd for forward etc., - following the early Logo traditions. This reduces the boring work of - typing long sequences of commands, which often occur in a natural way - when kids try to program fancy pictures on their first encounter with - turtle graphics. - -- Turtles now have an undo()-method with configurable undo-buffer. - -- Some simple commands/methods for creating event driven programs - (mouse-, key-, timer-events). Especially useful for programming games. - -- A scrollable Canvas class. The default scrollable Canvas can be - extended interactively as needed while playing around with the turtle(s). - -- A TurtleScreen class with methods controlling background color or - background image, window and canvas size and other properties of the - TurtleScreen. - -- There is a method, setworldcoordinates(), to install a user defined - coordinate-system for the TurtleScreen. - -- The implementation uses a 2-vector class named Vec2D, derived from tuple. - This class is public, so it can be imported by the application programmer, - which makes certain types of computations very natural and compact. - -- Appearance of the TurtleScreen and the Turtles at startup/import can be - configured by means of a turtle.cfg configuration file. - The default configuration mimics the appearance of the old turtle module. - -- If configured appropriately the module reads in docstrings from a docstring - dictionary in some different language, supplied separately and replaces - the English ones by those read in. There is a utility function - write_docstringdict() to write a dictionary with the original (English) - docstrings to disc, so it can serve as a template for translations. - -Behind the scenes there are some features included with possible -extensions in mind. These will be commented and documented elsewhere. +Imagine a robotic turtle starting at (0, 0) in the x-y plane. +After an `import turtle`, give it the command `turtle.forward(15)`, and it +moves (on-screen!) 15 pixels in the direction it is facing, drawing a line as +it moves. Give it the command `turtle.right(25)`, and it rotates in-place 25 +degrees clockwise. + +Turtle graphics is an implementation of the drawing tools introduced in Logo +in 1967. It was created as an educational tool, and its instant, visible +feedback makes it an effective way for learners to encounter programming +concepts. It is also a convenient way to produce simple graphical output +without bringing in external libraries. """ import tkinter as TK @@ -166,7 +103,7 @@ } def config_dict(filename): - """Convert content of config-file into dictionary.""" + """Convert the content of a config file into a dict.""" with open(filename, "r") as f: cfglines = f.readlines() cfgdict = {} @@ -195,17 +132,17 @@ def config_dict(filename): return cfgdict def readconfig(cfgdict): - """Read config-files, change configuration-dict accordingly. + """Read config files and update the configuration dict accordingly. If there is a turtle.cfg file in the current working directory, - read it from there. If this contains an importconfig-value, - say 'myway', construct filename turtle_mayway.cfg else use - turtle.cfg and read it from the import-directory, where - turtle.py is located. - Update configuration dictionary first according to config-file, - in the import directory, then according to config-file in the - current working directory. - If no config-file is found, the default configuration is used. + read it. If it contains an importconfig value, say 'myway', + use the filename turtle_myway.cfg, otherwise turtle.cfg, and + read that file from the directory where turtle.py is located. + + The configuration dict is updated first from the config file in + the turtle.py directory, then from the one in the current working + directory. If no config file is found, the default configuration + is used. """ default_cfg = "turtle.cfg" cfgdict1 = {} @@ -231,46 +168,54 @@ def readconfig(cfgdict): class Vec2D(tuple): - """A 2 dimensional vector class, used as a helper class - for implementing turtle graphics. - May be useful for turtle graphics programs also. - Derived from tuple, so a vector is a tuple! + """A two-dimensional vector class, used as a helper class for + implementing turtle graphics. May be useful for turtle graphics + programs too. Derived from tuple, so a vector is a tuple! Provides (for a, b vectors, k number): - a+b vector addition - a-b vector subtraction - a*b inner product - k*a and a*k multiplication with scalar - |a| absolute value of a - a.rotate(angle) rotation + a + b: vector addition + a - b: vector subtraction + a * b: inner product + k * a and a * k: multiplication with scalar + abs(a): absolute value of a + a.rotate(angle): rotation """ + def __new__(cls, x, y): return tuple.__new__(cls, (x, y)) + def __add__(self, other): return Vec2D(self[0]+other[0], self[1]+other[1]) + def __mul__(self, other): if isinstance(other, Vec2D): return self[0]*other[0]+self[1]*other[1] return Vec2D(self[0]*other, self[1]*other) + def __rmul__(self, other): if isinstance(other, int) or isinstance(other, float): return Vec2D(self[0]*other, self[1]*other) return NotImplemented + def __sub__(self, other): return Vec2D(self[0]-other[0], self[1]-other[1]) + def __neg__(self): return Vec2D(-self[0], -self[1]) + def __abs__(self): return math.hypot(*self) + def rotate(self, angle): - """rotate self counterclockwise by angle - """ + """Rotate self counterclockwise by angle.""" perp = Vec2D(-self[1], self[0]) angle = math.radians(angle) c, s = math.cos(angle), math.sin(angle) return Vec2D(self[0]*c+perp[0]*s, self[1]*c+perp[1]*s) + def __getnewargs__(self): return (self[0], self[1]) + def __repr__(self): return "(%.2f,%.2f)" % self @@ -284,7 +229,7 @@ def __repr__(self): ## to ScrolledCanvas class def __methodDict(cls, _dict): - """helper function for Scrolled Canvas""" + """Helper function for Scrolled Canvas.""" baseList = list(cls.__bases__) baseList.reverse() for _super in baseList: @@ -294,7 +239,7 @@ def __methodDict(cls, _dict): _dict[key] = value def __methods(cls): - """helper function for Scrolled Canvas""" + """Helper function for Scrolled Canvas.""" _dict = {} __methodDict(cls, _dict) return _dict.keys() @@ -356,7 +301,7 @@ def __init__(self, master, width=500, height=350, self._rootwindow.bind('', self.onResize) def reset(self, canvwidth=None, canvheight=None, bg = None): - """Adjust canvas and scrollbars according to given canvas size.""" + """Adjust canvas size, background color and scrollbars.""" if canvwidth: self.canvwidth = canvwidth if canvheight: @@ -374,8 +319,7 @@ def reset(self, canvwidth=None, canvheight=None, bg = None): def adjustScrolls(self): - """ Adjust scrollbars according to window- and canvas-size. - """ + """Adjust scrollbars according to window and canvas size.""" cwidth = self._canvas.winfo_width() cheight = self._canvas.winfo_height() self._canvas.xview_moveto(0.5*(self.canvwidth-cwidth)/self.canvwidth) @@ -453,7 +397,7 @@ def win_height(self): Canvas = TK.Canvas -class TurtleScreenBase(object): +class TurtleScreenBase: """Provide the basic graphics functionality. Interface between Tkinter and turtle.py. @@ -462,14 +406,13 @@ class TurtleScreenBase(object): """ def _blankimage(self): - """return a blank image object - """ + """Return a blank image object.""" img = TK.PhotoImage(width=1, height=1, master=self.cv) img.blank() return img def _image(self, filename): - """return an image object containing the + """Return an image object containing the imagedata from an image file named filename. """ return TK.PhotoImage(file=filename, master=self.cv) @@ -486,22 +429,24 @@ def __init__(self, cv): self.canvwidth = w self.canvheight = h self.xscale = self.yscale = 1.0 + self._updating = False def _createpoly(self): - """Create an invisible polygon item on canvas self.cv) - """ + """Create an invisible polygon item on canvas self.cv.""" return self.cv.create_polygon((0, 0, 0, 0, 0, 0), fill="", outline="") def _drawpoly(self, polyitem, coordlist, fill=None, outline=None, width=None, top=False): - """Configure polygonitem polyitem according to provided - arguments: - coordlist is sequence of coordinates - fill is filling color - outline is outline color - top is a boolean value, which specifies if polyitem - will be put on top of the canvas' displaylist so it - will not be covered by other items. + """Configure polygon item polyitem according to the given arguments. + + Arguments: + coordlist -- a sequence of coordinates + fill -- the fill color + outline -- the outline color + width -- the outline width + top -- a boolean value, which specifies if polyitem will be + put on top of the canvas' display list so it will not + be covered by other items """ cl = [] for x, y in coordlist: @@ -518,20 +463,21 @@ def _drawpoly(self, polyitem, coordlist, fill=None, self.cv.tag_raise(polyitem) def _createline(self): - """Create an invisible line item on canvas self.cv) - """ + """Create an invisible line item on canvas self.cv.""" return self.cv.create_line(0, 0, 0, 0, fill="", width=2, capstyle = TK.ROUND) def _drawline(self, lineitem, coordlist=None, fill=None, width=None, top=False): - """Configure lineitem according to provided arguments: - coordlist is sequence of coordinates - fill is drawing color - width is width of drawn line. - top is a boolean value, which specifies if polyitem - will be put on top of the canvas' displaylist so it - will not be covered by other items. + """Configure line item lineitem according to the given arguments. + + Arguments: + coordlist -- a sequence of coordinates + fill -- the drawing color + width -- the width of the drawn line + top -- a boolean value, which specifies if lineitem will be + put on top of the canvas' display list so it will not + be covered by other items """ if coordlist is not None: cl = [] @@ -548,14 +494,23 @@ def _drawline(self, lineitem, coordlist=None, def _delete(self, item): """Delete graphics item from canvas. - If item is"all" delete all graphics items. + + If item is "all", delete all graphics items. """ self.cv.delete(item) def _update(self): - """Redraw graphics items on canvas - """ - self.cv.update() + """Redraw graphics items on canvas.""" + if self._updating: + # Reentrant call (e.g. a drag handler moving the turtle, + # gh-50966): flush drawing without reprocessing input. + self.cv.update_idletasks() + return + self._updating = True + try: + self.cv.update() + finally: + self._updating = False def _delay(self, delay): """Delay subsequent canvas actions for delay ms.""" @@ -565,7 +520,7 @@ def _iscolorstring(self, color): """Check if the string color is a legal Tkinter color string. """ try: - rgb = self.cv.winfo_rgb(color) + self.cv.winfo_rgb(color) ok = True except TK.TclError: ok = False @@ -581,10 +536,11 @@ def _bgcolor(self, color=None): return self.cv.cget("bg") def _write(self, pos, txt, align, font, pencolor): - """Write txt at pos in canvas with specified font - and color. - Return text item and x-coord of right bottom corner - of text's bounding box.""" + """Write txt at pos on the canvas with specified font and color. + + Return text item and x-coord of right bottom corner of text's + bounding box. + """ x, y = pos x = x * self.xscale y = y * self.yscale @@ -737,13 +693,13 @@ def _type(self, item): return self.cv.type(item) def _pointlist(self, item): - """returns list of coordinate-pairs of points of item - Example (for insiders): - >>> from turtle import * + """Return list of coordinate pairs of points of item. + + For example: >>> getscreen()._pointlist(getturtle().turtle._item) [(0.0, 9.9999999999999982), (0.0, -9.9999999999999982), (9.9999999999999982, 0.0)] - >>> """ + """ cl = self.cv.coords(item) pl = [(cl[i], -cl[i+1]) for i in range(0, len(cl), 2)] return pl @@ -764,8 +720,9 @@ def _rescale(self, xscalefactor, yscalefactor): self.cv.coords(item, *newcoordlist) def _resize(self, canvwidth=None, canvheight=None, bg=None): - """Resize the canvas the turtles are drawing on. Does - not alter the drawing window. + """Resize the canvas the turtles are drawing on. + + Does not alter the drawing window. """ # needs amendment if not isinstance(self.cv, ScrolledCanvas): @@ -779,8 +736,7 @@ def _resize(self, canvwidth=None, canvheight=None, bg=None): self.cv.reset(canvwidth, canvheight, bg) def _window_size(self): - """ Return the width and height of the turtle window. - """ + """Return the width and height of the turtle window.""" width = self.cv.winfo_width() if width <= 1: # the window isn't managed by a geometry manager width = self.cv['width'] @@ -794,7 +750,7 @@ def mainloop(self): No argument. - Must be last statement in a turtle graphics program. + Must be the last statement in a turtle graphics program. Must NOT be used if a script is run from within IDLE in -n mode (No subprocess) - for interactive use of turtle graphics. @@ -857,11 +813,10 @@ class Terminator (Exception): class TurtleGraphicsError(Exception): - """Some TurtleGraphics Error - """ + """Raised for invalid arguments or operations.""" -class Shape(object): +class Shape: """Data structure modeling shapes. attribute _type is one of "polygon", "image", "compound" @@ -905,13 +860,15 @@ def addcomponent(self, poly, fill, outline=None): self._data.append([poly, fill, outline]) -class Tbuffer(object): - """Ring buffer used as undobuffer for RawTurtle objects.""" +class Tbuffer: + """Ring buffer used as undo buffer for RawTurtle objects.""" + def __init__(self, bufsize=10): self.bufsize = bufsize self.buffer = [[None]] * bufsize self.ptr = -1 self.cumulate = False + def reset(self, bufsize=None): if bufsize is None: for i in range(self.bufsize): @@ -920,6 +877,7 @@ def reset(self, bufsize=None): self.bufsize = bufsize self.buffer = [[None]] * bufsize self.ptr = -1 + def push(self, item): if self.bufsize > 0: if not self.cumulate: @@ -927,6 +885,7 @@ def push(self, item): self.buffer[self.ptr] = item else: self.buffer[self.ptr].append(item) + def pop(self): if self.bufsize > 0: item = self.buffer[self.ptr] @@ -936,8 +895,10 @@ def pop(self): self.buffer[self.ptr] = [None] self.ptr = (self.ptr - 1) % self.bufsize return (item) + def nr_of_items(self): return self.bufsize - self.buffer.count([None]) + def __repr__(self): return str(self.buffer) + " " + str(self.ptr) @@ -946,9 +907,9 @@ def __repr__(self): class TurtleScreen(TurtleScreenBase): """Provides screen oriented methods like bgcolor etc. - Only relies upon the methods of TurtleScreenBase and NOT - upon components of the underlying graphics toolkit - - which is Tkinter in this case. + Only relies upon the methods of TurtleScreenBase and not upon + components of the underlying graphics toolkit, which is Tkinter + in this case. """ _RUNNING = True @@ -989,8 +950,8 @@ def __init__(self, cv, mode=_CFG["mode"], # the Turtle window will show behind the Terminal window when you # start the demo from the command line. rootwindow = cv.winfo_toplevel() - rootwindow.call('wm', 'attributes', '.', '-topmost', '1') - rootwindow.call('wm', 'attributes', '.', '-topmost', '0') + rootwindow.wm_attributes(topmost=True) + rootwindow.wm_attributes(topmost=False) def clear(self): """Delete all drawings and all turtles from the TurtleScreen. @@ -1279,7 +1240,7 @@ def tracer(self, n=None, delay=None): self.update() def delay(self, delay=None): - """ Return or set the drawing delay in milliseconds. + """Return or set the drawing delay in milliseconds. Optional argument: delay -- positive integer @@ -1323,8 +1284,7 @@ def _incrementudc(self): self._updatecounter %= self._tracing def update(self): - """Perform a TurtleScreen update. - """ + """Perform a TurtleScreen update.""" tracing = self._tracing self._tracing = True for t in self.turtles(): @@ -1334,7 +1294,7 @@ def update(self): self._update() def window_width(self): - """ Return the width of the turtle window. + """Return the width of the turtle window. Example (for a TurtleScreen instance named screen): >>> screen.window_width() @@ -1343,7 +1303,7 @@ def window_width(self): return self._window_size()[0] def window_height(self): - """ Return the height of the turtle window. + """Return the height of the turtle window. Example (for a TurtleScreen instance named screen): >>> screen.window_height() @@ -1568,14 +1528,16 @@ def save(self, filename, *, overwrite=False): addshape = register_shape onkeyrelease = onkey -class TNavigator(object): - """Navigation part of the RawTurtle. +class TNavigator: + """The navigation part of RawTurtle. + Implements methods for turtle movement. """ START_ORIENTATION = { "standard": Vec2D(1.0, 0.0), "world" : Vec2D(1.0, 0.0), - "logo" : Vec2D(0.0, 1.0) } + "logo" : Vec2D(0.0, 1.0), + } DEFAULT_MODE = "standard" DEFAULT_ANGLEOFFSET = 0 DEFAULT_ANGLEORIENT = 1 @@ -1591,16 +1553,15 @@ def __init__(self, mode=DEFAULT_MODE): TNavigator.reset(self) def reset(self): - """reset turtle to its initial values + """Reset turtle to its initial values. - Will be overwritten by parent class + Will be overwritten by the parent class. """ self._position = Vec2D(0.0, 0.0) self._orient = TNavigator.START_ORIENTATION[self._mode] def _setmode(self, mode=None): - """Set turtle-mode to 'standard', 'world' or 'logo'. - """ + """Set the turtle's mode to 'standard', 'world' or 'logo'.""" if mode is None: return self._mode if mode not in ["standard", "logo", "world"]: @@ -1614,7 +1575,7 @@ def _setmode(self, mode=None): self._angleOrient = -1 def _setDegreesPerAU(self, fullcircle): - """Helper function for degrees() and radians()""" + """Helper function for degrees() and radians().""" self._fullcircle = fullcircle self._degreesPerAU = 360/fullcircle if self._mode == "standard": @@ -1623,55 +1584,59 @@ def _setDegreesPerAU(self, fullcircle): self._angleOffset = fullcircle/4. def degrees(self, fullcircle=360.0): - """ Set angle measurement units to degrees. + """Set the angle measurement units to degrees. - Optional argument: - fullcircle - a number + The number of degrees in a full circle is set to fullcircle, which + defaults to 360. - Set angle measurement units, i. e. set number - of 'degrees' for a full circle. Default value is - 360 degrees. + Argument: + fullcircle -- a number Example (for a Turtle instance named turtle): - >>> turtle.left(90) - >>> turtle.heading() - 90 - - Change angle measurement unit to grad (also known as gon, - grade, or gradian and equals 1/100-th of the right angle.) - >>> turtle.degrees(400.0) - >>> turtle.heading() - 100 - + >>> turtle.home() + >>> turtle.left(90) + >>> turtle.heading() + 90.0 + + >>> # Change angle measurement unit to grad (also known as gon, + >>> # grade, or gradian and equals 1/100-th of the right angle.) + >>> turtle.degrees(400.0) + >>> turtle.heading() + 100.0 + >>> turtle.degrees(360) + >>> turtle.heading() + 90.0 """ self._setDegreesPerAU(fullcircle) def radians(self): - """ Set the angle measurement units to radians. + """Set the angle measurement units to radians. - No arguments. + Equivalent to degrees(2 * math.pi). Example (for a Turtle instance named turtle): - >>> turtle.heading() - 90 - >>> turtle.radians() - >>> turtle.heading() - 1.5707963267948966 + >>> turtle.home() + >>> turtle.left(90) + >>> turtle.heading() + 90.0 + >>> turtle.radians() + >>> turtle.heading() + 1.5707963267948966 """ self._setDegreesPerAU(math.tau) def _go(self, distance): - """move turtle forward by specified distance""" + """Move the turtle forward by the specified distance.""" ende = self._position + self._orient * distance self._goto(ende) def _rotate(self, angle): - """Turn turtle counterclockwise by specified angle if angle > 0.""" + """Turn the turtle counterclockwise by specified angle, if the angle > 0.""" angle *= self._degreesPerAU self._orient = self._orient.rotate(angle) def _goto(self, end): - """move turtle to position end.""" + """Move the turtle to the end position.""" self._position = end def teleport(self, x=None, y=None, *, fill_gap: bool = False) -> None: @@ -1686,21 +1651,20 @@ def forward(self, distance): Aliases: forward | fd - Argument: - distance -- a number (integer or float) + The turtle moves in the direction it is headed. - Move the turtle forward by the specified distance, in the direction - the turtle is headed. + Argument: + distance -- a number Example (for a Turtle instance named turtle): - >>> turtle.position() - (0.00,0.00) - >>> turtle.forward(25) - >>> turtle.position() - (25.00,0.00) - >>> turtle.forward(-75) - >>> turtle.position() - (-50.00,0.00) + >>> turtle.position() + (0.00,0.00) + >>> turtle.forward(25) + >>> turtle.position() + (25.00,0.00) + >>> turtle.forward(-75) + >>> turtle.position() + (-50.00,0.00) """ self._go(distance) @@ -1709,134 +1673,125 @@ def back(self, distance): Aliases: back | backward | bk - Argument: - distance -- a number + The turtle moves opposite to the direction it is headed. + The turtle's heading does not change. - Move the turtle backward by distance, opposite to the direction the - turtle is headed. Do not change the turtle's heading. + Argument: + distance -- a number Example (for a Turtle instance named turtle): - >>> turtle.position() - (0.00,0.00) - >>> turtle.backward(30) - >>> turtle.position() - (-30.00,0.00) + >>> turtle.position() + (0.00,0.00) + >>> turtle.backward(30) + >>> turtle.position() + (-30.00,0.00) """ self._go(-distance) def right(self, angle): - """Turn turtle right by angle units. + """Turn the turtle right by the specified angle. Aliases: right | rt - Argument: - angle -- a number (integer or float) + The angle is measured in degrees by default; the unit can be + changed with degrees() or radians(). How the heading is measured + depends on the turtle mode, see mode(). - Turn turtle right by angle units. (Units are by default degrees, - but can be set via the degrees() and radians() functions.) - Angle orientation depends on mode. (See this.) + Argument: + angle -- a number Example (for a Turtle instance named turtle): - >>> turtle.heading() - 22.0 - >>> turtle.right(45) - >>> turtle.heading() - 337.0 + >>> turtle.heading() + 22.0 + >>> turtle.right(45) + >>> turtle.heading() + 337.0 """ self._rotate(-angle) def left(self, angle): - """Turn turtle left by angle units. + """Turn the turtle left by the specified angle. Aliases: left | lt - Argument: - angle -- a number (integer or float) + The angle is measured in degrees by default; the unit can be + changed with degrees() or radians(). How the heading is measured + depends on the turtle mode, see mode(). - Turn turtle left by angle units. (Units are by default degrees, - but can be set via the degrees() and radians() functions.) - Angle orientation depends on mode. (See this.) + Argument: + angle -- a number Example (for a Turtle instance named turtle): - >>> turtle.heading() - 22.0 - >>> turtle.left(45) - >>> turtle.heading() - 67.0 + >>> turtle.heading() + 22.0 + >>> turtle.left(45) + >>> turtle.heading() + 67.0 """ self._rotate(angle) def pos(self): - """Return the turtle's current location (x,y), as a Vec2D-vector. + """Return the turtle's current location (x,y) as a Vec2D vector. Aliases: pos | position - No arguments. - Example (for a Turtle instance named turtle): - >>> turtle.pos() - (0.00, 240.00) + >>> turtle.pos() + (0.00,240.00) """ return self._position def xcor(self): - """ Return the turtle's x coordinate. - - No arguments. + """Return the turtle's x coordinate. Example (for a Turtle instance named turtle): - >>> reset() - >>> turtle.left(60) - >>> turtle.forward(100) - >>> print(turtle.xcor()) - 50.0 + >>> turtle.home() + >>> turtle.left(50) + >>> turtle.forward(100) + >>> print(round(turtle.xcor(), 5)) + 64.27876 """ return self._position[0] def ycor(self): - """ Return the turtle's y coordinate - --- - No arguments. + """Return the turtle's y coordinate Example (for a Turtle instance named turtle): - >>> reset() - >>> turtle.left(60) - >>> turtle.forward(100) - >>> print(turtle.ycor()) - 86.6025403784 + >>> reset() + >>> turtle.left(60) + >>> turtle.forward(100) + >>> print(round(turtle.ycor(), 5)) + 86.60254 """ return self._position[1] def goto(self, x, y=None): - """Move turtle to an absolute position. - - Aliases: setpos | setposition | goto: + """Move the turtle to an absolute position. - Arguments: - x -- a number or a pair/vector of numbers - y -- a number None + Aliases: goto | setpos | setposition - call: goto(x, y) # two coordinates - --or: goto((x, y)) # a pair (tuple) of coordinates - --or: goto(vec) # e.g. as returned by pos() + If y is None, x must be a pair of coordinates or a Vec2D, for + example as returned by pos(). If the pen is down, a line is drawn. + The turtle's heading does not change. - Move turtle to an absolute position. If the pen is down, - a line will be drawn. The turtle's orientation does not change. + Arguments: + x -- a number or a pair/vector of numbers + y -- a number or None Example (for a Turtle instance named turtle): - >>> tp = turtle.pos() - >>> tp - (0.00,0.00) - >>> turtle.setpos(60,30) - >>> turtle.pos() - (60.00,30.00) - >>> turtle.setpos((20,80)) - >>> turtle.pos() - (20.00,80.00) - >>> turtle.setpos(tp) - >>> turtle.pos() - (0.00,0.00) + >>> tp = turtle.pos() + >>> tp + (0.00,0.00) + >>> turtle.goto(60,30) + >>> turtle.pos() + (60.00,30.00) + >>> turtle.goto((20,80)) + >>> turtle.pos() + (20.00,80.00) + >>> turtle.goto(tp) + >>> turtle.pos() + (0.00,0.00) """ if y is None: self._goto(Vec2D(*x)) @@ -1844,76 +1799,79 @@ def goto(self, x, y=None): self._goto(Vec2D(x, y)) def home(self): - """Move turtle to the origin - coordinates (0,0). + """Move the turtle to the origin, coordinates (0,0). - No arguments. - - Move turtle to the origin - coordinates (0,0) and set its - heading to its start-orientation (which depends on mode). + The turtle's heading is set to its start orientation, which + depends on the turtle mode, see mode(). Example (for a Turtle instance named turtle): - >>> turtle.home() + >>> turtle.heading() + 90.0 + >>> turtle.position() + (0.00,-10.00) + >>> turtle.home() + >>> turtle.position() + (0.00,0.00) + >>> turtle.heading() + 0.0 """ self.goto(0, 0) self.setheading(0) def setx(self, x): - """Set the turtle's first coordinate to x + """Set the turtle's x coordinate to x. - Argument: - x -- a number (integer or float) + The y coordinate is unchanged. - Set the turtle's first coordinate to x, leave second coordinate - unchanged. + Argument: + x -- a number Example (for a Turtle instance named turtle): - >>> turtle.position() - (0.00, 240.00) - >>> turtle.setx(10) - >>> turtle.position() - (10.00, 240.00) + >>> turtle.position() + (0.00,240.00) + >>> turtle.setx(10) + >>> turtle.position() + (10.00,240.00) """ self._goto(Vec2D(x, self._position[1])) def sety(self, y): - """Set the turtle's second coordinate to y + """Set the turtle's y coordinate to y. - Argument: - y -- a number (integer or float) + The x coordinate is unchanged. - Set the turtle's first coordinate to x, second coordinate remains - unchanged. + Argument: + y -- a number Example (for a Turtle instance named turtle): - >>> turtle.position() - (0.00, 40.00) - >>> turtle.sety(-10) - >>> turtle.position() - (0.00, -10.00) + >>> turtle.position() + (0.00,40.00) + >>> turtle.sety(-10) + >>> turtle.position() + (0.00,-10.00) """ self._goto(Vec2D(self._position[0], y)) def distance(self, x, y=None): """Return the distance from the turtle to (x,y) in turtle step units. - Arguments: - x -- a number or a pair/vector of numbers or a turtle instance - y -- a number None None + If y is None, x must be a pair of coordinates, a Vec2D, for example + as returned by pos(), or another turtle. - call: distance(x, y) # two coordinates - --or: distance((x, y)) # a pair (tuple) of coordinates - --or: distance(vec) # e.g. as returned by pos() - --or: distance(mypen) # where mypen is another turtle + Arguments: + x -- a number or a pair/vector of numbers or a turtle instance + y -- a number if x is a number, else None Example (for a Turtle instance named turtle): - >>> turtle.pos() - (0.00,0.00) - >>> turtle.distance(30,40) - 50.0 - >>> pen = Turtle() - >>> pen.forward(77) - >>> turtle.distance(pen) - 77.0 + >>> turtle.home() + >>> turtle.distance(30,40) + 50.0 + >>> turtle.distance((30,40)) + 50.0 + >>> joe = Turtle() + >>> joe.forward(77) + >>> turtle.distance(joe) + 77.0 """ if y is not None: pos = Vec2D(x, y) @@ -1926,26 +1884,21 @@ def distance(self, x, y=None): return abs(pos - self._position) def towards(self, x, y=None): - """Return the angle of the line from the turtle's position to (x, y). + """Return the angle of the line from the turtle's position to (x,y). - Arguments: - x -- a number or a pair/vector of numbers or a turtle instance - y -- a number None None + If y is None, x must be a pair of coordinates, a Vec2D, for example + as returned by pos(), or another turtle. The angle is measured from + the turtle's start orientation, which depends on the turtle mode, + see mode(). - call: distance(x, y) # two coordinates - --or: distance((x, y)) # a pair (tuple) of coordinates - --or: distance(vec) # e.g. as returned by pos() - --or: distance(mypen) # where mypen is another turtle - - Return the angle, between the line from turtle-position to position - specified by x, y and the turtle's start orientation. (Depends on - modes - "standard" or "logo") + Arguments: + x -- a number or a pair/vector of numbers or a turtle instance + y -- a number if x is a number, else None Example (for a Turtle instance named turtle): - >>> turtle.pos() - (10.00, 10.00) - >>> turtle.towards(0,0) - 225.0 + >>> turtle.goto(10, 10) + >>> turtle.towards(0,0) + 225.0 """ if y is not None: pos = Vec2D(x, y) @@ -1961,14 +1914,15 @@ def towards(self, x, y=None): return (self._angleOffset + self._angleOrient*result) % self._fullcircle def heading(self): - """ Return the turtle's current heading. + """Return the turtle's current heading. - No arguments. + The value depends on the turtle mode, see mode(). Example (for a Turtle instance named turtle): - >>> turtle.left(67) - >>> turtle.heading() - 67.0 + >>> turtle.home() + >>> turtle.left(67) + >>> turtle.heading() + 67.0 """ x, y = self._orient result = round(math.degrees(math.atan2(y, x)), 10) % 360.0 @@ -1976,62 +1930,56 @@ def heading(self): return (self._angleOffset + self._angleOrient*result) % self._fullcircle def setheading(self, to_angle): - """Set the orientation of the turtle to to_angle. - - Aliases: setheading | seth + """Set the turtle's heading to to_angle. - Argument: - to_angle -- a number (integer or float) + Aliases: setheading | seth - Set the orientation of the turtle to to_angle. Here are some common directions in degrees: - standard - mode: logo-mode: - -------------------|-------------------- - 0 - east 0 - north - 90 - north 90 - east - 180 - west 180 - south - 270 - south 270 - west + standard mode logo mode + -------------------|--------------- + 0 - east 0 - north + 90 - north 90 - east + 180 - west 180 - south + 270 - south 270 - west + + Argument: + to_angle -- a number Example (for a Turtle instance named turtle): - >>> turtle.setheading(90) - >>> turtle.heading() - 90 + >>> turtle.setheading(90) + >>> turtle.heading() + 90.0 """ - angle = (to_angle - self.heading())*self._angleOrient + angle = (to_angle - self.heading()) * self._angleOrient full = self._fullcircle angle = (angle+full/2.)%full - full/2. self._rotate(angle) def circle(self, radius, extent = None, steps = None): - """ Draw a circle with given radius. + """Draw a circle with the given radius. - Arguments: - radius -- a number - extent (optional) -- a number - steps (optional) -- an integer - - Draw a circle with given radius. The center is radius units left - of the turtle; extent - an angle - determines which part of the - circle is drawn. If extent is not given, draw the entire circle. - If extent is not a full circle, one endpoint of the arc is the - current pen position. Draw the arc in counterclockwise direction - if radius is positive, otherwise in clockwise direction. Finally - the direction of the turtle is changed by the amount of extent. + The center is radius units left of the turtle; extent, an angle, + determines which part of the circle is drawn. If extent is not + given, draw the entire circle. If extent is not a full circle, one + endpoint of the arc is the current pen position. Draw the arc in + the counterclockwise direction if radius is positive, otherwise in + the clockwise direction. Finally, the turtle's heading is changed by + extent. As the circle is approximated by an inscribed regular polygon, - steps determines the number of steps to use. If not given, - it will be calculated automatically. Maybe used to draw regular + steps determines the number of steps to use. If not given, it + will be calculated automatically. May be used to draw regular polygons. - call: circle(radius) # full circle - --or: circle(radius, extent) # arc - --or: circle(radius, extent, steps) - --or: circle(radius, steps=6) # 6-sided polygon + Arguments: + radius -- a number + extent -- a number or None + steps -- an integer or None Example (for a Turtle instance named turtle): - >>> turtle.circle(50) - >>> turtle.circle(120, 180) # semicircle + >>> turtle.circle(50) + >>> turtle.circle(120, 180) # draw a semicircle """ if self.undobuffer: self.undobuffer.push(["seq"]) @@ -2066,14 +2014,16 @@ def circle(self, radius, extent = None, steps = None): if self.undobuffer: self.undobuffer.cumulate = False -## three dummy methods to be implemented by child class: + # Three dummy methods to be implemented by the child class: def speed(self, s=0): - """dummy method - to be overwritten by child class""" + """Dummy method - to be overwritten by the child class.""" def _tracer(self, a=None, b=None): - """dummy method - to be overwritten by child class""" + """Dummy method - to be overwritten by the child class.""" def _delay(self, n=None): - """dummy method - to be overwritten by child class""" + """Dummy method - to be overwritten by the child class.""" + + # Aliases for the commonly used commands: fd = forward bk = back @@ -2086,10 +2036,12 @@ def _delay(self, n=None): seth = setheading -class TPen(object): +class TPen: """Drawing part of the RawTurtle. + Implements drawing properties. """ + def __init__(self, resizemode=_CFG["resizemode"]): self._resizemode = resizemode # or "user" or "noresize" self.undobuffer = None @@ -2562,7 +2514,7 @@ def _colorstr(self, args): ht = hideturtle -class _TurtleImage(object): +class _TurtleImage: """Helper class: Datatype to store Turtle attributes """ @@ -3396,9 +3348,10 @@ def _rotate(self, angle): self._update() def _newLine(self, usePos=True): - """Closes current line item and starts a new one. - Remark: if current line became too long, animation - performance (via _drawline) slowed down considerably. + """Closes the current line item and starts a new one. + + If the current line becomes too long, animation performance (via _drawline) + can slow down considerably. """ if len(self.currentLine) > 1: self.screen._drawline(self.currentLineItem, self.currentLine, @@ -3615,7 +3568,7 @@ def end_poly(self): self._creatingPoly = False def get_poly(self): - """Return the lastly recorded polygon. + """Return the last recorded polygon. No argument. @@ -3628,11 +3581,10 @@ def get_poly(self): return tuple(self._poly) def getscreen(self): - """Return the TurtleScreen object, the turtle is drawing on. + """Return the TurtleScreen object the turtle is drawing on. No argument. - Return the TurtleScreen object, the turtle is drawing on. So TurtleScreen-methods can be called for that object. Example (for a Turtle instance named turtle): @@ -3747,7 +3699,7 @@ def _undo(self, action, data): if action == "rot": angle, degPAU = data self._rotate(-angle*degPAU/self._degreesPerAU) - dummy = self.undobuffer.pop() + self.undobuffer.pop() elif action == "stamp": stitem = data[0] self.clearstamp(stitem) @@ -3999,7 +3951,7 @@ def write_docstringdict(filename="turtle_docstringdict"): f.close() def read_docstrings(lang): - """Read in docstrings from lang-specific docstring dictionary. + """Read in docstrings from a language-specific docstring dictionary. Transfer docstrings, translated to lang, from a dictionary-file to the methods of classes Screen and Turtle and - in revised form - diff --git a/Lib/typing.py b/Lib/typing.py index 2b4389b2967..5b764419d8a 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -28,6 +28,7 @@ import sys import types from types import GenericAlias +lazy import annotationlib from _typing import ( _idfunc, @@ -65,7 +66,6 @@ # ABCs (from collections.abc). 'AbstractSet', # collections.abc.Set. - 'ByteString', 'Container', 'ContextManager', 'Hashable', @@ -127,6 +127,7 @@ 'cast', 'clear_overloads', 'dataclass_transform', + 'disjoint_base', 'evaluate_forward_ref', 'final', 'get_args', @@ -140,8 +141,8 @@ 'Never', 'NewType', 'no_type_check', - 'no_type_check_decorator', 'NoDefault', + 'NoExtraItems', 'NoReturn', 'NotRequired', 'overload', @@ -156,21 +157,13 @@ 'Text', 'TYPE_CHECKING', 'TypeAlias', + 'TypeForm', 'TypeGuard', 'TypeIs', 'TypeAliasType', 'Unpack', ] -class _LazyAnnotationLib: - def __getattr__(self, attr): - global _lazy_annotationlib - import annotationlib - _lazy_annotationlib = annotationlib - return getattr(annotationlib, attr) - -_lazy_annotationlib = _LazyAnnotationLib() - def _type_convert(arg, module=None, *, allow_special_forms=False, owner=None): """For converting None to type(None), and strings to ForwardRef.""" @@ -254,19 +247,30 @@ def _type_repr(obj): if isinstance(obj, tuple): # Special case for `repr` of types with `ParamSpec`: return '[' + ', '.join(_type_repr(t) for t in obj) + ']' - return _lazy_annotationlib.type_repr(obj) + return annotationlib.type_repr(obj) -def _collect_type_parameters(args, *, enforce_default_ordering: bool = True): +def _collect_type_parameters( + args, + *, + enforce_default_ordering: bool = True, + validate_all: bool = False, +): """Collect all type parameters in args in order of first appearance (lexicographic order). + Having an explicit `Generic` or `Protocol` base class determines + the exact parameter order. + For example:: >>> P = ParamSpec('P') >>> T = TypeVar('T') >>> _collect_type_parameters((T, Callable[P, T])) (~T, ~P) + >>> _collect_type_parameters((list[T], Generic[P, T])) + (~P, ~T) + """ # required type parameter cannot appear after parameter with default default_encountered = False @@ -298,6 +302,17 @@ def _collect_type_parameters(args, *, enforce_default_ordering: bool = True): ' follows type parameter with a default') parameters.append(t) + elif ( + not validate_all + and isinstance(t, _GenericAlias) + and t.__origin__ in (Generic, Protocol) + ): + # If we see explicit `Generic[...]` or `Protocol[...]` base classes, + # we need to just copy them as-is. + # Unless `validate_all` is passed, in this case it means that + # we are doing a validation of `Generic` subclasses, + # then we collect all unique parameters to be able to inspect them. + parameters = t.__parameters__ else: if _is_unpacked_typevartuple(t): type_var_tuple_encountered = True @@ -408,6 +423,17 @@ def inner(*args, **kwds): return decorator +def _rebuild_generic_alias(alias: GenericAlias, args: tuple[object, ...]) -> GenericAlias: + is_unpacked = alias.__unpacked__ + if _should_unflatten_callable_args(alias, args): + t = alias.__origin__[(args[:-1], args[-1])] + else: + t = alias.__origin__[args] + if is_unpacked: + t = Unpack[t] + return t + + def _deprecation_warning_for_no_type_params_passed(funcname: str) -> None: import warnings @@ -421,16 +447,7 @@ def _deprecation_warning_for_no_type_params_passed(funcname: str) -> None: warnings.warn(depr_message, category=DeprecationWarning, stacklevel=3) -class _Sentinel: - __slots__ = () - def __repr__(self): - return '' - - -_sentinel = _Sentinel() - - -def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=frozenset(), +def _eval_type(t, globalns, localns, type_params, *, recursive_guard=frozenset(), format=None, owner=None, parent_fwdref=None, prefer_fwd_module=False): """Evaluate all forward references in the given type t. @@ -438,10 +455,7 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f recursive_guard is used to prevent infinite recursion with a recursive ForwardRef. """ - if type_params is _sentinel: - _deprecation_warning_for_no_type_params_passed("typing._eval_type") - type_params = () - if isinstance(t, _lazy_annotationlib.ForwardRef): + if isinstance(t, annotationlib.ForwardRef): # If the forward_ref has __forward_module__ set, evaluate() infers the globals # from the module, and it will probably pick better than the globals we have here. # We do this only for calls from get_type_hints() (which opts in through the @@ -467,25 +481,20 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f _make_forward_ref(arg, parent_fwdref=parent_fwdref) if isinstance(arg, str) else arg for arg in t.__args__ ) - is_unpacked = t.__unpacked__ - if _should_unflatten_callable_args(t, args): - t = t.__origin__[(args[:-1], args[-1])] - else: - t = t.__origin__[args] - if is_unpacked: - t = Unpack[t] + else: + args = t.__args__ ev_args = tuple( _eval_type( a, globalns, localns, type_params, recursive_guard=recursive_guard, format=format, owner=owner, prefer_fwd_module=prefer_fwd_module, ) - for a in t.__args__ + for a in args ) if ev_args == t.__args__: return t if isinstance(t, GenericAlias): - return GenericAlias(t.__origin__, ev_args) + return _rebuild_generic_alias(t, ev_args) if isinstance(t, Union): return functools.reduce(operator.or_, ev_args) else: @@ -573,6 +582,13 @@ def __getitem__(self, parameters): return self._getitem(self, *parameters) +class _TypeFormForm(_SpecialForm, _root=True): + # TypeForm(X) is equivalent to X but indicates to the type checker + # that the object is a TypeForm. + def __call__(self, obj, /): + return obj + + class _AnyMeta(type): def __instancecheck__(self, obj): if self is Any: @@ -883,6 +899,31 @@ def func1(val: list[object]): return _GenericAlias(self, (item,)) +@_TypeFormForm +def TypeForm(self, parameters): + """A special form representing the value that results from the evaluation + of a type expression. + + This value encodes the information supplied in the type expression, and it + represents the type described by that type expression. + + When used in a type expression, TypeForm describes a set of type form + objects. It accepts a single type argument, which must be a valid type + expression. ``TypeForm[T]`` describes the set of all type form objects that + represent the type T or types that are assignable to T. + + Usage:: + + def cast[T](typ: TypeForm[T], value: Any) -> T: ... + + reveal_type(cast(int, "x")) # int + + See PEP 747 for more information. + """ + item = _type_check(parameters, f'{self} accepts only single type.') + return _GenericAlias(self, (item,)) + + @_SpecialForm def TypeIs(self, parameters): """Special typing construct for marking user-defined type predicate functions. @@ -958,7 +999,7 @@ def _make_forward_ref(code, *, parent_fwdref=None, **kwargs): kwargs['module'] = parent_fwdref.__forward_module__ if parent_fwdref.__owner__ is not None: kwargs['owner'] = parent_fwdref.__owner__ - forward_ref = _lazy_annotationlib.ForwardRef(code, **kwargs) + forward_ref = annotationlib.ForwardRef(code, **kwargs) # For compatibility, eagerly compile the forwardref's code. forward_ref.__forward_code__ return forward_ref @@ -993,18 +1034,18 @@ def evaluate_forward_ref( VALUE. """ - if format == _lazy_annotationlib.Format.STRING: + if format == annotationlib.Format.STRING: return forward_ref.__resolved_str__ if forward_ref.__forward_arg__ in _recursive_guard: return forward_ref if format is None: - format = _lazy_annotationlib.Format.VALUE + format = annotationlib.Format.VALUE value = forward_ref.evaluate(globals=globals, locals=locals, type_params=type_params, owner=owner, format=format) - if (isinstance(value, _lazy_annotationlib.ForwardRef) - and format == _lazy_annotationlib.Format.FORWARDREF): + if (isinstance(value, annotationlib.ForwardRef) + and format == annotationlib.Format.FORWARDREF): return value if isinstance(value, str): @@ -1186,20 +1227,22 @@ def _generic_init_subclass(cls, *args, **kwargs): if error: raise TypeError("Cannot inherit from plain Generic") if '__orig_bases__' in cls.__dict__: - tvars = _collect_type_parameters(cls.__orig_bases__) + tvars = _collect_type_parameters(cls.__orig_bases__, validate_all=True) # Look for Generic[T1, ..., Tn]. # If found, tvars must be a subset of it. # If not found, tvars is it. # Also check for and reject plain Generic, # and reject multiple Generic[...]. gvars = None + basename = None for base in cls.__orig_bases__: if (isinstance(base, _GenericAlias) and - base.__origin__ is Generic): + base.__origin__ in (Generic, Protocol)): if gvars is not None: raise TypeError( "Cannot inherit from Generic[...] multiple times.") gvars = base.__parameters__ + basename = base.__origin__.__name__ if gvars is not None: tvarset = set(tvars) gvarset = set(gvars) @@ -1207,7 +1250,7 @@ def _generic_init_subclass(cls, *args, **kwargs): s_vars = ', '.join(str(t) for t in tvars if t not in gvarset) s_args = ', '.join(str(g) for g in gvars) raise TypeError(f"Some type variables ({s_vars}) are" - f" not listed in Generic[{s_args}]") + f" not listed in {basename}[{s_args}]") tvars = gvars cls.__parameters__ = tuple(tvars) @@ -1605,21 +1648,6 @@ def __ror__(self, left): return Union[left, self] -class _DeprecatedGenericAlias(_SpecialGenericAlias, _root=True): - def __init__( - self, origin, nparams, *, removal_version, inst=True, name=None - ): - super().__init__(origin, nparams, inst=inst, name=name) - self._removal_version = removal_version - - def __instancecheck__(self, inst): - import warnings - warnings._deprecated( - f"{self.__module__}.{self._name}", remove=self._removal_version - ) - return super().__instancecheck__(inst) - - class _CallableGenericAlias(_NotIterable, _GenericAlias, _root=True): def __repr__(self): assert self._name == 'Callable' @@ -1832,6 +1860,7 @@ class _TypingEllipsis: _TYPING_INTERNALS = frozenset({ '__parameters__', '__orig_bases__', '__orig_class__', '_is_protocol', '_is_runtime_protocol', '__protocol_attrs__', + '__typing_is_deprecated_inherited_runtime_protocol__', '__non_callable_proto_members__', '__type_params__', }) @@ -1861,8 +1890,8 @@ def _get_protocol_attrs(cls): annotations = base.__annotations__ except Exception: # Only go through annotationlib to handle deferred annotations if we need to - annotations = _lazy_annotationlib.get_annotations( - base, format=_lazy_annotationlib.Format.FORWARDREF + annotations = annotationlib.get_annotations( + base, format=annotationlib.Format.FORWARDREF ) for attr in (*base.__dict__, *annotations): if not attr.startswith('_abc_') and attr not in EXCLUDED_ATTRIBUTES: @@ -1916,7 +1945,9 @@ def _allow_reckless_class_checks(depth=2): The abc and functools modules indiscriminately call isinstance() and issubclass() on the whole MRO of a user class, which may contain protocols. """ - return _caller(depth) in {'abc', 'functools', None} + # gh-136047: When `_abc` module is not available, `_py_abc` is required to + # allow `_py_abc.ABCMeta` fallback. + return _caller(depth) in {'abc', '_py_abc', 'functools', None} _PROTO_ALLOWLIST = { @@ -2019,6 +2050,16 @@ def __subclasscheck__(cls, other): "Instance and class checks can only be used with " "@runtime_checkable protocols" ) + if getattr(cls, '__typing_is_deprecated_inherited_runtime_protocol__', False): + # See GH-132604. + import warnings + depr_message = ( + f"{cls!r} isn't explicitly decorated with @runtime_checkable but " + "it is used in issubclass() or isinstance(). Instance and class " + "checks can only be used with @runtime_checkable protocols. " + "This will raise a TypeError in Python 3.20." + ) + warnings.warn(depr_message, category=DeprecationWarning, stacklevel=2) if ( # this attribute is set by @runtime_checkable: cls.__non_callable_proto_members__ @@ -2048,6 +2089,18 @@ def __instancecheck__(cls, instance): raise TypeError("Instance and class checks can only be used with" " @runtime_checkable protocols") + if getattr(cls, '__typing_is_deprecated_inherited_runtime_protocol__', False): + # See GH-132604. + import warnings + + depr_message = ( + f"{cls!r} isn't explicitly decorated with @runtime_checkable but " + "it is used in issubclass() or isinstance(). Instance and class " + "checks can only be used with @runtime_checkable protocols. " + "This will raise a TypeError in Python 3.20." + ) + warnings.warn(depr_message, category=DeprecationWarning, stacklevel=2) + if _abc_instancecheck(cls, instance): return True @@ -2086,8 +2139,8 @@ def _proto_hook(cls, other): try: annos = base.__annotations__ except Exception: - annos = _lazy_annotationlib.get_annotations( - base, format=_lazy_annotationlib.Format.FORWARDREF + annos = annotationlib.get_annotations( + base, format=annotationlib.Format.FORWARDREF ) if attr in annos: break @@ -2140,6 +2193,11 @@ def __init_subclass__(cls, *args, **kwargs): if not cls.__dict__.get('_is_protocol', False): cls._is_protocol = any(b is Protocol for b in cls.__bases__) + # Mark inherited runtime checkability (deprecated). See GH-132604. + if cls._is_protocol and getattr(cls, '_is_runtime_protocol', False): + # This flag is set to False by @runtime_checkable. + cls.__typing_is_deprecated_inherited_runtime_protocol__ = True + # Set (or override) the protocol subclass hook. if '__subclasshook__' not in cls.__dict__: cls.__subclasshook__ = _proto_hook @@ -2286,6 +2344,9 @@ def close(self): ... raise TypeError('@runtime_checkable can be only applied to protocol classes,' ' got %r' % cls) cls._is_runtime_protocol = True + # See GH-132604. + if hasattr(cls, '__typing_is_deprecated_inherited_runtime_protocol__'): + cls.__typing_is_deprecated_inherited_runtime_protocol__ = False # PEP 544 prohibits using issubclass() # with protocols that have non-method members. # See gh-113320 for why we compute this attribute here, @@ -2366,14 +2427,14 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, """ if getattr(obj, '__no_type_check__', None): return {} - Format = _lazy_annotationlib.Format + Format = annotationlib.Format if format is None: format = Format.VALUE # Classes require a special treatment. if isinstance(obj, type): hints = {} for base in reversed(obj.__mro__): - ann = _lazy_annotationlib.get_annotations(base, format=format) + ann = annotationlib.get_annotations(base, format=format) if format == Format.STRING: hints.update(ann) continue @@ -2406,7 +2467,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, else: return {k: _strip_annotations(t) for k, t in hints.items()} - hints = _lazy_annotationlib.get_annotations(obj, format=format) + hints = annotationlib.get_annotations(obj, format=format) if ( not hints and not isinstance(obj, types.ModuleType) @@ -2482,7 +2543,7 @@ def _strip_annotations(t): stripped_args = tuple(_strip_annotations(a) for a in t.__args__) if stripped_args == t.__args__: return t - return GenericAlias(t.__origin__, stripped_args) + return _rebuild_generic_alias(t, stripped_args) if isinstance(t, Union): stripped_args = tuple(_strip_annotations(a) for a in t.__args__) if stripped_args == t.__args__: @@ -2630,23 +2691,6 @@ def no_type_check(arg): return arg -def no_type_check_decorator(decorator): - """Decorator to give another decorator the @no_type_check effect. - - This wraps the decorator with something that wraps the decorated - function in @no_type_check. - """ - import warnings - warnings._deprecated("typing.no_type_check_decorator", remove=(3, 15)) - @functools.wraps(decorator) - def wrapped_decorator(*args, **kwds): - func = decorator(*args, **kwds) - func = no_type_check(func) - return func - - return wrapped_decorator - - def _overload_dummy(*args, **kwds): """Helper for @overload to raise when called.""" raise NotImplementedError( @@ -2753,6 +2797,29 @@ class Other(Leaf): # Error reported by type checker return f +def disjoint_base(cls): + """This decorator marks a class as a disjoint base. + + Child classes of a disjoint base cannot inherit from other disjoint bases that are + not parent or child classes of the disjoint base. + + For example: + + @disjoint_base + class Disjoint1: pass + + @disjoint_base + class Disjoint2: pass + + class Disjoint3(Disjoint1, Disjoint2): pass # Type checker error + + Type checkers can use knowledge of disjoint bases to detect unreachable code + and determine when two types can overlap. + """ + cls.__disjoint_base__ = True + return cls + + # Some unconstrained type variables. These were initially used by the container types. # They were never meant for export and are now unused, but we keep them around to # avoid breaking compatibility with users who import them. @@ -2808,9 +2875,6 @@ class Other(Leaf): # Error reported by type checker MutableMapping = _alias(collections.abc.MutableMapping, 2) Sequence = _alias(collections.abc.Sequence, 1) MutableSequence = _alias(collections.abc.MutableSequence, 1) -ByteString = _DeprecatedGenericAlias( - collections.abc.ByteString, 0, removal_version=(3, 17) # Not generic. -) # Tuple accepts variable number of parameters. Tuple = _TupleType(tuple, -1, inst=False, name='Tuple') Tuple.__doc__ = \ @@ -2954,10 +3018,10 @@ def _make_eager_annotate(types): for key, val in types.items()} def annotate(format): match format: - case _lazy_annotationlib.Format.VALUE | _lazy_annotationlib.Format.FORWARDREF: + case annotationlib.Format.VALUE | annotationlib.Format.FORWARDREF: return checked_types - case _lazy_annotationlib.Format.STRING: - return _lazy_annotationlib.annotations_to_string(types) + case annotationlib.Format.STRING: + return annotationlib.annotations_to_string(types) case _: raise NotImplementedError(format) return annotate @@ -2987,9 +3051,9 @@ def __new__(cls, typename, bases, ns): types = ns["__annotations__"] field_names = list(types) annotate = _make_eager_annotate(types) - elif (original_annotate := _lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None: - types = _lazy_annotationlib.call_annotate_function( - original_annotate, _lazy_annotationlib.Format.FORWARDREF) + elif (original_annotate := annotationlib.get_annotate_from_class_namespace(ns)) is not None: + types = annotationlib.call_annotate_function( + original_annotate, annotationlib.Format.FORWARDREF) field_names = list(types) # For backward compatibility, type-check all the types at creation time @@ -2997,9 +3061,9 @@ def __new__(cls, typename, bases, ns): _type_check(typ, "field annotation must be a type") def annotate(format): - annos = _lazy_annotationlib.call_annotate_function( + annos = annotationlib.call_annotate_function( original_annotate, format) - if format != _lazy_annotationlib.Format.STRING: + if format != annotationlib.Format.STRING: return {key: _type_check(val, f"field {key} annotation must be a type") for key, val in annos.items()} return annos @@ -3049,7 +3113,7 @@ def annotate(format): return nm_tpl -def NamedTuple(typename, fields=_sentinel, /, **kwargs): +def NamedTuple(typename, fields, /): """Typed version of collections.namedtuple. Usage:: @@ -3069,49 +3133,9 @@ class Employee(NamedTuple): Employee = NamedTuple('Employee', [('name', str), ('id', int)]) """ - if fields is _sentinel: - if kwargs: - deprecated_thing = "Creating NamedTuple classes using keyword arguments" - deprecation_msg = ( - "{name} is deprecated and will be disallowed in Python {remove}. " - "Use the class-based or functional syntax instead." - ) - else: - deprecated_thing = "Failing to pass a value for the 'fields' parameter" - example = f"`{typename} = NamedTuple({typename!r}, [])`" - deprecation_msg = ( - "{name} is deprecated and will be disallowed in Python {remove}. " - "To create a NamedTuple class with 0 fields " - "using the functional syntax, " - "pass an empty list, e.g. " - ) + example + "." - elif fields is None: - if kwargs: - raise TypeError( - "Cannot pass `None` as the 'fields' parameter " - "and also specify fields using keyword arguments" - ) - else: - deprecated_thing = "Passing `None` as the 'fields' parameter" - example = f"`{typename} = NamedTuple({typename!r}, [])`" - deprecation_msg = ( - "{name} is deprecated and will be disallowed in Python {remove}. " - "To create a NamedTuple class with 0 fields " - "using the functional syntax, " - "pass an empty list, e.g. " - ) + example + "." - elif kwargs: - raise TypeError("Either list of fields or keywords" - " can be provided to NamedTuple, not both") - if fields is _sentinel or fields is None: - import warnings - warnings._deprecated(deprecated_thing, message=deprecation_msg, remove=(3, 15)) - fields = kwargs.items() types = {n: _type_check(t, f"field {n} annotation must be a type") for n, t in fields} - field_names = [n for n, _ in fields] - - nt = _make_nmtuple(typename, field_names, _make_eager_annotate(types), module=_caller()) + nt = _make_nmtuple(typename, types, _make_eager_annotate(types), module=_caller()) nt.__orig_bases__ = (NamedTuple,) return nt @@ -3124,6 +3148,9 @@ def _namedtuple_mro_entries(bases): NamedTuple.__mro_entries__ = _namedtuple_mro_entries +NoExtraItems = sentinel("NoExtraItems") + + def _get_typeddict_qualifiers(annotation_type): while True: annotation_origin = get_origin(annotation_type) @@ -3147,7 +3174,8 @@ def _get_typeddict_qualifiers(annotation_type): class _TypedDictMeta(type): - def __new__(cls, name, bases, ns, total=True): + def __new__(cls, name, bases, ns, total=True, closed=None, + extra_items=NoExtraItems): """Create a new typed dict class object. This method is called when TypedDict is subclassed, @@ -3159,6 +3187,8 @@ def __new__(cls, name, bases, ns, total=True): if type(base) is not _TypedDictMeta and base is not Generic: raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') + if closed is not None and extra_items is not NoExtraItems: + raise TypeError(f"Cannot combine closed={closed!r} and extra_items") if any(issubclass(b, Generic) for b in bases): generic_base = (Generic,) @@ -3175,9 +3205,9 @@ def __new__(cls, name, bases, ns, total=True): if ns_annotations is not None: own_annotate = None own_annotations = ns_annotations - elif (own_annotate := _lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None: - own_annotations = _lazy_annotationlib.call_annotate_function( - own_annotate, _lazy_annotationlib.Format.FORWARDREF, owner=tp_dict + elif (own_annotate := annotationlib.get_annotate_from_class_namespace(ns)) is not None: + own_annotations = annotationlib.call_annotate_function( + own_annotate, annotationlib.Format.FORWARDREF, owner=tp_dict ) else: own_annotate = None @@ -3244,20 +3274,20 @@ def __annotate__(format): base_annotate = base.__annotate__ if base_annotate is None: continue - base_annos = _lazy_annotationlib.call_annotate_function( + base_annos = annotationlib.call_annotate_function( base_annotate, format, owner=base) annos.update(base_annos) if own_annotate is not None: - own = _lazy_annotationlib.call_annotate_function( + own = annotationlib.call_annotate_function( own_annotate, format, owner=tp_dict) - if format != _lazy_annotationlib.Format.STRING: + if format != annotationlib.Format.STRING: own = { n: _type_check(tp, msg, module=tp_dict.__module__) for n, tp in own.items() } - elif format == _lazy_annotationlib.Format.STRING: - own = _lazy_annotationlib.annotations_to_string(own_annotations) - elif format in (_lazy_annotationlib.Format.FORWARDREF, _lazy_annotationlib.Format.VALUE): + elif format == annotationlib.Format.STRING: + own = annotationlib.annotations_to_string(own_annotations) + elif format in (annotationlib.Format.FORWARDREF, annotationlib.Format.VALUE): own = own_checked_annotations else: raise NotImplementedError(format) @@ -3270,6 +3300,8 @@ def __annotate__(format): tp_dict.__readonly_keys__ = frozenset(readonly_keys) tp_dict.__mutable_keys__ = frozenset(mutable_keys) tp_dict.__total__ = total + tp_dict.__closed__ = closed + tp_dict.__extra_items__ = extra_items return tp_dict __call__ = dict # static method @@ -3281,7 +3313,8 @@ def __subclasscheck__(cls, other): __instancecheck__ = __subclasscheck__ -def TypedDict(typename, fields=_sentinel, /, *, total=True): +def TypedDict(typename, fields, /, *, total=True, closed=None, + extra_items=NoExtraItems): """A simple typed namespace. At runtime it is equivalent to a plain dict. TypedDict creates a dictionary type such that a type checker will expect all @@ -3335,33 +3368,41 @@ class DatabaseUser(TypedDict): id: ReadOnly[int] # the "id" key must not be modified username: str # the "username" key can be changed - See PEPs 589, 655, and 705 for more information. - """ - if fields is _sentinel or fields is None: - import warnings + The closed argument controls whether the TypedDict allows additional + non-required items during inheritance and assignability checks. + If closed=True, the TypedDict does not allow additional items:: - if fields is _sentinel: - deprecated_thing = "Failing to pass a value for the 'fields' parameter" - else: - deprecated_thing = "Passing `None` as the 'fields' parameter" - - example = f"`{typename} = TypedDict({typename!r}, {{{{}}}})`" - deprecation_msg = ( - "{name} is deprecated and will be disallowed in Python {remove}. " - "To create a TypedDict class with 0 fields " - "using the functional syntax, " - "pass an empty dictionary, e.g. " - ) + example + "." - warnings._deprecated(deprecated_thing, message=deprecation_msg, remove=(3, 15)) - fields = {} + Point2D = TypedDict('Point2D', {'x': int, 'y': int}, closed=True) + class Point3D(Point2D): + z: int # Type checker error + + Passing closed=False explicitly requests TypedDict's default open behavior. + If closed is not provided, the behavior is inherited from the superclass. + A type checker is only expected to support a literal False or True as the + value of the closed argument. + + The extra_items argument can instead be used to specify the assignable type + of unknown non-required keys:: + Point2D = TypedDict('Point2D', {'x': int, 'y': int}, extra_items=int) + class Point3D(Point2D): + z: int # OK + label: str # Type checker error + + The extra_items argument is also inherited through subclassing. It is unset + by default, and it may not be used with the closed argument at the same + time. + + See PEPs 589, 655, 705, and 728 for more information. + """ ns = {'__annotations__': dict(fields)} module = _caller() if module is not None: # Setting correct module is necessary to make typed dict classes pickleable. ns['__module__'] = module - td = _TypedDictMeta(typename, (), ns, total=total) + td = _TypedDictMeta(typename, (), ns, total=total, closed=closed, + extra_items=extra_items) td.__orig_bases__ = (TypedDict,) return td @@ -3549,7 +3590,7 @@ def isatty(self) -> bool: pass @abstractmethod - def read(self, n: int = -1) -> AnyStr: + def read(self, n: int = -1, /) -> AnyStr: pass @abstractmethod @@ -3557,15 +3598,15 @@ def readable(self) -> bool: pass @abstractmethod - def readline(self, limit: int = -1) -> AnyStr: + def readline(self, limit: int = -1, /) -> AnyStr: pass @abstractmethod - def readlines(self, hint: int = -1) -> list[AnyStr]: + def readlines(self, hint: int = -1, /) -> list[AnyStr]: pass @abstractmethod - def seek(self, offset: int, whence: int = 0) -> int: + def seek(self, offset: int, whence: int = 0, /) -> int: pass @abstractmethod @@ -3577,7 +3618,7 @@ def tell(self) -> int: pass @abstractmethod - def truncate(self, size: int | None = None) -> int: + def truncate(self, size: int | None = None, /) -> int: pass @abstractmethod @@ -3585,11 +3626,11 @@ def writable(self) -> bool: pass @abstractmethod - def write(self, s: AnyStr) -> int: + def write(self, s: AnyStr, /) -> int: pass @abstractmethod - def writelines(self, lines: list[AnyStr]) -> None: + def writelines(self, lines: list[AnyStr], /) -> None: pass @abstractmethod @@ -3597,7 +3638,7 @@ def __enter__(self) -> IO[AnyStr]: pass @abstractmethod - def __exit__(self, type, value, traceback) -> None: + def __exit__(self, type, value, traceback, /) -> None: pass @@ -3607,7 +3648,7 @@ class BinaryIO(IO[bytes]): __slots__ = () @abstractmethod - def write(self, s: bytes | bytearray) -> int: + def write(self, s: bytes | bytearray, /) -> int: pass @abstractmethod @@ -3843,7 +3884,7 @@ def __getattr__(attr): are only created on-demand here. """ if attr == "ForwardRef": - obj = _lazy_annotationlib.ForwardRef + obj = annotationlib.ForwardRef elif attr in {"Pattern", "Match"}: import re obj = _alias(getattr(re, attr), 1) @@ -3860,6 +3901,48 @@ def __getattr__(attr): ) warnings.warn(depr_message, category=DeprecationWarning, stacklevel=2) obj = _collect_type_parameters + elif attr == "ByteString": + import warnings + + warnings._deprecated( + "typing.ByteString", + message=( + "{name!r} and 'collections.abc.ByteString' are deprecated " + "and slated for removal in Python {remove}" + ), + remove=(3, 17) + ) + + class _DeprecatedGenericAlias(_SpecialGenericAlias, _root=True): + def __init__( + self, origin, nparams, *, removal_version, inst=True, name=None + ): + super().__init__(origin, nparams, inst=inst, name=name) + self._removal_version = removal_version + + def __instancecheck__(self, inst): + import warnings + warnings._deprecated( + f"{self.__module__}.{self._name}", remove=self._removal_version + ) + return super().__instancecheck__(inst) + + def __subclasscheck__(self, cls): + import warnings + warnings._deprecated( + f"{self.__module__}.{self._name}", remove=self._removal_version + ) + return super().__subclasscheck__(cls) + + with warnings.catch_warnings( + action="ignore", category=DeprecationWarning + ): + # Not generic + ByteString = globals()["ByteString"] = _DeprecatedGenericAlias( + collections.abc.ByteString, 0, removal_version=(3, 17) + ) + + return ByteString else: raise AttributeError(f"module {__name__!r} has no attribute {attr!r}") globals()[attr] = obj diff --git a/Lib/unittest/_log.py b/Lib/unittest/_log.py index 94868e5bb95..3d69385ea24 100644 --- a/Lib/unittest/_log.py +++ b/Lib/unittest/_log.py @@ -30,7 +30,7 @@ class _AssertLogsContext(_BaseTestCaseContext): LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s" - def __init__(self, test_case, logger_name, level, no_logs): + def __init__(self, test_case, logger_name, level, no_logs, formatter=None): _BaseTestCaseContext.__init__(self, test_case) self.logger_name = logger_name if level: @@ -39,13 +39,14 @@ def __init__(self, test_case, logger_name, level, no_logs): self.level = logging.INFO self.msg = None self.no_logs = no_logs + self.formatter = formatter def __enter__(self): if isinstance(self.logger_name, logging.Logger): logger = self.logger = self.logger_name else: logger = self.logger = logging.getLogger(self.logger_name) - formatter = logging.Formatter(self.LOGGING_FORMAT) + formatter = self.formatter or logging.Formatter(self.LOGGING_FORMAT) handler = _CapturingHandler() handler.setLevel(self.level) handler.setFormatter(formatter) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 884fc1b21f6..a392238c85a 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -149,9 +149,7 @@ def doModuleCleanups(): except Exception as exc: exceptions.append(exc) if exceptions: - # Swallows all but first exception. If a multi-exception handler - # gets written we should use that here instead. - raise exceptions[0] + raise ExceptionGroup('module cleanup failed', exceptions) def skip(reason): @@ -303,7 +301,7 @@ def __enter__(self): v.__warningregistry__ = {} self.warnings_manager = warnings.catch_warnings(record=True) self.warnings = self.warnings_manager.__enter__() - warnings.simplefilter("always", self.expected) + warnings.simplefilter("always") return self def __exit__(self, exc_type, exc_value, tb): @@ -316,19 +314,44 @@ def __exit__(self, exc_type, exc_value, tb): except AttributeError: exc_name = str(self.expected) first_matching = None + matched = False + non_matching_warnings = [] for m in self.warnings: w = m.message if not isinstance(w, self.expected): + non_matching_warnings.append(m) continue if first_matching is None: first_matching = w if (self.expected_regex is not None and not self.expected_regex.search(str(w))): + non_matching_warnings.append(m) continue + if matched: + continue + matched = True # store warning for later retrieval self.warning = w self.filename = m.filename self.lineno = m.lineno + for m in non_matching_warnings: + module = m.module + module_globals = None + registry = None + if module is not None: + try: + module_globals = vars(sys.modules[module]) + except (KeyError, TypeError): + # module == "" or sys.modules[module] is None + pass + else: + registry = module_globals.setdefault("__warningregistry__", {}) + warnings.warn_explicit(m.message, m.category, m.filename, m.lineno, + module=module, + registry=registry, + module_globals=module_globals, + source=m.source) + if matched: return # Now we simply try to choose a helpful failure message if first_matching is not None: @@ -340,7 +363,6 @@ def __exit__(self, exc_type, exc_value, tb): else: self._raiseFailure("{} not triggered".format(exc_name)) - class _AssertNotWarnsContext(_AssertWarnsContext): def __exit__(self, exc_type, exc_value, tb): @@ -851,7 +873,7 @@ def _assertNotWarns(self, expected_warning, *args, **kwargs): context = _AssertNotWarnsContext(expected_warning, self) return context.handle('_assertNotWarns', args, kwargs) - def assertLogs(self, logger=None, level=None): + def assertLogs(self, logger=None, level=None, formatter=None): """Fail unless a log message of level *level* or higher is emitted on *logger_name* or its children. If omitted, *level* defaults to INFO and *logger* defaults to the root logger. @@ -863,6 +885,8 @@ def assertLogs(self, logger=None, level=None): `records` attribute will be a list of the corresponding LogRecord objects. + Optionally supply `formatter` to control how messages are formatted. + Example:: with self.assertLogs('foo', level='INFO') as cm: @@ -873,7 +897,7 @@ def assertLogs(self, logger=None, level=None): """ # Lazy import to avoid importing logging if it is not needed. from ._log import _AssertLogsContext - return _AssertLogsContext(self, logger, level, no_logs=False) + return _AssertLogsContext(self, logger, level, no_logs=False, formatter=formatter) def assertNoLogs(self, logger=None, level=None): """ Fail unless no log messages of level *level* or higher are emitted diff --git a/Lib/unittest/loader.py b/Lib/unittest/loader.py index a52950dad22..697520246f0 100644 --- a/Lib/unittest/loader.py +++ b/Lib/unittest/loader.py @@ -1,5 +1,6 @@ """Loading unittests.""" +import inspect import os import re import sys @@ -84,8 +85,10 @@ def loadTestsFromTestCase(self, testCaseClass): raise TypeError("Test cases should not be derived from " "TestSuite. Maybe you meant to derive from " "TestCase?") - if testCaseClass in (case.TestCase, case.FunctionTestCase): - # We don't load any tests from base types that should not be loaded. + if (testCaseClass in (case.TestCase, case.FunctionTestCase) or + inspect.isabstract(testCaseClass)): + # We don't load any tests from base types that should not be loaded, + # and abstract base classes that can't be instantiated testCaseNames = [] else: testCaseNames = self.getTestCaseNames(testCaseClass) @@ -103,6 +106,7 @@ def loadTestsFromModule(self, module, *, pattern=None): isinstance(obj, type) and issubclass(obj, case.TestCase) and obj not in (case.TestCase, case.FunctionTestCase) + and not inspect.isabstract(obj) ): tests.append(self.loadTestsFromTestCase(obj)) @@ -181,6 +185,9 @@ def loadTestsFromName(self, name, module=None): elif (isinstance(obj, types.FunctionType) and isinstance(parent, type) and issubclass(parent, case.TestCase)): + if inspect.isabstract(parent): + raise TypeError( + "Cannot instantiate abstract test case %s" % parent.__name__) name = parts[-1] inst = parent(name) # static methods follow a different path diff --git a/Lib/unittest/main.py b/Lib/unittest/main.py index be99d93c78c..6eeebf9657a 100644 --- a/Lib/unittest/main.py +++ b/Lib/unittest/main.py @@ -172,7 +172,7 @@ def _getParentArgParser(self): help='Show local variables in tracebacks') parser.add_argument('--durations', dest='durations', type=int, default=None, metavar="N", - help='Show the N slowest test cases (N=0 for all)') + help='Show the `N` slowest test cases (`N=0` for all)') if self.failfast is None: parser.add_argument('-f', '--failfast', dest='failfast', action='store_true', @@ -181,12 +181,12 @@ def _getParentArgParser(self): if self.catchbreak is None: parser.add_argument('-c', '--catch', dest='catchbreak', action='store_true', - help='Catch Ctrl-C and display results so far') + help='Catch `Ctrl-C` and display results so far') self.catchbreak = False if self.buffer is None: parser.add_argument('-b', '--buffer', dest='buffer', action='store_true', - help='Buffer stdout and stderr during tests') + help='Buffer `stdout` and `stderr` during tests') self.buffer = False if self.testNamePatterns is None: parser.add_argument('-k', dest='testNamePatterns', @@ -197,7 +197,7 @@ def _getParentArgParser(self): return parser def _getMainArgParser(self, parent): - parser = argparse.ArgumentParser(parents=[parent], color=True) + parser = argparse.ArgumentParser(parents=[parent]) parser.prog = self.progName parser.print_help = self._print_help @@ -208,16 +208,16 @@ def _getMainArgParser(self, parent): return parser def _getDiscoveryArgParser(self, parent): - parser = argparse.ArgumentParser(parents=[parent], color=True) + parser = argparse.ArgumentParser(parents=[parent]) parser.prog = '%s discover' % self.progName parser.epilog = ('For test discovery all test modules must be ' 'importable from the top level directory of the ' 'project.') parser.add_argument('-s', '--start-directory', dest='start', - help="Directory to start discovery ('.' default)") + help="Directory to start discovery (`.` default)") parser.add_argument('-p', '--pattern', dest='pattern', - help="Pattern to match tests ('test*.py' default)") + help="Pattern to match tests (`test*.py` default)") parser.add_argument('-t', '--top-level-directory', dest='top', help='Top level directory of project (defaults to ' 'start directory)') diff --git a/Lib/unittest/suite.py b/Lib/unittest/suite.py index 6f45b6fe5f6..ae9ca2d615d 100644 --- a/Lib/unittest/suite.py +++ b/Lib/unittest/suite.py @@ -223,6 +223,11 @@ def _handleModuleFixture(self, test, result): if result._moduleSetUpFailed: try: case.doModuleCleanups() + except ExceptionGroup as eg: + for e in eg.exceptions: + self._createClassOrModuleLevelException(result, e, + 'setUpModule', + currentModule) except Exception as e: self._createClassOrModuleLevelException(result, e, 'setUpModule', @@ -235,15 +240,15 @@ def _createClassOrModuleLevelException(self, result, exc, method_name, errorName = f'{method_name} ({parent})' self._addClassOrModuleLevelException(result, exc, errorName, info) - def _addClassOrModuleLevelException(self, result, exception, errorName, + def _addClassOrModuleLevelException(self, result, exc, errorName, info=None): error = _ErrorHolder(errorName) addSkip = getattr(result, 'addSkip', None) - if addSkip is not None and isinstance(exception, case.SkipTest): - addSkip(error, str(exception)) + if addSkip is not None and isinstance(exc, case.SkipTest): + addSkip(error, str(exc)) else: if not info: - result.addError(error, sys.exc_info()) + result.addError(error, (type(exc), exc, exc.__traceback__)) else: result.addError(error, info) @@ -273,6 +278,13 @@ def _handleModuleTearDown(self, result): previousModule) try: case.doModuleCleanups() + except ExceptionGroup as eg: + if isinstance(result, _DebugResult): + raise + for e in eg.exceptions: + self._createClassOrModuleLevelException(result, e, + 'tearDownModule', + previousModule) except Exception as e: if isinstance(result, _DebugResult): raise diff --git a/Lib/unittest/util.py b/Lib/unittest/util.py index 050eaed0b3f..0681163c979 100644 --- a/Lib/unittest/util.py +++ b/Lib/unittest/util.py @@ -1,7 +1,6 @@ """Various utility functions.""" from collections import namedtuple, Counter -from os.path import commonprefix __unittest = True @@ -21,13 +20,23 @@ def _shorten(s, prefixlen, suffixlen): s = '%s[%d chars]%s' % (s[:prefixlen], skip, s[len(s) - suffixlen:]) return s +def _common_prefix(m): + if not m: + return "" + s1 = min(m) + s2 = max(m) + for i, c in enumerate(s1): + if c != s2[i]: + return s1[:i] + return s1 + def _common_shorten_repr(*args): args = tuple(map(safe_repr, args)) maxlen = max(map(len, args)) if maxlen <= _MAX_LENGTH: return args - prefix = commonprefix(args) + prefix = _common_prefix(args) prefixlen = len(prefix) common_len = _MAX_LENGTH - \ @@ -54,6 +63,14 @@ def safe_repr(obj, short=False): def strclass(cls): return "%s.%s" % (cls.__module__, cls.__qualname__) +def _dedupe_sorted(lst): + """Remove consecutive duplicate elements from a sorted list.""" + result = [] + for item in lst: + if not result or result[-1] != item: + result.append(item) + return result + def sorted_list_difference(expected, actual): """Finds elements in only one or the other of two, sorted input lists. @@ -89,8 +106,8 @@ def sorted_list_difference(expected, actual): while actual[j] == a: j += 1 except IndexError: - missing.extend(expected[i:]) - unexpected.extend(actual[j:]) + missing.extend(_dedupe_sorted(expected[i:])) + unexpected.extend(_dedupe_sorted(actual[j:])) break return missing, unexpected diff --git a/Lib/urllib/error.py b/Lib/urllib/error.py index a9cd1ecadd6..beb32d0df48 100644 --- a/Lib/urllib/error.py +++ b/Lib/urllib/error.py @@ -18,7 +18,7 @@ class URLError(OSError): # URLError is a sub-type of OSError, but it doesn't share any of - # the implementation. need to override __init__ and __str__. + # the implementation. It overrides __init__ and __str__. # It sets self.args for compatibility with other OSError # subclasses, but args doesn't have the typical format with errno in # slot 0 and strerror in slot 1. This may be better than nothing. diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py index c9f8a33e72c..4247b9a4b07 100644 --- a/Lib/urllib/parse.py +++ b/Lib/urllib/parse.py @@ -97,11 +97,9 @@ def clear_cache(): _byte_quoter_factory.cache_clear() # Helpers for bytes handling -# For 3.2, we deliberately require applications that -# handle improperly quoted URLs to do their own -# decoding and encoding. If valid use cases are -# presented, we may relax this by using latin-1 -# decoding internally for 3.3 +# We deliberately require applications that +# handle improperly quoted URLs to do their +# own decoding and encoding. _implicit_encoding = 'ascii' _implicit_errors = 'strict' @@ -114,7 +112,10 @@ def _encode_result(obj, encoding=_implicit_encoding, def _decode_args(args, encoding=_implicit_encoding, errors=_implicit_errors): - return tuple(x.decode(encoding, errors) if x else '' for x in args) + return tuple(x.decode(encoding, errors) if x + else '' if x is not None + else None + for x in args) def _coerce_args(*args): # Invokes decode if necessary to create str args @@ -122,13 +123,20 @@ def _coerce_args(*args): # an appropriate result coercion function # - noop for str inputs # - encoding function otherwise - str_input = isinstance(args[0], str) - for arg in args[1:]: - # We special-case the empty string to support the - # "scheme=''" default argument to some functions - if arg and isinstance(arg, str) != str_input: - raise TypeError("Cannot mix str and non-str arguments") - if str_input: + str_input = None + for arg in args: + if arg: + if str_input is None: + str_input = isinstance(arg, str) + else: + if isinstance(arg, str) != str_input: + raise TypeError("Cannot mix str and non-str arguments") + if str_input is None: + for arg in args: + if arg is not None: + str_input = isinstance(arg, str) + break + if str_input is not False: return args + (_noop,) return _decode_args(args) + (_encode_result,) @@ -138,7 +146,14 @@ class _ResultMixinStr(object): __slots__ = () def encode(self, encoding='ascii', errors='strict'): - return self._encoded_counterpart(*(x.encode(encoding, errors) for x in self)) + result = self._encoded_counterpart(*(x.encode(encoding, errors) + if x is not None else None + for x in self)) + try: + result._keep_empty = self._keep_empty + except AttributeError: + pass + return result class _ResultMixinBytes(object): @@ -146,7 +161,14 @@ class _ResultMixinBytes(object): __slots__ = () def decode(self, encoding='ascii', errors='strict'): - return self._decoded_counterpart(*(x.decode(encoding, errors) for x in self)) + result = self._decoded_counterpart(*(x.decode(encoding, errors) + if x is not None else None + for x in self)) + try: + result._keep_empty = self._keep_empty + except AttributeError: + pass + return result class _NetlocResultMixinBase(object): @@ -193,6 +215,8 @@ class _NetlocResultMixinStr(_NetlocResultMixinBase, _ResultMixinStr): @property def _userinfo(self): netloc = self.netloc + if netloc is None: + return None, None userinfo, have_info, hostinfo = netloc.rpartition('@') if have_info: username, have_password, password = userinfo.partition(':') @@ -205,6 +229,8 @@ def _userinfo(self): @property def _hostinfo(self): netloc = self.netloc + if netloc is None: + return None, None _, _, hostinfo = netloc.rpartition('@') _, have_open_br, bracketed = hostinfo.partition('[') if have_open_br: @@ -223,6 +249,8 @@ class _NetlocResultMixinBytes(_NetlocResultMixinBase, _ResultMixinBytes): @property def _userinfo(self): netloc = self.netloc + if netloc is None: + return None, None userinfo, have_info, hostinfo = netloc.rpartition(b'@') if have_info: username, have_password, password = userinfo.partition(b':') @@ -235,6 +263,8 @@ def _userinfo(self): @property def _hostinfo(self): netloc = self.netloc + if netloc is None: + return None, None _, _, hostinfo = netloc.rpartition(b'@') _, have_open_br, bracketed = hostinfo.partition(b'[') if have_open_br: @@ -247,11 +277,70 @@ def _hostinfo(self): return hostname, port -_DefragResultBase = namedtuple('_DefragResultBase', 'url fragment') -_SplitResultBase = namedtuple( - '_SplitResultBase', 'scheme netloc path query fragment') -_ParseResultBase = namedtuple( - '_ParseResultBase', 'scheme netloc path params query fragment') +_UNSPECIFIED = sentinel("_UNSPECIFIED", repr="") +_MISSING_AS_NONE_DEFAULT = False + +class _ResultBase: + __slots__ = () + + def __replace__(self, /, **kwargs): + result = super().__replace__(**kwargs) + try: + result._keep_empty = self._keep_empty + except AttributeError: + pass + return result + + def _replace(self, /, **kwargs): + result = super()._replace(**kwargs) + try: + result._keep_empty = self._keep_empty + except AttributeError: + pass + return result + + def __copy__(self): + return self + + def __deepcopy__(self, memo): + return self + + def __getstate__(self): + state = super().__getstate__() + try: + if state[1]['_keep_empty'] == _MISSING_AS_NONE_DEFAULT: + del state[1]['_keep_empty'] + if state == (None, {}): + state = None + except LookupError: + pass + return state + + +class _DefragResultBase(_ResultBase, namedtuple('_DefragResultBase', 'url fragment')): + __slots__ = ('_keep_empty',) + + def geturl(self): + if self.fragment or (self.fragment is not None and + getattr(self, '_keep_empty', _MISSING_AS_NONE_DEFAULT)): + return self.url + self._HASH + self.fragment + else: + return self.url + +class _SplitResultBase(_ResultBase, namedtuple( + '_SplitResultBase', 'scheme netloc path query fragment')): + __slots__ = ('_keep_empty',) + + def geturl(self): + return urlunsplit(self) + +class _ParseResultBase(_ResultBase, namedtuple( + '_ParseResultBase', 'scheme netloc path params query fragment')): + __slots__ = ('_keep_empty',) + + def geturl(self): + return urlunparse(self) + _DefragResultBase.__doc__ = """ DefragResult(url, fragment) @@ -322,40 +411,24 @@ def _hostinfo(self): # Structured result objects for string data class DefragResult(_DefragResultBase, _ResultMixinStr): __slots__ = () - def geturl(self): - if self.fragment: - return self.url + '#' + self.fragment - else: - return self.url + _HASH = '#' class SplitResult(_SplitResultBase, _NetlocResultMixinStr): __slots__ = () - def geturl(self): - return urlunsplit(self) class ParseResult(_ParseResultBase, _NetlocResultMixinStr): __slots__ = () - def geturl(self): - return urlunparse(self) # Structured result objects for bytes data class DefragResultBytes(_DefragResultBase, _ResultMixinBytes): __slots__ = () - def geturl(self): - if self.fragment: - return self.url + b'#' + self.fragment - else: - return self.url + _HASH = b'#' class SplitResultBytes(_SplitResultBase, _NetlocResultMixinBytes): __slots__ = () - def geturl(self): - return urlunsplit(self) class ParseResultBytes(_ParseResultBase, _NetlocResultMixinBytes): __slots__ = () - def geturl(self): - return urlunparse(self) # Set up the encode/decode result pairs def _fix_result_transcoding(): @@ -371,7 +444,7 @@ def _fix_result_transcoding(): _fix_result_transcoding() del _fix_result_transcoding -def urlparse(url, scheme='', allow_fragments=True): +def urlparse(url, scheme=None, allow_fragments=True, *, missing_as_none=_MISSING_AS_NONE_DEFAULT): """Parse a URL into 6 components: :///;?# @@ -394,23 +467,33 @@ def urlparse(url, scheme='', allow_fragments=True): urlsplit() should generally be used instead of urlparse(). """ url, scheme, _coerce_result = _coerce_args(url, scheme) + if url is None: + url = '' scheme, netloc, url, params, query, fragment = _urlparse(url, scheme, allow_fragments) - result = ParseResult(scheme or '', netloc or '', url, params or '', query or '', fragment or '') - return _coerce_result(result) + if not missing_as_none: + if scheme is None: scheme = '' + if netloc is None: netloc = '' + if params is None: params = '' + if query is None: query = '' + if fragment is None: fragment = '' + result = ParseResult(scheme, netloc, url, params, query, fragment) + result = _coerce_result(result) + result._keep_empty = missing_as_none + return result def _urlparse(url, scheme=None, allow_fragments=True): scheme, netloc, url, query, fragment = _urlsplit(url, scheme, allow_fragments) if (scheme or '') in uses_params and ';' in url: - url, params = _splitparams(url, allow_none=True) + url, params = _splitparams(url, missing_as_none=True) else: params = None return (scheme, netloc, url, params, query, fragment) -def _splitparams(url, allow_none=False): +def _splitparams(url, missing_as_none=False): if '/' in url: i = url.find(';', url.rfind('/')) if i < 0: - return url, None if allow_none else '' + return url, None if missing_as_none else '' else: i = url.find(';') return url[:i], url[i+1:] @@ -472,7 +555,7 @@ def _check_bracketed_host(hostname): # typed=True avoids BytesWarnings being emitted during cache key # comparison since this API supports both bytes and str input. @functools.lru_cache(typed=True) -def urlsplit(url, scheme='', allow_fragments=True): +def urlsplit(url, scheme=None, allow_fragments=True, *, missing_as_none=_MISSING_AS_NONE_DEFAULT): """Parse a URL into 5 components: :///?# @@ -494,9 +577,18 @@ def urlsplit(url, scheme='', allow_fragments=True): """ url, scheme, _coerce_result = _coerce_args(url, scheme) + if url is None: + url = '' scheme, netloc, url, query, fragment = _urlsplit(url, scheme, allow_fragments) - v = SplitResult(scheme or '', netloc or '', url, query or '', fragment or '') - return _coerce_result(v) + if not missing_as_none: + if scheme is None: scheme = '' + if netloc is None: netloc = '' + if query is None: query = '' + if fragment is None: fragment = '' + result = SplitResult(scheme, netloc, url, query, fragment) + result = _coerce_result(result) + result._keep_empty = missing_as_none + return result def _urlsplit(url, scheme=None, allow_fragments=True): # Only lstrip url as some applications rely on preserving trailing space. @@ -532,38 +624,61 @@ def _urlsplit(url, scheme=None, allow_fragments=True): _checknetloc(netloc) return (scheme, netloc, url, query, fragment) -def urlunparse(components): +def urlunparse(components, *, keep_empty=_UNSPECIFIED): """Put a parsed URL back together again. This may result in a slightly different, but equivalent URL, if the URL that was parsed originally had redundant delimiters, e.g. a ? with an empty query - (the draft states that these are equivalent).""" + (the draft states that these are equivalent) and keep_empty is false + or components is the result of the urlparse() call with + missing_as_none=False.""" scheme, netloc, url, params, query, fragment, _coerce_result = ( _coerce_args(*components)) - if not netloc: - if scheme and scheme in uses_netloc and (not url or url[:1] == '/'): - netloc = '' - else: - netloc = None - if params: + if keep_empty is _UNSPECIFIED: + keep_empty = getattr(components, '_keep_empty', _MISSING_AS_NONE_DEFAULT) + elif keep_empty and not getattr(components, '_keep_empty', True): + raise ValueError('Cannot distinguish between empty and not defined ' + 'URI components in the result of parsing URL with ' + 'missing_as_none=False') + if not keep_empty: + if not netloc: + if scheme and scheme in uses_netloc and (not url or url[:1] == '/'): + netloc = '' + else: + netloc = None + if not scheme: scheme = None + if not params: params = None + if not query: query = None + if not fragment: fragment = None + if params is not None: url = "%s;%s" % (url, params) - return _coerce_result(_urlunsplit(scheme or None, netloc, url, - query or None, fragment or None)) + return _coerce_result(_urlunsplit(scheme, netloc, url, query, fragment)) -def urlunsplit(components): +def urlunsplit(components, *, keep_empty=_UNSPECIFIED): """Combine the elements of a tuple as returned by urlsplit() into a complete URL as a string. The data argument can be any five-item iterable. This may result in a slightly different, but equivalent URL, if the URL that was parsed originally had unnecessary delimiters (for example, a ? with an - empty query; the RFC states that these are equivalent).""" + empty query; the RFC states that these are equivalent) and keep_empty + is false or components is the result of the urlsplit() call with + missing_as_none=False.""" scheme, netloc, url, query, fragment, _coerce_result = ( _coerce_args(*components)) - if not netloc: - if scheme and scheme in uses_netloc and (not url or url[:1] == '/'): - netloc = '' - else: - netloc = None - return _coerce_result(_urlunsplit(scheme or None, netloc, url, - query or None, fragment or None)) + if keep_empty is _UNSPECIFIED: + keep_empty = getattr(components, '_keep_empty', _MISSING_AS_NONE_DEFAULT) + elif keep_empty and not getattr(components, '_keep_empty', True): + raise ValueError('Cannot distinguish between empty and not defined ' + 'URI components in the result of parsing URL with ' + 'missing_as_none=False') + if not keep_empty: + if not netloc: + if scheme and scheme in uses_netloc and (not url or url[:1] == '/'): + netloc = '' + else: + netloc = None + if not scheme: scheme = None + if not query: query = None + if not fragment: fragment = None + return _coerce_result(_urlunsplit(scheme, netloc, url, query, fragment)) def _urlunsplit(scheme, netloc, url, query, fragment): if netloc is not None: @@ -651,21 +766,24 @@ def urljoin(base, url, allow_fragments=True): resolved_path) or '/', query, fragment)) -def urldefrag(url): +def urldefrag(url, *, missing_as_none=_MISSING_AS_NONE_DEFAULT): """Removes any existing fragment from URL. Returns a tuple of the defragmented URL and the fragment. If the URL contained no fragments, the second element is the - empty string. + empty string or None if missing_as_none is True. """ url, _coerce_result = _coerce_args(url) if '#' in url: s, n, p, q, frag = _urlsplit(url) defrag = _urlunsplit(s, n, p, q, None) else: - frag = '' + frag = None defrag = url - return _coerce_result(DefragResult(defrag, frag or '')) + if not missing_as_none and frag is None: frag = '' + result = _coerce_result(DefragResult(defrag, frag)) + result._keep_empty = missing_as_none + return result _hexdig = '0123456789ABCDEFabcdef' _hextobyte = None @@ -1066,7 +1184,7 @@ def urlencode(query, doseq=False, safe='', encoding=None, errors=None, else: try: # Is this a sufficient test for sequence-ness? - x = len(v) + len(v) except TypeError: # not a sequence v = quote_via(str(v), safe, encoding, errors) @@ -1117,7 +1235,7 @@ def unwrap(url): def splittype(url): warnings.warn("urllib.parse.splittype() is deprecated as of 3.8, " - "use urllib.parse.urlparse() instead", + "use urllib.parse.urlsplit() instead", DeprecationWarning, stacklevel=2) return _splittype(url) @@ -1138,7 +1256,7 @@ def _splittype(url): def splithost(url): warnings.warn("urllib.parse.splithost() is deprecated as of 3.8, " - "use urllib.parse.urlparse() instead", + "use urllib.parse.urlsplit() instead", DeprecationWarning, stacklevel=2) return _splithost(url) @@ -1161,7 +1279,7 @@ def _splithost(url): def splituser(host): warnings.warn("urllib.parse.splituser() is deprecated as of 3.8, " - "use urllib.parse.urlparse() instead", + "use urllib.parse.urlsplit() instead", DeprecationWarning, stacklevel=2) return _splituser(host) @@ -1174,7 +1292,7 @@ def _splituser(host): def splitpasswd(user): warnings.warn("urllib.parse.splitpasswd() is deprecated as of 3.8, " - "use urllib.parse.urlparse() instead", + "use urllib.parse.urlsplit() instead", DeprecationWarning, stacklevel=2) return _splitpasswd(user) @@ -1187,7 +1305,7 @@ def _splitpasswd(user): def splitport(host): warnings.warn("urllib.parse.splitport() is deprecated as of 3.8, " - "use urllib.parse.urlparse() instead", + "use urllib.parse.urlsplit() instead", DeprecationWarning, stacklevel=2) return _splitport(host) @@ -1210,7 +1328,7 @@ def _splitport(host): def splitnport(host, defport=-1): warnings.warn("urllib.parse.splitnport() is deprecated as of 3.8, " - "use urllib.parse.urlparse() instead", + "use urllib.parse.urlsplit() instead", DeprecationWarning, stacklevel=2) return _splitnport(host, defport) @@ -1234,7 +1352,7 @@ def _splitnport(host, defport=-1): def splitquery(url): warnings.warn("urllib.parse.splitquery() is deprecated as of 3.8, " - "use urllib.parse.urlparse() instead", + "use urllib.parse.urlsplit() instead", DeprecationWarning, stacklevel=2) return _splitquery(url) @@ -1249,7 +1367,7 @@ def _splitquery(url): def splittag(url): warnings.warn("urllib.parse.splittag() is deprecated as of 3.8, " - "use urllib.parse.urlparse() instead", + "use urllib.parse.urlsplit() instead", DeprecationWarning, stacklevel=2) return _splittag(url) @@ -1264,7 +1382,7 @@ def _splittag(url): def splitattr(url): warnings.warn("urllib.parse.splitattr() is deprecated as of 3.8, " - "use urllib.parse.urlparse() instead", + "use urllib.parse.urlsplit() instead", DeprecationWarning, stacklevel=2) return _splitattr(url) diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index a8f40100b7b..9fa92659a25 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -415,6 +415,8 @@ def add_handler(self, handler): continue i = meth.find("_") + if i < 1: + continue protocol = meth[:i] condition = meth[i+1:] @@ -813,16 +815,17 @@ def add_password(self, realm, uri, user, passwd): self.passwd[realm] = {} for default_port in True, False: reduced_uri = tuple( - self.reduce_uri(u, default_port) for u in uri) + self._reduce_uri_with_scheme(u, default_port) for u in uri) self.passwd[realm][reduced_uri] = (user, passwd) def find_user_password(self, realm, authuri): domains = self.passwd.get(realm, {}) for default_port in True, False: - reduced_authuri = self.reduce_uri(authuri, default_port) + reduced_authuri = self._reduce_uri_with_scheme( + authuri, default_port) for uris, authinfo in domains.items(): for uri in uris: - if self.is_suburi(uri, reduced_authuri): + if self._is_suburi_with_scheme(uri, reduced_authuri): return authinfo return None, None @@ -849,6 +852,17 @@ def reduce_uri(self, uri, default_port=True): authority = "%s:%d" % (host, dport) return authority, path + def _reduce_uri_with_scheme(self, uri, default_port=True): + parts = urlsplit(uri) + scheme = parts[0] if parts[1] else None + return (scheme or None, *self.reduce_uri(uri, default_port)) + + def _is_suburi_with_scheme(self, base, test): + if (base[0] is not None and test[0] is not None and + base[0] != test[0]): + return False + return self.is_suburi(base[1:], test[1:]) + def is_suburi(self, base, test): """Check if test is below base in a URI tree @@ -894,14 +908,15 @@ def update_authenticated(self, uri, is_authenticated=False): for default_port in True, False: for u in uri: - reduced_uri = self.reduce_uri(u, default_port) + reduced_uri = self._reduce_uri_with_scheme(u, default_port) self.authenticated[reduced_uri] = is_authenticated def is_authenticated(self, authuri): for default_port in True, False: - reduced_authuri = self.reduce_uri(authuri, default_port) + reduced_authuri = self._reduce_uri_with_scheme( + authuri, default_port) for uri in self.authenticated: - if self.is_suburi(uri, reduced_authuri): + if self._is_suburi_with_scheme(uri, reduced_authuri): return self.authenticated[uri] diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py index 97aad6eea50..ec8b544a6b0 100644 --- a/Lib/webbrowser.py +++ b/Lib/webbrowser.py @@ -1,7 +1,8 @@ """Interfaces for launching and remotely controlling web browsers.""" -# Maintained by Georg Brandl. +import builtins # because we override open import os +lazy import plistlib import shlex import shutil import sys @@ -492,10 +493,15 @@ def register_standard_browsers(): _tryorder = [] if sys.platform == 'darwin': - register("MacOSX", None, MacOSXOSAScript('default')) - register("chrome", None, MacOSXOSAScript('google chrome')) - register("firefox", None, MacOSXOSAScript('firefox')) - register("safari", None, MacOSXOSAScript('safari')) + register("MacOS", None, MacOS('default')) + register("MacOSX", None, MacOS('default')) # backward compat alias + register("chrome", None, MacOS('google chrome')) + register("chromium", None, MacOS('chromium')) + register("firefox", None, MacOS('firefox')) + register("safari", None, MacOS('safari')) + register("opera", None, MacOS('opera')) + register("microsoft-edge", None, MacOS('microsoft edge')) + register("brave", None, MacOS('brave browser')) # macOS can use below Unix support (but we prefer using the macOS # specific stuff) @@ -614,8 +620,80 @@ def open(self, url, new=0, autoraise=True): # if sys.platform == 'darwin': + def _macos_default_browser_bundle_id(): + """Return the bundle ID of the default web browser. + + Reads the LaunchServices preferences file that macOS maintains + when the user sets a default browser. Returns 'com.apple.Safari' + if the file is absent or no https handler is recorded, because on + a fresh macOS installation Safari is the default browser and the + LaunchServices plist is not written until the user explicitly + changes their default browser. + """ + plist = os.path.expanduser( + '~/Library/Preferences/com.apple.LaunchServices/' + 'com.apple.launchservices.secure.plist' + ) + try: + with builtins.open(plist, 'rb') as f: + data = plistlib.load(f) + for handler in data.get('LSHandlers', []): + if handler.get('LSHandlerURLScheme') == 'https': + return (handler.get('LSHandlerRoleAll') + or handler.get('LSHandlerRoleViewer')) + except (OSError, KeyError, ValueError): + pass + return 'com.apple.Safari' + + class MacOS(BaseBrowser): + """Launcher class for macOS browsers, using /usr/bin/open. + + For http/https URLs with the default browser, /usr/bin/open is called + directly; macOS routes these to the registered browser. + + For all other URL schemes (e.g. file://) and for named browsers, + /usr/bin/open -b is used so that the URL is always passed + to a browser application rather than dispatched by the OS file handler. + This prevents file injection attacks where a file:// URL pointing to an + executable bundle could otherwise be launched by the OS. + + Named browsers with known bundle IDs use -b; unknown names fall back + to -a. + """ + + _BUNDLE_IDS = { + 'google chrome': 'com.google.Chrome', + 'firefox': 'org.mozilla.firefox', + 'safari': 'com.apple.Safari', + 'chromium': 'org.chromium.Chromium', + 'opera': 'com.operasoftware.Opera', + 'microsoft edge': 'com.microsoft.edgemac', + 'brave browser': 'com.brave.Browser', + } + + def open(self, url, new=0, autoraise=True): + sys.audit("webbrowser.open", url) + self._check_url(url) + if self.name == 'default': + proto, sep, _ = url.partition(':') + if sep and proto.lower() in {'http', 'https'}: + cmd = ['/usr/bin/open', url] + else: + bundle_id = _macos_default_browser_bundle_id() + cmd = ['/usr/bin/open', '-b', bundle_id, url] + else: + bundle_id = self._BUNDLE_IDS.get(self.name.lower()) + if bundle_id: + cmd = ['/usr/bin/open', '-b', bundle_id, url] + else: + cmd = ['/usr/bin/open', '-a', self.name, url] + proc = subprocess.run(cmd, stderr=subprocess.DEVNULL) + return proc.returncode == 0 + class MacOSXOSAScript(BaseBrowser): def __init__(self, name='default'): + import warnings + warnings._deprecated("webbrowser.MacOSXOSAScript", remove=(3, 17)) super().__init__(name) def open(self, url, new=0, autoraise=True): @@ -734,7 +812,7 @@ def open(self, url, new=0, autoraise=True): def parse_args(arg_list: list[str] | None): import argparse parser = argparse.ArgumentParser( - description="Open URL in a web browser.", color=True, + description="Open URL in a web browser.", ) parser.add_argument("url", help="URL to open") diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index f8fe89f6e13..b82862deea7 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -69,7 +69,8 @@ def read_environ(): # Python 3's http.server.CGIHTTPRequestHandler decodes # using the urllib.unquote default of UTF-8, amongst other - # issues. + # issues. While the CGI handler is removed in 3.15, this + # is kept for legacy reasons. elif ( software.startswith('simplehttp/') and 'python/3' in software diff --git a/Lib/wsgiref/simple_server.py b/Lib/wsgiref/simple_server.py index a0f2397fcf0..31efd8c9bae 100644 --- a/Lib/wsgiref/simple_server.py +++ b/Lib/wsgiref/simple_server.py @@ -16,11 +16,10 @@ from wsgiref.handlers import SimpleHandler from platform import python_implementation -__version__ = "0.2" __all__ = ['WSGIServer', 'WSGIRequestHandler', 'demo_app', 'make_server'] -server_version = "WSGIServer/" + __version__ +server_version = "WSGIServer" sys_version = python_implementation() + "/" + sys.version.split()[0] software_version = server_version + ' ' + sys_version @@ -70,7 +69,7 @@ def set_app(self,application): class WSGIRequestHandler(BaseHTTPRequestHandler): - server_version = "WSGIServer/" + __version__ + server_version = "WSGIServer" def get_environ(self): env = self.server.base_environ.copy() @@ -152,6 +151,15 @@ def make_server( return server +def __getattr__(name): + if name == "__version__": + from warnings import _deprecated + + _deprecated("__version__", remove=(3, 20)) + return "0.2" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + if __name__ == '__main__': with make_server('', 8000, demo_app) as httpd: sa = httpd.socket.getsockname() diff --git a/Lib/xml/__init__.py b/Lib/xml/__init__.py index bf6d8ddfd04..ecfce1c6ae5 100644 --- a/Lib/xml/__init__.py +++ b/Lib/xml/__init__.py @@ -16,5 +16,6 @@ """ +from .utils import * -__all__ = ["dom", "parsers", "sax", "etree"] +__all__ = ["dom", "parsers", "sax", "etree", "is_valid_name", "is_valid_text"] diff --git a/Lib/xml/dom/pulldom.py b/Lib/xml/dom/pulldom.py index 913141cd7ef..9d33d83458e 100644 --- a/Lib/xml/dom/pulldom.py +++ b/Lib/xml/dom/pulldom.py @@ -1,3 +1,5 @@ +"""Support for building partial DOM trees from SAX events.""" + import xml.sax import xml.sax.handler @@ -11,6 +13,10 @@ CHARACTERS = "CHARACTERS" class PullDOM(xml.sax.ContentHandler): + """Content handler which turns SAX events into pull parser events. + + The nodes are created, but they are not added to the tree.""" + _locator = None document = None @@ -202,6 +208,8 @@ def fatalError(self, exception): raise exception class DOMEventStream: + """Stream of the pull parser events.""" + def __init__(self, stream, parser, bufsize): self.stream = stream self.parser = parser @@ -211,6 +219,7 @@ def __init__(self, stream, parser, bufsize): self.reset() def reset(self): + """Discard unread events and prepare for parsing a new document.""" self.pulldom = PullDOM() # This content handler relies on namespace support self.parser.setFeature(xml.sax.handler.feature_namespaces, 1) @@ -226,6 +235,7 @@ def __iter__(self): return self def expandNode(self, node): + """Expand all children of the node into the node.""" event = self.getEvent() parents = [node] while event: @@ -241,6 +251,7 @@ def expandNode(self, node): event = self.getEvent() def getEvent(self): + """Return the next (event, node) tuple, or None at the end.""" # use IncrementalParser interface, so we get the desired # pull effect if not self.pulldom.firstEvent[1]: @@ -274,13 +285,14 @@ def _emit(self): return rc def clear(self): - """clear(): Explicitly release parsing objects""" + """Release the parser and the document.""" self.pulldom.clear() del self.pulldom self.parser = None self.stream = None class SAX2DOM(PullDOM): + """PullDOM which also adds every created node to the tree.""" def startElementNS(self, name, tagName , attrs): PullDOM.startElementNS(self, name, tagName, attrs) @@ -316,6 +328,7 @@ def characters(self, chars): default_bufsize = (2 ** 14) - 20 def parse(stream_or_string, parser=None, bufsize=None): + """Return a DOMEventStream for the given file name or file object.""" if bufsize is None: bufsize = default_bufsize if isinstance(stream_or_string, str): @@ -327,6 +340,7 @@ def parse(stream_or_string, parser=None, bufsize=None): return DOMEventStream(stream, parser, bufsize) def parseString(string, parser=None): + """Return a DOMEventStream for the given string.""" from io import StringIO bufsize = len(string) diff --git a/Lib/xml/etree/ElementPath.py b/Lib/xml/etree/ElementPath.py index dc6bd28c031..de1fd203ff6 100644 --- a/Lib/xml/etree/ElementPath.py +++ b/Lib/xml/etree/ElementPath.py @@ -324,15 +324,22 @@ def select_negated(context, result): index = -1 def select(context, result): parent_map = get_parent_map(context) + cache = {} for elem in result: try: parent = parent_map[elem] + except KeyError: + continue + key = (parent, elem.tag) + if key not in cache: # FIXME: what if the selector is "*" ? - elems = list(parent.findall(elem.tag)) - if elems[index] is elem: - yield elem - except (IndexError, KeyError): - pass + elems = parent.findall(elem.tag) + try: + cache[key] = elems[index] + except IndexError: + cache[key] = None + if cache[key] is elem: + yield elem return select raise SyntaxError("invalid predicate") diff --git a/Lib/xml/sax/expatreader.py b/Lib/xml/sax/expatreader.py index ba3c1e98517..37b1add2848 100644 --- a/Lib/xml/sax/expatreader.py +++ b/Lib/xml/sax/expatreader.py @@ -3,8 +3,6 @@ pyexpat.__version__ == '2.22'. """ -version = "0.20" - from xml.sax._exceptions import * from xml.sax.handler import feature_validation, feature_namespaces from xml.sax.handler import feature_namespace_prefixes @@ -446,6 +444,16 @@ def create_parser(*args, **kwargs): # --- +def __getattr__(name): + if name == "version": + from warnings import _deprecated + + _deprecated("version", remove=(3, 20)) + return "0.20" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + +# --- + if __name__ == "__main__": import xml.sax.saxutils p = create_parser() diff --git a/Lib/xml/sax/handler.py b/Lib/xml/sax/handler.py index 3183c3fe96d..9c2e3af838a 100644 --- a/Lib/xml/sax/handler.py +++ b/Lib/xml/sax/handler.py @@ -9,8 +9,6 @@ $Id$ """ -version = '2.0beta' - #============================================================================ # # HANDLER INTERFACES @@ -385,3 +383,12 @@ def startCDATA(self): def endCDATA(self): """Reports the end of a CDATA marked section.""" + + +def __getattr__(name): + if name == "version": + from warnings import _deprecated + + _deprecated("version", remove=(3, 20)) + return "2.0beta" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/xml/utils.py b/Lib/xml/utils.py new file mode 100644 index 00000000000..532aa224dae --- /dev/null +++ b/Lib/xml/utils.py @@ -0,0 +1,37 @@ +lazy import re as _re + + +def is_valid_name(name): + """Test whether a string is a valid element or attribute name.""" + # https://www.w3.org/TR/xml/#NT-Name + return _re.fullmatch( + # NameStartChar + '[' + ':A-Z_a-z' + '\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF' + '\u200C\u200D' + '\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF' + '\uF900-\uFDCF\uFDF0-\uFFFD\U00010000-\U000EFFFF' + ']' + # NameChar + '[' + r'\-.0-9:A-Z_a-z' + '\xB7' + '\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF' + '\u200C\u200D\u203F\u2040' + '\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF' + '\uF900-\uFDCF\uFDF0-\uFFFD\U00010000-\U000EFFFF' + ']*+', + name) is not None + +# https://www.w3.org/TR/xml/#charsets +_ILLEGAL_XML_CHAR = ( + '[' + '\x00-\x08\x0B\x0C\x0E-\x1F' # C0 controls except TAB, CR and LF + '\uD800-\uDFFF' # the surrogate blocks + '\uFFFE\uFFFF' # special Unicode characters + ']') + +def is_valid_text(data): + """Test whether a string is a sequence of legal XML 1.0 characters.""" + return _re.search(_ILLEGAL_XML_CHAR, data) is None diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 2b6e6163b96..552ab17bd67 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -786,7 +786,16 @@ def __init__(self): self._unconsumed = b'' self.eof = False - def decompress(self, data): + @property + def needs_input(self): + # While the LZMA properties header is still being buffered, more input + # is required; afterwards defer to the wrapped decompressor so a bounded + # decompress() call can be drained across reads. + if self._decomp is None: + return True + return self._decomp.needs_input + + def decompress(self, data, max_length=-1): if self._decomp is None: self._unconsumed += data if len(self._unconsumed) <= 4: @@ -802,7 +811,7 @@ def decompress(self, data): data = self._unconsumed[4 + psize:] del self._unconsumed - result = self._decomp.decompress(data) + result = self._decomp.decompress(data, max_length) self.eof = self._decomp.eof return result @@ -1171,8 +1180,15 @@ def _read1(self, n): data = self._decompressor.unconsumed_tail if n > len(data): data += self._read2(n - len(data)) - else: + elif self._compress_type == ZIP_STORED: data = self._read2(n) + else: + # bzip2/lzma/zstd: a bounded decompress() call may leave input + # buffered inside the decompressor; drain that before reading more. + if getattr(self._decompressor, "needs_input", True): + data = self._read2(n) + else: + data = b'' if self._compress_type == ZIP_STORED: self._eof = self._compress_left <= 0 @@ -1185,8 +1201,17 @@ def _read1(self, n): if self._eof: data += self._decompressor.flush() else: - data = self._decompressor.decompress(data) - self._eof = self._decompressor.eof or self._compress_left <= 0 + # Bound the output of a single decompress() call (mirroring the + # DEFLATE path above) so that a small compressed member cannot + # expand into one unbounded read. + try: + data = self._decompressor.decompress(data, max(n, self.MIN_READ_SIZE)) + except TypeError: + # See MonkeypatchedDecompressorTests in test_core.py + data = self._decompressor.decompress(data) + self._eof = (self._decompressor.eof or + self._compress_left <= 0 and + getattr(self._decompressor, "needs_input", True)) data = data[:self._left] self._left -= len(data) @@ -2242,10 +2267,10 @@ def writepy(self, pathname, basename="", filterfunc=None): basename = name if self.debug: print("Adding package in", pathname, "as", basename) - fname, arcname = self._get_codename(initname[0:-3], basename) + arcname, bytecode = self._get_code(initname[0:-3], basename) if self.debug: print("Adding", arcname) - self.write(fname, arcname) + self.writestr(arcname, bytecode) dirlist = sorted(os.listdir(pathname)) dirlist.remove("__init__.py") # Add all *.py files and package subdirectories @@ -2262,11 +2287,10 @@ def writepy(self, pathname, basename="", filterfunc=None): if self.debug: print('file %r skipped by filterfunc' % path) continue - fname, arcname = self._get_codename(path[0:-3], - basename) + arcname, bytecode = self._get_code(path[0:-3], basename) if self.debug: print("Adding", arcname) - self.write(fname, arcname) + self.writestr(arcname, bytecode) else: # This is NOT a package directory, add its files at top level if self.debug: @@ -2279,108 +2303,65 @@ def writepy(self, pathname, basename="", filterfunc=None): if self.debug: print('file %r skipped by filterfunc' % path) continue - fname, arcname = self._get_codename(path[0:-3], - basename) + arcname, bytecode = self._get_code(path[0:-3], basename) if self.debug: print("Adding", arcname) - self.write(fname, arcname) + self.writestr(arcname, bytecode) else: if pathname[-3:] != ".py": raise RuntimeError( 'Files added with writepy() must end with ".py"') - fname, arcname = self._get_codename(pathname[0:-3], basename) + arcname, bytecode = self._get_code(pathname[0:-3], basename) if self.debug: print("Adding file", arcname) - self.write(fname, arcname) + self.writestr(arcname, bytecode) - def _get_codename(self, pathname, basename): - """Return (filename, archivename) for the path. + def _get_code(self, pathname, basename): + """Return (arcname, bytecode) for the path. - Given a module name path, return the correct file path and - archive name, compiling if necessary. For example, given - /python/lib/string, return (/python/lib/string.pyc, string). + Given a module name path, return the bytecode and archive + name. For example, given /python/lib/string, return + ('string', b''). """ - def _compile(file, optimize=-1): - import py_compile - if self.debug: - print("Compiling", file) - try: - py_compile.compile(file, doraise=True, optimize=optimize) - except py_compile.PyCompileError as err: - print(err.msg) - return False - return True + import importlib._bootstrap_external + import importlib.machinery file_py = pathname + ".py" file_pyc = pathname + ".pyc" - pycache_opt0 = importlib.util.cache_from_source(file_py, optimization='') - pycache_opt1 = importlib.util.cache_from_source(file_py, optimization=1) - pycache_opt2 = importlib.util.cache_from_source(file_py, optimization=2) - if self._optimize == -1: - # legacy mode: use whatever file is present - if (os.path.isfile(file_pyc) and - os.stat(file_pyc).st_mtime >= os.stat(file_py).st_mtime): - # Use .pyc file. - arcname = fname = file_pyc - elif (os.path.isfile(pycache_opt0) and - os.stat(pycache_opt0).st_mtime >= os.stat(file_py).st_mtime): - # Use the __pycache__/*.pyc file, but write it to the legacy pyc - # file name in the archive. - fname = pycache_opt0 - arcname = file_pyc - elif (os.path.isfile(pycache_opt1) and - os.stat(pycache_opt1).st_mtime >= os.stat(file_py).st_mtime): - # Use the __pycache__/*.pyc file, but write it to the legacy pyc - # file name in the archive. - fname = pycache_opt1 - arcname = file_pyc - elif (os.path.isfile(pycache_opt2) and - os.stat(pycache_opt2).st_mtime >= os.stat(file_py).st_mtime): - # Use the __pycache__/*.pyc file, but write it to the legacy pyc - # file name in the archive. - fname = pycache_opt2 - arcname = file_pyc - else: - # Compile py into PEP 3147 pyc file. - if _compile(file_py): - if sys.flags.optimize == 0: - fname = pycache_opt0 - elif sys.flags.optimize == 1: - fname = pycache_opt1 - else: - fname = pycache_opt2 - arcname = file_pyc - else: - fname = arcname = file_py + archivename = os.path.split(file_pyc)[1] + + loader = importlib.machinery.SourceFileLoader('', file_py) + source_bytes = loader.get_data(file_py) + try: + if self.debug: + print("Compiling", file_py) + code = loader.source_to_code(source_bytes, archivename) + except Exception as err: + # Historically, this function prints messages here rather than raising + # (see test_zipfile.test_write_filtered_python_package) + from py_compile import PyCompileError + print(PyCompileError(type(err), err, file_py).msg) + + archivename = os.path.split(file_py)[1] + bytecode = source_bytes else: - # new mode: use given optimization level - if self._optimize == 0: - fname = pycache_opt0 - arcname = file_pyc - else: - arcname = file_pyc - if self._optimize == 1: - fname = pycache_opt1 - elif self._optimize == 2: - fname = pycache_opt2 - else: - msg = "invalid value for 'optimize': {!r}".format(self._optimize) - raise ValueError(msg) - if not (os.path.isfile(fname) and - os.stat(fname).st_mtime >= os.stat(file_py).st_mtime): - if not _compile(file_py, optimize=self._optimize): - fname = arcname = file_py - archivename = os.path.split(arcname)[1] + # Historically this function has used timestamp comparisons, so we + # keep using it until someone makes that specific improvement. + source_stats = loader.path_stats(file_py) + bytecode = importlib._bootstrap_external._code_to_timestamp_pyc( + code, source_stats['mtime'], source_stats['size']) + if basename: archivename = "%s/%s" % (basename, archivename) - return (fname, archivename) + + return archivename, bytecode def main(args=None): import argparse description = 'A simple command-line interface for zipfile module.' - parser = argparse.ArgumentParser(description=description, color=True) + parser = argparse.ArgumentParser(description=description) group = parser.add_mutually_exclusive_group(required=True) group.add_argument('-l', '--list', metavar='', help='Show listing of a zipfile') @@ -2393,7 +2374,7 @@ def main(args=None): group.add_argument('-t', '--test', metavar='', help='Test if a zipfile is valid') parser.add_argument('--metadata-encoding', metavar='', - help='Specify encoding of member names for -l, -e and -t') + help='Specify encoding of member names for `-l`, `-e` and `-t`') args = parser.parse_args(args) encoding = args.metadata_encoding diff --git a/Lib/zipfile/_path/__init__.py b/Lib/zipfile/_path/__init__.py index 80f5d607731..faae4c84cae 100644 --- a/Lib/zipfile/_path/__init__.py +++ b/Lib/zipfile/_path/__init__.py @@ -7,7 +7,7 @@ for more detail. """ -import contextlib +import functools import io import itertools import pathlib @@ -17,6 +17,7 @@ import sys import zipfile +from ._functools import save_method_args from .glob import Translator __all__ = ['Path'] @@ -85,13 +86,12 @@ class InitializedState: Mix-in to save the initialization state for pickling. """ + @save_method_args def __init__(self, *args, **kwargs): - self.__args = args - self.__kwargs = kwargs super().__init__(*args, **kwargs) def __getstate__(self): - return self.__args, self.__kwargs + return self._saved___init__.args, self._saved___init__.kwargs def __setstate__(self, state): args, kwargs = state @@ -180,16 +180,19 @@ class FastLookup(CompleteDirs): """ def namelist(self): - with contextlib.suppress(AttributeError): - return self.__names - self.__names = super().namelist() - return self.__names + return self._namelist + + @functools.cached_property + def _namelist(self): + return super().namelist() def _name_set(self): - with contextlib.suppress(AttributeError): - return self.__lookup - self.__lookup = super()._name_set() - return self.__lookup + return self._name_set_prop + + @functools.cached_property + def _name_set_prop(self): + return super()._name_set() + def _extract_text_encoding(encoding=None, *args, **kwargs): # compute stack level so that the caller of the caller sees any warning. diff --git a/Lib/zipfile/_path/_functools.py b/Lib/zipfile/_path/_functools.py new file mode 100644 index 00000000000..7390be21873 --- /dev/null +++ b/Lib/zipfile/_path/_functools.py @@ -0,0 +1,20 @@ +import collections +import functools + + +# from jaraco.functools 4.0.2 +def save_method_args(method): + """ + Wrap a method such that when it is called, the args and kwargs are + saved on the method. + """ + args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs') # noqa: PYI024 + + @functools.wraps(method) + def wrapper(self, /, *args, **kwargs): + attr_name = '_saved_' + method.__name__ + attr = args_and_kwargs(args, kwargs) + setattr(self, attr_name, attr) + return method(self, *args, **kwargs) + + return wrapper diff --git a/Lib/zoneinfo/_tzpath.py b/Lib/zoneinfo/_tzpath.py index 177d32c35ef..3661c837daa 100644 --- a/Lib/zoneinfo/_tzpath.py +++ b/Lib/zoneinfo/_tzpath.py @@ -177,6 +177,10 @@ def valid_key(fpath): # posixrules is a special symlink-only time zone where it exists, it # should not be included in the output valid_zones.remove("posixrules") + if "localtime" in valid_zones: + # localtime is a special symlink-only time zone where it exists, it + # should not be included in the output + valid_zones.remove("localtime") return valid_zones diff --git a/Lib/zoneinfo/_zoneinfo.py b/Lib/zoneinfo/_zoneinfo.py index 7063eb6a902..48154775568 100644 --- a/Lib/zoneinfo/_zoneinfo.py +++ b/Lib/zoneinfo/_zoneinfo.py @@ -640,11 +640,11 @@ def _parse_tz_str(tz_str): parser_re = re.compile( r""" - (?P[^<0-9:.+-]+|<[a-zA-Z0-9+-]+>) + (?P[a-zA-Z]+|<[a-zA-Z0-9+-]+>) (?: (?P[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?) (?: - (?P[^0-9:.+-]+|<[a-zA-Z0-9+-]+>) + (?P[a-zA-Z]+|<[a-zA-Z0-9+-]+>) (?P[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)? )? # dst )? # stdoff @@ -672,7 +672,8 @@ def _parse_tz_str(tz_str): except ValueError as e: raise ValueError(f"Invalid STD offset in {tz_str}") from e else: - std_offset = 0 + # The STD offset is required + raise ValueError(f"Invalid STD offset in {tz_str}") if dst_abbr is not None: if dst_offset := m.group("dstoff"): @@ -707,7 +708,7 @@ def _parse_dst_start_end(dststr): type = date[:1] if type == "M": n_is_julian = False - m = re.fullmatch(r"M(\d{1,2})\.(\d).(\d)", date, re.ASCII) + m = re.fullmatch(r"M(\d{1,2})\.(\d)\.(\d)", date, re.ASCII) if m is None: raise ValueError(f"Invalid dst start/end date: {dststr}") date_offset = tuple(map(int, m.groups())) @@ -719,6 +720,8 @@ def _parse_dst_start_end(dststr): else: n_is_julian = False + if re.fullmatch(r"\d{1,3}", date, re.ASCII) is None: + raise ValueError(f"Invalid dst start/end date: {dststr}") doy = int(date) offset = _DayOffset(doy, n_is_julian) diff --git a/Parser/Python.asdl b/Parser/Python.asdl new file mode 100644 index 00000000000..2f0b123858f --- /dev/null +++ b/Parser/Python.asdl @@ -0,0 +1,154 @@ +-- ASDL's 4 builtin types are: +-- identifier, int, string, constant + +module Python +{ + mod = Module(stmt* body, type_ignore* type_ignores) + | Interactive(stmt* body) + | Expression(expr body) + | FunctionType(expr* argtypes, expr returns) + + stmt = FunctionDef(identifier name, arguments args, + stmt* body, expr* decorator_list, expr? returns, + string? type_comment, type_param* type_params) + | AsyncFunctionDef(identifier name, arguments args, + stmt* body, expr* decorator_list, expr? returns, + string? type_comment, type_param* type_params) + + | ClassDef(identifier name, + expr* bases, + keyword* keywords, + stmt* body, + expr* decorator_list, + type_param* type_params) + | Return(expr? value) + + | Delete(expr* targets) + | Assign(expr* targets, expr value, string? type_comment) + | TypeAlias(expr name, type_param* type_params, expr value) + | AugAssign(expr target, operator op, expr value) + -- 'simple' indicates that we annotate simple name without parens + | AnnAssign(expr target, expr annotation, expr? value, int simple) + + -- use 'orelse' because else is a keyword in target languages + | For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) + | AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) + | While(expr test, stmt* body, stmt* orelse) + | If(expr test, stmt* body, stmt* orelse) + | With(withitem* items, stmt* body, string? type_comment) + | AsyncWith(withitem* items, stmt* body, string? type_comment) + + | Match(expr subject, match_case* cases) + + | Raise(expr? exc, expr? cause) + | Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) + | TryStar(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) + | Assert(expr test, expr? msg) + + | Import(alias* names, int? is_lazy) + | ImportFrom(identifier? module, alias* names, int? level, int? is_lazy) + + | Global(identifier* names) + | Nonlocal(identifier* names) + | Expr(expr value) + | Pass | Break | Continue + + -- col_offset is the byte offset in the utf8 string the parser uses + attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) + + -- BoolOp() can use left & right? + expr = BoolOp(boolop op, expr* values) + | NamedExpr(expr target, expr value) + | BinOp(expr left, operator op, expr right) + | UnaryOp(unaryop op, expr operand) + | Lambda(arguments args, expr body) + | IfExp(expr test, expr body, expr orelse) + | Dict(expr?* keys, expr* values) + | Set(expr* elts) + | ListComp(expr elt, comprehension* generators) + | SetComp(expr elt, comprehension* generators) + | DictComp(expr key, expr? value, comprehension* generators) + | GeneratorExp(expr elt, comprehension* generators) + -- the grammar constrains where yield expressions can occur + | Await(expr value) + | Yield(expr? value) + | YieldFrom(expr value) + -- need sequences for compare to distinguish between + -- x < 4 < 3 and (x < 4) < 3 + | Compare(expr left, cmpop* ops, expr* comparators) + | Call(expr func, expr* args, keyword* keywords) + | FormattedValue(expr value, int conversion, expr? format_spec) + | Interpolation(expr value, constant str, int conversion, expr? format_spec) + | JoinedStr(expr* values) + | TemplateStr(expr* values) + | Constant(constant value, string? kind) + + -- the following expression can appear in assignment context + | Attribute(expr value, identifier attr, expr_context ctx) + | Subscript(expr value, expr slice, expr_context ctx) + | Starred(expr value, expr_context ctx) + | Name(identifier id, expr_context ctx) + | List(expr* elts, expr_context ctx) + | Tuple(expr* elts, expr_context ctx) + + -- can appear only in Subscript + | Slice(expr? lower, expr? upper, expr? step) + + -- col_offset is the byte offset in the utf8 string the parser uses + attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) + + expr_context = Load | Store | Del + + boolop = And | Or + + operator = Add | Sub | Mult | MatMult | Div | Mod | Pow | LShift + | RShift | BitOr | BitXor | BitAnd | FloorDiv + + unaryop = Invert | Not | UAdd | USub + + cmpop = Eq | NotEq | Lt | LtE | Gt | GtE | Is | IsNot | In | NotIn + + comprehension = (expr target, expr iter, expr* ifs, int is_async) + + excepthandler = ExceptHandler(expr? type, identifier? name, stmt* body) + attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) + + arguments = (arg* posonlyargs, arg* args, arg? vararg, arg* kwonlyargs, + expr?* kw_defaults, arg? kwarg, expr* defaults) + + arg = (identifier arg, expr? annotation, string? type_comment) + attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) + + -- keyword arguments supplied to call (NULL identifier for **kwargs) + keyword = (identifier? arg, expr value) + attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) + + -- import name with optional 'as' alias. + alias = (identifier name, identifier? asname) + attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) + + withitem = (expr context_expr, expr? optional_vars) + + match_case = (pattern pattern, expr? guard, stmt* body) + + pattern = MatchValue(expr value) + | MatchSingleton(constant value) + | MatchSequence(pattern* patterns) + | MatchMapping(expr* keys, pattern* patterns, identifier? rest) + | MatchClass(expr cls, pattern* patterns, identifier* kwd_attrs, pattern* kwd_patterns) + + | MatchStar(identifier? name) + -- The optional "rest" MatchMapping parameter handles capturing extra mapping keys + + | MatchAs(pattern? pattern, identifier? name) + | MatchOr(pattern* patterns) + + attributes (int lineno, int col_offset, int end_lineno, int end_col_offset) + + type_ignore = TypeIgnore(int lineno, string tag) + + type_param = TypeVar(identifier name, expr? bound, expr? default_value) + | ParamSpec(identifier name, expr? default_value) + | TypeVarTuple(identifier name, expr? default_value) + attributes (int lineno, int col_offset, int end_lineno, int end_col_offset) +} diff --git a/Parser/README.md b/Parser/README.md new file mode 100644 index 00000000000..b458094d32f --- /dev/null +++ b/Parser/README.md @@ -0,0 +1,8 @@ +# CPython ASDL test fixtures + +`asdl.py` and `Python.asdl` are copied unchanged from CPython v3.15.0rc3, +commit `8a8eb0b90d40f0e03f8252e9f73cad156e1caca8`. They supply the source-build +fixtures used by the canonical `Lib/test/test_asdl_parser.py` suite. + +Source: https://github.com/python/cpython/tree/v3.15.0rc3/Parser +License: [PSF License](../Lib/PSF-LICENSE) diff --git a/Parser/asdl.py b/Parser/asdl.py new file mode 100644 index 00000000000..f762ed8110d --- /dev/null +++ b/Parser/asdl.py @@ -0,0 +1,399 @@ +#------------------------------------------------------------------------------- +# Parser for ASDL [1] definition files. Reads in an ASDL description and parses +# it into an AST that describes it. +# +# The EBNF we're parsing here: Figure 1 of the paper [1]. Extended to support +# modules and attributes after a product. Words starting with Capital letters +# are terminals. Literal tokens are in "double quotes". Others are +# non-terminals. Id is either TokenId or ConstructorId. +# +# module ::= "module" Id "{" [definitions] "}" +# definitions ::= { TypeId "=" type } +# type ::= product | sum +# product ::= fields ["attributes" fields] +# fields ::= "(" { field, "," } field ")" +# field ::= TypeId { "?" | "*" } [Id] +# sum ::= constructor { "|" constructor } ["attributes" fields] +# constructor ::= ConstructorId [fields] +# +# [1] "The Zephyr Abstract Syntax Description Language" by Wang, et. al. See +# http://asdl.sourceforge.net/ +#------------------------------------------------------------------------------- +from collections import namedtuple +import enum +import re + +__all__ = [ + 'builtin_types', 'parse', 'AST', 'Module', 'Type', 'Constructor', + 'Field', 'Sum', 'Product', 'VisitorBase', 'Check', 'check'] + +# The following classes define nodes into which the ASDL description is parsed. +# Note: this is a "meta-AST". ASDL files (such as Python.asdl) describe the AST +# structure used by a programming language. But ASDL files themselves need to be +# parsed. This module parses ASDL files and uses a simple AST to represent them. +# See the EBNF at the top of the file to understand the logical connection +# between the various node types. + +builtin_types = {'identifier', 'string', 'int', 'constant'} + +class AST: + def __repr__(self): + raise NotImplementedError + +class Module(AST): + def __init__(self, name, dfns): + self.name = name + self.dfns = dfns + self.types = {type.name: type.value for type in dfns} + + def __repr__(self): + return 'Module({0.name}, {0.dfns})'.format(self) + +class Type(AST): + def __init__(self, name, value): + self.name = name + self.value = value + + def __repr__(self): + return 'Type({0.name}, {0.value})'.format(self) + +class Constructor(AST): + def __init__(self, name, fields=None): + self.name = name + self.fields = fields or [] + + def __repr__(self): + return 'Constructor({0.name}, {0.fields})'.format(self) + +class Quantifier(enum.Enum): + OPTIONAL = enum.auto() + SEQUENCE = enum.auto() + +class Field(AST): + def __init__(self, type, name=None, quantifiers=None): + self.type = type + self.name = name + self.seq = False + self.opt = False + self.quantifiers = quantifiers or [] + if len(self.quantifiers) > 0: + self.seq = self.quantifiers[-1] is Quantifier.SEQUENCE + self.opt = self.quantifiers[-1] is Quantifier.OPTIONAL + + def __str__(self): + extra = "" + for mod in self.quantifiers: + if mod is Quantifier.SEQUENCE: + extra += "*" + elif mod is Quantifier.OPTIONAL: + extra += "?" + + return "{}{} {}".format(self.type, extra, self.name) + + def __repr__(self): + if self.quantifiers: + texts = [] + for mod in self.quantifiers: + if mod is Quantifier.SEQUENCE: + texts.append("SEQUENCE") + elif mod is Quantifier.OPTIONAL: + texts.append("OPTIONAL") + extra = ", quantifiers=[{}]".format(", ".join(texts)) + else: + extra = "" + + if self.name is None: + return 'Field({0.type}{1})'.format(self, extra) + else: + return 'Field({0.type}, {0.name}{1})'.format(self, extra) + +class Sum(AST): + def __init__(self, types, attributes=None): + self.types = types + self.attributes = attributes or [] + + def __repr__(self): + if self.attributes: + return 'Sum({0.types}, {0.attributes})'.format(self) + else: + return 'Sum({0.types})'.format(self) + +class Product(AST): + def __init__(self, fields, attributes=None): + self.fields = fields + self.attributes = attributes or [] + + def __repr__(self): + if self.attributes: + return 'Product({0.fields}, {0.attributes})'.format(self) + else: + return 'Product({0.fields})'.format(self) + +# A generic visitor for the meta-AST that describes ASDL. This can be used by +# emitters. Note that this visitor does not provide a generic visit method, so a +# subclass needs to define visit methods from visitModule to as deep as the +# interesting node. +# We also define a Check visitor that makes sure the parsed ASDL is well-formed. + +class VisitorBase(object): + """Generic tree visitor for ASTs.""" + def __init__(self): + self.cache = {} + + def visit(self, obj, *args): + klass = obj.__class__ + meth = self.cache.get(klass) + if meth is None: + methname = "visit" + klass.__name__ + meth = getattr(self, methname, None) + self.cache[klass] = meth + if meth: + try: + meth(obj, *args) + except Exception as e: + print("Error visiting %r: %s" % (obj, e)) + raise + +class Check(VisitorBase): + """A visitor that checks a parsed ASDL tree for correctness. + + Errors are printed and accumulated. + """ + def __init__(self): + super(Check, self).__init__() + self.cons = {} + self.errors = 0 + self.types = {} + + def visitModule(self, mod): + for dfn in mod.dfns: + self.visit(dfn) + + def visitType(self, type): + self.visit(type.value, str(type.name)) + + def visitSum(self, sum, name): + for t in sum.types: + self.visit(t, name) + + def visitConstructor(self, cons, name): + key = str(cons.name) + conflict = self.cons.get(key) + if conflict is None: + self.cons[key] = name + else: + print('Redefinition of constructor {}'.format(key)) + print('Defined in {} and {}'.format(conflict, name)) + self.errors += 1 + for f in cons.fields: + self.visit(f, key) + + def visitField(self, field, name): + key = str(field.type) + l = self.types.setdefault(key, []) + l.append(name) + + def visitProduct(self, prod, name): + for f in prod.fields: + self.visit(f, name) + +def check(mod): + """Check the parsed ASDL tree for correctness. + + Return True if success. For failure, the errors are printed out and False + is returned. + """ + v = Check() + v.visit(mod) + + for t in v.types: + if t not in mod.types and not t in builtin_types: + v.errors += 1 + uses = ", ".join(v.types[t]) + print('Undefined type {}, used in {}'.format(t, uses)) + return not v.errors + +# The ASDL parser itself comes next. The only interesting external interface +# here is the top-level parse function. + +def parse(filename): + """Parse ASDL from the given file and return a Module node describing it.""" + with open(filename, encoding="utf-8") as f: + parser = ASDLParser() + return parser.parse(f.read()) + +# Types for describing tokens in an ASDL specification. +class TokenKind: + """TokenKind is provides a scope for enumerated token kinds.""" + (ConstructorId, TypeId, Equals, Comma, Question, Pipe, Asterisk, + LParen, RParen, LBrace, RBrace) = range(11) + + operator_table = { + '=': Equals, ',': Comma, '?': Question, '|': Pipe, '(': LParen, + ')': RParen, '*': Asterisk, '{': LBrace, '}': RBrace} + +Token = namedtuple('Token', 'kind value lineno') + +class ASDLSyntaxError(Exception): + def __init__(self, msg, lineno=None): + self.msg = msg + self.lineno = lineno or '' + + def __str__(self): + return 'Syntax error on line {0.lineno}: {0.msg}'.format(self) + +def tokenize_asdl(buf): + """Tokenize the given buffer. Yield Token objects.""" + for lineno, line in enumerate(buf.splitlines(), 1): + for m in re.finditer(r'\s*(\w+|--.*|.)', line.strip()): + c = m.group(1) + if c[0].isalpha(): + # Some kind of identifier + if c[0].isupper(): + yield Token(TokenKind.ConstructorId, c, lineno) + else: + yield Token(TokenKind.TypeId, c, lineno) + elif c[:2] == '--': + # Comment + break + else: + # Operators + try: + op_kind = TokenKind.operator_table[c] + except KeyError: + raise ASDLSyntaxError('Invalid operator %s' % c, lineno) + yield Token(op_kind, c, lineno) + +class ASDLParser: + """Parser for ASDL files. + + Create, then call the parse method on a buffer containing ASDL. + This is a simple recursive descent parser that uses tokenize_asdl for the + lexing. + """ + def __init__(self): + self._tokenizer = None + self.cur_token = None + + def parse(self, buf): + """Parse the ASDL in the buffer and return an AST with a Module root. + """ + self._tokenizer = tokenize_asdl(buf) + self._advance() + return self._parse_module() + + def _parse_module(self): + if self._at_keyword('module'): + self._advance() + else: + raise ASDLSyntaxError( + 'Expected "module" (found {})'.format(self.cur_token.value), + self.cur_token.lineno) + name = self._match(self._id_kinds) + self._match(TokenKind.LBrace) + defs = self._parse_definitions() + self._match(TokenKind.RBrace) + return Module(name, defs) + + def _parse_definitions(self): + defs = [] + while self.cur_token.kind == TokenKind.TypeId: + typename = self._advance() + self._match(TokenKind.Equals) + type = self._parse_type() + defs.append(Type(typename, type)) + return defs + + def _parse_type(self): + if self.cur_token.kind == TokenKind.LParen: + # If we see a (, it's a product + return self._parse_product() + else: + # Otherwise it's a sum. Look for ConstructorId + sumlist = [Constructor(self._match(TokenKind.ConstructorId), + self._parse_optional_fields())] + while self.cur_token.kind == TokenKind.Pipe: + # More constructors + self._advance() + sumlist.append(Constructor( + self._match(TokenKind.ConstructorId), + self._parse_optional_fields())) + return Sum(sumlist, self._parse_optional_attributes()) + + def _parse_product(self): + return Product(self._parse_fields(), self._parse_optional_attributes()) + + def _parse_fields(self): + fields = [] + self._match(TokenKind.LParen) + while self.cur_token.kind == TokenKind.TypeId: + typename = self._advance() + quantifiers = self._parse_optional_field_quantifier() + id = (self._advance() if self.cur_token.kind in self._id_kinds + else None) + fields.append(Field(typename, id, quantifiers=quantifiers)) + if self.cur_token.kind == TokenKind.RParen: + break + elif self.cur_token.kind == TokenKind.Comma: + self._advance() + self._match(TokenKind.RParen) + return fields + + def _parse_optional_fields(self): + if self.cur_token.kind == TokenKind.LParen: + return self._parse_fields() + else: + return None + + def _parse_optional_attributes(self): + if self._at_keyword('attributes'): + self._advance() + return self._parse_fields() + else: + return None + + def _parse_optional_field_quantifier(self): + quantifiers = [] + while self.cur_token.kind in (TokenKind.Asterisk, TokenKind.Question): + if self.cur_token.kind == TokenKind.Asterisk: + quantifiers.append(Quantifier.SEQUENCE) + elif self.cur_token.kind == TokenKind.Question: + quantifiers.append(Quantifier.OPTIONAL) + self._advance() + return quantifiers + + def _advance(self): + """ Return the value of the current token and read the next one into + self.cur_token. + """ + cur_val = None if self.cur_token is None else self.cur_token.value + try: + self.cur_token = next(self._tokenizer) + except StopIteration: + self.cur_token = None + return cur_val + + _id_kinds = (TokenKind.ConstructorId, TokenKind.TypeId) + + def _match(self, kind): + """The 'match' primitive of RD parsers. + + * Verifies that the current token is of the given kind (kind can + be a tuple, in which the kind must match one of its members). + * Returns the value of the current token + * Reads in the next token + """ + if (isinstance(kind, tuple) and self.cur_token.kind in kind or + self.cur_token.kind == kind + ): + value = self.cur_token.value + self._advance() + return value + else: + raise ASDLSyntaxError( + 'Unmatched {} (found {})'.format(kind, self.cur_token.kind), + self.cur_token.lineno) + + def _at_keyword(self, keyword): + return (self.cur_token.kind == TokenKind.TypeId and + self.cur_token.value == keyword) diff --git a/build.rs b/build.rs index d6597fcba1b..eb4f92cd0f6 100644 --- a/build.rs +++ b/build.rs @@ -10,6 +10,20 @@ fn main() { println!("cargo:rustc-link-arg-bin=rustpython=-Wl,-export_dynamic"); } "windows" => { + if capi_enabled { + println!("cargo:rerun-if-changed=capi-exports.def"); + let exports = + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("capi-exports.def"); + let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap(); + match target_env.as_str() { + "msvc" => println!( + "cargo:rustc-link-arg-bin=rustpython=/DEF:{}", + exports.display() + ), + "gnu" => println!("cargo:rustc-link-arg-bin=rustpython={}", exports.display()), + _ => panic!("Unsupported Windows C API linker environment: {target_env}"), + } + } println!("cargo:rerun-if-changed=logo.ico"); let mut res = winresource::WindowsResource::new(); if std::path::Path::new("logo.ico").exists() { diff --git a/capi-exports.def b/capi-exports.def new file mode 100644 index 00000000000..b07cda3bfd1 --- /dev/null +++ b/capi-exports.def @@ -0,0 +1,516 @@ +; Generated by scripts/generate_capi_exports.py; do not edit by hand. +; CPython v3.15.0: d220409a13b77de7a6c30abe16ea90db01528edc +; Misc/stable_abi.toml SHA-256: eab726f2482c71e67a23ea04aaa9c5a11d565bee4472320f813262f273c1e966 +; Implemented Stable ABI functions/data, including ABI-only names. +; Non-Stable ABI compatibility export: PyFrame_GetLocals. +EXPORTS + PyABIInfo_Check + PyAIter_Check + PyBool_FromLong + PyByteArray_AsString + PyByteArray_FromObject + PyByteArray_FromStringAndSize + PyByteArray_Resize + PyByteArray_Size + PyBytes_AsString + PyBytes_AsStringAndSize + PyBytes_FromFormat + PyBytes_FromFormatV + PyBytes_FromObject + PyBytes_FromString + PyBytes_FromStringAndSize + PyBytes_Size + PyCFunction_Call + PyCFunction_New + PyCFunction_NewEx + PyCMethod_New + PyCallIter_New + PyCallable_Check + PyCapsule_GetContext + PyCapsule_GetName + PyCapsule_GetPointer + PyCapsule_Import + PyCapsule_IsValid + PyCapsule_New + PyCapsule_SetContext + PyCapsule_SetPointer + PyCodec_BackslashReplaceErrors + PyCodec_Decode + PyCodec_Decoder + PyCodec_Encode + PyCodec_Encoder + PyCodec_IgnoreErrors + PyCodec_IncrementalDecoder + PyCodec_IncrementalEncoder + PyCodec_KnownEncoding + PyCodec_LookupError + PyCodec_NameReplaceErrors + PyCodec_Register + PyCodec_RegisterError + PyCodec_ReplaceErrors + PyCodec_StreamReader + PyCodec_StreamWriter + PyCodec_StrictErrors + PyCodec_Unregister + PyCodec_XMLCharRefReplaceErrors + PyComplex_FromDoubles + PyComplex_ImagAsDouble + PyComplex_RealAsDouble + PyCriticalSection2_Begin + PyCriticalSection2_End + PyCriticalSection_Begin + PyCriticalSection_End + PyDescr_NewClassMethod + PyDescr_NewGetSet + PyDescr_NewMember + PyDescr_NewMethod + PyDictProxy_New + PyDict_Clear + PyDict_Contains + PyDict_Copy + PyDict_DelItem + PyDict_DelItemString + PyDict_GetItem + PyDict_GetItemRef + PyDict_GetItemString + PyDict_GetItemStringRef + PyDict_GetItemWithError + PyDict_Items + PyDict_Keys + PyDict_Merge + PyDict_MergeFromSeq2 + PyDict_New + PyDict_Next + PyDict_SetDefaultRef + PyDict_SetItem + PyDict_SetItemString + PyDict_Size + PyDict_Update + PyDict_Values + PyErr_DisplayException + PyErr_GetRaisedException + PyErr_GivenExceptionMatches + PyErr_NewException + PyErr_NewExceptionWithDoc + PyErr_Occurred + PyErr_PrintEx + PyErr_SetObject + PyErr_SetRaisedException + PyErr_SetString + PyErr_WarnEx + PyErr_WarnExplicit + PyErr_WriteUnraisable + PyEval_EvalCode + PyEval_EvalFrame + PyEval_EvalFrameEx + PyEval_GetBuiltins + PyEval_GetFrame + PyEval_GetFrameBuiltins + PyEval_GetFrameGlobals + PyEval_GetFrameLocals + PyEval_GetFuncDesc + PyEval_GetGlobals + PyEval_GetLocals + PyEval_RestoreThread + PyEval_SaveThread + PyExc_ArithmeticError DATA + PyExc_AssertionError DATA + PyExc_AttributeError DATA + PyExc_BaseException DATA + PyExc_BaseExceptionGroup DATA + PyExc_BlockingIOError DATA + PyExc_BrokenPipeError DATA + PyExc_BufferError DATA + PyExc_BytesWarning DATA + PyExc_ChildProcessError DATA + PyExc_ConnectionAbortedError DATA + PyExc_ConnectionError DATA + PyExc_ConnectionRefusedError DATA + PyExc_ConnectionResetError DATA + PyExc_DeprecationWarning DATA + PyExc_EOFError DATA + PyExc_EncodingWarning DATA + PyExc_Exception DATA + PyExc_FileExistsError DATA + PyExc_FileNotFoundError DATA + PyExc_FloatingPointError DATA + PyExc_FutureWarning DATA + PyExc_GeneratorExit DATA + PyExc_ImportError DATA + PyExc_ImportWarning DATA + PyExc_IndentationError DATA + PyExc_IndexError DATA + PyExc_InterruptedError DATA + PyExc_IsADirectoryError DATA + PyExc_KeyError DATA + PyExc_KeyboardInterrupt DATA + PyExc_LookupError DATA + PyExc_MemoryError DATA + PyExc_ModuleNotFoundError DATA + PyExc_NameError DATA + PyExc_NotADirectoryError DATA + PyExc_NotImplementedError DATA + PyExc_OSError DATA + PyExc_OverflowError DATA + PyExc_PendingDeprecationWarning DATA + PyExc_PermissionError DATA + PyExc_ProcessLookupError DATA + PyExc_RecursionError DATA + PyExc_ReferenceError DATA + PyExc_ResourceWarning DATA + PyExc_RuntimeError DATA + PyExc_RuntimeWarning DATA + PyExc_StopAsyncIteration DATA + PyExc_StopIteration DATA + PyExc_SyntaxError DATA + PyExc_SyntaxWarning DATA + PyExc_SystemError DATA + PyExc_SystemExit DATA + PyExc_TabError DATA + PyExc_TimeoutError DATA + PyExc_TypeError DATA + PyExc_UnboundLocalError DATA + PyExc_UnicodeDecodeError DATA + PyExc_UnicodeEncodeError DATA + PyExc_UnicodeError DATA + PyExc_UnicodeTranslateError DATA + PyExc_UnicodeWarning DATA + PyExc_UserWarning DATA + PyExc_ValueError DATA + PyExc_Warning DATA + PyExc_ZeroDivisionError DATA + PyException_GetCause + PyException_GetContext + PyException_GetTraceback + PyException_SetCause + PyException_SetContext + PyException_SetTraceback + PyFloat_AsDouble + PyFloat_FromDouble + PyFloat_FromString + PyFloat_GetInfo + PyFloat_GetMax + PyFloat_GetMin + PyFrame_GetCode + PyFrame_GetLineNumber + PyFrame_GetLocals + PyFrozenSet_New + PyGC_Collect + PyGC_Disable + PyGC_Enable + PyGC_IsEnabled + PyGILState_Ensure + PyGILState_Release + PyImport_AddModuleRef + PyImport_ExecCodeModuleEx + PyImport_Import + PyIndex_Check + PyInterpreterState_Get + PyInterpreterState_GetID + PyIter_Check + PyIter_Next + PyIter_NextItem + PyIter_Send + PyList_Append + PyList_AsTuple + PyList_GetItemRef + PyList_GetSlice + PyList_Insert + PyList_New + PyList_Reverse + PyList_SetItem + PyList_SetSlice + PyList_Size + PyList_Sort + PyLongWriter_Create + PyLongWriter_Discard + PyLongWriter_Finish + PyLong_AsDouble + PyLong_AsInt + PyLong_AsInt32 + PyLong_AsInt64 + PyLong_AsLong + PyLong_AsLongLong + PyLong_AsNativeBytes + PyLong_AsSize_t + PyLong_AsSsize_t + PyLong_AsUInt32 + PyLong_AsUInt64 + PyLong_AsUnsignedLong + PyLong_AsUnsignedLongLong + PyLong_AsUnsignedLongLongMask + PyLong_AsUnsignedLongMask + PyLong_AsVoidPtr + PyLong_Export + PyLong_FreeExport + PyLong_FromDouble + PyLong_FromInt32 + PyLong_FromInt64 + PyLong_FromLong + PyLong_FromLongLong + PyLong_FromNativeBytes + PyLong_FromSize_t + PyLong_FromSsize_t + PyLong_FromString + PyLong_FromUInt32 + PyLong_FromUInt64 + PyLong_FromUnsignedLong + PyLong_FromUnsignedLongLong + PyLong_FromUnsignedNativeBytes + PyLong_FromVoidPtr + PyLong_GetNativeLayout + PyMapping_Check + PyMapping_GetItemString + PyMapping_GetOptionalItem + PyMapping_GetOptionalItemString + PyMapping_HasKey + PyMapping_HasKeyString + PyMapping_HasKeyStringWithError + PyMapping_HasKeyWithError + PyMapping_Items + PyMapping_Keys + PyMapping_Length + PyMapping_SetItemString + PyMapping_Size + PyMapping_Values + PyMem_Calloc + PyMem_Free + PyMem_Malloc + PyMem_RawCalloc + PyMem_RawFree + PyMem_RawMalloc + PyMem_RawRealloc + PyMem_Realloc + PyMemoryView_FromObject + PyModule_AddIntConstant + PyModule_AddObject + PyModule_AddObjectRef + PyModule_AddStringConstant + PyModule_AddType + PyModule_GetDict + PyModule_GetFilenameObject + PyModule_GetNameObject + PyModule_New + PyModule_NewObject + PyNumber_Absolute + PyNumber_Add + PyNumber_And + PyNumber_AsSsize_t + PyNumber_Check + PyNumber_Divmod + PyNumber_Float + PyNumber_FloorDivide + PyNumber_InPlaceAdd + PyNumber_InPlaceAnd + PyNumber_InPlaceFloorDivide + PyNumber_InPlaceLshift + PyNumber_InPlaceMatrixMultiply + PyNumber_InPlaceMultiply + PyNumber_InPlaceOr + PyNumber_InPlacePower + PyNumber_InPlaceRemainder + PyNumber_InPlaceRshift + PyNumber_InPlaceSubtract + PyNumber_InPlaceTrueDivide + PyNumber_InPlaceXor + PyNumber_Index + PyNumber_Invert + PyNumber_Long + PyNumber_Lshift + PyNumber_MatrixMultiply + PyNumber_Multiply + PyNumber_Negative + PyNumber_Or + PyNumber_Positive + PyNumber_Power + PyNumber_Remainder + PyNumber_Rshift + PyNumber_Subtract + PyNumber_TrueDivide + PyNumber_Xor + PyOS_FSPath + PyOS_mystricmp + PyOS_mystrnicmp + PyObject_ASCII + PyObject_Bytes + PyObject_Call + PyObject_CallFunctionObjArgs + PyObject_CallMethodObjArgs + PyObject_CallNoArgs + PyObject_CallObject + PyObject_Calloc + PyObject_ClearWeakRefs + PyObject_DelAttr + PyObject_DelAttrString + PyObject_DelItem + PyObject_DelItemString + PyObject_Dir + PyObject_Format + PyObject_Free + PyObject_GC_IsFinalized + PyObject_GC_IsTracked + PyObject_GC_Track + PyObject_GC_UnTrack + PyObject_GenericGetAttr + PyObject_GenericGetDict + PyObject_GenericSetAttr + PyObject_GenericSetDict + PyObject_GetAIter + PyObject_GetAttr + PyObject_GetAttrString + PyObject_GetItem + PyObject_GetIter + PyObject_GetOptionalAttr + PyObject_GetOptionalAttrString + PyObject_GetTypeData + PyObject_GetTypeData_DuringGC + PyObject_HasAttr + PyObject_HasAttrString + PyObject_HasAttrStringWithError + PyObject_HasAttrWithError + PyObject_Hash + PyObject_HashNotImplemented + PyObject_IsInstance + PyObject_IsSubclass + PyObject_IsTrue + PyObject_Length + PyObject_Malloc + PyObject_Not + PyObject_Realloc + PyObject_Repr + PyObject_RichCompare + PyObject_RichCompareBool + PyObject_SelfIter + PyObject_SetAttr + PyObject_SetAttrString + PyObject_SetItem + PyObject_Size + PyObject_Str + PyObject_Type + PyObject_Vectorcall + PyObject_VectorcallMethod + PySeqIter_New + PySequence_Check + PySequence_Concat + PySequence_Contains + PySequence_Count + PySequence_DelItem + PySequence_DelSlice + PySequence_GetItem + PySequence_GetSlice + PySequence_In + PySequence_InPlaceConcat + PySequence_InPlaceRepeat + PySequence_Index + PySequence_Length + PySequence_List + PySequence_Repeat + PySequence_SetItem + PySequence_SetSlice + PySequence_Size + PySequence_Tuple + PySet_Add + PySet_Clear + PySet_Contains + PySet_Discard + PySet_New + PySet_Pop + PySet_Size + PySlice_AdjustIndices + PySlice_New + PySlice_Unpack + PyTraceBack_Print + PyTuple_GetItem + PyTuple_GetSlice + PyTuple_New + PyTuple_Pack + PyTuple_SetItem + PyTuple_Size + PyType_Freeze + PyType_FromSlots + PyType_GetFlags + PyType_GetFullyQualifiedName + PyType_GetModuleName + PyType_GetName + PyType_GetQualName + PyType_GetSlot + PyType_IsSubtype + PyUnicodeDecodeError_Create + PyUnicode_AsASCIIString + PyUnicode_AsEncodedString + PyUnicode_AsLatin1String + PyUnicode_AsRawUnicodeEscapeString + PyUnicode_AsUTF16String + PyUnicode_AsUTF32String + PyUnicode_AsUTF8AndSize + PyUnicode_AsUTF8String + PyUnicode_AsUnicodeEscapeString + PyUnicode_Compare + PyUnicode_CompareWithASCIIString + PyUnicode_Concat + PyUnicode_Contains + PyUnicode_Decode + PyUnicode_DecodeASCII + PyUnicode_DecodeFSDefault + PyUnicode_DecodeFSDefaultAndSize + PyUnicode_DecodeLatin1 + PyUnicode_DecodeRawUnicodeEscape + PyUnicode_DecodeUTF7 + PyUnicode_DecodeUTF8 + PyUnicode_DecodeUnicodeEscape + PyUnicode_EncodeFSDefault + PyUnicode_Equal + PyUnicode_EqualToUTF8 + PyUnicode_EqualToUTF8AndSize + PyUnicode_Format + PyUnicode_FromEncodedObject + PyUnicode_FromObject + PyUnicode_FromOrdinal + PyUnicode_FromString + PyUnicode_FromStringAndSize + PyUnicode_GetDefaultEncoding + PyUnicode_GetLength + PyUnicode_InternFromString + PyUnicode_InternInPlace + PyUnicode_IsIdentifier + PyUnicode_Partition + PyUnicode_RPartition + PyUnicode_Translate + PyVectorcall_Call + PyWeakref_GetRef + PyWeakref_NewProxy + PyWeakref_NewRef + PyWrapper_New + Py_CompileString + Py_DecRef + Py_EnterRecursiveCall + Py_Finalize + Py_FinalizeEx + Py_GenericAlias + Py_GetBuildInfo + Py_GetCompiler + Py_GetConstant + Py_GetConstantBorrowed + Py_GetCopyright + Py_GetPlatform + Py_GetVersion + Py_IS_TYPE + Py_IncRef + Py_Initialize + Py_InitializeEx + Py_Is + Py_IsFalse + Py_IsFinalizing + Py_IsInitialized + Py_IsNone + Py_IsTrue + Py_LeaveRecursiveCall + Py_NewRef + Py_REFCNT + Py_ReprEnter + Py_ReprLeave + Py_TYPE + Py_Version DATA + Py_XNewRef + _Py_DecRef + _Py_IncRef diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index 08c102b4a64..64076b44fce 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -17,7 +17,7 @@ itertools = { workspace = true } libc = { workspace = true } malachite-bigint = { workspace = true } num-complex = { workspace = true } -rustpython-vm = { workspace = true, features = ["threading", "compiler", "importlib", "host_env"] } +rustpython-vm = { workspace = true, features = ["capi", "threading", "compiler", "importlib", "host_env"] } rustpython-stdlib = {workspace = true, features = ["threading"] } rustpython-pylib = { workspace = true } diff --git a/crates/capi/src/ceval.rs b/crates/capi/src/ceval.rs index 036c259454b..c7267e2e9aa 100644 --- a/crates/capi/src/ceval.rs +++ b/crates/capi/src/ceval.rs @@ -303,4 +303,122 @@ assert not hidden_leaked assert!(err.is_instance_of::(py)); }) } + + #[pyfunction] + fn frame_global_views(py: Python<'_>) -> PyResult>> { + unsafe { + let frame = super::PyEval_GetFrame(); + Ok(vec![ + Bound::from_borrowed_ptr(py, super::PyEval_GetGlobals().cast()), + Bound::from_owned_ptr_or_err(py, super::PyEval_GetFrameGlobals().cast())?, + Bound::from_owned_ptr_or_err(py, crate::pyframe::PyFrame_GetGlobals(frame).cast())?, + ]) + } + } + + fn eval_with_globals<'py>( + globals: &Bound<'py, PyAny>, + source: &str, + mode: &str, + locals: Option<&Bound<'py, PyAny>>, + ) -> PyResult> { + let py = globals.py(); + let code = py.import("builtins")?.getattr("compile")?.call1(( + source, + "", + mode, + ))?; + unsafe { + Bound::from_owned_ptr_or_err( + py, + pyo3::ffi::PyEval_EvalCode( + code.as_ptr(), + globals.as_ptr(), + locals.map_or(core::ptr::null_mut(), Bound::as_ptr), + ), + ) + } + } + + #[test] + fn code_eval_frozen_globals() { + Python::attach(|py| { + let globals = py.eval(c"frozendict(answer=40)", None, None).unwrap(); + let result = eval_with_globals(&globals, "answer + len((1, 2))", "eval", None).unwrap(); + assert_eq!(result.extract::().unwrap(), 42); + let identity = eval_with_globals(&globals, "globals()", "eval", None).unwrap(); + assert!(identity.is(&globals)); + assert!(!globals.contains("__builtins__").unwrap()); + + let namespace = pyo3::types::PyDict::new(py); + namespace + .set_item( + "frame_global_views", + wrap_pyfunction!(frame_global_views, py).unwrap(), + ) + .unwrap(); + let globals = py + .import("builtins") + .unwrap() + .getattr("frozendict") + .unwrap() + .call1((namespace,)) + .unwrap(); + let views = eval_with_globals(&globals, "frame_global_views()", "eval", None).unwrap(); + for view in views.try_iter().unwrap() { + assert!(view.unwrap().is(&globals)); + } + + let explicit = py + .eval( + c"frozendict(answer=40, __builtins__=frozendict(len=lambda x: 9))", + None, + None, + ) + .unwrap(); + let result = eval_with_globals(&explicit, "answer + len(())", "eval", None).unwrap(); + assert_eq!(result.extract::().unwrap(), 49); + }) + } + + #[test] + fn code_eval_frozen_globals_mutation() { + Python::attach(|py| { + let globals = py.eval(c"frozendict(answer=40)", None, None).unwrap(); + for source in [ + "answer = 1", + "global answer; answer = 1", + "global answer; del answer", + ] { + let error = eval_with_globals(&globals, source, "exec", None).unwrap_err(); + assert!(error.is_instance_of::(py)); + } + let locals = pyo3::types::PyDict::new(py); + eval_with_globals( + &globals, + "local_answer = answer + 2", + "exec", + Some(locals.as_any()), + ) + .unwrap(); + assert_eq!( + locals + .get_item("local_answer") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 42 + ); + assert_eq!( + globals + .get_item("answer") + .unwrap() + .extract::() + .unwrap(), + 40 + ); + assert!(!globals.contains("__builtins__").unwrap()); + }) + } } diff --git a/crates/capi/src/dictobject.rs b/crates/capi/src/dictobject.rs index 1e6fab1b131..79742a4506f 100644 --- a/crates/capi/src/dictobject.rs +++ b/crates/capi/src/dictobject.rs @@ -480,4 +480,342 @@ mod tests { ); }) } + // PyO3 has no frozendict wrapper yet. Keep object ownership in Bound while + // exercising the existing limited dictionary API with either dictionary type. + struct DictApi<'py>(Bound<'py, PyAny>); + + impl<'py> DictApi<'py> { + fn list( + &self, + api: unsafe extern "C" fn(*mut pyo3::ffi::PyObject) -> *mut pyo3::ffi::PyObject, + ) -> PyResult> { + unsafe { Bound::from_owned_ptr_or_err(self.0.py(), api(self.0.as_ptr())) } + } + + fn lookup( + &self, + key: &Bound<'py, PyAny>, + with_error: bool, + ) -> PyResult>> { + let value = unsafe { + if with_error { + pyo3::ffi::PyDict_GetItemWithError(self.0.as_ptr(), key.as_ptr()) + } else { + pyo3::ffi::PyDict_GetItem(self.0.as_ptr(), key.as_ptr()) + } + }; + if let Some(error) = PyErr::take(self.0.py()) { + return Err(error); + } + Ok(unsafe { Bound::from_borrowed_ptr_or_opt(self.0.py(), value) }) + } + + fn lookup_ref(&self, key: &Bound<'py, PyAny>) -> PyResult>> { + let mut value = core::ptr::null_mut(); + let status = + unsafe { pyo3::ffi::PyDict_GetItemRef(self.0.as_ptr(), key.as_ptr(), &mut value) }; + if status < 0 { + assert!(value.is_null()); + return Err(PyErr::fetch(self.0.py())); + } + Ok(unsafe { Bound::from_owned_ptr_or_opt(self.0.py(), value) }) + } + + fn lookup_string( + &self, + key: &core::ffi::CStr, + owned: bool, + ) -> PyResult>> { + if owned { + let mut value = core::ptr::null_mut(); + let status = unsafe { + pyo3::ffi::PyDict_GetItemStringRef(self.0.as_ptr(), key.as_ptr(), &mut value) + }; + if status < 0 { + assert!(value.is_null()); + return Err(PyErr::fetch(self.0.py())); + } + Ok(unsafe { Bound::from_owned_ptr_or_opt(self.0.py(), value) }) + } else { + let value = + unsafe { pyo3::ffi::PyDict_GetItemString(self.0.as_ptr(), key.as_ptr()) }; + if let Some(error) = PyErr::take(self.0.py()) { + return Err(error); + } + Ok(unsafe { Bound::from_borrowed_ptr_or_opt(self.0.py(), value) }) + } + } + + fn size(&self) -> PyResult { + let size = unsafe { pyo3::ffi::PyDict_Size(self.0.as_ptr()) }; + if size < 0 { + Err(PyErr::fetch(self.0.py())) + } else { + Ok(size as usize) + } + } + + fn contains(&self, key: &Bound<'py, PyAny>) -> PyResult { + let status = unsafe { pyo3::ffi::PyDict_Contains(self.0.as_ptr(), key.as_ptr()) }; + if status < 0 { + Err(PyErr::fetch(self.0.py())) + } else { + Ok(status != 0) + } + } + + fn entries(&self) -> Vec<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { + let mut entries = Vec::new(); + let mut pos = 0; + let mut key = core::ptr::null_mut(); + let mut value = core::ptr::null_mut(); + while unsafe { pyo3::ffi::PyDict_Next(self.0.as_ptr(), &mut pos, &mut key, &mut value) } + != 0 + { + entries.push(unsafe { + ( + Bound::from_borrowed_ptr(self.0.py(), key), + Bound::from_borrowed_ptr(self.0.py(), value), + ) + }); + } + assert!(!PyErr::occurred(self.0.py())); + entries + } + + fn clear(&self) { + unsafe { pyo3::ffi::PyDict_Clear(self.0.as_ptr()) }; + assert!(!PyErr::occurred(self.0.py())); + } + + fn mutate( + &self, + operation: &str, + key: &Bound<'py, PyAny>, + value: &Bound<'py, PyAny>, + ) -> PyResult<()> { + let status = unsafe { + match operation { + "set" => { + pyo3::ffi::PyDict_SetItem(self.0.as_ptr(), key.as_ptr(), value.as_ptr()) + } + "set_string" => pyo3::ffi::PyDict_SetItemString( + self.0.as_ptr(), + c"x".as_ptr(), + value.as_ptr(), + ), + "delete" => pyo3::ffi::PyDict_DelItem(self.0.as_ptr(), key.as_ptr()), + "delete_string" => { + pyo3::ffi::PyDict_DelItemString(self.0.as_ptr(), c"x".as_ptr()) + } + "setdefault" => pyo3::ffi::PyDict_SetDefaultRef( + self.0.as_ptr(), + key.as_ptr(), + value.as_ptr(), + core::ptr::null_mut(), + ), + "update" => pyo3::ffi::PyDict_Update(self.0.as_ptr(), value.as_ptr()), + "merge" => pyo3::ffi::PyDict_Merge(self.0.as_ptr(), value.as_ptr(), 0), + "merge_sequence" => { + pyo3::ffi::PyDict_MergeFromSeq2(self.0.as_ptr(), value.as_ptr(), 0) + } + _ => panic!("unknown dictionary operation"), + } + }; + if status < 0 { + Err(PyErr::fetch(self.0.py())) + } else { + Ok(()) + } + } + } + + fn frozen_dicts(py: Python<'_>) -> Vec> { + let globals = PyDict::new(py); + py.run( + c"class FrozenSubclass(frozendict):\n def __getitem__(self, key):\n raise AssertionError('must bypass overrides')\n def keys(self):\n raise AssertionError('must bypass overrides')\n def __len__(self):\n raise AssertionError('must bypass overrides')", + Some(&globals), + None, + ).unwrap(); + [ + py.import("builtins") + .unwrap() + .getattr("frozendict") + .unwrap(), + globals.get_item("FrozenSubclass").unwrap().unwrap(), + ] + .into_iter() + .map(|class| { + let items = [("x", 1), ("y", 2)].into_py_dict(py).unwrap(); + DictApi(class.call1((items,)).unwrap()) + }) + .collect() + } + + #[test] + fn frozendict_read_apis() { + Python::attach(|py| { + let key = "x".into_pyobject(py).unwrap().into_any(); + let missing = "missing".into_pyobject(py).unwrap().into_any(); + let unhashable = PyList::empty(py).into_any(); + for dict in frozen_dicts(py) { + assert!(!dict.0.is_instance_of::()); + assert_eq!(dict.size().unwrap(), 2); + assert!(dict.contains(&key).unwrap()); + assert!(!dict.contains(&missing).unwrap()); + for with_error in [false, true] { + assert_eq!( + dict.lookup(&key, with_error) + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 1 + ); + assert!(dict.lookup(&missing, with_error).unwrap().is_none()); + } + assert_eq!( + dict.lookup_ref(&key) + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 1 + ); + assert!(dict.lookup_ref(&missing).unwrap().is_none()); + assert!(dict.lookup(&unhashable, false).unwrap().is_none()); + assert!( + dict.lookup(&unhashable, true) + .unwrap_err() + .is_instance_of::(py) + ); + assert!( + dict.lookup_ref(&unhashable) + .unwrap_err() + .is_instance_of::(py) + ); + for owned in [false, true] { + assert_eq!( + dict.lookup_string(c"x", owned) + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 1 + ); + assert!(dict.lookup_string(c"missing", owned).unwrap().is_none()); + } + assert_eq!( + dict.list(pyo3::ffi::PyDict_Keys) + .unwrap() + .extract::>() + .unwrap(), + ["x", "y"] + ); + assert_eq!( + dict.list(pyo3::ffi::PyDict_Values) + .unwrap() + .extract::>() + .unwrap(), + [1, 2] + ); + assert_eq!( + dict.list(pyo3::ffi::PyDict_Items) + .unwrap() + .extract::>() + .unwrap(), + [("x".to_owned(), 1), ("y".to_owned(), 2)] + ); + let entries: Vec<(String, i32)> = dict + .entries() + .into_iter() + .map(|(key, value)| (key.extract().unwrap(), value.extract().unwrap())) + .collect(); + assert_eq!(entries, [("x".to_owned(), 1), ("y".to_owned(), 2)]); + } + }) + } + + #[test] + fn frozendict_write_apis() { + Python::attach(|py| { + let key = "x".into_pyobject(py).unwrap().into_any(); + let value = PyDict::new(py).into_any(); + let unhashable = PyList::empty(py).into_any(); + for dict in frozen_dicts(py) { + for operation in [ + "set", + "set_string", + "setdefault", + "update", + "merge", + "merge_sequence", + "delete", + "delete_string", + ] { + let error = dict.mutate(operation, &key, &value).unwrap_err(); + assert!(error.is_instance_of::(py)); + let action = if operation.starts_with("delete") { + "deletion" + } else { + "assignment" + }; + assert_eq!( + error.value(py).str().unwrap().to_str().unwrap(), + format!("frozendict object does not support item {action}") + ); + } + let error = dict.mutate("delete", &unhashable, &value).unwrap_err(); + assert_eq!( + error.value(py).str().unwrap().to_str().unwrap(), + "cannot use 'list' as a frozendict key (unhashable type: 'list')" + ); + assert!( + dict.list(pyo3::ffi::PyDict_Copy) + .unwrap_err() + .is_instance_of::(py) + ); + dict.clear(); + assert_eq!(dict.size().unwrap(), 2); + } + }) + } + + #[test] + fn frozendict_merge_source() { + Python::attach(|py| { + let source = py.eval(c"frozendict(x=1, y=2)", None, None).unwrap(); + let source = source.cast::().unwrap(); + let target = [("x", 0)].into_py_dict(py).unwrap(); + target.update_if_missing(source).unwrap(); + assert_eq!( + target + .get_item("x") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 0 + ); + assert_eq!( + target + .get_item("y") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 2 + ); + target.update(source).unwrap(); + assert_eq!( + target + .get_item("x") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 1 + ); + }) + } } diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 1f2a3baa731..ef28462a8b4 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -11,6 +11,16 @@ use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; define_py_check!(fn PyCFunction_Check, types.builtin_function_or_method_type); define_py_check!(exact fn PyCFunction_CheckExact, types.builtin_function_or_method_type); +// Removed from the public API in 3.13, but retained in the stable ABI. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCFunction_Call( + callable: *mut PyObject, + args: *mut PyObject, + kwargs: *mut PyObject, +) -> *mut PyObject { + unsafe { crate::abstract_::PyObject_Call(callable, args, kwargs) } +} + #[repr(C)] #[derive(Debug)] pub struct PyMethodDef { @@ -317,7 +327,46 @@ mod tests { use pyo3::exceptions::PyException; use pyo3::ffi::{PyLong_FromLong, PyObject}; use pyo3::prelude::*; - use pyo3::types::{PyCFunction, PyInt, PyString}; + use pyo3::types::{PyCFunction, PyDict, PyInt, PyString, PyTuple}; + + #[test] + fn legacy_call_python_function() { + Python::attach(|py| { + let callable = py + .eval(c"lambda *args, **kwargs: (args, kwargs)", None, None) + .unwrap(); + let args = PyTuple::new(py, [1, 2]).unwrap(); + let kwargs = PyDict::new(py); + kwargs.set_item("num", 5).unwrap(); + // PyO3 no longer declares this ABI-only compatibility symbol. + let result = unsafe { + Bound::::from_owned_ptr_or_err( + py, + super::PyCFunction_Call( + callable.as_ptr().cast(), + args.as_ptr().cast(), + kwargs.as_ptr().cast(), + ) + .cast(), + ) + } + .unwrap(); + assert_eq!( + result.get_item(0).unwrap().extract::<(i32, i32)>().unwrap(), + (1, 2) + ); + assert_eq!( + result + .get_item(1) + .unwrap() + .get_item("num") + .unwrap() + .extract::() + .unwrap(), + 5 + ); + }); + } #[test] fn closure_function() { diff --git a/crates/capi/src/monitoring.rs b/crates/capi/src/monitoring.rs new file mode 100644 index 00000000000..2253619eca9 --- /dev/null +++ b/crates/capi/src/monitoring.rs @@ -0,0 +1,202 @@ +use crate::{PyObject, pystate::with_vm, util::FfiPtrExt}; +use core::ffi::c_int; +use rustpython_vm::stdlib::sys::monitoring_capi; + +pub use monitoring_capi::EventState as PyMonitoringState; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMonitoring_EnterScope( + state_array: *mut PyMonitoringState, + version: *mut u64, + event_types: *const u8, + length: isize, +) -> c_int { + with_vm(|vm| { + let length = usize::try_from(length) + .map_err(|_| vm.new_value_error("invalid monitoring scope length"))?; + let (states, events) = if length == 0 { + (&mut [][..], &[][..]) + } else { + unsafe { + ( + core::slice::from_raw_parts_mut(state_array, length), + core::slice::from_raw_parts(event_types, length), + ) + } + }; + monitoring_capi::enter_scope(vm, states, unsafe { &mut *version }, events) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyMonitoring_ExitScope() -> c_int { + monitoring_capi::exit_scope(); + 0 +} + +macro_rules! fire_event { + ($name:ident, $event:expr $(, $arg:ident)*) => { + #[unsafe(no_mangle)] + pub unsafe extern "C" fn $name( + state: *mut PyMonitoringState, + codelike: *mut PyObject, + offset: i32, + $($arg: *mut PyObject,)* + ) -> c_int { + with_vm(|vm| { + // Exception setup precedes reading the codelike or arguments, + // matching CPython's exception_event_setup error ordering. + if matches!($event, 11..=15 | 17) + && unsafe { (*state).active } != 0 + && vm.raised_exception().is_none() + { + return Err(vm.new_value_error(format!( + "Firing event {} with no exception set", $event + ))); + } + unsafe { monitoring_capi::fire_event( + vm, + state, + codelike.assume_borrowed(), + offset, + $event, + &[$($arg.assume_borrowed().to_owned(),)*], + ) } + }) + } + }; +} + +fire_event!(_PyMonitoring_FirePyStartEvent, 0); +fire_event!(_PyMonitoring_FirePyResumeEvent, 1); +fire_event!(_PyMonitoring_FirePyReturnEvent, 2, retval); +fire_event!(_PyMonitoring_FirePyYieldEvent, 3, retval); +fire_event!(_PyMonitoring_FireCallEvent, 4, callable, arg0); +fire_event!(_PyMonitoring_FireJumpEvent, 7, target_offset); +fire_event!(_PyMonitoring_FireBranchLeftEvent, 8, target_offset); +fire_event!(_PyMonitoring_FireBranchRightEvent, 9, target_offset); +// The deprecated BRANCH entry point fires BRANCH_RIGHT. +fire_event!(_PyMonitoring_FireBranchEvent, 9, target_offset); +fire_event!(_PyMonitoring_FireRaiseEvent, 11); +fire_event!(_PyMonitoring_FireExceptionHandledEvent, 12); +fire_event!(_PyMonitoring_FirePyUnwindEvent, 13); +fire_event!(_PyMonitoring_FirePyThrowEvent, 14); +fire_event!(_PyMonitoring_FireReraiseEvent, 15); +fire_event!(_PyMonitoring_FireCReturnEvent, 16, retval); +fire_event!(_PyMonitoring_FireCRaiseEvent, 17); + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn _PyMonitoring_FireLineEvent( + state: *mut PyMonitoringState, + codelike: *mut PyObject, + offset: i32, + lineno: c_int, +) -> c_int { + with_vm(|vm| unsafe { + monitoring_capi::fire_event( + vm, + state, + codelike.assume_borrowed(), + offset, + 5, + &[vm.ctx.new_int(lineno).into()], + ) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn _PyMonitoring_FireStopIterationEvent( + state: *mut PyMonitoringState, + codelike: *mut PyObject, + offset: i32, + value: *mut PyObject, +) -> c_int { + with_vm(|vm| unsafe { + let value = value.assume_borrowed_or_opt().map(PyObject::to_owned); + monitoring_capi::fire_event( + vm, + state, + codelike.assume_borrowed(), + offset, + 10, + value.as_slice(), + ) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use pyo3::{prelude::*, types::PyModule}; + + #[test] + fn native_scope_and_disable() { + // Monitoring callbacks are interpreter-wide. Other C-API unit tests + // execute Python concurrently in the shared main interpreter. + const CHILD_MARKER: &str = "RUSTPYTHON_CAPI_MONITORING_TEST_CHILD"; + if std::env::var_os(CHILD_MARKER).is_none() { + let status = std::process::Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "monitoring::tests::native_scope_and_disable", + "--nocapture", + ]) + .env(CHILD_MARKER, "1") + .status() + .unwrap(); + assert!( + status.success(), + "isolated monitoring test failed: {status}" + ); + return; + } + Python::attach(|py| { + let fixture = PyModule::from_code( + py, + c"import sys\nseen = []\ndef callback(code, offset):\n seen.append((code, offset))\n return sys.monitoring.DISABLE\n", + c"monitoring_fixture.py", + c"monitoring_fixture", + ) + .unwrap(); + let monitoring = py.import("sys").unwrap().getattr("monitoring").unwrap(); + monitoring + .call_method1("use_tool_id", (5, "capi scope")) + .unwrap(); + monitoring + .call_method1( + "register_callback", + (5, 1, fixture.getattr("callback").unwrap()), + ) + .unwrap(); + monitoring.call_method1("set_events", (5, 1)).unwrap(); + let mut state = PyMonitoringState::default(); + let mut version = 0; + assert_eq!( + unsafe { PyMonitoring_EnterScope(&mut state, &mut version, &0, 1) }, + 0 + ); + assert_eq!(state.active, 1 << 5); + let codelike = py.None(); + assert_eq!( + unsafe { _PyMonitoring_FirePyStartEvent(&mut state, codelike.as_ptr().cast(), 42) }, + 0 + ); + assert_eq!(state.active, 0); + let seen: Vec<(Option, i32)> = fixture.getattr("seen").unwrap().extract().unwrap(); + assert_eq!(seen, [(None, 42)]); + assert_eq!( + unsafe { PyMonitoring_EnterScope(&mut state, &mut version, &0, 1) }, + 0 + ); + assert_eq!(state.active, 0); + monitoring.call_method0("restart_events").unwrap(); + assert_eq!( + unsafe { PyMonitoring_EnterScope(&mut state, &mut version, &0, 1) }, + 0 + ); + assert_eq!(state.active, 1 << 5); + assert_eq!(PyMonitoring_ExitScope(), 0); + monitoring.call_method1("free_tool_id", (5,)).unwrap(); + }); + } +} diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index 0bf843aa442..f46c83edaeb 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -101,7 +101,7 @@ define_py_check!(fn PyExceptionInstance_Check, exceptions.base_exception_type); #[unsafe(no_mangle)] pub extern "C" fn PyErr_Occurred() -> *mut PyObject { with_vm(|vm| { - vm.current_exception() + vm.raised_exception() .map(|exc| exc.class().as_object().as_raw()) .unwrap_or_default() }) @@ -121,7 +121,7 @@ pub unsafe extern "C" fn PyErr_SetRaisedException(exc: *mut PyObject) { with_vm(|vm| { let exception = unsafe { exc.assume_owned_or_opt() }.map(|exc| unsafe { exc.downcast_unchecked() }); - vm.set_exception(exception); + vm.set_raised_exception(exception); }) } diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 7255a0e34a5..f760ae3f893 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -16,10 +16,12 @@ pub(crate) static MAIN_INTERP_PTR: AtomicPtr = AtomicPtr::new(core: /// Request a thread local vm from the main interpreter pub(crate) fn request_vm_from_interpreter() -> ThreadedVirtualMachine { + // Attachment may park forever during finalization. Clone the inactive + // template here, then let the caller attach after releasing this mutex. get_main_interpreter() .as_ref() .expect("Interpreter not initialized") - .enter(|vm| vm.new_thread()) + .new_thread() } #[unsafe(no_mangle)] @@ -43,8 +45,10 @@ pub extern "C" fn Py_InitializeEx(_initsigs: c_int) { unsafe { init_exception_statics(&Context::genesis().exceptions) }; let builder = Interpreter::builder(Default::default()); let defs = rustpython_stdlib::stdlib_module_defs(&builder.ctx); + let testcapi = rustpython_vm::stdlib::capi_test_module_def(&builder.ctx); *interp = builder .add_native_modules(&defs) + .add_native_module(testcapi) .init_hook(|vm| { let state = PyRc::get_mut(&mut vm.state).unwrap(); let path = rustpython_pylib::LIB_PATH.to_owned(); diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index d8926f44d28..fefa8c6afe6 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -194,7 +194,7 @@ impl FfiResult<()> for PyResult { fn into_output(self, vm: &VirtualMachine) { match self { - Err(err) => vm.set_exception(Some(err)), + Err(err) => vm.set_raised_exception(Some(err)), } } } @@ -208,7 +208,7 @@ where fn into_output(self, vm: &VirtualMachine) -> Output { self.map_or_else( |err| { - vm.set_exception(Some(err)); + vm.set_raised_exception(Some(err)); T::ERR_VALUE }, |obj| obj.into_output(vm), diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index fb561db6d93..f31a9aada8d 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -3668,7 +3668,6 @@ impl<'warnings> Compiler<'warnings> { Ok(()) } - /// `_PyCompile_InExceptionHandler` fn in_exception_handler(&mut self) -> bool { self.current_code_info().fblock.iter().any(|block| { matches!( @@ -4379,6 +4378,7 @@ impl<'warnings> Compiler<'warnings> { let key = self.symbol_table_stack.len() - 1; let lineno = self.get_source_line_number().get().to_u32(); self.enter_scope(alias_name, CompilerScope::TypeAlias, key, lineno)?; + self.set_qualname(); self.configure_annotation_format_parameter(); self.emit_format_validation(); @@ -9407,14 +9407,14 @@ impl<'warnings> Compiler<'warnings> { ), generators, &|compiler, collection_add_i| { - compiler.compile_comprehension_element(elt)?; + let unpack = compiler.compile_comprehension_element(elt)?; compiler.set_source_node(elt.range(), elt.node_index()); - emit!( - compiler, - Instruction::ListAppend { - i: collection_add_i.to_u32(), - } - ); + let i = collection_add_i.to_u32(); + if unpack { + emit!(compiler, Instruction::ListExtend { i }); + } else { + emit!(compiler, Instruction::ListAppend { i }); + } Ok(()) }, ComprehensionType::List, @@ -9437,14 +9437,14 @@ impl<'warnings> Compiler<'warnings> { ), generators, &|compiler, collection_add_i| { - compiler.compile_comprehension_element(elt)?; + let unpack = compiler.compile_comprehension_element(elt)?; compiler.set_source_node(elt.range(), elt.node_index()); - emit!( - compiler, - Instruction::SetAdd { - i: collection_add_i.to_u32(), - } - ); + let i = collection_add_i.to_u32(); + if unpack { + emit!(compiler, Instruction::SetUpdate { i }); + } else { + emit!(compiler, Instruction::SetAdd { i }); + } Ok(()) }, ComprehensionType::Set, @@ -9460,11 +9460,9 @@ impl<'warnings> Compiler<'warnings> { generators, .. }) => { - let Some(key) = key.as_deref() else { - self.restore_source_span(expression_span); - return Err(self.error(CodegenErrorType::SyntaxError( - "dict unpacking cannot be used in dict comprehension".to_owned(), - ))); + let element_span = match key.as_deref() { + Some(key) => self.dict_comprehension_element_span(key, value)?, + None => self.node_span(value.as_ref()), }; self.compile_comprehension( "", @@ -9476,29 +9474,30 @@ impl<'warnings> Compiler<'warnings> { ), generators, &|compiler, collection_add_i| { - // changed evaluation order for Py38 named expression PEP 572 - compiler.compile_expression(key)?; + // Keys precede values; a missing Ruff key denotes **value. + if let Some(key) = key { + // Changed evaluation order for Py38 named expression PEP 572. + compiler.compile_expression(key)?; + } compiler.compile_expression(value)?; - compiler.restore_source_span( - compiler.dict_comprehension_element_span(key, value)?, - ); - emit!( - compiler, - Instruction::MapAdd { - i: collection_add_i.to_u32(), - } - ); + compiler.restore_source_span(element_span); + let i = collection_add_i.to_u32(); + if key.is_some() { + emit!(compiler, Instruction::MapAdd { i }); + } else { + emit!(compiler, Instruction::DictUpdate { i }); + } Ok(()) }, ComprehensionType::Dict, - Self::contains_await(key) + key.as_deref().is_some_and(Self::contains_await) || Self::contains_await(value) || Self::generators_contain_await(generators), expression_span, - self.dict_comprehension_element_span(key, value)?, - self.node_span(key), + element_span, + self.node_span(key.as_deref().unwrap_or(value)), )?; } ast::Expr::Generator(ast::ExprGenerator { @@ -10241,16 +10240,13 @@ impl<'warnings> Compiler<'warnings> { Ok(()) } - fn compile_comprehension_element(&mut self, element: &ast::Expr) -> CompileResult<()> { - self.compile_expression(element).map_err(|e| { - if let CodegenErrorType::InvalidStarExpr = e.error { - self.error(CodegenErrorType::SyntaxError( - "iterable unpacking cannot be used in comprehension".to_owned(), - )) - } else { - e - } - }) + fn compile_comprehension_element(&mut self, element: &ast::Expr) -> CompileResult { + let (value, unpack) = match element { + ast::Expr::Starred(starred) => (&*starred.value, true), + _ => (element, false), + }; + self.compile_expression(value)?; + Ok(unpack) } fn compile_generator_expression( @@ -10271,7 +10267,15 @@ impl<'warnings> Compiler<'warnings> { // Compile the element expression // Note: if element is an async comprehension, compile_expression // already handles awaiting it, so we don't need to await again here - compiler.compile_comprehension_element(elt)?; + let unpack = compiler.compile_comprehension_element(elt)?; + let unpack_loop = unpack.then(|| (compiler.new_block(), compiler.new_block())); + if let Some((start, end)) = unpack_loop { + // Unpacking forwards values, not send()/throw() as yield-from would. + compiler.set_source_node(elt.range(), elt.node_index()); + emit!(compiler, Instruction::GetIter { mode: 0 }); + compiler.use_cpython_label_block(start); + emit!(compiler, Instruction::ForIter { delta: end }); + } compiler.mark_generator(); if compiler.ctx.func == FunctionContext::AsyncFunction { @@ -10293,6 +10297,12 @@ impl<'warnings> Compiler<'warnings> { } ); emit!(compiler, Instruction::PopTop); + if let Some((start, end)) = unpack_loop { + emit!(compiler, PseudoInstruction::Jump { delta: start }); + compiler.set_no_location(); + compiler.use_cpython_label_block(end); + compiler.emit_sync_comprehension_end_for(); + } Ok(()) }, @@ -12288,12 +12298,12 @@ impl<'warnings> Compiler<'warnings> { &mut self, expr: &ast::Expr, ) -> CompileResult> { - // CPython 3.14 ast_preprocess.c::fold_const_match_patterns() + // CPython ast_preprocess.c::fold_const_match_patterns() // folds only the constant forms needed by match patterns before // codegen_pattern_value()/codegen_pattern_mapping_key() visit them. Ok(match expr { ast::Expr::UnaryOp(ast::ExprUnaryOp { - op: ast::UnaryOp::USub, + op: op @ (ast::UnaryOp::UAdd | ast::UnaryOp::USub), operand, .. }) => { @@ -12301,7 +12311,11 @@ impl<'warnings> Compiler<'warnings> { else { return Ok(None); }; - Self::try_negate_match_pattern_constant(constant) + if *op == ast::UnaryOp::USub { + Self::try_negate_match_pattern_constant(constant) + } else { + Some(constant) + } } ast::Expr::BinOp(ast::ExprBinOp { left, op, right, .. @@ -12326,7 +12340,7 @@ impl<'warnings> Compiler<'warnings> { return Ok(Some(constant)); } let ast::Expr::UnaryOp(ast::ExprUnaryOp { - op: ast::UnaryOp::USub, + op: op @ (ast::UnaryOp::UAdd | ast::UnaryOp::USub), operand, .. }) = expr @@ -12336,7 +12350,11 @@ impl<'warnings> Compiler<'warnings> { let Some(constant) = self.try_compile_match_pattern_real_constant(operand)? else { return Ok(None); }; - Ok(Self::try_negate_match_pattern_constant(constant)) + Ok(if *op == ast::UnaryOp::USub { + Self::try_negate_match_pattern_constant(constant) + } else { + Some(constant) + }) } fn try_compile_match_pattern_real_constant( @@ -15073,6 +15091,66 @@ match x: ); } + #[test] + fn empty_module_implicit_return_inherits_resume_location_like_cpython() { + let code = compile_exec(""); + // codegen emits the implicit LOAD_CONST/RETURN_VALUE with + // NO_LOCATION, then flowgraph.c::propagate_line_numbers() propagates + // the module RESUME location, whose line is 0. + assert_eq!(code.linetable.as_ref(), &[0xf3, 0x03, 0x01, 0x01, 0x01]); + } + + #[test] + fn module_docstring_load_uses_doc_location_like_cpython() { + let code = compile_exec( + "\ +\"doc\" +x = 1 +", + ); + + // codegen_body() emits the docstring LOAD_CONST at the + // string expression location, then emits STORE_NAME __doc__ with + // NO_LOCATION. + assert_eq!( + code.linetable.as_ref(), + &[ + 0xf1, 0x03, 0x01, 0x01, 0x01, 0xd9, 0x00, 0x05, 0xd8, 0x04, 0x05, 0x82, 0x01, + ], + ); + } + + #[test] + fn redundant_nop_location_copies_full_location_like_cpython() { + let code = compile_exec( + "\ +def f(x, y, z): + while x: + if y: + pass + elif z: + if y < 0: + return y + if z: + y = y + 1 + elif y: + return 1 + return -1 +", + ); + let f = find_code(&code, "f").expect("missing function code"); + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xdf, 0x0a, 0x0b, 0xdf, 0x0b, 0x0c, 0xd9, 0x0c, 0x10, 0xdf, 0x0d, 0x0e, + 0xd8, 0x0f, 0x10, 0x90, 0x31, 0x8c, 0x75, 0xd8, 0x17, 0x18, 0x90, 0x08, 0xdf, 0x0f, + 0x10, 0xd8, 0x14, 0x15, 0x98, 0x01, 0x95, 0x45, 0x92, 0x01, 0xf1, 0x03, 0x00, 0x10, + 0x11, 0xe7, 0x0d, 0x0e, 0x89, 0x51, 0xd9, 0x13, 0x14, 0xd8, 0x0b, 0x0d, 0x80, 0x49, + ], + "CPython basicblock_remove_redundant_nops() copies the full NOP location into a following no-location jump" + ); + } + fn scan_program_symbol_table(source: &str) -> SymbolTable { let source_file = SourceFileBuilder::new("source_path", source).finish(); let parsed = ruff_python_parser::parse( @@ -16981,6 +17059,86 @@ def f(buffer, pos, last_char): }) } + #[test] + fn annotated_multiline_function_body_keeps_def_firstlineno_like_cpython() { + let code = compile_exec( + r#" +a = 1 +def f( + x: a, +): ... +"#, + ); + let f = find_code(&code, "f").expect("missing f code"); + // CPython 3.14 codegen_function() computes firstlineno from the + // FunctionDef before compiling annotations, then passes it to + // codegen_function_body(). + assert_eq!(f.linetable.as_ref(), &[0x81, 0x00, 0xe1, 0x03, 0x06]); + } + + #[test] + fn annotation_scope_return_uses_function_location_like_cpython() { + let code = compile_exec( + r#" +def g(): + def f(x: not (int is int), /): ... +"#, + ); + let g = find_code(&code, "g").expect("missing g code"); + let annotate = find_code(g, "__annotate__").expect("missing annotation code"); + // CPython 3.14 codegen_function_annotations() receives LOC(function) + // and uses it for the annotation closure's BUILD_MAP/RETURN_VALUE and + // for the parent MAKE_FUNCTION annotate sequence. + assert_eq!(g.linetable.as_ref(), &[0x81, 0x00, 0xdf, 0x04, 0x26]); + assert_eq!( + annotate.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd7, 0x04, 0x26, 0xd1, 0x04, 0x26, 0x94, 0x23, 0x9c, 0x13, 0xd0, 0x0d, + 0x1d, 0xd1, 0x04, 0x26, + ], + ); + } + + #[test] + fn starred_arg_annotation_unpack_uses_function_location_like_cpython() { + let code = compile_exec("def f(*args: *Ts): pass\n"); + let annotate = find_code(&code, "__annotate__").expect("missing annotation code"); + + // codegen_argannotation() visits `Ts` at the annotation + // expression location, then emits UNPACK_SEQUENCE at LOC(function). + assert_eq!( + annotate.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd7, 0x00, 0x17, 0xd1, 0x00, 0x17, 0x8c, 0x62, 0xd3, 0x00, 0x17 + ], + ); + } + + #[test] + fn module_deferred_annotations_use_start_location_like_cpython() { + let code = compile_exec( + "\ +import os +X: int +Y: str +", + ); + let annotate = find_code(&code, "__annotate__").expect("missing __annotate__ code"); + + // compile.c::start_location() passes the first module + // statement location into _PyCodegen_Module(), and + // codegen_process_deferred_annotations() uses that loc for annotation + // scope setup, BUILD_MAP, STORE_SUBSCR, and RETURN_VALUE. + assert_eq!( + annotate.linetable.as_ref(), + &[ + 0x81, 0x00, 0x87, 0x09, 0x81, 0x09, 0xdf, 0x00, 0x06, 0x82, 0x06, 0x84, 0x33, 0x81, + 0x06, 0xf1, 0x03, 0x00, 0x01, 0x0a, 0xe7, 0x00, 0x06, 0x82, 0x06, 0x84, 0x33, 0x81, + 0x06, 0xf2, 0x05, 0x00, 0x01, 0x0a, + ] + ); + } + #[test] fn super_method_call_kw_names_use_attribute_location_like_cpython() { let code = compile_exec( @@ -17231,6 +17389,83 @@ def continues(xs): } } + #[test] + fn not_compare_uses_unary_location_like_cpython() { + let code = compile_exec( + "\ +def f(self, other): + return not self == other +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + // CPython 3.14 parses the Compare inside UnaryOp(Not) with the + // UnaryOp start location, so codegen_compare() emits COMPARE_OP at + // the full "not self == other" range before flowgraph folds TO_BOOL. + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd8, 0x0f, 0x13, 0xd2, 0x0b, 0x1c, 0xd0, 0x04, 0x1c, + ] + ); + } + + #[test] + fn not_chained_compare_keeps_compare_location_like_cpython() { + let code = compile_exec( + "\ +def f(c): + return not (b\" \" <= c <= b\"~\") +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + // CPython's single Compare under UnaryOp(Not) includes "not" in the + // Compare range, but chained comparisons keep their inner range for + // compare scaffolding and only use the UnaryOp range for TO_BOOL and + // UNARY_NOT. + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd8, 0x10, 0x14, 0x98, 0x01, 0xd7, 0x10, 0x21, 0xd4, 0x10, 0x21, 0x98, + 0x54, 0xd1, 0x10, 0x21, 0xd4, 0x0b, 0x22, 0xd0, 0x04, 0x22, 0xd1, 0x10, 0x21, 0xd4, + 0x0b, 0x22, 0xd0, 0x04, 0x22, + ] + ); + } + + #[test] + fn type_param_scopes_use_cpython_locations() { + let code = compile_exec("type BoundGenericAlias[X: int] = set[X]\n"); + let type_params = find_code(&code, "") + .expect("missing generic parameters code"); + let bound = find_direct_child_code(type_params, "X").expect("missing X bound code"); + let alias = + find_direct_child_code(type_params, "BoundGenericAlias").expect("missing alias code"); + + // CPython 3.14 codegen_type_params() emits type-parameter ops at + // LOC(typeparam), bound/default evaluator ops at LOC(e), and type alias + // body plumbing at LOC(s). + assert_eq!( + type_params.linetable.as_ref(), + &[ + 0xf8, 0x81, 0x00, 0xd0, 0x00, 0x27, 0x90, 0x76, 0x9b, 0x23, 0x93, 0x76, 0xd7, 0x00, + 0x27, 0xd1, 0x00, 0x27, + ], + ); + assert_eq!( + bound.linetable.as_ref(), + &[0x81, 0x00, 0x9f, 0x23, 0x9e, 0x23] + ); + assert_eq!( + alias.linetable.as_ref(), + &[ + 0xf8, 0x81, 0x00, 0xd7, 0x00, 0x27, 0xd0, 0x00, 0x27, 0xa4, 0x13, 0xa0, 0x51, 0xa5, + 0x16, 0xd0, 0x00, 0x27, + ], + ); + } + #[test] fn typealias_value_scope_has_single_return_like_cpython() { let code = compile_exec("type Alias = int\n"); @@ -17247,6 +17482,23 @@ def continues(xs): ); } + #[test] + fn generic_typealias_wrapper_return_uses_alias_location_like_cpython() { + let code = compile_exec("type A[T] = T\n"); + let type_params = + find_code(&code, "").expect("missing type params code"); + + // codegen_typealias() assembles the generic-parameters + // wrapper with addNone=0 after codegen_typealias_body() leaves the type + // alias object on the stack. The final RETURN_VALUE keeps LOC(type alias). + assert_eq!( + type_params.linetable.as_ref(), + &[ + 0xf8, 0x81, 0x00, 0x80, 0x0d, 0x84, 0x71, 0x87, 0x0d, 0x81, 0x0d + ], + ); + } + #[test] fn type_param_bound_scope_has_single_return_like_cpython() { let code = compile_exec("type Alias[T: int] = T\n"); @@ -17281,6 +17533,25 @@ def continues(xs): ); } + #[test] + fn generic_function_annotation_scope_uses_function_location_like_cpython() { + let code = compile_exec("def f[T](x: int): ...\n"); + let type_params = + find_code(&code, "").expect("missing type params code"); + let annotate = + find_direct_child_code(type_params, "__annotate__").expect("missing annotation code"); + + // CPython 3.14 passes LOC(function) into codegen_function_annotations(), + // even when the annotation closure is emitted inside the generic + // parameters scope after codegen_type_params(). + assert_eq!( + annotate.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd7, 0x00, 0x15, 0xd1, 0x00, 0x15, 0x8c, 0x43, 0xd1, 0x00, 0x15, + ], + ); + } + #[test] fn decorated_generic_function_type_params_use_decorator_firstlineno_like_cpython() { let code = compile_exec( @@ -17298,6 +17569,27 @@ def f[T](): pass assert_eq!(type_params.first_line_number.unwrap().get(), 2); } + #[test] + fn generic_class_type_params_store_uses_class_location_like_cpython() { + let code = compile_exec( + "\ +def outer(): + class X[T]: ... +", + ); + let type_params = + find_code(&code, "").expect("missing type params code"); + + // CPython 3.14 codegen_class() calls codegen_type_params(), then stores + // the resulting .type_params cell with codegen_nameop(c, LOC(class), ...). + assert_eq!( + type_params.linetable.as_ref(), + &[ + 0xf8, 0x81, 0x00, 0x8c, 0x41, 0x87, 0x4f, 0x87, 0x4f, 0x80, 0x4f, + ] + ); + } + #[test] fn generic_class_wrapper_ops_use_class_location_like_cpython() { let code = compile_exec( @@ -17522,6 +17814,44 @@ class C[T]: pass assert_eq!(type_params.first_line_number.unwrap().get(), 2); } + #[test] + fn class_deferred_annotations_use_class_body_location_like_cpython() { + let code = compile_exec( + r#" +class C: + "doc" + x: int +"#, + ); + let class_code = find_code(&code, "C").expect("missing class code"); + + // CPython 3.14 calls codegen_body(c, loc, ...) from codegen_class_body() + // with LOCATION(firstlineno, firstlineno, 0, 0). Deferred annotation + // closure setup and following artificial class tail inherit that class + // body location, not the annotation expression location. + assert_eq!( + class_code.linetable.as_ref(), + &[ + 0xf8, 0x87, 0x00, 0x81, 0x00, 0xd9, 0x04, 0x09, 0xf7, 0x03, 0x00, 0x01, 0x01, 0x83, + 0x00, + ], + ); + } + + #[test] + fn future_annotation_string_uses_annotation_location_like_cpython() { + let code = compile_exec("from __future__ import annotations\nclass Bar:\n foo: Foo\n"); + let class_code = find_code(&code, "Bar").expect("missing class code"); + + // CPython 3.14 codegen_annassign() calls codegen_visit_annexpr(), + // which emits the stringized annotation at LOC(annotation), then emits + // the __annotations__ store sequence at LOC(AnnAssign). + assert_eq!( + class_code.linetable.as_ref(), + &[0x87, 0x00, 0x80, 0x00, 0xd8, 0x09, 0x0c, 0x87, 0x48] + ); + } + #[test] fn future_tstring_annotation_preserves_interpolation_source_like_cpython() { let code = compile_exec( @@ -17565,6 +17895,41 @@ class C[T]: pass ); } + #[test] + fn lambda_dict_literal_ops_use_dict_location_like_cpython() { + let code = compile_exec( + "\ +f = lambda data: {'x': data} +g = lambda i: {**i} +", + ); + let f = find_code(&code, "").expect("missing f lambda code"); + let g = code + .constants + .iter() + .filter_map(|constant| { + if let ConstantData::Code { code } = constant { + (code.obj_name == "").then_some(code.as_ref()) + } else { + None + } + }) + .nth(1) + .expect("missing g lambda code"); + + // CPython 3.14 codegen_dict()/codegen_subdict() uses LOC(dict) for + // BUILD_MAP, MAP_ADD, and DICT_UPDATE, so the lambda RETURN_VALUE + // inherits the full dict literal location after compiling its body. + assert_eq!( + f.linetable.as_ref(), + &[0x81, 0x00, 0x90, 0x23, 0x90, 0x74, 0x91, 0x1b] + ); + assert_eq!( + g.linetable.as_ref(), + &[0x81, 0x00, 0x88, 0x65, 0x90, 0x11, 0x89, 0x65] + ); + } + #[test] fn dict_unpacking_large_regular_run_uses_subdict_chunks_like_cpython() { let pairs = (0..17) @@ -18347,6 +18712,30 @@ def f(x): ); } + #[test] + fn match_or_alternative_copies_use_alternative_locations_like_cpython() { + let code = compile_exec( + "\ +def f(): + x = False + match 0: + case 0 | 1 | 2 | 3: + x = True + return x +", + ); + let f = find_code(&code, "f").expect("missing f code"); + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd8, 0x08, 0x0d, 0x80, 0x41, 0xd8, 0x0a, 0x0b, 0xdf, 0x0d, 0x0e, 0x97, + 0x11, 0x97, 0x51, 0x9f, 0x11, 0x88, 0x5d, 0xe0, 0x0b, 0x0c, 0x80, 0x48, 0xf0, 0x05, + 0x00, 0x0e, 0x1b, 0xd8, 0x10, 0x14, 0x88, 0x41, 0xd8, 0x0b, 0x0c, 0x80, 0x48, + ], + "CPython codegen_pattern_or() emits each alternative COPY with LOC(alt)" + ); + } + #[test] fn match_or_conflicting_bind_error_uses_or_pattern_location_like_cpython() { let error = compile_exec_error( @@ -18389,6 +18778,31 @@ def f(value): ); } + #[test] + fn match_success_jump_uses_no_location_like_cpython() { + let code = compile_exec( + "\ +def f(self): + match 0: + case 0: + x = True + case 0: + x = False + self.assertIs(x, True) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd8, 0x0a, 0x0b, 0xde, 0x0d, 0x0e, 0xd9, 0x10, 0x14, 0x89, 0x41, 0xdd, + 0x0d, 0x0e, 0xd8, 0x10, 0x15, 0x88, 0x41, 0xd8, 0x04, 0x08, 0x87, 0x4d, 0x81, 0x4d, + 0x90, 0x21, 0x90, 0x54, 0xd6, 0x04, 0x1a, + ], + "CPython codegen_match_inner() emits the success jump with NO_LOCATION" + ); + } + #[test] fn match_default_simple_guard_jump_uses_guard_location_like_cpython() { let code = compile_exec( @@ -18438,6 +18852,32 @@ def f(x, y): ); } + #[test] + fn match_mapping_keys_scaffolding_uses_mapping_location_like_cpython() { + let code = compile_exec( + "\ +def f(self): + x = {} + y = None + match x: + case {0: 0}: + y = 0 + self.assertIs(y, None) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd8, 0x08, 0x0a, 0x80, 0x41, 0xd8, 0x08, 0x0c, 0x80, 0x41, 0xd8, 0x0a, + 0x0b, 0xdf, 0x0d, 0x13, 0x8f, 0x56, 0x8a, 0x56, 0x95, 0x11, 0x89, 0x56, 0xd8, 0x10, + 0x11, 0x89, 0x41, 0xf2, 0x03, 0x00, 0x0e, 0x14, 0xe0, 0x04, 0x08, 0x87, 0x4d, 0x81, + 0x4d, 0x90, 0x21, 0x90, 0x54, 0xd6, 0x04, 0x1a, + ], + "CPython codegen_pattern_mapping() returns to LOC(p) for BUILD_TUPLE/MATCH_KEYS scaffolding" + ); + } + #[test] fn match_mapping_rest_cleanup_uses_mapping_location_like_cpython() { let code = compile_exec( @@ -18489,6 +18929,29 @@ def f(x): } } + #[test] + fn match_class_scaffolding_uses_class_pattern_location_like_cpython() { + let code = compile_exec( + "\ +def f(x): + match x: + case bool(z): + y = 0 + return y, z +", + ); + let f = find_code(&code, "f").expect("missing f code"); + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd8, 0x0a, 0x0b, 0xdc, 0x0d, 0x11, 0x8f, 0x57, 0x88, 0x57, 0xd8, 0x10, + 0x11, 0x88, 0x41, 0xd8, 0x0b, 0x0c, 0x88, 0x34, 0x80, 0x4b, 0xf0, 0x05, 0x00, 0x0e, + 0x15, 0xe0, 0x0b, 0x0c, 0x88, 0x61, 0x88, 0x34, 0x80, 0x4b, + ], + "CPython codegen_pattern_class() returns to LOC(p) after VISIT(cls)" + ); + } + #[test] fn match_class_wildcard_pop_uses_class_pattern_location_like_cpython() { let code = compile_exec( @@ -18912,6 +19375,77 @@ x = 1 or 2 or 3 ); } + #[test] + fn taken_constant_boolop_load_const_uses_literal_location_like_cpython() { + let code = compile_exec( + "\ +def and_false(x): + return False and x + +def or_true(x): + return True or x +", + ); + let and_false = find_code(&code, "and_false").expect("missing and_false code"); + let or_true = find_code(&code, "or_true").expect("missing or_true code"); + + // CPython 3.14 codegen_boolop() VISITs the selected literal before the + // short-circuit jump is optimized away, so the surviving LOAD_CONST + // keeps the literal range rather than the whole BoolOp range. + assert_eq!( + and_false.linetable.as_ref(), + &[0x81, 0x00, 0xd8, 0x0b, 0x10, 0xd0, 0x04, 0x16] + ); + assert_eq!( + or_true.linetable.as_ref(), + &[0x81, 0x00, 0xd8, 0x0b, 0x0f, 0xd0, 0x04, 0x14] + ); + } + + #[test] + fn assert_false_message_call_uses_assert_location_like_cpython() { + let code = compile_exec( + "\ +def f(): + assert False, \"x\" +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + // CPython 3.14 codegen_assert() emits LOAD_COMMON_CONSTANT and CALL + // at LOC(assert statement), then RAISE_VARARGS at LOC(test). + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd8, 0x04, 0x15, 0x90, 0x23, 0xd3, 0x04, 0x15, 0x88, 0x35, + ] + ); + } + + #[test] + fn static_swap_implicit_return_keeps_preswap_store_location_like_cpython() { + let code = compile_exec( + "\ +def f(a, b): + a, b = a, b + b, a = a, b +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + // CPython 3.14 flowgraph.c resolves line numbers before + // optimize_basic_block() turns BUILD_TUPLE/UNPACK_SEQUENCE into SWAP + // and apply_static_swaps() reorders the STORE_FAST pair. The + // synthetic return epilogue therefore keeps the pre-swap final store + // location. + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd8, 0x0b, 0x0c, 0x80, 0x71, 0xd8, 0x0b, 0x0c, 0x82, 0x71, + ] + ); + } + #[test] fn unpack_store_pair_jump_uses_second_target_location_like_cpython() { let code = compile_exec( @@ -19023,6 +19557,173 @@ def f(flag, n, exp): assert_eq!(jump_position, (3, 12, 3, 13)); } + #[test] + fn genexpr_make_closure_and_call_use_genexpr_location_like_cpython() { + let code = compile_exec( + "\ +def f(parameters): + return ((p, type(p)) for p in parameters) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let genexpr = find_code(f, "").expect("missing genexpr code"); + + // CPython 3.15 uses LOC(e) for MAKE_FUNCTION and the outer CALL. + // The genexpr preamble loads .0 and acquires the iterator at the + // outermost iterable's location before RETURN_GENERATOR. + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd9, 0x0b, 0x2d, 0xa0, 0x2a, 0xd3, 0x0b, 0x2d, 0xd0, 0x04, 0x2d, + ] + ); + assert_eq!( + genexpr.linetable.as_ref(), + &[ + 0xf9, 0xa2, 0x2a, 0xe9, 0x00, 0x81, 0x00, 0xa1, 0x2a, 0x98, 0x51, 0x94, 0x04, 0x90, + 0x51, 0x93, 0x07, 0x8e, 0x4c, 0xa3, 0x2a, 0xf9, + ] + ); + } + + #[test] + fn implicit_call_genexpr_range_includes_call_parens_like_cpython() { + let code = compile_exec( + "\ +def implicit(): + return list(x for x in range(10)) + +def explicit(): + return list((x for x in range(10))) +", + ); + let implicit = find_code(&code, "implicit").expect("missing implicit code"); + let implicit_gen = find_code(implicit, "").expect("missing implicit genexpr code"); + let explicit = find_code(&code, "explicit").expect("missing explicit code"); + let explicit_gen = find_code(explicit, "").expect("missing explicit genexpr code"); + + // CPython's parser gives an unparenthesized sole GeneratorExp call + // argument the call-parenthesized range, and codegen_comprehension() + // uses LOC(e) for MAKE_FUNCTION and the outer CALL. The eager .0 + // LOAD_FAST uses the iterable's range in the genexpr preamble. + // Explicitly parenthesized genexprs retain their own parentheses. + assert_eq!( + implicit.linetable.as_ref(), + &[ + 0x81, 0x00, 0xdf, 0x0b, 0x0f, 0x8c, 0x34, 0xd1, 0x0f, 0x25, 0x9c, 0x35, 0xa0, 0x12, + 0x9b, 0x39, 0xd3, 0x0f, 0x25, 0x8f, 0x34, 0xd0, 0x04, 0x25, 0x88, 0x34, 0xd1, 0x0f, + 0x25, 0x9c, 0x35, 0xa0, 0x12, 0x9b, 0x39, 0xd3, 0x0f, 0x25, 0xd3, 0x0b, 0x25, 0xd0, + 0x04, 0x25, + ] + ); + assert_eq!( + implicit_gen.linetable.as_ref(), + &[ + 0xf9, 0x9a, 0x39, 0xe9, 0x00, 0x81, 0x00, 0x99, 0x39, 0x90, 0x61, 0x95, 0x01, 0x9b, + 0x39, 0xf9, + ] + ); + assert_eq!( + explicit_gen.linetable.as_ref(), + &[ + 0xf9, 0x9a, 0x49, 0xe9, 0x00, 0x81, 0x00, 0x99, 0x49, 0x90, 0x71, 0x95, 0x11, 0x9b, + 0x49, 0xf9, + ] + ); + } + + #[test] + fn implicit_call_genexpr_parenthesized_element_range_like_cpython() { + let code = compile_exec( + "\ +def bytes_binop(): + return bytes((x ^ 0x5C) for x in range(256)) + +def dict_tuple(d): + return dict((v, k) for (k, v) in d.items()) + +def plain_tuple_elt(xs): + return list((x, y) for x, y in xs) + +def explicit_gen(xs): + return list(((x, y) for x, y in xs)) +", + ); + let bytes_binop = find_code(&code, "bytes_binop").expect("missing bytes_binop code"); + let bytes_gen = find_code(bytes_binop, "").expect("missing bytes genexpr code"); + let dict_tuple = find_code(&code, "dict_tuple").expect("missing dict_tuple code"); + let dict_gen = find_code(dict_tuple, "").expect("missing dict genexpr code"); + let plain_tuple_elt = + find_code(&code, "plain_tuple_elt").expect("missing plain_tuple_elt code"); + let plain_gen = + find_code(plain_tuple_elt, "").expect("missing plain genexpr code"); + let explicit_gen = find_code(&code, "explicit_gen").expect("missing explicit_gen code"); + let explicit_inner = + find_code(explicit_gen, "").expect("missing explicit genexpr code"); + + // CPython 3.15's parser includes the call argument parentheses in + // LOC(GeneratorExp) for implicit sole-argument generator expressions, + // even when the element expression itself starts with parentheses. + assert_eq!( + bytes_binop.linetable.as_ref(), + &[ + 0x81, 0x00, 0xdc, 0x0b, 0x10, 0xd1, 0x10, 0x30, 0xa4, 0x55, 0xa8, 0x33, 0xa3, 0x5a, + 0xd3, 0x10, 0x30, 0xd3, 0x0b, 0x30, 0xd0, 0x04, 0x30, + ] + ); + assert_eq!( + bytes_gen.linetable.as_ref(), + &[ + 0xf9, 0xa2, 0x5a, 0xe9, 0x00, 0x81, 0x00, 0xa1, 0x5a, 0xa0, 0x01, 0x90, 0x64, 0x97, + 0x28, 0x93, 0x28, 0xa3, 0x5a, 0xf9, + ] + ); + assert_eq!( + dict_tuple.linetable.as_ref(), + &[ + 0x81, 0x00, 0xdc, 0x0b, 0x0f, 0xd1, 0x0f, 0x2f, 0xa0, 0x51, 0xa7, 0x57, 0xa1, 0x57, + 0xa3, 0x59, 0xd3, 0x0f, 0x2f, 0xd3, 0x0b, 0x2f, 0xd0, 0x04, 0x2f, + ] + ); + assert_eq!( + dict_gen.linetable.as_ref(), + &[ + 0xf9, 0xa2, 0x59, 0xe9, 0x00, 0x81, 0x00, 0xa1, 0x59, 0x99, 0x36, 0x98, 0x41, 0x90, + 0x11, 0x96, 0x06, 0xa3, 0x59, 0xf9, + ] + ); + assert_eq!( + plain_tuple_elt.linetable.as_ref(), + &[ + 0x81, 0x00, 0xdf, 0x0b, 0x0f, 0x8c, 0x34, 0xd1, 0x0f, 0x26, 0xa0, 0x32, 0xd3, 0x0f, + 0x26, 0x8f, 0x34, 0xd0, 0x04, 0x26, 0x88, 0x34, 0xd1, 0x0f, 0x26, 0xa0, 0x32, 0xd3, + 0x0f, 0x26, 0xd3, 0x0b, 0x26, 0xd0, 0x04, 0x26, + ] + ); + assert_eq!( + plain_gen.linetable.as_ref(), + &[ + 0xf9, 0xa2, 0x32, 0xe9, 0x00, 0x81, 0x00, 0xa1, 0x32, 0x99, 0x34, 0x98, 0x31, 0x90, + 0x11, 0x96, 0x06, 0xa3, 0x32, 0xf9, + ] + ); + assert_eq!( + explicit_gen.linetable.as_ref(), + &[ + 0x81, 0x00, 0xdf, 0x0b, 0x0f, 0x8c, 0x34, 0xd1, 0x10, 0x27, 0xa0, 0x42, 0xd3, 0x10, + 0x27, 0x8f, 0x34, 0xd0, 0x04, 0x28, 0x88, 0x34, 0xd1, 0x10, 0x27, 0xa0, 0x42, 0xd3, + 0x10, 0x27, 0xd3, 0x0b, 0x28, 0xd0, 0x04, 0x28, + ] + ); + assert_eq!( + explicit_inner.linetable.as_ref(), + &[ + 0xf9, 0xa2, 0x42, 0xe9, 0x00, 0x81, 0x00, 0xa1, 0x42, 0x99, 0x44, 0x98, 0x41, 0x90, + 0x21, 0x96, 0x16, 0xa3, 0x42, 0xf9, + ] + ); + } + #[test] fn implicit_call_genexpr_operator_element_range_like_cpython() { // The element opens with a parenthesized group that the rest of the @@ -19083,6 +19784,71 @@ w = f'' 'a' f'' ); } + #[test] + fn genexpr_filter_cleanup_jumps_use_element_location_like_cpython() { + let code = compile_exec( + "\ +def simple(names): + return (x for x in names if not _ishidden(x)) + +def boolop(fields): + return (f for f in fields if f.init and not f.kw_only) +", + ); + let simple = find_code(&code, "simple").expect("missing simple code"); + let simple_gen = find_code(simple, "").expect("missing simple genexpr code"); + let boolop = find_code(&code, "boolop").expect("missing boolop code"); + let boolop_gen = find_code(boolop, "").expect("missing boolop genexpr code"); + + // codegen_sync_comprehension_generator() emits the + // comprehension guard jump to if_cleanup, then emits the if_cleanup + // backedge with elt_loc. flowgraph.c::jump_thread() copies that target + // jump location to the threaded POP_JUMP/NOT_TAKEN cleanup path. + assert_eq!( + simple_gen.linetable.as_ref(), + &[ + 0xf9, 0x92, 0x75, 0xe9, 0x00, 0x81, 0x00, 0x91, 0x75, 0x90, 0x21, 0xa4, 0x49, 0xa8, + 0x61, 0xa7, 0x4c, 0x8f, 0x41, 0x8b, 0x41, 0x93, 0x75, 0xf9, + ] + ); + assert_eq!( + boolop_gen.linetable.as_ref(), + &[ + 0xf9, 0x92, 0x76, 0xe9, 0x00, 0x81, 0x00, 0x91, 0x76, 0x90, 0x21, 0xa7, 0x16, 0xa5, + 0x16, 0x8c, 0x41, 0xb0, 0x01, 0xb7, 0x09, 0xb5, 0x09, 0x8f, 0x41, 0x8b, 0x41, 0x93, + 0x76, 0xf9, + ] + ); + } + + #[test] + fn try_finally_exception_scaffolding_uses_no_location_like_cpython() { + let code = compile_exec( + "\ +def f(self, node): + self.flag = True + try: + self.body(node) + finally: + self.flag = False +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + // codegen_try_finally() emits the exception path + // SETUP_CLEANUP/PUSH_EXC_INFO and POP_EXCEPT_AND_RERAISE with + // NO_LOCATION; flowgraph line propagation then gives only the + // finalbody's direct RERAISE the finalbody location. + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd8, 0x10, 0x14, 0x80, 0x44, 0x84, 0x49, 0xf0, 0x02, 0x03, 0x05, 0x1a, + 0xd8, 0x08, 0x0c, 0x8f, 0x09, 0x89, 0x09, 0x90, 0x24, 0x8c, 0x0f, 0xe0, 0x14, 0x19, + 0x88, 0x04, 0x8e, 0x09, 0xf8, 0x90, 0x45, 0x88, 0x04, 0x8d, 0x09, 0xfa, + ] + ); + } + #[test] fn return_debug_in_finally_uses_cpython_preprocessed_constant_order() { let code = compile_exec( @@ -19208,6 +19974,76 @@ def f(close): ); } + #[test] + fn adjacent_no_location_entries_merge_like_cpython() { + let code = compile_exec( + "\ +def f(file): + if sys.platform == \"win32\": + try: + import nt + if not nt._supports_virtual_terminal(): + return False + except (ImportError, AttributeError): + return False + try: + return os.isatty(file.fileno()) + except OSError: + return hasattr(file, \"isatty\") and file.isatty() +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + // CPython's NO_LOCATION is {-1, -1, -1, -1}, and + // assemble.c::assemble_location_info() merges adjacent instructions + // with the same NO_LOCATION into one linetable entry. + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xdc, 0x07, 0x0a, 0x87, 0x7c, 0x81, 0x7c, 0x90, 0x77, 0xd4, 0x07, 0x1e, + 0xf0, 0x02, 0x05, 0x09, 0x19, 0xdb, 0x0c, 0x15, 0xd8, 0x13, 0x15, 0xd7, 0x13, 0x30, + 0xd1, 0x13, 0x30, 0xd7, 0x13, 0x32, 0xd2, 0x13, 0x32, 0xd9, 0x17, 0x1c, 0xf0, 0x03, + 0x00, 0x14, 0x33, 0xf0, 0x08, 0x03, 0x05, 0x39, 0xdc, 0x0f, 0x11, 0x8f, 0x79, 0x89, + 0x79, 0x98, 0x14, 0x9f, 0x1b, 0x99, 0x1b, 0x9b, 0x1d, 0xd3, 0x0f, 0x27, 0xd0, 0x08, + 0x27, 0xf8, 0xf4, 0x07, 0x00, 0x11, 0x1c, 0x9c, 0x5e, 0xd0, 0x0f, 0x2c, 0xf4, 0x00, + 0x01, 0x09, 0x19, 0xda, 0x13, 0x18, 0xf0, 0x03, 0x01, 0x09, 0x19, 0xfb, 0xf4, 0x08, + 0x00, 0x0c, 0x13, 0xf4, 0x00, 0x01, 0x05, 0x39, 0xdc, 0x0f, 0x16, 0x90, 0x74, 0x98, + 0x58, 0xd3, 0x0f, 0x26, 0xd7, 0x0f, 0x38, 0xd0, 0x0f, 0x38, 0xa8, 0x34, 0xaf, 0x3b, + 0xa9, 0x3b, 0xab, 0x3d, 0xd2, 0x08, 0x38, 0xf0, 0x03, 0x01, 0x05, 0x39, 0xfa, + ] + ); + } + + #[test] + fn fstring_format_ops_use_formatted_value_location_like_cpython() { + let code = compile_exec( + "\ +def simple(self): + return f'{self.value}' + +def spec(x): + return f'{x!r:>3}' +", + ); + let simple = find_code(&code, "simple").expect("missing simple code"); + let spec = find_code(&code, "spec").expect("missing spec code"); + + // CPython 3.14 codegen_formatted_value() VISITs the inner expression + // first, then emits CONVERT_VALUE / FORMAT_* at LOC(FormattedValue). + assert_eq!( + simple.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd8, 0x0e, 0x12, 0x8f, 0x6a, 0x89, 0x6a, 0x88, 0x5c, 0xd0, 0x04, 0x1a, + ] + ); + assert_eq!( + spec.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd8, 0x0e, 0x0f, 0x88, 0x58, 0x90, 0x22, 0x88, 0x58, 0xd0, 0x04, 0x16, + ] + ); + } + #[test] fn debug_fstring_literal_merging_and_location_like_cpython() { fn string_load_position(code: &CodeObject, expected: &str) -> (usize, usize, usize, usize) { @@ -19305,6 +20141,29 @@ def padded(digits, int_len): ); } + #[test] + fn joined_string_literals_extend_pending_literal_location_like_cpython() { + let code = compile_exec( + "\ +def f(a): + return ( + 'x' + f'y{a}z' + 'w' + ) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xf0, 0x04, 0x01, 0x09, 0x0c, 0xd8, 0x0c, 0x0d, 0x88, 0x33, 0xf0, 0x00, + 0x01, 0x0f, 0x0c, 0xf0, 0x03, 0x02, 0x09, 0x0c, 0xf0, 0x03, 0x04, 0x05, 0x06, + ], + "CPython parser/codegen represents adjacent f-string literal fragments as Constant ranges spanning the merged fragments" + ); + } + #[test] fn starred_call_preserves_bool_op_short_circuit_shape() { let code = compile_exec( @@ -19377,6 +20236,60 @@ def outer(null): ); } + #[test] + fn decorated_definitions_use_cpython_locations() { + let code = compile_exec( + "\ +def dec(f): return f + +class C: + @dec + def f(self): + yield + +@dec +class D: + pass + +class E: + @dec + def g(self, flags: int, /) -> memoryview: + raise NotImplementedError +", + ); + let c = find_code(&code, "C").expect("missing C code"); + let d = find_code(&code, "D").expect("missing D code"); + let e = find_code(&code, "E").expect("missing E code"); + let annotate = find_code(e, "__annotate__").expect("missing annotation code"); + + // CPython 3.14 codegen_function()/codegen_class() evaluate + // decorators first, then use LOC(s) for codegen_make_closure() and + // codegen_nameop(); codegen_apply_decorators() emits CALL at each + // decorator expression's location. + assert_eq!( + c.linetable.as_ref(), + &[ + 0xf8, 0x87, 0x00, 0x81, 0x00, 0xd8, 0x05, 0x08, 0xf1, 0x02, 0x01, 0x05, 0x0e, 0xf3, + 0x03, 0x00, 0x06, 0x09, 0xf6, 0x02, 0x01, 0x05, 0x0e, + ] + ); + assert_eq!(d.linetable.as_ref(), &[0x87, 0x00, 0xe3, 0x04, 0x08]); + assert_eq!( + e.linetable.as_ref(), + &[ + 0xf8, 0x87, 0x00, 0x81, 0x00, 0xd8, 0x05, 0x08, 0xf7, 0x02, 0x01, 0x05, 0x22, 0xf3, + 0x03, 0x00, 0x06, 0x09, 0xf6, 0x02, 0x01, 0x05, 0x22, + ] + ); + assert_eq!( + annotate.linetable.as_ref(), + &[ + 0xf8, 0x81, 0x00, 0xf7, 0x00, 0x01, 0x05, 0x22, 0xf1, 0x00, 0x01, 0x05, 0x22, 0x91, + 0x73, 0xf0, 0x00, 0x01, 0x05, 0x22, 0xa1, 0x2a, 0xf1, 0x00, 0x01, 0x05, 0x22, + ] + ); + } + #[test] fn taken_constant_boolop_jump_disables_following_borrows() { for source in [ @@ -20152,6 +21065,56 @@ def outer(log): ); } + #[test] + fn builtin_any_genexpr_call_is_optimized() { + let code = compile_exec( + "\ +def f(xs): + return any(x for x in xs) +", + ); + let f = find_code(&code, "f").expect("missing function code"); + + assert!(has_common_constant(f, bytecode::CommonConstant::BuiltinAny)); + assert!( + f.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::PopJumpIfTrue { .. })) + ); + assert!( + f.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::NotTaken)) + ); + assert_eq!( + f.instructions + .iter() + .filter(|unit| matches!(unit.op, Instruction::PushNull)) + .count(), + 2, + "iterator index and fallback call path should remain for shadowed any()" + ); + let genexpr_const_count = f + .constants + .iter() + .filter(|constant| { + matches!(constant, ConstantData::Code { code } if code.obj_name == "") + }) + .count(); + assert_eq!( + genexpr_const_count, 1, + "optimized and fallback any(genexpr) paths should share the same CPython-range code const" + ); + assert_eq!( + f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xdf, 0x0b, 0x0e, 0x8b, 0x33, 0x89, 0x6f, 0x98, 0x22, 0x8b, 0x6f, 0x8f, + 0x33, 0x8d, 0x33, 0xd0, 0x04, 0x1d, 0x8a, 0x33, 0xd0, 0x04, 0x1d, 0x88, 0x33, 0x89, + 0x6f, 0x98, 0x22, 0x8b, 0x6f, 0xd3, 0x0b, 0x1d, 0xd0, 0x04, 0x1d, + ] + ); + } + #[test] fn builtin_any_async_genexpr_call_is_not_optimized_like_cpython() { for source in [ @@ -20198,6 +21161,84 @@ async def f(get_xs): ); } + #[test] + fn builtin_tuple_list_set_genexpr_calls_are_optimized_like_cpython() { + let code = compile_exec( + "\ +def tuple_f(xs): + return tuple(x for x in xs) + +def list_f(xs): + return list(x for x in xs) + +def set_f(xs): + return set(x for x in xs) +", + ); + + let tuple_f = find_code(&code, "tuple_f").expect("missing tuple_f code"); + assert!(has_common_constant( + tuple_f, + bytecode::CommonConstant::BuiltinTuple + )); + assert!(has_intrinsic_1(tuple_f, IntrinsicFunction1::ListToTuple)); + let tuple_list_append = tuple_f + .instructions + .iter() + .find_map(|unit| match unit.op { + Instruction::ListAppend { .. } => Some(u32::from(u8::from(unit.arg))), + _ => None, + }) + .expect("tuple(genexpr) fast path should emit LIST_APPEND"); + assert_eq!(tuple_list_append, 3); + assert_eq!( + tuple_f.linetable.as_ref(), + &[ + 0x81, 0x00, 0xdf, 0x0b, 0x10, 0x8c, 0x35, 0x91, 0x0f, 0x98, 0x42, 0x93, 0x0f, 0x8f, + 0x35, 0x88, 0x35, 0xd0, 0x04, 0x1f, 0x88, 0x35, 0x91, 0x0f, 0x98, 0x42, 0x93, 0x0f, + 0xd3, 0x0b, 0x1f, 0xd0, 0x04, 0x1f + ] + ); + + let list_f = find_code(&code, "list_f").expect("missing list_f code"); + assert!( + list_f + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::Call { .. })), + "list(genexpr) retains the shadowed-name fallback call" + ); + assert!( + has_common_constant(list_f, bytecode::CommonConstant::BuiltinList), + "CPython 3.15 guards list(genexpr) with the builtin list constant" + ); + + let set_f = find_code(&code, "set_f").expect("missing set_f code"); + assert!( + set_f + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::Call { .. })), + "set(genexpr) retains the shadowed-name fallback call" + ); + assert!( + has_common_constant(set_f, bytecode::CommonConstant::BuiltinSet), + "CPython 3.15 guards set(genexpr) with the builtin set constant" + ); + assert!( + list_f + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::ListAppend { .. }) + && u8::from(unit.arg) == 3) + ); + assert!( + set_f.instructions.iter().any( + |unit| matches!(unit.op, Instruction::SetAdd { .. }) && u8::from(unit.arg) == 3 + ) + ); + } + #[test] fn builtin_tuple_genexpr_try_assignment_uses_shared_tail() { let code = compile_exec( @@ -20636,6 +21677,27 @@ def aug(x, a, b, y): ); } + #[test] + fn augassign_constant_slice_copy_uses_subscript_location_like_cpython() { + let code = compile_exec( + "\ +def aug_const(x, y): + x[1:2] += y +", + ); + let aug_const = find_code(&code, "aug_const").expect("missing aug_const code"); + + // CPython 3.14 codegen_augassign() visits a constant slice, then emits + // COPY/COPY/BINARY_OP NB_SUBSCR at LOC(target), not at LOC(slice). + assert_eq!( + aug_const.linetable.as_ref(), + &[ + 0x81, 0x00, 0xd8, 0x04, 0x05, 0x80, 0x63, 0x87, 0x46, 0x88, 0x61, 0x85, 0x4b, 0x85, + 0x46, + ] + ); + } + #[test] fn augassign_attribute_copy_uses_target_location_like_cpython() { let code = compile_exec( @@ -32284,6 +33346,30 @@ def g(): ); } + #[test] + fn comprehension_list_iterable_build_uses_iter_location_like_cpython() { + let code = compile_exec( + "\ +async def f(i): + return i + +async def run_list(): + return [await c for c in [f(1), f(41)]] +", + ); + let run_list = find_code(&code, "run_list").expect("missing run_list code"); + assert_eq!( + run_list.linetable.as_ref(), + &[ + 0xe9, 0x00, 0x81, 0x00, 0xdc, 0x1e, 0x1f, 0xa0, 0x01, 0x9b, 0x64, 0xa4, 0x41, 0xa0, + 0x62, 0xa3, 0x45, 0x98, 0x5d, 0xd3, 0x0b, 0x2b, 0x9b, 0x5d, 0x98, 0x01, 0x8f, 0x47, + 0x8c, 0x47, 0x99, 0x5d, 0xd1, 0x0b, 0x2b, 0xd0, 0x04, 0x2b, 0x89, 0x47, 0xf9, 0xd2, + 0x0b, 0x2b, 0xf9, + ], + "CPython codegen_comprehension_iter() emits GET_ITER at LOC(comp->iter)" + ); + } + #[test] fn comprehension_boolop_iter_get_iter_uses_iter_location_like_cpython() { let code = compile_exec( @@ -32314,6 +33400,30 @@ def f(self): ); } + #[test] + fn inlined_comprehension_backedges_use_element_location_like_cpython() { + let code = compile_exec( + "\ +async def f(i): + return i + +async def run_list(): + return [s for c in [f(''), f('abc')] for s in await c] +", + ); + let run_list = find_code(&code, "run_list").expect("missing run_list code"); + assert_eq!( + run_list.linetable.as_ref(), + &[ + 0xe9, 0x00, 0x81, 0x00, 0xdc, 0x18, 0x19, 0x98, 0x22, 0x9b, 0x05, 0x9c, 0x71, 0xa0, + 0x15, 0x9b, 0x78, 0xd0, 0x17, 0x28, 0xd4, 0x0b, 0x3a, 0xd3, 0x17, 0x28, 0x90, 0x21, + 0xb7, 0x27, 0xb5, 0x27, 0xa8, 0x51, 0x8a, 0x41, 0xb1, 0x27, 0x89, 0x41, 0xd1, 0x17, + 0x28, 0xd2, 0x0b, 0x3a, 0xd0, 0x04, 0x3a, 0xb1, 0x27, 0xf9, 0xd3, 0x0b, 0x3a, 0xf9, + ], + "CPython codegen_sync_comprehension_generator() emits comprehension backedges at elt_loc" + ); + } + #[test] fn nested_dict_comprehension_outer_backedge_uses_key_location_like_cpython() { let code = compile_exec( @@ -32405,6 +33515,53 @@ def f(fields): ); } + #[test] + fn inlined_async_comprehension_end_async_for_uses_comprehension_location_like_cpython() { + let code = compile_exec( + "\ +async def f(it): + for i in it: + yield i + +async def run_list(): + return [i + 1 async for i in f([10, 20])] +", + ); + let run_list = find_code(&code, "run_list").expect("missing run_list code"); + assert_eq!( + run_list.linetable.as_ref(), + &[ + 0xe9, 0x00, 0x81, 0x00, 0xdc, 0x21, 0x22, 0xa0, 0x42, 0xa8, 0x02, 0xa0, 0x38, 0xa3, + 0x1b, 0xd3, 0x0b, 0x2d, 0xa0, 0x1b, 0xd7, 0x0b, 0x2d, 0xd1, 0x0b, 0x2d, 0x98, 0x41, + 0x90, 0x01, 0x8f, 0x45, 0x88, 0x45, 0xd4, 0x0b, 0x2d, 0xd0, 0x04, 0x2d, 0xf9, 0xd2, + 0x0b, 0x2d, 0xf9 + ], + "CPython codegen_async_comprehension_generator() emits END_ASYNC_FOR at comprehension loc" + ); + } + + #[test] + fn async_for_anext_sequence_uses_statement_location_like_cpython() { + let code = compile_exec( + "\ +async def f(source, buffer): + async for i1, i2 in source(): + buffer.append(i1 + i2) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + assert_eq!( + f.linetable.as_ref(), + &[ + 0xe9, 0x00, 0x81, 0x00, 0xd9, 0x18, 0x1e, 0x9c, 0x08, 0xf7, 0x00, 0x01, 0x05, 0x1f, + 0xf2, 0x00, 0x01, 0x05, 0x1f, 0x89, 0x66, 0x88, 0x62, 0xd8, 0x08, 0x0e, 0x8f, 0x0d, + 0x89, 0x0d, 0x90, 0x62, 0x95, 0x67, 0xd6, 0x08, 0x1e, 0xf1, 0x03, 0x01, 0x05, 0x1f, + 0x9a, 0x08, 0xf9, + ], + "CPython codegen_async_for() emits GET_ANEXT/yield-from scaffolding at LOC(s)" + ); + } + #[test] fn nested_comprehension_list_iterable_keeps_list_before_saved_locals() { let code = compile_exec( @@ -33085,6 +34242,27 @@ def f(seq, value): })); } + #[test] + fn namedexpr_copy_uses_namedexpr_location_like_cpython() { + let code = compile_exec( + "\ +def outer(): + a = 10 + def spam(): + nonlocal a + (a := 20) +", + ); + let spam = find_code(&code, "spam").expect("missing spam code"); + + // CPython 3.14 NamedExpr_kind emits COPY at LOC(named expression), + // between visiting the value and visiting the target. + assert_eq!( + spam.linetable.as_ref(), + &[0xf8, 0x81, 0x00, 0xe0, 0x0e, 0x10, 0x88, 0x17, 0x8b, 0x11,] + ); + } + #[test] fn genexpr_namedexpr_target_is_cell_not_fast_local() { let code = compile_exec( diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index 9976d485029..917b189c715 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -5445,19 +5445,24 @@ fn fold_constant_sequence_into_load_const( block: &mut Block, i: usize, ) -> crate::InternalResult { - let (intrinsic, is_set) = match block.instructions[i].instr.real() { + let (append_op, build_op, mut expect_append) = match block.instructions[i].instr.real() { Some(Instruction::CallIntrinsic1 { func }) if func.get(block.instructions[i].arg) == IntrinsicFunction1::ListToTuple => { - (true, false) + (Opcode::ListAppend, Opcode::BuildList, true) + } + Some(Instruction::ListAppend { .. }) if u32::from(block.instructions[i].arg) == 1 => { + (Opcode::ListAppend, Opcode::BuildList, false) + } + Some(Instruction::SetAdd { .. }) if u32::from(block.instructions[i].arg) == 1 => { + (Opcode::SetAdd, Opcode::BuildSet, false) } - Some(Instruction::ListAppend { .. }) => (false, false), - Some(Instruction::SetAdd { .. }) => (false, true), _ => return Ok(false), }; + // An intrinsic follows the last append; an append target follows its value. + let includes_target = !expect_append; let mut consts_found = 0usize; - let mut expect_append = intrinsic; let mut pos = i; while let Some(prev) = pos.checked_sub(1) { pos = prev; @@ -5466,12 +5471,7 @@ fn fold_constant_sequence_into_load_const( continue; } - let is_start = if is_set { - matches!(instr.instr.real(), Some(Instruction::BuildSet { .. })) - } else { - matches!(instr.instr.real(), Some(Instruction::BuildList { .. })) - }; - if is_start && u32::from(instr.arg) == 0 { + if instr.instr.real_opcode() == Some(build_op) && u32::from(instr.arg) == 0 { if !expect_append { return Ok(false); } @@ -5480,7 +5480,7 @@ fn fold_constant_sequence_into_load_const( elements .try_reserve_exact(consts_found) .map_err(|_| InternalError::MalformedControlFlowGraph)?; - let end = if intrinsic { i } else { i + 1 }; + let end = i + usize::from(includes_target); for idx in (pos..end).rev() { if matches!(block.instructions[idx].instr.real(), Some(Instruction::Nop)) { continue; @@ -5491,29 +5491,26 @@ fn fold_constant_sequence_into_load_const( }; elements.push(value); } - block.instructions[idx].nop_out_no_location(); } debug_assert_eq!(elements.len(), consts_found); elements.reverse(); - instr_make_load_const( - metadata, - &mut block.instructions[i], - if is_set { - ConstantData::Frozenset { elements } - } else { - ConstantData::Tuple { elements } - }, - )?; + let constant = if build_op == Opcode::BuildSet { + ConstantData::Frozenset { elements } + } else { + ConstantData::Tuple { elements } + }; + // Leave the sequence intact if a constant cannot be represented. + for instr in &mut block.instructions[pos..end] { + if !matches!(instr.instr.real(), Some(Instruction::Nop)) { + instr.nop_out_no_location(); + } + } + instr_make_load_const(metadata, &mut block.instructions[i], constant)?; return Ok(true); } if expect_append { - let is_append = if is_set { - matches!(instr.instr.real(), Some(Instruction::SetAdd { .. })) - } else { - matches!(instr.instr.real(), Some(Instruction::ListAppend { .. })) - }; - if !is_append || u32::from(instr.arg) != 1 { + if instr.instr.real_opcode() != Some(append_op) || u32::from(instr.arg) != 1 { return Ok(false); } } else { @@ -7369,6 +7366,131 @@ mod tests { } } + fn test_sequence_block(instructions: &[(Opcode, u32)]) -> Block { + let mut block = Block::default(); + for (i, &(op, arg)) in instructions.iter().enumerate() { + let mut instr = test_instr(op.into(), i as u32 + 1); + instr.arg = OpArg::new(arg); + test_block_push(&mut block, instr); + } + block + } + + #[test] + fn constant_append_sequences_fold_for_iteration_and_membership() { + for (build, append) in [ + (Opcode::BuildList, Opcode::ListAppend), + (Opcode::BuildSet, Opcode::SetAdd), + ] { + for consumer in [Opcode::GetIter, Opcode::ContainsOp] { + for intrinsic in [false, true] { + if intrinsic && build == Opcode::BuildSet { + continue; + } + let mut instructions = vec![(build, 0)]; + for value in [3, 1, 3] { + instructions.extend([(Opcode::LoadSmallInt, value), (append, 1)]); + } + if intrinsic { + instructions.push(( + Opcode::CallIntrinsic1, + IntrinsicFunction1::ListToTuple as u32, + )); + } + let folded_index = instructions.len() - 1; + instructions.push((consumer, 0)); + let mut info = test_code_info(test_sequence_block(&instructions)); + info.blocks + .optimize_basic_block(&mut info.metadata, BlockIdx::new(0)) + .unwrap(); + let block = &info.blocks[0]; + assert!( + block.instructions[..folded_index] + .iter() + .all(|instr| { instr.instr.real_opcode() == Some(Opcode::Nop) }) + ); + let expected = if build == Opcode::BuildSet { + ConstantData::Frozenset { + elements: vec![int_const(3), int_const(1)], + } + } else { + ConstantData::Tuple { + elements: vec![int_const(3), int_const(1), int_const(3)], + } + }; + assert_eq!( + get_const_value(&info.metadata, &block.instructions[folded_index]), + Some(expected) + ); + assert_eq!( + block.instructions[folded_index + 1].instr.real_opcode(), + Some(consumer) + ); + } + } + } + } + + #[test] + fn constant_append_sequences_require_unaliased_matching_constant_elements() { + for instructions in [ + vec![ + (Opcode::BuildList, 0), + (Opcode::LoadName, 0), + (Opcode::ListAppend, 1), + ], + vec![ + (Opcode::BuildList, 0), + (Opcode::LoadSmallInt, 1), + (Opcode::SetAdd, 1), + ], + vec![ + (Opcode::BuildSet, 0), + (Opcode::LoadSmallInt, 1), + (Opcode::ListAppend, 1), + ], + vec![ + (Opcode::BuildList, 0), + (Opcode::LoadSmallInt, 1), + (Opcode::ListAppend, 2), + ], + vec![ + (Opcode::BuildList, 0), + (Opcode::BuildList, 0), + (Opcode::ListAppend, 1), + ], + vec![ + (Opcode::BuildList, 0), + (Opcode::Copy, 1), + (Opcode::LoadSmallInt, 1), + (Opcode::ListAppend, 1), + ], + vec![ + (Opcode::BuildList, 0), + (Opcode::LoadConst, 0), + (Opcode::ListAppend, 1), + (Opcode::LoadSmallInt, 1), + (Opcode::ListAppend, 1), + ], + ] { + let mut block = test_sequence_block(&instructions); + let mut metadata = test_code_info(Block::default()).metadata; + assert!( + !fold_constant_sequence_into_load_const( + &mut metadata, + &mut block, + instructions.len() - 1, + ) + .unwrap() + ); + assert!(metadata.consts.is_empty()); + for (instr, (op, arg)) in block.instructions.iter().zip(instructions) { + assert_eq!(instr.instr.real_opcode(), Some(op)); + assert_eq!(u32::from(instr.arg), arg); + } + } + } + #[test] fn get_stack_effects_rejects_cpython_deopt_opcodes() { match get_stack_effects(Instruction::BinaryOpAddInt.into(), OpArg::new(0), 0) { @@ -7571,6 +7693,87 @@ mod tests { ); } + #[test] + fn redundant_nop_compaction_clears_vacated_instruction_metadata() { + let handler = ExceptHandlerInfo { + handler_block: BlockIdx::new(2), + preserve_lasti: true, + }; + let mut block = Block::default(); + for _ in 0..2 { + let mut nop = test_instr(Instruction::Nop, 10); + nop.lineno_override = Some(NO_LOCATION_OVERRIDE); + nop.target = BlockIdx::new(1); + nop.except_handler = Some(handler); + test_block_push(&mut block, nop); + } + let mut jump = test_instr(Opcode::PopJumpIfTrue.into(), 10); + jump.target = BlockIdx::new(1); + jump.except_handler = Some(handler); + test_block_push(&mut block, jump); + let mut blocks = Blocks::from([block, Block::default(), Block::default()]); + + assert_eq!(blocks.basicblock_remove_redundant_nops(BlockIdx::new(0)), 2); + assert_eq!(blocks[0].instructions[0].target, jump.target); + assert_eq!(blocks[0].instructions[0].except_handler, Some(handler)); + for slot in &blocks[0].instructions[1..3] { + assert_eq!(slot.target, BlockIdx::NULL); + assert_eq!(slot.except_handler, None); + } + + blocks.normalize_jumps_in_block(BlockIdx::new(0)).unwrap(); + let not_taken = &blocks[0].instructions[1]; + assert_eq!(not_taken.instr.real_opcode(), Some(Opcode::NotTaken)); + assert_eq!(not_taken.except_handler, None); + } + + #[test] + fn with_cleanup_exception_table_excludes_synthetic_not_taken() { + use rustpython_compiler_core::{Mode, SourceFileBuilder, bytecode::decode_exception_table}; + + let source = "def f(c):\n with c:\n x = 1\n y = 2\n"; + let source_file = SourceFileBuilder::new("source_path", source).finish(); + let ast = ruff_python_parser::parse(source, ruff_python_parser::Mode::Module.into()) + .unwrap() + .into_syntax(); + let module = crate::compile::compile_top( + ast, + source_file, + Mode::Exec, + crate::CompileOpts::default(), + ) + .unwrap(); + let code = module + .constants + .iter() + .find_map(|constant| match constant { + ConstantData::Code { code } => Some(code), + _ => None, + }) + .expect("missing with function"); + let entries = decode_exception_table(&code.exceptiontable); + let not_taken = code + .instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::NotTaken)) + .expect("missing with cleanup NOT_TAKEN") as u32; + + // CPython 3.15rc3 Lib/test/test_dis.py's _with fixture has a body + // range and two cleanup ranges separated by the synthetic NOT_TAKEN. + assert_eq!(entries.len(), 3); + assert!( + entries + .iter() + .all(|entry| { not_taken < entry.start || not_taken >= entry.end }) + ); + assert_eq!(entries[1].end, not_taken); + assert_eq!(entries[2].start, not_taken + 1); + assert_eq!(entries[1].target, entries[2].target); + assert_eq!(entries[1].depth, 4); + assert_eq!(entries[2].depth, 4); + assert!(entries[1].push_lasti && entries[2].push_lasti); + } + #[test] fn basicblock_addop_reuses_cpython_spare_except_handler_slot() { let handler = ExceptHandlerInfo { @@ -7922,6 +8125,69 @@ mod tests { )); } + #[test] + fn constant_sequence_loads_use_cpython_opcode_has_const_metadata() { + let mut metadata = CodeUnitMetadata { + name: "".to_owned(), + qualname: Some("".to_owned()), + consts: Default::default(), + names: IndexSet::default(), + varnames: IndexSet::default(), + cellvars: IndexSet::default(), + freevars: IndexSet::default(), + fast_hidden: IndexMap::default(), + fast_hidden_final: IndexSet::default(), + argcount: 0, + posonlyargcount: 0, + kwonlyargcount: 0, + firstlineno: OneIndexed::MIN, + }; + let (left, _) = metadata + .consts + .insert_full(ConstantData::Str { value: "a".into() }); + let (right, _) = metadata + .consts + .insert_full(ConstantData::Str { value: "b".into() }); + + let mut first_load = test_instr(Instruction::Nop, 90); + first_load.instr = Opcode::LoadConst.into(); + first_load.arg = OpArg::new(left as u32); + let mut second_load = test_instr(Instruction::Nop, 90); + second_load.instr = Opcode::LoadConst.into(); + second_load.arg = OpArg::new(right as u32); + let mut build = test_instr(Opcode::BuildTuple.into(), 90); + build.arg = OpArg::new(2); + let mut block = Block::default(); + for info in [first_load, second_load, build] { + test_block_push(&mut block, info); + } + + assert!( + fold_tuple_of_constants(&mut metadata, &mut block, 2) + .expect("fold_tuple_of_constants succeeds") + ); + + // CPython 3.15 represents constant-pool loads with LOAD_CONST. + // Both loads must participate in tuple folding and become NOPs. + assert!(matches!( + block.instructions[0].instr.real(), + Some(Instruction::Nop) + )); + assert!(matches!( + block.instructions[1].instr.real(), + Some(Instruction::Nop) + )); + let folded = &block.instructions[2]; + assert!(matches!( + folded.instr.real(), + Some(Instruction::LoadConst { .. }) + )); + assert!(matches!( + &metadata.consts[u32::from(folded.arg) as usize], + ConstantData::Tuple { elements } if elements.len() == 2 + )); + } + #[test] fn empty_tuple_repeat_folds_negative_count_like_cpython() { let folded = const_folding_safe_multiply( diff --git a/crates/codegen/src/symboltable.rs b/crates/codegen/src/symboltable.rs index 4af1ee0bed4..91bc5b50d01 100644 --- a/crates/codegen/src/symboltable.rs +++ b/crates/codegen/src/symboltable.rs @@ -1455,7 +1455,7 @@ impl SymbolTableBuilder { if table.symbols.contains_key(parameter.name.as_str()) { return Err(self.error_ranged( format!( - "duplicate argument '{}' in function definition", + "duplicate parameter '{}' in function definition", parameter.name ), parameter.name.range, @@ -2419,16 +2419,14 @@ impl SymbolTableBuilder { self.in_iter_def_exp = true; } // Dict comprehension - is_generator = false (can be inlined) - let Some(key) = key.as_deref() else { - return Err(self.error_ranged( - "dict unpacking cannot be used in dict comprehension".to_owned(), - *range, - )); + let (element, value) = match key.as_deref() { + Some(key) => (key, Some(value.as_ref())), + None => (value.as_ref(), None), }; self.scan_comprehension( &"".into(), - key, - Some(value), + element, + value, generators, *range, false, @@ -3204,7 +3202,7 @@ impl SymbolTableBuilder { if matches!(role, SymbolUsage::Parameter) && flags.contains(SymbolFlags::DEF_PARAM) { return Err(SymbolTableError { - error: format!("duplicate argument '{original_name}' in function definition"), + error: format!("duplicate parameter '{original_name}' in function definition"), location, end_location, }); @@ -3489,7 +3487,7 @@ mod tests { assert_eq!( err.error, - "duplicate argument '_C__x' in function definition" + "duplicate parameter '_C__x' in function definition" ); } diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 636182c7708..d27464baacc 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -544,6 +544,19 @@ fn default_parse_diagnostic( error: parser::ParseError, source_file: &SourceFile, ) -> NormalizedParseDiagnostic { + if let parser::ParseErrorType::MissingCommaAfterConcatenatedLiteral { + diagnostic_start, .. + } = &error.error + { + // Concatenation hints can have empty or reversed CPython diagnostic ranges. + let (location, end_location) = + source_locations(source_file, *diagnostic_start, error.location.end()); + return NormalizedParseDiagnostic::new(error.error, location, end_location); + } + if matches!(error.error, parser::ParseErrorType::EmptyImportNames) { + let location = source_location(source_file, error.location.start()); + return NormalizedParseDiagnostic::new(error.error, location, location); + } let (loc, end_loc) = adjusted_error_locations(source_file, error.location); NormalizedParseDiagnostic::new(error.error, loc, end_loc) } @@ -1107,6 +1120,55 @@ pub fn too_deeply_nested_error( })) } +/// Source-only rejection performed by CPython's checked_from_import parser action. +#[doc(hidden)] +#[must_use] +pub fn lazy_future_import_error(ast: &ast::Mod, source_file: &SourceFile) -> Option { + use ast::statement_visitor::StatementVisitor; + + struct Checker<'a> { + pending: Vec<&'a ast::Stmt>, + } + + impl<'a> StatementVisitor<'a> for Checker<'a> { + fn visit_stmt(&mut self, stmt: &'a ast::Stmt) { + self.pending.push(stmt); + } + } + + let ast::Mod::Module(module) = ast else { + return None; + }; + let mut checker = Checker { + pending: module.body.iter().collect(), + }; + let mut first = None; + while let Some(stmt) = checker.pending.pop() { + if let ast::Stmt::ImportFrom(import) = stmt + && import.is_lazy + && import.level == 0 + && import + .module + .as_ref() + .is_some_and(|name| name.as_str() == "__future__") + { + let start = import.range.start().to_usize(); + first = Some(first.map_or(start, |previous: usize| previous.min(start))); + } + ast::statement_visitor::walk_stmt(&mut checker, stmt); + } + first.map(|start| { + CompileError::from_source_error( + source_file, + CpythonDiagnostic::new( + "lazy from __future__ import is not allowed".to_owned(), + start, + start + 4, + ), + ) + }) +} + /// Syntax the reference grammar has no rule for, but that this parser accepts. /// /// A bare generator expression is one: `f(x for x in y)` is the `primary @@ -1338,15 +1400,16 @@ pub fn single_mode_multiple_statements_error( let position = match newline_index.checked_sub(1).map(|index| &tokens[index]) { Some(comment) if comment.kind() == TokenKind::Comment => comment.start(), _ => newline.start(), - } - .to_usize(); - Some(CompileError::from_source_error( + }; + Some(CompileError::from_ruff_parse_error( + parser::ParseError { + error: parser::ParseErrorType::OtherError( + "multiple statements found while compiling a single statement".to_owned(), + ), + location: ruff_text_size::TextRange::new(position, position + TextSize::new(1)), + }, source_file, - CpythonDiagnostic::new( - "multiple statements found while compiling a single statement".to_owned(), - position, - position, - ), + Mode::Single, )) } @@ -1554,6 +1617,11 @@ fn _compile_with_syntax_warning_handler<'a>( ); pre_parse_source_error(&source_file)?; let parsed = parser::parse_unchecked(barry_source.source(), parser_options); + if parsed.errors().is_empty() + && let Some(error) = lazy_future_import_error(parsed.syntax(), &source_file) + { + return Err(error); + } if matches!(mode, Mode::Single) && let Some(error) = single_mode_multiple_statements_error(&source_file, &parsed) { @@ -1669,7 +1737,27 @@ impl BarrySource<'_> { parse_error: Option<&parser::ParseError>, source_file: &SourceFile, ) -> Option { - if let Some(range) = parse_error.and_then(|error| self.invalid_legacy_operator(error)) { + if let Some(error) = parse_error + && let Some(range) = self.invalid_legacy_operator(error) + { + if error.location.start() > range.start() { + return Some(CompileError::from_source_error( + source_file, + CpythonDiagnostic::new( + "invalid syntax. Maybe you meant '!=' instead of '<>'?".to_owned(), + range.start().to_usize(), + range.end().to_usize(), + ), + )); + } + // At expression start, '<' is already invalid: the comparison + // production (and its obsolete-operator hint) was never entered. + // Barry mode instead lexed the rewritten '!=' as one token. + let range = if self.source.as_bytes()[range.start().to_usize()] == b'<' { + ruff_text_size::TextRange::at(range.start(), TextSize::new(1)) + } else { + range + }; return Some(barry_as_flufl_invalid_legacy_operator_error( source_file, range, @@ -1808,6 +1896,11 @@ pub fn interactive_input( future_features.contains(core::bytecode::CodeFlags::FUTURE_BARRY_AS_BDFL), ); let parsed = parser::parse_unchecked(prepared.source(), options); + if parsed.errors().is_empty() + && let Some(error) = lazy_future_import_error(parsed.syntax(), &source_file) + { + return Err(error); + } if let Some(error) = single_mode_multiple_statements_error(&source_file, &parsed) { return Err(error); } @@ -1911,6 +2004,11 @@ pub fn _compile_symtable( Mode::Exec | Mode::Single | Mode::BlockExpr => { pre_parse_source_error(&source_file)?; let parsed = ruff_python_parser::parse_unchecked(barry_source.source(), parser_options); + if parsed.errors().is_empty() + && let Some(error) = lazy_future_import_error(parsed.syntax(), &source_file) + { + return Err(error); + } if matches!(mode, Mode::Single) && let Some(error) = single_mode_multiple_statements_error(&source_file, &parsed) { @@ -2655,7 +2753,10 @@ mod tests { fn obsolete_not_equal_diagnostic_spans_the_whole_operator() { let err = compile("2 <> 3\n", Mode::Exec, "", CompileOpts::default()) .expect_err("'<>' outside Barry mode is a syntax error"); - assert_eq!(err.to_string(), "invalid syntax"); + assert_eq!( + err.to_string(), + "invalid syntax. Maybe you meant '!=' instead of '<>'?" + ); assert_eq!(err.python_location(), (1, 3)); assert_eq!(err.python_end_location(), Some((1, 5))); @@ -2668,12 +2769,12 @@ mod tests { assert_eq!(err.python_end_location(), Some((1, 5))); // A `<>` that starts a statement is reported at the `<` too, where the - // parser stops instead of one character in. + // parser stops; only that first character is highlighted. let err = compile("<>\n", Mode::Exec, "", CompileOpts::default()) .expect_err("a bare '<>' is a syntax error"); assert_eq!(err.to_string(), "invalid syntax"); assert_eq!(err.python_location(), (1, 1)); - assert_eq!(err.python_end_location(), Some((1, 3))); + assert_eq!(err.python_end_location(), Some((1, 2))); // A bracket left open earlier in the source outranks the operator. let err = compile( diff --git a/crates/derive-impl/src/compile_bytecode.rs b/crates/derive-impl/src/compile_bytecode.rs index e08c230e27d..6a7a327a4f9 100644 --- a/crates/derive-impl/src/compile_bytecode.rs +++ b/crates/derive-impl/src/compile_bytecode.rs @@ -219,7 +219,8 @@ impl CompilationSource { Ok(code) => code, Err(_) if stem.starts_with("badsyntax_") - | parent.ends_with(".encoded_modules") => + || parent.ends_with(".encoded_modules") + || parent == "test.test_lazy_import.data.badsyntax" => { // TODO: handle with macro arg rather than hard-coded path continue; diff --git a/crates/doc/generate.py b/crates/doc/generate.py index 4b64ce5e236..5be0deceffe 100644 --- a/crates/doc/generate.py +++ b/crates/doc/generate.py @@ -6,7 +6,6 @@ import os import pathlib import platform -import pydoc import re import sys import types @@ -14,6 +13,11 @@ import warnings from importlib.machinery import EXTENSION_SUFFIXES, ExtensionFileLoader +try: + from inspect import _getowndoc +except ImportError: + from pydoc import _getowndoc + if typing.TYPE_CHECKING: from collections.abc import Iterable @@ -146,7 +150,7 @@ def traverse( parts += (obj.__name__,) if any(f(obj) for f in (inspect.ismodule, inspect.isclass, inspect.isbuiltin)): - yield DocEntry(parts, pydoc._getowndoc(obj)) + yield DocEntry(parts, _getowndoc(obj)) if inspect.isclass(obj): # getmembers() shows the class's own metadata for these names (e.g. @@ -159,7 +163,7 @@ def traverse( if isinstance( attr, (types.GetSetDescriptorType, types.MemberDescriptorType) ): - yield DocEntry(parts + (name,), pydoc._getowndoc(attr)) + yield DocEntry(parts + (name,), _getowndoc(attr)) for name, attr in inspect.getmembers(obj): if name in IGNORED_ATTRS: @@ -211,7 +215,7 @@ def traverse( ) if is_callable or is_func: - yield DocEntry(new_parts, pydoc._getowndoc(attr)) + yield DocEntry(new_parts, _getowndoc(attr)) # A native type the module exposes only through an instance # (e.g. the type of sys.flags). if ( @@ -283,7 +287,7 @@ def iter_native_on_python_modules( if inspect.isclass(attr): yield from traverse(attr, module, parts) else: - yield DocEntry(parts, pydoc._getowndoc(attr)) + yield DocEntry(parts, _getowndoc(attr)) def find_doc_entries() -> "Iterable[DocEntry]": @@ -322,7 +326,7 @@ def find_doc_entries() -> "Iterable[DocEntry]": module_names.append(typ.__module__) for module_name in module_names: parts = (module_name, typ.__name__) - yield DocEntry(parts, pydoc._getowndoc(typ)) + yield DocEntry(parts, _getowndoc(typ)) yield from traverse(typ, __builtins__, parts) diff --git a/crates/doc/src/data.inc.rs b/crates/doc/src/data.inc.rs index 0d10b224d20..e2c0cbcc862 100644 --- a/crates/doc/src/data.inc.rs +++ b/crates/doc/src/data.inc.rs @@ -685,7 +685,7 @@ pub const DB: &[(&str, &str)] = &[ ("_ast.Dict.__str__", "Return str(self)."), ("_ast.Dict.__subclasshook__", "Abstract classes can override this to customize issubclass().\n\nThis is invoked early on by abc.ABCMeta.__subclasscheck__().\nIt should return True, False or NotImplemented. If it returns\nNotImplemented, the normal algorithm is used. Otherwise, it\noverrides the normal algorithm (and the outcome is cached).\n"), ("_ast.Dict.__weakref__", "list of weak references to the object"), - ("_ast.DictComp", "DictComp(expr key, expr value, comprehension* generators)"), + ("_ast.DictComp", "DictComp(expr key, expr? value, comprehension* generators)"), ("_ast.DictComp.__class__", "the object's class"), ("_ast.DictComp.__delattr__", "Implement delattr(self, name)."), ("_ast.DictComp.__dir__", "Default dir() implementation."), @@ -2886,7 +2886,7 @@ pub const DB: &[(&str, &str)] = &[ ("_ast.excepthandler.__str__", "Return str(self)."), ("_ast.excepthandler.__subclasshook__", "Abstract classes can override this to customize issubclass().\n\nThis is invoked early on by abc.ABCMeta.__subclasscheck__().\nIt should return True, False or NotImplemented. If it returns\nNotImplemented, the normal algorithm is used. Otherwise, it\noverrides the normal algorithm (and the outcome is cached).\n"), ("_ast.excepthandler.__weakref__", "list of weak references to the object"), - ("_ast.expr", "expr = BoolOp(boolop op, expr* values)\n | NamedExpr(expr target, expr value)\n | BinOp(expr left, operator op, expr right)\n | UnaryOp(unaryop op, expr operand)\n | Lambda(arguments args, expr body)\n | IfExp(expr test, expr body, expr orelse)\n | Dict(expr?* keys, expr* values)\n | Set(expr* elts)\n | ListComp(expr elt, comprehension* generators)\n | SetComp(expr elt, comprehension* generators)\n | DictComp(expr key, expr value, comprehension* generators)\n | GeneratorExp(expr elt, comprehension* generators)\n | Await(expr value)\n | Yield(expr? value)\n | YieldFrom(expr value)\n | Compare(expr left, cmpop* ops, expr* comparators)\n | Call(expr func, expr* args, keyword* keywords)\n | FormattedValue(expr value, int conversion, expr? format_spec)\n | Interpolation(expr value, constant str, int conversion, expr? format_spec)\n | JoinedStr(expr* values)\n | TemplateStr(expr* values)\n | Constant(constant value, string? kind)\n | Attribute(expr value, identifier attr, expr_context ctx)\n | Subscript(expr value, expr slice, expr_context ctx)\n | Starred(expr value, expr_context ctx)\n | Name(identifier id, expr_context ctx)\n | List(expr* elts, expr_context ctx)\n | Tuple(expr* elts, expr_context ctx)\n | Slice(expr? lower, expr? upper, expr? step)"), + ("_ast.expr", "expr = BoolOp(boolop op, expr* values)\n | NamedExpr(expr target, expr value)\n | BinOp(expr left, operator op, expr right)\n | UnaryOp(unaryop op, expr operand)\n | Lambda(arguments args, expr body)\n | IfExp(expr test, expr body, expr orelse)\n | Dict(expr?* keys, expr* values)\n | Set(expr* elts)\n | ListComp(expr elt, comprehension* generators)\n | SetComp(expr elt, comprehension* generators)\n | DictComp(expr key, expr? value, comprehension* generators)\n | GeneratorExp(expr elt, comprehension* generators)\n | Await(expr value)\n | Yield(expr? value)\n | YieldFrom(expr value)\n | Compare(expr left, cmpop* ops, expr* comparators)\n | Call(expr func, expr* args, keyword* keywords)\n | FormattedValue(expr value, int conversion, expr? format_spec)\n | Interpolation(expr value, constant str, int conversion, expr? format_spec)\n | JoinedStr(expr* values)\n | TemplateStr(expr* values)\n | Constant(constant value, string? kind)\n | Attribute(expr value, identifier attr, expr_context ctx)\n | Subscript(expr value, expr slice, expr_context ctx)\n | Starred(expr value, expr_context ctx)\n | Name(identifier id, expr_context ctx)\n | List(expr* elts, expr_context ctx)\n | Tuple(expr* elts, expr_context ctx)\n | Slice(expr? lower, expr? upper, expr? step)"), ("_ast.expr.__class__", "the object's class"), ("_ast.expr.__delattr__", "Implement delattr(self, name)."), ("_ast.expr.__dir__", "Default dir() implementation."), @@ -24395,7 +24395,7 @@ pub const DB: &[(&str, &str)] = &[ ("math.atanh", "Return the inverse hyperbolic tangent of x."), ("math.cbrt", "Return the cube root of x."), ("math.ceil", "Return the ceiling of x as an Integral.\n\nThis is the smallest integer >= x."), - ("math.comb", "Number of ways to choose k items from n items without repetition and without order.\n\nEvaluates to n! / (k! * (n - k)!) when k <= n and evaluates\nto zero when k > n.\n\nAlso called the binomial coefficient because it is equivalent\nto the coefficient of k-th term in polynomial expansion of the\nexpression (1 + x)**n.\n\nRaises TypeError if either of the arguments are not integers.\nRaises ValueError if either of the arguments are negative."), + ("math.comb", "Number of ways to choose k items from n items without repetition and without order.\n\nEvaluates to n! / (k! * (n - k)!) when k <= n and evaluates\nto zero when k > n.\n\nAlso called the binomial coefficient because it is equivalent\nto the coefficient of k-th term in polynomial expansion of the\nexpression (1 + x)**n.\n\nRaises ValueError if either of the arguments are negative."), ("math.copysign", "Return a float with the magnitude (absolute value) of x but the sign of y.\n\nOn platforms that support signed zeros, copysign(1.0, -0.0)\nreturns -1.0.\n"), ("math.cos", "Return the cosine of x (measured in radians)."), ("math.cosh", "Return the hyperbolic cosine of x."), @@ -24430,7 +24430,7 @@ pub const DB: &[(&str, &str)] = &[ ("math.log2", "Return the base 2 logarithm of x."), ("math.modf", "Return the fractional and integer parts of x.\n\nBoth results carry the sign of x and are floats."), ("math.nextafter", "Return the floating-point value the given number of steps after x towards y.\n\nIf steps is not specified or is None, it defaults to 1.\n\nRaises a TypeError, if x or y is not a double, or if steps is not\nan integer. Raises ValueError if steps is negative."), - ("math.perm", "Number of ways to choose k items from n items without repetition and with order.\n\nEvaluates to n! / (n - k)! when k <= n and evaluates\nto zero when k > n.\n\nIf k is not specified or is None, then k defaults to n\nand the function returns n!.\n\nRaises TypeError if either of the arguments are not integers.\nRaises ValueError if either of the arguments are negative."), + ("math.perm", "Number of ways to choose k items from n items without repetition and with order.\n\nEvaluates to n! / (n - k)! when k <= n and evaluates\nto zero when k > n.\n\nIf k is not specified or is None, then k defaults to n\nand the function returns n!.\n\nRaises ValueError if either of the arguments are negative."), ("math.pow", "Return x**y (x to the power of y)."), ("math.prod", "Calculate the product of all the elements in the input iterable.\n\nThe default start value for the product is 1.\n\nWhen the iterable is empty, return the start value. This function is\nintended specifically for use with numeric values and may reject\nnon-numeric types."), ("math.radians", "Convert angle x from degrees to radians."), diff --git a/crates/host_env/src/crt_fd.rs b/crates/host_env/src/crt_fd.rs index 5b2cf685be1..6a8cc1987f9 100644 --- a/crates/host_env/src/crt_fd.rs +++ b/crates/host_env/src/crt_fd.rs @@ -370,7 +370,7 @@ pub fn closerange(fd_low: Raw, fd_high: Raw) { // CPython clamps low to 0. let fd_low = fd_low.max(0); // close_range(2) is [low, high] whereas CPython's is [low, high). - if fd_high < fd_low { + if fd_high <= fd_low { return; } let high = fd_high - 1; diff --git a/crates/host_env/src/ctypes.rs b/crates/host_env/src/ctypes.rs index 440d2078eac..bf825e46106 100644 --- a/crates/host_env/src/ctypes.rs +++ b/crates/host_env/src/ctypes.rs @@ -221,6 +221,36 @@ const LONG_DOUBLE_SIZE: usize = core::mem::size_of::(); )))] const LONG_DOUBLE_SIZE: usize = core::mem::size_of::(); +/// Return the loader's current shared-object names, including its empty main entry. +#[cfg(target_os = "linux")] +pub fn dllist() -> Vec { + use std::os::unix::ffi::OsStringExt; + + unsafe extern "C" fn collect( + info: *mut libc::dl_phdr_info, + _size: libc::size_t, + data: *mut c_void, + ) -> c_int { + // dl_iterate_phdr supplies a valid record and calls synchronously while + // the loader keeps the object and its NUL-terminated name alive. + let name = unsafe { (*info).dlpi_name }; + let names = unsafe { &mut *data.cast::>() }; + if !name.is_null() { + let bytes = unsafe { CStr::from_ptr(name) }.to_bytes().to_vec(); + names.push(std::ffi::OsString::from_vec(bytes)); + } + 0 + } + + let mut names = Vec::new(); + // The callback only copies loader-owned bytes into this live, exclusive + // vector. It never invokes Python or mutates the loader's object list. + unsafe { + libc::dl_iterate_phdr(Some(collect), core::ptr::from_mut(&mut names).cast()); + } + names +} + pub fn simple_type_size(ty: &str) -> Option { match ty { "c" | "b" => Some(core::mem::size_of::()), @@ -2011,7 +2041,7 @@ pub enum CallRet<'a> { Aggregate(&'a CTypeLayout), } -/// Per-call error-swapping options. +/// Per-call ABI and error-swapping options. #[cfg(all( any( target_os = "linux", @@ -2023,6 +2053,10 @@ pub enum CallRet<'a> { ))] #[derive(Debug, Clone, Copy, Default)] pub struct CallOptions { + /// Declared argument count for a variadic call. Values must already have + /// their promoted C types; libffi rejects explicit narrow ctypes values. + /// `None` uses a fixed-arity CIF. + pub fixed_arg_count: Option, /// Swap the ctypes-local errno around the raw call (unix; ignored on windows). pub use_errno: bool, /// Swap the ctypes-local last error around the raw call (windows; ignored @@ -2066,6 +2100,9 @@ pub enum CallValue { #[derive(Debug, Clone, PartialEq, Eq)] pub enum CallError { NullFunctionPointer, + InvalidCallInterface { + variadic: bool, + }, UnknownTypeCode(String), /// An aggregate argument's buffer was shorter than its layout size. BufferTooSmall { @@ -2156,7 +2193,13 @@ pub fn call( }) .collect(); - let cif = Cif::new(ffi_arg_types, ffi_return_type); + let cif = match options.fixed_arg_count { + Some(fixed) => Cif::try_new_variadic(ffi_arg_types, fixed, ffi_return_type), + None => Cif::try_new(ffi_arg_types, ffi_return_type), + } + .map_err(|_| CallError::InvalidCallInterface { + variadic: options.fixed_arg_count.is_some(), + })?; // Allocate the aggregate return buffer outside the error-swap window so no // allocation runs between the raw call and the errno/last-error capture. @@ -3659,6 +3702,7 @@ mod tests { CallOptions { use_errno: true, use_last_error: false, + ..CallOptions::default() }, ) .unwrap(); diff --git a/crates/host_env/src/fs.rs b/crates/host_env/src/fs.rs index d6516c76374..498ffbf6896 100644 --- a/crates/host_env/src/fs.rs +++ b/crates/host_env/src/fs.rs @@ -11,6 +11,10 @@ pub fn open(path: impl AsRef) -> io::Result { File::open(path) } +pub fn create(path: impl AsRef) -> io::Result { + File::create(path) +} + pub fn read(path: impl AsRef) -> io::Result> { fs::read(path) } diff --git a/crates/host_env/src/signal.rs b/crates/host_env/src/signal.rs index f5205d84190..495296497c3 100644 --- a/crates/host_env/src/signal.rs +++ b/crates/host_env/src/signal.rs @@ -395,18 +395,47 @@ pub fn wakeup_fd_is_socket(fd: libc::SOCKET) -> io::Result { Ok(false) } -#[cfg(windows)] -pub struct WakeupSendError(pub i32); +#[cfg(any(unix, windows))] +pub enum WakeupError { + Write(i32), + #[cfg(windows)] + Send(i32), +} -#[cfg(windows)] -impl WakeupSendError { +#[cfg(any(unix, windows))] +impl WakeupError { pub fn is_would_block(&self) -> bool { - self.0 == windows_sys::Win32::Networking::WinSock::WSAEWOULDBLOCK + match *self { + Self::Write(errno) => errno == libc::EAGAIN || errno == libc::EWOULDBLOCK, + #[cfg(windows)] + Self::Send(errno) => errno == windows_sys::Win32::Networking::WinSock::WSAEWOULDBLOCK, + } } // Only convert to an allocating error outside the OS signal handler. pub fn into_io_error(self) -> io::Error { - io::Error::from_raw_os_error(self.0) + match self { + #[cfg(unix)] + Self::Write(errno) => io::Error::from_raw_os_error(errno), + #[cfg(windows)] + Self::Write(errno) => crate::os::io_error_from_errno(errno), + #[cfg(windows)] + Self::Send(errno) => io::Error::from_raw_os_error(errno), + } + } +} + +#[cfg(any(unix, windows))] +fn write_signal(signum: i32, wakeup_fd: i32) -> Option { + let sigbyte = signum as u8; + loop { + if unsafe { libc::write(wakeup_fd, &sigbyte as *const u8 as *const _, 1) } >= 0 { + return None; + } + let errno = crate::os::get_errno(); + if errno != libc::EINTR { + return Some(WakeupError::Write(errno)); + } } } @@ -416,7 +445,8 @@ pub fn notify_signal( wakeup_fd: libc::SOCKET, wakeup_is_socket: bool, sigint_event: Option, -) -> Option { +) -> Option { + let saved_errno = crate::os::get_errno(); if signum == libc::SIGINT && let Some(handle) = sigint_event { @@ -425,12 +455,10 @@ pub fn notify_signal( } } - if wakeup_fd == INVALID_SOCKET { - return None; - } - - let sigbyte = signum as u8; - if wakeup_is_socket { + let error = if wakeup_fd == INVALID_SOCKET { + None + } else if wakeup_is_socket { + let sigbyte = signum as u8; let result = unsafe { windows_sys::Win32::Networking::WinSock::send( wakeup_fd, @@ -440,21 +468,31 @@ pub fn notify_signal( ) }; if result < 0 { - Some(WakeupSendError(unsafe { + Some(WakeupError::Send(unsafe { windows_sys::Win32::Networking::WinSock::WSAGetLastError() })) } else { None } } else { - unsafe { - let _ = libc::write(wakeup_fd as _, &sigbyte as *const u8 as *const _, 1); - } - None + write_signal(signum, wakeup_fd as _) + }; + crate::os::set_errno(saved_errno); + error +} + +#[cfg(unix)] +pub fn notify_signal(signum: i32, wakeup_fd: i32) -> Option { + if wakeup_fd == -1 { + return None; } + let saved_errno = crate::os::get_errno(); + let error = write_signal(signum, wakeup_fd); + crate::os::set_errno(saved_errno); + error } -#[cfg(any(unix, target_os = "wasi"))] +#[cfg(target_os = "wasi")] pub fn notify_signal(signum: i32, wakeup_fd: i32) { if wakeup_fd == -1 { return; diff --git a/crates/host_env/src/time.rs b/crates/host_env/src/time.rs index e79be43a501..f7393009a32 100644 --- a/crates/host_env/src/time.rs +++ b/crates/host_env/src/time.rs @@ -141,7 +141,6 @@ unsafe extern "C" { } #[cfg(windows)] -#[cfg_attr(target_env = "musl", allow(deprecated))] pub fn current_time_t() -> TimeT { unsafe { c_time(core::ptr::null_mut()) } } diff --git a/crates/stdlib/src/_remote_debugging.rs b/crates/stdlib/src/_remote_debugging.rs index 618ea9fe0a8..101e294f1ef 100644 --- a/crates/stdlib/src/_remote_debugging.rs +++ b/crates/stdlib/src/_remote_debugging.rs @@ -1,19 +1,86 @@ pub(crate) use _remote_debugging::module_def; +#[path = "_remote_debugging/binary.rs"] +mod binary; + #[pymodule] mod _remote_debugging { + use super::binary; use crate::vm::{ - Py, PyObjectRef, PyResult, VirtualMachine, - builtins::PyType, - function::FuncArgs, + AsObject, Py, PyObjectRef, PyResult, VirtualMachine, + builtins::{PyBaseExceptionRef, PyDictRef, PyList, PyTuple, PyType}, + common::lock::PyMutex, + convert::{ToPyException, ToPyObject}, + function::{ArgIntoFloat, FsPath, FuncArgs, OptionalArg}, + host_env::fs as host_fs, types::{Constructor, PyStructSequence}, }; + use alloc::sync::Arc; + use std::fs::File; + + #[pyattr] + const THREAD_STATUS_HAS_GIL: u8 = 1; + #[pyattr] + const THREAD_STATUS_ON_CPU: u8 = 2; + #[pyattr] + const THREAD_STATUS_UNKNOWN: u8 = 4; + #[pyattr] + const THREAD_STATUS_GIL_REQUESTED: u8 = 8; + #[pyattr] + const THREAD_STATUS_HAS_EXCEPTION: u8 = 16; + #[pyattr] + const THREAD_STATUS_MAIN_THREAD: u8 = 32; + + #[pyattr] + const PROCESS_VM_READV_SUPPORTED: u8 = 0; + + #[pyfunction] + fn zstd_available() -> bool { + false + } + + #[pystruct_sequence_data] + struct InterpreterInfoData { + interpreter_id: PyObjectRef, + threads: PyObjectRef, + } + + #[pyattr] + #[pystruct_sequence( + name = "InterpreterInfo", + module = "_remote_debugging", + data = "InterpreterInfoData" + )] + struct InterpreterInfo; + + #[pyclass(with(PyStructSequence))] + impl InterpreterInfo {} + + #[pystruct_sequence_data] + struct LocationInfoData { + lineno: PyObjectRef, + end_lineno: PyObjectRef, + col_offset: PyObjectRef, + end_col_offset: PyObjectRef, + } + + #[pyattr] + #[pystruct_sequence( + name = "LocationInfo", + module = "_remote_debugging", + data = "LocationInfoData" + )] + struct LocationInfo; + + #[pyclass(with(PyStructSequence))] + impl LocationInfo {} #[pystruct_sequence_data] struct FrameInfoData { filename: String, - lineno: i64, + location: PyObjectRef, funcname: String, + opcode: PyObjectRef, } #[pyattr] @@ -58,6 +125,7 @@ mod _remote_debugging { #[pystruct_sequence_data] struct ThreadInfoData { thread_id: PyObjectRef, + status: PyObjectRef, frame_info: PyObjectRef, } @@ -104,4 +172,535 @@ mod _remote_debugging { #[pyclass(with(Constructor))] impl RemoteUnwinder {} + + fn file_error(error: binary::Error, vm: &VirtualMachine) -> PyBaseExceptionRef { + match error { + binary::Error::Io(error) => error.to_pyexception(vm), + binary::Error::Value(message) => vm.new_value_error(message), + binary::Error::Runtime(message) => vm.new_runtime_error(message), + binary::Error::Overflow(message) => vm.new_overflow_error(message), + binary::Error::Memory => vm.no_memory_error(), + } + } + + fn open_file(filename: PyObjectRef, write: bool, vm: &VirtualMachine) -> PyResult { + let path = FsPath::try_from_path_like(filename.clone(), true, vm)?.to_path_buf(vm)?; + let result = if write { + host_fs::create(path) + } else { + host_fs::open(path) + }; + result.map_err(|error| { + let exception = error.to_pyexception(vm); + // Py_fopen preserves the original path-like object on the error. + let _ = exception.as_object().set_attr("filename", filename, vm); + exception + }) + } + + fn sequence<'a>( + object: &'a PyObjectRef, + length: usize, + vm: &VirtualMachine, + ) -> PyResult<&'a [PyObjectRef]> { + object + .downcast_ref::() + .map(|value| value.as_slice()) + .filter(|value| value.len() == length) + .ok_or_else(|| vm.new_type_error(format!("expected a {length}-item tuple"))) + } + + fn list( + object: &PyObjectRef, + message: &'static str, + vm: &VirtualMachine, + ) -> PyResult> { + object + .downcast_ref::() + .map(|value| value.borrow_vec().to_vec()) + .ok_or_else(|| vm.new_type_error(message)) + } + + fn parse_frame(object: &PyObjectRef, vm: &VirtualMachine) -> PyResult { + let items = sequence(object, 4, vm)?; + let mut location = [-1; 4]; + if !vm.is_none(&items[1]) { + for (output, item) in location.iter_mut().zip(sequence(&items[1], 4, vm)?) { + *output = item.try_to_value::(vm).unwrap_or(-1); + } + } + Ok(binary::Frame { + filename: items[0].try_to_value(vm)?, + funcname: items[2].try_to_value(vm)?, + location, + opcode: items[3].try_to_value::(vm).unwrap_or(255), + }) + } + + #[derive(FromArgs)] + struct WriterArgs { + #[pyarg(any)] + filename: PyObjectRef, + #[pyarg(any)] + sample_interval_us: u64, + #[pyarg(any)] + start_time_us: u64, + #[pyarg(named, default = 0)] + compression: i32, + #[pyarg(named, default = -1)] + mode: i32, + #[pyarg(named, default = -1)] + capture_features: i32, + } + + #[derive(Debug)] + struct WriterState { + writer: Option, + finalized_samples: u64, + } + + #[pyattr] + #[pyclass(name = "BinaryWriter", module = "_remote_debugging")] + #[derive(Debug, PyPayload)] + struct BinaryWriter { + state: PyMutex, + } + + impl Constructor for BinaryWriter { + type Args = WriterArgs; + + fn py_new(_cls: &Py, args: Self::Args, vm: &VirtualMachine) -> PyResult { + if !(-1..=4).contains(&args.mode) { + return Err(vm.new_value_error("invalid profiling mode")); + } + if !(-1..=31).contains(&args.capture_features) { + return Err(vm.new_value_error("invalid capture features")); + } + match args.compression { + 0 => (), + 1 => return Err(vm.new_runtime_error("zstd support not compiled in")), + _ => return Err(vm.new_value_error("invalid compression type")), + } + let file = open_file(args.filename, true, vm)?; + let writer = binary::Writer::new( + file, + args.sample_interval_us, + args.start_time_us, + args.mode, + args.capture_features, + ) + .map_err(|error| file_error(error, vm))?; + Ok(Self { + state: PyMutex::new(WriterState { + writer: Some(writer), + finalized_samples: 0, + }), + }) + } + } + + #[derive(FromArgs)] + struct ExitArgs { + #[pyarg(any, optional)] + exc_type: OptionalArg, + #[pyarg(any, optional, name = "exc_val")] + _exc_val: OptionalArg, + #[pyarg(any, optional, name = "exc_tb")] + _exc_tb: OptionalArg, + } + + #[derive(FromArgs)] + struct SampleArgs { + #[pyarg(any)] + stack_frames: PyObjectRef, + #[pyarg(any)] + timestamp_us: u64, + } + + #[derive(FromArgs)] + struct ProfileStatsArgs { + #[pyarg(any)] + duration_sec: ArgIntoFloat, + #[pyarg(any)] + sample_rate: ArgIntoFloat, + #[pyarg(any, optional)] + error_rate: OptionalArg>, + #[pyarg(any, optional)] + missed_samples: OptionalArg>, + } + + #[pyclass(with(Constructor))] + impl BinaryWriter { + #[pymethod] + fn write_sample(zelf: &Py, args: SampleArgs, vm: &VirtualMachine) -> PyResult<()> { + if zelf.state.lock().writer.is_none() { + return Err(vm.new_value_error("Writer is closed")); + } + // Convert Python values before acquiring the writer lock. Integer + // conversion may call user code, including close() on this writer. + let mut samples = Vec::new(); + for interpreter in list(&args.stack_frames, "stack_frames must be a list", vm)? { + let items = sequence(&interpreter, 2, vm)?; + let interpreter_id = items[0].try_to_value::(vm)?; + for thread in list(&items[1], "threads must be a list", vm)? { + let items = sequence(&thread, 3, vm)?; + let id = items[0].try_to_value::(vm)?; + let status = items[1].try_to_value::(vm)? as u8; + let frames = list(&items[2], "frames must be a list", vm)? + .iter() + .take(256) + .map(|frame| parse_frame(frame, vm)) + .collect::>>()?; + samples.push((id, interpreter_id, status, frames)); + } + } + let mut state = zelf.state.lock(); + let writer = state + .writer + .as_mut() + .ok_or_else(|| vm.new_value_error("Writer is closed"))?; + for (id, interpreter, status, frames) in samples { + writer + .sample(id, interpreter, status, frames, args.timestamp_us) + .map_err(|error| file_error(error, vm))?; + } + Ok(()) + } + + #[pymethod] + fn set_stats(zelf: &Py, args: ProfileStatsArgs, vm: &VirtualMachine) -> PyResult<()> { + let mut state = zelf.state.lock(); + let writer = state + .writer + .as_mut() + .ok_or_else(|| vm.new_value_error("Writer is closed"))?; + let stats = binary::ProfileStats { + duration: args.duration_sec.into_float(), + sample_rate: args.sample_rate.into_float(), + error_rate: args.error_rate.flatten().map(ArgIntoFloat::into_float), + missed_samples: args.missed_samples.flatten().map(ArgIntoFloat::into_float), + }; + stats.validate().map_err(|error| file_error(error, vm))?; + writer.profile_stats = Some(stats); + Ok(()) + } + + #[pymethod] + fn finalize(zelf: &Py, vm: &VirtualMachine) -> PyResult<()> { + let mut state = zelf.state.lock(); + let writer = state + .writer + .as_mut() + .ok_or_else(|| vm.new_value_error("Writer is already closed"))?; + let version = crate::vm::version::MAJOR as u8; + writer + .finalize([ + version, + crate::vm::version::MINOR as u8, + crate::vm::version::MICRO as u8, + ]) + .map_err(|error| file_error(error, vm))?; + state.finalized_samples = writer.stats.total_samples; + state.writer = None; + Ok(()) + } + + #[pymethod] + fn close(zelf: &Py) { + zelf.state.lock().writer = None; + } + + #[pygetset] + fn total_samples(zelf: &Py) -> u64 { + let state = zelf.state.lock(); + state + .writer + .as_ref() + .map_or(state.finalized_samples, |writer| writer.stats.total_samples) + } + + #[pymethod] + fn get_stats(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let state = zelf.state.lock(); + let writer = state + .writer + .as_ref() + .ok_or_else(|| vm.new_value_error("Writer is closed"))?; + let stats = &writer.stats; + let dict = stats_dict(stats, vm)?; + for (name, value) in [ + ("total_frames_written", stats.total_frames_written), + ("frames_saved", stats.frames_saved), + ("bytes_written", stats.bytes_written), + ] { + dict.set_item(name, value.to_pyobject(vm), vm)?; + } + let frames = stats.total_frames_written + stats.frames_saved; + let percentage = if frames == 0 { + 0.0 + } else { + stats.frames_saved as f64 / frames as f64 * 100.0 + }; + dict.set_item("frame_compression_pct", percentage.to_pyobject(vm), vm)?; + Ok(dict) + } + + #[pymethod] + fn __enter__(zelf: PyObjectRef) -> PyObjectRef { + zelf + } + + #[pymethod] + fn __exit__(zelf: &Py, args: ExitArgs, vm: &VirtualMachine) -> PyResult { + if zelf.state.lock().writer.is_some() { + if args + .exc_type + .into_option() + .is_none_or(|value| vm.is_none(&value)) + { + let result = Self::finalize(zelf, vm); + if result.is_err() { + Self::close(zelf); + } + result?; + } else { + Self::close(zelf); + } + } + Ok(false) + } + } + + fn stats_dict(stats: &binary::Stats, vm: &VirtualMachine) -> PyResult { + let dict = vm.ctx.new_dict(); + for (name, value) in [ + ("repeat_records", stats.records[0]), + ("full_records", stats.records[1]), + ("suffix_records", stats.records[2]), + ("pop_push_records", stats.records[3]), + ("repeat_samples", stats.repeat_samples), + ("total_records", stats.records.iter().sum()), + ("total_samples", stats.total_samples), + ] { + dict.set_item(name, value.to_pyobject(vm), vm)?; + } + Ok(dict) + } + + #[pyattr] + #[pyclass(name = "BinaryReader", module = "_remote_debugging")] + #[derive(Debug, PyPayload)] + struct BinaryReader { + reader: PyMutex>>, + stats: PyMutex, + } + + #[derive(FromArgs)] + struct ReaderArgs { + #[pyarg(any)] + filename: PyObjectRef, + } + + impl Constructor for BinaryReader { + type Args = ReaderArgs; + + fn py_new(_cls: &Py, args: Self::Args, vm: &VirtualMachine) -> PyResult { + let file = open_file(args.filename, false, vm)?; + let reader = binary::Reader::open(file).map_err(|error| file_error(error, vm))?; + Ok(Self { + reader: PyMutex::new(Some(Arc::new(reader))), + stats: PyMutex::new(binary::Stats::default()), + }) + } + } + + #[derive(FromArgs)] + struct ReplayArgs { + #[pyarg(any)] + collector: PyObjectRef, + #[pyarg(any, optional)] + progress_callback: OptionalArg, + } + + impl BinaryReader { + fn reader(&self, vm: &VirtualMachine) -> PyResult> { + self.reader + .lock() + .clone() + .ok_or_else(|| vm.new_value_error("Reader is closed")) + } + } + + #[pyclass(with(Constructor))] + impl BinaryReader { + #[pymethod] + fn get_info(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let reader = zelf.reader(vm)?; + let dict = vm.ctx.new_dict(); + for (name, value) in [ + ("version", 1), + ("start_time_us", reader.start_time), + ("sample_interval_us", reader.interval), + ("sample_count", reader.sample_count), + ("thread_count", reader.thread_count as u64), + ("string_count", reader.strings.len() as u64), + ("frame_count", reader.frame_count() as u64), + ("compression_type", 0), + ] { + dict.set_item(name, value.to_pyobject(vm), vm)?; + } + let python_version: (u8, u8, u8) = reader.python_version.into(); + dict.set_item("python_version", python_version.to_pyobject(vm), vm)?; + dict.set_item("mode", reader.mode.to_pyobject(vm), vm)?; + dict.set_item("capture_features", reader.features.to_pyobject(vm), vm)?; + for (name, value) in [ + ( + "duration_sec", + reader.profile_stats.map(|stats| stats.duration), + ), + ( + "sample_rate", + reader.profile_stats.map(|stats| stats.sample_rate), + ), + ( + "error_rate", + reader.profile_stats.and_then(|stats| stats.error_rate), + ), + ( + "missed_samples", + reader.profile_stats.and_then(|stats| stats.missed_samples), + ), + ] { + dict.set_item(name, value.to_pyobject(vm), vm)?; + } + Ok(dict) + } + + #[pymethod] + fn get_stats(zelf: &Py, vm: &VirtualMachine) -> PyResult { + zelf.reader(vm)?; + let stats = zelf.stats.lock(); + let dict = stats_dict(&stats, vm)?; + dict.set_item( + "stack_reconstructions", + stats.stack_reconstructions.to_pyobject(vm), + vm, + )?; + Ok(dict) + } + + #[pymethod] + fn replay(zelf: &Py, args: ReplayArgs, vm: &VirtualMachine) -> PyResult { + let reader = zelf.reader(vm)?; + args.collector.get_attr("collect", vm).map_err(|error| { + if error.fast_isinstance(vm.ctx.exceptions.attribute_error) { + vm.new_type_error("Collector must have a collect() method") + } else { + error + } + })?; + let progress = args + .progress_callback + .into_option() + .filter(|callback| !vm.is_none(callback)); + if let Some(callback) = &progress { + callback.call((0, reader.sample_count), vm)?; + } + let mut replay = reader.replay(); + let result = (|| { + while let Some(batch) = replay.next().map_err(|error| file_error(error, vm))? { + if batch.timestamps.is_empty() { + continue; + } + let mut frames = Vec::with_capacity(batch.stack.len()); + for index in batch.stack { + let frame = reader.frame(index).map_err(|error| file_error(error, vm))?; + let [line, end_line, column, end_column] = frame.location; + let location = if line == -1 { + vm.ctx.none() + } else { + (line, end_line, column, end_column).to_pyobject(vm) + }; + frames.push( + FrameInfoData { + filename: frame.filename, + funcname: frame.funcname, + location, + opcode: (frame.opcode != 255) + .then_some(frame.opcode) + .to_pyobject(vm), + } + .to_pyobject(vm), + ); + } + let thread = ThreadInfoData { + thread_id: batch.id.to_pyobject(vm), + status: batch.status.to_pyobject(vm), + frame_info: vm.ctx.new_list(frames).into(), + } + .to_pyobject(vm); + let interpreter = InterpreterInfoData { + interpreter_id: batch.interpreter.to_pyobject(vm), + threads: vm.ctx.new_list(vec![thread]).into(), + } + .to_pyobject(vm); + let count = batch.timestamps.len() as u64; + let timestamps = vm.ctx.new_list( + batch + .timestamps + .into_iter() + .map(|value| value.to_pyobject(vm)) + .collect(), + ); + args.collector + .get_attr("collect", vm)? + .call((vm.ctx.new_list(vec![interpreter]), timestamps), vm)?; + if replay.stats.total_samples / 1000 + != (replay.stats.total_samples - count) / 1000 + && let Some(callback) = &progress + { + callback.call((replay.stats.total_samples, reader.sample_count), vm)?; + } + } + if let Some(callback) = &progress { + callback.call((replay.stats.total_samples, reader.sample_count), vm)?; + } + Ok(replay.stats.total_samples) + })(); + *zelf.stats.lock() = replay.stats; + result + } + + #[pymethod] + fn close(zelf: &Py) { + *zelf.reader.lock() = None; + } + + #[pygetset] + fn sample_count(zelf: &Py) -> u64 { + zelf.reader + .lock() + .as_ref() + .map_or(0, |reader| reader.sample_count) + } + + #[pygetset] + fn sample_interval_us(zelf: &Py) -> u64 { + zelf.reader + .lock() + .as_ref() + .map_or(0, |reader| reader.interval) + } + + #[pymethod] + fn __enter__(zelf: PyObjectRef) -> PyObjectRef { + zelf + } + + #[pymethod] + fn __exit__(zelf: &Py, _args: ExitArgs) -> bool { + Self::close(zelf); + false + } + } } diff --git a/crates/stdlib/src/_remote_debugging/binary.rs b/crates/stdlib/src/_remote_debugging/binary.rs new file mode 100644 index 00000000000..b923071a056 --- /dev/null +++ b/crates/stdlib/src/_remote_debugging/binary.rs @@ -0,0 +1,975 @@ +//! The TACH v1 profiling file format. This module only reads and writes files; +//! collecting stacks from a running process is a separate capability. + +use std::{ + collections::HashMap, + fs::File, + io::{self, BufWriter, Read, Seek, SeekFrom, Write}, +}; + +const HEADER_SIZE: usize = 64; +const FOOTER_SIZE: usize = 32; +const MAX_STACK: usize = 256; +const MAX_RLE_BYTES: usize = 16 * 1024; + +#[derive(Debug)] +pub(super) enum Error { + Io(io::Error), + Value(String), + Runtime(&'static str), + Overflow(&'static str), + Memory, +} + +impl From for Error { + fn from(error: io::Error) -> Self { + Self::Io(error) + } +} + +type Result = core::result::Result; + +fn invalid(message: impl Into) -> Error { + Error::Value(message.into()) +} + +fn varint(out: &mut Vec, mut value: u64) { + while value >= 128 { + out.push((value as u8) | 128); + value >>= 7; + } + out.push(value as u8); +} + +fn signed_varint(out: &mut Vec, value: i32) { + varint(out, ((value as u32) << 1 ^ (value >> 31) as u32) as u64); +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub(super) struct Frame { + pub filename: String, + pub funcname: String, + pub location: [i32; 4], + pub opcode: u8, +} + +#[derive(Clone, Debug)] +struct FrameEntry { + filename: u32, + funcname: u32, + location: [i32; 4], + opcode: u8, +} + +#[derive(Clone, Copy, Debug)] +pub(super) struct ProfileStats { + pub duration: f64, + pub sample_rate: f64, + pub error_rate: Option, + pub missed_samples: Option, +} + +impl ProfileStats { + pub(super) fn validate(&self) -> Result<()> { + for (value, name) in [ + (self.duration, "duration"), + (self.sample_rate, "sample rate"), + ] + .into_iter() + .chain(self.error_rate.map(|value| (value, "error rate"))) + { + if !value.is_finite() || value < 0.0 { + return Err(invalid(format!( + "{name} must be a finite non-negative value" + ))); + } + } + if self.missed_samples.is_some_and(|value| !value.is_finite()) { + return Err(invalid("missed samples must be a finite value")); + } + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, Default)] +pub(super) struct Stats { + pub records: [u64; 4], + pub repeat_samples: u64, + pub total_samples: u64, + pub total_frames_written: u64, + pub frames_saved: u64, + pub bytes_written: u64, + pub stack_reconstructions: u64, +} + +#[derive(Debug)] +struct WriterThread { + id: u64, + interpreter: u32, + timestamp: u64, + stack: Vec, + repeats: Vec, + repeat_count: u32, +} + +#[derive(Debug)] +pub(super) struct Writer { + // Present until Drop takes ownership without flushing the buffer. + file: Option>, + start_time: u64, + interval: u64, + config: u32, + strings: Vec, + string_indices: HashMap, + frames: Vec, + frame_indices: HashMap, + threads: Vec, + thread_indices: HashMap<(u64, u32), usize>, + pub stats: Stats, + pub profile_stats: Option, +} + +impl Drop for Writer { + fn drop(&mut self) { + if let Some(file) = self.file.take() { + // Finalization flushes pending samples. Close and destruction + // discard them instead of letting BufWriter flush on drop. + let _ = file.into_parts(); + } + } +} + +impl Writer { + pub(super) fn new( + file: File, + interval: u64, + start_time: u64, + mode: i32, + features: i32, + ) -> Result { + let mut file = BufWriter::with_capacity(512 * 1024, file); + file.write_all(&[0; HEADER_SIZE])?; + // Publish the placeholder even if the writer is closed before finalizing. + file.flush()?; + Ok(Self { + file: Some(file), + start_time, + interval, + config: (mode + 1) as u32 + | if features < 0 { + 0 + } else { + 8 | (features as u32) << 4 + }, + strings: Vec::new(), + string_indices: HashMap::new(), + frames: Vec::new(), + frame_indices: HashMap::new(), + threads: Vec::new(), + thread_indices: HashMap::new(), + stats: Stats::default(), + profile_stats: None, + }) + } + + fn intern_string(&mut self, value: &str) -> Result { + if let Some(&index) = self.string_indices.get(value) { + return Ok(index); + } + u32::try_from(value.len()).map_err(|_| Error::Overflow("String is too long"))?; + let index = + u32::try_from(self.strings.len()).map_err(|_| Error::Overflow("Too many strings"))?; + self.strings.push(value.to_owned()); + self.string_indices.insert(value.to_owned(), index); + Ok(index) + } + + fn intern_frame(&mut self, frame: Frame) -> Result { + if let Some(&index) = self.frame_indices.get(&frame) { + return Ok(index); + } + let index = + u32::try_from(self.frames.len()).map_err(|_| Error::Overflow("Too many frames"))?; + let entry = FrameEntry { + filename: self.intern_string(&frame.filename)?, + funcname: self.intern_string(&frame.funcname)?, + location: frame.location, + opcode: frame.opcode, + }; + self.frames.push(entry); + self.frame_indices.insert(frame, index); + Ok(index) + } + + fn header(out: &mut Vec, thread: &WriterThread, encoding: u8) { + out.extend_from_slice(&thread.id.to_le_bytes()); + out.extend_from_slice(&thread.interpreter.to_le_bytes()); + out.push(encoding); + } + + fn flush_repeats(&mut self, index: usize) -> Result<()> { + let thread = &mut self.threads[index]; + if thread.repeat_count == 0 { + return Ok(()); + } + let mut bytes = Vec::with_capacity(18 + thread.repeats.len()); + Self::header(&mut bytes, thread, 0); + varint(&mut bytes, thread.repeat_count as u64); + bytes.extend_from_slice(&thread.repeats); + self.file.as_mut().unwrap().write_all(&bytes)?; + self.stats.records[0] += 1; + self.stats.repeat_samples += thread.repeat_count as u64; + self.stats.frames_saved += thread.repeat_count as u64 * thread.stack.len() as u64; + self.stats.bytes_written += bytes.len() as u64; + thread.repeats.clear(); + thread.repeat_count = 0; + Ok(()) + } + + pub(super) fn sample( + &mut self, + id: u64, + interpreter: u32, + status: u8, + frames: Vec, + timestamp: u64, + ) -> Result<()> { + let stack = frames + .into_iter() + .take(MAX_STACK) + .map(|frame| self.intern_frame(frame)) + .collect::>>()?; + let key = (id, interpreter); + let index = match self.thread_indices.get(&key) { + Some(&index) => index, + None => { + u32::try_from(self.threads.len() + 1) + .map_err(|_| Error::Overflow("Too many threads"))?; + let index = self.threads.len(); + self.threads.push(WriterThread { + id, + interpreter, + timestamp: self.start_time, + stack: Vec::new(), + repeats: Vec::new(), + repeat_count: 0, + }); + self.thread_indices.insert(key, index); + index + } + }; + let delta = timestamp.wrapping_sub(self.threads[index].timestamp); + if stack == self.threads[index].stack { + if self.threads[index].repeats.len() + 11 > MAX_RLE_BYTES { + self.flush_repeats(index)?; + } + let thread = &mut self.threads[index]; + varint(&mut thread.repeats, delta); + thread.repeats.push(status); + thread.repeat_count += 1; + } else { + self.flush_repeats(index)?; + let thread = &mut self.threads[index]; + let shared = stack + .iter() + .rev() + .zip(thread.stack.iter().rev()) + .take_while(|(left, right)| left == right) + .count(); + let pop = thread.stack.len() - shared; + let push = stack.len() - shared; + let encoding = if shared == 0 { + 1 + } else if pop == 0 && push > 0 { + 2 + } else if shared < stack.len() / 2 { + 1 + } else { + 3 + }; + let mut bytes = Vec::with_capacity(26 + stack.len() * 5); + Self::header(&mut bytes, thread, encoding); + varint(&mut bytes, delta); + bytes.push(status); + let written = if encoding == 1 { + varint(&mut bytes, stack.len() as u64); + stack.len() + } else { + varint(&mut bytes, if encoding == 2 { shared } else { pop } as u64); + varint(&mut bytes, push as u64); + push + }; + for &frame in &stack[..written] { + varint(&mut bytes, frame as u64); + } + self.file.as_mut().unwrap().write_all(&bytes)?; + self.stats.records[encoding as usize] += 1; + self.stats.total_frames_written += written as u64; + self.stats.frames_saved += (stack.len() - written) as u64; + self.stats.bytes_written += bytes.len() as u64; + thread.stack = stack; + } + self.threads[index].timestamp = timestamp; + self.stats.total_samples += 1; + Ok(()) + } + + pub(super) fn finalize(&mut self, python_version: [u8; 3]) -> Result<()> { + for index in 0..self.threads.len() { + self.flush_repeats(index)?; + } + let file = self.file.as_mut().unwrap(); + let string_offset = file.stream_position()?; + let mut buffer = Vec::new(); + for string in &self.strings { + buffer.clear(); + varint(&mut buffer, string.len() as u64); + file.write_all(&buffer)?; + file.write_all(string.as_bytes())?; + } + let frame_offset = file.stream_position()?; + for frame in &self.frames { + buffer.clear(); + varint(&mut buffer, frame.filename as u64); + varint(&mut buffer, frame.funcname as u64); + for pair in frame.location.as_chunks::<2>().0 { + signed_varint(&mut buffer, pair[0]); + signed_varint( + &mut buffer, + if pair[0] == -1 || pair[1] == -1 { + 0 + } else { + pair[1].wrapping_sub(pair[0]) + }, + ); + } + buffer.push(frame.opcode); + file.write_all(&buffer)?; + } + if let Some(stats) = self.profile_stats { + buffer.clear(); + for value in [ + stats.duration, + stats.sample_rate, + stats.error_rate.unwrap_or_default(), + stats.missed_samples.unwrap_or_default(), + ] { + buffer.extend_from_slice(&value.to_le_bytes()); + } + let present = u32::from(stats.error_rate.is_some()) + | (u32::from(stats.missed_samples.is_some()) << 1); + buffer.extend_from_slice(&present.to_le_bytes()); + buffer.extend_from_slice(&[0; 4]); + buffer.extend_from_slice(b"TACHSTAT"); + buffer.extend_from_slice(&1u32.to_le_bytes()); + buffer.extend_from_slice(&56u32.to_le_bytes()); + file.write_all(&buffer)?; + } + let size = file.stream_position()? + FOOTER_SIZE as u64; + buffer.clear(); + buffer.extend_from_slice(&(self.strings.len() as u32).to_le_bytes()); + buffer.extend_from_slice(&(self.frames.len() as u32).to_le_bytes()); + buffer.extend_from_slice(&size.to_le_bytes()); + buffer.extend_from_slice(&[0; 16]); + file.write_all(&buffer)?; + buffer.clear(); + buffer.extend_from_slice(&0x54414348u32.to_le_bytes()); + buffer.extend_from_slice(&1u32.to_le_bytes()); + buffer.extend_from_slice(&python_version); + buffer.push(0); + for value in [self.start_time, self.interval, self.stats.total_samples] { + buffer.extend_from_slice(&value.to_le_bytes()); + } + buffer.extend_from_slice(&(self.threads.len() as u32).to_le_bytes()); + buffer.extend_from_slice(&string_offset.to_le_bytes()); + buffer.extend_from_slice(&frame_offset.to_le_bytes()); + buffer.extend_from_slice(&0u32.to_le_bytes()); + buffer.extend_from_slice(&self.config.to_le_bytes()); + file.seek(SeekFrom::Start(0))?; + file.write_all(&buffer)?; + file.flush()?; + Ok(()) + } +} + +#[derive(Debug)] +struct Cursor<'a> { + bytes: &'a [u8], + offset: usize, + big_endian: bool, +} + +impl<'a> Cursor<'a> { + fn take(&mut self, count: usize) -> Result<&'a [u8]> { + let end = self + .offset + .checked_add(count) + .ok_or_else(|| invalid("Invalid file offset"))?; + let bytes = self + .bytes + .get(self.offset..end) + .ok_or_else(|| invalid("Unexpected end of file"))?; + self.offset = end; + Ok(bytes) + } + fn byte(&mut self) -> Result { + Ok(self.take(1)?[0]) + } + fn u32(&mut self) -> Result { + let bytes = self.take(4)?.try_into().unwrap(); + Ok(if self.big_endian { + u32::from_be_bytes(bytes) + } else { + u32::from_le_bytes(bytes) + }) + } + fn u64(&mut self) -> Result { + let bytes = self.take(8)?.try_into().unwrap(); + Ok(if self.big_endian { + u64::from_be_bytes(bytes) + } else { + u64::from_le_bytes(bytes) + }) + } + fn varint(&mut self) -> Result { + let mut value = 0; + for shift in (0..70).step_by(7) { + let byte = self.byte().map_err(|_| invalid("Malformed varint"))?; + if shift == 63 && byte > 1 { + return Err(invalid("Malformed varint")); + } + value |= (byte as u64 & 127) << shift; + if byte & 128 == 0 { + return Ok(value); + } + } + Err(invalid("Malformed varint")) + } + fn varint32(&mut self) -> Result { + u32::try_from(self.varint()?).map_err(|_| invalid("Malformed varint")) + } + fn signed(&mut self) -> Result { + let value = self.varint32()?; + Ok((value >> 1) as i32 ^ -((value & 1) as i32)) + } +} + +#[derive(Debug)] +pub(super) struct Reader { + bytes: Vec, + sample_end: usize, + big_endian: bool, + pub python_version: [u8; 3], + pub start_time: u64, + pub interval: u64, + pub sample_count: u64, + pub thread_count: u32, + pub mode: Option, + pub features: Option, + pub profile_stats: Option, + pub strings: Vec, + frames: Vec, +} + +impl Reader { + pub(super) fn open(mut file: File) -> Result { + let size = usize::try_from(file.metadata()?.len()) + .map_err(|_| Error::Overflow("File is too large to read"))?; + let mut bytes = Vec::new(); + bytes.try_reserve_exact(size).map_err(|_| Error::Memory)?; + file.read_to_end(&mut bytes)?; + Self::parse(bytes) + } + + pub(super) fn parse(bytes: Vec) -> Result { + if bytes.len() < HEADER_SIZE { + return Err(invalid("File too small for header")); + } + let mut cursor = Cursor { + bytes: &bytes, + offset: 0, + big_endian: false, + }; + let magic = cursor.u32()?; + cursor.big_endian = match magic { + 0x54414348 => false, + 0x48434154 => true, + _ => return Err(invalid(format!("Invalid magic number: 0x{magic:08x}"))), + }; + let version = cursor.u32()?; + if version != 1 { + return Err(invalid(format!( + "Unsupported format version {version} (this reader supports version 1)" + ))); + } + let python_version = cursor.take(4)?[..3].try_into().unwrap(); + let start_time = cursor.u64()?; + let interval = cursor.u64()?; + let sample_count = cursor.u64()?; + let thread_count = cursor.u32()?; + let string_offset = cursor.u64()?; + let frame_offset = cursor.u64()?; + let compression = cursor.u32()?; + let config = cursor.u32()?; + if config & 7 > 5 { + return Err(invalid(format!( + "Invalid profiling mode in header: {}", + config & 7 + ))); + } + for (name, offset) in [("string", string_offset), ("frame", frame_offset)] { + if offset > bytes.len() as u64 { + return Err(invalid(format!( + "Invalid {name} table offset: {offset} exceeds file size {}", + bytes.len() + ))); + } + if offset < HEADER_SIZE as u64 { + return Err(invalid(format!( + "Invalid {name} table offset: {offset} is before data section" + ))); + } + } + if string_offset > frame_offset { + return Err(invalid(format!( + "Invalid table offsets: string table ({string_offset}) is after frame table ({frame_offset})" + ))); + } + match compression { + 0 => (), + 1 => { + return Err(Error::Runtime( + "File uses zstd compression but zstd support not compiled in", + )); + } + _ => { + return Err(invalid(format!( + "Unsupported compression type: {compression}" + ))); + } + } + cursor.offset = bytes.len() - FOOTER_SIZE; + let string_count = cursor.u32()?; + let frame_count = cursor.u32()?; + let mut profile_stats = None; + if bytes.len() >= FOOTER_SIZE + 32 + && &bytes[bytes.len() - 48..bytes.len() - 40] == b"TACHSTAT" + { + cursor.offset = bytes.len() - 40; + let version = cursor.u32()?; + let size = cursor.u32()? as usize; + if size < 32 || size > bytes.len() - FOOTER_SIZE { + return Err(invalid("Invalid profiling statistics size")); + } + if version == 1 { + cursor.offset = bytes.len() - FOOTER_SIZE - size; + let mut stats = ProfileStats { + duration: f64::from_bits(cursor.u64()?), + sample_rate: f64::from_bits(cursor.u64()?), + error_rate: None, + missed_samples: None, + }; + if size >= 56 { + let error = f64::from_bits(cursor.u64()?); + let missed = f64::from_bits(cursor.u64()?); + let present = cursor.u32()?; + stats.error_rate = (present & 1 != 0).then_some(error); + stats.missed_samples = (present & 2 != 0).then_some(missed); + } + stats + .validate() + .map_err(|_| invalid("Invalid profiling statistics values"))?; + profile_stats = Some(stats); + } + } + let string_offset = string_offset as usize; + let frame_offset = frame_offset as usize; + for (name, count, offset, entry_size) in [ + ("string", string_count, string_offset, 1), + ("frame", frame_count, frame_offset, 7), + ] { + let maximum = (bytes.len() - offset) / entry_size; + if count as usize > maximum { + return Err(invalid(format!( + "Invalid {name} count {count} exceeds maximum possible {maximum}" + ))); + } + } + let mut strings = Vec::new(); + strings + .try_reserve_exact(string_count as usize) + .map_err(|_| Error::Memory)?; + cursor.offset = string_offset; + for _ in 0..string_count { + let size = cursor.varint32()? as usize; + let string = cursor + .take(size) + .map_err(|_| invalid("String table overflow"))?; + strings.push(String::from_utf8_lossy(string).into_owned()); + } + let mut frames = Vec::new(); + frames + .try_reserve_exact(frame_count as usize) + .map_err(|_| Error::Memory)?; + cursor.offset = frame_offset; + for _ in 0..frame_count { + let filename = cursor.varint32()?; + let funcname = cursor.varint32()?; + let mut location = [0; 4]; + for pair in location.as_chunks_mut::<2>().0 { + pair[0] = cursor.signed()?; + let delta = cursor.signed()?; + pair[1] = if pair[0] == -1 { + -1 + } else { + pair[0].wrapping_add(delta) + }; + } + frames.push(FrameEntry { + filename, + funcname, + location, + opcode: cursor.byte()?, + }); + } + let big_endian = cursor.big_endian; + Ok(Self { + bytes, + sample_end: string_offset, + big_endian, + python_version, + start_time, + interval, + sample_count, + thread_count, + mode: (config & 7).checked_sub(1), + features: (config & 8 != 0).then_some((config >> 4) & 31), + profile_stats, + strings, + frames, + }) + } + + pub(super) fn frame_count(&self) -> usize { + self.frames.len() + } + + pub(super) fn frame(&self, index: u32) -> Result { + let frame = self + .frames + .get(index as usize) + .ok_or_else(|| invalid(format!("Invalid frame index: {index}")))?; + let filename = self + .strings + .get(frame.filename as usize) + .ok_or_else(|| invalid("Invalid string index in frame"))? + .clone(); + let funcname = self + .strings + .get(frame.funcname as usize) + .ok_or_else(|| invalid("Invalid string index in frame"))? + .clone(); + Ok(Frame { + filename, + funcname, + location: frame.location, + opcode: frame.opcode, + }) + } + + pub(super) fn replay(&self) -> Replay<'_> { + Replay { + reader: self, + cursor: Cursor { + bytes: &self.bytes[..self.sample_end], + offset: HEADER_SIZE, + big_endian: self.big_endian, + }, + threads: HashMap::new(), + stats: Stats::default(), + pending: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn file(samples: &[u8], count: u64) -> Vec { + let mut bytes = vec![0; HEADER_SIZE]; + bytes[..4].copy_from_slice(&0x54414348u32.to_le_bytes()); + bytes[4..8].copy_from_slice(&1u32.to_le_bytes()); + bytes[28..36].copy_from_slice(&count.to_le_bytes()); + bytes[36..40].copy_from_slice(&1u32.to_le_bytes()); + let table = (HEADER_SIZE + samples.len()) as u64; + bytes[40..48].copy_from_slice(&table.to_le_bytes()); + bytes[48..56].copy_from_slice(&table.to_le_bytes()); + bytes.extend_from_slice(samples); + bytes.extend_from_slice(&[0; FOOTER_SIZE]); + bytes + } + + fn sample_header(encoding: u8) -> Vec { + let mut bytes = vec![0; 13]; + bytes[12] = encoding; + bytes + } + + #[test] + fn varint_rejects_overflow_and_truncation() { + let mut encoded = Vec::new(); + varint(&mut encoded, u64::MAX); + let mut cursor = Cursor { + bytes: &encoded, + offset: 0, + big_endian: false, + }; + assert_eq!(cursor.varint().unwrap(), u64::MAX); + encoded[9] = 2; + let mut cursor = Cursor { + bytes: &encoded, + offset: 0, + big_endian: false, + }; + assert!(cursor.varint().is_err()); + let mut cursor = Cursor { + bytes: &[128], + offset: 0, + big_endian: false, + }; + assert!(cursor.varint().is_err()); + } + + #[test] + fn full_stack_depth_is_bounded_before_allocation() { + let mut bytes = sample_header(1); + bytes.extend_from_slice(&[0, 0]); + varint(&mut bytes, 257); + let reader = Reader::parse(file(&bytes, 1)).unwrap(); + assert!( + matches!(reader.replay().next(), Err(Error::Value(message)) if message == "Stack depth 257 exceeds capacity 256") + ); + } + + #[test] + fn repeat_count_is_bounded_by_remaining_bytes() { + let mut bytes = sample_header(0); + varint(&mut bytes, u32::MAX as u64); + let reader = Reader::parse(file(&bytes, 1)).unwrap(); + assert!( + matches!(reader.replay().next(), Err(Error::Value(message)) if message == "Invalid RLE count 4294967295 exceeds maximum possible 0") + ); + } + + #[test] + fn empty_repeat_records_do_not_recurse() { + let mut bytes = sample_header(0); + bytes.push(0); + let reader = Reader::parse(file(&bytes.repeat(10_000), 0)).unwrap(); + let mut replay = reader.replay(); + let mut records = 0; + while let Some(batch) = replay.next().unwrap() { + assert!(batch.timestamps.is_empty()); + records += 1; + } + assert_eq!(records, 10_000); + assert_eq!(replay.stats.total_samples, 0); + } + + #[test] + fn table_count_is_bounded_before_allocation() { + let mut bytes = file(&[], 0); + bytes[HEADER_SIZE..HEADER_SIZE + 4].copy_from_slice(&u32::MAX.to_le_bytes()); + assert!( + matches!(Reader::parse(bytes), Err(Error::Value(message)) if message == "Invalid string count 4294967295 exceeds maximum possible 32") + ); + } +} + +#[derive(Debug, Default)] +struct ReaderThread { + timestamp: u64, + stack: Vec, +} + +pub(super) struct Batch { + pub id: u64, + pub interpreter: u32, + pub status: u8, + pub stack: Vec, + pub timestamps: Vec, +} + +struct Repeat { + id: u64, + interpreter: u32, + remaining: u32, +} + +pub(super) struct Replay<'a> { + reader: &'a Reader, + cursor: Cursor<'a>, + threads: HashMap<(u64, u32), ReaderThread>, + pub stats: Stats, + pending: Option, +} + +impl Replay<'_> { + pub(super) fn next(&mut self) -> Result> { + if let Some(mut repeat) = self.pending.take() { + let thread = self + .threads + .get_mut(&(repeat.id, repeat.interpreter)) + .unwrap(); + let mut status = 0; + let mut timestamps = Vec::new(); + // A file can advertise a large repeat group; emit bounded batches. + while repeat.remaining != 0 && timestamps.len() < MAX_RLE_BYTES { + let before = self.cursor.offset; + let delta = self.cursor.varint()?; + let current_status = self.cursor.byte()?; + if !timestamps.is_empty() && status != current_status { + self.cursor.offset = before; + break; + } + status = current_status; + thread.timestamp = thread.timestamp.wrapping_add(delta); + timestamps.push(thread.timestamp); + repeat.remaining -= 1; + } + let batch = Batch { + id: repeat.id, + interpreter: repeat.interpreter, + status, + stack: thread.stack.clone(), + timestamps, + }; + self.stats.total_samples = self + .stats + .total_samples + .checked_add(batch.timestamps.len() as u64) + .ok_or(Error::Overflow("Sample count exceeds maximum"))?; + if repeat.remaining > 0 { + self.pending = Some(repeat); + } + return Ok(Some(batch)); + } + let remaining = self.cursor.bytes.len() - self.cursor.offset; + if remaining == 0 { + if self.stats.total_samples != self.reader.sample_count { + return Err(invalid(format!( + "Sample count mismatch: header declares {} samples but replay decoded {}", + self.reader.sample_count, self.stats.total_samples + ))); + } + return Ok(None); + } + if remaining < 13 { + return Err(invalid(format!( + "Truncated sample data: {remaining} trailing bytes" + ))); + } + let id = self.cursor.u64()?; + let interpreter = self.cursor.u32()?; + let encoding = self.cursor.byte()?; + let key = (id, interpreter); + if !self.threads.contains_key(&key) { + if self.threads.len() >= self.reader.thread_count as usize { + return Err(invalid(format!( + "Invalid thread count: sample data contains more unique threads than declared in header (declared {}, found at least {})", + self.reader.thread_count, + self.threads.len() + 1 + ))); + } + self.threads.insert( + key, + ReaderThread { + timestamp: self.reader.start_time, + stack: Vec::new(), + }, + ); + } + if encoding > 3 { + return Err(invalid(format!("Unknown stack encoding: {encoding}"))); + } + self.stats.records[encoding as usize] += 1; + if encoding == 0 { + let count = self.cursor.varint32()?; + let max = (self.cursor.bytes.len() - self.cursor.offset) / 2; + if count as usize > max { + return Err(invalid(format!( + "Invalid RLE count {count} exceeds maximum possible {max}" + ))); + } + self.stats.repeat_samples += count as u64; + if count != 0 { + self.pending = Some(Repeat { + id, + interpreter, + remaining: count, + }); + } + // Continue iteratively for a zero-length repeat record, avoiding + // recursion controlled by untrusted file contents. + if count == 0 { + return Ok(Some(Batch { + id, + interpreter, + status: 0, + stack: Vec::new(), + timestamps: Vec::new(), + })); + } + return self.next(); + } + let delta = self.cursor.varint()?; + let status = self.cursor.byte()?; + let thread = self.threads.get_mut(&key).unwrap(); + thread.timestamp = thread.timestamp.wrapping_add(delta); + let first = self.cursor.varint32()? as usize; + let (keep, push) = if encoding == 1 { + (0, first) + } else { + let push = self.cursor.varint32()? as usize; + let keep = if encoding == 2 { + if first > thread.stack.len() { + return Err(invalid(format!( + "Shared count {first} exceeds current stack depth {}", + thread.stack.len() + ))); + } + first + } else { + thread.stack.len().saturating_sub(first) + }; + (keep, push) + }; + let depth = keep as u64 + push as u64; + if depth > MAX_STACK as u64 { + let name = if encoding == 1 { + "Stack" + } else { + "Final stack" + }; + return Err(invalid(format!( + "{name} depth {depth} exceeds capacity {MAX_STACK}" + ))); + } + let mut stack = Vec::with_capacity(depth as usize); + for _ in 0..push { + stack.push(self.cursor.varint32()?); + } + stack.extend_from_slice(&thread.stack[thread.stack.len() - keep..]); + thread.stack = stack; + self.stats.stack_reconstructions += 1; + self.stats.total_samples += 1; + Ok(Some(Batch { + id, + interpreter, + status, + stack: thread.stack.clone(), + timestamps: vec![thread.timestamp], + })) + } +} diff --git a/crates/stdlib/src/pickle.rs b/crates/stdlib/src/pickle.rs index 51427c0c9b0..f9a462f0912 100644 --- a/crates/stdlib/src/pickle.rs +++ b/crates/stdlib/src/pickle.rs @@ -5,7 +5,7 @@ pub(crate) use _pickle::module_def; mod _pickle { use crate::common::{ lock::{PyMutex, PyRwLock}, - wtf8::Wtf8Buf, + wtf8::{Wtf8Buf, wtf8_concat}, }; use crate::vm::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, @@ -15,6 +15,7 @@ mod _pickle { PyInt, PyList, PySet, PyStr, PyTuple, PyTupleRef, PyType, PyTypeRef, }, function::{FuncArgs, OptionalArg, PySetterValue}, + object::{Traverse, TraverseFn}, protocol::{PyBuffer, PyIter, PyIterReturn}, types::{AsBuffer, Constructor, Initializer, Representable}, }; @@ -228,6 +229,9 @@ mod _pickle { /// Amount of data `peek()` grabs at a time from a file-like input. const PREFETCH: usize = 8192; + // Grow reads geometrically from this bound, rather than trusting a length + // advertised by a potentially truncated pickle. + const MIN_READ_BUF_SIZE: usize = 1 << 20; // input handling @@ -247,14 +251,29 @@ mod _pickle { frame_end: Option, } + // SAFETY: these are all the Python references owned by the input state. + unsafe impl Traverse for ReadState { + fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { + self.read.traverse(traverse_fn); + self.readline.traverse(traverse_fn); + self.peek.traverse(traverse_fn); + } + } + fn to_byte_vec(obj: &PyObject, what: &str, vm: &VirtualMachine) -> PyResult> { - obj.downcast_ref::() - .map(|b| b.as_bytes().to_vec()) + let bytes = obj + .downcast_ref::() .ok_or_else(|| { vm.new_type_error(format!( "{what}() from the underlying stream did not return bytes" )) - }) + })? + .as_bytes(); + let mut data = Vec::new(); + data.try_reserve_exact(bytes.len()) + .map_err(|_| vm.no_memory_error())?; + data.extend_from_slice(bytes); + Ok(data) } fn truncated(vm: &VirtualMachine) -> PyBaseExceptionRef { @@ -304,8 +323,19 @@ mod _pickle { } } let read = self.read.clone().expect("file input"); - let data = read.call((n,), vm)?; - let data = to_byte_vec(&data, "read", vm)?; + let mut chunk_size = n.min(MIN_READ_BUF_SIZE); + let data = read.call((chunk_size,), vm)?; + let mut data = to_byte_vec(&data, "read", vm)?; + let mut received = data.len(); + while received == chunk_size && data.len() < n { + chunk_size = data.len().min(n - data.len()); + let chunk = read.call((chunk_size,), vm)?; + let chunk = to_byte_vec(&chunk, "read", vm)?; + received = chunk.len(); + data.try_reserve(received) + .map_err(|_| vm.no_memory_error())?; + data.extend_from_slice(&chunk); + } let len = data.len(); self.set_input(data, true); Ok(len) @@ -316,7 +346,7 @@ mod _pickle { if let Some(end) = self.frame_end { if self.pos >= end { self.frame_end = None; - } else if self.pos + n > end { + } else if n > end - self.pos { return Err(new_unpickling_error( vm, "pickle exhausted before end of frame", @@ -398,7 +428,7 @@ mod _pickle { // memo proxies #[pyattr] - #[pyclass(module = "_pickle", name = "UnpicklerMemoProxy")] + #[pyclass(module = "_pickle", name = "UnpicklerMemoProxy", traverse)] #[derive(Debug, PyPayload)] struct UnpicklerMemoProxy { unpickler: PyRef, @@ -464,6 +494,13 @@ mod _pickle { buffers: Option, } + // SAFETY: buffers is the only Python reference owned by the configuration. + unsafe impl Traverse for UnpicklerConfig { + fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { + self.buffers.traverse(traverse_fn); + } + } + impl Default for UnpicklerConfig { fn default() -> Self { Self { @@ -478,7 +515,7 @@ mod _pickle { } #[pyattr] - #[pyclass(module = "_pickle", name = "Unpickler")] + #[pyclass(module = "_pickle", name = "Unpickler", traverse = "manual")] #[derive(Debug, PyPayload)] pub(super) struct PyUnpickler { read_state: PyMutex, @@ -486,6 +523,34 @@ mod _pickle { config: PyRwLock, } + // SAFETY: visit each owned reference once, including sparse memo entries. + unsafe impl Traverse for PyUnpickler { + fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { + self.read_state.traverse(traverse_fn); + self.config.traverse(traverse_fn); + if let Some(memo) = self.memo.try_read_recursive() { + #[expect( + clippy::iter_over_hash_type, + reason = "GC tracing visits every memo reference regardless of index order" + )] + for value in memo.values() { + value.traverse(traverse_fn); + } + } + } + + fn clear(&mut self, out: &mut Vec) { + let state = self.read_state.get_mut(); + out.extend(state.read.take()); + out.extend(state.readline.take()); + out.extend(state.peek.take()); + out.extend(self.memo.get_mut().drain().map(|(_, value)| value)); + let config = self.config.get_mut(); + out.extend(config.buffers.take()); + config.initialized = false; + } + } + #[derive(FromArgs)] pub(super) struct UnpicklerNewArgs { #[pyarg(any)] @@ -556,23 +621,29 @@ mod _pickle { return Err(vm.new_type_error("file must have 'read' and 'readline' attributes")); } - *state = ReadState { - read, - readline, - peek, - ..ReadState::default() - }; + let old_state = core::mem::replace( + &mut *state, + ReadState { + read, + readline, + peek, + ..ReadState::default() + }, + ); let old_memo = core::mem::take(&mut *zelf.memo.write()); - *zelf.config.write() = UnpicklerConfig { - initialized: true, - proto: 0, - fix_imports: args.fix_imports, - encoding, - errors, - buffers, - }; + let old_config = core::mem::replace( + &mut *zelf.config.write(), + UnpicklerConfig { + initialized: true, + proto: 0, + fix_imports: args.fix_imports, + encoding, + errors, + buffers, + }, + ); drop(state); - drop(old_memo); + drop((old_state, old_memo, old_config)); Ok(()) } } @@ -3432,6 +3503,7 @@ mod _pickle { .map_err(|_| new_pickling_error(vm, "the global name must be a string"))? } }; + let name = mangle_global_name(name, vm); let module_name = whichmodule(obj, &name, vm)?; if self.proto >= 2 { @@ -3606,6 +3678,38 @@ mod _pickle { .collect() } + fn mangle_global_name(name: PyRef, vm: &VirtualMachine) -> PyRef { + if !name.as_bytes().windows(3).any(|part| part == b".__") { + return name; + } + let mut parts = split_dotted(&name, vm); + let mut changed = false; + // Work backwards so each parent still has its original class name. + for i in (1..parts.len()).rev() { + let part = &parts[i]; + if part.as_bytes().starts_with(b"__") && !part.as_bytes().ends_with(b"__") { + let parent = parts[i - 1] + .as_wtf8() + .trim_start_matches(|ch| ch.to_u32() == u32::from(b'_')); + if !parent.is_empty() { + parts[i] = vm.ctx.new_str(wtf8_concat!("_", parent, part.as_wtf8())); + changed = true; + } + } + } + if !changed { + return name; + } + let mut mangled = Wtf8Buf::new(); + for (i, part) in parts.iter().enumerate() { + if i != 0 { + mangled.push_char('.'); + } + mangled.push_wtf8(part.as_wtf8()); + } + vm.ctx.new_str(mangled) + } + fn whichmodule( obj: &PyObject, name: &Py, diff --git a/crates/stdlib/src/pyexpat.rs b/crates/stdlib/src/pyexpat.rs index 724fdb75926..c982cb19821 100644 --- a/crates/stdlib/src/pyexpat.rs +++ b/crates/stdlib/src/pyexpat.rs @@ -168,7 +168,9 @@ mod _pyexpat { use crate::vm::{ AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, - builtins::{PyBytesRef, PyException, PyModule, PyStr, PyStrRef, PyType, PyUtf8StrRef}, + builtins::{ + PyBytesRef, PyException, PyFloat, PyModule, PyStr, PyStrRef, PyType, PyUtf8StrRef, + }, extend_module, function::{ArgBytesLike, Either, IntoFuncArgs, OptionalArg, OptionalOption}, types::Constructor, @@ -176,7 +178,8 @@ mod _pyexpat { use alloc::collections::VecDeque; use alloc::rc::Rc; use core::cell::RefCell; - use core::sync::atomic::{AtomicBool, Ordering}; + use core::sync::atomic::{AtomicBool, AtomicI32, Ordering}; + use num_traits::Signed; use rustpython_common::lock::PyRwLock; use std::io::Read; use xml::common::Position; @@ -247,6 +250,29 @@ mod _pyexpat { // become harmless no-ops instead of starting a new document. #[pytraverse(skip)] finished: AtomicBool, + #[pytraverse(skip)] + namespace_stack: PyRwLock)>>, + #[pytraverse(skip)] + reparse_deferral_enabled: AtomicBool, + #[pytraverse(skip)] + parameter_entity_parsing: AtomicI32, + #[pytraverse(skip)] + use_foreign_dtd: AtomicBool, + #[pytraverse(skip)] + standalone: AtomicBool, + #[pytraverse(skip)] + external_subset_handled: AtomicBool, + #[pytraverse(skip)] + started: AtomicBool, + #[pytraverse(skip)] + amplification: PyRwLock, + // Preserve a protection breach after tearing down the backend, so + // later feeds cannot accidentally restart the document's accounting. + #[pytraverse(skip)] + amplification_error: PyRwLock>, + #[pytraverse(skip)] + external_entity: Option, + parent: Option, start_element: MutableObject, end_element: MutableObject, character_data: MutableObject, @@ -296,6 +322,96 @@ mod _pyexpat { Ok(()) } + fn invoke_int_handler( + vm: &VirtualMachine, + handler: &MutableObject, + name: &str, + args: T, + ) -> PyResult> + where + T: IntoFuncArgs, + { + let handler = handler.read().clone(); + if vm.is_none(&handler) { + return Ok(None); + } + let result = handler.call(args, vm)?; + core::ffi::c_long::try_from_object(vm, result) + .map(|value| { + // Expat callbacks convert C long to int. These types have the + // same width on Windows, but differ on 64-bit Unix targets. + #[allow(clippy::unnecessary_cast)] + let value = value as i32; + Some(value) + }) + .inspect_err(|error| { + let _ = error.add_note( + vm.ctx + .new_str(format!("invalid '{name}' event handler return value")), + vm, + ); + }) + } + + #[derive(Debug)] + struct ExternalEntity { + context: Option, + encoding: Option, + } + + #[derive(Debug)] + struct AmplificationSettings { + maximum: f32, + threshold: u64, + frozen: bool, + } + + struct Doctype { + name: String, + system_id: Option, + public_id: Option, + has_internal_subset: bool, + } + + impl Doctype { + fn from_reader(parser: &xml::EventReader, event: &XmlEvent) -> Option { + let XmlEvent::Doctype { syntax } = event else { + return None; + }; + let ids = parser.doctype_ids()?; + let mut quote = None; + let has_internal_subset = syntax.chars().any(|character| { + if let Some(delimiter) = quote { + if character == delimiter { + quote = None; + } + } else if matches!(character, '\'' | '"') { + quote = Some(character); + } else if character == '[' { + return true; + } + false + }); + Some(Self { + name: ids.name().to_owned(), + system_id: ids.system_id().map(str::to_owned), + public_id: ids.public_id().map(str::to_owned), + has_internal_subset, + }) + } + } + + fn is_incomplete(error: &xml::reader::Error) -> bool { + match error.kind() { + xml::reader::ErrorKind::UnexpectedEof => true, + xml::reader::ErrorKind::Syntax(message) => { + message.starts_with("Unexpected end of stream") + || message.starts_with("Unclosed false, + } + } + /// Tracks byte offsets of line starts as bytes are handed to the parser /// thread, so that `TextPosition` (row/column, in characters) can be /// translated into an approximation of libexpat's `CurrentByteIndex` @@ -531,6 +647,7 @@ mod _pyexpat { #[cfg(not(target_arch = "wasm32"))] struct PreparedEvent { event: XmlEvent, + doctype: Option, line: i64, column: i64, byte_index: i64, @@ -547,10 +664,8 @@ mod _pyexpat { Event(PreparedEvent), /// `EndDocument` was reached; the parser thread has exited. Finished, - /// xml-rs reported a parse error. Mirrors the previous - /// whole-buffer implementation, which silently stopped dispatching - /// on the first parse error rather than raising `ExpatError`. - Error, + /// xml-rs reported a parse error, to be raised on the calling thread. + Error(xml::reader::Error), } /// Set, once and permanently, by a `pthread_atfork` child hook the @@ -714,6 +829,7 @@ mod _pyexpat { let pos = parser.position(); let byte_index = tracker.borrow_mut().byte_index_for(pos); let msg = ParserMsg::Event(PreparedEvent { + doctype: Doctype::from_reader(&parser, &event), event, line: pos.row() as i64 + 1, column: pos.column() as i64, @@ -723,8 +839,8 @@ mod _pyexpat { break; } } - Err(_) => { - let _ = tx.send(ParserMsg::Error); + Err(error) => { + let _ = tx.send(ParserMsg::Error(error)); break; } } @@ -807,6 +923,8 @@ mod _pyexpat { fn new( namespace_separator: Option, intern: OptionalOption, + parent: Option, + external_entity: Option, vm: &VirtualMachine, ) -> PyExpatLikeXmlParserRef { let intern_dict = match intern { @@ -823,6 +941,21 @@ mod _pyexpat { backend: PyRwLock::new(None), busy: AtomicBool::new(false), finished: AtomicBool::new(false), + namespace_stack: PyRwLock::new(Vec::new()), + reparse_deferral_enabled: AtomicBool::new(true), + parameter_entity_parsing: AtomicI32::new(XML_PARAM_ENTITY_PARSING_NEVER), + use_foreign_dtd: AtomicBool::new(false), + standalone: AtomicBool::new(false), + external_subset_handled: AtomicBool::new(false), + started: AtomicBool::new(false), + amplification: PyRwLock::new(AmplificationSettings { + maximum: 100.0, + threshold: 8_388_608, + frozen: false, + }), + amplification_error: PyRwLock::new(None), + external_entity, + parent, start_element: MutableObject::new(vm.ctx.none()), end_element: MutableObject::new(vm.ctx.none()), character_data: MutableObject::new(vm.ctx.none()), @@ -1006,29 +1139,180 @@ mod _pyexpat { } fn create_config(&self) -> xml::ParserConfig { - xml::ParserConfig::new() + let mut config = xml::ParserConfig::new() .cdata_to_characters(false) .coalesce_characters(false) .ignore_comments(false) - .whitespace_to_characters(true) + .whitespace_to_characters(true); + let settings = self.amplification.read(); + config.amplification_limits = Some( + xml::reader::AmplificationLimits::new(settings.maximum, settings.threshold) + .expect("validated amplification settings"), + ); + config + } + + fn update_amplification_settings( + &self, + update: impl FnOnce(&mut AmplificationSettings), + vm: &VirtualMachine, + ) -> PyResult<()> { + if self.parent.is_some() { + return Err(self.amplification_argument_error(vm, "parser must be a root parser")?); + } + { + let mut settings = self.amplification.write(); + if !settings.frozen { + update(&mut settings); + return Ok(()); + } + } + // Error allocation can run GC callbacks on non-threaded builds. + // Keep it outside the atomic frozen-check/update critical section. + Err(vm.new_not_implemented_error( + "changing amplification protection after parsing has begun is not supported by the xml-rs backend", + )) + } + + #[pymethod(name = "SetBillionLaughsAttackProtectionMaximumAmplification")] + fn set_maximum_amplification( + zelf: &Py, + max_factor: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + // PyFloat_AsDouble accepts float subclasses without invoking + // their __float__, then Expat validates the rounded C float. + let maximum = if let Some(value) = max_factor.downcast_ref::() { + value.to_f64() + } else { + max_factor.try_float(vm)?.to_f64() + } as f32; + if maximum.is_nan() || maximum < 1.0 { + return Err( + zelf.amplification_argument_error(vm, "'max_factor' must be at least 1.0")? + ); + } + zelf.update_amplification_settings(|settings| settings.maximum = maximum, vm) + } + + #[pymethod(name = "SetBillionLaughsAttackProtectionActivationThreshold")] + fn set_amplification_threshold( + zelf: &Py, + threshold: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + let threshold = threshold.try_index(vm)?; + if threshold.as_bigint().is_negative() { + return Err(vm.new_value_error("Cannot convert negative int")); + } + let threshold = u64::try_from(threshold.as_bigint()).map_err(|_| { + vm.new_overflow_error("Python int too large for C unsigned long long") + })?; + zelf.update_amplification_settings(|settings| settings.threshold = threshold, vm) + } + + #[pymethod(name = "GetReparseDeferralEnabled")] + fn get_reparse_deferral_enabled(zelf: &Py) -> bool { + zelf.reparse_deferral_enabled.load(Ordering::Relaxed) + } + + #[pymethod(name = "SetReparseDeferralEnabled")] + fn set_reparse_deferral_enabled(zelf: &Py, enabled: bool) { + // The streaming backend resumes a suspended parser rather than + // reparsing an unfinished token, so it needs no deferral to avoid + // quadratic work. Preserve the setting used by callers that + // temporarily disable it to flush already supplied input. + zelf.reparse_deferral_enabled + .store(enabled, Ordering::Relaxed); } #[pymethod(name = "SetParamEntityParsing")] - fn set_param_entity_parsing(_zelf: &Py, _flag: i32) -> i32 { - // Compatibility shim: xml.sax requires this setup API, but xml-rs - // does not expose Expat parameter entity parsing configuration. + fn set_param_entity_parsing(zelf: &Py, flag: i32) -> i32 { + if zelf.started.load(Ordering::Relaxed) && !zelf.finished.load(Ordering::Relaxed) { + return 0; + } + zelf.parameter_entity_parsing.store(flag, Ordering::Relaxed); 1 } #[pymethod(name = "UseForeignDTD")] - fn use_foreign_dtd(_zelf: &Py, _flag: OptionalArg) { - // Compatibility shim: CPython's implementation forwards the flag to - // libexpat's XML_UseForeignDTD, which lets a DTD handler splice in an - // external subset for documents that only declare one (e.g. via - // NotStandaloneHandler). The xml-rs backend used here has no such hook - // and always parses documents standalone, so the flag is accepted and - // ignored purely so callers that toggle this setting (e.g. genshi) don't - // fail with AttributeError. + fn use_foreign_dtd( + zelf: &Py, + flag: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult<()> { + if zelf.started.load(Ordering::Relaxed) && !zelf.finished.load(Ordering::Relaxed) { + return Err(zelf.handler_error( + vm, + 26, + "cannot change setting once parsing has begun", + )?); + } + zelf.use_foreign_dtd + .store(flag.unwrap_or(true), Ordering::Relaxed); + Ok(()) + } + + #[pymethod(name = "ExternalEntityParserCreate")] + fn external_entity_parser_create( + zelf: PyRef, + context: Option, + encoding: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + let context = context.map(|s| s.as_str().to_owned()); + let encoding = encoding.into_option().map(|s| s.as_str().to_owned()); + if context.as_ref().is_some_and(|s| s.contains('\0')) + || encoding.as_ref().is_some_and(|s| s.contains('\0')) + { + return Err(vm.new_value_error("embedded null character")); + } + let child = Self::new( + zelf.namespace_separator.clone(), + OptionalArg::Present(Some(zelf.intern.clone())), + Some(zelf.clone()), + Some(ExternalEntity { context, encoding }), + vm, + ); + child.reparse_deferral_enabled.store( + zelf.reparse_deferral_enabled.load(Ordering::Relaxed), + Ordering::Relaxed, + ); + child.parameter_entity_parsing.store( + zelf.parameter_entity_parsing.load(Ordering::Relaxed), + Ordering::Relaxed, + ); + for (source, target) in [ + (&zelf.buffer_text, &child.buffer_text), + (&zelf.namespace_prefixes, &child.namespace_prefixes), + (&zelf.ordered_attributes, &child.ordered_attributes), + (&zelf.specified_attributes, &child.specified_attributes), + (&zelf.start_element, &child.start_element), + (&zelf.end_element, &child.end_element), + (&zelf.character_data, &child.character_data), + (&zelf.entity_decl, &child.entity_decl), + (&zelf.processing_instruction, &child.processing_instruction), + (&zelf.unparsed_entity_decl, &child.unparsed_entity_decl), + (&zelf.notation_decl, &child.notation_decl), + (&zelf.start_namespace_decl, &child.start_namespace_decl), + (&zelf.end_namespace_decl, &child.end_namespace_decl), + (&zelf.comment, &child.comment), + (&zelf.start_cdata_section, &child.start_cdata_section), + (&zelf.end_cdata_section, &child.end_cdata_section), + (&zelf.default, &child.default), + (&zelf.default_expand, &child.default_expand), + (&zelf.not_standalone, &child.not_standalone), + (&zelf.external_entity_ref, &child.external_entity_ref), + (&zelf.start_doctype_decl, &child.start_doctype_decl), + (&zelf.end_doctype_decl, &child.end_doctype_decl), + (&zelf.xml_decl, &child.xml_decl), + (&zelf.element_decl, &child.element_decl), + (&zelf.attlist_decl, &child.attlist_decl), + (&zelf.skipped_entity, &child.skipped_entity), + ] { + *target.write() = source.read().clone(); + } + Ok(child) } #[pymethod(name = "SetBase")] @@ -1048,9 +1332,18 @@ mod _pyexpat { } /// Construct element name with namespace if separator is set - fn make_name(&self, name: &xml::name::OwnedName) -> String { + fn make_name(&self, name: &xml::name::OwnedName, vm: &VirtualMachine) -> String { match (&self.namespace_separator, &name.namespace) { - (Some(sep), Some(ns)) => format!("{}{}{}", ns, sep, name.local_name), + (Some(sep), Some(ns)) if !ns.is_empty() => { + let mut expanded = format!("{}{}{}", ns, sep, name.local_name); + if self.namespace_prefixes.read().is(&vm.ctx.true_value) + && let Some(prefix) = &name.prefix + { + expanded.push_str(sep); + expanded.push_str(prefix); + } + expanded + } (None, _) => match &name.prefix { Some(prefix) => format!("{prefix}:{}", name.local_name), None => name.local_name.clone(), @@ -1087,17 +1380,110 @@ mod _pyexpat { /// Dispatch a single parser event to the registered handlers. Runs on /// the calling thread only -- see the module doc comment. - fn dispatch(&self, vm: &VirtualMachine, event: XmlEvent) -> PyResult<()> { + fn dispatch( + &self, + vm: &VirtualMachine, + event: XmlEvent, + doctype: Option, + ) -> PyResult<()> { match event { + XmlEvent::StartDocument { standalone, .. } => { + self.standalone + .store(standalone == Some(true), Ordering::Relaxed); + Ok(()) + } + XmlEvent::Doctype { .. } => { + if let Some(doctype) = doctype { + self.external_subset_handled.store(true, Ordering::Relaxed); + let parse_entities = self.parse_parameter_entities(); + if doctype.system_id.is_some() + && !parse_entities + && !self.standalone.load(Ordering::Relaxed) + && invoke_int_handler(vm, &self.not_standalone, "NotStandalone", ())? + == Some(0) + { + return Err(self.handler_error( + vm, + 22, + "document is not standalone", + )?); + } + let name = self.intern_name(vm, doctype.name); + let system_id = doctype.system_id.map(|s| vm.ctx.new_str(s)); + let public_id = doctype.public_id.map(|s| vm.ctx.new_str(s)); + invoke_handler( + vm, + &self.start_doctype_decl, + ( + name, + system_id.clone(), + public_id.clone(), + i32::from(doctype.has_internal_subset), + ), + )?; + if parse_entities + && (system_id.is_some() || self.use_foreign_dtd.load(Ordering::Relaxed)) + { + self.handle_external_subset(vm, system_id, public_id)?; + } + invoke_handler(vm, &self.end_doctype_decl, ())?; + } + Ok(()) + } XmlEvent::StartElement { - name, attributes, .. + name, + attributes, + namespace, } => { + if !self.external_subset_handled.swap(true, Ordering::Relaxed) + && self.parent.is_none() + && self.use_foreign_dtd.load(Ordering::Relaxed) + && self.parse_parameter_entities() + { + self.handle_external_subset(vm, None, None)?; + } + if self.namespace_separator.is_some() { + let declarations = { + let mut stack = self.namespace_stack.write(); + let previous = stack.last().map(|(namespace, _)| namespace); + let declarations: Vec<_> = namespace + .iter() + .filter(|(prefix, uri)| { + previous.map_or_else( + || { + !matches!( + (*prefix, *uri), + ("", "") + | ("xml", xml::namespace::NS_XML_URI) + | ("xmlns", xml::namespace::NS_XMLNS_URI) + ) + }, + |previous| previous.get(prefix) != Some(*uri), + ) + }) + .map(|(prefix, uri)| (prefix.to_owned(), uri.to_owned())) + .collect(); + stack.push(( + namespace, + declarations + .iter() + .map(|(prefix, _)| prefix.clone()) + .collect(), + )); + declarations + }; + for (prefix, uri) in declarations { + let prefix = (!prefix.is_empty()).then(|| vm.ctx.new_str(prefix)); + let uri = (!uri.is_empty()).then(|| vm.ctx.new_str(uri)); + invoke_handler(vm, &self.start_namespace_decl, (prefix, uri))?; + } + } let ordered = self.ordered_attributes.read().is(&vm.ctx.true_value); // Build the container. let attrs: PyObjectRef = if ordered { let mut items = Vec::with_capacity(attributes.len() * 2); for attribute in attributes { - let key = self.intern_name(vm, self.make_name(&attribute.name)); + let key = self.intern_name(vm, self.make_name(&attribute.name, vm)); items.push(key.into()); items.push(vm.ctx.new_str(attribute.value).into()); } @@ -1105,7 +1491,7 @@ mod _pyexpat { } else { let dict = vm.ctx.new_dict(); for attribute in attributes { - let key = self.intern_name(vm, self.make_name(&attribute.name)); + let key = self.intern_name(vm, self.make_name(&attribute.name, vm)); dict.set_item( AsRef::::as_ref(&key), vm.ctx.new_str(attribute.value).into(), @@ -1116,12 +1502,20 @@ mod _pyexpat { dict.into() }; - let name_str = self.intern_name(vm, self.make_name(&name)); + let name_str = self.intern_name(vm, self.make_name(&name, vm)); invoke_handler(vm, &self.start_element, (name_str, attrs)) } XmlEvent::EndElement { name, .. } => { - let name_str = self.intern_name(vm, self.make_name(&name)); - invoke_handler(vm, &self.end_element, (name_str,)) + let name_str = self.intern_name(vm, self.make_name(&name, vm)); + invoke_handler(vm, &self.end_element, (name_str,))?; + let declarations = self.namespace_stack.write().pop(); + if let Some((_, prefixes)) = declarations { + for prefix in prefixes.into_iter().rev() { + let prefix = (!prefix.is_empty()).then(|| vm.ctx.new_str(prefix)); + invoke_handler(vm, &self.end_namespace_decl, (prefix,))?; + } + } + Ok(()) } XmlEvent::Characters(chars) => { let str = PyStr::from(chars).into_ref(&vm.ctx); @@ -1156,12 +1550,144 @@ mod _pyexpat { *guard = None; } + fn parse_parameter_entities(&self) -> bool { + let mode = self.parameter_entity_parsing.load(Ordering::Relaxed); + mode == XML_PARAM_ENTITY_PARSING_ALWAYS + || (mode == XML_PARAM_ENTITY_PARSING_UNLESS_STANDALONE + && !self.standalone.load(Ordering::Relaxed)) + } + + fn handle_external_subset( + &self, + vm: &VirtualMachine, + system_id: Option, + public_id: Option, + ) -> PyResult<()> { + let base = self.base.read().as_ref().map(|s| vm.ctx.new_str(s.clone())); + let result = invoke_int_handler( + vm, + &self.external_entity_ref, + "ExternalEntityRef", + (vm.ctx.none(), base, system_id, public_id), + )?; + if result == Some(0) { + return Err(self.handler_error( + vm, + 21, + "error in processing external entity reference", + )?); + } + Ok(()) + } + + fn handler_error( + &self, + vm: &VirtualMachine, + code: i32, + description: &str, + ) -> PyResult { + let line = *self.current_line.read(); + let column = *self.current_column.read(); + let error = vm.new_exception_msg( + PyExpatError::class(&vm.ctx).to_owned(), + format!("{description}: line {line}, column {column}").into(), + ); + error + .as_object() + .set_attr("code", vm.ctx.new_int(code), vm)?; + error + .as_object() + .set_attr("lineno", vm.ctx.new_int(line), vm)?; + error + .as_object() + .set_attr("offset", vm.ctx.new_int(column), vm)?; + Ok(error) + } + + fn amplification_argument_error( + &self, + vm: &VirtualMachine, + description: &str, + ) -> PyResult { + let error = vm.new_exception_msg( + PyExpatError::class(&vm.ctx).to_owned(), + description.to_owned().into(), + ); + error.as_object().set_attr("code", vm.ctx.new_int(41), vm)?; + error + .as_object() + .set_attr("lineno", vm.ctx.new_int(*self.current_line.read()), vm)?; + error.as_object().set_attr( + "offset", + vm.ctx.new_int(*self.current_column.read()), + vm, + )?; + Ok(error) + } + + fn parse_error( + &self, + vm: &VirtualMachine, + error: xml::reader::Error, + ) -> PyResult { + let position = error.position(); + let line = position.row() as i64 + 1; + let column = position.column() as i64; + *self.current_line.write() = line; + *self.current_column.write() = column; + let (code, description) = match error.kind() { + xml::reader::ErrorKind::AmplificationLimit => { + *self.amplification_error.write() = Some(error.clone()); + ( + 43, + "limit on input amplification factor (from DTD and entities) breached", + ) + } + xml::reader::ErrorKind::UnexpectedEof => (5, "unclosed token"), + xml::reader::ErrorKind::Syntax(message) => { + if message.starts_with("Unexpected end of stream:") { + (3, "no element found") + } else if message.starts_with("Unexpected end of stream") { + (5, "unclosed token") + } else if message.starts_with("Unexpected closing tag:") { + (7, "mismatched tag") + } else if message.starts_with("Attribute ") + && message.ends_with(" is redefined") + { + (8, "duplicate attribute") + } else if message.starts_with("Undefined entity:") { + (11, "undefined entity") + } else if message.starts_with("Unclosed (4, "not well-formed (invalid token)"), + _ => (2, "syntax error"), + }; + let message = format!("{description}: line {line}, column {column}"); + let error = + vm.new_exception_msg(PyExpatError::class(&vm.ctx).to_owned(), message.into()); + error + .as_object() + .set_attr("code", vm.ctx.new_int(code), vm)?; + error + .as_object() + .set_attr("lineno", vm.ctx.new_int(line), vm)?; + error + .as_object() + .set_attr("offset", vm.ctx.new_int(column), vm)?; + Ok(error) + } + /// Drain events already produced by the parser thread (and any /// produced while draining), dispatching each to its handler, until /// the parser thread reports it is blocked waiting for more input /// (`NeedMore`), has finished (`Finished`), or hit a parse error - /// (`Error`, silently swallowed for compatibility -- see - /// `ParserMsg::Error`). If a handler raises, dispatching stops and + /// (`Error`, raised as `ExpatError`). If a handler raises, dispatching stops and /// the error propagates, but the parser thread and any events it /// already produced are kept around so a later `Parse()` call /// resumes exactly where dispatch left off, mirroring libexpat @@ -1205,15 +1731,15 @@ mod _pyexpat { self.teardown_backend(); return Ok(()); } - ParserMsg::Error => { + ParserMsg::Error(error) => { self.teardown_backend(); - return Ok(()); + return Err(self.parse_error(vm, error)?); } ParserMsg::Event(ev) => { *self.current_line.write() = ev.line; *self.current_column.write() = ev.column; *self.current_byte_index.write() = ev.byte_index; - self.dispatch(vm, ev.event)?; + self.dispatch(vm, ev.event, ev.doctype)?; } } } @@ -1290,7 +1816,8 @@ mod _pyexpat { if this_index < skip { continue; } - if let Err(e) = self.dispatch(vm, event) { + let doctype = Doctype::from_reader(&parser, &event); + if let Err(e) = self.dispatch(vm, event, doctype) { // The handler for `this_index` was invoked (it // raised, but it ran), so don't replay it next // call -- mirrors `Backend::Threaded`, where an @@ -1300,13 +1827,11 @@ mod _pyexpat { break Err(e); } } - // xml-rs is stricter than libexpat about a document - // prefix that is only well-formed once more bytes - // arrive (or never, if this chunk really is malformed); - // mirrors `ParserMsg::Error`, which is silently - // swallowed for compatibility rather than raising - // `ExpatError`. - Err(_) => break Ok(()), + // A non-final buffer can end in the middle of a token; + // defer only that incomplete-input error until more data + // arrives. Malformed complete input must still fail. + Err(error) if !isfinal && is_incomplete(&error) => break Ok(()), + Err(error) => break Err(self.parse_error(vm, error)?), } }; @@ -1348,6 +1873,32 @@ mod _pyexpat { } fn feed_inner(&self, vm: &VirtualMachine, chunk: &[u8], isfinal: bool) -> PyResult<()> { + let amplification_error = self.amplification_error.read().clone(); + if let Some(error) = amplification_error { + return Err(self.parse_error(vm, error)?); + } + if let Some(entity) = &self.external_entity { + // xml-rs only parses complete documents. In particular, an + // external DTD is not a document, and a general external + // entity can contain multiple top-level nodes and uses the + // namespace/entity context supplied by the parent. Keep + // construction and inherited configuration available, but + // never silently treat those inputs as ordinary documents. + let kind = if entity.context.is_none() { + "external DTD" + } else { + "external entity fragment" + }; + let encoding = if entity.encoding.is_some() { + " with an explicit encoding" + } else { + "" + }; + return Err(vm.new_not_implemented_error(format!( + "{kind} parsing{encoding} is not supported by the xml-rs backend" + ))); + } + self.started.store(true, Ordering::Relaxed); if self.finished.load(Ordering::SeqCst) { // The document already ended; further data is ignored, same // as libexpat happily no-oping once XML_Parse has seen EOF. @@ -1407,6 +1958,7 @@ mod _pyexpat { isfinal: OptionalArg, vm: &VirtualMachine, ) -> PyResult { + zelf.amplification.write().frozen = true; let bytes = match data { Either::A(s) => s.as_bytes().to_vec(), Either::B(b) => b.as_bytes().to_vec(), @@ -1417,6 +1969,7 @@ mod _pyexpat { #[pymethod(name = "ParseFile")] fn parse_file(zelf: &Py, file: PyObjectRef, vm: &VirtualMachine) -> PyResult { + zelf.amplification.write().frozen = true; let read_res = vm.call_method(&file, "read", ())?; let bytes_like = ArgBytesLike::try_from_object(vm, read_res)?; let buf = bytes_like.borrow_buf().to_vec(); @@ -1458,7 +2011,13 @@ mod _pyexpat { // encoding parameter is currently not used (xml-rs handles encoding from XML declaration) let _ = args.encoding; - Ok(PyExpatLikeXmlParser::new(ns_sep, args.intern, vm)) + Ok(PyExpatLikeXmlParser::new( + ns_sep, + args.intern, + None, + None, + vm, + )) } // TODO: Tie this exception to the module's state. diff --git a/crates/stdlib/src/syslog.rs b/crates/stdlib/src/syslog.rs index 5966dced299..cb00ee08030 100644 --- a/crates/stdlib/src/syslog.rs +++ b/crates/stdlib/src/syslog.rs @@ -76,6 +76,9 @@ mod syslog { #[pyfunction] fn openlog(args: OpenLogArgs, vm: &VirtualMachine) -> PyResult<()> { + if !vm.state.is_main_interpreter() { + return Err(vm.new_runtime_error("subinterpreter can't use syslog.openlog()")); + } let logoption = args.logoption; let facility = args.facility; let ident = match args.ident.clone() { @@ -116,6 +119,11 @@ mod syslog { vm.audit("syslog.syslog", || (priority, msg.clone()))?; if !host_syslog::is_open() { + if !vm.state.is_main_interpreter() { + return Err(vm.new_runtime_error( + "subinterpreter can't use syslog.syslog() until the syslog is opened by the main interpreter", + )); + } openlog( OpenLogArgs { facility: LOG_USER, @@ -132,6 +140,9 @@ mod syslog { #[pyfunction] fn closelog(vm: &VirtualMachine) -> PyResult<()> { + if !vm.state.is_main_interpreter() { + return Err(vm.new_runtime_error("subinterpreter can't use syslog.closelog()")); + } vm.audit("syslog.closelog", || ())?; host_syslog::closelog(); diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index 862929c1ae5..d1c4f9c43f8 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -10,6 +10,7 @@ repository.workspace = true license.workspace = true [features] +capi = [] default = ["compiler", "wasmbind", "gc", "host_env", "stdio", "doc"] doc = ["dep:rustpython-doc", "dep:xz"] host_env = ["rustpython-host_env/ctypes"] diff --git a/crates/vm/src/builtins/builtin_func.rs b/crates/vm/src/builtins/builtin_func.rs index 917b7b24fcc..5f0991fef2f 100644 --- a/crates/vm/src/builtins/builtin_func.rs +++ b/crates/vm/src/builtins/builtin_func.rs @@ -195,13 +195,9 @@ impl PyNativeFunction { // meth_get__self__ in CPython #[pygetset] fn __self__(zelf: NativeFunctionOrMethod, vm: &VirtualMachine) -> PyObjectRef { - if let Some(bound) = &zelf.0.zelf { - return bound.clone(); - } - if let Some(module) = &zelf.0.module_object { - return module.clone(); - } - vm.ctx.none() + zelf.0 + .get_self() + .map_or_else(|| vm.ctx.none(), ToOwned::to_owned) } // meth_reduce: the name when unbound or bound to a module, otherwise diff --git a/crates/vm/src/builtins/descriptor.rs b/crates/vm/src/builtins/descriptor.rs index 4c7c1804d4c..047a3aa3cef 100644 --- a/crates/vm/src/builtins/descriptor.rs +++ b/crates/vm/src/builtins/descriptor.rs @@ -1638,7 +1638,7 @@ fn vectorcall_wrapper( ))); } let obj = args.remove(0); - if !obj.fast_isinstance(zelf.typ) { + if !obj.fast_isinstance(&zelf.typ) { return Err(vm.new_type_error(format!( "descriptor '{}' requires a '{}' object but received a '{}'", zelf.name.as_str(), @@ -1993,18 +1993,23 @@ fn parse_buffer_flags( // wrapper_descriptor: wraps a slot function as a Python method // = PyWrapperDescrObject -#[pyclass(name = "wrapper_descriptor", module = false)] +#[pyclass(name = "wrapper_descriptor", module = false, traverse)] #[derive(Debug)] pub(crate) struct PyWrapper { #[pymember(name = "__objclass__")] - pub typ: &'static Py, + pub typ: PyTypeRef, #[pymember(name = "__name__")] + #[pytraverse(skip)] pub name: &'static PyStrInterned, + #[pytraverse(skip)] pub wrapped: SlotFunc, /// Slot text, including the text signature. + #[pytraverse(skip)] pub doc: Option<&'static str>, /// Plain docstring for this slot when the table has one. + #[pytraverse(skip)] pub plain_off: u32, + #[pytraverse(skip)] pub plain_len: u32, } @@ -2042,7 +2047,7 @@ impl Callable for PyWrapper { // list.__init__(l, [1,2,3]) form - first arg is self let (obj, rest): (PyObjectRef, FuncArgs) = args.bind(vm)?; - if !obj.fast_isinstance(zelf.typ) { + if !obj.fast_isinstance(&zelf.typ) { return Err(vm.new_type_error(format!( "descriptor '{}' requires a '{}' object but received a '{}'", zelf.name.as_str(), @@ -2057,7 +2062,7 @@ impl Callable for PyWrapper { #[pyclass( with(GetDescriptor, Callable, Representable), - flags(DISALLOW_INSTANTIATION) + flags(DISALLOW_INSTANTIATION, METHOD_DESCRIPTOR) )] impl Py { #[pymethod] @@ -2128,7 +2133,7 @@ impl Callable for PyMethodWrapper { fn call(zelf: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { // bpo-37619: Check type compatibility before calling wrapped slot - if !zelf.obj.fast_isinstance(zelf.wrapper.typ) { + if !zelf.obj.fast_isinstance(&zelf.wrapper.typ) { return Err(vm.new_type_error(format!( "descriptor '{}' requires a '{}' object but received a '{}'", zelf.wrapper.name.as_str(), diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index 5f59675b9dd..ee822bd0e89 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -690,7 +690,11 @@ impl Py { } /// Create generator, coroutine, or async generator from a FrameObject. - fn make_generator_or_coro(&self, frame: FrameObjectRef, vm: &VirtualMachine) -> PyObjectRef { + pub(crate) fn make_generator_or_coro( + &self, + frame: FrameObjectRef, + vm: &VirtualMachine, + ) -> PyObjectRef { let code = frame.iframe().code(); let is_async_gen = code.flags.contains(bytecode::CodeFlags::ASYNC_GENERATOR); let is_gen = code.flags.contains(bytecode::CodeFlags::GENERATOR); diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index d7dca044bbe..a45e9bf4cb0 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -3239,6 +3239,9 @@ impl SetAttr for PyType { } } Ok(prev_value) + }) + .inspect_err(|exc| { + vm.set_attribute_error_context(exc, zelf.to_owned().into(), attr_name.to_owned()); })?; Ok(()) } diff --git a/crates/vm/src/class.rs b/crates/vm/src/class.rs index fe655968665..7b7c854a4bd 100644 --- a/crates/vm/src/class.rs +++ b/crates/vm/src/class.rs @@ -16,7 +16,7 @@ use rustpython_common::static_cell; /// 1. Has a function set in the type's slots /// 2. Doesn't already have an attribute in the type's dict #[cfg(feature = "doc")] -pub fn add_operators(class: &'static Py, ctx: &Context, attr_docs: &[(&str, u32, u32)]) { +pub fn add_operators(class: &Py, ctx: &Context, attr_docs: &[(&str, u32, u32)]) { add_operators_inner(class, ctx, |name| match attr_doc(attr_docs, name) { Some((offset, len)) if len != 0 => (offset, len), _ => (0, 0), @@ -24,7 +24,7 @@ pub fn add_operators(class: &'static Py, ctx: &Context, attr_docs: &[(&s } #[cfg(not(feature = "doc"))] -pub fn add_operators(class: &'static Py, ctx: &Context, attr_docs: &[&str]) { +pub fn add_operators(class: &Py, ctx: &Context, attr_docs: &[&str]) { add_operators_inner(class, ctx, |name| { if attr_name_present(attr_docs, name) { // Present in the database: do not fall back to the slot text. @@ -36,7 +36,7 @@ pub fn add_operators(class: &'static Py, ctx: &Context, attr_docs: &[&st } fn add_operators_inner( - class: &'static Py, + class: &Py, ctx: &Context, mut plain_span: impl FnMut(&str) -> (u32, u32), ) { @@ -77,7 +77,7 @@ fn add_operators_inner( // Create and add the wrapper let wrapper = PyWrapper { - typ: class, + typ: class.to_owned(), name: attr_name, wrapped: slot_func, doc: Some(def.doc), diff --git a/crates/vm/src/coroutine.rs b/crates/vm/src/coroutine.rs index b1ea72b131b..637e5b97728 100644 --- a/crates/vm/src/coroutine.rs +++ b/crates/vm/src/coroutine.rs @@ -676,12 +676,16 @@ pub(crate) fn get_awaitable_iter(obj: PyObjectRef, vm: &VirtualMachine) -> PyRes .contains(crate::bytecode::CodeFlags::ITERABLE_COROUTINE) }) { - return Err(vm.new_type_error("__await__() returned a coroutine")); + return Err(vm.new_type_error(format!( + "{}.__await__() must return an iterator, not coroutine", + obj.class().fully_qualified_name(vm)? + ))); } if !PyIter::check(&result) { return Err(vm.new_type_error(format!( - "__await__() returned non-iterator of type '{}'", - result.class().name() + "{}.__await__() must return an iterator, not {}", + obj.class().fully_qualified_name(vm)?, + result.class().fully_qualified_name(vm)? ))); } return Ok(result); diff --git a/crates/vm/src/exceptions.rs b/crates/vm/src/exceptions.rs index b6f0cad9303..bbdb17a9bed 100644 --- a/crates/vm/src/exceptions.rs +++ b/crates/vm/src/exceptions.rs @@ -1824,7 +1824,7 @@ pub(super) mod types { }, convert::ToPyResult, function::{ArgBytesLike, FuncArgs, KwArgs, PySetterValue}, - types::{Constructor, Initializer}, + types::{Constructor, Initializer, Representable}, }; use core::sync::atomic::{AtomicBool, AtomicIsize, Ordering}; use crossbeam_utils::atomic::AtomicCell; @@ -2213,7 +2213,35 @@ pub(super) mod types { } } - #[pyexception(with(Constructor, Initializer))] + impl Representable for PyImportError { + fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let base: &Py = zelf + .as_object() + .downcast_ref() + .expect("ImportError is a BaseException"); + let mut has_args = !base.args().is_empty(); + if zelf.name.load_owned().is_none() && zelf.path.load_owned().is_none() { + return PyBaseException::repr_str(base, vm); + } + let mut result = PyBaseException::repr_str(base, vm)?; + result.pop(); + for (name, field) in [("name", &zelf.name), ("path", &zelf.path)] { + if let Some(value) = field.load_owned() { + if has_args { + result.push_str(", "); + } + result.push_str(name); + result.push('='); + result.push_str(&value.repr(vm)?.to_string_lossy()); + has_args = true; + } + } + result.push(')'); + Ok(result) + } + } + + #[pyexception(with(Constructor, Initializer, Representable))] impl PyImportError { #[pyslot] fn slot_str(zelf: &PyObject, vm: &VirtualMachine) -> PyResult { diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index 20759fb5f8b..c3668b2a548 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -50,7 +50,7 @@ use num_traits::{ToPrimitive, Zero}; use rustpython_common::atomic::{PyAtomic, Radium}; use rustpython_common::{ lock::{OnceCell, PyMutex}, - wtf8::{Wtf8, Wtf8Buf, wtf8_concat}, + wtf8::{Wtf8Buf, wtf8_concat}, }; use rustpython_compiler_core::{OneIndexed, SourceLocation}; @@ -4880,65 +4880,71 @@ impl ExecutingFrame<'_> { let dict: &Py = unsafe { dict_ref.downcast_unchecked_ref() }; - // Get callable for error messages - // Stack: [callable, self_or_null, args_tuple, kwargs_dict] - let callable = self.nth_value(idx + 2); - let func_str = Self::object_function_str(callable, vm); - - // Fast path: source is an exact dict (not a subclass, which may - // override `keys`/`__getitem__`). Iterate its entries natively - // instead of going through the mapping protocol, mirroring - // CPython's `PyDict_Merge` fast path for `PyDict_Check(other)`. - let source = if source.class().is(vm.ctx.types.dict_type) { - let src_dict = source - .downcast_ref::() - .expect("exact dict must have a PyDict payload"); - // Preserve stored hashes: copying an exact dict must not call - // a key's __hash__ before the callable receives its arguments. - // Snapshot under one lock before any equality callbacks run. - for (key, value, hash) in src_dict.items_with_hashes_vec() { - if dict.contains_known_hash(&key, hash, vm)? { - let key_str = key.str(vm)?; - return Err(vm.new_type_error(format!( - "{} got multiple values for keyword argument '{}'", - func_str, - key_str.as_wtf8() - ))); + let merge_result = (|| -> PyResult> { + // Fast path: source is an exact dict (not a subclass, which may + // override `keys`/`__getitem__`). Iterate its entries natively + // instead of going through the mapping protocol, mirroring + // CPython's `PyDict_Merge` fast path for `PyDict_Check(other)`. + if source.class().is(vm.ctx.types.dict_type) { + let src_dict = source + .downcast_ref::() + .expect("exact dict must have a PyDict payload"); + // Preserve stored hashes: copying an exact dict must not call + // a key's __hash__ before the callable receives its arguments. + // Snapshot under one lock before any equality callbacks run. + for (key, value, hash) in src_dict.items_with_hashes_vec() { + if dict.contains_known_hash(&key, hash, vm)? { + return Ok(Some(key)); + } + dict.set_item_known_hash(&key, hash, value, vm)?; } - dict.set_item_known_hash(&key, hash, value, vm)?; + return Ok(None); } - return Ok(None); - } else { - source - }; - // Check if source is a mapping - if vm - .get_method(source.clone(), vm.ctx.intern_str("keys")) - .is_none() - { - return Err(vm.new_type_error(format!( - "{} argument after ** must be a mapping, not {}", - func_str, - source.class().name() - ))); - } + // Merge keys, checking for duplicates. + let keys = source.mapping_unchecked().keys(vm)?; + let keys_iter = PyIter::try_from_object(vm, keys)?; + while let PyIterReturn::Return(key) = keys_iter.next(vm)? { + let hash = PyDict::hash_or_unhashable(&*key, vm)?; + if dict.entries.contains(vm, &*key, hash)? { + return Ok(Some(key)); + } + let value = source.get_item(&*key, vm)?; + dict.set_item(&*key, value, vm)?; + } + Ok(None) + })(); - // Merge keys, checking for duplicates - let keys_iter = vm.call_method(&source, "keys", ())?; - for key in keys_iter.try_to_value::>(vm)? { - if dict.contains_key(&*key, vm) { + match merge_result { + Ok(Some(key)) => { + // Stack: [callable, self_or_null, args_tuple, kwargs_dict] + let callable = self.nth_value(idx + 2); + let func_str = Self::object_function_str(callable, vm); let key_str = key.str(vm)?; - return Err(vm.new_type_error(format!( + Err(vm.new_type_error(format!( "{} got multiple values for keyword argument '{}'", func_str, key_str.as_wtf8() - ))); + ))) + } + Ok(None) => Ok(None), + Err(error) if error.fast_isinstance(vm.ctx.exceptions.attribute_error) => { + // Only missing keys make this a non-mapping. Preserve callback + // errors, chaining a failed recheck as _PyEval_FormatKwargsError does. + match vm.get_attribute_opt(&source, identifier!(vm, keys)) { + Ok(None) => Err(vm.new_type_error(format!( + "Value after ** must be a mapping, not {}", + source.class().fully_qualified_name(vm)? + ))), + Ok(Some(_)) => Err(error), + Err(lookup_error) => { + lookup_error.set_context(Some(error)); + Err(lookup_error) + } + } } - let value = vm.call_method(&source, "__getitem__", (key.clone(),))?; - dict.set_item(&*key, value, vm)?; + Err(error) => Err(error), } - Ok(None) } Instruction::EndAsyncFor => { // Pops (awaitable, exc) from stack. @@ -9019,9 +9025,7 @@ impl ExecutingFrame<'_> { if let Some(kw_obj) = kwargs_or_null { // Stack: [callable, self_or_null, args_tuple] let callable = self.nth_value(2); - let func_str = Self::object_function_str(callable, vm); - - Self::iterate_mapping_keys(vm, &kw_obj, &func_str, |key, value| { + Self::iterate_mapping_keys(vm, &kw_obj, callable, |key, value| { // `PyStr`, not `PyUtf8Str`: CPython only checks that the key is a // `str`, not that it is valid UTF-8, so surrogate keys are accepted. key.downcast_ref::() @@ -9038,9 +9042,6 @@ impl ExecutingFrame<'_> { tuple.as_slice().to_vec() } else { // Single *arg passed directly; convert to sequence at runtime. - // Stack: [callable, self_or_null] - let callable = self.nth_value(1); - let func_str = Self::object_function_str(callable, vm); let not_iterable = args_obj.class().slots().iter.load().is_none() && args_obj .get_class_attr(vm.ctx.intern_str("__getitem__")) @@ -9048,8 +9049,7 @@ impl ExecutingFrame<'_> { args_obj.try_to_value::>(vm).map_err(|e| { if not_iterable && e.class().is(vm.ctx.exceptions.type_error) { vm.new_type_error(format!( - "{} argument after * must be an iterable, not {}", - func_str, + "Value after * must be an iterable, not {}", args_obj.class().slot_name() )) } else { @@ -9091,7 +9091,7 @@ impl ExecutingFrame<'_> { fn iterate_mapping_keys( vm: &VirtualMachine, mapping: &PyObject, - func_str: &Wtf8, + callable: &PyObject, mut key_handler: F, ) -> PyResult<()> where @@ -9115,12 +9115,17 @@ impl ExecutingFrame<'_> { let Some(keys_method) = vm.get_method(mapping.to_owned(), vm.ctx.intern_str("keys")) else { return Err(vm.new_type_error(format!( "{} argument after ** must be a mapping, not {}", - func_str, + Self::object_function_str(callable, vm), mapping.class().name() ))); }; - let keys = PyIter::try_from_object(vm, keys_method?.call((), vm)?)?; + let keys = mapping.mapping_unchecked().collect_method_output( + identifier!(vm, keys), + keys_method?.call((), vm)?, + vm, + )?; + let keys = PyIter::try_from_object(vm, keys)?; while let PyIterReturn::Return(key) = keys.next(vm)? { let value = mapping.get_item(&*key, vm)?; key_handler(key, value)?; diff --git a/crates/vm/src/getpath.rs b/crates/vm/src/getpath.rs index 68e95583a13..6bccb30261b 100644 --- a/crates/vm/src/getpath.rs +++ b/crates/vm/src/getpath.rs @@ -203,7 +203,6 @@ pub fn init_path_config(settings: &Settings) -> Paths { /// Identify an uninstalled RustPython source library independently of the /// executable location (Cargo permits targets outside the source tree). -#[cfg(windows)] pub(crate) fn source_directory(paths: &Paths) -> Option<&Path> { let library = Path::new(paths.stdlib_dir.as_deref()?); let source = library.parent()?; diff --git a/crates/vm/src/import.rs b/crates/vm/src/import.rs index a6d137d7d2c..13c5aa5fe95 100644 --- a/crates/vm/src/import.rs +++ b/crates/vm/src/import.rs @@ -8,11 +8,105 @@ use crate::{ vm::{VirtualMachine, resolve_frozen_alias, thread}, }; +use core::{cell::Cell, sync::atomic::Ordering, time::Duration}; +use std::{io::Write, time::Instant}; + +/// Nested imports are timed per thread, so concurrent imports do not subtract +/// each other's elapsed time. The output header belongs to the interpreter. +#[derive(Clone, Copy, Default)] +pub(crate) struct ImportTimingState { + depth: usize, + children: Duration, +} + +fn import_time_header(vm: &VirtualMachine) { + let mut stderr = std::io::stderr().lock(); + if !vm.state.import_time_header.swap(true, Ordering::Relaxed) { + let _ = writeln!( + stderr, + "import time: self [us] | cumulative | imported package" + ); + } +} + +fn import_time_name(name: &Py) -> String { + use core::fmt::Write; + let mut output = String::with_capacity(name.as_bytes().len()); + for codepoint in name.as_wtf8().code_points() { + match codepoint.to_char() { + Some(ch) => output.push(ch), + None => { + let _ = write!(output, "\\u{:04x}", codepoint.to_u32()); + } + } + } + output +} + +pub(crate) fn report_cached_import(name: &Py, vm: &VirtualMachine) { + if vm.state.config.settings.import_time == 2 { + import_time_header(vm); + let width = vm.import_timing.get().depth * 2; + let _ = writeln!( + std::io::stderr(), + "import time: cached | cached | {:>width$}", + import_time_name(name) + ); + } +} + +struct ImportTimer<'a> { + state: &'a Cell, + parent: ImportTimingState, + start: Instant, + name: &'a Py, +} + +impl<'a> ImportTimer<'a> { + fn start(name: &'a Py, vm: &'a VirtualMachine) -> Option { + if vm.state.config.settings.import_time == 0 { + return None; + } + import_time_header(vm); + let parent = vm.import_timing.replace(ImportTimingState { + depth: vm.import_timing.get().depth + 1, + children: Duration::ZERO, + }); + Some(Self { + state: &vm.import_timing, + parent, + start: Instant::now(), + name, + }) + } +} + +impl Drop for ImportTimer<'_> { + fn drop(&mut self) { + let cumulative = self.start.elapsed(); + let own = cumulative.saturating_sub(self.state.get().children); + self.state.set(ImportTimingState { + depth: self.parent.depth, + children: self.parent.children.saturating_add(cumulative), + }); + let width = self.parent.depth * 2; + let _ = writeln!( + std::io::stderr(), + "import time: {:9} | {:10} | {:width$}{}", + own.as_nanos().div_ceil(1000), + cumulative.as_nanos().div_ceil(1000), + "", + import_time_name(self.name), + ); + } +} + pub(crate) fn find_and_load( name: &Py, function: &'static str, vm: &VirtualMachine, ) -> PyResult { + let _timer = ImportTimer::start(name, vm); vm.importlib .get_attr(function, vm)? .call((name.to_owned(), vm.import_func.clone()), vm) @@ -39,6 +133,8 @@ pub(crate) fn init_importlib_base(vm: &mut VirtualMachine) -> PyResult find_and_load(&abs_name, "_find_and_load", vm)?, + Ok(_) => find_and_load(&abs_name, "_find_and_load", vm)?, + Err(error) if error.fast_isinstance(vm.ctx.exceptions.key_error) => { + find_and_load(&abs_name, "_find_and_load", vm)? + } + Err(error) => return Err(error), }; // Handle fromlist diff --git a/crates/vm/src/protocol/mapping.rs b/crates/vm/src/protocol/mapping.rs index 997df878ff0..c91a53d3812 100644 --- a/crates/vm/src/protocol/mapping.rs +++ b/crates/vm/src/protocol/mapping.rs @@ -212,21 +212,35 @@ impl PyMapping<'_> { vm: &VirtualMachine, ) -> PyResult { let meth_output = vm.call_method(self.obj, method_name.as_str(), ())?; - if meth_output.is(vm.ctx.types.list_type) { + self.collect_method_output(method_name, meth_output, vm) + } + + pub(crate) fn collect_method_output( + self, + method_name: &'static PyStrInterned, + meth_output: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + if meth_output.class().is(vm.ctx.types.list_type) { return Ok(meth_output); } - let iter = meth_output.get_iter(vm).map_err(|_| { - vm.new_type_error(format!( - "{}.{}() returned a non-iterable (type {})", - self.obj.class().slot_name(), - method_name.as_str(), - meth_output.class().slot_name() - )) - })?; - - // TODO - // PySequence::from(&iter).list(vm).map(|x| x.into()) - vm.ctx.new_list(iter.try_to_value(vm)?).to_pyresult(vm) + let iter = match meth_output.get_iter(vm) { + Ok(iter) => iter, + Err(error) if error.fast_isinstance(vm.ctx.exceptions.type_error) => { + return Err(vm.new_type_error(format!( + "{}.{}() must return an iterable, not {}", + self.obj.class().fully_qualified_name(vm)?, + method_name.as_str(), + meth_output.class().fully_qualified_name(vm)? + ))); + } + Err(error) => return Err(error), + }; + + drop(meth_output); + let result = vm.ctx.new_list(Vec::new()); + result.extend(iter.into(), vm)?; + Ok(result.into()) } } diff --git a/crates/vm/src/protocol/number.rs b/crates/vm/src/protocol/number.rs index db14a08d4eb..0708e830039 100644 --- a/crates/vm/src/protocol/number.rs +++ b/crates/vm/src/protocol/number.rs @@ -703,21 +703,22 @@ and may be removed in a future version of Python.", } let ret_class = ret.class().to_owned(); + let message = format!( + "{}.__index__() must return an int, not {}", + self.class().fully_qualified_name(vm)?, + ret_class.fully_qualified_name(vm)? + ); if let Some(ret) = ret.downcast_ref::() { let msg = format!( - "__index__ returned non-int (type {}). \ + "{message}. \ The ability to return an instance of a strict subclass of int is deprecated, \ -and may be removed in a future version of Python.", - ret_class.slot_name() +and may be removed in a future version of Python." ); _warnings::warn(vm.ctx.exceptions.deprecation_warning, msg, 1, vm)?; Ok(ret.to_owned()) } else { - Err(vm.new_type_error(format!( - "__index__ returned non-int (type {})", - ret_class.slot_name() - ))) + Err(vm.new_type_error(message)) } }) } @@ -732,23 +733,22 @@ and may be removed in a future version of Python.", } let ret_class = ret.class().to_owned(); + let message = format!( + "{}.__float__() must return a float, not {}", + self.class().fully_qualified_name(vm)?, + ret_class.fully_qualified_name(vm)? + ); if let Some(ret) = ret.downcast_ref::() { let msg = format!( - "{}.__float__ returned non-float (type {}). \ + "{message}. \ The ability to return an instance of a strict subclass of float is deprecated, \ -and may be removed in a future version of Python.", - self.class().slot_name(), - ret_class.slot_name() +and may be removed in a future version of Python." ); _warnings::warn(vm.ctx.exceptions.deprecation_warning, msg, 1, vm)?; Ok(ret.to_owned()) } else { - Err(vm.new_type_error(format!( - "{}.__float__ returned non-float (type {})", - self.class().slot_name(), - ret_class.slot_name() - ))) + Err(vm.new_type_error(message)) } }) } diff --git a/crates/vm/src/signal.rs b/crates/vm/src/signal.rs index 4ea023505a8..c558bd24637 100644 --- a/crates/vm/src/signal.rs +++ b/crates/vm/src/signal.rs @@ -134,39 +134,49 @@ fn trigger_signals(vm: &VirtualMachine) -> PyResult<()> { } let _guard = SignalHandlerGuard; - #[cfg(all(feature = "host_env", windows))] if vm.is_main_thread() && vm.state.is_main_interpreter() { - // Report before a Python signal handler can raise out of the current - // error-reporting scope. - crate::stdlib::_signal::_signal::report_wakeup_send_errors(vm); - } else if crate::stdlib::_signal::_signal::wakeup_send_errors_pending() { - // A different interpreter/thread must leave the report for the main one. - set_triggered(); - } - - let signal_handlers = vm - .signal_handlers - .get() - .expect("should never fail since we check above"); - - for (signum, trigger) in TRIGGERS.iter().enumerate().skip(1) { - let triggered = trigger.swap(false, Ordering::Relaxed); - if !triggered { - continue; - } + // Report before invoking a Python handler, which may raise and leave + // the current stderr redirection scope before the next signal check. + #[cfg(all(feature = "host_env", any(unix, windows)))] + crate::stdlib::_signal::_signal::report_wakeup_errors(vm); + + let signal_handlers = vm + .signal_handlers + .get() + .expect("should never fail since we check above"); + + for (signum, trigger) in TRIGGERS.iter().enumerate().skip(1) { + let triggered = trigger.swap(false, Ordering::Relaxed); + if !triggered { + continue; + } - // SAFETY: TRIGGERS has the same length as the signal_handlers - let signum = unsafe { SignalNum::new_unchecked(signum as i32) }; + // SAFETY: TRIGGERS has the same length as the signal_handlers + let signum = unsafe { SignalNum::new_unchecked(signum as i32) }; - // Read the handler out and drop the borrow before running it. A - // handler is free to call signal.signal(), which takes the same cell - // mutably, and a live read borrow turns that into a panic. - let handler = signal_handlers.borrow()[signum].clone(); + // Read the handler out and drop the borrow before running it. A + // handler is free to call signal.signal(), which takes the same cell + // mutably, and a live read borrow turns that into a panic. + let handler = signal_handlers.borrow()[signum].clone(); - if let Some(handler) = handler - && let Some(callable) = handler.to_callable() - { - callable.invoke((signum.as_i32(), vm.ctx.none()), vm)?; + if let Some(handler) = handler + && let Some(callable) = handler.to_callable() + { + callable.invoke((signum.as_i32(), vm.ctx.none()), vm)?; + } + } + } else { + let os_signals_pending = TRIGGERS + .iter() + .skip(1) + .any(|trigger| trigger.load(Ordering::Relaxed)); + #[cfg(all(feature = "host_env", any(unix, windows)))] + let os_signals_pending = + os_signals_pending || crate::stdlib::_signal::_signal::wakeup_errors_pending(); + if os_signals_pending { + // Leave OS signals for the main interpreter's main thread. Re-arm + // before running VM-local callbacks, which may raise. + set_triggered(); } } diff --git a/crates/vm/src/stdlib/_ast/expression.rs b/crates/vm/src/stdlib/_ast/expression.rs index 3aa2e6fdaef..a5625379f45 100644 --- a/crates/vm/src/stdlib/_ast/expression.rs +++ b/crates/vm/src/stdlib/_ast/expression.rs @@ -858,16 +858,19 @@ fn expr_dict_comp_from_object_with_range( object: &PyObject, range: AstNodeRange, ) -> PyResult { + // Ruff uses a missing key for unpacking; Python's AST uses a missing value. + let key = get_required_node_field(vm, source_file, object, "key", "DictComp")?; + let value = get_node_field_opt(vm, object, "value")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?; + let (key, value) = match value { + Some(value) => (Some(key), value), + None => (None, key), + }; Ok(ast::ExprDictComp { node_index: range.node_index(), - key: Some(get_required_node_field( - vm, - source_file, - object, - "key", - "DictComp", - )?), - value: get_required_node_field(vm, source_file, object, "value", "DictComp")?, + key, + value, generators: get_node_list_field(vm, source_file, object, "generators", "DictComp")? .into_boxed_slice(), range: range.range, @@ -883,6 +886,10 @@ impl Node for ast::ExprDictComp { generators, range, } = self; + let (key, value) = match key { + Some(key) => (key, Some(value)), + None => (value, None), + }; let node = NodeAst .into_ref_with_type(vm, pyast::NodeExprDictComp::static_type().to_owned()) .unwrap(); diff --git a/crates/vm/src/stdlib/_ast/pyast.rs b/crates/vm/src/stdlib/_ast/pyast.rs index 20e0940fb0e..4632d6e0381 100644 --- a/crates/vm/src/stdlib/_ast/pyast.rs +++ b/crates/vm/src/stdlib/_ast/pyast.rs @@ -72,17 +72,13 @@ macro_rules! impl_base_node { super::python::_ast::ast_reduce(&zelf, vm) } - #[pymethod] - fn __replace__( - zelf: PyObjectRef, - fields: crate::function::KwArgs, - vm: &VirtualMachine, - ) -> PyResult { - super::python::_ast::ast_replace(&zelf, fields.into(), vm) - } - #[extend_class] fn extend_class(ctx: &Context, class: &'static Py) { + class.set_str_attr( + "__replace__", + super::python::_ast::AST_REPLACE.to_proper_method(class, ctx), + ctx, + ); // AST types are mutable (heap types, not IMMUTABLETYPE). class.slots.flags.remove(crate::types::PyTypeFlags::IMMUTABLETYPE); class.set_attr( @@ -116,17 +112,13 @@ macro_rules! impl_base_node { super::python::_ast::ast_reduce(&zelf, vm) } - #[pymethod] - fn __replace__( - zelf: PyObjectRef, - fields: crate::function::KwArgs, - vm: &VirtualMachine, - ) -> PyResult { - super::python::_ast::ast_replace(&zelf, fields.into(), vm) - } - #[extend_class] fn extend_class_with_fields(ctx: &Context, class: &'static Py) { + class.set_str_attr( + "__replace__", + super::python::_ast::AST_REPLACE.to_proper_method(class, ctx), + ctx, + ); // AST types are mutable (heap types, not IMMUTABLETYPE). class.slots.flags.remove(crate::types::PyTypeFlags::IMMUTABLETYPE); class.set_attr( @@ -1299,7 +1291,7 @@ const FIELD_TYPES: &[(&str, &[(&str, FieldType)])] = &[ "DictComp", &[ ("key", FieldType::Node("expr")), - ("value", FieldType::Node("expr")), + ("value", FieldType::Optional("expr")), ("generators", FieldType::ListOf("comprehension")), ], ), diff --git a/crates/vm/src/stdlib/_ast/python.rs b/crates/vm/src/stdlib/_ast/python.rs index 7e146a5d479..ce8710f82d7 100644 --- a/crates/vm/src/stdlib/_ast/python.rs +++ b/crates/vm/src/stdlib/_ast/python.rs @@ -15,6 +15,8 @@ pub(crate) mod _ast { types::{Constructor, Initializer}, warn, }; + use rustpython_common::wtf8::Wtf8Buf; + #[pyattr] #[pyclass(module = "ast", name = "AST")] #[derive(Debug, PyPayload)] @@ -167,6 +169,17 @@ pub(crate) mod _ast { obj.set_attr(&name, value, vm) } + pub(crate) const AST_REPLACE: PyMethodDef = PyMethodDef::new_const( + "__replace__", + |zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine| -> PyResult { + ast_replace(&zelf, args, vm) + }, + PyMethodFlags::METHOD, + crate::function::ItemDoc::static_text( + "__replace__($self, /, **fields)\n--\n\nReturn a copy of the AST node with new values for the specified fields.", + ), + ); + pub(crate) fn ast_replace(zelf: &PyObject, args: FuncArgs, vm: &VirtualMachine) -> PyResult { if !args.args.is_empty() { return Err(vm.new_type_error("__replace__() takes no positional arguments")); @@ -293,7 +306,7 @@ pub(crate) mod _ast { impl Constructor for NodeAst { type Args = FuncArgs; - fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn slot_new(cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult { // Keep _instance for parser-internal shared operator/context nodes, // but match CPython's public constructor behavior by allocating a // fresh object for Python-level ast.Load()/ast.Add()/... calls. @@ -309,13 +322,7 @@ pub(crate) mod _ast { } else { None }; - let zelf = vm.ctx.new_base_object(cls, dict); - - // type.__call__ does not invoke slot_init after slot_new - // for types with a custom slot_new, so we must call it here. - Self::slot_init(&zelf, args, vm)?; - - Ok(zelf) + Ok(vm.ctx.new_base_object(cls, dict)) } fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { @@ -417,68 +424,78 @@ This will become an error in Python 3.20", zelf.set_attr(vm.ctx.intern_str(key), value, vm)?; } - // Use _field_types to determine defaults for unset fields. - // Only built-in AST node classes have _field_types populated. - let field_types = zelf.class().get_attr(vm.ctx.intern_str("_field_types")); - if let Some(Ok(ft_dict)) = - field_types.map(|ft| ft.downcast::()) + // Custom AST subclasses without _field_types retain their permissive + // treatment of fields omitted from the constructor. + if !remaining_fields.elements().is_empty() + && let Some(field_types) = zelf.class().get_attr(vm.ctx.intern_str("_field_types")) { + let ft_dict = field_types + .downcast::() + .map_err(|_| vm.new_system_error("bad argument to internal function"))?; let expr_ctx_type: PyObjectRef = super::super::pyast::NodeExprContext::make_static_type().into(); + let missing_names = PySet::default().into_ref(&vm.ctx); - let mut missing = Vec::new(); - // Render missing arguments in their declared positional order. - for i in 0..numfields { - let field = fields_seq.get_item(i as isize, vm)?; - if !ast_replace_set_discard(&remaining_fields, &field, vm)? { - continue; - } - if let Some(ftype) = ft_dict.get_item_opt(&*field, vm)? { - if ftype.fast_isinstance(vm.ctx.types.union_type) { - // Optional field (T | None) — no default - } else if ftype.fast_isinstance(vm.ctx.types.generic_alias_type) { - // List field (list[T]) — default to [] - let empty_list: PyObjectRef = vm.ctx.new_list(vec![]).into(); - ast_set_attr(zelf, &field, empty_list, vm)?; - } else if ftype.is(&expr_ctx_type) { - // expr_context — default to Load() - let load_type = - super::super::pyast::NodeExprContextLoad::make_static_type(); - let load_instance = load_type - .get_attr(vm.ctx.intern_str("_instance")) - .unwrap_or_else(|| { - vm.ctx.new_base_object(load_type, Some(vm.ctx.new_dict())) - }); - ast_set_attr(zelf, &field, load_instance, vm)?; - } else { - missing.push(format!("'{}'", field.str(vm)?)); - } - } else { + for field in remaining_fields.elements() { + let Some(ftype) = ft_dict.get_item_opt(&*field, vm)? else { return Err(vm.new_type_error(format!( "Field {} is missing from {}._field_types", field.repr(vm)?, zelf.class().fully_qualified_name(vm)? ))); + }; + if ftype.fast_isinstance(vm.ctx.types.union_type) { + // Optional fields already have a None default on the class. + } else if ftype.class().is(vm.ctx.types.generic_alias_type) { + let empty_list: PyObjectRef = vm.ctx.new_list(vec![]).into(); + ast_set_attr(zelf, &field, empty_list, vm)?; + } else if ftype.is(&expr_ctx_type) { + let load_type = + super::super::pyast::NodeExprContextLoad::make_static_type(); + let load_instance = load_type + .get_attr(vm.ctx.intern_str("_instance")) + .unwrap_or_else(|| { + vm.ctx.new_base_object(load_type, Some(vm.ctx.new_dict())) + }); + ast_set_attr(zelf, &field, load_instance, vm)?; + } else { + missing_names.add(field, vm)?; } } - if !missing.is_empty() { - let count = missing.len(); - let names = match missing.as_slice() { - [name] => name.clone(), - [first, last] => format!("{first} and {last}"), - names => format!( - "{}, and {}", - names[..count - 1].join(", "), - names[count - 1] - ), - }; - return Err(vm.new_type_error(format!( - "{}.__init__ missing {} required positional argument{}: {}", + + let count = missing_names.elements().len(); + if count > 0 { + // Match constructor order and preserve non-UTF-8 field names. + let mut message = Wtf8Buf::from(format!( + "{}.__init__ missing {} required positional argument{}: ", zelf.class().fully_qualified_name(vm)?, count, if count == 1 { "" } else { "s" }, - names - ))); + )); + let mut num_left = count as isize; + for i in 0..numfields { + let field = fields_seq.get_item(i as isize, vm)?; + if missing_names.contains(&field, vm)? { + let field = field + .downcast::() + .map_err(|_| vm.new_system_error("non-string AST field name"))?; + message.push_wtf8("'".as_ref()); + message.push_wtf8(field.as_wtf8()); + message.push_wtf8("'".as_ref()); + let separator = if num_left == 1 { + "" + } else if count == 2 { + " and " + } else if num_left == 2 { + ", and " + } else { + ", " + }; + message.push_wtf8(separator.as_ref()); + num_left -= 1; + } + } + return Err(vm.new_type_error(message)); } } @@ -530,24 +547,10 @@ This will become an error in Python 3.20", let set_empty_annotations = |typ: &Py| { typ.set_str_attr("__annotations__", ctx.new_dict(), ctx); }; - set_empty_annotations(&ast_type); ast_type.set_str_attr("_fields", empty_tuple.clone(), ctx); ast_type.set_str_attr("_attributes", empty_tuple.clone(), ctx); ast_type.set_str_attr("__match_args__", empty_tuple, ctx); - for typ in [ - super::super::pyast::NodeMod::static_type(), - super::super::pyast::NodeStmt::static_type(), - super::super::pyast::NodeExpr::static_type(), - super::super::pyast::NodeExprContext::static_type(), - super::super::pyast::NodeBoolOp::static_type(), - super::super::pyast::NodeOperator::static_type(), - super::super::pyast::NodeUnaryOp::static_type(), - super::super::pyast::NodeCmpOp::static_type(), - super::super::pyast::NodeExceptHandler::static_type(), - super::super::pyast::NodePattern::static_type(), - super::super::pyast::NodeTypeIgnore::static_type(), - super::super::pyast::NodeTypeParam::static_type(), - ] { + for typ in abstract_types() { set_empty_annotations(typ); } @@ -559,16 +562,6 @@ This will become an error in Python 3.20", PyMethodFlags::METHOD, crate::function::ItemDoc::static_text("__reduce__($self, /)\n--\n\n"), ); - const AST_REPLACE: PyMethodDef = PyMethodDef::new_const( - "__replace__", - |zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine| -> PyResult { - ast_replace(&zelf, args, vm) - }, - PyMethodFlags::METHOD, - crate::function::ItemDoc::static_text( - "__replace__($self, /, **fields)\n--\n\nReturn a copy of the AST node with new values for the specified fields.", - ), - ); let base_type = NodeAst::static_type(); ast_type.set_str_attr( "__reduce__", @@ -592,7 +585,7 @@ This will become an error in Python 3.20", | Set(expr* elts)\n\ | ListComp(expr elt, comprehension* generators)\n\ | SetComp(expr elt, comprehension* generators)\n\ - | DictComp(expr key, expr value, comprehension* generators)\n\ + | DictComp(expr key, expr? value, comprehension* generators)\n\ | GeneratorExp(expr elt, comprehension* generators)\n\ | Await(expr value)\n\ | Yield(expr? value)\n\ diff --git a/crates/vm/src/stdlib/_ast/validate.rs b/crates/vm/src/stdlib/_ast/validate.rs index b2d2b0a4b8b..1d8a1c4fa95 100644 --- a/crates/vm/src/stdlib/_ast/validate.rs +++ b/crates/vm/src/stdlib/_ast/validate.rs @@ -200,11 +200,11 @@ fn ensure_literal_number(expr: &ast::Expr, allow_real: bool, allow_imaginary: bo } } -fn ensure_literal_negative(expr: &ast::Expr, allow_real: bool, allow_imaginary: bool) -> bool { +fn ensure_literal_signed(expr: &ast::Expr, allow_real: bool, allow_imaginary: bool) -> bool { let ast::Expr::UnaryOp(unary) = expr else { return false; }; - if unary.op != ast::UnaryOp::USub { + if !matches!(unary.op, ast::UnaryOp::UAdd | ast::UnaryOp::USub) { return false; } ensure_literal_number(&unary.operand, allow_real, allow_imaginary) @@ -218,7 +218,7 @@ fn ensure_literal_complex(expr: &ast::Expr) -> bool { return false; } let real_left = ensure_literal_number(&bin.left, true, false) - || ensure_literal_negative(&bin.left, true, false); + || ensure_literal_signed(&bin.left, true, false); real_left && ensure_literal_number(&bin.right, false, true) } @@ -244,7 +244,7 @@ fn validate_pattern_match_value(vm: &VirtualMachine, expr: &ast::Expr) -> PyResu Ok(()) } ast::Expr::Attribute(_) => Ok(()), - ast::Expr::UnaryOp(_) if ensure_literal_negative(expr, true, true) => Ok(()), + ast::Expr::UnaryOp(_) if ensure_literal_signed(expr, true, true) => Ok(()), ast::Expr::BinOp(_) if ensure_literal_complex(expr) => Ok(()), ast::Expr::FString(_) | ast::Expr::TString(_) => Ok(()), ast::Expr::BooleanLiteral(_) @@ -608,10 +608,9 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) - } ast::Expr::DictComp(dict) => { validate_comprehension(vm, &dict.generators)?; - let key = dict.key.as_deref().ok_or_else(|| { - vm.new_value_error("field 'key' is required for DictComp".to_owned()) - })?; - validate_expr(vm, key, ast::ExprContext::Load)?; + if let Some(key) = &dict.key { + validate_expr(vm, key, ast::ExprContext::Load)?; + } validate_expr(vm, &dict.value, ast::ExprContext::Load) } ast::Expr::Generator(generator) => { diff --git a/crates/vm/src/stdlib/_collections/ordered_dict.rs b/crates/vm/src/stdlib/_collections/ordered_dict.rs index 1a2222c7bba..a0879545a44 100644 --- a/crates/vm/src/stdlib/_collections/ordered_dict.rs +++ b/crates/vm/src/stdlib/_collections/ordered_dict.rs @@ -684,8 +684,8 @@ pub(crate) mod ordered_dict { fn as_number() -> &'static PyNumberMethods { static AS_NUMBER: PyNumberMethods = PyNumberMethods { or: Some(|a, b, vm| { - if !a.fast_isinstance(vm.ctx.types.dict_type) - || !b.fast_isinstance(vm.ctx.types.dict_type) + if crate::builtins::PyAnyDict::from_object(a).is_none() + || crate::builtins::PyAnyDict::from_object(b).is_none() { return Ok(vm.ctx.not_implemented()); } diff --git a/crates/vm/src/stdlib/_ctypes.rs b/crates/vm/src/stdlib/_ctypes.rs index 780b2bc0bd2..d69396176fa 100644 --- a/crates/vm/src/stdlib/_ctypes.rs +++ b/crates/vm/src/stdlib/_ctypes.rs @@ -92,7 +92,7 @@ pub(crate) use _ctypes::module_def; #[pymodule] pub(crate) mod _ctypes { use super::{PyCArray, PyCData, PyCPointer, PyCSimple, PyCStructure, PyCUnion}; - use crate::builtins::{PyType, PyTypeRef}; + use crate::builtins::{PyStrRef, PyType, PyTypeRef}; use crate::class::StaticType; #[cfg(windows)] use crate::convert::ToPyException; @@ -104,12 +104,14 @@ pub(crate) mod _ctypes { // CArgObject - returned by byref() and paramfunc // tagPyCArgObject - #[pyclass(name = "CArgObject", module = "_ctypes", no_attr)] + #[pyclass(name = "CArgObject", module = "_ctypes", no_attr, traverse)] #[derive(Debug, PyPayload)] pub(crate) struct CArgObject { /// Type tag ('P', 'V', 'i', 'd', etc.) + #[pytraverse(skip)] pub tag: u8, /// The actual foreign-call value (mirrors union value) + #[pytraverse(skip)] pub value: super::CArgValue, /// Reference to original object (for memory safety) pub obj: PyObjectRef, @@ -118,8 +120,10 @@ pub(crate) mod _ctypes { pub keep: Option, /// Size for struct/union ('V' tag) #[allow(dead_code)] + #[pytraverse(skip)] pub size: usize, /// Offset for byref() + #[pytraverse(skip)] pub offset: isize, } @@ -233,8 +237,22 @@ pub(crate) mod _ctypes { } } - #[pyattr(name = "__version__")] - const __VERSION__: &str = "1.1.0"; + #[pyfunction] + fn __getattr__(name: PyStrRef, vm: &VirtualMachine) -> PyResult { + if name.to_str() == Some("__version__") { + crate::stdlib::_warnings::warn( + vm.ctx.exceptions.deprecation_warning, + "'__version__' is deprecated and slated for removal in Python 3.20".to_owned(), + 1, + vm, + )?; + return Ok(vm.ctx.new_str("1.1.0").into()); + } + Err(vm.new_attribute_error(format!( + "module '_ctypes' has no attribute {}", + name.repr(vm)? + ))) + } // TODO: get properly #[pyattr] @@ -400,6 +418,15 @@ pub(crate) mod _ctypes { Err(vm.new_type_error("this type has no size")) } + #[cfg(target_os = "linux")] + #[pyfunction] + fn dllist(vm: &VirtualMachine) -> Vec { + rustpython_host_env::ctypes::dllist() + .into_iter() + .map(|name| vm.fsdecode(name).into()) + .collect() + } + #[cfg(windows)] #[pyfunction(name = "LoadLibrary")] fn load_library_windows( diff --git a/crates/vm/src/stdlib/_ctypes/base.rs b/crates/vm/src/stdlib/_ctypes/base.rs index 2b697ef5558..9348e31d3ec 100644 --- a/crates/vm/src/stdlib/_ctypes/base.rs +++ b/crates/vm/src/stdlib/_ctypes/base.rs @@ -213,7 +213,7 @@ impl StgInfo { size, align, length, - proto: None, + proto: Some(element_type.clone()), flags, element_type: Some(element_type), element_size, @@ -1899,14 +1899,17 @@ fn array_paramfunc(obj: &PyObject, vm: &VirtualMachine) -> PyResult /// PyCPointerType_paramfunc fn pointer_paramfunc(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + use super::function::PyCFuncPtr; use super::pointer::PyCPointer; - let ptr = obj - .downcast_ref::() - .ok_or_else(|| vm.new_type_error("expected pointer"))?; - // parg->value.p = *(void **)self->b_ptr - let ptr_val = ptr.get_ptr_value(); + let ptr_val = if let Some(ptr) = obj.downcast_ref::() { + ptr.get_ptr_value() + } else if let Some(funcptr) = obj.downcast_ref::() { + funcptr.get_func_ptr() + } else { + return Err(vm.new_type_error("expected pointer")); + }; Ok(CArgObject { tag: b'P', diff --git a/crates/vm/src/stdlib/_ctypes/function.rs b/crates/vm/src/stdlib/_ctypes/function.rs index 689d13b6e81..e1e08919713 100644 --- a/crates/vm/src/stdlib/_ctypes/function.rs +++ b/crates/vm/src/stdlib/_ctypes/function.rs @@ -17,7 +17,7 @@ use crate::{ vm::thread::with_current_vm, }; use alloc::borrow::Cow; -use core::ffi::c_void; +use core::ffi::{c_int, c_long, c_ulong, c_void}; use core::fmt::Debug; use core::ptr::NonNull; use num_traits::{Signed, ToPrimitive}; @@ -41,42 +41,36 @@ pub(super) const INTERNAL_MEMORYVIEW_AT_ADDR: usize = 4; /// Convert any object to a pointer value for c_void_p arguments /// Follows ConvParam logic for pointer types -fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult { +fn convert_to_pointer( + value: &PyObject, + vm: &VirtualMachine, +) -> PyResult<(CArgValue, Option)> { + let pointer = |address| Ok((CArgValue::pointer(address), Some(value.to_owned()))); // 0. CArgObject (from byref()) -> buffer address + offset if let Some(carg) = value.downcast_ref::() { - // Get buffer address from the underlying object - let base_addr = if let Some(cdata) = carg.obj.downcast_ref::() { - cdata.buffer.read().as_ptr() as usize - } else { - return Err(vm.new_type_error(format!( - "byref() argument must be a ctypes instance, not '{}'", - carg.obj.class().name() - ))); - }; - let addr = (base_addr as isize + carg.offset) as usize; - return Ok(CArgValue::pointer(addr)); + return Ok((carg.value.clone(), Some(value.to_owned()))); } // 1. None -> NULL if value.is(&vm.ctx.none) { - return Ok(CArgValue::pointer(0)); + return Ok((CArgValue::pointer(0), None)); } // 2. PyCArray -> buffer address (PyCArrayType_paramfunc) if let Some(array) = value.downcast_ref::() { let addr = array.0.buffer.read().as_ptr() as usize; - return Ok(CArgValue::pointer(addr)); + return pointer(addr); } // 3. PyCPointer -> stored pointer value if let Some(ptr) = value.downcast_ref::() { - return Ok(CArgValue::pointer(ptr.get_ptr_value())); + return pointer(ptr.get_ptr_value()); } // 4. PyCStructure -> buffer address if let Some(struct_obj) = value.downcast_ref::() { let addr = struct_obj.0.buffer.read().as_ptr() as usize; - return Ok(CArgValue::pointer(addr)); + return pointer(addr); } // 5. PyCSimple (c_void_p, c_char_p, etc.) -> value from buffer @@ -84,14 +78,14 @@ fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult buffer address (PyBytes_AsString) + // 6. bytes -> owned, null-terminated buffer if let Some(bytes) = value.downcast_ref::() { - let addr = bytes.as_bytes().as_ptr() as usize; - return Ok(CArgValue::pointer(addr)); + let (owner, address) = super::base::ensure_z_null_terminated(bytes, vm); + return Ok((CArgValue::pointer(address), Some(owner))); } // 7. Integer -> direct value (PyLong_AsVoidPtr behavior) @@ -100,10 +94,10 @@ fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult PyResult { // 1. CArgObject (from byref() or paramfunc) -> use stored value if let Some(carg) = value.downcast_ref::() { return Ok(Argument { - keep: carg.keep.clone(), + // Keep the wrapper, which owns both the source and any auxiliary + // pointer storage. `_as_parameter_` may have created it on demand. + keep: Some(value.to_owned()), value: carg.value.clone(), }); } @@ -141,9 +137,10 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { // 3. ctypes objects -> use paramfunc if let Ok(carg) = super::base::call_paramfunc(value, vm) { + let value = carg.value.clone(); return Ok(Argument { - keep: carg.keep, - value: carg.value, + keep: Some(carg.into_ref(&vm.ctx).into()), + value, }); } @@ -175,7 +172,26 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { // `__int__` would accept a float and pass its truncated value where the // callee expects a pointer. if let Some(int_val) = value.downcast_ref::() { - let val = int_val.as_bigint().to_i32().unwrap_or(0); + // CPython first accepts the platform's unsigned long / signed long + // range, then stores the low C-int bits in the argument carrier. + let bigint = int_val.as_bigint(); + #[allow( + clippy::useless_conversion, + clippy::unnecessary_cast, + reason = "C long is 32 bits on Windows and 64 bits on LP64 targets" + )] + let val = if bigint.is_negative() { + bigint + .to_i64() + .and_then(|value| c_long::try_from(value).ok()) + .map(|value| value as c_int) + } else { + bigint + .to_u64() + .and_then(|value| c_ulong::try_from(value).ok()) + .map(|value| value as c_int) + } + .ok_or_else(|| vm.new_overflow_error("int too long to convert"))?; return Ok(Argument { keep: None, value: CArgValue::Int(val), @@ -193,6 +209,29 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { ))) } +fn conv_param_for_call(value: &PyObject, index: usize, vm: &VirtualMachine) -> PyResult { + match conv_param(value, vm) { + Ok(argument) => Ok(argument), + Err(error) => { + let error_type = vm + .import("_ctypes", 0)? + .get_attr("ArgumentError", vm)? + .downcast::() + .map_err(|_| vm.new_type_error("ArgumentError is not a type"))?; + Err(vm.new_exception_msg( + error_type, + format!( + "argument {}: {}: {}", + index + 1, + error.class().name(), + error.as_object().str(vm)?, + ) + .into(), + )) + } + } +} + trait ArgumentType { /// Convert an argument for this type into a foreign-call value plus an /// optional owner keeping any referenced memory alive. @@ -214,6 +253,7 @@ impl ArgumentType for PyTypeRef { // type must carry a known _type_ code; anything else is unsupported. let type_code = if self.fast_issubclass(CArgObject::static_type()) || self.fast_issubclass(PyCPointer::static_type()) + || self.fast_issubclass(PyCFuncPtr::static_type()) || self.fast_issubclass(PyCStructure::static_type()) || self.fast_issubclass(PyCUnion::static_type()) { @@ -246,7 +286,7 @@ impl ArgumentType for PyTypeRef { // Then pass the converted value to ConvParam logic // CArgObject (from from_param) -> use stored value and keepalive directly if let Some(carg) = converted.downcast_ref::() { - return Ok((carg.value.clone(), carg.keep.clone())); + return Ok((carg.value.clone(), Some(converted.clone()))); } // None -> NULL pointer @@ -254,12 +294,20 @@ impl ArgumentType for PyTypeRef { return Ok((CArgValue::pointer(0), None)); } + if self.fast_issubclass(PyCFuncPtr::static_type()) { + let argument = conv_param(&converted, vm)?; + return Ok((argument.value, argument.keep)); + } + // For pointer types (POINTER(T)), we need to pass the pointer VALUE stored in buffer if self.fast_issubclass(PyCPointer::static_type()) { if let Some(pointer) = converted.downcast_ref::() { - return Ok((CArgValue::pointer(pointer.get_ptr_value()), None)); + return Ok(( + CArgValue::pointer(pointer.get_ptr_value()), + Some(converted.clone()), + )); } - return Ok((convert_to_pointer(&converted, vm)?, None)); + return convert_to_pointer(&converted, vm); } // For structure/union types, pass the aggregate by value: snapshot the @@ -279,12 +327,12 @@ impl ArgumentType for PyTypeRef { // owns, which must outlive the foreign call. return Ok((CArgValue::aggregate(layout, bytes), Some(converted.clone()))); } - return Ok((convert_to_pointer(&converted, vm)?, None)); + return convert_to_pointer(&converted, vm); } // For pointer types (c_void_p, c_char_p, c_wchar_p), handle as pointer if matches!(type_code.as_deref(), Some("P" | "z" | "Z")) { - return Ok((convert_to_pointer(&converted, vm)?, None)); + return convert_to_pointer(&converted, vm); } // PyCSimple (already a ctypes instance from from_param) @@ -293,7 +341,7 @@ impl ArgumentType for PyTypeRef { .as_deref() .and_then(|s| s.chars().next()) .ok_or_else(|| vm.new_type_error("Unsupported argument type"))?; - return Ok((simple.to_carg_value(code), None)); + return Ok((simple.to_carg_value(code), Some(simple.into()))); } Err(vm.new_type_error("Unsupported argument type")) @@ -333,6 +381,25 @@ impl Initializer for PyCFuncPtrType { #[pyclass(flags(IMMUTABLETYPE), with(Initializer))] impl PyCFuncPtrType { + #[pymethod] + fn from_param(zelf: PyTypeRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult { + if value.is_instance(zelf.as_object(), vm)? { + return Ok(value); + } + + if let Some(as_parameter) = vm.get_attribute_opt(&value, "_as_parameter_")? { + return vm.with_recursion(" while processing _as_parameter_", || { + Self::from_param(zelf, as_parameter, vm) + }); + } + + Err(vm.new_type_error(format!( + "expected {} instance instead of {}", + zelf.name(), + value.class().name() + ))) + } + #[pygetset(name = "__pointer_type__")] fn pointer_type(zelf: PyTypeRef, vm: &VirtualMachine) -> PyResult { super::base::pointer_type_get(&zelf, vm) @@ -635,7 +702,7 @@ pub(super) fn cast_impl( impl PyCFuncPtr { /// Get function pointer address from buffer - fn get_func_ptr(&self) -> usize { + pub(super) fn get_func_ptr(&self) -> usize { let buffer = self._base.buffer.read(); rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer) } @@ -1197,7 +1264,8 @@ fn build_callargs_no_argtypes( let arguments: Vec = args .args .iter() - .map(|arg| conv_param(arg, vm)) + .enumerate() + .map(|(index, arg)| conv_param_for_call(arg, index, vm)) .collect::>>()?; Ok((arguments, Vec::new())) } @@ -1206,18 +1274,30 @@ fn build_callargs_no_argtypes( fn build_callargs_simple( args: &FuncArgs, arg_types: &[PyTypeRef], + allow_variadic: bool, vm: &VirtualMachine, ) -> PyResult<(Vec, OutBuffers)> { + if args.args.len() < arg_types.len() || (!allow_variadic && args.args.len() != arg_types.len()) + { + return Err(vm.new_type_error(format!( + "this function takes {}{} argument{} ({} given)", + if allow_variadic { "at least " } else { "" }, + arg_types.len(), + if arg_types.len() == 1 { "" } else { "s" }, + args.args.len(), + ))); + } let arguments: Vec = args .args .iter() .enumerate() .map(|(n, arg)| { - let arg_type = arg_types - .get(n) - .ok_or_else(|| vm.new_type_error("argument amount mismatch"))?; - let (value, keep) = arg_type.convert_object(arg.clone(), vm)?; - Ok(Argument { value, keep }) + if let Some(arg_type) = arg_types.get(n) { + let (value, keep) = arg_type.convert_object(arg.clone(), vm)?; + Ok(Argument { value, keep }) + } else { + conv_param_for_call(arg, n, vm) + } }) .collect::>>()?; Ok((arguments, Vec::new())) @@ -1293,6 +1373,7 @@ fn build_callargs( call_info: &CallInfo, paramflags: Option<&ParsedParamFlags>, is_com_method: bool, + allow_variadic: bool, vm: &VirtualMachine, ) -> PyResult<(Vec, OutBuffers)> { let Some(ref arg_types) = call_info.explicit_arg_types else { @@ -1319,7 +1400,7 @@ fn build_callargs( Ok((arguments, Vec::new())) } else { // Regular function - build_callargs_simple(args, arg_types, vm) + build_callargs_simple(args, arg_types, allow_variadic, vm) } } @@ -1391,11 +1472,11 @@ fn convert_raw_result( result: &CallValue, call_info: &CallInfo, vm: &VirtualMachine, -) -> Option { +) -> PyResult> { // Result register image as bytes + size (None for void): pointer/scalar // returns are pointer/register sized. let (result_bytes, result_size) = match result { - CallValue::Void => return None, + CallValue::Void => return Ok(None), CallValue::Pointer(ptr) => (ptr.to_ne_bytes().to_vec(), size_of::()), CallValue::Scalar(bytes) | CallValue::Aggregate(bytes) => (bytes.clone(), bytes.len()), }; @@ -1416,14 +1497,14 @@ fn convert_raw_result( let restype = match &call_info.restype_obj { None => { // Default: return as int - return Some(vm.ctx.new_int(result_word as isize).into()); + return Ok(Some(vm.ctx.new_int(result_word as isize).into())); } Some(r) => r, }; // 2. restype is None → return None if restype.is(&vm.ctx.none()) { - return None; + return Ok(None); } // 3. Get restype as PyType @@ -1431,7 +1512,7 @@ fn convert_raw_result( Ok(t) => t, Err(_) => { // Not a type, call it with int result - return restype.call((result_word as isize,), vm).ok(); + return restype.call((result_word as isize,), vm).map(Some); } }; @@ -1443,7 +1524,7 @@ fn convert_raw_result( return restype_type .as_object() .call((result_word as isize,), vm) - .ok(); + .map(Some); } let info = stg_info.unwrap(); @@ -1461,24 +1542,22 @@ fn convert_raw_result( && let Some(type_str) = type_attr.downcast_ref::() && type_str.to_str() == Some("O") { - let ptr = NonNull::new(result_word as *mut PyObject).or_else(|| { - vm.set_exception(Some(vm.new_value_error("PyObject is NULL"))); - None - })?; + let ptr = NonNull::new(result_word as *mut PyObject) + .ok_or_else(|| vm.new_value_error("PyObject is NULL"))?; unsafe { let obj = PyObjectRef::from_raw(ptr); - return Some(obj); + return Ok(Some(obj)); } } // 5. Simple type with getfunc → use bytes_to_pyobject (info->getfunc) // is_simple_instance returns TRUE for c_int, c_void_p, etc. if super::base::is_simple_instance(&restype_type) { - return Some(super::base::bytes_to_pyobject( + return Ok(Some(super::base::bytes_to_pyobject( &restype_type, &result_bytes, vm, - )); + ))); } // 6. Complex type → create ctypes instance (PyCData_FromBaseObj) @@ -1488,16 +1567,16 @@ fn convert_raw_result( if is_pointer_type && has_proto && let CallValue::Pointer(ptr) = result - && let Ok(instance) = restype_type.as_object().call((), vm) { + let instance = restype_type.as_object().call((), vm)?; if let Some(pointer) = instance.downcast_ref::() { pointer.set_ptr_value(*ptr); } - return Some(instance); + return Ok(Some(instance)); } // Create instance and copy result data - pycdata_from_ffi_result(&restype_type, &result_bytes, result_size, vm).ok() + pycdata_from_ffi_result(&restype_type, &result_bytes, result_size, vm).map(Some) } /// Create a ctypes instance from FFI result (PyCData_FromBaseObj equivalent) @@ -1556,7 +1635,7 @@ fn build_result( } // Convert the foreign-call result to a Python object - let mut result = convert_raw_result(&call_result, call_info, vm); + let mut result = convert_raw_result(&call_result, call_info, vm)?; // Apply errcheck if set if let Some(errcheck) = zelf.errcheck.read().as_ref() { @@ -1603,8 +1682,16 @@ impl Callable for PyCFuncPtr { let paramflags = parse_paramflags(zelf, vm)?; // 5. Build call arguments - let (arguments, out_buffers) = - build_callargs(&args, &call_info, paramflags.as_ref(), is_com_method, vm)?; + let flags = Py::::_flags_(zelf, vm); + let allow_variadic = flags & StgInfoFlags::FUNCFLAG_CDECL.bits() != 0; + let (arguments, out_buffers) = build_callargs( + &args, + &call_info, + paramflags.as_ref(), + is_com_method, + allow_variadic, + vm, + )?; // 6. Function address (usize); the unified `call` rejects a NULL address. let addr = match func_ptr { @@ -1613,24 +1700,47 @@ impl Callable for PyCFuncPtr { }; // 7. Errno / last-error swap options from flags - let flags = Py::::_flags_(zelf, vm); let options = CallOptions { + fixed_arg_count: call_info + .explicit_arg_types + .as_ref() + .map(Vec::len) + .filter(|&fixed| allow_variadic && fixed != 0 && arguments.len() > fixed), use_errno: flags & super::base::StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0, use_last_error: flags & super::base::StgInfoFlags::FUNCFLAG_USE_LASTERROR.bits() != 0, }; - // 8. Call the function through the unified entry point. - let call_result = ctypes_callproc(addr, &arguments, &call_info.ret, options).map_err( - |err| match err { - CallError::NullFunctionPointer => vm.new_value_error("NULL function pointer"), - CallError::UnknownTypeCode(code) => { - vm.new_type_error(format!("Unsupported argument type: {code}")) - } - CallError::BufferTooSmall { expected, got } => vm.new_value_error(format!( - "argument buffer too small: expected {expected}, got {got}" - )), - }, - )?; + // Native calls may block while another thread runs Python. Callbacks + // reattach before executing Python; PythonAPI calls stay attached. + let native_call = || ctypes_callproc(addr, &arguments, &call_info.ret, options); + let call_result = if flags & StgInfoFlags::FUNCFLAG_PYTHONAPI.bits() != 0 { + native_call() + } else { + vm.allow_threads(native_call) + } + .map_err(|err| match err { + CallError::NullFunctionPointer => vm.new_value_error("NULL function pointer"), + CallError::InvalidCallInterface { variadic } => vm.new_runtime_error(if variadic { + "ffi_prep_cif_var failed" + } else { + "ffi_prep_cif failed" + }), + CallError::UnknownTypeCode(code) => { + vm.new_type_error(format!("Unsupported argument type: {code}")) + } + CallError::BufferTooSmall { expected, got } => vm.new_value_error(format!( + "argument buffer too small: expected {expected}, got {got}" + )), + })?; + + // PythonAPI functions report errors through the native raised-error + // slot. Check it before a NULL PyObject* can be decoded, and consume + // the exception exactly once without touching handled exceptions. + if flags & StgInfoFlags::FUNCFLAG_PYTHONAPI.bits() != 0 + && let Some(exception) = vm.take_raised_exception() + { + return Err(exception); + } // 9. Build result build_result(call_result, &call_info, out_buffers, zelf, &args, vm) @@ -1789,7 +1899,23 @@ fn ffi_to_python( args: *const *const c_void, index: usize, vm: &VirtualMachine, -) -> PyObjectRef { +) -> PyResult { + if ty.fast_issubclass(PyCPointer::static_type()) { + // POINTER(T) uses a pointer argument in the CIF, even though its + // `_type_` is a type object rather than a simple type code. + let rustpython_host_env::ctypes::DecodedValue::Pointer(address) = + (unsafe { rustpython_host_env::ctypes::callback_arg_value_at(Some("P"), args, index) }) + else { + return Err(vm.new_type_error("invalid pointer callback argument")); + }; + let instance = ty.as_object().call((), vm)?; + let pointer = instance + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("pointer constructor returned a non-pointer"))?; + pointer.set_ptr_value(address); + return Ok(instance); + } + let type_code = ty.type_code(vm); let raw_value: PyObjectRef = match unsafe { rustpython_host_env::ctypes::callback_arg_value_at(type_code.as_deref(), args, index) @@ -1805,11 +1931,12 @@ fn ffi_to_python( }; if !is_simple_subclass(ty, vm) { - return raw_value; + return Ok(raw_value); } - ty.as_object() + Ok(ty + .as_object() .call((raw_value.clone(),), vm) - .unwrap_or(raw_value) + .unwrap_or(raw_value)) } /// Convert a Python object to a C value and store it at the result pointer @@ -1891,32 +2018,35 @@ unsafe extern "C" fn thunk_callback( userdata: &ThunkUserData, ) { with_current_vm(|vm| { - let use_errno = userdata.flags & StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0; - let py_result = - rustpython_host_env::ctypes::with_callback_errno_preserved(use_errno, || { - let py_args: Vec = userdata - .arg_types - .iter() - .enumerate() - .map(|(i, ty)| ffi_to_python(ty, args, i, vm)) - .collect(); - - userdata.callable.call(py_args, vm) - }); - - // Call unraisable hook if exception occurred - if let Err(exc) = &py_result { - let repr = userdata - .callable - .repr(vm) - .map_or_else(|_| "".to_string(), |s| s.to_string()); - let msg = format!("Exception ignored while calling ctypes callback function {repr}"); - vm.run_unraisable(exc.clone(), Some(msg), vm.ctx.none()); - } + vm.attach_for_callback(|| { + let use_errno = userdata.flags & StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0; + let py_result = + rustpython_host_env::ctypes::with_callback_errno_preserved(use_errno, || { + let py_args: Vec = userdata + .arg_types + .iter() + .enumerate() + .map(|(i, ty)| ffi_to_python(ty, args, i, vm)) + .collect::>>()?; + + userdata.callable.call(py_args, vm) + }); + + // Call unraisable hook if exception occurred + if let Err(exc) = &py_result { + let repr = userdata + .callable + .repr(vm) + .map_or_else(|_| "".to_string(), |s| s.to_string()); + let msg = + format!("Exception ignored while calling ctypes callback function {repr}"); + vm.run_unraisable(exc.clone(), Some(msg), vm.ctx.none()); + } - if let Some(ref res_type) = userdata.res_type { - python_to_ffi(py_result, res_type, result as *mut c_void, vm); - } + if let Some(ref res_type) = userdata.res_type { + python_to_ffi(py_result, res_type, result as *mut c_void, vm); + } + }); }); } @@ -1963,9 +2093,13 @@ impl PyCThunk { let ffi_arg_types: Vec = arg_type_vec .iter() .map(|ty| { - ty.type_code(vm) - .and_then(|code| ffi_type_from_code(&code)) - .unwrap_or_else(ffi_pointer_type) + if ty.fast_issubclass(PyCPointer::static_type()) { + ffi_pointer_type() + } else { + ty.type_code(vm) + .and_then(|code| ffi_type_from_code(&code)) + .unwrap_or_else(ffi_pointer_type) + } }) .collect(); diff --git a/crates/vm/src/stdlib/_ctypes/simple.rs b/crates/vm/src/stdlib/_ctypes/simple.rs index 18b67fca7c8..5003f772db2 100644 --- a/crates/vm/src/stdlib/_ctypes/simple.rs +++ b/crates/vm/src/stdlib/_ctypes/simple.rs @@ -57,6 +57,12 @@ fn new_simple_type( let tp_str = _type_.str(vm)?.to_string(); + // Complex ctypes storage and FFI are not implemented. Preserve the + // optional-type AttributeError contract for the PEP 3118 codes. + if matches!(tp_str.as_str(), "Zd" | "Zf" | "Zg") { + return Err(vm.new_attribute_error("complex ctypes types are not supported")); + } + if tp_str.len() != 1 { return Err(vm.new_value_error(format!( "class must define a '_type_' attribute which must be a string of length 1, str: {tp_str}" @@ -555,6 +561,12 @@ impl Initializer for PyCSimpleType { let type_str = type_attr.str(vm)?.to_string(); // Validate _type_ is a single character + // Complex ctypes storage and FFI are not implemented. Preserve the + // optional-type AttributeError contract for the PEP 3118 codes. + if matches!(type_str.as_str(), "Zd" | "Zf" | "Zg") { + return Err(vm.new_attribute_error("complex ctypes types are not supported")); + } + if type_str.len() != 1 { return Err(vm.new_value_error( "class must define a '_type_' attribute which must be a string of length 1", diff --git a/crates/vm/src/stdlib/_imp.rs b/crates/vm/src/stdlib/_imp.rs index 6d3a6b0b868..dfc8f3273ff 100644 --- a/crates/vm/src/stdlib/_imp.rs +++ b/crates/vm/src/stdlib/_imp.rs @@ -180,10 +180,14 @@ fn find_frozen(name: &str, vm: &VirtualMachine) -> Result 0 → true, override < 0 → false, 0 → default (true) + // use_frozen(): override > 0 → true, override < 0 → false, 0 → startup setting // When disabled, non-bootstrap modules are simply not found (same as look_up_frozen) let override_val = vm.state.override_frozen_modules.load(); - if override_val < 0 { + if override_val < 0 + || (override_val == 0 + && vm.import_bootstrap_complete + && !vm.state.config.settings.use_frozen_modules) + { return Err(FrozenError::NotFound); } @@ -239,13 +243,15 @@ mod _imp { } #[pyfunction] - fn is_builtin(name: PyUtf8StrRef, vm: &VirtualMachine) -> bool { - vm.state.module_defs.contains_key(name.as_str()) + fn is_builtin(name: PyStrRef, vm: &VirtualMachine) -> bool { + name.to_str() + .is_some_and(|name| vm.state.module_defs.contains_key(name)) } #[pyfunction] - fn is_frozen(name: PyUtf8StrRef, vm: &VirtualMachine) -> bool { - super::find_frozen(name.as_str(), vm).is_ok() + fn is_frozen(name: PyStrRef, vm: &VirtualMachine) -> bool { + name.to_str() + .is_some_and(|name| super::find_frozen(name, vm).is_ok()) } #[pyfunction] @@ -477,7 +483,7 @@ mod _imp { #[derive(FromArgs)] struct FindFrozenArgs { #[pyarg(positional)] - name: PyUtf8StrRef, + name: PyStrRef, #[pyarg(named, default)] withdata: bool, } @@ -489,8 +495,9 @@ mod _imp { vm: &VirtualMachine, ) -> PyResult>, bool, Option)>> { let FindFrozenArgs { name, withdata } = args; - - let name_str = name.as_str(); + let Some(name_str) = name.to_str() else { + return Ok(None); + }; let info = match super::find_frozen(name_str, vm) { Ok(info) => info, Err(FrozenError::NotFound | FrozenError::Disabled | FrozenError::BadName) => { diff --git a/crates/vm/src/stdlib/_interpreters.rs b/crates/vm/src/stdlib/_interpreters.rs index 903a14f6acc..802e6c77c4e 100644 --- a/crates/vm/src/stdlib/_interpreters.rs +++ b/crates/vm/src/stdlib/_interpreters.rs @@ -2,7 +2,7 @@ //! //! Mirrors CPython `Modules/_interpretersmodule.c`. -pub(crate) use _interpreters::{config_from_pyobject, init_xi_types, module_def}; +pub(crate) use _interpreters::{config_from_pyobject, init_xi_types, module_def, parse_id}; #[cfg_attr(not(feature = "threading"), allow(unused_imports))] pub(crate) use _interpreters::{ interpreter_error, interpreter_not_found, not_shareable_error, xibufferview_from_buffer, @@ -170,7 +170,7 @@ pub(crate) mod _interpreters { } /// `_PyInterpreterState_ObjectToID`. - fn parse_id(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + pub(crate) fn parse_id(obj: &PyObject, vm: &VirtualMachine) -> PyResult { if !obj.number().is_index() { return Err(vm.new_type_error(format!( "interpreter ID must be an int, got {}", @@ -672,7 +672,7 @@ pub(crate) mod _interpreters { args, |i, obj, vm| match i { 1 => second_check(obj, vm), - 2 => check_dict(obj, func, "argument 3", vm), + 2 => check_dict(obj, func, "argument 'shared'", vm), _ => Ok(()), }, vm, @@ -910,7 +910,7 @@ pub(crate) mod _interpreters { .parse_with( &args, |i, obj, vm| match i { - 1 => check_dict(obj, FUNCNAME, "argument 2", vm), + 1 => check_dict(obj, FUNCNAME, "argument 'updates'", vm), _ => Ok(()), }, vm, diff --git a/crates/vm/src/stdlib/_lsprof.rs b/crates/vm/src/stdlib/_lsprof.rs new file mode 100644 index 00000000000..03ea5e6dd91 --- /dev/null +++ b/crates/vm/src/stdlib/_lsprof.rs @@ -0,0 +1,620 @@ +//! Deterministic profiling driven by the VM's execution monitoring events. +// cspell:ignore pystart pythrow creturn + +pub(crate) use _lsprof::module_def; + +#[pymodule] +mod _lsprof { + use crate::{ + AsObject, Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, + builtins::{ + PyFloat, PyStr, + builtin_func::{PyNativeFunction, PyNativeMethod}, + descriptor::PyMethodDescriptor, + }, + common::lock::PyMutex, + convert::ToPyObject, + function::{OptionalArg, PyMethodDef}, + object::{Traverse, TraverseFn}, + stdlib::sys::monitoring::{self, MonitoringEvent}, + types::{Constructor, DefaultConstructor, Initializer, PyStructSequence}, + }; + use std::collections::HashMap; + + const TOOL_ID: i32 = 2; + const CALLBACKS: &[(MonitoringEvent, &str)] = &[ + (MonitoringEvent::PyStart, "_pystart_callback"), + (MonitoringEvent::PyResume, "_pystart_callback"), + (MonitoringEvent::PyThrow, "_pythrow_callback"), + (MonitoringEvent::PyReturn, "_pyreturn_callback"), + (MonitoringEvent::PyYield, "_pyreturn_callback"), + (MonitoringEvent::PyUnwind, "_pyreturn_callback"), + (MonitoringEvent::Call, "_ccall_callback"), + (MonitoringEvent::CReturn, "_creturn_callback"), + (MonitoringEvent::CRaise, "_creturn_callback"), + ]; + + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + enum Key { + Python(usize), + Native(usize), + } + + #[derive(Clone, Debug, Default)] + struct Counts { + total: i128, + inline: i128, + calls: usize, + recursive: usize, + depth: isize, + } + + impl Counts { + fn stop(&mut self, total: i128, inline: i128) { + self.depth -= 1; + if self.depth == 0 { + self.total += total; + } else { + self.recursive += 1; + } + self.inline += inline; + self.calls += 1; + } + } + + #[derive(Clone, Debug)] + struct Entry { + code: PyObjectRef, + // Dynamically allocated native method definitions must outlive their keys. + owner: Option, + counts: Counts, + callees: HashMap, + } + + #[derive(Debug)] + struct Context { + key: Key, + start: i128, + subtime: i128, + } + + #[derive(Debug, Default)] + struct State { + entries: HashMap, + stack: Vec, + timer: Option, + unit: f64, + enabled: bool, + subcalls: bool, + builtins: bool, + in_timer: bool, + } + + #[pyattr] + #[pyclass(name = "Profiler", module = "_lsprof", traverse = "manual")] + #[derive(Debug, Default, PyPayload)] + struct Profiler { + state: PyMutex, + } + + // SAFETY: Each owned Python reference is visited exactly once. Contexts + // contain keys only; they do not own additional references to the entries. + unsafe impl Traverse for Profiler { + fn traverse(&self, tracer: &mut TraverseFn<'_>) { + if let Some(state) = self.state.try_lock() { + state.timer.traverse(tracer); + #[expect( + clippy::iter_over_hash_type, + reason = "GC tracing visits every owned reference regardless of entry order" + )] + for entry in state.entries.values() { + entry.code.traverse(tracer); + entry.owner.traverse(tracer); + } + } + } + + fn clear(&mut self, out: &mut Vec) { + let state = self.state.get_mut(); + out.extend(state.timer.take()); + #[expect( + clippy::iter_over_hash_type, + reason = "GC clearing transfers every owned reference; entry order is irrelevant" + )] + for (_, entry) in state.entries.drain() { + out.push(entry.code); + out.extend(entry.owner); + } + state.stack.clear(); + } + } + + #[derive(FromArgs)] + struct InitArgs { + #[pyarg(any, optional)] + timer: OptionalArg, + #[pyarg(any, default = 0.0)] + timeunit: f64, + #[pyarg(any, default = true)] + subcalls: bool, + #[pyarg(any, default = true)] + builtins: bool, + } + + #[derive(FromArgs)] + struct EnableArgs { + #[pyarg(any, default = true)] + subcalls: bool, + #[pyarg(any, default = true)] + builtins: bool, + } + + impl DefaultConstructor for Profiler {} + + impl Initializer for Profiler { + type Args = InitArgs; + + fn init(zelf: &Py, args: InitArgs, _vm: &VirtualMachine) -> PyResult<()> { + let old_timer = { + let mut state = zelf.state.lock(); + state.unit = args.timeunit; + state.subcalls = args.subcalls; + state.builtins = args.builtins; + core::mem::replace(&mut state.timer, args.timer.into_option()) + }; + drop(old_timer); + Ok(()) + } + } + + impl Profiler { + fn timer(&self, vm: &VirtualMachine) -> i128 { + let (timer, unit) = { + let mut state = self.state.lock(); + state.in_timer = true; + (state.timer.clone(), state.unit) + }; + // No profiler lock can be held across a custom timer: it may call + // getstats(), enable(), disable(), clear(), or a Python finalizer. + let result = if let Some(timer) = &timer { + timer.call((), vm).and_then(|value| { + if unit > 0.0 { + value.try_into_value::(vm).map(i128::from) + } else if let Some(value) = value.downcast_ref::() { + let nanos = (value.to_f64() * 1_000_000_000.0).floor(); + if nanos.is_nan() { + Err(vm.new_value_error("Invalid value NaN (not a number)")) + } else if !(i64::MIN as f64..-(i64::MIN as f64)).contains(&nanos) { + Err(vm + .new_overflow_error("timestamp too large to convert to C PyTime_t")) + } else { + Ok(nanos as i64 as i128) + } + } else { + let seconds = value.try_into_value::(vm)?; + seconds + .checked_mul(1_000_000_000) + .map(i128::from) + .ok_or_else(|| { + vm.new_overflow_error( + "timestamp too large to convert to C PyTime_t", + ) + }) + } + }) + } else { + super::super::time::profiler_time(vm).map(|nanos| nanos as i128) + }; + self.state.lock().in_timer = false; + match result { + Ok(time) => time, + Err(error) => { + let message = + timer + .as_ref() + .and_then(|timer| timer.repr(vm).ok()) + .map(|timer| { + format!("Exception ignored while calling _lsprof timer {timer}") + }); + vm.run_unraisable(error, message, vm.ctx.none()); + 0 + } + } + } + + fn enter( + &self, + key: Key, + code: PyObjectRef, + owner: Option, + vm: &VirtualMachine, + ) { + { + let mut state = self.state.lock(); + if state.in_timer { + return; + } + state + .entries + .entry(key) + .or_insert_with(|| Entry { + code, + owner, + counts: Counts::default(), + callees: HashMap::new(), + }) + .counts + .depth += 1; + let caller = state + .subcalls + .then(|| state.stack.last().map(|ctx| ctx.key)) + .flatten(); + if let Some(caller) = caller { + state + .entries + .get_mut(&caller) + .unwrap() + .callees + .entry(key) + .or_default() + .depth += 1; + } + state.stack.push(Context { + key, + start: 0, + subtime: 0, + }); + } + let start = self.timer(vm); + // clear()/disable() are rejected inside the timer, so the context + // remains valid even when the timer executes arbitrary Python. + if let Some(context) = self.state.lock().stack.last_mut() { + context.start = start; + } + } + + fn leave(&self, key: Key, vm: &VirtualMachine) { + { + let state = self.state.lock(); + if state.in_timer || state.stack.last().is_none_or(|ctx| ctx.key != key) { + return; + } + } + let now = self.timer(vm); + let mut state = self.state.lock(); + let Some(context) = state.stack.pop() else { + return; + }; + let total = now - context.start; + let inline = total - context.subtime; + if let Some(parent) = state.stack.last_mut() { + parent.subtime += total; + } + if let Some(entry) = state.entries.get_mut(&context.key) { + entry.counts.stop(total, inline); + } + let caller = state + .subcalls + .then(|| state.stack.last().map(|ctx| ctx.key)) + .flatten(); + if let Some(caller) = caller + && let Some(counts) = state + .entries + .get_mut(&caller) + .and_then(|entry| entry.callees.get_mut(&context.key)) + { + counts.stop(total, inline); + } + } + } + + // Builtin keys use the method definition, never the transient bound method + // object or its receiver. This both aggregates calls and avoids retaining + // every receiver that the application has used while profiling. + enum NativeCall<'a> { + Function(&'a PyNativeFunction), + Descriptor(&'a Py, &'a PyObject), + } + + impl NativeCall<'_> { + fn key(&self) -> Key { + let method = match self { + Self::Function(function) => function.value, + Self::Descriptor(descriptor, _) => descriptor.method, + }; + Key::Native(method as *const PyMethodDef as usize) + } + + fn owner(&self) -> Option { + match self { + Self::Function(function) => function._method_def_owner.clone(), + Self::Descriptor(descriptor, _) => descriptor._method_def_owner.clone(), + } + } + + fn label(&self, vm: &VirtualMachine) -> PyObjectRef { + let function = match self { + Self::Descriptor(descriptor, receiver) => { + let name = descriptor.method.name; + if let Some(attribute) = receiver.class().get_attr(vm.ctx.intern_str(name)) + && let Ok(label) = attribute.repr(vm) + { + return label.into(); + } + return vm.ctx.new_str(format!("")).into(); + } + Self::Function(function) => function, + }; + let name = function.value.name; + let module = function + .module + .deref() + .and_then(|module| module.downcast_ref::()) + .map(|module| module.to_string_lossy().into_owned()); + let label = if let Some(receiver) = function.get_self() { + if let Some(descriptor) = receiver.class().get_attr(vm.ctx.intern_str(name)) + && let Ok(label) = descriptor.repr(vm) + { + return label.into(); + } + if let Some(module) = module { + format!("") + } else { + format!("") + } + } else { + match module { + Some(module) if module != "builtins" => format!("<{module}.{name}>"), + _ => format!("<{name}>"), + } + }; + vm.ctx.new_str(label).into() + } + } + + fn native_call<'a>( + callable: &'a PyObject, + arg: &'a PyObject, + vm: &VirtualMachine, + ) -> Option> { + if let Some(descriptor) = callable.downcast_ref::() { + if arg.is(&monitoring::get_missing(vm)) || !arg.fast_isinstance(descriptor.common.typ) { + return None; + } + return Some(NativeCall::Descriptor(descriptor, arg)); + } + let function = if let Some(function) = callable.downcast_ref::() { + &**function + } else { + let method = callable.downcast_ref::()?; + &method.func + }; + Some(NativeCall::Function(function)) + } + + #[pyclass(with(Constructor, Initializer), flags(BASETYPE))] + impl Profiler { + #[pymethod] + fn enable(zelf: &Py, args: EnableArgs, vm: &VirtualMachine) -> PyResult<()> { + { + let mut state = zelf.state.lock(); + state.subcalls = args.subcalls; + state.builtins = args.builtins; + } + let monitoring = vm.sys_module.get_attr("monitoring", vm)?; + vm.call_method(&monitoring, "use_tool_id", (TOOL_ID, "cProfile"))?; + let mut events = 0; + for &(event, method) in CALLBACKS { + let callback = zelf.as_object().get_attr(method, vm)?; + vm.call_method( + &monitoring, + "register_callback", + (TOOL_ID, event.mask(), callback), + )?; + events |= event.mask(); + } + vm.call_method(&monitoring, "set_events", (TOOL_ID, events))?; + zelf.state.lock().enabled = true; + Ok(()) + } + + #[pymethod] + fn disable(zelf: &Py, vm: &VirtualMachine) -> PyResult<()> { + { + let state = zelf.state.lock(); + if state.in_timer { + return Err(vm.new_runtime_error("cannot disable profiler in external timer")); + } + if !state.enabled { + return Ok(()); + } + } + let monitoring = vm.sys_module.get_attr("monitoring", vm)?; + for &(event, _) in CALLBACKS { + vm.call_method( + &monitoring, + "register_callback", + (TOOL_ID, event.mask(), vm.ctx.none()), + )?; + } + vm.call_method(&monitoring, "set_events", (TOOL_ID, 0))?; + vm.call_method(&monitoring, "free_tool_id", (TOOL_ID,))?; + zelf.state.lock().enabled = false; + loop { + let key = zelf.state.lock().stack.last().map(|ctx| ctx.key); + let Some(key) = key else { + break; + }; + zelf.leave(key, vm); + } + Ok(()) + } + + #[pymethod] + fn clear(zelf: &Py, vm: &VirtualMachine) -> PyResult<()> { + let entries = { + let mut state = zelf.state.lock(); + if state.in_timer { + return Err(vm.new_runtime_error("cannot clear profiler in external timer")); + } + state.stack.clear(); + core::mem::take(&mut state.entries) + }; + drop(entries); + Ok(()) + } + + #[pymethod] + fn getstats(zelf: &Py, vm: &VirtualMachine) -> Vec { + let (entries, factor) = { + let state = zelf.state.lock(); + let factor = if state.timer.is_some() && state.unit != 0.0 { + state.unit + } else { + 1e-9 + }; + (state.entries.clone(), factor) + }; + entries + .values() + .filter(|entry| entry.counts.calls != 0) + .map(|entry| { + let calls = if entry.callees.is_empty() { + vm.ctx.none() + } else { + vm.ctx + .new_list( + entry + .callees + .iter() + .map(|(key, counts)| { + SubentryData { + code: entries[key].code.clone(), + callcount: counts.calls, + reccallcount: counts.recursive, + totaltime: counts.total as f64 * factor, + inlinetime: counts.inline as f64 * factor, + } + .to_pyobject(vm) + }) + .collect(), + ) + .into() + }; + EntryData { + code: entry.code.clone(), + callcount: entry.counts.calls, + reccallcount: entry.counts.recursive, + totaltime: entry.counts.total as f64 * factor, + inlinetime: entry.counts.inline as f64 * factor, + calls, + } + .to_pyobject(vm) + }) + .collect() + } + + #[pymethod] + fn _pystart_callback( + zelf: &Py, + code: PyObjectRef, + _offset: PyObjectRef, + vm: &VirtualMachine, + ) { + zelf.enter(Key::Python(code.get_id()), code, None, vm); + } + + #[pymethod] + fn _pythrow_callback( + zelf: &Py, + code: PyObjectRef, + _offset: PyObjectRef, + _exception: PyObjectRef, + vm: &VirtualMachine, + ) { + zelf.enter(Key::Python(code.get_id()), code, None, vm); + } + + #[pymethod] + fn _pyreturn_callback( + zelf: &Py, + code: PyObjectRef, + _offset: PyObjectRef, + _retval: PyObjectRef, + vm: &VirtualMachine, + ) { + zelf.leave(Key::Python(code.get_id()), vm); + } + + #[pymethod] + fn _ccall_callback( + zelf: &Py, + _code: PyObjectRef, + _offset: PyObjectRef, + callable: PyObjectRef, + arg: PyObjectRef, + vm: &VirtualMachine, + ) { + let builtins = zelf.state.lock().builtins; + if builtins && let Some(call) = native_call(&callable, &arg, vm) { + let key = call.key(); + let existing = zelf + .state + .lock() + .entries + .get(&key) + .map(|entry| entry.code.clone()); + let code = existing.unwrap_or_else(|| call.label(vm)); + zelf.enter(key, code, call.owner(), vm); + } + } + + #[pymethod] + fn _creturn_callback( + zelf: &Py, + _code: PyObjectRef, + _offset: PyObjectRef, + callable: PyObjectRef, + arg: PyObjectRef, + vm: &VirtualMachine, + ) { + let builtins = zelf.state.lock().builtins; + if builtins && let Some(call) = native_call(&callable, &arg, vm) { + zelf.leave(call.key(), vm); + } + } + } + + #[pystruct_sequence_data] + struct EntryData { + code: PyObjectRef, + callcount: usize, + reccallcount: usize, + totaltime: f64, + inlinetime: f64, + calls: PyObjectRef, + } + + #[pyattr] + #[pystruct_sequence(name = "profiler_entry", module = "_lsprof", data = "EntryData")] + struct ProfilerEntry; + + #[pyclass(with(PyStructSequence))] + impl ProfilerEntry {} + + #[pystruct_sequence_data] + struct SubentryData { + code: PyObjectRef, + callcount: usize, + reccallcount: usize, + totaltime: f64, + inlinetime: f64, + } + + #[pyattr] + #[pystruct_sequence(name = "profiler_subentry", module = "_lsprof", data = "SubentryData")] + struct ProfilerSubentry; + + #[pyclass(with(PyStructSequence))] + impl ProfilerSubentry {} +} diff --git a/crates/vm/src/stdlib/_signal.rs b/crates/vm/src/stdlib/_signal.rs index 79867ff522a..64c4a79e48b 100644 --- a/crates/vm/src/stdlib/_signal.rs +++ b/crates/vm/src/stdlib/_signal.rs @@ -79,10 +79,11 @@ pub(crate) mod _signal { } } - #[cfg(windows)] static WARN_ON_FULL_BUFFER: atomic::AtomicBool = atomic::AtomicBool::new(true); // Bounded pending-call storage: signal handlers cannot allocate or lock. + #[cfg(any(unix, windows))] + static WAKEUP_WRITE_ERRORS: [atomic::AtomicI32; 32] = [const { atomic::AtomicI32::new(0) }; 32]; #[cfg(windows)] static WAKEUP_SEND_ERRORS: [atomic::AtomicI32; 32] = [const { atomic::AtomicI32::new(0) }; 32]; @@ -357,8 +358,6 @@ pub(crate) mod _signal { #[pyfunction] fn set_wakeup_fd(args: SetWakeupFdArgs, vm: &VirtualMachine) -> PyResult { - // TODO: implement warn_on_full_buffer for the remaining wakeup write paths. - let _ = args.warn_on_full_buffer; let fd = cfg_select! { windows => args.fd.0, _ => args.fd, @@ -394,12 +393,10 @@ pub(crate) mod _signal { } let old_fd = WAKEUP.swap(fd, Ordering::Relaxed); + WARN_ON_FULL_BUFFER.store(args.warn_on_full_buffer, Ordering::Relaxed); #[cfg(windows)] - { - WARN_ON_FULL_BUFFER.store(args.warn_on_full_buffer, Ordering::Relaxed); - WAKEUP_IS_SOCKET.store(is_socket, Ordering::Relaxed); - } + WAKEUP_IS_SOCKET.store(is_socket, Ordering::Relaxed); #[cfg(windows)] if old_fd == INVALID_WAKEUP { @@ -547,57 +544,89 @@ pub(crate) mod _signal { signal::TRIGGERS[signum as usize].store(true, Ordering::Relaxed); signal::set_triggered(); - #[cfg(not(windows))] + #[cfg(target_os = "wasi")] host_signal::notify_signal(signum, WAKEUP.load(Ordering::Relaxed)); - #[cfg(windows)] - let error = host_signal::notify_signal( - signum, - WAKEUP.load(Ordering::Relaxed), - WAKEUP_IS_SOCKET.load(Ordering::Relaxed), - signal::get_sigint_event(), - ); - #[cfg(windows)] - if let Some(error) = error - && (WARN_ON_FULL_BUFFER.load(Ordering::Relaxed) || !error.is_would_block()) + #[cfg(any(unix, windows))] { - for pending in &WAKEUP_SEND_ERRORS { - if pending - .compare_exchange(0, error.0, Ordering::Relaxed, Ordering::Relaxed) - .is_ok() - { - // The VM may have handled the signal on another thread - // before the failed wakeup send completed. - signal::set_triggered(); - break; + let error = host_signal::notify_signal( + signum, + WAKEUP.load(Ordering::Relaxed), + #[cfg(windows)] + WAKEUP_IS_SOCKET.load(Ordering::Relaxed), + #[cfg(windows)] + signal::get_sigint_event(), + ); + if let Some(error) = error + && (WARN_ON_FULL_BUFFER.load(Ordering::Relaxed) || !error.is_would_block()) + { + let (errors, errno) = match error { + host_signal::WakeupError::Write(errno) => (&WAKEUP_WRITE_ERRORS, errno), + #[cfg(windows)] + host_signal::WakeupError::Send(errno) => (&WAKEUP_SEND_ERRORS, errno), + }; + for pending in errors { + if pending + .compare_exchange(0, errno, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + // The VM may have handled the signal on another thread + // before the failed wakeup write completed. + signal::set_triggered(); + break; + } } } } } - #[cfg(windows)] - pub(crate) fn wakeup_send_errors_pending() -> bool { - WAKEUP_SEND_ERRORS + #[cfg(any(unix, windows))] + pub(crate) fn wakeup_errors_pending() -> bool { + let pending = WAKEUP_WRITE_ERRORS .iter() - .any(|error| error.load(Ordering::Relaxed) != 0) + .any(|error| error.load(Ordering::Relaxed) != 0); + #[cfg(windows)] + let pending = pending + || WAKEUP_SEND_ERRORS + .iter() + .any(|error| error.load(Ordering::Relaxed) != 0); + pending } - #[cfg(windows)] - pub(crate) fn report_wakeup_send_errors(vm: &VirtualMachine) { - for pending in &WAKEUP_SEND_ERRORS { - let error = pending.swap(0, Ordering::Relaxed); - if error != 0 { - vm.run_unraisable( - host_signal::WakeupSendError(error) - .into_io_error() - .into_pyexception(vm), - Some( - "Exception ignored while trying to send to the signal wakeup fd".to_owned(), - ), - vm.ctx.none(), - ); + #[cfg(any(unix, windows))] + pub(crate) fn report_wakeup_errors(vm: &VirtualMachine) { + fn report( + errors: &[atomic::AtomicI32], + make_error: fn(i32) -> host_signal::WakeupError, + operation: &str, + vm: &VirtualMachine, + ) { + for pending in errors { + let errno = pending.swap(0, Ordering::Relaxed); + if errno != 0 { + vm.run_unraisable( + make_error(errno).into_io_error().into_pyexception(vm), + Some(format!( + "Exception ignored while trying to {operation} to the signal wakeup fd" + )), + vm.ctx.none(), + ); + } } } + report( + &WAKEUP_WRITE_ERRORS, + host_signal::WakeupError::Write, + "write", + vm, + ); + #[cfg(windows)] + report( + &WAKEUP_SEND_ERRORS, + host_signal::WakeupError::Send, + "send", + vm, + ); } /// Reset wakeup fd after fork in child process. @@ -605,6 +634,9 @@ pub(crate) mod _signal { #[cfg(unix)] pub(crate) fn clear_wakeup_fd_after_fork() { WAKEUP.store(INVALID_WAKEUP, Ordering::Relaxed); + for pending in &WAKEUP_WRITE_ERRORS { + pending.store(0, Ordering::Relaxed); + } } #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] diff --git a/crates/vm/src/stdlib/_sre.rs b/crates/vm/src/stdlib/_sre.rs index 2e24b1477cc..1cca04d1c0f 100644 --- a/crates/vm/src/stdlib/_sre.rs +++ b/crates/vm/src/stdlib/_sre.rs @@ -543,6 +543,7 @@ mod _sre { flags(HAS_WEAKREF) )] impl Py { + #[pymethod(name = "prefixmatch")] #[pymethod(name = "match")] fn py_match( zelf: PyRef, @@ -1161,6 +1162,7 @@ mod _sre { self.pattern.clone() } + #[pymethod(name = "prefixmatch")] #[pymethod(name = "match")] fn py_match(&self, vm: &VirtualMachine) -> PyResult>> { with_sre_str!(self.pattern, &self.string.clone(), vm, |s| { diff --git a/crates/vm/src/stdlib/_symtable.rs b/crates/vm/src/stdlib/_symtable.rs index c2d781dbba4..fc24ab8a5f4 100644 --- a/crates/vm/src/stdlib/_symtable.rs +++ b/crates/vm/src/stdlib/_symtable.rs @@ -3,8 +3,8 @@ pub(crate) use _symtable::module_def; #[pymodule] mod _symtable { use crate::{ - AsObject, Py, PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{PyBaseExceptionRef, PyDictRef, PyListRef, PyStr, PyUtf8StrRef}, + AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, + builtins::{PyBaseExceptionRef, PyDictRef, PyListRef, PyStr, PyStrRef, PyUtf8StrRef}, compiler, function::{ArgStrOrBytesLike, FsPath}, types::Representable, @@ -96,13 +96,26 @@ mod _symtable { #[pyattr] pub(super) const TYPE_TYPE_VARIABLE: i32 = 6; - #[pyfunction] - fn symtable( + #[derive(FromArgs)] + struct SymtableArgs { + #[pyarg(positional)] source: ArgStrOrBytesLike, + #[pyarg(positional)] filename: FsPath, + #[pyarg(positional)] startstr: PyUtf8StrRef, - vm: &VirtualMachine, - ) -> PyResult> { + #[pyarg(named, optional)] + module: Option, + } + + #[pyfunction] + fn symtable(args: SymtableArgs, vm: &VirtualMachine) -> PyResult> { + let SymtableArgs { + source, + filename, + startstr, + module, + } = args; let mode = startstr .as_str() .parse::() @@ -128,10 +141,23 @@ mod _symtable { "source code string cannot contain null bytes".into(), )); } - let symtable = compiler::compile_symtable(&source, mode, &filename).map_err(|err| { - let err = vm.new_syntax_error(&err, Some(&source)); - set_syntax_error_filename(err, &filename_obj, vm) - })?; + let module = module.filter(|module| !vm.is_none(module)); + let module = module + .map(|module| { + PyStrRef::try_from_object(vm, module.clone()).map_err(|_| { + vm.new_type_error(format!( + "symtable() argument 'module' must be str or None, not {}", + module.class().name() + )) + }) + }) + .transpose()?; + let symtable = vm + .compile_symtable_with_module(&source, mode, &filename, module.as_deref()) + .map_err(|err| { + let err = err.into_pyexception(vm, Some(&source)); + set_syntax_error_filename(err, &filename_obj, vm) + })?; Ok(to_py_symbol_table(symtable, vm)) } @@ -221,7 +247,12 @@ mod _symtable { impl Py { #[pygetset] fn name(&self) -> String { - self.symtable.name.to_string() + match self.symtable.typ { + CompilerScope::Lambda | CompilerScope::Comprehension => { + format!("<{}>", self.symtable.name) + } + _ => self.symtable.name.to_string(), + } } #[pygetset(name = "type")] diff --git a/crates/vm/src/stdlib/_sysconfigdata.rs b/crates/vm/src/stdlib/_sysconfigdata.rs index df25246e330..8b58de873ef 100644 --- a/crates/vm/src/stdlib/_sysconfigdata.rs +++ b/crates/vm/src/stdlib/_sysconfigdata.rs @@ -19,6 +19,13 @@ mod _sysconfigdata { let paths = &vm.state.config.paths; build_time_vars.set_item("prefix", paths.prefix.clone().to_pyobject(vm), vm)?; build_time_vars.set_item("exec_prefix", paths.exec_prefix.clone().to_pyobject(vm), vm)?; + if let Some(source_dir) = crate::getpath::source_directory(paths) { + build_time_vars.set_item( + "srcdir", + source_dir.to_string_lossy().as_ref().to_pyobject(vm), + vm, + )?; + } if let Some(stdlib_dir) = &paths.stdlib_dir { build_time_vars.set_item("LIBDEST", stdlib_dir.to_pyobject(vm), vm)?; } diff --git a/crates/vm/src/stdlib/_testcapi.rs b/crates/vm/src/stdlib/_testcapi.rs new file mode 100644 index 00000000000..c6820f4e1f6 --- /dev/null +++ b/crates/vm/src/stdlib/_testcapi.rs @@ -0,0 +1,391 @@ +// Native fixtures shared by the standard-library tests. This deliberately +// implements only the fixtures below, not CPython's complete C API test module. +pub(crate) use _testcapi::module_def; +mod call_buffer; +mod heap; +mod meth; +#[cfg(feature = "capi")] +mod monitoring; +mod runtime; +#[cfg(feature = "threading")] +mod threads; + +#[pymodule] +mod _testcapi { + use crate::{ + AsObject, Context, Py, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine, + builtins::{PyCode, PyInt, PyModule, PyStrRef, PyType, PyTypeRef, PyUtf8StrRef}, + function::{ArgBytesLike, ArgIntoBool, ItemDoc, PosArgs, PyMethodDef, PyMethodFlags}, + types::Constructor, + }; + use core::sync::atomic::Ordering; + + pub(crate) fn module_exec(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + __module_exec(vm, module); + super::call_buffer::extend_module(vm, module)?; + super::meth::extend_module(vm, module)?; + super::runtime::extend_module(vm, module)?; + #[cfg(feature = "threading")] + super::threads::extend_module(vm, module)?; + #[cfg(feature = "capi")] + super::monitoring::extend_module(vm, module)?; + Ok(()) + } + + #[pyattr] + const ONE: u8 = 1; + + #[pyattr(name = "HeapCCollection")] + fn heap_c_collection(vm: &VirtualMachine) -> PyTypeRef { + super::heap::heap_c_collection(vm) + } + + #[pyattr(name = "HeapCTypeMetaclassNullNew")] + fn heap_ctype_metaclass_null_new(vm: &VirtualMachine) -> PyTypeRef { + super::heap::heap_ctype_metaclass_null_new(vm) + } + + #[pyfunction] + fn pytype_fromspec_meta(meta: PyObjectRef, vm: &VirtualMachine) -> PyResult { + super::heap::pytype_fromspec_meta(meta, vm) + } + + #[pyfunction] + fn bad_get( + descriptor: PyObjectRef, + object: PyObjectRef, + class: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + super::heap::bad_get(descriptor, object, class, vm) + } + + #[cfg(feature = "threading")] + #[pyfunction] + fn run_in_subinterp(code: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { + if code.as_str().contains('\0') { + return Err(vm.new_value_error("embedded null character")); + } + crate::stdlib::_testinternalcapi::run_string_in_new_subinterp( + code.as_str(), + crate::vm::InterpreterConfig::LEGACY, + crate::vm::InterpreterWhence::LegacyCapi, + vm, + ) + } + + #[pyattr] + const INT_MIN: libc::c_int = libc::c_int::MIN; + #[pyattr] + const INT_MAX: libc::c_int = libc::c_int::MAX; + #[pyattr] + const UINT_MAX: libc::c_uint = libc::c_uint::MAX; + #[pyattr] + const LONG_MIN: libc::c_long = libc::c_long::MIN; + #[pyattr] + const LONG_MAX: libc::c_long = libc::c_long::MAX; + #[pyattr] + const ULONG_MAX: libc::c_ulong = libc::c_ulong::MAX; + #[pyattr] + const ULLONG_MAX: libc::c_ulonglong = libc::c_ulonglong::MAX; + #[pyattr] + const PY_SSIZE_T_MIN: isize = isize::MIN; + #[pyattr] + const PY_SSIZE_T_MAX: isize = isize::MAX; + #[pyattr] + const FLT_MAX: f64 = f32::MAX as f64; + #[pyattr] + const FLT_MIN: f64 = f32::MIN_POSITIVE as f64; + #[pyattr] + const DBL_MAX: f64 = f64::MAX; + #[pyattr] + const DBL_MIN: f64 = f64::MIN_POSITIVE; + #[pyattr(name = "nan_msb_is_signaling")] + const NAN_MSB_IS_SIGNALING: bool = f64::NAN.to_bits() & (1_u64 << 51) == 0; + + #[pyfunction] + fn code_offset_to_line(args: PosArgs, vm: &VirtualMachine) -> PyResult { + let [code, offset] = args.as_ref() else { + return Err(vm.new_type_error("code_offset_to_line takes 2 arguments")); + }; + let offset: i32 = + offset.try_index(vm)?.as_bigint().try_into().map_err(|_| { + vm.new_overflow_error("Python int too large to convert to C int32_t") + })?; + let code = code + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("first arg must be a code object"))?; + Ok(code.addr2line(offset)) + } + + #[pyfunction] + fn config_get(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { + // Expose configuration stored by this interpreter; other CPython-only + // configuration names have no corresponding runtime setting here. + let settings = &vm.state.config.settings; + let value = match name.as_str() { + "code_debug_ranges" => vm.ctx.new_bool(settings.code_debug_ranges).into(), + "isolated" => vm.ctx.new_bool(settings.isolated).into(), + "dev_mode" => vm.ctx.new_bool(settings.dev_mode).into(), + "faulthandler" => vm.ctx.new_bool(settings.faulthandler).into(), + "site_import" => vm.ctx.new_bool(settings.import_site).into(), + "inspect" => vm.ctx.new_bool(settings.inspect).into(), + "interactive" => vm.ctx.new_bool(settings.interactive).into(), + "write_bytecode" => vm.ctx.new_bool(settings.write_bytecode).into(), + "warn_default_encoding" => vm.ctx.new_bool(settings.warn_default_encoding).into(), + "thread_inherit_context" => vm.ctx.new_bool(settings.thread_inherit_context).into(), + "context_aware_warnings" => vm.ctx.new_bool(settings.context_aware_warnings).into(), + "optimization_level" => vm.ctx.new_int(settings.optimize).into(), + "verbose" => vm.ctx.new_int(settings.verbose).into(), + "bytes_warning" => vm.ctx.new_int(settings.bytes_warning).into(), + "lazy_imports" => vm.ctx.new_int(settings.lazy_imports).into(), + _ => { + return Err( + vm.new_value_error(format!("unknown config option name: {}", name.as_str())) + ); + } + }; + Ok(value) + } + + #[pyfunction(name = "PyTime_AsSecondsDouble")] + fn pytime_as_seconds_double(args: PosArgs, vm: &VirtualMachine) -> PyResult { + let [object] = args.as_ref() else { + return Err(vm.new_type_error(format!( + "function takes exactly 1 argument ({} given)", + args.as_ref().len() + ))); + }; + let Some(nanoseconds) = object.downcast_ref::() else { + return Err(vm.new_type_error(format!( + "expect int, got {}", + object.class().fully_qualified_name(vm)? + ))); + }; + let nanoseconds = nanoseconds + .as_bigint() + .try_into() + .map_err(|_| vm.new_overflow_error("int too big to convert"))?; + Ok(crate::stdlib::time::pytime::as_seconds_double(nanoseconds)) + } + + #[pyfunction] + fn type_get_version(ty: PyTypeRef) -> u32 { + ty.tp_version_tag().load(Ordering::Acquire) + } + + #[pyfunction] + fn type_assign_version(ty: PyTypeRef, vm: &VirtualMachine) -> i32 { + i32::from(ty.version_for_specialization(vm) != 0) + } + + #[pyfunction] + fn type_modified(ty: PyTypeRef) { + ty.modified(); + } + + #[pyfunction] + fn set_errno(value: i32) { + crate::host_env::os::set_errno(value); + } + + #[pyfunction(name = "PyImport_GetLazyImportsMode")] + fn get_lazy_imports_mode(vm: &VirtualMachine) -> &'static str { + if vm.state.lazy_imports.all.load(Ordering::Acquire) { + "all" + } else { + "normal" + } + } + + #[pyfunction(name = "PyImport_SetLazyImportsMode")] + fn set_lazy_imports_mode(mode: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<()> { + let all = match mode.as_str() { + "normal" => false, + "all" => true, + _ => return Err(vm.new_value_error("invalid mode")), + }; + vm.state.lazy_imports.all.store(all, Ordering::Release); + Ok(()) + } + + #[pyfunction(name = "PyImport_GetLazyImportsFilter")] + fn get_lazy_imports_filter(vm: &VirtualMachine) -> PyObjectRef { + vm.state + .lazy_imports + .filter() + .unwrap_or_else(|| vm.ctx.none()) + } + + #[pyfunction(name = "PyImport_SetLazyImportsFilter")] + fn set_lazy_imports_filter(filter: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let filter = if vm.is_none(&filter) { + None + } else { + if !filter.is_callable() { + return Err(vm.new_value_error("filter provided but is not callable")); + } + Some(filter) + }; + vm.state.lazy_imports.set_filter(filter); + Ok(()) + } + + #[pyfunction] + fn lazy_import_without_frame(name: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let lazy_import = vm.builtins.get_attr("__lazy_import__", vm)?; + let saved = crate::vm::thread::set_current_frame(core::ptr::null()); + let _restore = scopeguard::guard(saved, |frame| { + let _ = crate::vm::thread::set_current_frame(frame); + }); + lazy_import.call((name,), vm) + } + + #[cfg(all(unix, feature = "threading"))] + struct NativeWaiter { + state: alloc::sync::Arc<(std::sync::Mutex, std::sync::Condvar)>, + thread: std::thread::JoinHandle<()>, + } + + #[cfg(all(unix, feature = "threading"))] + static NATIVE_WAITER: std::sync::Mutex> = std::sync::Mutex::new(None); + + #[cfg(all(unix, feature = "threading"))] + #[pyfunction] + fn _spawn_pthread_waiter(vm: &VirtualMachine) -> PyResult<()> { + let mut slot = NATIVE_WAITER.lock().unwrap_or_else(|e| e.into_inner()); + if slot.is_some() { + return Err(vm.new_runtime_error("thread already running")); + } + let state = + alloc::sync::Arc::new((std::sync::Mutex::new(false), std::sync::Condvar::new())); + let child_state = state.clone(); + // A host thread with no Python thread state is intentional: the fixture + // exercises fork detection beyond threading.active_count(). + let thread = std::thread::Builder::new() + .spawn(move || { + let (lock, wake) = &*child_state; + let released = lock.lock().unwrap_or_else(|e| e.into_inner()); + drop(wake.wait_while(released, |released| !*released)); + }) + .map_err(|err| vm.new_runtime_error(err.to_string()))?; + *slot = Some(NativeWaiter { state, thread }); + Ok(()) + } + + #[cfg(all(unix, feature = "threading"))] + #[pyfunction] + fn _end_spawned_pthread(vm: &VirtualMachine) -> PyResult<()> { + let waiter = NATIVE_WAITER + .lock() + .unwrap_or_else(|e| e.into_inner()) + .take() + .ok_or_else(|| vm.new_runtime_error("call _spawn_pthread_waiter 1st"))?; + let (lock, wake) = &*waiter.state; + *lock.lock().unwrap_or_else(|e| e.into_inner()) = true; + wake.notify_one(); + vm.allow_threads(|| waiter.thread.join()) + .map_err(|_| vm.new_runtime_error("native waiter thread panicked")) + } + + // The `y` argument format used by CPython accepts buffers that do not + // require a release callback, and rejects embedded NUL bytes. + struct FatalMessage(Vec); + + impl TryFromObject for FatalMessage { + fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { + let slots = &obj.class().slots; + if slots.has_release_buffer.load() || slots.python_release_buffer.load() { + return Err(vm.new_type_error(format!( + "fatal_error() argument 1 must be read-only bytes-like object, not {}", + obj.class().name() + ))); + } + let buffer = ArgBytesLike::try_from_object(vm, obj)?; + let bytes = buffer.borrow_buf(); + if bytes.contains(&0) { + return Err(vm.new_value_error("embedded null byte")); + } + Ok(Self(bytes.to_vec())) + } + } + + #[derive(FromArgs)] + struct FatalErrorArgs { + message: FatalMessage, + #[pyarg(positional, default = false)] + release_gil: ArgIntoBool, + } + + #[pyfunction] + fn fatal_error(args: FatalErrorArgs, vm: &VirtualMachine) -> PyResult<()> { + // Capture the Python stack while attached. Fatal diagnostics and the + // abort itself must not depend on Python's replaceable stderr object. + let mut diagnostic = b"Fatal Python error: _testcapi_fatal_error_impl: ".to_vec(); + diagnostic.extend_from_slice(&args.message.0); + diagnostic.extend_from_slice( + b"\nPython runtime state: initialized\n\nStack (most recent call first):\n", + ); + let mut frame = vm.current_frame(); + while let Some(current) = frame { + let code = current.f_code(); + diagnostic.extend_from_slice( + format!( + " File \"{}\", line {} in {}\n", + code.source_path().as_wtf8(), + current.lineno(), + code.obj_name.as_wtf8() + ) + .as_bytes(), + ); + frame = current.f_back(vm); + } + let abort = || { + use std::io::Write; + let _ = std::io::stderr().write_all(&diagnostic); + rustpython_host_env::os::abort() + }; + if args.release_gil.into_bool() { + vm.allow_threads(abort) + } else { + abort() + } + } + + #[pyattr] + #[pyclass(module = "_testcapi", name = "DocStringUnrepresentableSignatureTest")] + #[derive(Debug, PyPayload)] + struct DocStringUnrepresentableSignatureTest; + + #[pyclass(with(Constructor))] + impl DocStringUnrepresentableSignatureTest { + #[extend_class] + fn extend_class(ctx: &Context, class: &'static Py) { + // CPython's fixture deliberately accepts any positional arguments; + // its text signature exists to test module-scope default lookup. + const WITH_DEFAULT: PyMethodDef = PyMethodDef::new_const( + "with_default", + DocStringUnrepresentableSignatureTest::with_default, + PyMethodFlags::METHOD.union(PyMethodFlags::VARARGS), + ItemDoc::static_text( + "with_default($self, /, x=ONE)\n--\n\nThis instance method has a default parameter value from the module scope.", + ), + ); + class.set_str_attr( + "with_default", + WITH_DEFAULT.to_proper_method(class, ctx), + ctx, + ); + } + + fn with_default(&self, _args: PosArgs) {} + } + + impl Constructor for DocStringUnrepresentableSignatureTest { + type Args = (); + + fn py_new(_cls: &Py, _args: (), _vm: &VirtualMachine) -> PyResult { + Ok(Self) + } + } +} diff --git a/crates/vm/src/stdlib/_testcapi/call_buffer.rs b/crates/vm/src/stdlib/_testcapi/call_buffer.rs new file mode 100644 index 00000000000..8d6be842a6d --- /dev/null +++ b/crates/vm/src/stdlib/_testcapi/call_buffer.rs @@ -0,0 +1,197 @@ +use crate::{ + Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, + builtins::{PyDict, PyDictRef, PyModule, PyStr, PyTuple, PyTupleRef, PyType}, + class::PyClassImpl, + common::atomic::{AtomicUsize, Ordering}, + function::{Callee, FuncArgs, ItemDoc, KwArgs, OptionalArg, PyMethodDef, PyMethodFlags}, + protocol::{BufferDescriptor, BufferFlags, BufferMethods, PyBuffer}, + types::{AsBuffer, Constructor}, +}; + +fn fastcall_args(args: &PyObject, vm: &VirtualMachine) -> PyResult> { + if vm.is_none(args) { + Ok(Vec::new()) + } else if let Some(args) = args.downcast_ref::() { + Ok(args.as_slice().to_vec()) + } else { + Err(vm.new_type_error("args must be None or a tuple")) + } +} + +fn check_keyword_names(names: &[PyObjectRef], vm: &VirtualMachine) -> PyResult<()> { + if names + .iter() + .any(|name| name.downcast_ref::().is_none()) + { + return Err(vm.new_type_error("keywords must be strings")); + } + Ok(()) +} + +// These adapters exercise the native VM call paths, not exported C-ABI symbols. +fn pyobject_vectorcall(vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult { + let (_module, callable, args, kwnames): (PyObjectRef, PyObjectRef, PyObjectRef, PyObjectRef) = + args.bind_for(vm, callee)?; + let args = fastcall_args(&args, vm)?; + let kwnames = if vm.is_none(&kwnames) { + None + } else { + Some( + kwnames + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("kwnames must be None or a tuple"))? + .as_slice(), + ) + }; + let nargs = args + .len() + .checked_sub(kwnames.map_or(0, <[PyObjectRef]>::len)) + .ok_or_else(|| vm.new_value_error("kwnames longer than args"))?; + if let Some(names) = kwnames { + // The VM's internal vectorcall conversion assumes string keys. + check_keyword_names(names, vm)?; + } + callable.vectorcall(args, nargs, kwnames, vm) +} + +fn pyobject_fastcalldict(vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult { + let (_module, callable, args, kwargs): (PyObjectRef, PyObjectRef, PyObjectRef, PyObjectRef) = + args.bind_for(vm, callee)?; + let mut args = fastcall_args(&args, vm)?; + let nargs = args.len(); + let mut names = Vec::new(); + if !vm.is_none(&kwargs) { + let kwargs = kwargs + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("kwnames must be None or a dict"))?; + for (key, value) in kwargs.items_vec() { + names.push(key); + args.push(value); + } + check_keyword_names(&names, vm)?; + } + let kwnames = (!names.is_empty()).then_some(names.as_slice()); + callable.vectorcall(args, nargs, kwnames, vm) +} + +fn pycfunction_call(vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult { + let (_module, callable, args, kwargs): ( + PyObjectRef, + PyObjectRef, + PyTupleRef, + OptionalArg, + ) = args.bind_for(vm, callee)?; + let kwargs = match kwargs { + OptionalArg::Missing => KwArgs::default(), + OptionalArg::Present(kwargs) => kwargs + .items_vec() + .into_iter() + .map(|(key, value)| { + let name = key + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("keywords must be strings"))?; + Ok((name.as_wtf8().to_owned(), value)) + }) + .collect::>()?, + }; + callable.call_with_args(FuncArgs::new(args.as_slice().to_vec(), kwargs), vm) +} + +#[pyclass(module = false, name = "testBufType")] +#[derive(Debug, PyPayload)] +struct TestBuf { + data: [u8; 4], + references: AtomicUsize, +} + +#[pyclass(with(Constructor, AsBuffer), flags(IMMUTABLETYPE))] +impl TestBuf { + #[pygetset] + fn references(zelf: &Py) -> usize { + zelf.references.load(Ordering::Relaxed) + } +} + +impl Constructor for TestBuf { + type Args = FuncArgs; + + fn py_new(_cls: &Py, _args: FuncArgs, _vm: &VirtualMachine) -> PyResult { + Ok(Self { + data: *b"test", + references: AtomicUsize::new(0), + }) + } +} + +static BUFFER_METHODS: BufferMethods = BufferMethods { + obj_bytes: |buffer| buffer.obj_as::().data.as_slice().into(), + obj_bytes_mut: |_| unreachable!("testBuf exports a read-only buffer"), + retain: |buffer| { + buffer + .obj_as::() + .references + .fetch_add(1, Ordering::Relaxed); + }, + release: |buffer| { + let previous = buffer + .obj_as::() + .references + .fetch_sub(1, Ordering::Relaxed); + debug_assert!(previous > 0); + }, +}; + +impl AsBuffer for TestBuf { + const RELEASE_BUFFER: bool = true; + + fn slot_as_buffer( + zelf: &PyObject, + flags: BufferFlags, + vm: &VirtualMachine, + ) -> PyResult { + flags.fill_info_check(true, vm)?; + Self::as_buffer(zelf.try_downcast_ref::(vm)?, vm) + } + + fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + Ok(PyBuffer::new( + zelf.to_owned().into(), + BufferDescriptor::simple(zelf.data.len(), true), + &BUFFER_METHODS, + )) + } +} + +pub(super) fn extend_module(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + module.set_attr("testBuf", TestBuf::make_static_type(), vm)?; + const METHODS: &[PyMethodDef] = &[ + PyMethodDef::new_raw_const( + "pyobject_vectorcall", + pyobject_vectorcall, + PyMethodFlags::FASTCALL, + ItemDoc::NONE, + ), + PyMethodDef::new_raw_const( + "pyobject_fastcalldict", + pyobject_fastcalldict, + PyMethodFlags::FASTCALL, + ItemDoc::NONE, + ), + PyMethodDef::new_raw_const( + "pycfunction_call", + pycfunction_call, + PyMethodFlags::VARARGS, + ItemDoc::NONE, + ), + ]; + for method in METHODS { + let function = method.build_bound_function(&vm.ctx, module.to_owned().into()); + drop( + function + .module + .store(Some(vm.ctx.new_str("_testcapi").into())), + ); + module.set_attr(method.name, function, vm)?; + } + Ok(()) +} diff --git a/crates/vm/src/stdlib/_testcapi/heap.rs b/crates/vm/src/stdlib/_testcapi/heap.rs new file mode 100644 index 00000000000..449ad934075 --- /dev/null +++ b/crates/vm/src/stdlib/_testcapi/heap.rs @@ -0,0 +1,210 @@ +use crate::{ + Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + builtins::{PyStrRef, PyType, PyTypeRef, type_::PyAttributes}, + class::add_operators, + function::FuncArgs, + object::{Traverse, TraverseFn}, + protocol::PySequence, + types::{PyTypeFlags, PyTypeSlots, fn_addr}, +}; + +// Like RustPython's tuple payload, the element storage is separately allocated +// and independent of the instance's managed dictionary and weakref cells. +#[pyclass(module = "_testcapi", name = "HeapCCollection", traverse = "manual")] +#[derive(Debug)] +struct HeapCCollection { + items: Box<[PyObjectRef]>, +} + +// This payload has no process-global Python class. Its allocation and typed +// access use the per-module class marker and the payload ID, respectively. +impl PyPayload for HeapCCollection { + fn class(ctx: &Context) -> &'static Py { + ctx.types.object_type + } + + fn try_downcast_from(obj: &PyObject, vm: &VirtualMachine) -> PyResult<()> { + if obj.downcastable::() { + Ok(()) + } else { + Err(vm.new_type_error("expected a HeapCCollection instance")) + } + } +} + +struct HeapCCollectionType; + +// SAFETY: every owned element is visited exactly once. Clearing takes ownership +// of all elements before the collector releases any child references. +unsafe impl Traverse for HeapCCollection { + fn traverse(&self, tracer: &mut TraverseFn<'_>) { + self.items.traverse(tracer); + } + + fn clear(&mut self, out: &mut Vec) { + out.extend(core::mem::take(&mut self.items).into_vec()); + } +} + +impl HeapCCollection { + const BASICSIZE: usize = crate::object::payload_offset::() + core::mem::size_of::(); + + fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + // Check the physical base chain, rather than a customizable MRO. Only + // classes created by this module can supply the native allocation root. + let mut base = Some(cls.clone()); + let mut native_base = false; + while let Some(current) = base { + if current.has_type_data::() { + native_base = true; + break; + } + base = current.base.load_owned(); + } + if !native_base + || cls.slots.basicsize != Self::BASICSIZE + || cls.slots.itemsize != core::mem::size_of::() + { + return Err(vm.new_type_error(format!( + "cannot create '{}' with HeapCCollection's native layout", + cls.name() + ))); + } + // The C fixture stores each positional argument and ignores keywords. + let payload = Self { + items: args.args.into_boxed_slice(), + }; + let dict = cls + .slots + .flags + .has_feature(PyTypeFlags::HAS_DICT) + .then(|| vm.ctx.new_dict()); + Ok(PyRef::new_ref(payload, cls, dict).into()) + } + + fn length(sequence: PySequence<'_>, vm: &VirtualMachine) -> PyResult { + let collection = sequence + .obj + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("expected a HeapCCollection instance"))?; + Ok(collection.items.len()) + } + + fn item(sequence: PySequence<'_>, index: isize, vm: &VirtualMachine) -> PyResult { + let collection = sequence + .obj + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("expected a HeapCCollection instance"))?; + let index = if index < 0 { + index + collection.items.len() as isize + } else { + index + }; + usize::try_from(index) + .ok() + .and_then(|index| collection.items.get(index)) + .cloned() + .ok_or_else(|| vm.new_index_error(format!("index {index} out of range"))) + } +} + +pub(crate) fn heap_c_collection(vm: &VirtualMachine) -> PyTypeRef { + let slots = PyTypeSlots { + flags: crate::types::AtomicPyTypeFlags::from_plain(PyTypeFlags::from_slice(&[ + PyTypeFlags::BASETYPE, + PyTypeFlags::HAVE_GC, + ])), + basicsize: HeapCCollection::BASICSIZE, + itemsize: core::mem::size_of::(), + ..PyTypeSlots::default() + }; + let class = vm.ctx.new_class( + Some("_testcapi"), + "HeapCCollection", + vm.ctx.types.object_type.to_owned(), + slots, + ); + class + .init_type_data(HeapCCollectionType) + .expect("new HeapCCollection type has no existing type data"); + class.slots.new.store(Some(HeapCCollection::slot_new)); + class + .slots + .as_sequence + .length + .store(Some(HeapCCollection::length)); + class + .slots + .as_sequence + .item + .store(Some(HeapCCollection::item)); + let new = Context::genesis() + .slot_new_wrapper + .build_bound_function(&vm.ctx, class.clone().into()); + class.set_attr(identifier!(vm, __new__), new.into()); + class.set_attr(identifier!(vm, __doc__), vm.ctx.none()); + add_operators(&class, &vm.ctx, &[]); + class +} + +pub(crate) fn heap_ctype_metaclass_null_new(vm: &VirtualMachine) -> PyTypeRef { + let slots = PyTypeSlots { + flags: crate::types::AtomicPyTypeFlags::from_plain(PyTypeFlags::from_slice(&[ + PyTypeFlags::DISALLOW_INSTANTIATION, + ])), + itemsize: vm.ctx.types.type_type.slots.itemsize, + ..PyTypeSlots::default() + }; + vm.ctx.new_class( + Some("_testcapi"), + "HeapCTypeMetaclassNullNew", + vm.ctx.types.type_type.to_owned(), + slots, + ) +} + +pub(crate) fn pytype_fromspec_meta(meta: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let meta = meta.downcast::().map_err(|_| { + vm.new_type_error("pytype_fromspec_meta: must be invoked with a type argument!") + })?; + if !meta.fast_issubclass(vm.ctx.types.type_type) { + return Err(vm.new_type_error(format!( + "Metaclass '{}' is not a subclass of 'type'.", + meta.name() + ))); + } + if meta.slots.new.load().is_some_and(|new| { + Some(fn_addr(new)) != vm.ctx.types.type_type.slots.new.load().map(fn_addr) + }) { + return Err(vm.new_type_error("Metaclasses with custom tp_new are not supported.")); + } + let mut attrs = PyAttributes::default(); + attrs.insert( + identifier!(vm, __module__), + vm.ctx.new_str("_testcapi").into(), + ); + PyType::new_heap( + "HeapCTypeViaMetaclass", + vec![vm.ctx.types.object_type.to_owned()], + attrs, + PyTypeSlots { + flags: crate::types::AtomicPyTypeFlags::from_plain(PyTypeFlags::HEAP_TYPE), + ..PyTypeSlots::default() + }, + meta, + &vm.ctx, + ) + .map_err(|message| vm.new_type_error(message)) +} + +pub(crate) fn bad_get( + descriptor: PyObjectRef, + _object: PyObjectRef, + class: PyObjectRef, + vm: &VirtualMachine, +) -> PyResult { + // Keep an owned reference across the call, which may remove the descriptor + // from its class and trigger collection of otherwise unreachable objects. + drop(class.call((), vm)?); + descriptor.repr(vm) +} diff --git a/crates/vm/src/stdlib/_testcapi/meth.rs b/crates/vm/src/stdlib/_testcapi/meth.rs new file mode 100644 index 00000000000..b83b5503b16 --- /dev/null +++ b/crates/vm/src/stdlib/_testcapi/meth.rs @@ -0,0 +1,210 @@ +use crate::{ + Context, Py, PyPayload, PyResult, VirtualMachine, + builtins::{PyModule, PyStaticMethod, PyType}, + class::PyClassImpl, + function::{Callee, FuncArgs, ItemDoc, PyMethodDef, PyMethodFlags}, + types::Constructor, +}; + +const MODULE: u8 = 0; +const INSTANCE: u8 = 1; +const CLASS: u8 = 2; +const STATIC: u8 = 3; +const VARARGS: u8 = 0; +const VARARGS_KEYWORDS: u8 = 1; +const O: u8 = 2; +const NOARGS: u8 = 3; +const FASTCALL: u8 = 4; +const FASTCALL_KEYWORDS: u8 = 5; + +const NAMES: [&str; 6] = [ + "meth_varargs", + "meth_varargs_keywords", + "meth_o", + "meth_noargs", + "meth_fastcall", + "meth_fastcall_keywords", +]; + +// These fixtures expose the arguments delivered by the native call machinery. +// Both its regular-call and vectorcall paths use FuncArgs in RustPython. +fn call( + vm: &VirtualMachine, + mut args: FuncArgs, + _callee: Callee, +) -> PyResult { + let name = NAMES[CONVENTION as usize]; + let owner = match BINDING { + MODULE => "_testcapi", + INSTANCE => "MethInstance", + CLASS => "MethClass", + STATIC => "MethStatic", + _ => unreachable!(), + }; + let zelf = if BINDING == STATIC { + vm.ctx.none() + } else { + if args.args.is_empty() { + return Err( + vm.new_type_error(format!("unbound method {owner}.{name}() needs an argument")) + ); + } + args.args.remove(0) + }; + let accepts_keywords = matches!(CONVENTION, VARARGS_KEYWORDS | FASTCALL_KEYWORDS); + if !accepts_keywords && !args.kwargs.is_empty() { + // METH_VARARGS uses the older, unqualified error spelling. + let qualified = if CONVENTION == VARARGS { + name.to_owned() + } else { + format!("{owner}.{name}") + }; + return Err(vm.new_type_error(format!("{qualified}() takes no keyword arguments"))); + } + let given = args.args.len(); + if CONVENTION == NOARGS { + if given != 0 { + return Err(vm.new_type_error(format!( + "{owner}.{name}() takes no arguments ({given} given)" + ))); + } + return Ok(zelf); + } + if CONVENTION == O { + if given != 1 { + return Err(vm.new_type_error(format!( + "{owner}.{name}() takes exactly one argument ({given} given)" + ))); + } + return Ok(vm.ctx.new_tuple(vec![zelf, args.args.remove(0)]).into()); + } + let positional = vm.ctx.new_tuple(args.args); + if accepts_keywords { + let keywords = vm.ctx.new_dict(); + for (name, value) in args.kwargs { + keywords.set_item(&*vm.ctx.new_str(name), value, vm)?; + } + Ok(vm + .ctx + .new_tuple(vec![zelf, positional.into(), keywords.into()]) + .into()) + } else { + Ok(vm.ctx.new_tuple(vec![zelf, positional.into()]).into()) + } +} + +const fn method() -> PyMethodDef { + let convention = match CONVENTION { + VARARGS => PyMethodFlags::VARARGS, + VARARGS_KEYWORDS => PyMethodFlags::VARARGS.union(PyMethodFlags::KEYWORDS), + O => PyMethodFlags::O, + NOARGS => PyMethodFlags::NOARGS, + FASTCALL => PyMethodFlags::FASTCALL, + FASTCALL_KEYWORDS => PyMethodFlags::FASTCALL.union(PyMethodFlags::KEYWORDS), + _ => unreachable!(), + }; + let binding = match BINDING { + MODULE => PyMethodFlags::EMPTY, + INSTANCE => PyMethodFlags::METHOD, + CLASS => PyMethodFlags::CLASS, + STATIC => PyMethodFlags::STATIC, + _ => unreachable!(), + }; + let doc = match (BINDING, CONVENTION) { + (CLASS, O) => ItemDoc::static_text("meth_o($type, object, /)\n--\n\n"), + (STATIC, O) => ItemDoc::static_text("meth_o(object, /)\n--\n\n"), + (_, O) => ItemDoc::static_text("meth_o($self, object, /)\n--\n\n"), + (CLASS, NOARGS) => ItemDoc::static_text("meth_noargs($type, /)\n--\n\n"), + (STATIC, NOARGS) => ItemDoc::static_text("meth_noargs()\n--\n\n"), + (_, NOARGS) => ItemDoc::static_text("meth_noargs($self, /)\n--\n\n"), + _ => ItemDoc::NONE, + }; + PyMethodDef::new_raw_const( + NAMES[CONVENTION as usize], + call::, + convention.union(binding), + doc, + ) +} + +const fn methods() -> [PyMethodDef; 6] { + [ + method::(), + method::(), + method::(), + method::(), + method::(), + method::(), + ] +} + +fn extend_class(ctx: &Context, class: &'static Py, methods: &'static [PyMethodDef]) { + for method in methods { + let value = if method.flags.contains(PyMethodFlags::STATIC) { + let function = method.build_staticmethod(ctx, class); + drop(function.module.store(Some(ctx.new_str("builtins").into()))); + PyStaticMethod::new(function.into()).into_ref(ctx).into() + } else { + method.to_proper_method(class, ctx) + }; + class.set_str_attr(method.name, value, ctx); + } +} + +/// Class with normal (instance) methods to test calling conventions +#[pyclass(module = false, name = "MethInstance")] +#[derive(Debug, PyPayload)] +struct MethInstance; + +/// Class with class methods to test calling conventions +#[pyclass(module = false, name = "MethClass")] +#[derive(Debug, PyPayload)] +struct MethClass; + +/// Class with static methods to test calling conventions +#[pyclass(module = false, name = "MethStatic")] +#[derive(Debug, PyPayload)] +struct MethStatic; + +macro_rules! implement_fixture { + ($class:ident, $binding:ident) => { + #[pyclass(with(Constructor), flags(IMMUTABLETYPE))] + impl $class { + #[extend_class] + fn extend_class(ctx: &Context, class: &'static Py) { + const METHODS: [PyMethodDef; 6] = methods::<$binding>(); + extend_class(ctx, class, &METHODS); + } + } + + impl Constructor for $class { + type Args = FuncArgs; + + fn py_new(_cls: &Py, _args: FuncArgs, _vm: &VirtualMachine) -> PyResult { + // PyType_GenericNew ignores both positional and keyword args. + Ok(Self) + } + } + }; +} + +implement_fixture!(MethInstance, INSTANCE); +implement_fixture!(MethClass, CLASS); +implement_fixture!(MethStatic, STATIC); + +pub(super) fn extend_module(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + module.set_attr("MethInstance", MethInstance::make_static_type(), vm)?; + module.set_attr("MethClass", MethClass::make_static_type(), vm)?; + module.set_attr("MethStatic", MethStatic::make_static_type(), vm)?; + const METHODS: [PyMethodDef; 6] = methods::(); + for method in &METHODS { + let function = method.build_bound_function(&vm.ctx, module.to_owned().into()); + drop( + function + .module + .store(Some(vm.ctx.new_str("_testcapi").into())), + ); + module.set_attr(method.name, function, vm)?; + } + Ok(()) +} diff --git a/crates/vm/src/stdlib/_testcapi/monitoring.rs b/crates/vm/src/stdlib/_testcapi/monitoring.rs new file mode 100644 index 00000000000..04edc7bf24a --- /dev/null +++ b/crates/vm/src/stdlib/_testcapi/monitoring.rs @@ -0,0 +1,193 @@ +use crate::{ + AsObject, Py, PyObject, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, + builtins::{PyBaseException, PyModule, PyStrRef, PyType}, + class::PyClassImpl, + common::lock::PyMutex, + function::{Callee, FuncArgs, ItemDoc, PyMethodDef, PyMethodFlags}, + stdlib::sys::monitoring_capi::{self, EventState, EventStateAccess}, + types::Constructor, +}; + +#[derive(Debug)] +struct CodeLikeState { + events: Vec, + version: u64, +} + +/// CodeLike objects +#[pyclass(module = "monitoring", name = "CodeLike")] +#[derive(Debug, PyPayload)] +struct CodeLike { + state: PyMutex, +} + +#[pyclass(with(Constructor), flags(IMMUTABLETYPE))] +impl CodeLike { + #[pyslot] + fn slot_str(zelf: &PyObject, vm: &VirtualMachine) -> PyResult { + let zelf = zelf.try_downcast_ref::(vm)?; + let state = zelf.state.lock(); + let mut parts = vec!["PyCodeLikeObject".to_owned()]; + parts.extend( + state + .events + .iter() + .map(|event| format!(" {}", event.active)), + ); + Ok(vm.ctx.new_str(parts.join(": "))) + } +} + +impl Constructor for CodeLike { + type Args = (i32,); + + fn py_new(_cls: &Py, (length,): Self::Args, vm: &VirtualMachine) -> PyResult { + let length = usize::try_from(length) + .map_err(|_| vm.new_value_error("event count must be non-negative"))?; + let mut events = Vec::new(); + events + .try_reserve_exact(length) + .map_err(|_| vm.new_memory_error("cannot allocate monitoring states"))?; + events.resize(length, EventState::default()); + Ok(Self { + state: PyMutex::new(CodeLikeState { events, version: 0 }), + }) + } +} + +struct CodeLikeEvent<'a> { + codelike: &'a CodeLike, + index: usize, +} + +impl EventStateAccess for CodeLikeEvent<'_> { + fn active(&self) -> u8 { + self.codelike.state.lock().events[self.index].active + } + + fn disable(&self, tool: usize) { + self.codelike.state.lock().events[self.index].active &= !(1 << tool); + } +} + +fn fire(vm: &VirtualMachine, mut args: FuncArgs, _callee: Callee) -> PyResult { + // Like the CPython fixtures, offset indexes the codelike's state array as + // well as supplying the instruction offset delivered to the callback. + let extra_count = match EVENT { + 0 | 1 => 0, + 4 => 2, + _ => 1, + }; + if !args.kwargs.is_empty() || args.args.len() != 3 + extra_count { + return Err(vm.new_type_error(format!("expected {} positional arguments", 2 + extra_count))); + } + args.args.remove(0); + let codelike = PyRef::::try_from_object(vm, args.args.remove(0))?; + let offset = i32::try_from_object(vm, args.args.remove(0))?; + let index = + usize::try_from(offset).map_err(|_| vm.new_value_error("offset must be non-negative"))?; + if index >= codelike.state.lock().events.len() { + return Err(vm.new_index_error("monitoring state index out of range")); + } + let event_state = CodeLikeEvent { + codelike: &codelike, + index, + }; + if matches!(EVENT, 11..=15 | 17) { + let exception = args.args.remove(0); + let exception = if vm.is_none(&exception) { + None + } else { + Some(PyRef::::try_from_object(vm, exception)?) + }; + vm.set_raised_exception(exception); + } else if EVENT == 5 { + let line = i32::try_from_object(vm, args.args.remove(0))?; + args.args.push(vm.ctx.new_int(line).into()); + } else if EVENT == 10 && vm.is_none(&args.args[0]) { + args.args.clear(); + } + let result = monitoring_capi::fire_event_with_state( + vm, + &event_state, + codelike.as_object(), + offset, + EVENT, + &args.args, + ); + let active = event_state.active(); + drop(vm.take_raised_exception()); + result?; + Ok(vm.ctx.new_int(active).into()) +} + +fn enter_scope(vm: &VirtualMachine, mut args: FuncArgs, _callee: Callee) -> PyResult { + if !args.kwargs.is_empty() || !(3..=4).contains(&args.args.len()) { + return Err(vm.new_type_error("expected a code-like and one or two event types")); + } + args.args.remove(0); + let codelike = PyRef::::try_from_object(vm, args.args.remove(0))?; + let events = args + .args + .into_iter() + .map(|arg| u8::try_from_object(vm, arg)) + .collect::>>()?; + let mut state = codelike.state.lock(); + let CodeLikeState { + events: states, + version, + } = &mut *state; + let states = states + .get_mut(..events.len()) + .ok_or_else(|| vm.new_value_error("code-like has too few monitoring states"))?; + monitoring_capi::enter_scope(vm, states, version, &events)?; + Ok(vm.ctx.none()) +} + +fn exit_scope(vm: &VirtualMachine, args: FuncArgs, _callee: Callee) -> PyResult { + if !args.kwargs.is_empty() || args.args.len() != 1 { + return Err(vm.new_type_error("expected no arguments")); + } + monitoring_capi::exit_scope(); + Ok(vm.ctx.none()) +} + +pub(super) fn extend_module(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + module.set_attr("CodeLike", CodeLike::make_static_type(), vm)?; + macro_rules! method { + ($name:literal, $function:expr) => { + PyMethodDef::new_raw_const($name, $function, PyMethodFlags::VARARGS, ItemDoc::NONE) + }; + } + const METHODS: &[PyMethodDef] = &[ + method!("fire_event_py_start", fire::<0>), + method!("fire_event_py_resume", fire::<1>), + method!("fire_event_py_return", fire::<2>), + method!("fire_event_py_yield", fire::<3>), + method!("fire_event_call", fire::<4>), + method!("fire_event_line", fire::<5>), + method!("fire_event_jump", fire::<7>), + method!("fire_event_branch_left", fire::<8>), + method!("fire_event_branch_right", fire::<9>), + method!("fire_event_stop_iteration", fire::<10>), + method!("fire_event_raise", fire::<11>), + method!("fire_event_exception_handled", fire::<12>), + method!("fire_event_py_unwind", fire::<13>), + method!("fire_event_py_throw", fire::<14>), + method!("fire_event_reraise", fire::<15>), + method!("fire_event_c_return", fire::<16>), + method!("fire_event_c_raise", fire::<17>), + method!("monitoring_enter_scope", enter_scope), + method!("monitoring_exit_scope", exit_scope), + ]; + for method in METHODS { + let function = method.build_bound_function(&vm.ctx, module.to_owned().into()); + drop( + function + .module + .store(Some(vm.ctx.new_str("_testcapi").into())), + ); + module.set_attr(method.name, function, vm)?; + } + Ok(()) +} diff --git a/crates/vm/src/stdlib/_testcapi/runtime.rs b/crates/vm/src/stdlib/_testcapi/runtime.rs new file mode 100644 index 00000000000..a1255f44853 --- /dev/null +++ b/crates/vm/src/stdlib/_testcapi/runtime.rs @@ -0,0 +1,188 @@ +use crate::{ + AsObject, Py, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine, + builtins::{PyBaseExceptionRef, PyBytes, PyModule, PyStr, PyStrRef, PyTraceback, PyType}, + function::{ItemDoc, PosArgs, PyMethodDef, PyMethodFlags}, +}; + +fn format_size( + chars: &mut core::iter::Peekable>, + vm: &VirtualMachine, +) -> PyResult> { + let mut value = None; + while let Some(character) = chars.peek().copied().filter(char::is_ascii_digit) { + chars.next(); + let size = value + .unwrap_or(0usize) + .checked_mul(10) + .and_then(|value| value.checked_add((character as u8 - b'0') as usize)) + .filter(|value| isize::try_from(*value).is_ok()) + .ok_or_else(|| vm.new_value_error("format field too big"))?; + value = Some(size); + } + Ok(value) +} + +fn format_type_name( + class: &Py, + alternate: bool, + vm: &VirtualMachine, +) -> PyResult { + let mut name = class.fully_qualified_name(vm)?; + if alternate { + let module = class.__module__(vm)?; + if let Some(module) = module + .downcast_ref::() + .and_then(|module| module.to_str()) + && !matches!(module, "builtins" | "__main__") + && name + .strip_prefix(module) + .is_some_and(|rest| rest.starts_with('.')) + { + name.replace_range(module.len()..=module.len(), ":"); + } + } + Ok(vm.ctx.new_str(name)) +} + +// The fixture supplies PyObject* varargs. Parse the C formatter's object +// conversions before using the ordinary string renderer for width/precision; +// notably a bare precision dot leaves precision unspecified in the C API. +fn format_unraisable_message( + format: &str, + objects: &[PyObjectRef], + vm: &VirtualMachine, +) -> PyResult { + if !format.is_ascii() { + return Err(vm.new_value_error("PyUnicode_FromFormatV requires an ASCII format")); + } + let mut chars = format.chars().peekable(); + let mut translated = String::new(); + let mut converted = Vec::new(); + let mut objects = objects.iter(); + while let Some(character) = chars.next() { + translated.push(character); + if character != '%' { + continue; + } + if chars.next_if_eq(&'%').is_some() { + translated.push('%'); + continue; + } + let mut left_justify = false; + let mut alternate = false; + loop { + match chars.peek() { + Some('-') => left_justify = true, + Some('#') => alternate = true, + Some('0') => {} + _ => break, + } + chars.next(); + } + let width = format_size(&mut chars, vm)?; + let precision = if chars.next_if_eq(&'.').is_some() { + format_size(&mut chars, vm)? + } else { + None + }; + let spec = chars + .next() + .ok_or_else(|| vm.new_value_error("incomplete format"))?; + let object = objects + .next() + .ok_or_else(|| vm.new_type_error("not enough format arguments"))?; + let value = match spec { + 'R' => object.repr(vm)?, + 'S' => object.str(vm)?, + 'A' => crate::stdlib::builtins::ascii(object.clone(), vm)?, + 'U' => PyStrRef::try_from_object(vm, object.clone())?, + 'T' => format_type_name(object.class(), alternate, vm)?, + 'N' => format_type_name(object.try_downcast_ref::(vm)?, alternate, vm)?, + 'p' if width.is_none() && precision.is_none() => { + vm.ctx.new_str(format!("0x{:x}", object.get_id())) + } + // Integer and character-pointer varargs cannot be represented by + // this PyObject*-only fixture without inventing C ABI behavior. + _ => return Err(vm.new_value_error("unsupported object format")), + }; + if left_justify { + translated.push('-'); + } + if let Some(width) = width { + translated.push_str(&width.to_string()); + } + if let Some(precision) = precision { + translated.push('.'); + translated.push_str(&precision.to_string()); + } + translated.push('s'); + converted.push(vm.ctx.new_str(value.as_wtf8()).into()); + } + let translated = vm.ctx.new_str(translated); + let values = vm.ctx.new_tuple(converted); + let message = crate::cformat::cformat_string(vm, translated.as_wtf8(), values.as_object())?; + Ok(vm.ctx.new_str(message)) +} + +fn err_formatunraisable(args: PosArgs, vm: &VirtualMachine) -> PyResult<()> { + let [exception, format, objects @ ..] = args.as_ref() else { + return Err(vm.new_type_error("err_formatunraisable requires at least 2 arguments")); + }; + if objects.len() > 10 { + return Err(vm.new_type_error("err_formatunraisable takes at most 12 arguments")); + } + let exception = PyBaseExceptionRef::try_from_object(vm, exception.clone())?; + let bytes = if vm.is_none(format) { + None + } else if let Some(format) = format.downcast_ref::() { + let encoded = vm + .state + .codec_registry + .encode_text(format.to_owned(), "utf-8", None, vm)?; + Some(encoded.as_bytes().to_vec()) + } else if let Some(bytes) = format.downcast_ref::() { + Some(bytes.as_bytes().to_vec()) + } else { + return Err(vm.new_type_error("format must be str, bytes or None")); + }; + let message = bytes.as_ref().and_then(|bytes| { + let end = bytes + .iter() + .position(|byte| *byte == 0) + .unwrap_or(bytes.len()); + core::str::from_utf8(&bytes[..end]) + .ok() + .and_then(|format| format_unraisable_message(format, objects, vm).ok()) + }); + // PyErr_FormatUnraisable attaches the calling frame when the exception + // did not already carry a traceback. + if exception.traceback().is_none() + && let Some(frame) = vm.current_frame() + { + let lasti = frame.lasti().saturating_sub(1); + let lineno = frame.current_location().line; + let traceback = PyTraceback::new(None, frame, (lasti * 2) as i32, lineno).into_ref(&vm.ctx); + exception.set_traceback(Some(traceback)); + } + vm.run_unraisable_with_message(exception, message, vm.ctx.none()); + Ok(()) +} + +pub(super) fn extend_module(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + const METHODS: &[PyMethodDef] = &[PyMethodDef::new_const( + "err_formatunraisable", + err_formatunraisable, + PyMethodFlags::VARARGS, + ItemDoc::NONE, + )]; + for method in METHODS { + let function = method.build_function(&vm.ctx); + drop( + function + .module + .store(Some(vm.ctx.new_str("_testcapi").into())), + ); + module.set_attr(method.name, function, vm)?; + } + Ok(()) +} diff --git a/crates/vm/src/stdlib/_testcapi/threads.rs b/crates/vm/src/stdlib/_testcapi/threads.rs new file mode 100644 index 00000000000..d6d206c876a --- /dev/null +++ b/crates/vm/src/stdlib/_testcapi/threads.rs @@ -0,0 +1,123 @@ +use crate::{ + Py, PyObjectRef, PyResult, VirtualMachine, + builtins::PyModule, + function::{ItemDoc, OptionalArg, PyMethodDef, PyMethodFlags}, + vm::thread::{attach_current_thread, release_current_thread, with_current_vm}, +}; +use std::sync::{Mutex, mpsc}; +use std::thread::JoinHandle; + +struct TemporaryThread { + interpreter_id: i64, + handle: JoinHandle<()>, +} + +static TEMPORARY_THREAD: Mutex> = Mutex::new(None); + +fn join_temporary_c_thread(vm: &VirtualMachine) -> PyResult<()> { + let thread = { + let mut pending = vm.allow_threads(|| { + TEMPORARY_THREAD + .lock() + .unwrap_or_else(|error| error.into_inner()) + }); + let Some(thread) = pending.as_ref() else { + return Err(vm.new_runtime_error("no temporary thread is running")); + }; + if thread.interpreter_id != vm.state.interpreter_id { + return Err(vm.new_runtime_error("temporary thread belongs to another interpreter")); + } + if thread.handle.thread().id() == std::thread::current().id() { + return Err(vm.new_runtime_error("cannot join the current thread")); + } + pending.take().unwrap() + }; + vm.allow_threads(|| thread.handle.join()) + .map_err(|_| vm.new_runtime_error("temporary thread panicked")) +} + +fn call_in_temporary_c_thread( + callback: PyObjectRef, + wait: OptionalArg, + vm: &VirtualMachine, +) -> PyResult<()> { + if !vm.state.allow_threads() { + return Err(vm.new_runtime_error("thread is not supported for isolated subinterpreters")); + } + if vm + .state + .finalizing + .load(core::sync::atomic::Ordering::Acquire) + { + return Err(vm.new_exception_msg( + vm.ctx.exceptions.python_finalization_error.to_owned(), + "can't create new thread at interpreter shutdown".into(), + )); + } + let (started, ready) = mpsc::sync_channel(1); + { + let mut pending = vm.allow_threads(|| { + TEMPORARY_THREAD + .lock() + .unwrap_or_else(|error| error.into_inner()) + }); + if pending.is_some() { + return Err(vm.new_runtime_error("temporary thread already running")); + } + let thread_vm = vm.new_thread(); + let builder = + crate::stdlib::_thread::apply_thread_stack_size(std::thread::Builder::new(), vm); + let handle = builder + .spawn(move || { + // Match the foreign-thread fixture: signal startup before attaching + // a Python thread state, then release that state after the callback. + let _ = started.send(()); + let state = attach_current_thread(|| thread_vm); + scopeguard::defer! { release_current_thread(state); } + with_current_vm(|vm| { + let callback = callback; + if let Err(exception) = callback.call((), vm) { + vm.print_exception(&exception); + } + }); + }) + .map_err(|_| vm.new_runtime_error("unable to start the thread"))?; + *pending = Some(TemporaryThread { + interpreter_id: vm.state.interpreter_id, + handle, + }); + } + vm.allow_threads(|| ready.recv()) + .map_err(|_| vm.new_runtime_error("temporary thread failed to start"))?; + if wait.unwrap_or(1) != 0 { + join_temporary_c_thread(vm)?; + } + Ok(()) +} + +pub(super) fn extend_module(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + const METHODS: &[PyMethodDef] = &[ + PyMethodDef::new_const( + "call_in_temporary_c_thread", + call_in_temporary_c_thread, + PyMethodFlags::VARARGS, + ItemDoc::NONE, + ), + PyMethodDef::new_const( + "join_temporary_c_thread", + join_temporary_c_thread, + PyMethodFlags::NOARGS, + ItemDoc::NONE, + ), + ]; + for method in METHODS { + let function = method.build_function(&vm.ctx); + drop( + function + .module + .store(Some(vm.ctx.new_str("_testcapi").into())), + ); + module.set_attr(method.name, function, vm)?; + } + Ok(()) +} diff --git a/crates/vm/src/stdlib/_testinternalcapi.rs b/crates/vm/src/stdlib/_testinternalcapi.rs index 9e368d4462b..608981302f7 100644 --- a/crates/vm/src/stdlib/_testinternalcapi.rs +++ b/crates/vm/src/stdlib/_testinternalcapi.rs @@ -31,19 +31,21 @@ mod _testinternalcapi { AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, atomic_func, builtins::{ - PyBytesRef, PyCode, PyDict, PyDictRef, PyStrRef, PyType, PyTypeRef, - descriptor::PyWrapper, + PyBytesRef, PyCapsule, PyCode, PyDict, PyDictRef, PyStr, PyStrRef, PyType, PyTypeRef, + PyUtf8Str, descriptor::PyWrapper, }, common::{hash::PyHash, lock::LazyLock}, dict_inner, frame::FrameObject, - function::{OptionalArg, PyComparisonValue}, + function::{ArgIntoBool, OptionalArg, PosArgs, PyComparisonValue}, object::{Traverse, TraverseFn}, protocol::{PyIterReturn, PyMappingMethods, PySequenceMethods}, + stdlib::time::pytime::{self, Round}, types::{ AsMapping, AsSequence, Comparable, IterNext, Iterable, PyComparisonOp, PyTypeFlags, SelfIter, }, + vm::crossinterp::{Fallback, SharedValue}, }; use std::collections::HashMap; @@ -69,7 +71,8 @@ mod _testinternalcapi { const SIZEOF_PYOBJECT: usize = core::mem::size_of::(); #[pyattr] - const SIZEOF_TIME_T: usize = 8; + #[cfg_attr(target_env = "musl", allow(deprecated))] + const SIZEOF_TIME_T: usize = core::mem::size_of::(); // JUMP_BACKWARD_INITIAL_VALUE + 1 #[pyattr] @@ -148,6 +151,107 @@ mod _testinternalcapi { crate::vm::crossinterp::code_returns_only_none(&code) } + static CROSSINTERP_DATA_TAG: u8 = 0; + + fn crossinterp_data_tag() -> *mut core::ffi::c_void { + core::ptr::from_ref(&CROSSINTERP_DATA_TAG).cast_mut().cast() + } + + unsafe extern "C" fn release_crossinterp_data(obj: *mut PyObject) { + // SAFETY: this is installed only as a PyCapsule destructor; the object + // remains alive for the duration of the call. + let Some(capsule) = (unsafe { &*obj }).downcast_ref::() else { + return; + }; + if capsule.context() != crossinterp_data_tag() { + return; + } + let data = capsule.pointer().cast::(); + capsule.set_pointer(core::ptr::null_mut()); + if !data.is_null() { + // SAFETY: get_crossinterp_data transferred this Box to the capsule. + // Clearing the pointer above prevents a second release. + drop(unsafe { Box::from_raw(data) }); + } + } + + #[derive(FromArgs)] + struct GetCrossinterpDataArgs { + obj: PyObjectRef, + #[pyarg(any, optional)] + mode: OptionalArg, + } + + #[pyfunction] + fn get_crossinterp_data(args: GetCrossinterpDataArgs, vm: &VirtualMachine) -> PyResult { + let mode_obj = args.mode.into_option().filter(|mode| !vm.is_none(mode)); + let mode = match &mode_obj { + None => "xidata", + Some(mode) => { + if !mode.downcastable::() { + return Err( + vm.new_type_error(format!("expected mode str, got {}", mode.repr(vm)?)) + ); + } + let mode = mode.try_downcast_ref::(vm)?.as_str(); + // The reference hook compares a NUL-terminated UTF-8 string. + mode.split('\0').next().unwrap_or_default() + } + }; + let data = match mode { + "" | "xidata" => SharedValue::from_object(&args.obj, Fallback::XidataOnly, vm), + "fallback" => SharedValue::from_object(&args.obj, Fallback::Full, vm), + "pickle" => SharedValue::from_pickle(&args.obj, vm), + "marshal" => SharedValue::from_marshal(&args.obj, vm), + "code" => { + let code = args.obj.downcast_ref::().ok_or_else(|| { + crate::stdlib::_interpreters::not_shareable_error( + vm, + format!( + "expected code, got {}", + args.obj.repr(vm).unwrap_or_else(|_| { + vm.ctx.new_str(args.obj.class().name().to_string()) + }) + ), + ) + })?; + SharedValue::from_code(code, vm) + } + "func" => SharedValue::from_function(&args.obj, vm), + "script" => SharedValue::from_script(&args.obj, false, vm), + "script-pure" => SharedValue::from_script(&args.obj, true, vm), + _ => { + return Err(vm.new_value_error(format!( + "unsupported mode {}", + mode_obj.as_ref().unwrap().repr(vm)? + ))); + } + }?; + let data = Box::into_raw(Box::new(data)).cast(); + let capsule = vm + .ctx + .new_capsule(data, None, Some(release_crossinterp_data)); + capsule.set_context(crossinterp_data_tag()); + Ok(capsule.into()) + } + + #[pyfunction] + fn restore_crossinterp_data(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let invalid = + || vm.new_value_error("PyCapsule_GetPointer called with invalid PyCapsule object"); + let capsule = obj.downcast_ref::().ok_or_else(invalid)?; + if capsule.name().is_some() + || capsule.context() != crossinterp_data_tag() + || capsule.pointer().is_null() + { + return Err(invalid()); + } + // SAFETY: only get_crossinterp_data creates capsules with this context. + // `obj` owns the capsule during the clone, so the payload cannot be freed. + let data = unsafe { &*capsule.pointer().cast::() }.clone(); + data.into_object(vm) + } + #[pyfunction] fn get_co_localskinds(code: PyRef, vm: &VirtualMachine) -> PyResult { let kinds = vm.ctx.new_dict(); @@ -266,27 +370,41 @@ mod _testinternalcapi { ) } - #[pyfunction] - fn run_in_subinterp_with_config( + #[derive(FromArgs)] + struct RunInSubinterpArgs { + #[pyarg(any)] code: PyStrRef, + #[pyarg(any)] config: PyObjectRef, - xi: OptionalArg, + #[pyarg(named, default = false)] + xi: ArgIntoBool, + } + + #[pyfunction] + fn run_in_subinterp_with_config( + args: RunInSubinterpArgs, vm: &VirtualMachine, ) -> PyResult { - if xi.unwrap_or(false) { - return Err(vm.new_runtime_error("cross-interpreter execution is not supported")); - } - let config = crate::stdlib::_interpreters::config_from_pyobject(&config, vm)?; - let source = code + let config = crate::stdlib::_interpreters::config_from_pyobject(&args.config, vm)?; + let source = args + .code .to_str() .ok_or_else(|| vm.new_value_error("surrogates not allowed in interpreter source"))?; + if source.contains('\0') { + return Err(vm.new_value_error("embedded null character")); + } + let whence = if args.xi.into_bool() { + crate::vm::InterpreterWhence::Xi + } else { + crate::vm::InterpreterWhence::Capi + }; #[cfg(feature = "threading")] { - run_string_in_new_subinterp(source, config, vm) + run_string_in_new_subinterp(source, config, whence, vm) } #[cfg(not(feature = "threading"))] { - let _ = (source, config); + let _ = (source, config, whence); Err(vm.new_runtime_error("isolated interpreters require threading")) } } @@ -461,49 +579,265 @@ mod _testinternalcapi { Ok(settings) } - #[pyfunction] - fn create_interpreter( + #[derive(FromArgs)] + struct CreateInterpreterArgs { + #[pyarg(any, optional)] config: OptionalArg, - _whence: OptionalArg, - vm: &VirtualMachine, - ) -> PyResult { - let cfg = match config.into_option() { - Some(obj) if !vm.is_none(&obj) => { - crate::stdlib::_interpreters::config_from_pyobject(&obj, vm)? + #[pyarg(named, default = 4)] + whence: i64, + } + + #[pyfunction] + fn create_interpreter(args: CreateInterpreterArgs, vm: &VirtualMachine) -> PyResult { + use crate::vm::{InterpreterConfig, InterpreterWhence}; + let whence = match args.whence { + 0 => InterpreterWhence::Unknown, + 2 => InterpreterWhence::LegacyCapi, + 3 => InterpreterWhence::Capi, + 4 => InterpreterWhence::Xi, + value => return Err(vm.new_value_error(format!("unsupported whence {value}"))), + }; + let config = args.config.into_option().filter(|obj| !vm.is_none(obj)); + let cfg = if matches!( + whence, + InterpreterWhence::Unknown | InterpreterWhence::LegacyCapi + ) { + if config.is_some() { + return Err(vm.new_value_error("got unexpected config")); } - _ => crate::vm::InterpreterConfig::ISOLATED, + InterpreterConfig::LEGACY + } else if let Some(config) = config { + crate::stdlib::_interpreters::config_from_pyobject(&config, vm)? + } else { + InterpreterConfig::ISOLATED }; #[cfg(feature = "threading")] { - let interp = crate::Interpreter::create_subinterpreter_from_vm(vm, cfg) - .map_err(|msg| crate::stdlib::_interpreters::interpreter_error(vm, msg))?; + let interp = + crate::Interpreter::create_subinterpreter_from_vm_with_whence(vm, cfg, whence) + .map_err(|msg| crate::stdlib::_interpreters::interpreter_error(vm, msg))?; Ok(crate::vm::runtime::store_owned_interpreter(interp)) } #[cfg(not(feature = "threading"))] { - let _ = cfg; + let _ = (cfg, whence); Err(vm.new_runtime_error("isolated interpreters require threading")) } } + #[pyfunction] + fn next_interpreter_id() -> i64 { + crate::vm::runtime::next_interpreter_id() + } + + #[derive(FromArgs)] + struct DestroyInterpreterArgs { + #[pyarg(any)] + id: PyObjectRef, + #[pyarg(any, default = false)] + basic: ArgIntoBool, + } + + #[pyfunction] + fn destroy_interpreter(args: DestroyInterpreterArgs, vm: &VirtualMachine) -> PyResult<()> { + use crate::stdlib::_interpreters::{interpreter_error, interpreter_not_found}; + use crate::vm::{crossinterp, runtime}; + let id = crate::stdlib::_interpreters::parse_id(&args.id, vm)?; + runtime::lookup_interpreter(id).ok_or_else(|| interpreter_not_found(vm, id))?; + if id == vm.state.interpreter_id { + return Err(interpreter_error( + vm, + "cannot destroy the current interpreter", + )); + } + if crossinterp::is_running(id) { + return Err(interpreter_error(vm, "interpreter running")); + } + #[cfg(feature = "threading")] + { + let interp = + runtime::take_owned_interpreter(id).ok_or_else(|| interpreter_not_found(vm, id))?; + if args.basic.into_bool() && interp.global_state.ready.load(Ordering::Acquire) { + // Exercise out-of-order VM-state destruction before ending the + // interpreter, as the basic C-API fixture does with thread states. + let first = interp.new_thread(); + let second = interp.new_thread(); + second.run(|_| drop(first)); + drop(second); + } + let _ = interp.finalize(None); + Ok(()) + } + #[cfg(not(feature = "threading"))] + { + let _ = args.basic; + Err(vm.new_runtime_error("isolated interpreters require threading")) + } + } + + #[derive(FromArgs)] + struct ExecInterpreterArgs { + #[pyarg(any)] + id: PyObjectRef, + #[pyarg(any)] + code: PyStrRef, + #[pyarg(named, default = false)] + main: ArgIntoBool, + } + + #[pyfunction] + fn exec_interpreter(args: ExecInterpreterArgs, vm: &VirtualMachine) -> PyResult { + let source = args + .code + .to_str() + .ok_or_else(|| vm.new_value_error("surrogates not allowed in interpreter source"))?; + if source.contains('\0') { + return Err(vm.new_value_error("embedded null character")); + } + let id = crate::stdlib::_interpreters::parse_id(&args.id, vm)?; + let state = crate::vm::runtime::lookup_interpreter(id) + .ok_or_else(|| crate::stdlib::_interpreters::interpreter_not_found(vm, id))?; + if !state.ready.load(Ordering::Acquire) { + return Err(crate::stdlib::_interpreters::interpreter_error( + vm, + "interpreter not ready", + )); + } + #[cfg(feature = "threading")] + { + if args.main.into_bool() { + crate::vm::crossinterp::with_interpreter(id, vm, |target| { + run_interpreter_source(source, target) + }) + } else if id == vm.state.interpreter_id { + vm.new_thread() + .run(|target| run_interpreter_source(source, target)) + } else { + let thread = crate::vm::runtime::owned_new_thread(id) + .ok_or_else(|| crate::stdlib::_interpreters::interpreter_not_found(vm, id))?; + thread.run(|target| run_interpreter_source(source, target)) + } + } + #[cfg(not(feature = "threading"))] + { + let _ = (source, args.main); + Err(vm.new_runtime_error("isolated interpreters require threading")) + } + } + + fn pytime_args<'a, const N: usize>( + args: &'a PosArgs, + vm: &VirtualMachine, + ) -> PyResult<&'a [PyObjectRef; N]> { + let values: &[PyObjectRef] = args.as_ref(); + values.try_into().map_err(|_| { + vm.new_type_error(format!( + "function takes exactly {} argument{} ({} given)", + N, + if N == 1 { "" } else { "s" }, + values.len() + )) + }) + } + + // PyArg_ParseTuple's "i" first converts to C long, then checks C int bounds. + fn pytime_c_int(object: &PyObject, vm: &VirtualMachine) -> PyResult { + let value: core::ffi::c_long = object + .try_index(vm)? + .as_bigint() + .try_into() + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C long"))?; + num_traits::ToPrimitive::to_i32(&value).ok_or_else(|| { + vm.new_overflow_error(if value < 0 { + "signed integer is less than minimum" + } else { + "signed integer is greater than maximum" + }) + }) + } + + fn pytime_i64(object: &PyObject, vm: &VirtualMachine) -> PyResult { + object + .try_index(vm)? + .as_bigint() + .try_into() + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C int64_t")) + } + + #[pyfunction(name = "_PyTime_FromSeconds")] + fn pytime_from_seconds(args: PosArgs, vm: &VirtualMachine) -> PyResult { + let [seconds] = pytime_args::<1>(&args, vm)?; + Ok(pytime::from_seconds(pytime_c_int(seconds, vm)?)) + } + + #[pyfunction(name = "_PyTime_FromSecondsObject")] + fn pytime_from_seconds_object(args: PosArgs, vm: &VirtualMachine) -> PyResult { + let [object, round] = pytime_args::<2>(&args, vm)?; + let round = Round::from_int(pytime_c_int(round, vm)?, vm)?; + pytime::from_seconds_object(object, round, vm) + } + + #[pyfunction(name = "_PyTime_AsMilliseconds")] + fn pytime_as_milliseconds(args: PosArgs, vm: &VirtualMachine) -> PyResult { + let [ns, round] = pytime_args::<2>(&args, vm)?; + let round = pytime_c_int(round, vm)?; + let ns = pytime_i64(ns, vm)?; + Ok(Round::from_int(round, vm)?.divide(ns, 1_000_000)) + } + + #[pyfunction(name = "_PyTime_AsMicroseconds")] + fn pytime_as_microseconds(args: PosArgs, vm: &VirtualMachine) -> PyResult { + let [ns, round] = pytime_args::<2>(&args, vm)?; + let round = pytime_c_int(round, vm)?; + let ns = pytime_i64(ns, vm)?; + Ok(Round::from_int(round, vm)?.divide(ns, 1_000)) + } + #[pyfunction(name = "_PyTime_AsTimespec")] - fn pytime_as_timespec(ns: i64) -> (i64, i64) { - let sec = ns.div_euclid(1_000_000_000); - let nsec = ns.rem_euclid(1_000_000_000); - (sec, nsec) + fn pytime_as_timespec(args: PosArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> { + let [ns] = pytime_args::<1>(&args, vm)?; + pytime::as_timespec(pytime_i64(ns, vm)?, false, vm) } #[pyfunction(name = "_PyTime_AsTimespec_clamp")] - fn pytime_as_timespec_clamp(ns: i64) -> (i64, i64) { - pytime_as_timespec(ns) + fn pytime_as_timespec_clamp(args: PosArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> { + let [ns] = pytime_args::<1>(&args, vm)?; + pytime::as_timespec(pytime_i64(ns, vm)?, true, vm) + } + + #[pyfunction(name = "_PyTime_AsTimeval")] + fn pytime_as_timeval(args: PosArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> { + let [ns, round] = pytime_args::<2>(&args, vm)?; + let round = Round::from_int(pytime_c_int(round, vm)?, vm)?; + pytime::as_timeval(pytime_i64(ns, vm)?, round, false, vm) } #[pyfunction(name = "_PyTime_AsTimeval_clamp")] - fn pytime_as_timeval_clamp(ns: i64, _rnd: OptionalArg) -> (i64, i64) { - let us = ns.div_euclid(1000); - let sec = us.div_euclid(1_000_000); - let usec = us.rem_euclid(1_000_000); - (sec, usec) + fn pytime_as_timeval_clamp(args: PosArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> { + let [ns, round] = pytime_args::<2>(&args, vm)?; + let round = Round::from_int(pytime_c_int(round, vm)?, vm)?; + pytime::as_timeval(pytime_i64(ns, vm)?, round, true, vm) + } + + #[pyfunction(name = "_PyTime_ObjectToTime_t")] + fn pytime_object_to_time_t(args: PosArgs, vm: &VirtualMachine) -> PyResult { + let [object, round] = pytime_args::<2>(&args, vm)?; + let round = Round::from_int(pytime_c_int(round, vm)?, vm)?; + pytime::object_to_time_t(object, round, vm) + } + + #[pyfunction(name = "_PyTime_ObjectToTimespec")] + fn pytime_object_to_timespec(args: PosArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> { + let [object, round] = pytime_args::<2>(&args, vm)?; + let round = Round::from_int(pytime_c_int(round, vm)?, vm)?; + pytime::object_to_denominator(object, 1_000_000_000, round, vm) + } + + #[pyfunction(name = "_PyTime_ObjectToTimeval")] + fn pytime_object_to_timeval(args: PosArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> { + let [object, round] = pytime_args::<2>(&args, vm)?; + let round = Round::from_int(pytime_c_int(round, vm)?, vm)?; + pytime::object_to_denominator(object, 1_000_000, round, vm) } #[pyfunction] @@ -1638,41 +1972,52 @@ fn sep_or_end(path: &[char], idx: usize, size: usize) -> bool { } #[cfg(feature = "threading")] -fn run_string_in_new_subinterp( +pub(crate) fn run_string_in_new_subinterp( source: &str, config: crate::vm::InterpreterConfig, + whence: crate::vm::InterpreterWhence, vm: &VirtualMachine, ) -> PyResult { - let interp = crate::Interpreter::create_subinterpreter_from_vm(vm, config).map_err(|msg| { - let cause = vm.new_runtime_error(msg.to_owned()); - let exc = - crate::stdlib::_interpreters::interpreter_error(vm, "sub-interpreter creation failed"); - exc.set_context(Some(cause)); - exc - })?; + let interp = crate::Interpreter::create_subinterpreter_from_vm_with_whence(vm, config, whence) + .map_err(|msg| { + let cause = vm.new_runtime_error(msg.to_owned()); + let exc = crate::stdlib::_interpreters::interpreter_error( + vm, + "sub-interpreter creation failed", + ); + exc.set_context(Some(cause)); + exc + })?; let id = crate::vm::runtime::store_owned_interpreter(interp); - let outcome = crate::vm::crossinterp::with_interpreter(id, vm, |target| { - match target.compile(source, crate::compiler::Mode::Exec, "") { - Ok(code) => { - let ns = target.main_namespace()?; - let scope = crate::scope::Scope::with_builtins(None, ns, target); - match target.run_code_obj(code, scope) { - Ok(_) => Ok(0), - Err(exc) => { - target.print_exception(&exc); - Ok(-1) - } + let outcome = { + let thread = crate::vm::runtime::owned_new_thread(id) + .ok_or_else(|| crate::stdlib::_interpreters::interpreter_not_found(vm, id))?; + thread.run(|target| run_interpreter_source(source, target)) + }; + let _ = crate::vm::runtime::destroy_owned_interpreter(id); + outcome +} + +#[cfg(feature = "threading")] +fn run_interpreter_source(source: &str, target: &VirtualMachine) -> PyResult { + match target.compile(source, crate::compiler::Mode::Exec, "") { + Ok(code) => { + let ns = target.main_namespace()?; + let scope = crate::scope::Scope::with_builtins(None, ns, target); + match target.run_code_obj(code, scope) { + Ok(_) => Ok(0), + Err(exc) => { + target.print_exception(&exc); + Ok(-1) } } - Err(err) => { - let exc = err.into_pyexception(target, Some(source)); - target.print_exception(&exc); - Ok(-1) - } } - }); - let _ = crate::vm::runtime::destroy_owned_interpreter(id); - outcome + Err(err) => { + let exc = err.into_pyexception(target, Some(source)); + target.print_exception(&exc); + Ok(-1) + } + } } #[cfg(feature = "codegen")] diff --git a/crates/vm/src/stdlib/_thread.rs b/crates/vm/src/stdlib/_thread.rs index d086c3f7636..a7a595e6d57 100644 --- a/crates/vm/src/stdlib/_thread.rs +++ b/crates/vm/src/stdlib/_thread.rs @@ -5,10 +5,13 @@ pub(crate) use _thread::after_fork_child; pub use _thread::get_ident; +#[cfg(feature = "capi")] +pub(crate) use _thread::apply_thread_stack_size; + #[cfg_attr(target_arch = "wasm32", allow(unused_imports))] pub(crate) use _thread::{ - CurrentFrameSlot, HandleEntry, RawRMutex, ShutdownEntry, get_all_current_frames, - init_main_thread_ident, module_def, + CurrentFrameSlot, HandleEntry, RawRMutex, ShutdownEntry, cleanup_thread_local_data, + get_all_current_frames, init_main_thread_ident, module_def, }; #[pymodule] @@ -864,7 +867,7 @@ pub(crate) mod _thread { /// thousand, so a size that holds a Python call chain in release holds /// three of its frames here — starting a thread at all needs six. The /// value `threading.stack_size()` reports is untouched. - fn apply_thread_stack_size( + pub(crate) fn apply_thread_stack_size( thread_builder: thread::Builder, vm: &VirtualMachine, ) -> thread::Builder { @@ -885,7 +888,7 @@ pub(crate) mod _thread { /// Clean up thread-local data for the current thread. /// This triggers __del__ on objects stored in thread-local variables. - fn cleanup_thread_local_data() { + pub(crate) fn cleanup_thread_local_data() { // Move all guards out before dropping them. A local dict's __del__ may // re-enter thread-local access and borrow LOCAL_GUARDS again. let guards = LOCAL_GUARDS.take(); diff --git a/crates/vm/src/stdlib/_warnings.rs b/crates/vm/src/stdlib/_warnings.rs index ddbb38bbdc3..c92fd559b16 100644 --- a/crates/vm/src/stdlib/_warnings.rs +++ b/crates/vm/src/stdlib/_warnings.rs @@ -21,7 +21,7 @@ pub fn warn( mod _warnings { use crate::{ AsObject, PyObject, PyObjectRef, PyResult, VirtualMachine, - builtins::{PyDict, PyDictRef, PyListRef, PyStrRef, PyTupleRef, PyTypeRef}, + builtins::{PyAnyDict, PyDictRef, PyListRef, PyStrRef, PyTupleRef, PyTypeRef}, convert::TryFromObject, function::OptionalArg, }; @@ -180,10 +180,10 @@ mod _warnings { let source_line = if let Some(mg) = args.module_globals.into_option() { if vm.is_none(&mg) { None - } else if mg.downcast_ref::().is_none() { + } else if PyAnyDict::from_object(&mg).is_none() { return Err(vm.new_type_error(format!( - "module_globals must be a dict, not '{}'", - mg.class().name() + "module_globals must be a dict or a frozendict, not {}", + mg.class().fully_qualified_name(vm)? ))); } else { crate::warn::get_source_line(&mg, args.lineno, vm)? diff --git a/crates/vm/src/stdlib/builtins.rs b/crates/vm/src/stdlib/builtins.rs index 96b6db38ab3..38a642ab1de 100644 --- a/crates/vm/src/stdlib/builtins.rs +++ b/crates/vm/src/stdlib/builtins.rs @@ -680,8 +680,18 @@ mod builtins { if let Some(code) = crate::frame::current_code() { opts.future_features = code.flags & bytecode::CodeFlags::FUTURE_MASK; } - vm.compile_with_opts(source, mode, "", opts) - .map_err(|err| err.into_pyexception(vm, Some(source)))? + let module = scope + .globals + .inner_getitem_opt(identifier!(vm, __name__), vm)?; + vm.compile_with_opts_and_module( + source, + mode, + "", + opts, + module.as_deref(), + &[], + ) + .map_err(|err| err.into_pyexception(vm, Some(source)))? } #[cfg(not(feature = "rustpython-compiler"))] Either::B(_) => return Err(vm.new_type_error(CODEGEN_NOT_SUPPORTED)), diff --git a/crates/vm/src/stdlib/mod.rs b/crates/vm/src/stdlib/mod.rs index b1442645b7f..f6aecc55765 100644 --- a/crates/vm/src/stdlib/mod.rs +++ b/crates/vm/src/stdlib/mod.rs @@ -5,8 +5,9 @@ mod _codecs; mod _collections; pub(crate) use _collections::ordered_dict::ordered_dict::PyOrderedDict; mod _functools; -mod _imp; +pub(crate) mod _imp; pub mod _io; +mod _lsprof; mod _operator; mod _sre; mod _stat; @@ -15,6 +16,8 @@ mod _string; mod _symtable; mod _sysconfig; mod _sysconfigdata; +#[cfg(all(feature = "capi", feature = "host_env"))] +mod _testcapi; pub(crate) mod _testinternalcapi; mod _types; pub mod _typing; @@ -83,6 +86,14 @@ mod winsound; use crate::{Context, builtins::PyModuleDef}; +// Register this only from an executable or embedding library that actually +// links the C API. Cargo feature unification alone does not establish that. +#[cfg(all(feature = "capi", feature = "host_env"))] +#[must_use] +pub fn capi_test_module_def(ctx: &Context) -> &'static PyModuleDef { + _testcapi::module_def(ctx) +} + /// Returns module definitions for multi-phase init modules. /// /// These modules use multi-phase initialization pattern: @@ -113,6 +124,7 @@ pub fn builtin_module_defs(ctx: &Context) -> Vec<&'static PyModuleDef> { gc::module_def(ctx), _imp::module_def(ctx), _io::module_def(ctx), + _lsprof::module_def(ctx), itertools::module_def(ctx), marshal::module_def(ctx), #[cfg(all(feature = "host_env", windows))] diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index 6051723b19a..1f8da1c7d59 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -1,4 +1,6 @@ pub(crate) mod monitoring; +#[cfg(feature = "capi")] +pub use monitoring::capi as monitoring_capi; use crate::{Py, PyPayload, PyResult, VirtualMachine, builtins::PyModule, convert::ToPyObject}; @@ -299,6 +301,11 @@ pub mod sys { vm.state.config.paths.stdlib_dir.clone().to_pyobject(vm) } + #[pyattr] + fn _is_python_build(vm: &VirtualMachine) -> bool { + crate::getpath::source_directory(&vm.state.config.paths).is_some() + } + // alphabetical order with segments of pyattr and others #[pyattr] @@ -647,6 +654,16 @@ pub mod sys { .to_owned() } + #[pyattr] + fn abi_info(vm: &VirtualMachine) -> PyRef { + py_namespace!(vm, { + "pointer_bits" => vm.ctx.new_int(usize::BITS), + "free_threaded" => vm.ctx.new_bool(true), + "debug" => vm.ctx.new_bool(false), + "byteorder" => byteorder(vm), + }) + } + #[pyattr] fn _base_executable(vm: &VirtualMachine) -> String { vm.state.config.paths.base_executable.clone() @@ -1632,12 +1649,12 @@ pub mod sys { dev_mode: bool, /// -X utf8 utf8_mode: u8, - /// -X int_max_str_digits=number - int_max_str_digits: i64, - /// -P, `PYTHONSAFEPATH` - safe_path: bool, /// -X warn_default_encoding, PYTHONWARNDEFAULTENCODING warn_default_encoding: u8, + /// -P, `PYTHONSAFEPATH` + safe_path: bool, + /// -X int_max_str_digits=number + int_max_str_digits: i64, } impl FlagsData { @@ -1680,13 +1697,18 @@ pub mod sys { } #[pygetset] - fn context_aware_warnings(&self, vm: &VirtualMachine) -> bool { - vm.state.config.settings.context_aware_warnings + const fn gil(&self) -> u8 { + 0 + } + + #[pygetset] + fn context_aware_warnings(&self, vm: &VirtualMachine) -> u8 { + vm.state.config.settings.context_aware_warnings as u8 } #[pygetset] - fn thread_inherit_context(&self, vm: &VirtualMachine) -> bool { - vm.state.config.settings.thread_inherit_context + fn thread_inherit_context(&self, vm: &VirtualMachine) -> u8 { + vm.state.config.settings.thread_inherit_context as u8 } #[pygetset] @@ -1707,9 +1729,9 @@ pub mod sys { impl ThreadInfoData { const INFO: Self = Self { name: crate::stdlib::_thread::_thread::PYTHREAD_NAME, - // As I know, there's only way to use lock as "Mutex" in Rust - // with satisfying python document spec. - lock: Some("mutex+cond"), + // RustPython uses parking_lot rather than CPython's PyMutex. + // None denotes a lock implementation without a Python ABI name. + lock: None, version: None, }; } diff --git a/crates/vm/src/stdlib/sys/monitoring.rs b/crates/vm/src/stdlib/sys/monitoring.rs index cf2b13c78fa..a2f8570e4a2 100644 --- a/crates/vm/src/stdlib/sys/monitoring.rs +++ b/crates/vm/src/stdlib/sys/monitoring.rs @@ -7,6 +7,9 @@ use core::sync::atomic::Ordering; use crossbeam_utils::atomic::AtomicCell; use std::collections::{HashMap, HashSet}; +#[cfg(feature = "capi")] +pub mod capi; + pub(crate) const TOOL_LIMIT: usize = 6; const EVENTS_COUNT: usize = 19; const UNGROUPED_EVENTS_COUNT: usize = 16; @@ -94,6 +97,8 @@ const EVENT_NAMES: [&str; EVENTS_COUNT] = [ /// Interpreter-level monitoring state, shared by all threads. pub struct MonitoringState { + #[cfg(feature = "capi")] + native_version: u64, pub tool_names: [Option; TOOL_LIMIT], pub global_events: [u32; TOOL_LIMIT], pub local_events: HashMap<(usize, usize), u32>, @@ -112,6 +117,8 @@ pub struct MonitoringState { impl Default for MonitoringState { fn default() -> Self { Self { + #[cfg(feature = "capi")] + native_version: 1, tool_names: Default::default(), global_events: [0; TOOL_LIMIT], local_events: HashMap::new(), @@ -651,6 +658,10 @@ fn release_tool_callbacks(tool_id: i32, free: bool, vm: &VirtualMachine) -> PyRe let callbacks = { let mut state = vm.state.monitoring.lock(); let callbacks = if state.tool_names[tool].is_some() { + #[cfg(feature = "capi")] + { + state.native_version += 1; + } state.global_events[tool] = 0; state .local_events @@ -740,6 +751,10 @@ fn set_events(tool_id: i32, event_set: i32, vm: &VirtualMachine) -> PyResult<()> check_tool_in_use(tool, vm)?; let normalized = normalize_event_set(event_set, false, vm)?; let mut state = vm.state.monitoring.lock(); + #[cfg(feature = "capi")] + if state.global_events[tool] != normalized { + state.native_version += 1; + } state.global_events[tool] = normalized; update_events_mask(vm, &state); Ok(()) @@ -785,6 +800,10 @@ fn set_local_events( fn restart_events(vm: &VirtualMachine) { let mut state = vm.state.monitoring.lock(); state.disabled.clear(); + #[cfg(feature = "capi")] + { + state.native_version += 1; + } } fn all_events(vm: &VirtualMachine) -> PyResult { diff --git a/crates/vm/src/stdlib/sys/monitoring/capi.rs b/crates/vm/src/stdlib/sys/monitoring/capi.rs new file mode 100644 index 00000000000..2a0d0e9e862 --- /dev/null +++ b/crates/vm/src/stdlib/sys/monitoring/capi.rs @@ -0,0 +1,176 @@ +use super::{MonitoringEvent, TOOL_LIMIT, call_instrument, cannot_disable, is_instrumented_event}; +use crate::{ + AsObject, PyObject, PyObjectRef, PyResult, VirtualMachine, builtins::PyTuple, + function::FuncArgs, +}; + +/// Native event state with the layout of CPython's PyMonitoringState. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct EventState { + pub active: u8, + pub opaque: u8, +} + +/// Refresh native event states only when the global event configuration changes. +pub fn enter_scope( + vm: &VirtualMachine, + states: &mut [EventState], + version: &mut u64, + event_types: &[u8], +) -> PyResult<()> { + if states.len() != event_types.len() + || event_types.iter().any(|&event| { + MonitoringEvent::from_id(event as usize) + .is_none_or(|event| event == MonitoringEvent::Branch) + }) + { + return Err(vm.new_value_error("invalid monitoring scope events")); + } + let monitoring = vm.state.monitoring.lock(); + if *version == monitoring.native_version { + return Ok(()); + } + for (state, &event) in states.iter_mut().zip(event_types) { + let check_event = match event { + 16 | 17 => MonitoringEvent::Call as u8, + event => event, + }; + state.active = 0; + for tool in 0..TOOL_LIMIT { + if monitoring.global_events[tool] & (1 << check_event) != 0 { + state.active |= 1 << tool; + } + } + } + *version = monitoring.native_version; + Ok(()) +} + +/// The current API does not keep a stack of scopes. +pub fn exit_scope() {} + +/// Fire a native event using the interpreter's monitoring callbacks. +/// Exception events consume the raised exception during callbacks and restore it +/// on success. A callback error replaces it, as for the C exception indicator. +/// +/// # Safety +/// `state` must remain valid throughout the call. It is accessed through a raw +/// pointer so a Python callback may refresh the same state with EnterScope. +pub unsafe fn fire_event( + vm: &VirtualMachine, + state: *mut EventState, + codelike: &PyObject, + offset: i32, + event: u8, + extra: &[PyObjectRef], +) -> PyResult<()> { + struct NativeState(*mut EventState); + impl EventStateAccess for NativeState { + fn active(&self) -> u8 { + unsafe { (*self.0).active } + } + + fn disable(&self, tool: usize) { + unsafe { (*self.0).active &= !(1 << tool) }; + } + } + fire_event_with_state(vm, &NativeState(state), codelike, offset, event, extra) +} + +pub(crate) trait EventStateAccess { + fn active(&self) -> u8; + fn disable(&self, tool: usize); +} + +// The native ABI and thread-safe test fixtures update their actual state after +// each callback, so subsequent callbacks can observe an earlier DISABLE. +pub(crate) fn fire_event_with_state( + vm: &VirtualMachine, + state: &impl EventStateAccess, + codelike: &PyObject, + offset: i32, + event: u8, + extra: &[PyObjectRef], +) -> PyResult<()> { + let tools = state.active(); + if tools == 0 { + return Ok(()); + } + let event = MonitoringEvent::from_id(event as usize) + .ok_or_else(|| vm.new_value_error("invalid monitoring event"))?; + let exception = if matches!( + event, + MonitoringEvent::PyThrow + | MonitoringEvent::Raise + | MonitoringEvent::Reraise + | MonitoringEvent::ExceptionHandled + | MonitoringEvent::PyUnwind + | MonitoringEvent::CRaise + ) { + Some(vm.take_raised_exception().ok_or_else(|| { + vm.new_value_error(format!( + "Firing event {} with no exception set", + event as usize + )) + })?) + } else { + None + }; + if offset < 0 { + return Err(vm.new_value_error("offset must be non-negative")); + } + let mut args = vec![codelike.to_owned()]; + if event != MonitoringEvent::Line { + args.push(vm.ctx.new_int(offset).into()); + } + if let Some(exception) = &exception { + args.push(exception.clone().into()); + } else if event == MonitoringEvent::StopIteration { + let exception = match extra.first() { + Some(value) if value.fast_isinstance(vm.ctx.exceptions.stop_iteration) => value.clone(), + value => { + // PyErr_SetObject expands tuple values into constructor args; + // other values (including unrelated exceptions) stay one arg. + let args = match value { + Some(value) if !vm.is_none(value) => value + .downcast_ref::() + .map_or_else(|| vec![value.clone()], |tuple| tuple.as_slice().to_vec()), + _ => vec![], + }; + vm.invoke_exception(vm.ctx.exceptions.stop_iteration, args)? + .into() + } + }; + args.push(exception); + } else { + args.extend_from_slice(extra); + } + let args = FuncArgs::from(args); + // Match CPython's highest-tool-first dispatch. Fetch each callback as it is + // reached, so an earlier callback may replace a later tool's callback. + for tool in (0..TOOL_LIMIT).rev() { + if tools & (1 << tool) == 0 { + continue; + } + let callback = vm + .state + .monitoring + .lock() + .callbacks + .get(&(tool, event as usize)) + .cloned(); + if let Some(callback) = callback + && call_instrument(vm, event, &callback, args.clone())? + { + if !is_instrumented_event(event as usize) { + return Err(cannot_disable(vm, event, tool)); + } + state.disable(tool); + } + } + if let Some(exception) = exception { + vm.set_raised_exception(Some(exception)); + } + Ok(()) +} diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index ec6db117cea..51d9cae5fc6 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -5,8 +5,10 @@ // https://docs.python.org/3/library/time.html pub use decl::time; +pub(crate) mod pytime; pub(crate) use decl::module_def; +pub(crate) use decl::perf_counter_ns as profiler_time; #[pymodule(name = "time", with(#[cfg(any(unix, windows))] platform))] mod decl { @@ -32,7 +34,6 @@ mod decl { use core::time::Duration; #[cfg(not(any(unix, windows)))] use jiff::{Timestamp, Zoned, civil::DateTime, tz::TimeZone}; - use num_traits::ToPrimitive; #[cfg(target_os = "wasi")] use rustpython_host_env::time::ClockId; #[cfg(any(unix, windows))] @@ -127,39 +128,9 @@ mod decl { fn sleep(object: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { vm.audit("time.sleep", || (object.clone(),))?; - let overflow = || vm.new_overflow_error("timestamp out of range for C PyTime_t"); - let nanoseconds = if object.number().is_index() { - let seconds = object.try_index(vm).map_err(|e| { - if e.fast_isinstance(vm.ctx.exceptions.overflow_error) { - overflow() - } else { - e - } - })?; - seconds - .as_bigint() - .to_i64() - .and_then(|seconds| seconds.checked_mul(SEC_TO_NS)) - .ok_or_else(overflow)? - } else { - let seconds = object.try_float(vm)?.to_f64(); - if seconds.is_nan() { - return Err(vm.new_value_error("Invalid value NaN (not a number)")); - } - // _PyTime_ROUND_TIMEOUT rounds away from zero. - let nanoseconds = seconds * SEC_TO_NS as f64; - let nanoseconds = if nanoseconds >= 0.0 { - nanoseconds.ceil() - } else { - nanoseconds.floor() - }; - let minimum = i64::MIN as f64; - // The signed maximum rounds up to 2**63 as a double. - if !(minimum..-minimum).contains(&nanoseconds) { - return Err(overflow()); - } - nanoseconds as i64 - }; + // _PyTime_ROUND_TIMEOUT rounds away from zero. + let nanoseconds = + super::pytime::from_seconds_object(&object, super::pytime::Round::Up, vm)?; if nanoseconds < 0 { return Err(vm.new_value_error("sleep length must be non-negative")); } @@ -262,7 +233,7 @@ mod decl { } #[pyfunction] - fn perf_counter_ns(vm: &VirtualMachine) -> PyResult { + pub(crate) fn perf_counter_ns(vm: &VirtualMachine) -> PyResult { Ok(get_perf_time(vm)?.as_nanos()) } diff --git a/crates/vm/src/stdlib/time/pytime.rs b/crates/vm/src/stdlib/time/pytime.rs new file mode 100644 index 00000000000..952e57e6be2 --- /dev/null +++ b/crates/vm/src/stdlib/time/pytime.rs @@ -0,0 +1,221 @@ +//! Signed nanosecond timestamps and the rounding used by Python's time APIs. + +#![cfg_attr(target_env = "musl", allow(deprecated))] + +use crate::{AsObject, PyObject, PyResult, VirtualMachine, builtins::PyFloat}; +use num_traits::ToPrimitive; +use rustpython_host_env::time::{SEC_TO_NS, SEC_TO_US, US_TO_NS}; + +#[cfg(any(unix, windows))] +pub(crate) use rustpython_host_env::time::TimeT; +#[cfg(not(any(unix, windows)))] +pub(crate) type TimeT = i64; + +const TIME_T_MIN: i64 = TimeT::MIN as _; +const TIME_T_MAX: i64 = TimeT::MAX as _; +const PYTIME_OVERFLOW: &str = "timestamp out of range for C PyTime_t"; +const TIME_T_OVERFLOW: &str = "timestamp out of range for platform time_t"; + +#[derive(Clone, Copy)] +pub(crate) enum Round { + Floor, + Ceiling, + HalfEven, + Up, +} + +impl Round { + pub(crate) fn from_int(value: i32, vm: &VirtualMachine) -> PyResult { + match value { + 0 => Ok(Self::Floor), + 1 => Ok(Self::Ceiling), + 2 => Ok(Self::HalfEven), + 3 => Ok(Self::Up), + _ => Err(vm.new_value_error("invalid rounding")), + } + } + + fn round(self, value: f64) -> f64 { + match self { + Self::Floor => value.floor(), + Self::Ceiling => value.ceil(), + Self::HalfEven => value.round_ties_even(), + Self::Up if value >= 0.0 => value.ceil(), + Self::Up => value.floor(), + } + } + + pub(crate) fn divide(self, value: i64, divisor: i64) -> i64 { + debug_assert!(divisor > 1); + let quotient = value / divisor; + let remainder = value % divisor; + let round_away = match self { + Self::Floor => remainder < 0, + Self::Ceiling => remainder > 0, + Self::HalfEven => { + let remainder = remainder.abs(); + remainder > divisor / 2 + || (divisor % 2 == 0 && remainder == divisor / 2 && quotient % 2 != 0) + } + Self::Up => remainder != 0, + }; + // Adjust the quotient, not the input: adding to i64::MAX or + // subtracting from i64::MIN before dividing would overflow. + quotient + if round_away { value.signum() } else { 0 } + } +} + +pub(crate) fn from_seconds(seconds: i32) -> i64 { + i64::from(seconds) * SEC_TO_NS +} + +fn index_seconds(object: &PyObject, message: &str, vm: &VirtualMachine) -> PyResult { + let seconds = object.try_index(vm).map_err(|err| { + if err.fast_isinstance(vm.ctx.exceptions.overflow_error) { + vm.new_overflow_error(message) + } else { + err + } + })?; + seconds + .as_bigint() + .to_i64() + .ok_or_else(|| vm.new_overflow_error(message)) +} + +fn float_seconds(object: &PyObject, vm: &VirtualMachine) -> PyResult { + // PyFloat_AsDouble bypasses __float__ overrides on float subclasses. + let seconds = match object.downcast_ref::() { + Some(float) => float.to_f64(), + None => object.try_float(vm)?.to_f64(), + }; + if seconds.is_nan() { + return Err(vm.new_value_error("Invalid value NaN (not a number)")); + } + Ok(seconds) +} + +pub(crate) fn from_seconds_object( + object: &PyObject, + round: Round, + vm: &VirtualMachine, +) -> PyResult { + // _PyTime_FromSecondsObject uses __index__ before __float__ and bounds + // the converted value to signed 64-bit nanoseconds. + if object.number().is_index() { + index_seconds(object, PYTIME_OVERFLOW, vm)? + .checked_mul(SEC_TO_NS) + .ok_or_else(|| vm.new_overflow_error(PYTIME_OVERFLOW)) + } else { + let nanoseconds = round.round(float_seconds(object, vm)? * SEC_TO_NS as f64); + let minimum = i64::MIN as f64; + // The maximum rounds up to 2**63 as a double, so use an exclusive bound. + if !(minimum..-minimum).contains(&nanoseconds) { + return Err(vm.new_overflow_error(PYTIME_OVERFLOW)); + } + Ok(nanoseconds as i64) + } +} + +#[cfg(all(feature = "capi", feature = "host_env"))] +pub(crate) fn as_seconds_double(nanoseconds: i64) -> f64 { + if nanoseconds % SEC_TO_NS == 0 { + (nanoseconds / SEC_TO_NS) as f64 + } else { + nanoseconds as f64 / SEC_TO_NS as f64 + } +} + +fn check_time_t(seconds: i64, vm: &VirtualMachine) -> PyResult { + if !(TIME_T_MIN..=TIME_T_MAX).contains(&seconds) { + return Err(vm.new_overflow_error(TIME_T_OVERFLOW)); + } + Ok(seconds) +} + +fn float_to_time_t(seconds: f64, vm: &VirtualMachine) -> PyResult { + let minimum = TIME_T_MIN as f64; + if !(minimum..-minimum).contains(&seconds) { + return Err(vm.new_overflow_error(TIME_T_OVERFLOW)); + } + Ok(seconds as i64) +} + +pub(crate) fn object_to_time_t( + object: &PyObject, + round: Round, + vm: &VirtualMachine, +) -> PyResult { + if object.number().is_index() { + check_time_t(index_seconds(object, TIME_T_OVERFLOW, vm)?, vm) + } else { + float_to_time_t(round.round(float_seconds(object, vm)?), vm) + } +} + +pub(crate) fn object_to_denominator( + object: &PyObject, + denominator: i64, + round: Round, + vm: &VirtualMachine, +) -> PyResult<(i64, i64)> { + if object.number().is_index() { + return Ok(( + check_time_t(index_seconds(object, TIME_T_OVERFLOW, vm)?, vm)?, + 0, + )); + } + let value = float_seconds(object, vm)?; + let mut seconds = value.trunc(); + let denominator = denominator as f64; + let mut numerator = round.round(value.fract() * denominator); + if numerator >= denominator { + numerator -= denominator; + seconds += 1.0; + } else if numerator < 0.0 { + numerator += denominator; + seconds -= 1.0; + } + Ok((float_to_time_t(seconds, vm)?, numerator as i64)) +} + +fn as_time_pair(value: i64, denominator: i64, minimum: i64, maximum: i64) -> (i64, i64) { + let seconds = value.div_euclid(denominator); + if seconds < minimum { + (minimum, 0) + } else if seconds > maximum { + (maximum, 0) + } else { + (seconds, value.rem_euclid(denominator)) + } +} + +pub(crate) fn as_timespec( + nanoseconds: i64, + clamp: bool, + vm: &VirtualMachine, +) -> PyResult<(i64, i64)> { + if !clamp { + check_time_t(nanoseconds.div_euclid(SEC_TO_NS), vm)?; + } + Ok(as_time_pair(nanoseconds, SEC_TO_NS, TIME_T_MIN, TIME_T_MAX)) +} + +pub(crate) fn as_timeval( + nanoseconds: i64, + round: Round, + clamp: bool, + vm: &VirtualMachine, +) -> PyResult<(i64, i64)> { + // Windows uses C long for timeval.tv_sec even when time_t is 64-bit. + #[cfg(windows)] + let (minimum, maximum) = (i64::from(i32::MIN), i64::from(i32::MAX)); + #[cfg(not(windows))] + let (minimum, maximum) = (TIME_T_MIN, TIME_T_MAX); + let microseconds = round.divide(nanoseconds, US_TO_NS); + let seconds = microseconds.div_euclid(SEC_TO_US); + if !clamp && !(minimum..=maximum).contains(&seconds) { + return Err(vm.new_overflow_error(TIME_T_OVERFLOW)); + } + Ok(as_time_pair(microseconds, SEC_TO_US, minimum, maximum)) +} diff --git a/crates/vm/src/types/slot.rs b/crates/vm/src/types/slot.rs index 8a7f750cdb6..9854bebe6d6 100644 --- a/crates/vm/src/types/slot.rs +++ b/crates/vm/src/types/slot.rs @@ -1270,7 +1270,7 @@ impl PyType { attr.class().is(ctx.types.wrapper_descriptor_type) && attr.downcast_ref::().is_some_and(|descr| { descr.name.as_str() == "__add__" - && self.mro.read().iter().any(|cls| cls.is(descr.typ)) + && self.mro.read().iter().any(|cls| cls.is(&descr.typ)) && matches!(descr.wrapped, SlotFunc::SeqConcat(_)) }) }); @@ -1895,7 +1895,7 @@ impl PyType { if attr.class().is(ctx.types.wrapper_descriptor_type) { attr.downcast_ref::().and_then(|wrapper| { // Only extract slot if for_class is a subclass of wrapper.typ - if is_subclass_of(for_class_mro, wrapper.typ) { + if is_subclass_of(for_class_mro, &wrapper.typ) { extract(&wrapper.wrapped) } else { None diff --git a/crates/vm/src/vm/compile.rs b/crates/vm/src/vm/compile.rs index 95dbcac5b8f..96086dc33ee 100644 --- a/crates/vm/src/vm/compile.rs +++ b/crates/vm/src/vm/compile.rs @@ -5,7 +5,7 @@ use core::fmt; use crate::{ - AsObject, Py, PyObjectRef, PyRef, PyResult, VirtualMachine, + AsObject, Py, PyObject, PyObjectRef, PyRef, PyResult, VirtualMachine, builtins::{PyBaseExceptionRef, PyCode, PyStr}, compiler::{self, CompileError, CompileOpts}, vm::compile_mode::{CompileStart, CompilerFlags, compile_future_features_from_flags}, @@ -416,6 +416,8 @@ impl VirtualMachine { ) -> PyResult { use crate::stdlib::_ast; + let module = module.map(AsObject::as_object); + let cf = CompilerFlags::from_bits_retain(flags); let Some(start) = CompileStart::from_i32(start) else { return Err(self.new_system_error("Invalid start argument passed to Py_CompileString")); @@ -443,22 +445,26 @@ impl VirtualMachine { None }; - let emitted_parser_escapes = if is_ast_only { - self.emit_ast_parser_warnings( + let parser_mode = match start { + CompileStart::Single | CompileStart::File => Some(ruff_python_parser::Mode::Module), + CompileStart::Eval => Some(ruff_python_parser::Mode::Expression), + CompileStart::FuncType => None, + }; + let emitted_escapes = self + .emit_parser_syntax_warnings( source, filename, module, - start, + parser_mode, future_features.contains(crate::bytecode::CodeFlags::FUTURE_BARRY_AS_BDFL), + escape_warnings::WarningEmission::Ast { + tokenizer_recovery: is_ast_only + && matches!(start, CompileStart::File | CompileStart::Eval), + }, ) - .map_err(|warning| { - VmCompileError::Warning(warning).into_pyexception(self, Some(source)) - })? - } else { - Vec::new() - }; - let mut emit_ast_warning = |offset, ch| { - if emitted_parser_escapes.binary_search(&offset).is_ok() { + .map_err(|err| err.into_pyexception(self, Some(source)))?; + let emit_ast_warning = |offset, ch| { + if emitted_escapes.binary_search(&offset).is_ok() { return Ok(()); } escape_warnings::warn_invalid_escape_sequence( @@ -471,8 +477,9 @@ impl VirtualMachine { .map_err(VmCompileError::Warning) }; if is_ast_only { - if start == CompileStart::FuncType { - return _ast::parse_func_type( + let mut emit_ast_warning = emit_ast_warning; + let parsed = if start == CompileStart::FuncType { + _ast::parse_func_type( self, source, filename, @@ -480,30 +487,28 @@ impl VirtualMachine { target_version, &mut emit_ast_warning, ) - .map_err(|e| { - e.into_pyexception_maybe_incomplete(self, Some(source), allow_incomplete) - }); + } else { + let (parser_mode, interactive) = match start { + CompileStart::Single => (ruff_python_parser::Mode::Module, true), + CompileStart::File => (ruff_python_parser::Mode::Module, false), + CompileStart::Eval => (ruff_python_parser::Mode::Expression, false), + CompileStart::FuncType => unreachable!(), + }; + _ast::parse( + self, + source, + filename, + parser_mode, + optimize, + target_version, + type_comments, + optimized_ast, + interactive, + future_features, + dont_imply_dedent, + &mut emit_ast_warning, + ) } - let (parser_mode, interactive) = match start { - CompileStart::Single => (ruff_python_parser::Mode::Module, true), - CompileStart::File => (ruff_python_parser::Mode::Module, false), - CompileStart::Eval => (ruff_python_parser::Mode::Expression, false), - CompileStart::FuncType => unreachable!(), - }; - let parsed = _ast::parse( - self, - source, - filename, - parser_mode, - optimize, - target_version, - type_comments, - optimized_ast, - interactive, - future_features, - dont_imply_dedent, - &mut emit_ast_warning, - ) .map_err(|e| { e.into_pyexception_maybe_incomplete(self, Some(source), allow_incomplete) })?; @@ -513,7 +518,6 @@ impl VirtualMachine { return Ok(parsed); } - let mut emitted_escapes = Vec::new(); if type_comments { let parser_mode = match start { CompileStart::Single | CompileStart::File => ruff_python_parser::Mode::Module, @@ -532,20 +536,11 @@ impl VirtualMachine { start == CompileStart::Single, future_features, dont_imply_dedent, - |offset, ch| { - emit_ast_warning(offset, ch)?; - // The ordinary compiler must not emit this same escape again. - emitted_escapes.push(offset); - Ok(()) - }, + emit_ast_warning, ) .map_err(|e: VmCompileError| { e.into_pyexception_maybe_incomplete(self, Some(source), allow_incomplete) })?; - // AST traversal need not follow source order. The later compiler - // uses binary search to skip these already-considered offsets. - emitted_escapes.sort_unstable(); - emitted_escapes.dedup(); } let mode = match start { @@ -560,7 +555,7 @@ impl VirtualMachine { opts.future_features = future_features; opts.dont_imply_dedent = dont_imply_dedent; let code = self - .compile_with_opts_and_module(source, mode, filename, opts, module, &emitted_escapes) + .compile_after_parser_warnings(source, mode, filename, opts, module) .map_err(|err| { err.into_pyexception_maybe_incomplete(self, Some(source), allow_incomplete) })?; @@ -586,13 +581,43 @@ impl VirtualMachine { self.compile_with_opts_and_module(source, mode, source_path, opts, None, &[]) } + pub(crate) fn compile_symtable_with_module( + &self, + source: &str, + mode: compiler::Mode, + filename: &str, + module: Option<&Py>, + ) -> Result { + let module = module.map(AsObject::as_object); + #[cfg(feature = "parser")] + { + self.emit_parser_syntax_warnings( + source, + filename, + module, + Some(escape_warnings::parser_mode(mode)), + false, + escape_warnings::WarningEmission::Standard { + emitted_escapes: &[], + }, + ) + .map_err(VmCompileError::Warning)?; + } + #[cfg(not(feature = "parser"))] + let _ = module; + // Symbol-table construction emits parser warnings, not codegen or + // AST-preprocessing warnings. Reuse compile()'s module filtering and + // warning-to-SyntaxError conversion without compiling bytecode. + compiler::compile_symtable(source, mode, filename).map_err(VmCompileError::Compile) + } + pub(crate) fn compile_with_opts_and_module( &self, source: &str, mode: compiler::Mode, source_path: impl Into, opts: CompileOpts, - module: Option<&Py>, + module: Option<&PyObject>, emitted_escapes: &[usize], ) -> Result, VmCompileError> { let source_path = source_path.into(); @@ -600,10 +625,16 @@ impl VirtualMachine { let _ = (module, emitted_escapes); #[cfg(feature = "parser")] { - self.emit_tokenizer_syntax_warnings(source, &source_path, module) - .map_err(VmCompileError::Warning)?; - self.emit_string_escape_warnings(source, &source_path, module, emitted_escapes) - .map_err(VmCompileError::Warning)?; + self.emit_parser_syntax_warnings( + source, + &source_path, + module, + Some(escape_warnings::parser_mode(mode)), + opts.future_features + .contains(crate::bytecode::CodeFlags::FUTURE_BARRY_AS_BDFL), + escape_warnings::WarningEmission::Standard { emitted_escapes }, + ) + .map_err(VmCompileError::Warning)?; } self.compile_after_parser_warnings(source, mode, source_path, opts, module) } @@ -614,7 +645,7 @@ impl VirtualMachine { mode: compiler::Mode, source_path: impl Into, opts: CompileOpts, - module: Option<&Py>, + module: Option<&PyObject>, ) -> Result, VmCompileError> { let source_path = source_path.into(); #[cfg(not(feature = "parser"))] @@ -667,8 +698,7 @@ impl VirtualMachine { } } -/// Scan source for invalid escape sequences in all string literals and emit -/// SyntaxWarning. +/// Collect and emit tokenizer and parser SyntaxWarnings in source order. /// /// Corresponds to: /// - `warn_invalid_escape_sequence()` in `Parser/string_parser.c` @@ -747,23 +777,40 @@ mod escape_warnings { Tokenizer, } + pub(super) enum WarningEmission<'a> { + Standard { emitted_escapes: &'a [usize] }, + Ast { tokenizer_recovery: bool }, + Interactive(&'a mut super::InteractiveWarnings), + } + enum ParserWarning { - /// The escape, where it is reported from, and for a bytes literal - /// escape on the literal's first line, the error column that - /// string_parser.c reports. Escape(InvalidEscape, EscapeOrigin, Option), Numeric { offset: usize, last_offset: usize, message: String, }, + Syntax { + offset: usize, + message: String, + end_offset: Option, + }, } impl ParserWarning { fn offset(&self) -> usize { match self { - Self::Escape(escape, ..) => escape.offset(), - Self::Numeric { offset, .. } => *offset, + Self::Escape(escape, _, _) => escape.offset(), + Self::Numeric { offset, .. } | Self::Syntax { offset, .. } => *offset, + } + } + } + + pub(super) fn parser_mode(mode: compiler::Mode) -> ruff_python_parser::Mode { + match mode { + compiler::Mode::Eval => ruff_python_parser::Mode::Expression, + compiler::Mode::Exec | compiler::Mode::Single | compiler::Mode::BlockExpr => { + ruff_python_parser::Mode::Module } } } @@ -922,7 +969,7 @@ mod escape_warnings { source: &str, escape: InvalidEscape, filename: &str, - module: Option<&Py>, + module: Option<&PyObject>, vm: &VirtualMachine, ) -> Result<(), CompileWarningError> { let (lineno, column) = line_offset_at(source, escape.offset()); @@ -934,7 +981,7 @@ mod escape_warnings { vm.ctx.new_str(warning).into(), fname, lineno, - module.map(|module| module.to_owned().into()), + module.map(ToOwned::to_owned), vm.ctx.none(), None, None, @@ -955,7 +1002,7 @@ mod escape_warnings { fn warn_syntax_at_offset( source: &str, filename: &str, - module: Option<&Py>, + module: Option<&PyObject>, offset: usize, message: String, vm: &VirtualMachine, @@ -968,7 +1015,7 @@ mod escape_warnings { message.into(), fname, lineno, - module.map(|module| module.to_owned().into()), + module.map(ToOwned::to_owned), vm.ctx.none(), None, None, @@ -979,7 +1026,7 @@ mod escape_warnings { pub(super) fn warn_syntax_at_location( filename: &str, - module: Option<&Py>, + module: Option<&PyObject>, location: compiler::core::SourceLocation, message: String, vm: &VirtualMachine, @@ -991,7 +1038,7 @@ mod escape_warnings { message.into(), fname, location.line.get(), - module.map(|module| module.to_owned().into()), + module.map(ToOwned::to_owned), vm.ctx.none(), None, None, @@ -1272,11 +1319,9 @@ mod escape_warnings { return; } // In CPython, _PyTokenizer_warn_invalid_escape_sequence handles - // `\{` and `\}` for FSTRING_MIDDLE/FSTRING_END tokens. Ruff - // splits the literal element before the interpolation delimiter, - // so the `\` sits at the end of the literal range and the `{`/`}` - // sits just after it. Only warn when the number of trailing - // backslashes is odd (an even count means they are all escaped). + // `\{` and `\}` for FSTRING_MIDDLE/FSTRING_END tokens. Ruff splits + // before the delimiter, leaving the backslash at the literal's end. + // An even number of trailing backslashes is already escaped. let trailing_bs = source.as_bytes()[start..end] .iter() .rev() @@ -1532,7 +1577,6 @@ mod escape_warnings { struct EscapeWarningVisitor<'a, 'w> { source: &'a str, warnings: &'w mut Vec, - ast_only: bool, /// This pass runs before the compile that rejects an over-nested /// tree, so it has to stop itself. depth: usize, @@ -1631,14 +1675,6 @@ mod escape_warnings { } } ast::FStringPartRef::FString(fstring) => { - if !self.ast_only - && matches!( - fstring.flags.prefix(), - ast::str_prefix::FStringPrefix::Raw { .. } - ) - { - continue; - } self.visit_fstring_elements( &fstring.elements, matches!( @@ -1650,7 +1686,7 @@ mod escape_warnings { } } } - ast::Expr::TString(tstring) if self.ast_only => { + ast::Expr::TString(tstring) => { for part in tstring.value.as_slice() { self.visit_fstring_elements(&part.elements, part.flags.prefix().is_raw()); } @@ -1666,205 +1702,97 @@ mod escape_warnings { } } - fn collect_parser_warnings( + fn collect_relative_lazy_import_warnings( source: &str, - filename: &str, - mode: Option, - inherited_barry_as_flufl: bool, - ) -> (Vec, bool) { - let mut ast_tokenizer_recovery = true; - let mut warnings = Vec::new(); - collect_numeric_literal_warnings(source, &mut warnings); - // The compile that follows rejects this source; parsing it here - // would build a tree that exhausts the stack when dropped. - let source_file = compiler::core::SourceFileBuilder::new(filename, source).finish(); - if compiler::pre_parse_source_error(&source_file).is_ok() - && let Some(mode) = mode - { - let options = ruff_python_parser::ParseOptions::from(mode); - let barry_source = compiler::prepare_barry_as_flufl_source( - source, - options.clone(), - inherited_barry_as_flufl, - ); - let parsed = ruff_python_parser::parse_unchecked(barry_source.source(), options); - if parsed.has_valid_syntax() { - let mut visitor = EscapeWarningVisitor { - source, - warnings: &mut warnings, - ast_only: true, - depth: 0, - depth_limit: compiler::CompileOpts::default().recursion_limit, - }; - match parsed.syntax() { - ast::Mod::Module(module) => { - for stmt in &module.body { - visitor.visit_stmt(stmt); - } - } - ast::Mod::Expression(expr) => visitor.visit_expr(&expr.body), - } - } else { - ast_tokenizer_recovery = false; - // The tokenizer can read the token at which parsing fails, - // but later recovered statements were never parsed by - // CPython and must not produce string/parser warnings. - let end = parsed - .errors() - .iter() - // An unclosed-bracket diagnostic points back to its - // opening token, not where parsing stopped. Other - // errors retain that boundary; otherwise it was EOF. - .filter(|error| { - !matches!( - error.error, - ruff_python_parser::ParseErrorType::Lexical( - ruff_python_parser::LexicalErrorType::UnclosedBracket { .. } - ) - ) - }) - .min_by_key(|error| error.location.start()) - .map_or(source.len(), |error| { - // Tokens after a complete statement or eval - // expression were not decoded by CPython. - if matches!( - error.error, - ruff_python_parser::ParseErrorType::SimpleStatementsOnSameLine - | ruff_python_parser::ParseErrorType::UnexpectedExpressionToken - ) { - error.location.start().to_usize() - } else { - error.location.end().to_usize() - } - }); - collect_string_escape_warnings_in_range(source, 0, end, None, &mut warnings); - } - } else if mode.is_none() && compiler::pre_parse_source_error(&source_file).is_ok() { - ast_tokenizer_recovery = false; - // Function-type input has a separate AST parser; its return - // expression follows an arrow that expression mode rejects. - // There are no import statements in this grammar. - collect_string_escape_warnings_in_range(source, 0, source.len(), None, &mut warnings); - } - warnings.sort_by_key(ParserWarning::offset); - // Interpolated tokenizer warnings have their own stop/error boundary. - // Numeric-only recovery must not skip across one of those events. - if warnings.iter().any(|warning| { - matches!( - warning, - ParserWarning::Escape(_, EscapeOrigin::Tokenizer, _) - ) - }) { - ast_tokenizer_recovery = false; - } - (warnings, ast_tokenizer_recovery) - } - - enum WarningEmission<'a> { - Standard { emitted_escapes: &'a [usize] }, - Ast { tokenizer_recovery: bool }, - Interactive(&'a mut super::InteractiveWarnings), - } + parsed: &ruff_python_parser::Parsed, + warnings: &mut Vec, + ) { + use ast::statement_visitor::StatementVisitor; + use ruff_text_size::Ranged; - impl VirtualMachine { - // Keep ordinary source compilation's existing warning order and - // recovery surface; interactive records use the bounded parser below. - pub(super) fn emit_tokenizer_syntax_warnings( - &self, - source: &str, - filename: &str, - module: Option<&Py>, - ) -> Result<(), CompileWarningError> { - let mut warnings = Vec::new(); - collect_numeric_literal_warnings(source, &mut warnings); - self.emit_parser_warning_batch( - source, - filename, - module, - warnings, - WarningEmission::Standard { - emitted_escapes: &[], - }, - ) - .map(|_| ()) + struct Imports<'a> { + pending: Vec<&'a ast::Stmt>, } - - pub(super) fn emit_string_escape_warnings( - &self, - source: &str, - filename: &str, - module: Option<&Py>, - emitted_escapes: &[usize], - ) -> Result<(), CompileWarningError> { - let source_file = compiler::core::SourceFileBuilder::new(filename, source).finish(); - if compiler::pre_parse_source_error(&source_file).is_err() { - return Ok(()); + impl<'a> StatementVisitor<'a> for Imports<'a> { + fn visit_stmt(&mut self, stmt: &'a ast::Stmt) { + self.pending.push(stmt); } - let mut warnings = Vec::new(); - if let Ok(parsed) = - ruff_python_parser::parse(source, ruff_python_parser::Mode::Module.into()) + } + + // This is a parser warning: AST-only compilation and optimized-away + // statements must observe it too. Inspect parsed imports so strings, + // comments and other uses of the soft keyword never trigger it. + let ast::Mod::Module(body) = parsed.syntax() else { + return; + }; + let first_error = parsed + .errors() + .iter() + .map(|error| error.location.start()) + .min(); + let mut imports = Imports { + pending: body.body.iter().rev().collect(), + }; + while let Some(stmt) = imports.pending.pop() { + if let ast::Stmt::ImportFrom(import) = stmt + && !import.is_lazy + && import.level > 0 + && !import.names.is_empty() + && first_error.is_none_or(|offset| import.names[0].name.range().end() <= offset) + && let Some(name) = &import.module + && name.as_str() == "lazy" { - let mut visitor = EscapeWarningVisitor { - source, - warnings: &mut warnings, - ast_only: false, - depth: 0, - depth_limit: compiler::CompileOpts::default().recursion_limit, - }; - match parsed.syntax() { - ast::Mod::Module(module) => { - for stmt in &module.body { - visitor.visit_stmt(stmt); + let start = name.range().start().to_usize(); + if start > 0 && source.as_bytes()[start - 1] != b'.' { + let dots = ".".repeat(import.level as usize); + let message = format!( + "'from {dots} lazy import' is the same as 'from {dots}lazy import'; \ + did you mean 'lazy from {dots} import'?" + ); + // Recovery can extend the AST past the first parse error. + // A completed name list can still warn before a bad token, + // but an open parenthesis or a trailing comma means the + // import's grammar action was never reached. + let mut parentheses = 0usize; + let mut last_kind = None; + for token in parsed.tokens().in_range(import.range) { + if first_error.is_some_and(|offset| token.range().end() > offset) { + if token.kind() == compiler::TokenKind::Comma { + last_kind = Some(token.kind()); + } + break; } + match token.kind() { + compiler::TokenKind::Lpar => parentheses += 1, + compiler::TokenKind::Rpar => { + parentheses = parentheses.saturating_sub(1) + } + compiler::TokenKind::Comment + | compiler::TokenKind::NonLogicalNewline + | compiler::TokenKind::Newline => continue, + _ => {} + } + last_kind = Some(token.kind()); + } + if parentheses == 0 + && (last_kind != Some(compiler::TokenKind::Comma) + || import.names[0].name.as_str() == "*") + { + warnings.push(ParserWarning::Syntax { + offset: start, + message, + end_offset: Some(name.range().end().to_usize()), + }); } - ast::Mod::Expression(expr) => visitor.visit_expr(&expr.body), } - } else { - collect_string_escape_warnings_in_range( - source, - 0, - source.len(), - None, - &mut warnings, - ); } - self.emit_parser_warning_batch( - source, - filename, - module, - warnings, - WarningEmission::Standard { emitted_escapes }, - ) - .map(|_| ()) - } - - pub(super) fn emit_ast_parser_warnings( - &self, - source: &str, - filename: &str, - module: Option<&Py>, - start: CompileStart, - inherited_barry_as_flufl: bool, - ) -> Result, CompileWarningError> { - let mode = match start { - CompileStart::Single | CompileStart::File => Some(ruff_python_parser::Mode::Module), - CompileStart::Eval => Some(ruff_python_parser::Mode::Expression), - CompileStart::FuncType => None, - }; - let (warnings, recovery) = - collect_parser_warnings(source, filename, mode, inherited_barry_as_flufl); - self.emit_parser_warning_batch( - source, - filename, - module, - warnings, - WarningEmission::Ast { - tokenizer_recovery: recovery - && matches!(start, CompileStart::File | CompileStart::Eval), - }, - ) + let previous = imports.pending.len(); + ast::statement_visitor::walk_stmt(&mut imports, stmt); + imports.pending[previous..].reverse(); } + } + impl VirtualMachine { /// Emit the tokenizer warnings for the interactive input read so far. pub(in crate::vm) fn emit_interactive_parser_warnings( &self, @@ -1874,47 +1802,137 @@ mod escape_warnings { inherited_barry_as_flufl: bool, emitted: &mut super::InteractiveWarnings, ) -> Result<(), CompileWarningError> { - let (warnings, _) = collect_parser_warnings( + self.emit_parser_syntax_warnings( source, filename, + module.map(AsObject::as_object), Some(ruff_python_parser::Mode::Module), inherited_barry_as_flufl, - ); - self.emit_parser_warning_batch( - source, - filename, - module, - warnings, WarningEmission::Interactive(emitted), ) .map(|_| ()) } - fn emit_parser_warning_batch( + /// Collect parser warnings before emitting them in source order. AST + /// traversal order can differ from lexical order (for example, a + /// function's defaults and annotations), and tokenizer warnings must + /// interleave with the warnings produced by grammar actions. + pub(super) fn emit_parser_syntax_warnings( &self, source: &str, filename: &str, - module: Option<&Py>, - warnings: Vec, + module: Option<&PyObject>, + mode: Option, + inherited_barry_as_flufl: bool, emission: WarningEmission<'_>, ) -> Result, CompileWarningError> { - let (emitted_escapes, ast_tokenizer_recovery, mut interactive) = match emission { + let (emitted_escapes, mut ast_tokenizer_recovery, mut interactive) = match emission { WarningEmission::Standard { emitted_escapes } => (emitted_escapes, false, None), WarningEmission::Ast { tokenizer_recovery } => (&[][..], tokenizer_recovery, None), WarningEmission::Interactive(emitted) => (&[][..], false, Some(emitted)), }; + let mut warnings = Vec::new(); + collect_numeric_literal_warnings(source, &mut warnings); + // The compile that follows rejects this source; parsing it here + // would build a tree that exhausts the stack when dropped. + let source_file = compiler::core::SourceFileBuilder::new(filename, source).finish(); + if compiler::pre_parse_source_error(&source_file).is_ok() + && let Some(mode) = mode + { + let options = ruff_python_parser::ParseOptions::from(mode); + let barry_source = compiler::prepare_barry_as_flufl_source( + source, + options.clone(), + inherited_barry_as_flufl, + ); + let parsed = ruff_python_parser::parse_unchecked(barry_source.source(), options); + collect_relative_lazy_import_warnings(source, &parsed, &mut warnings); + if parsed.has_valid_syntax() { + let mut visitor = EscapeWarningVisitor { + source, + warnings: &mut warnings, + depth: 0, + depth_limit: compiler::CompileOpts::default().recursion_limit, + }; + match parsed.syntax() { + ast::Mod::Module(module) => { + for stmt in &module.body { + visitor.visit_stmt(stmt); + } + } + ast::Mod::Expression(expr) => visitor.visit_expr(&expr.body), + } + } else { + ast_tokenizer_recovery = false; + // The tokenizer can read the token at which parsing fails, + // but later recovered statements were never parsed by + // CPython and must not produce string/parser warnings. + let end = parsed + .errors() + .iter() + // An unclosed-bracket diagnostic points back to its + // opening token, not where parsing stopped. Other + // errors retain that boundary; otherwise it was EOF. + .filter(|error| { + !matches!( + error.error, + ruff_python_parser::ParseErrorType::Lexical( + ruff_python_parser::LexicalErrorType::UnclosedBracket { .. } + ) + ) + }) + .min_by_key(|error| error.location.start()) + .map_or(source.len(), |error| { + // Tokens after a complete statement or eval + // expression were not decoded by CPython. + if matches!( + error.error, + ruff_python_parser::ParseErrorType::SimpleStatementsOnSameLine + | ruff_python_parser::ParseErrorType::UnexpectedExpressionToken + ) { + error.location.start().to_usize() + } else { + error.location.end().to_usize() + } + }); + collect_string_escape_warnings_in_range(source, 0, end, None, &mut warnings); + } + } else if mode.is_none() && compiler::pre_parse_source_error(&source_file).is_ok() { + ast_tokenizer_recovery = false; + // Function-type input has a separate AST parser; its return + // expression follows an arrow that expression mode rejects. + // There are no import statements in this grammar. + collect_string_escape_warnings_in_range( + source, + 0, + source.len(), + None, + &mut warnings, + ); + } + warnings.sort_by_key(ParserWarning::offset); + // Numeric-only recovery must not skip an interpolated tokenizer boundary. + if warnings.iter().any(|warning| { + matches!( + warning, + ParserWarning::Escape(_, EscapeOrigin::Tokenizer, _) + ) + }) { + ast_tokenizer_recovery = false; + } let mut considered_escapes = Vec::new(); let mut parser_error = None; for warning in warnings { - // CPython resumes tokenization after a parser-origin SyntaxError. - // Preserve ordinary warning order and never replay prior warnings. + // After a parser-origin warning becomes an error, CPython resumes + // tokenization without replaying grammar-action or string warnings. if parser_error.is_some() && !matches!(warning, ParserWarning::Numeric { .. }) { continue; } if let Some(state) = interactive.as_deref_mut() { let message = match &warning { ParserWarning::Escape(escape, ..) => escape.warning_message(), - ParserWarning::Numeric { message, .. } => message.clone(), + ParserWarning::Numeric { message, .. } + | ParserWarning::Syntax { message, .. } => message.clone(), }; if !state.emitted.insert((warning.offset(), message)) { continue; @@ -1932,9 +1950,7 @@ mod escape_warnings { source, escape, filename, module, self, ) .map_err(|mut error| { - if interactive.is_some() - && let Some(column) = error_column - { + if let Some(column) = error_column { error.offset = column; if let Some(replacement) = &mut error.replacement { replacement.end_offset = column + 2; @@ -1957,19 +1973,15 @@ mod escape_warnings { } } ParserWarning::Numeric { - offset, last_offset, message, + .. } => { warn_syntax_at_offset( source, filename, module, - if interactive.is_some() { - last_offset - } else { - offset - }, + last_offset, message.clone(), self, ) @@ -1977,10 +1989,37 @@ mod escape_warnings { // Tokenizer numeric warnings become a zero-width // error at the literal's last character, in character // columns rather than grammar-action byte columns. - if interactive.is_some() { + error.replacement = Some(SyntaxErrorReplacement { + message, + end_offset: error.offset, + }); + error + })?; + } + ParserWarning::Syntax { + offset, + message, + end_offset, + } => { + warn_syntax_at_offset( + source, + filename, + module, + offset, + message.clone(), + self, + ) + .map_err(|mut error| { + if let Some(end_offset) = end_offset { + // Grammar-action warnings use the AST's + // UTF-8 byte columns, including for names + // normalized from non-ASCII identifiers. + let line_start = + source[..offset].rfind('\n').map_or(0, |index| index + 1); + error.offset = offset - line_start + 1; error.replacement = Some(SyntaxErrorReplacement { message, - end_offset: error.offset, + end_offset: end_offset - line_start + 1, }); } error @@ -2194,11 +2233,15 @@ mod escape_warnings { } #[test] - fn unparsed_identifier_suffix_is_not_a_raw_prefix() { + fn unparsed_adjacent_string_does_not_emit_escape_warning() { let message = compile_error_message("bar'\\z' $\n"); assert!( - message.contains("\"\\z\" is an invalid escape sequence"), - "trailing r in an identifier must not suppress the warning, got {message:?}" + message.contains("invalid syntax"), + "expected syntax error before decoding the adjacent string, got {message:?}" + ); + assert!( + !message.contains("invalid escape"), + "an unparsed adjacent string must not emit an escape warning, got {message:?}" ); } diff --git a/crates/vm/src/vm/crossinterp.rs b/crates/vm/src/vm/crossinterp.rs index 1fb46a64356..b3f7ebcad97 100644 --- a/crates/vm/src/vm/crossinterp.rs +++ b/crates/vm/src/vm/crossinterp.rs @@ -138,6 +138,8 @@ pub enum SharedValue { Buffer(Vec, BufferDescriptor), /// Marshalled code object. Code(Vec), + /// A value explicitly shared through marshal rather than XI registration. + Marshalled(Vec), /// Marshalled code of a stateless function; rebuilt against `__main__`. Function(Vec), /// `pickle.dumps` output, used by [`Fallback::Full`]. @@ -257,7 +259,7 @@ impl SharedValue { } /// `_PyFunction_GetXIData`: only stateless functions are shareable. - fn from_function(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + pub fn from_function(obj: &PyObject, vm: &VirtualMachine) -> PyResult { let func = obj.downcast_ref::().ok_or_else(|| { not_shareable_error( vm, @@ -289,6 +291,25 @@ impl SharedValue { Ok(Self::Code(marshal_dumps(code.as_object(), vm)?)) } + /// `_PyPickle_GetXIData`, without trying other sharing mechanisms first. + pub fn from_pickle(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + pickle_dumps(obj, vm) + .map(Self::Pickled) + .map_err(|cause| not_shareable_error_from(vm, "object could not be pickled", cause)) + } + + /// `_PyMarshal_GetXIData`. + pub fn from_marshal(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + marshal_dumps(obj, vm).map(Self::Marshalled) + } + + /// `_PyCode_GetScriptXIData` and `_PyCode_GetPureScriptXIData`. + pub fn from_script(obj: &PyObject, pure: bool, vm: &VirtualMachine) -> PyResult { + let code = script_code_with_purity(obj, pure, vm) + .map_err(|cause| not_shareable_error_from(vm, "object not a valid script", cause))?; + Self::from_code(&code, vm) + } + pub fn from_buffer_object(obj: &PyObject, vm: &VirtualMachine) -> PyResult { let view = PyMemoryView::from_object(obj, vm)?; #[cfg(feature = "threading")] @@ -332,7 +353,7 @@ impl SharedValue { .into_pybuffer_with_descriptor(desc); memoryview_from_buffer(buffer, vm) } - Self::Code(data) => marshal_loads(&data, vm), + Self::Code(data) | Self::Marshalled(data) => marshal_loads(&data, vm), Self::Function(data) => { let code = marshal_loads(&data, vm)? .downcast::() @@ -348,17 +369,23 @@ impl SharedValue { } fn marshal_dumps(obj: &PyObject, vm: &VirtualMachine) -> PyResult> { - let dumps = vm.import("marshal", 0)?.get_attr("dumps", vm)?; - let bytes = dumps.call((obj.to_owned(),), vm)?; - let bytes = bytes - .downcast::() - .map_err(|_| vm.new_type_error("marshal.dumps() did not return bytes"))?; - Ok(bytes.as_bytes().to_vec()) + let dump = || -> PyResult> { + let dumps = vm.import("marshal", 0)?.get_attr("dumps", vm)?; + let bytes = dumps.call((obj.to_owned(),), vm)?; + let bytes = bytes + .downcast::() + .map_err(|_| vm.new_type_error("marshal.dumps() did not return bytes"))?; + Ok(bytes.as_bytes().to_vec()) + }; + dump().map_err(|cause| not_shareable_error_from(vm, "object could not be marshalled", cause)) } fn marshal_loads(data: &[u8], vm: &VirtualMachine) -> PyResult { - let loads = vm.import("marshal", 0)?.get_attr("loads", vm)?; - loads.call((vm.ctx.new_bytes(data.to_vec()),), vm) + let load = || -> PyResult { + let loads = vm.import("marshal", 0)?.get_attr("loads", vm)?; + loads.call((vm.ctx.new_bytes(data.to_vec()),), vm) + }; + load().map_err(|cause| not_shareable_error_from(vm, "object could not be unmarshalled", cause)) } /// `_memoryview_from_xid`. @@ -828,37 +855,38 @@ pub(crate) fn code_returns_only_none(code: &Py) -> bool { if !is_pure_function(code) { return false; } - let units = &code.instructions; - // Look up None in co_consts. - let none_index = code - .constants - .iter() - .position(|c| matches!(c.borrow_constant(), BorrowedConstant::None)); - // We don't worry about EXTENDED_ARG for now. - for (at, op, _) in walk_instructions(code) { - if !matches!(op, Instruction::ReturnValue) { - continue; - } - debug_assert_ne!(at, 0); - let prev_op = units.read_op(at - 1).deoptimize(); - let prev_arg = u32::from(u8::from(units.read_arg(at - 1))); - if matches!(prev_op, Instruction::LoadCommonConstant { .. }) - && prev_arg == u32::from(CommonConstant::None) - { + let mut previous_loads_none = false; + for (_, instruction, arg) in walk_instructions(code) { + if matches!(instruction, Instruction::ExtendedArg) { continue; } - if matches!(prev_op, Instruction::LoadConst { .. }) - && none_index.is_some_and(|none_index| prev_arg as usize == none_index) - { - continue; + if matches!(instruction, Instruction::ReturnValue) && !previous_loads_none { + return false; } - return false; + previous_loads_none = match instruction { + Instruction::LoadCommonConstant { idx } => idx.get(arg) == CommonConstant::None, + Instruction::LoadConst { consti } => code + .constants + .get(consti.get(arg).as_usize()) + .is_some_and(|constant| { + matches!(constant.borrow_constant(), BorrowedConstant::None) + }), + _ => false, + }; } true } /// Extract a script code object from source text / function / code object. pub fn script_code(obj: &PyObject, vm: &VirtualMachine) -> PyResult> { + script_code_with_purity(obj, false, vm) +} + +fn script_code_with_purity( + obj: &PyObject, + pure: bool, + vm: &VirtualMachine, +) -> PyResult> { let code = if let Ok(code) = obj.to_owned().downcast::() { code } else if let Some(func) = obj.downcast_ref::() { @@ -877,6 +905,22 @@ pub fn script_code(obj: &PyObject, vm: &VirtualMachine) -> PyResult() { + verify_stateless_function(func, vm)?; + } else { + let builtins = + crate::frame::current_builtins().unwrap_or_else(|| vm.builtins.dict().into()); + let builtins = builtins.downcast_ref::().ok_or_else(|| { + vm.new_type_error(format!( + "unsupported builtins {}", + render_repr(&builtins, vm) + )) + })?; + let globals = vm.ctx.new_dict(); + verify_stateless(&code, Some((&globals, builtins)), vm)?; + } + } verify_script(&code, vm)?; Ok(code) } diff --git a/crates/vm/src/vm/interpreter.rs b/crates/vm/src/vm/interpreter.rs index b703da7d3ae..a1162ed49d4 100644 --- a/crates/vm/src/vm/interpreter.rs +++ b/crates/vm/src/vm/interpreter.rs @@ -166,6 +166,7 @@ where atexit_funcs: PyMutex::default(), audit_hooks: PyMutex::default(), codec_registry, + import_time_header: AtomicBool::new(false), lazy_imports, struct_format_cache: crate::buffer::FormatSpecCache::default(), finalizing: AtomicBool::new(false), @@ -225,10 +226,12 @@ where // `initialize()` runs Python bytecode directly (e.g. importing `codecs` // and `encodings`) before any `enter_vm` scope exists, so attach this // thread for the duration so type cache reads see it as ATTACHED. - let vm_guard = thread::VmBootstrapGuard::new(&vm); - vm.initialize(); - vm.state.ready.store(true, Ordering::Release); - drop(vm_guard); + if whence != InterpreterWhence::Unknown { + let vm_guard = thread::VmBootstrapGuard::new(&vm); + vm.initialize(); + vm.state.ready.store(true, Ordering::Release); + drop(vm_guard); + } // Clone global_state for Interpreter after all initialization is done let global_state = vm.state.clone(); @@ -239,6 +242,7 @@ where fn create_subinterpreter_from_parent( parent: &VirtualMachine, config: runtime::InterpreterConfig, + whence: InterpreterWhence, ) -> Result { config.check()?; // Suspend the caller's current VM attachment (if any) for the duration @@ -265,7 +269,7 @@ fn create_subinterpreter_from_parent( frozen_modules: Vec::new(), init_hooks: Vec::new(), is_main: false, - whence: InterpreterWhence::Stdlib, + whence, parent_state: Some(&parent.state), interp_config: config, }, @@ -273,10 +277,12 @@ fn create_subinterpreter_from_parent( ); let interp = Interpreter { global_state, vm }; // Every interpreter has a `__main__` module once it is initialized. - interp.enter(|vm| { - let _ = vm.ensure_main_module(); - vm.apply_startup_lazy_imports(); - }); + if whence != InterpreterWhence::Unknown { + interp.enter(|vm| { + let _ = vm.ensure_main_module(); + vm.apply_startup_lazy_imports(); + }); + } Ok(interp) } @@ -574,7 +580,16 @@ impl Interpreter { parent: &VirtualMachine, config: runtime::InterpreterConfig, ) -> Result { - create_subinterpreter_from_parent(parent, config) + create_subinterpreter_from_parent(parent, config, InterpreterWhence::Stdlib) + } + + #[cfg(feature = "threading")] + pub(crate) fn create_subinterpreter_from_vm_with_whence( + parent: &VirtualMachine, + config: runtime::InterpreterConfig, + whence: InterpreterWhence, + ) -> Result { + create_subinterpreter_from_parent(parent, config, whence) } /// Create a subinterpreter with an explicit PEP 734 / `PyInterpreterConfig`. @@ -582,7 +597,7 @@ impl Interpreter { &self, config: runtime::InterpreterConfig, ) -> Result { - create_subinterpreter_from_parent(&self.vm, config) + create_subinterpreter_from_parent(&self.vm, config, InterpreterWhence::Stdlib) } /// Spawn a new OS-thread VM that shares this interpreter's `sys` / builtins. @@ -702,6 +717,11 @@ impl Interpreter { } fn finalize_inner(self, exit_code: impl FnOnce(&VirtualMachine) -> u32) -> u32 { + // A PyInterpreterState_New-style state has never run Python and has + // no modules, streams or shutdown callbacks to finalize. + if !self.vm.initialized { + return 0; + } self.enter(|vm| { let flush_status = vm.flush_std(); diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 0ef153b7248..5094520a61a 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -95,6 +95,7 @@ pub struct VirtualMachine { exceptions: RefCell, pub import_func: PyObjectRef, pub(crate) importlib: PyObjectRef, + pub(crate) import_timing: Cell, pub profile_func: RefCell, pub trace_func: RefCell, pub use_tracing: Cell, @@ -109,6 +110,8 @@ pub struct VirtualMachine { pub(crate) lazy_imports_resolving: RefCell>, pub state: PyRc, pub initialized: bool, + pub(crate) import_bootstrap_complete: bool, + startup_error: Option, recursion_depth: Cell, /// Depth of native recursion that pushes no Python frame, counted only /// where the stack pointer cannot be read. Everywhere else the native @@ -179,6 +182,8 @@ unsafe impl Send for FramePtr {} #[derive(Debug)] struct ExceptionStack { + /// Pending native C API error, independent of handled `sys.exception()`. + raised: Option, /// Linked list of handled-exception slots (`_PyErr_StackItem` chain). /// Bottom element is the thread's base slot; generator/coroutine resume /// pushes an additional slot. Normal frame calls do **not** push/pop. @@ -188,7 +193,10 @@ struct ExceptionStack { impl Default for ExceptionStack { fn default() -> Self { // Thread's base `_PyErr_StackItem` – always present. - Self { stack: vec![None] } + Self { + raised: None, + stack: vec![None], + } } } @@ -875,6 +883,7 @@ pub struct PyGlobalState { /// `sys.addaudithook` hooks, shared by all threads of this interpreter. pub(crate) audit_hooks: PyMutex>, pub codec_registry: CodecsRegistry, + pub(crate) import_time_header: AtomicBool, pub(crate) lazy_imports: crate::lazy_import::LazyImportsState, pub struct_format_cache: crate::buffer::FormatSpecCache, pub finalizing: AtomicBool, @@ -1353,6 +1362,7 @@ impl VirtualMachine { exceptions: RefCell::default(), import_func, importlib, + import_timing: Cell::default(), profile_func, trace_func, use_tracing: Cell::new(false), @@ -1365,6 +1375,8 @@ impl VirtualMachine { lazy_imports_resolving: RefCell::default(), state, initialized: false, + import_bootstrap_complete: false, + startup_error: None, recursion_depth: Cell::new(0), #[cfg(any(miri, target_env = "musl"))] native_recursion_depth: Cell::new(0), @@ -1402,6 +1414,9 @@ impl VirtualMachine { #[cfg(feature = "encodings")] fn import_encodings(&mut self) -> PyResult<()> { self.import("encodings", 0).map_err(|import_err| { + if self.state.config.settings.home.is_some() { + return import_err; + } let rustpythonpath_env = crate::host_env::os::var("RUSTPYTHONPATH").ok(); let pythonpath_env = crate::host_env::os::var("PYTHONPATH").ok(); let env_set = rustpythonpath_env.as_ref().is_some() || pythonpath_env.as_ref().is_some(); @@ -1445,7 +1460,7 @@ impl VirtualMachine { // Use dotted names when freeze-stdlib is enabled (modules come from Lib/encodings/), // otherwise use underscored names (modules come from core_modules/). let (ascii_module_name, utf8_module_name, latin1_module_name) = - if cfg!(feature = "freeze-stdlib") { + if cfg!(feature = "freeze-stdlib") && self.state.config.settings.use_frozen_modules { ("encodings.ascii", "encodings.utf_8", "encodings.latin_1") } else { ("encodings_ascii", "encodings_utf_8", "encodings_latin_1") @@ -1509,7 +1524,7 @@ impl VirtualMachine { drop(prewarmed_memory_errors); stdlib::builtins::init_module(self, &self.builtins); - // Explicit lazy imports can occur in frozen startup code. + // Startup all-mode can reach frozen Python before importlib is installed. let lazy_callback = self.builtins.get_attr("__lazy_import__", self); let lazy_callback = self.expect_pyresult(lazy_callback, "failed to initialize lazy imports"); @@ -1639,6 +1654,7 @@ impl VirtualMachine { let res = essential_init(); let importlib = self.expect_pyresult(res, "essential initialization failed"); + self.import_bootstrap_complete = true; #[cfg(feature = "host_env")] if self.state.config.settings.allow_external_library @@ -1660,10 +1676,14 @@ impl VirtualMachine { #[cfg(feature = "encodings")] if _expect_stdlib { if let Err(e) = self.import_encodings() { - eprintln!( - "encodings initialization failed. Only utf-8 encoding will be supported." - ); - self.print_exception(&e); + if self.state.config.settings.home.is_some() { + self.startup_error = Some(e); + } else { + eprintln!( + "encodings initialization failed. Only utf-8 encoding will be supported." + ); + self.print_exception(&e); + } } } else { // Here may not be the best place to give general `path_list` advice, @@ -1678,6 +1698,46 @@ impl VirtualMachine { self.initialized = true; } + /// Report an invalid explicit standard-library configuration to a command-line + /// host. Embedders may continue using the bootstrap codecs without calling this. + pub fn check_stdlib_initialization(&self) -> PyResult<()> { + let Some(error) = &self.startup_error else { + return Ok(()); + }; + let settings = &self.state.config.settings; + let paths = &self.state.config.paths; + let repr = |value: &str| { + self.ctx + .new_str(value) + .repr(self) + .unwrap_or_else(|_| "?".to_owned()) + }; + eprintln!("Python path configuration:"); + eprintln!( + " PYTHONHOME = {}", + settings + .home + .as_deref() + .map_or_else(|| "(not set)".to_owned(), repr) + ); + eprintln!(" isolated = {}", u8::from(settings.isolated)); + eprintln!(" environment = {}", u8::from(!settings.ignore_environment)); + eprintln!(" sys.executable = {}", repr(&paths.executable)); + eprintln!(" sys.prefix = {}", repr(&paths.prefix)); + eprintln!(" sys.exec_prefix = {}", repr(&paths.exec_prefix)); + eprintln!(" sys.path = ["); + for path in &paths.module_search_paths { + eprintln!(" {},", repr(path)); + } + eprintln!(" ]"); + // A startup import failure is fatal even if the imported module raised + // SystemExit(0) or SystemExit(None). Keep its exception as the cause, + // without passing its exit code to ordinary user-code finalization. + let startup_error = self.new_runtime_error("Failed to import encodings module"); + startup_error.set_cause(Some(error.clone())); + Err(startup_error) + } + /// Apply configured global lazy imports after startup imports, including site. /// Hosts that perform their own main-module setup can call this at that boundary. pub fn apply_startup_lazy_imports(&self) { @@ -1750,7 +1810,16 @@ impl VirtualMachine { #[cold] pub fn run_unraisable(&self, e: PyBaseExceptionRef, msg: Option, object: PyObjectRef) { - let msg = msg.map(|msg| self.ctx.new_str(msg)); + self.run_unraisable_with_message(e, msg.map(|msg| self.ctx.new_str(msg)), object); + } + + #[cold] + pub(crate) fn run_unraisable_with_message( + &self, + e: PyBaseExceptionRef, + msg: Option, + object: PyObjectRef, + ) { let write_original = || { let msg = msg.as_ref().map(|msg| msg.to_string_lossy()); self.write_unraisable_to_stderr(&e, msg.as_deref(), &object); @@ -3373,6 +3442,7 @@ impl VirtualMachine { && builtins.is(self.builtins.dict().as_object()) && let Some(cached) = self.try_import_cached(module)? { + import::report_cached_import(module, self); return Ok(cached); } @@ -3895,17 +3965,18 @@ impl VirtualMachine { } } + pub fn raised_exception(&self) -> Option { + self.exceptions.borrow().raised.clone() + } + + pub fn set_raised_exception(&self, exc: Option) { + // Release the borrow before dropping an exception can invoke __del__. + let previous = core::mem::replace(&mut self.exceptions.borrow_mut().raised, exc); + drop(previous); + } + pub fn take_raised_exception(&self) -> Option { - let mut excs = self.exceptions.borrow_mut(); - if let Some(top) = excs.stack.last_mut() { - let exc = top.take(); - drop(excs); - #[cfg(feature = "threading")] - thread::update_thread_exception(self.topmost_exception()); - exc - } else { - None - } + self.exceptions.borrow_mut().raised.take() } /// `_PyErr_ChainStackItem`: if the current `exc_info` slot is occupied, diff --git a/crates/vm/src/vm/python_run.rs b/crates/vm/src/vm/python_run.rs index a9fcb56ca63..e40bca944cc 100644 --- a/crates/vm/src/vm/python_run.rs +++ b/crates/vm/src/vm/python_run.rs @@ -83,7 +83,7 @@ mod file_run { /// _PyRun_SimpleFileObject /// /// Execute a Python file with __main__ module setup. - /// Sets __file__ and __cached__ before execution, removes them after. + /// Sets __file__ before execution and removes it after. fn run_simple_file(&self, scope: Scope, path: &str) -> PyResult<()> { self.with_simple_run(path, |module_dict| { self.run_simple_file_inner(module_dict, scope, path) @@ -128,7 +128,14 @@ mod file_run { let source = String::from_utf8(source_bytes) .map_err(|err| self.new_os_error(err.to_string()))?; let code_obj = self - .compile(&source, compiler::Mode::Exec, path) + .compile_with_opts_and_module( + &source, + compiler::Mode::Exec, + path, + self.compile_opts(), + Some(identifier!(self, __main__).as_object()), + &[], + ) .map_err(|err| err.into_pyexception(self, Some(&source)))?; self.run_code_obj(code_obj, scope)?; } @@ -450,7 +457,7 @@ impl VirtualMachine { mode, filename, opts, - Some(identifier!(self, __main__)), + Some(identifier!(self, __main__).as_object()), ) .map_err(|error| error.into_pyexception(self, Some(source)))?; let linecache = self.import("linecache", 0)?; @@ -544,7 +551,7 @@ impl VirtualMachine { compiler::Mode::Exec, "", self.compile_opts(), - Some(identifier!(self, __main__)), + Some(identifier!(self, __main__).as_object()), &[], ) .map_err(|error| error.into_pyexception(self, Some(&source)))?; diff --git a/crates/vm/src/vm/runtime.rs b/crates/vm/src/vm/runtime.rs index 3f447fcbd3c..59104d85293 100644 --- a/crates/vm/src/vm/runtime.rs +++ b/crates/vm/src/vm/runtime.rs @@ -296,6 +296,10 @@ pub(crate) fn alloc_interpreter_id() -> i64 { registry().next_id.fetch_add(1, Ordering::Relaxed) } +pub(crate) fn next_interpreter_id() -> i64 { + registry().next_id.load(Ordering::Relaxed) +} + /// Gate between registering an interpreter and a collection's stop-the-world. /// /// A collection snapshots the registry, stops every interpreter in the diff --git a/crates/vm/src/vm/setting.rs b/crates/vm/src/vm/setting.rs index 9f3ef85f7d4..c2a2c01bad1 100644 --- a/crates/vm/src/vm/setting.rs +++ b/crates/vm/src/vm/setting.rs @@ -64,7 +64,8 @@ pub struct Settings { // int tracemalloc; // int perf_profiling; - // int import_time; + /// -X importtime, PYTHONPROFILEIMPORTTIME: 2 also reports cached imports. + pub import_time: u8, /// -X no_debug_ranges: disable column info in bytecode pub code_debug_ranges: bool, // int show_ref_count; @@ -138,7 +139,8 @@ pub struct Settings { /// --check-hash-based-pycs pub check_hash_pycs_mode: CheckHashPycsMode, - // int use_frozen_modules; + /// -X frozen_modules=on|off (bootstrap modules remain available). + pub use_frozen_modules: bool, /// -P pub safe_path: bool, @@ -203,6 +205,7 @@ impl Default for Settings { install_signal_handlers: true, user_site_directory: true, import_site: true, + use_frozen_modules: true, ignore_environment: false, verbose: 0, quiet: false, @@ -222,6 +225,7 @@ impl Default for Settings { argv: vec![], hash_seed: None, faulthandler: false, + import_time: 0, code_debug_ranges: true, buffered_stdio: true, check_hash_pycs_mode: CheckHashPycsMode::Default, diff --git a/crates/vm/src/vm/thread.rs b/crates/vm/src/vm/thread.rs index 8d0aa70e6eb..0221d298bbe 100644 --- a/crates/vm/src/vm/thread.rs +++ b/crates/vm/src/vm/thread.rs @@ -398,6 +398,9 @@ pub fn release_current_thread(state: CurrentVmAttachState) { return; } + // Clear Python thread-local values before detaching, while their + // destructors can still run in this native thread's VM context. + crate::stdlib::_thread::cleanup_thread_local_data(); let gilstate_vm = GILSTATE_VM.with(|gilstate_vm| gilstate_vm.borrow_mut().take()); drop(gilstate_vm); @@ -1467,6 +1470,7 @@ impl VirtualMachine { exceptions: RefCell::default(), import_func: self.import_func.clone(), importlib: self.importlib.clone(), + import_timing: Cell::default(), profile_func: RefCell::new(global_profile.unwrap_or_else(|| self.ctx.none())), trace_func: RefCell::new(global_trace.unwrap_or_else(|| self.ctx.none())), use_tracing: Cell::new(use_tracing), @@ -1479,6 +1483,8 @@ impl VirtualMachine { lazy_imports_resolving: RefCell::default(), state: self.state.clone(), initialized: self.initialized, + import_bootstrap_complete: self.import_bootstrap_complete, + startup_error: self.startup_error.clone(), recursion_depth: Cell::new(0), #[cfg(any(miri, target_env = "musl"))] native_recursion_depth: Cell::new(0), diff --git a/crates/vm/src/vm/vm_new.rs b/crates/vm/src/vm/vm_new.rs index 217d63ab267..88c311fcbbd 100644 --- a/crates/vm/src/vm/vm_new.rs +++ b/crates/vm/src/vm/vm_new.rs @@ -517,7 +517,7 @@ impl VirtualMachine { ), .. }) => incomplete_or_syntax(allow_incomplete), - // Unclosed bracket errors (converted from Eof by from_ruff_parse_error) + // The parser distinguishes incomplete brackets from invalid earlier tokens. #[cfg(feature = "parser")] crate::compiler::CompileError::Parse(rustpython_compiler::ParseError { is_unclosed_bracket: true, @@ -730,7 +730,23 @@ impl VirtualMachine { // Set end_lineno and end_offset if available if let Some((end_lineno, end_offset)) = error.python_end_location() { // EOF errors have no source span in CPython. + let standalone_case_suite = cfg_select! { + feature = "parser" => { + matches!( + error, + crate::compiler::CompileError::Parse(rustpython_compiler::ParseError { + error: ruff_python_parser::ParseErrorType::ExpectedIndentedBlock { + clause: ruff_python_parser::BlockClause::StandaloneCase, + .. + }, + .. + }) + ) + } + _ => false, + }; let no_end_offset = unexpected_eof_error + || standalone_case_suite || (check_version_suite_error && statement .as_deref() diff --git a/crates/vm/src/warn.rs b/crates/vm/src/warn.rs index dee57e50659..e336509296a 100644 --- a/crates/vm/src/warn.rs +++ b/crates/vm/src/warn.rs @@ -103,7 +103,7 @@ impl WarningsState { fn check_matched( obj: &PyObject, arg: Option<&PyObject>, - arg2: Option<&Py>, + filename: Option<&Py>, vm: &VirtualMachine, ) -> PyResult { // A 'None' filter always matches @@ -117,15 +117,19 @@ fn check_matched( None => Ok(false), }; } - // Otherwise assume a regex filter and call its match() method - let result = match arg { - Some(arg) => vm.call_method(obj, "match", (arg.to_owned(),))?, - None => { - let match_filename = vm - .import("_py_warnings", 0)? - .get_attr("_match_filename", vm)?; - match_filename.call((obj.to_owned(), arg2.map(|f| f.to_owned())), vm)? - } + let result = if let Some(arg) = arg { + vm.call_method(obj, "match", (arg.to_owned(),))? + } else { + let helper = vm + .import("_py_warnings", 0)? + .get_attr("_match_filename", vm)?; + helper.call( + ( + obj.to_owned(), + filename.expect("filename for omitted module").to_owned(), + ), + vm, + )? }; result.is_true(vm) } @@ -519,10 +523,8 @@ pub fn warn_explicit( source: Option, vm: &VirtualMachine, ) -> PyResult<()> { - // module can be None if a warning is emitted late during Python shutdown. - // In this case, the Python warnings module was probably unloaded, filters - // are no more available to choose as action. It is safer to ignore the - // warning and do nothing. + // Explicit None remains the late-shutdown sentinel. An omitted module + // selects the filename-pattern matching added in Python 3.15. if module.as_ref().is_some_and(|module| vm.is_none(module)) { return Ok(()); } @@ -645,8 +647,7 @@ fn call_show_warning( module: Option, vm: &VirtualMachine, ) -> PyResult<()> { - let Some(show_fn) = get_warnings_attr(vm, identifier!(vm, _showwarnmsg), source.is_some())? - else { + let Some(show_fn) = get_warnings_attr(vm, identifier!(vm, _showwarnmsg), true)? else { show_warning(&filename, lineno, text, &category, source_line, vm); return Ok(()); }; diff --git a/extra_tests/frozendict_contract.py b/extra_tests/frozendict_contract.py new file mode 100644 index 00000000000..68d872323db --- /dev/null +++ b/extra_tests/frozendict_contract.py @@ -0,0 +1,57 @@ +"""Native frozendict hash reentry and GC ownership regressions.""" + +import gc +import unittest + + +class FrozenDictContractTests(unittest.TestCase): + def test_value_hash_can_reenter_same_frozendict(self): + class Reentrant: + def __init__(self): + self.calls = 0 + self.owner = None + self.nested = None + + def __hash__(self): + self.calls += 1 + if self.calls == 1: + self.nested = hash(self.owner) + return 127 + + value = Reentrant() + frozen = frozendict(recursive=value) + value.owner = frozen + result = hash(frozen) + self.assertEqual(result, value.nested) + self.assertEqual(hash(frozen), result) + self.assertEqual(value.calls, 2) + + def test_gc_does_not_expose_mutable_backing_mapping(self): + marker = object() + frozen = frozendict(private_payload=marker) + for referent in gc.get_referents(frozen): + self.assertFalse(isinstance(referent, dict)) + self.assertIs(frozen["private_payload"], marker) + + def test_gc_does_not_publish_partly_constructed_mapping(self): + marker = object() + observed = [] + + def source(): + yield "private_payload", marker + observed.extend( + candidate + for candidate in gc.get_objects() + if type(candidate) is frozendict + and candidate.get("private_payload") is marker + ) + yield "finished", True + + frozen = frozendict(source()) + self.assertEqual(observed, []) + self.assertIs(frozen["private_payload"], marker) + self.assertIs(frozen["finished"], True) + + +if __name__ == "__main__": + unittest.main() diff --git a/extra_tests/snippets/builtin_memoryview.py b/extra_tests/snippets/builtin_memoryview.py index ea0475d7c63..4b7ca742d86 100644 --- a/extra_tests/snippets/builtin_memoryview.py +++ b/extra_tests/snippets/builtin_memoryview.py @@ -517,7 +517,7 @@ def test_fortran_contiguity(): def test_cast_arguments(): - # cast() takes a native single character format, optionally '@'-prefixed; + # cast() takes a native format, optionally '@'-prefixed; # a zero-size format used to reach a division by zero. assert memoryview(b"abcd").cast("@i").itemsize == 4 for fmt in ("0s", "4s", "&2\n" + "exit 86\n", + encoding="utf-8", + ) + os.chmod(decoy, 0o755) + + def run(command, *, expected_returncode=0): + remaining = deadline - time.monotonic() + assert remaining > 0, "venv nested regression exceeded its 55s budget" + command = [str(arg) for arg in command] + try: + result = subprocess.run( + command, + cwd=unrelated_cwd, + env=child_env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=min(15.0, remaining), + ) + except OSError as exc: + raise AssertionError(f"Cannot launch {command!r}: {exc}") from exc + assert result.returncode == expected_returncode, ( + f"Command {command!r} exited {result.returncode}; " + f"expected {expected_returncode}\n" + f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + return result.stdout + + def check_identity(executable, prefix=None): + result = json.loads(run([executable, "-I", "-c", identity_code])) + assert result["implementation"] == "rustpython", result + assert result["base_exists"], result + assert os.path.realpath(result["base"]) == os.path.realpath(base), result + if prefix is not None: + assert os.path.realpath(result["prefix"]) == os.path.realpath(prefix), ( + result + ) + + check_identity(base) + run([decoy], expected_returncode=86) + + for outer_mode in ("copies", "symlinks"): + outer = work / f"outer-{outer_mode}" + run([base, "-I", "-m", "venv", "--without-pip", f"--{outer_mode}", outer]) + check_identity(outer / "bin" / base.name, outer) + alias = outer / "bin" / "python" + check_identity(alias, outer) + + nested_environments = [] + for nested_mode in ("copies", "symlinks"): + nested = work / f"nested-{outer_mode}-{nested_mode}" + # The common python alias is the trigger. Both modes must use + # the original base even though unrelated home/python exists. + run( + [ + alias, + "-I", + "-m", + "venv", + "--without-pip", + f"--{nested_mode}", + nested, + ] + ) + check_identity(nested / "bin" / base.name, nested) + check_identity(nested / "bin" / "python", nested) + nested_environments.append(nested) + + # A copied nested environment's creator metadata names its parent + # venv. Stable base metadata must remain usable after that parent + # and its copied interpreter are gone. + shutil.rmtree(outer) + for nested in nested_environments: + check_identity(nested / "bin" / base.name, nested) + check_identity(nested / "bin" / "python", nested) + shutil.rmtree(nested) + print( + f"PASS: outer {outer_mode}; nested copies/symlinks survive parent deletion" + ) + + print("PASS: nested venv aliases preserve the RustPython base interpreter") + + +if __name__ == "__main__": + main() diff --git a/extra_tests/snippets/stdlib_warning_followups_315.py b/extra_tests/snippets/stdlib_warning_followups_315.py new file mode 100644 index 00000000000..72672ed4790 --- /dev/null +++ b/extra_tests/snippets/stdlib_warning_followups_315.py @@ -0,0 +1,42 @@ +"""Native warning metadata and argument-precedence regressions.""" + +import _warnings +import unittest +import warnings + + +class WarningFollowupTests(unittest.TestCase): + def test_invalid_module_name_normalizes(self): + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter("always") + exec("_warnings.warn('metadata')", {"_warnings": _warnings, "__name__": 4}) + self.assertEqual(len(recorded), 1) + self.assertEqual(recorded[0].module, "") + + def test_module_globals_type_diagnostic(self): + Namespace = type( + "Namespace", + (), + {"__module__": "reviewpkg", "__qualname__": "Outer.Namespace"}, + ) + with self.assertRaises(TypeError) as caught: + _warnings.warn_explicit( + "msg", UserWarning, "foo.py", 1, module_globals=Namespace() + ) + self.assertEqual( + str(caught.exception), + "module_globals must be a dict or a frozendict, not reviewpkg.Outer.Namespace", + ) + + def test_warning_instance_ignores_category(self): + warning = UserWarning("original instance") + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter("always") + _warnings.warn(warning, int) + self.assertEqual(len(recorded), 1) + self.assertIs(recorded[0].message, warning) + self.assertIs(recorded[0].category, UserWarning) + + +if __name__ == "__main__": + unittest.main() diff --git a/extra_tests/snippets/stdlib_warning_module_315.py b/extra_tests/snippets/stdlib_warning_module_315.py new file mode 100644 index 00000000000..1fd881de823 --- /dev/null +++ b/extra_tests/snippets/stdlib_warning_module_315.py @@ -0,0 +1,34 @@ +"""Native omitted-module and frozen warning-global boundaries.""" + +import unittest +import warnings + + +class WarningModuleTests(unittest.TestCase): + def test_omitted_and_explicit_none(self): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + warnings.warn_explicit("omitted", UserWarning, "/tmp/pkg/mod.py", 1) + warnings.warn_explicit( + "none", UserWarning, "/tmp/pkg/mod.py", 1, module=None + ) + self.assertEqual([str(x.message) for x in caught], ["omitted"]) + self.assertIsNone(caught[0].module) + + def test_frozen_module_globals(self): + namespace = frozendict(__name__="warning_probe", __spec__=None) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + warnings.warn_explicit( + "message", + UserWarning, + "probe.py", + 1, + module="warning_probe", + module_globals=namespace, + ) + self.assertEqual(caught[0].module, "warning_probe") + + +if __name__ == "__main__": + unittest.main() diff --git a/extra_tests/snippets/stdlib_weakref_gc_315.py b/extra_tests/snippets/stdlib_weakref_gc_315.py new file mode 100644 index 00000000000..eac54116830 --- /dev/null +++ b/extra_tests/snippets/stdlib_weakref_gc_315.py @@ -0,0 +1,61 @@ +"""Regressions for callbacks created or retained during cyclic collection.""" + +import gc +import unittest +import weakref + + +class WeakrefGcOrderingTests(unittest.TestCase): + def setUp(self): + self.was_enabled = gc.isenabled() + gc.disable() + gc.collect() + + def tearDown(self): + gc.collect() + if self.was_enabled: + gc.enable() + + def test_new_callback_in_finalizer_is_not_called(self): + refs = [] + callbacks = [] + + class Class: + def __del__(self): + refs.append(weakref.ref(self, lambda wr: callbacks.append("called"))) + + obj = Class() + obj.cycle = obj + del obj + gc.collect() + self.assertEqual(len(refs), 1) + self.assertIsNone(refs[0]()) + self.assertEqual(callbacks, []) + + def test_gc_keeps_callback_alive_until_weakref_dies(self): + calls = [] + + class Class: + pass + + class Callback: + def __call__(self, wr): + calls.append(wr() is None) + + obj = Class() + obj.cycle = obj + callback = Callback() + callback_observer = weakref.ref(callback) + wr = weakref.ref(obj, callback) + del callback, obj + gc.collect() + self.assertIsNone(wr()) + self.assertIs(wr.__callback__, callback_observer()) + self.assertEqual(calls, [True]) + self.assertIsNotNone(callback_observer()) + del wr + self.assertIsNone(callback_observer()) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/extra_tests/snippets/stdlib_weakref_gc_threads_315.py b/extra_tests/snippets/stdlib_weakref_gc_threads_315.py new file mode 100644 index 00000000000..4087211a5bb --- /dev/null +++ b/extra_tests/snippets/stdlib_weakref_gc_threads_315.py @@ -0,0 +1,103 @@ +"""Threaded weakref resurrection and late-upgrade stress; run in a subprocess.""" + +import gc +import threading +import time +import unittest +import weakref + + +class WeakrefThreadTests(unittest.TestCase): + def test_other_thread_can_resurrect_during_finalizer(self): + finalizing = threading.Event() + acquired = threading.Event() + held = [] + errors = [] + marker = object() + + class Target: + def __del__(self): + finalizing.set() + if not acquired.wait(5): + errors.append("thread did not finish weakref upgrade") + + obj = Target() + obj.cycle = obj + obj.marker = marker + observer = weakref.ref(obj) + + def upgrade(): + if not finalizing.wait(5): + errors.append("finalizer did not run") + else: + target = observer() + if target is None: + errors.append("callback-free weakref cleared before finalizer") + else: + held.append(target) + acquired.set() + + thread = threading.Thread(target=upgrade) + thread.start() + del obj + gc.collect() + thread.join(5) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + self.assertEqual(len(held), 1) + self.assertIs(observer(), held[0]) + self.assertIs(held[0].marker, marker) + held.clear() + gc.collect() + self.assertIsNone(observer()) + + def test_concurrent_upgrade_never_sees_cleared_payload(self): + enabled = gc.isenabled() + gc.disable() + marker = object() + refs = () + stop = threading.Event() + ready = threading.Event() + errors = [] + + class Target: + pass + + def upgrade(): + ready.set() + while not stop.is_set(): + for ref in refs: + obj = ref() + if obj is not None: + time.sleep(0) + if getattr(obj, "marker", None) is not marker: + errors.append("live upgraded reference has cleared payload") + del obj + time.sleep(0) + + worker = threading.Thread(target=upgrade) + worker.start() + try: + self.assertTrue(ready.wait(5)) + for _ in range(30): + batch = [] + for _ in range(64): + obj = Target() + obj.marker = marker + obj.cycle = obj + batch.append(weakref.ref(obj)) + del obj + refs = tuple(batch) + time.sleep(0) + gc.collect() + self.assertEqual(errors, []) + finally: + stop.set() + worker.join(5) + if enabled: + gc.enable() + self.assertFalse(worker.is_alive()) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/ruff.toml b/ruff.toml index 7cfaafdb08a..97cd17047f6 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,6 @@ exclude = [ "Lib", + "Parser", # Preserve the pinned CPython source-build fixtures. "vm/Lib", "benches", "syntax_*.py", # Do not format files that are specifically testing for syntax diff --git a/scripts/dis_dump.py b/scripts/dis_dump.py index 813de22e658..3bba4916c88 100755 --- a/scripts/dis_dump.py +++ b/scripts/dis_dump.py @@ -170,12 +170,20 @@ def _resolve_arg_fallback(code, opname, arg): "LOAD_ATTR", "STORE_ATTR", "DELETE_ATTR", - "IMPORT_NAME", "IMPORT_FROM", "LOAD_FROM_DICT_OR_GLOBALS", ): if 0 <= arg < len(code.co_names): return code.co_names[arg] + elif opname == "IMPORT_NAME": + name_idx = arg >> 2 + if 0 <= name_idx < len(code.co_names): + name = code.co_names[name_idx] + if arg & 1: + return f"{name} + lazy" + if arg & 2: + return f"{name} + eager" + return name elif opname == "LOAD_SUPER_ATTR": name_idx = arg >> 2 if 0 <= name_idx < len(code.co_names): diff --git a/scripts/generate_capi_exports.py b/scripts/generate_capi_exports.py new file mode 100644 index 00000000000..fd983cbda5f --- /dev/null +++ b/scripts/generate_capi_exports.py @@ -0,0 +1,212 @@ +"""Generate Windows C API exports from implemented definitions. + +Exports are Stable ABI definitions plus the explicitly listed existing +non-Stable ABI compatibility function. + +Maintenance dependencies: Python 3.11+, Git, the pinned CPython checkout, and +RustPython source. Compiled verification additionally needs GNU nm and a matching +ELF cdylib. Source-only generation explicitly leaves compiled verification open. +""" + +import argparse +import hashlib +import re +import subprocess +import sys +from pathlib import Path + +import tomllib + +CPYTHON_REVISION = "d220409a13b77de7a6c30abe16ea90db01528edc" +MANIFEST_SHA256 = "eab726f2482c71e67a23ea04aaa9c5a11d565bee4472320f813262f273c1e966" +EXPORT_ATTR = "#[unsafe(no_mangle)]" +ITEM_PREFIX = r"#\[unsafe\(no_mangle\)\](?:\s*#\[[^\n]*\])*\s*pub\s+" +# Already implemented and exported on Unix; needed for Windows frame access. +# This function is not part of CPython's Stable ABI. +NON_STABLE_COMPATIBILITY_EXPORTS = {"PyFrame_GetLocals": "function"} + + +def fail(message): + raise SystemExit(message) + + +def source_symbols(repo): + symbols = {} + macro_templates = {"object.rs": 2, "pyerrors.rs": 1, "monitoring.rs": 1} + for path in sorted((repo / "crates/capi/src").rglob("*.rs")): + source, _, tests = path.read_text().partition("#[cfg(test)]") + if EXPORT_ATTR in tests: + fail(f"Review exports inside/after the test module: {path}") + if "export_name" in source: + fail(f"Review new export_name syntax: {path}") + functions = re.findall( + ITEM_PREFIX + r'(?:unsafe\s+)?extern\s+"C"\s+fn\s+([A-Za-z_]\w*)', + source, + ) + data = re.findall( + ITEM_PREFIX + r"static\s+(?:mut\s+)?((?!mut\b)[A-Za-z_]\w*)\s*:", + source, + ) + accounted = len(functions) + len(data) + macro_templates.get(path.name, 0) + if source.count(EXPORT_ATTR) != accounted: + fail(f"Unrecognized no_mangle item or macro template: {path}") + functions += re.findall(r"define_py_check!\(\s*(?:exact\s+)?fn\s+(\w+)", source) + functions += re.findall(r"^fire_event!\((\w+)", source, re.MULTILINE) + data += re.findall(r"^\s*(PyExc_\w+)\s*=>", source, re.MULTILINE) + # Current production cfg only selects private FfiResult implementations. + # New conditional modules/exports require an explicit target mapping. + if re.search(r"#\[cfg(?:_attr)?\b", source) and ( + path.name != "util.rs" or functions or data or re.search(r"\bmod\s", source) + ): + fail(f"Production cfg needs an explicit target-aware review: {path}") + for kind, names in (("function", functions), ("data", data)): + for name in names: + if name in symbols: + fail(f"Duplicate source export: {name}") + symbols[name] = kind + if not symbols: + fail("No source C API exports found") + return symbols + + +def compiled_symbols(library, nm): + output = subprocess.check_output( + [nm, "-D", "--defined-only", str(library)], text=True + ) + symbols = {} + for line in output.splitlines(): + fields = line.split() + if len(fields) != 3 or not fields[2].startswith(("Py", "_Py")): + continue + _, kind, name = fields + if kind in "TW": + symbols[name] = "function" + elif kind in "BDGRSV": + symbols[name] = "data" + else: + fail(f"Unrecognized nm symbol kind: {line}") + if not symbols: + fail("No C API definitions found in the supplied ELF library") + return symbols + + +def generate(args): + manifest_bytes = (args.cpython / "Misc/stable_abi.toml").read_bytes() + revision = subprocess.check_output( + ["git", "-C", str(args.cpython), "rev-parse", "HEAD"], text=True + ).strip() + if revision != CPYTHON_REVISION: + fail(f"CPython reference must be {CPYTHON_REVISION}, got {revision}") + if hashlib.sha256(manifest_bytes).hexdigest() != MANIFEST_SHA256: + fail("The pinned stable ABI manifest has changed") + manifest = tomllib.loads(manifest_bytes.decode()) + source = source_symbols(args.repo) + if args.source_only: + if args.allow_unbuilt: + fail("--allow-unbuilt requires an actual --library comparison") + print("SOURCE-ONLY: ELF and PE verification are pending", file=sys.stderr) + else: + compiled = compiled_symbols(args.library, args.nm) + missing = source.keys() - compiled.keys() + unexpected = compiled.keys() - source.keys() + allowed = set(args.allow_unbuilt) + if missing != allowed or unexpected: + fail( + f"Source/library mismatch: unbuilt={sorted(missing)}, stale={sorted(unexpected)}" + ) + for name in source.keys() & compiled.keys(): + if source[name] != compiled[name]: + fail(f"Source/library kind mismatch: {name}") + if allowed: + print( + f"UNVERIFIED compiled definitions: {', '.join(sorted(allowed))}", + file=sys.stderr, + ) + print(f"{len(compiled)} compiled exports compared") + exports = {} + for name, kind in source.items(): + entry = manifest.get(kind, {}).get(name) + if entry is None: + continue + if "ifdef" in entry: + fail(f"Review target condition for {name}: {entry['ifdef']}") + exports[name] = kind + stable_count = len(exports) + for name, kind in NON_STABLE_COMPATIBILITY_EXPORTS.items(): + if source.get(name) != kind: + fail(f"Missing or mismatched compatibility definition: {name}") + if name in manifest.get(kind, {}): + fail(f"Remove the now-stable compatibility exception for {name}") + exports[name] = kind + contents = ( + "; Generated by scripts/generate_capi_exports.py; do not edit by hand.\n" + f"; CPython v3.15.0: {CPYTHON_REVISION}\n" + f"; Misc/stable_abi.toml SHA-256: {MANIFEST_SHA256}\n" + "; Implemented Stable ABI functions/data, including ABI-only names.\n" + "; Non-Stable ABI compatibility export: PyFrame_GetLocals.\n" + "EXPORTS\n" + ) + "".join( + f" {name}{' DATA' if kind == 'data' else ''}\n" + for name, kind in sorted(exports.items()) + ) + if args.check: + if args.output.read_text() != contents: + fail(f"Export list is stale: {args.output}") + else: + args.output.write_text(contents) + print( + f"{len(source)} source exports; {len(exports)} Windows exports " + f"({stable_count} Stable ABI, {len(NON_STABLE_COMPATIBILITY_EXPORTS)} non-Stable ABI); " + f"({sum(kind == 'data' for kind in exports.values())} data)" + ) + + +def verify_pe(args): + expected = { + line.split()[0] + for line in args.definition.read_text().splitlines() + if line.strip() and not line.startswith(";") and line.strip() != "EXPORTS" + } + actual = set( + re.findall( + r"^\s*\d+\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s+((?:_?Py)\w*)\b", + args.dumpbin.read_text(encoding="utf-8-sig"), + re.MULTILINE, + ) + ) + if actual != expected: + fail( + f"PE export mismatch: missing={sorted(expected - actual)}, unexpected={sorted(actual - expected)}" + ) + print(f"Verified {len(actual)} C API names in the Windows PE export table") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + modes = parser.add_subparsers(dest="mode", required=True) + generation = modes.add_parser("generate") + generation.add_argument( + "--repo", type=Path, default=Path(__file__).resolve().parents[1] + ) + generation.add_argument("--cpython", type=Path, required=True) + evidence = generation.add_mutually_exclusive_group(required=True) + evidence.add_argument("--library", type=Path) + evidence.add_argument("--source-only", action="store_true") + generation.add_argument("--nm", default="nm") + generation.add_argument("--output", type=Path, required=True) + generation.add_argument("--check", action="store_true") + generation.add_argument( + "--allow-unbuilt", action="append", default=[], choices=["PyCFunction_Call"] + ) + pe = modes.add_parser("verify-pe") + pe.add_argument("--definition", type=Path, required=True) + pe.add_argument("--dumpbin", type=Path, required=True) + args = parser.parse_args() + if args.mode == "generate": + generate(args) + else: + verify_pe(args) + + +if __name__ == "__main__": + main() diff --git a/scripts/update_lib/deps.py b/scripts/update_lib/deps.py index 129faf3dea6..6ce85750582 100644 --- a/scripts/update_lib/deps.py +++ b/scripts/update_lib/deps.py @@ -701,6 +701,8 @@ def clear_import_graph_caches() -> None: "test__interpreters.py", "test__interpchannels.py", "test_crossinterp.py", + "_crossinterp_definitions.py", + "_code_definitions.py", ], }, "atexit": { diff --git a/src/interpreter.rs b/src/interpreter.rs index c2ddf8cc5fa..303b0cb3c98 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -55,6 +55,11 @@ fn install_default_tls_provider(_vm: &mut crate::VirtualMachine) { fn set_frozen_stdlib_dir(vm: &mut crate::VirtualMachine) { use rustpython_vm::common::rc::PyRc; + if !vm.state.config.settings.use_frozen_modules { + setup_dynamic_stdlib(vm); + return; + } + if use_build_stdlib(vm) { let state = PyRc::get_mut(&mut vm.state).unwrap(); state.config.paths.stdlib_dir = @@ -63,7 +68,7 @@ fn set_frozen_stdlib_dir(vm: &mut crate::VirtualMachine) { } /// Setup dynamic standard library loading from filesystem -#[cfg(all(feature = "stdlib", not(feature = "freeze-stdlib")))] +#[cfg(feature = "stdlib")] fn setup_dynamic_stdlib(vm: &mut crate::VirtualMachine) { use rustpython_vm::common::rc::PyRc; @@ -117,7 +122,7 @@ fn canonical_stdlib_path(path: String) -> String { } /// Collect standard library paths from build-time configuration -#[cfg(all(feature = "stdlib", not(feature = "freeze-stdlib")))] +#[cfg(feature = "stdlib")] fn collect_stdlib_paths() -> Vec { // BUILDTIME_RUSTPYTHONPATH should be set when distributing if let Some(paths) = option_env!("BUILDTIME_RUSTPYTHONPATH") { diff --git a/src/lib.rs b/src/lib.rs index 2f6ee077f53..d962c38723a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,6 +121,12 @@ pub fn run(mut builder: InterpreterBuilder) -> ExitCode { builder = builder.settings(settings); + #[cfg(feature = "capi")] + { + let def = rustpython_vm::stdlib::capi_test_module_def(&builder.ctx); + builder = builder.add_native_module(def); + } + let interp = builder.interpreter(); let exitcode = cfg_select! { feature = "capi" => rustpython_capi::run_main_interpreter(interp, move |vm| run_rustpython(vm, run_mode)), @@ -314,11 +320,18 @@ fn run_rustpython(vm: &VirtualMachine, run_mode: RunMode) -> u32 { // pymain_run_python fn pymain_run_python(vm: &VirtualMachine, run_mode: RunMode) -> PyResult { + vm.check_stdlib_initialization()?; let scope = vm.new_scope_with_main()?; - // Initialize warnings module to process sys.warnoptions - // _PyWarnings_Init() - if vm.import("warnings", 0).is_err() { + // The native warnings state already holds the default filters. Import the + // Python module only when startup options require processing (-W, -b, dev). + let warnoptions = vm.sys_module.dict().get_item_opt("warnoptions", vm)?; + if warnoptions + .as_ref() + .and_then(|options| options.downcast_ref::()) + .is_some_and(|options| options.__len__() > 0) + && vm.import("warnings", 0).is_err() + { warn!("Failed to import warnings module"); } diff --git a/src/settings.rs b/src/settings.rs index a5b210a5f2c..15e322d2ada 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -309,8 +309,18 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { settings.lazy_imports = parse_lazy_imports(val.to_str(), "PYTHON_LAZY_IMPORTS"); } - // CPython uses the first occurrence of this -X option. + if let Some(value) = get_env("PYTHONPROFILEIMPORTTIME").filter(|value| !value.is_empty()) { + settings.import_time = parse_import_time(value.to_str(), "PYTHONPROFILEIMPORTTIME"); + } + + if let Some(value) = get_env("PYTHON_FROZEN_MODULES").filter(|value| !value.is_empty()) { + settings.use_frozen_modules = parse_frozen_modules(value.to_str(), "PYTHON_FROZEN_MODULES"); + } + + // CPython uses the first occurrence of these -X options. let mut lazy_imports_option_seen = false; + let mut import_time_option_seen = false; + let mut frozen_modules_option_seen = false; let xopts = args.implementation_option.into_iter().map(|s| { let (name, value) = match s.split_once('=') { Some((name, value)) => (name.to_owned(), Some(value)), @@ -318,6 +328,14 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { }; match &*name { "dev" => settings.dev_mode = true, + "importtime" if !import_time_option_seen => { + settings.import_time = parse_import_time(value, "-X importtime"); + import_time_option_seen = true; + } + "frozen_modules" if !frozen_modules_option_seen => { + settings.use_frozen_modules = parse_frozen_modules(value, "-X frozen_modules"); + frozen_modules_option_seen = true; + } "faulthandler" => settings.faulthandler = true, "warn_default_encoding" => settings.warn_default_encoding = true, "utf8" => { @@ -504,6 +522,40 @@ fn parse_cpu_count(value: Option<&str>) -> Result, ()> { } } +fn parse_frozen_modules(value: Option<&str>, source: &str) -> bool { + match value { + None | Some("" | "on") => true, + Some("off") => false, + _ => { + eprintln!( + "Fatal Python error: config_init_import: bad value for {source} (expected \"on\" or \"off\")" + ); + std::process::exit(1); + } + } +} + +/// = config_init_import_time +fn parse_import_time(value: Option<&str>, source: &str) -> u8 { + let value = value + .and_then(|value| { + value + .trim_start_matches([' ', '\t', '\n', '\r', '\x0b', '\x0c']) + .parse::() + .ok() + }) + .unwrap_or(1); + if !(0..=2).contains(&value) { + eprintln!( + "Fatal Python error: config_init_import_time: {source}: \ + values other than 1 and 2 are reserved for future use.\n\ + Python runtime state: preinitialized" + ); + std::process::exit(1); + } + value as u8 +} + /// = config_init_lazy_imports fn parse_lazy_imports(value: Option<&str>, source: &str) -> i8 { match value { diff --git a/tests/frozendict.rs b/tests/frozendict.rs new file mode 100644 index 00000000000..053dd7a5b65 --- /dev/null +++ b/tests/frozendict.rs @@ -0,0 +1,45 @@ +#![allow(clippy::tests_outside_test_module)] + +use rustpython::{InterpreterBuilder, InterpreterBuilderExt}; + +fn run_suite(name: &str, source: &str) { + InterpreterBuilder::new() + .init_stdlib() + .interpreter() + .enter(|vm| { + let scope = vm.new_scope_with_builtins(); + scope + .globals + .set_item("suite_name", vm.ctx.new_str(name).into(), vm) + .unwrap(); + scope + .globals + .set_item("suite_source", vm.ctx.new_str(source).into(), vm) + .unwrap(); + let runner = r#" +import sys +import types +import unittest + +module = types.ModuleType(suite_name) +sys.modules[suite_name] = module +exec(compile(suite_source, suite_name + '.py', 'exec'), module.__dict__) +suite = unittest.defaultTestLoader.loadTestsFromModule(module) +result = unittest.TextTestRunner(verbosity=2).run(suite) +assert result.wasSuccessful(), 'frozendict regression suite failed' +assert not result.skipped, 'frozendict regressions must all execute' +"#; + if let Err(err) = vm.run_string(scope, runner, "frozendict-test-runner") { + vm.print_exception(&err); + panic!("{name} failed"); + } + }); +} + +#[test] +fn frozen_mapping_contract() { + run_suite( + "frozendict_contract", + include_str!("../extra_tests/frozendict_contract.py"), + ); +} diff --git a/tools/opcode_metadata/cpython.py b/tools/opcode_metadata/cpython.py index 56743a88842..7e32430a2c5 100644 --- a/tools/opcode_metadata/cpython.py +++ b/tools/opcode_metadata/cpython.py @@ -1,3 +1,4 @@ +import functools import os import pathlib import sys @@ -18,6 +19,7 @@ from stack import get_stack_effect +@functools.cache def get_analysis() -> Analysis: from generators_common import DEFAULT_INPUT diff --git a/tools/opcode_metadata/opcodes.py b/tools/opcode_metadata/opcodes.py index 9178921b2c0..3f8fe65e923 100644 --- a/tools/opcode_metadata/opcodes.py +++ b/tools/opcode_metadata/opcodes.py @@ -33,10 +33,18 @@ def deopts(self) -> dict[str, list[str]]: for member in family.members: member_name = to_pascal_case(member.name) - if member.name == family_name: + if member_name == family_name or member_name not in names: continue - res[family_name].append(member_name) + if member_name not in res[family_name]: + res[family_name].append(member_name) + + for opcode in self: + if (base := opcode.override.deopt) is not None: + if base not in names: + raise ValueError(f"Unknown deoptimization target {base}") + if opcode.rust_name not in res[base]: + res[base].append(opcode.rust_name) return dict(res) @@ -76,9 +84,23 @@ def iter_opcodes(text: str, override_confs: OverrideConfs) -> Iterable[Opcode]: opcode = Opcode.from_str(entry) rust_name = opcode.rust_name + # Shared opcodes must use the target CPython's wire-format IDs. Checking + # the original name leaves explicitly retained RustPython-only opcodes + # free to use vacant slots, even when their metadata aliases a base op. + expected_id = analysis.opmap.get(opcode.cpython_name) + if expected_id is not None and opcode.id != expected_id: + raise ValueError( + f"{rust_name} has opcode ID {opcode.id}; CPython requires {expected_id}" + ) + if expected_id is None and opcode.id in analysis.opmap.values(): + raise ValueError( + f"RustPython-only opcode {rust_name} collides with CPython ID {opcode.id}" + ) override = override_confs.get(rust_name, SKIP_OVERRIDE) - cpython_name = opcode.cpython_name + cpython_name = override.cpython_name or opcode.cpython_name + if override.cpython_name and cpython_name not in analysis.instructions: + raise ValueError(f"Unknown CPython instruction {cpython_name}") kwargs = {} if instr := analysis.instructions.get(cpython_name): @@ -97,6 +119,14 @@ def iter_opcodes(text: str, override_confs: OverrideConfs) -> Iterable[Opcode]: " from CPython or override conf" ) + if override.properties: + kwargs["properties"] = dataclasses.replace( + kwargs.get("properties", SKIP_PROPERTIES), **override.properties + ) + if override.cache_entries is not None: + # Instruction.size includes the opcode itself. + kwargs["cache_entry"] = override.cache_entries + 1 + yield dataclasses.replace(opcode, override=override, **kwargs) diff --git a/tools/opcode_metadata/utils.py b/tools/opcode_metadata/utils.py index bc3a9ace8d5..19db5b2fe21 100644 --- a/tools/opcode_metadata/utils.py +++ b/tools/opcode_metadata/utils.py @@ -18,6 +18,10 @@ class StackEffect: @dataclasses.dataclass(frozen=True, kw_only=True, slots=True) class Override: + cpython_name: str | None = None + deopt: str | None = None + cache_entries: int | None = None + properties: dict[str, bool] = dataclasses.field(default_factory=dict) is_instrumented: bool | None = None stack_effect: StackEffect = dataclasses.field(default_factory=StackEffect) diff --git a/vendor/rustpython-ruff_python_parser/.cargo_vcs_info.json b/vendor/rustpython-ruff_python_parser/.cargo_vcs_info.json new file mode 100644 index 00000000000..d4eb7151112 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "8b59b8534415d79b28376a9808fc1b425d04b3ec" + }, + "path_in_vcs": "crates/ruff_python_parser" +} \ No newline at end of file diff --git a/vendor/rustpython-ruff_python_parser/Cargo.toml b/vendor/rustpython-ruff_python_parser/Cargo.toml new file mode 100644 index 00000000000..7e098e83070 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/Cargo.toml @@ -0,0 +1,226 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +rust-version = "1.97" +name = "rustpython-ruff_python_parser" +version = "0.16.10" +authors = [ + "Charlie Marsh ", + "RustPython Team", +] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Unofficial fork for RustPython" +homepage = "https://docs.astral.sh/ruff" +documentation = "https://docs.astral.sh/ruff" +readme = "README.md" +license = "MIT" +repository = "https://github.com/astral-sh/ruff" +resolver = "2" + +[lib] +name = "ruff_python_parser" +path = "src/lib.rs" + +# Local RustPython compatibility: wording only; independent of grammar target. +[features] +python315-diagnostics = [] + +[dependencies.bitflags] +version = "2.5.0" +default-features = false + +[dependencies.bstr] +version = "1.9.1" +features = ["std"] +default-features = false + +[dependencies.drop_bomb] +version = "0.1.5" +default-features = false + +[dependencies.get-size2] +version = "0.11.0" +features = [ + "compact-str", + "derive", + "hashbrown", + "ordermap", + "smallvec", + "std", + "thin-vec", +] +default-features = false + +[dependencies.hashbrown] +version = "0.17.0" +features = ["inline-more"] +default-features = false + +[dependencies.icu_properties] +version = "2.1.2" +features = ["compiled_data"] +default-features = false + +[dependencies.memchr] +version = "2.7.1" +features = ["std"] +default-features = false + +[dependencies.ruff_python_ast] +version = "0.16.10" +features = ["get-size"] +package = "rustpython-ruff_python_ast" + +[dependencies.ruff_python_trivia] +version = "0.16.10" +package = "rustpython-ruff_python_trivia" + +[dependencies.ruff_text_size] +version = "0.16.10" +features = ["get-size"] +package = "rustpython-ruff_text_size" + +[dependencies.rustc-hash] +version = "2.0.0" +features = ["std"] +default-features = false + +[dependencies.stacker] +version = "0.1.24" +default-features = false + +[dependencies.static_assertions] +version = "1.1.0" +default-features = false + +[dependencies.thin-vec] +version = "0.2.14" +features = ["std"] +default-features = false + +[dependencies.unicode-ident] +version = "1.0.12" +default-features = false + +[dependencies.unicode-normalization] +version = "0.1.23" +features = ["std"] +default-features = false + +[dependencies.unicode_names2] +version = "1.2.2" +default-features = false + +[dev-dependencies.anyhow] +version = "1.0.80" +features = ["std"] +default-features = false + +[dev-dependencies.datatest-stable] +version = "0.3.3" +default-features = false + +[dev-dependencies.insta] +version = "1.35.1" +features = ["colors"] +default-features = false + +[dev-dependencies.itertools] +version = "0.15.0" +features = ["use_std"] +default-features = false + +[dev-dependencies.ruff_python_ast] +version = "0.16.10" +features = ["serde"] +package = "rustpython-ruff_python_ast" + +[dev-dependencies.ruff_source_file] +version = "0.16.10" +package = "rustpython-ruff_source_file" + +[dev-dependencies.serde] +version = "1.0.197" +features = [ + "derive", + "std", +] +default-features = false + +[dev-dependencies.serde_json] +version = "1.0.113" +features = ["std"] +default-features = false + +[dev-dependencies.walkdir] +version = "2.3.2" +default-features = false + +[lints.clippy] +assert_is_empty = "allow" +char_lit_as_u8 = "allow" +collapsible_else_if = "allow" +collapsible_if = "allow" +collapsible_match = "allow" +dbg_macro = "warn" +debug_assert_with_mut_call = "warn" +disallowed_methods = "allow" +empty_drop = "warn" +empty_structs_with_brackets = "warn" +exit = "warn" +get_unwrap = "warn" +if_not_else = "allow" +ignore_without_reason = "allow" +implicit_hasher = "allow" +iter_over_hash_type = "warn" +map_unwrap_or = "allow" +match_same_arms = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +module_name_repetitions = "allow" +must_use_candidate = "allow" +needless_continue = "allow" +needless_raw_string_hashes = "allow" +nonstandard_macro_braces = "allow" +print_stderr = "warn" +print_stdout = "warn" +rc_buffer = "warn" +rc_mutex = "warn" +redundant_clone = "warn" +rest_pat_in_fully_bound_structs = "warn" +similar_names = "allow" +single_match_else = "allow" +too_many_lines = "allow" +unnecessary_debug_formatting = "allow" +unused_peekable = "warn" + +[lints.clippy.pedantic] +level = "warn" +priority = -2 + +[lints.rust] +unreachable_pub = "warn" +unsafe_code = "warn" + +[lints.rust.unexpected_cfgs] +level = "warn" +priority = 0 +check-cfg = [ + "cfg(fuzzing)", + "cfg(codspeed)", +] diff --git a/vendor/rustpython-ruff_python_parser/Cargo.toml.orig b/vendor/rustpython-ruff_python_parser/Cargo.toml.orig new file mode 100644 index 00000000000..aca2d2829a9 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/Cargo.toml.orig @@ -0,0 +1,58 @@ +[package] +description = "Unofficial fork for RustPython" +name = "rustpython-ruff_python_parser" +version = "0.16.10" +authors = ["Charlie Marsh ", "RustPython Team"] +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[lib] +name = "ruff_python_parser" + +# Local RustPython compatibility: wording only; independent of grammar target. +[features] +python315-diagnostics = [] + +[dependencies] +ruff_python_ast = { workspace = true, features = ["get-size"] } +ruff_python_trivia = { workspace = true } +ruff_text_size = { workspace = true, features = ["get-size"] } + +bitflags = { workspace = true } +bstr = { workspace = true } +drop_bomb = { workspace = true } +get-size2 = { workspace = true } +hashbrown = { workspace = true } +icu_properties = { workspace = true } +memchr = { workspace = true } +rustc-hash = { workspace = true } +static_assertions = { workspace = true } +stacker = { workspace = true } +thin-vec = { workspace = true } +unicode-ident = { workspace = true } +unicode-normalization = { workspace = true } +unicode_names2 = { workspace = true } + +[dev-dependencies] +ruff_annotate_snippets = { workspace = true } +ruff_python_ast = { workspace = true, features = ["serde"] } +ruff_source_file = { workspace = true } + +anyhow = { workspace = true } +datatest-stable = { workspace = true } +insta = { workspace = true } +itertools = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +walkdir = { workspace = true } + +[[test]] +name = "fixtures" +harness = false + +[lints] +workspace = true diff --git a/vendor/rustpython-ruff_python_parser/LICENSE b/vendor/rustpython-ruff_python_parser/LICENSE new file mode 100644 index 00000000000..655a0c76fc5 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/LICENSE @@ -0,0 +1,430 @@ +MIT License + +Copyright (c) 2022 Charles Marsh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +end of terms and conditions + +The externally maintained libraries from which parts of the Software is derived +are: + +- autoflake, licensed as follows: + """ + Copyright (C) 2012-2018 Steven Myint + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- autotyping, licensed as follows: + """ + MIT License + + Copyright (c) 2023 Jelle Zijlstra + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- Flake8, licensed as follows: + """ + == Flake8 License (MIT) == + + Copyright (C) 2011-2013 Tarek Ziade + Copyright (C) 2012-2016 Ian Cordasco + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-eradicate, licensed as follows: + """ + MIT License + + Copyright (c) 2018 Nikita Sobolev + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-pyi, licensed as follows: + """ + The MIT License (MIT) + + Copyright (c) 2016 Łukasz Langa + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-simplify, licensed as follows: + """ + MIT License + + Copyright (c) 2020 Martin Thoma + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- isort, licensed as follows: + """ + The MIT License (MIT) + + Copyright (c) 2013 Timothy Edmund Crosley + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + """ + +- pygrep-hooks, licensed as follows: + """ + Copyright (c) 2018 Anthony Sottile + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + """ + +- pycodestyle, licensed as follows: + """ + Copyright © 2006-2009 Johann C. Rocholl + Copyright © 2009-2014 Florent Xicluna + Copyright © 2014-2020 Ian Lee + + Licensed under the terms of the Expat License + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- pydocstyle, licensed as follows: + """ + Copyright (c) 2012 GreenSteam, + + Copyright (c) 2014-2020 Amir Rachum, + + Copyright (c) 2020 Sambhav Kothari, + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- Pyflakes, licensed as follows: + """ + Copyright 2005-2011 Divmod, Inc. + Copyright 2013-2014 Florent Xicluna + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + """ + +- Pyright, licensed as follows: + """ + MIT License + + Pyright - A static type checker for the Python language + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + """ + +- pyupgrade, licensed as follows: + """ + Copyright (c) 2017 Anthony Sottile + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + """ + +- rome/tools, licensed under the MIT license: + """ + MIT License + + Copyright (c) Rome Tools, Inc. and its affiliates. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- RustPython, licensed as follows: + """ + MIT License + + Copyright (c) 2020 RustPython Team + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- rust-analyzer/text-size, licensed under the MIT license: + """ + Permission is hereby granted, free of charge, to any + person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the + Software without restriction, including without + limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software + is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice + shall be included in all copies or substantial portions + of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF + ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT + SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR + IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + """ diff --git a/vendor/rustpython-ruff_python_parser/README.md b/vendor/rustpython-ruff_python_parser/README.md new file mode 100644 index 00000000000..8a184750934 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/README.md @@ -0,0 +1,30 @@ +# Ruff Python Parser + +Ruff's Python parser is a hand-written [recursive descent parser] which can parse +Python source code into an [Abstract Syntax Tree (AST)](https://crates.io/crates/ruff_python_ast). It also utilizes the [Pratt +parsing](https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html) +technique to parse expressions with different [precedence](https://docs.python.org/3/reference/expressions.html#operator-precedence). + +Try out the parser in the [playground](https://play.ruff.rs/?secondary=AST). + +## Contributing + +Refer to the [contributing guidelines](./CONTRIBUTING.md) to get started and GitHub issues with the +[parser label](https://github.com/astral-sh/ruff/issues?q=is:open+is:issue+label:parser) for issues that need help. + +## Versioning + + + +This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed +here is unstable and will have frequent breaking changes. + +This version (0.0.16) is a component of [Ruff 0.16.10](https://crates.io/crates/ruff/0.16.10). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.10/crates/ruff_python_parser). + +See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for +details on versioning. + + + +[recursive descent parser]: https://en.wikipedia.org/wiki/Recursive_descent_parser diff --git a/vendor/rustpython-ruff_python_parser/README.rustpython.md b/vendor/rustpython-ruff_python_parser/README.rustpython.md new file mode 100644 index 00000000000..2543360346a --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/README.rustpython.md @@ -0,0 +1,149 @@ +# RustPython CPython 3.15rc3 parser fixes + +This source-only copy of `rustpython-ruff_python_parser` 0.16.10 carries exactly +six local RustPython patches for CPython 3.15.0rc3: structured unpacking +diagnostics, full-expression operands in starred generator expressions, parameter +wording, parser-owned statement diagnostics, and f/t-string error context and +precedence, and multiline concatenation hint ranges. The latter four use the optional +`python315-diagnostics` feature. The published +0.16.10 parser already includes the official unary-plus pattern backports; +those patches are not reapplied or retained here. + +## Source and integrity + +- Package: +- Archive: +- Archive SHA-256: `7c9729e35cac02839c7a67aaccfea3eba32032f7aeacca2e82ad79e8d7b7fd95` +- Source revision: +- Official fork tag: `0.16.10-rustpython` +- Source path: `crates/ruff_python_parser` +- License: `LICENSE`, copied from that source revision; upstream Git blob + `655a0c76fc5539b2f8e63b673741a5fd0e0c5799` + +The archive checksum matches RustPython main's `Cargo.lock` at +`a40334179123949fbeef1cdc7557539c06cd187b`; the archive's `.cargo_vcs_info.json` +identifies the source revision above. `UPSTREAM-SHA256.json` records the original +archive bytes for every included upstream file. `LICENSE` is verified separately +because it is absent from the published crate archive. + +The copy retains `src/`, `README.md`, both manifests and the upstream revision +metadata. All original unit-test source and snapshots are retained. The separate +`tests/` and `resources/` trees are omitted, as in the preceding source-only +vendor; their `fixtures` and `generate_inline_tests` target declarations are +omitted from the normalized `Cargo.toml`. All dependency versions and features +remain those in the published 0.16.10 manifest. + +## Local source changes + +Apply these patches in order, from the RustPython root, with `git apply`: + +1. `patches/rustpython-rc3-unpack-diagnostics.patch` adds six structured + `ParseErrorType` variants and precise token/expression ranges for the + unpacking invalid productions in CPython 3.15.0rc3. It preserves the newer + parser's missing-comma, dictionary-colon/value, and statement-after-else + handling. It accommodates `InvalidAssignmentTarget { .. }` and existing + 0.16.10 AST constructors without changing the AST crate. +2. `patches/rustpython-rc3-starred-generator-operands.patch` allows the first + parenthesized starred element to use full expression precedence when a + generator clause follows. Tuple/group and sequence-display restrictions + remain. Display diagnostics are deferred in encounter order, restored only + after another parse error, and rolled back with parser checkpoints. The + newer tokenizer-error prioritization remains in effect. +3. `patches/rustpython-rc3-parameter-diagnostics.patch` declares the default-off + `python315-diagnostics` Cargo feature in both manifests. When enabled, it uses + CPython 3.15 parameter terminology for exactly six diagnostic families: + parameters following `**kwargs`, bare `*`, defaults for `*args` and `**kwargs`, + repeated `*`, and a leading `/`. It preserves structured error variants, + ranges, error ordering, recovery, tokens, and the AST. Grammar target version + remains independent: CPython 3.15 uses the same diagnostic wording even when + `ast.parse(feature_version=...)` requests an older grammar. With the feature + disabled, the original upstream messages and fixtures remain unchanged. + +4. `patches/rustpython-rc3-statement-diagnostics.patch` extends the same feature + with structured standalone-case, raise/from, trailing-with-comma, + mapping-rest, assert-assignment/named-expression, misplaced-lazy-import, and + forbidden lazy-future-import diagnostics. Parser checkpoints validate the + relevant grammar using existing productions, then retain the original + recovered AST and token stream. Unparenthesized from-import trailing commas + are diagnosed at their terminating newline. The lazy-future grammar action + runs after targets complete and before later statements; a missing optional + alias after `as` can roll back to the valid unparenthesized name prefix. + Missing names, parentheses, and trailing-comma errors keep precedence. + Missing import aliases after `as` use the generic syntax diagnostic at the + existing token range, preserving relative-lazy warning collection. + +5. `patches/rustpython-rc3-fstring-diagnostics.patch` preserves multiline + replacement-field opening-line diagnostics and their priority over expression + hints within the same field. It uses the existing lexer bracket/interpolation + state and token ordering, including nested format fields and continuation + boundaries. Grammar acceptance, recovered ASTs and tokens are unchanged. + Behavior is gated by `python315-diagnostics`; its structured + `UnclosedLbraceOnLine` error variant is present unconditionally. + +6. `patches/rustpython-rc3-concat-comma-diagnostics.patch` places a missing-comma + hint for a multiline concatenated literal on its final line, retaining the + original starting column. The structured error retains the expression span + for error precedence and a separate diagnostic start for empty or reversed + CPython ranges. RustPython's compiler maps those offsets without scanning + source or rewriting the message. Behavior is gated by the same feature. + +All six are local RustPython patches, not upstream Ruff commits. The first two +port the local parser patches previously carried on RustPython `70e069972`. +The third through sixth preserve diagnostics formerly handled by that revision's +compiler and VM normalization, without restoring source scanners or VM string +rewriting. All use CPython +`v3.15.0rc3` (`8a8eb0b90d40f0e03f8252e9f73cad156e1caca8`) as the reference. +No canonical assertions, upstream snapshots, or skip/xfail markers are changed. + +Reproduce the vendor by verifying and extracting the archive, copying its +retained paths, removing the two omitted test target declarations, copying the +license from the exact source revision, and applying the six patches above. +The provenance manifest always records unmodified archive bytes, including the +original normalized manifest before removing the test targets. + +## Workspace use and publication + +Keep the parser outside the workspace member list and use a workspace dependency +with `path = "vendor/rustpython-ruff_python_parser"`, `version = "0.16.10"`, +and `features = ["python315-diagnostics"]`. +The path also applies when RustPython is another workspace's Git/path dependency. +A root `[patch.crates-io]` override alone does not have that property. + +Cargo replaces path dependencies with their version requirements when publishing +to a registry. Before publishing RustPython crates, switch to a published parser +containing all six fixes; the unmodified 0.16.10 registry crate lacks them +and does not declare the local diagnostic feature. The published dependency +must retain that feature or provide equivalent wording unconditionally and +remove the now-unnecessary feature selection. + +When a compatible published parser contains equivalent fixes, remove this vendor +and its workspace exclusion, switch to that registry version, and regenerate the +lockfile. Keep the newer AST interfaces and diagnostic plumbing. + +## Diagnostic feature compatibility + +`python315-diagnostics` is a build-time choice for the fixed-3.15 RustPython +runtime, not a language-version flag. Cargo feature unification applies to all +consumers of this package in the same dependency graph. Consumers that require +legacy diagnostics must use a graph where the feature is disabled. No new +parser entrypoint or option is added. The fourth patch adds meaningful +structured error variants and a standalone-case block-clause distinction. +The fifth adds an interpolation-error variant carrying the opening line; +the sixth adds a concatenation-hint variant with separate display offsets. +Downstream exhaustive Rust matches must account for these enum additions; +feature-off compatibility describes runtime output, not enum shape. + +The immutable upstream snapshots retain their original diagnostics. Run their +default configuration with this feature off. Feature on intentionally changes +the preserved rc3 messages/ranges and rejects forbidden lazy future imports at +parse time, where CPython's grammar rejects them. The runtime already rejected +these imports; AST and tokens stay unchanged. No upstream snapshot or canonical +assertion was edited. Exact CPython rc3 records are the feature-on reference. + +Native exception conversion still owns concatenation display-offset mapping, +newline-end normalization, zero-width +empty import names, standalone-case IndentationError end_offset=-1, and +unclosed-bracket end_offset=0. Barry preprocessing and single-input boundary +precedence remain caller responsibilities. Callers must honor structured +LazyFutureImport before later errors, but a preceding single-input boundary +still takes precedence. diff --git a/vendor/rustpython-ruff_python_parser/UPSTREAM-SHA256.json b/vendor/rustpython-ruff_python_parser/UPSTREAM-SHA256.json new file mode 100644 index 00000000000..f762e6ca4f3 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/UPSTREAM-SHA256.json @@ -0,0 +1,231 @@ +{ + ".cargo_vcs_info.json": "cdcf572ec055df0fbf81f23095c4eee488dbd0fcb4c8d74b3a35957530ab24de", + "Cargo.toml": "8aee59a434d1f6537c2b7a5e48a060c508e50b46b559eac3113f51a82db4705a", + "Cargo.toml.orig": "cbd26dfc3925d3bd645dcc4377e4d56ef38518ed43aabc81671046907bba5256", + "README.md": "af7ba302da5e85e6c6d13e0546609088bd92aedeaea1336fa1e48823e8215bdf", + "src/error.rs": "7ccda94fb8f8f75bf04d8a1a860dd58049413b90fef9551f5c16f2be99b6b825", + "src/lexer.rs": "4b65e7811210139ef6c77e86c5f355070f0488eeb31be647ad276139d739a5ba", + "src/lexer/cursor.rs": "7631cb23110d75273be440da6d1de187f9ec62fc2d796bf2ddde09f1ce037c47", + "src/lexer/indentation.rs": "7996d9fe6f9780a3e70de7d179015685e3f85ee9eec9a265eb314c6113419d34", + "src/lexer/interpolated_string.rs": "7799369c76c02fc669afc3e6c6e5a6ce280e2c8514bf7d35c023907d99ca9f63", + "src/lib.rs": "c21a19a68609f0f440426dc4391267e9d4e4f64bd40eec06444e2766ea61d49a", + "src/parser/expression.rs": "f318937a80177fa3c9ed5e7c9950625db327a1b860fd5929cd96be070ec8a960", + "src/parser/helpers.rs": "8f5017280e71e31aaeb19e20423a597eb8ff1cbb4f01212b9bb2c34f25bba3cb", + "src/parser/mod.rs": "c0cee8933264c32b0779b8079fd51931474e03155877be422fa5acbeaea47915", + "src/parser/options.rs": "7ab7642d2075fa098837cc7f9fb65b8bd145b7139e7376a6503e168b22d48640", + "src/parser/pattern.rs": "ddc6fc7faedf10b4a1953e676f1f097e042b4935977b75ce7b453e62b6217a9f", + "src/parser/progress.rs": "3d4ec92e9041221df5e62495f93685f3c41652eaa8b0f21ef232f0baf3d7f1bb", + "src/parser/recovery.rs": "ae35291db205f066a89a690335398eeabe73355d8f1154648c72284d6f17e10b", + "src/parser/scratch_buffer.rs": "5b6244ccea2c5cebf39694d6b06306c039f9a1e56c19383db415240510662340", + "src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax1.snap": "1812093bde934798c37e67f8b863d187017b0ac8f9567254edeeb6dc5ac82f06", + "src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax2.snap": "c6bea7f7489c1ce4e4cfcb253c598e31d51b52f5085873f3ff069775f4d2a9b7", + "src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax3.snap": "c6bea7f7489c1ce4e4cfcb253c598e31d51b52f5085873f3ff069775f4d2a9b7", + "src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_valid_syntax.snap": "d255f5caf0a1c853414cf891cf37e08d756aa468a5ec6b20ea4076effc6f7f00", + "src/parser/snapshots/ruff_python_parser__parser__tests__fstring_expr_inner_line_continuation_and_t_string.snap": "d510f4257542a1d048e3c3d5280ff70f21628ac83564cd730eb3458cefe23de0", + "src/parser/snapshots/ruff_python_parser__parser__tests__fstring_expr_inner_line_continuation_newline_t_string.snap": "2f26f07d3bf70f77196c2a785da6d83e926b57ab2723d9947651dfc75ab15e86", + "src/parser/snapshots/ruff_python_parser__parser__tests__ipython_escape_commands.snap": "0516fd87c3b8e712c5c685972e07a95b0f6ed1f7d19b1c06dd22d1235c88726c", + "src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle.snap": "551d251d3edb194c4c0d00078693c1379d5d36f5d212f5b75140873224e90bd0", + "src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle_fuzzer.snap": "af0d88e1b87ce7b70f0855eb84b68a489f1e655ffb37980909d061bbe12231ce", + "src/parser/snapshots/ruff_python_parser__parser__tests__unicode_aliases.snap": "950c8fc694f14e6e76785051f36c168b6b388d3709122b4cee8d181bc22b6967", + "src/parser/statement.rs": "c9cec90084c31707695496cab6275bb730739ad1e118d4343dde1a7fb93f37b5", + "src/parser/tests.rs": "e966fdf764d7aa498b3be8844f3783c82f898dcbf9b2019c17523d8094f3b449", + "src/semantic_errors.rs": "c83ff23ef139584e055ab1160558e855792169506afbeb9b7ba76bff8839838d", + "src/snapshots/ruff_python_parser__lexer__tests__assignment.snap": "1795dbee0210f6ceeb8eb275f49935f3ce8b3157340dfd4b7e40d64e359d0264", + "src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_at_root.snap": "5a66190210db29649ea99f3f4952babde37b27849b4404ad1c6f7eed66e66e48", + "src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_indentation.snap": "a1f109d5d5efc922a04036897a8b5849e983c0e3484686c5c923f151367464f5", + "src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_mismatch_indentation.snap": "c7fd9e0cf3e5c35f54fe66a0f3f189c83ccc23e75bb6219a88a0bafdacf0edc6", + "src/snapshots/ruff_python_parser__lexer__tests__bom.snap": "2b77efd7e36bf6c5a499b417a332b9c02e277152bf315e61ca61d1d7074d23ee", + "src/snapshots/ruff_python_parser__lexer__tests__bom_with_offset.snap": "7b4d6fccfa28811a5dc80a61ddc5bb5f524b510a463a37d516cb631c73abc863", + "src/snapshots/ruff_python_parser__lexer__tests__bom_with_offset_edge.snap": "e0ba0c172dd5173ab6ef1987e1b04980b41ae951e5cc815b0c8348b724735e10", + "src/snapshots/ruff_python_parser__lexer__tests__comment_until_mac_eol.snap": "52b711ab9be58aee762aac073bdea255a0f30c40c86afcd0c238dd463a60e3c0", + "src/snapshots/ruff_python_parser__lexer__tests__comment_until_unix_eol.snap": "e2eb65343f0938943d0b8c84e87579c481ca9d64ba7347c768a9558520c3ba42", + "src/snapshots/ruff_python_parser__lexer__tests__comment_until_windows_eol.snap": "803734a2193f9fad4cd13138edd6ba5f64c81aeb407541de095d9969ce03ff08", + "src/snapshots/ruff_python_parser__lexer__tests__dedent_after_whitespace.snap": "c8fabc103171d24a27b2e740557763d7f61448a8800b604830b6b89f25fcb9e6", + "src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_mac_eol.snap": "49c5fc075f642f08e5ff3c26f69b32d6a9f6f0e2cff9ce8f7e2dfde116f7f6b3", + "src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_mac_eol.snap": "2ac4366ff16c752381e3fe284cf5521fc856cf8ad259105a954d582b0481d1da", + "src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_unix_eol.snap": "a91ae211301183bc880acb6519155d08cc3237397f00312999b75eefd7a7d1f9", + "src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_windows_eol.snap": "1bfee3fc7d15a53ca0f9ad9c4425771a9211cfb345d3e010aceef3fe3e4c253f", + "src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_unix_eol.snap": "eae943779210b01fff86b135886cbba73f9bf0888d0662585479e20b82258800", + "src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_windows_eol.snap": "7575e52bfd63997cb508179e2999521bb9bcde0d1c7331f911d557fae6b60a39", + "src/snapshots/ruff_python_parser__lexer__tests__emoji_identifier.snap": "78c86f6ad92a0cc00ad90a1318677a018932c8fcf5cf8ee69a27d401a02246b3", + "src/snapshots/ruff_python_parser__lexer__tests__empty_fstrings.snap": "acaf82da7c67e8a9e58e63d5050c773635e0bec61a33ffe3b6aa5eeb44857b62", + "src/snapshots/ruff_python_parser__lexer__tests__empty_ipython_escape_command.snap": "924d6fe7dcd4665ed97e6552a144dc244d1cad775ef51b9592af333949634a5b", + "src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap": "adf46c8920501b5ccb07f2c40d02a8b350bbb7dda5298d9dcc824add284e9a27", + "src/snapshots/ruff_python_parser__lexer__tests__escape_unicode_name.snap": "19e0f9f1ff4bbdb5e2fd887613de47feb6820d31ba5b8d8e2085fada3e82b570", + "src/snapshots/ruff_python_parser__lexer__tests__fstring.snap": "cada291f4d10de4d80423d5bf49aa1c356b735577c066d908b274a423a801358", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap": "51bffed2d16cf486d0d1573d6e9697168d8c4a3fc4a747cb73e6da765389a715", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap": "3000cdf399144579c0a98ed292a40a6a5605907411a32adcc0b8cbe13c91c861", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap": "56247cc18a55215126f2dc2d0b48fbde129ade3ec20fd2560427f9827bff8e70", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap": "a07fde9f947790d4244759737f87dc608f5ef633151880ff54125d70aa3c8ac8", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap": "a11565f0647aa4084453ba2348dc02fae1b35d6bdb4965ec012490aafccd7242", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap": "cb6987d127d29fdd760ae9c3abf7b9ce9e328ce164444198797ba7ffe2ea7589", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap": "1ff38f1b9d212364fb6774c338c1d5f56c6a3dd08be14fdebf4e4bc94a231116", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap": "be5df5d28359f2ac828804985b5884063eead381478b99e0c22a3fc85ab3e4bf", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap": "fd733a5bf5fc579bbad1539b110a1288d7c49c843e903d1815bbf22fce97f540", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap": "dc1a93ff12506231736e7939406a6b1eb4e0198f7aed361f9042372c3f2b3b9e", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_newline_format_spec.snap": "cb266cd6d3ee2ca2c81d9c93d1ef6bc575ab56f48aaacef91c2becff7ab87ece", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap": "e2959c0978a3449fdadebab0b3b445aa83396cefff082a814641c61c2c61ba8d", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_prefix.snap": "e56af299010bcd9569f4def998379fc9b726da2f8003d2834800aa1cc255ec84", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap": "128723d8b3fff9d18a0e19f2579a88925afed98133292ed0ad95a4ef5a281b58", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap": "92f3a3c3d641764c88ff41b82c41779da16fcd4c908fac5c8abd78a843f39854", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap": "5d9d9a184d0f87777b2b5738ee0a9da61fc79e26d01d6f5aa4c8e33dd2dee457", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap": "4c888f0364fcb9a32eef69fb40fbbc22b03f193b661a960579e924d3dc8b107c", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap": "96cccd3acb3ed45097dcd993c99c16ef4b21f6bad82d8d81fb94d84a62e1bb4c", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_with_lambda_expression.snap": "d981a673ac77091fd8b944fc2be95ae7460cd6a981ddc39f699332165536e1f7", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap": "ee9757fa63b13feef9aa484c7897324f6266284c72ccc283be43d2793834a7b1", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap": "4f49c76dbbf9c5803e5de8b85b912fad29acdeee8b5867ded0b6d60fd94e76cf", + "src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap": "1817ea7a6ff2df2707cd697459f7a548a77b9667291fb3cce73a8aaa87f98adb", + "src/snapshots/ruff_python_parser__lexer__tests__indentation_with_mac_eol.snap": "50da810f92a0d2b311c67a493e50ec82fcd2ae8634af8788bd79983013a395e4", + "src/snapshots/ruff_python_parser__lexer__tests__indentation_with_unix_eol.snap": "11a5059e6472c19aeb73db5fdde00d22ef2f0674e41f68beefbe09126219dba7", + "src/snapshots/ruff_python_parser__lexer__tests__indentation_with_windows_eol.snap": "8ae339d89cae4ae5aabb63ff7df8f3a2c2435aa6356a46d59ab9540e2301d930", + "src/snapshots/ruff_python_parser__lexer__tests__invalid_leading_zero_big.snap": "1c8b4e2e1a8702cec9c5208b1c1874080d78104adc5c53ca7ee1ffc0925edc89", + "src/snapshots/ruff_python_parser__lexer__tests__invalid_leading_zero_small.snap": "527c78f9f9636f699b3e93e6844f3d47cff331205d87e8106578c84f7843eefb", + "src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command.snap": "0bf16a4692ddf588fc772001a263f1821e2fbfb27e0bbe6aa868e453ebad7022", + "src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_assignment.snap": "307ee9f2039211924f62ff027ad91c0e27c151df1d2f53d557f5534c50261483", + "src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_indentation.snap": "8e512e265213c455b2e29f809830d4a8e130afc24bcdb4779d0afcacfce8fb01", + "src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_mac_eol.snap": "f7f9d75e61576cf6f38b8b60d7ceec98af5c7b02bfc88ba5dc070f8850d96bcd", + "src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_unix_eol.snap": "97905be3df2b76d948a3e8513abcacbe9ad3cf893c61c94510d2263b41651535", + "src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_windows_eol.snap": "7e7452d0b37f808484995b864fe10dde9dda9705cd31c75f52b4b518bd7643e8", + "src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_mac_eol_and_eof.snap": "33e45fda2cd7d2df7151e8667a2d468bfc850e04dc68cf1a6010154439ff459e", + "src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_unix_eol_and_eof.snap": "bacb5d1afa7a82a528462154c43333bf96fb64b5ee7febf4de142614b71d7b56", + "src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_windows_eol_and_eof.snap": "f9a273017352eb1190c1c35ea39ea192a7d239ce45dcacfecf5fcbdbaa1887d3", + "src/snapshots/ruff_python_parser__lexer__tests__ipython_help_end_escape_command.snap": "4af3b266fb2909936426b50c410b04933d18d5be6ead7a6ecef3f322538b95e3", + "src/snapshots/ruff_python_parser__lexer__tests__line_comment_empty.snap": "c73f78743080894cd6ef8979badb13d160aea0c7160455d968e952ec3fdd7c40", + "src/snapshots/ruff_python_parser__lexer__tests__line_comment_long.snap": "dfb58988243aacc5c207c52735893aac05fefba4961cea29e7439f4af111c767", + "src/snapshots/ruff_python_parser__lexer__tests__line_comment_single_whitespace.snap": "4137ad100ade8413b65557641778bc4da54e8c5caadcb98f9111e4e6d3449f6e", + "src/snapshots/ruff_python_parser__lexer__tests__line_comment_whitespace.snap": "de23ecfe70bbaa0d2477523f6d218dcfba2f10672446a8ce8251c9b217fd38e9", + "src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_mac_eol.snap": "48cb858ac915f9d05553932086209bd23eed9379d5422320a2257f73c72a7081", + "src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_unix_eol.snap": "9e59a70f960d45f16e11701430e8863f1d9ac16fd77c8f246a9576a041c5c5c4", + "src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_windows_eol.snap": "c0aaa84b773869be0de6e1a7877ff84b8ba2170a1fdf6aede4e75d07cd0dc139", + "src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_mac_eol.snap": "f94cc6f4d1ea5b9150dcf108ba54c29af2680da1fae9f10e6a41feb42f67ded1", + "src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_unix_eol.snap": "bf2fb0065ec9eec46e685230baec478bb96049625154e4e361bd4deb18a54a28", + "src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_windows_eol.snap": "061e97f0f2424181a5fbd06bba35eea667414049f62155b4e1574ab8759bd1e9", + "src/snapshots/ruff_python_parser__lexer__tests__logical_newline_line_comment.snap": "5e925622301c0a419f581ecd29a54c50e1a8ca8f3971f2f32db40a7e627d24eb", + "src/snapshots/ruff_python_parser__lexer__tests__match_softkeyword_in_notebook.snap": "f086718f6a512df933e9a4cde4aef680033b47d0cb0b6a1e0a84bae0f50b59d7", + "src/snapshots/ruff_python_parser__lexer__tests__multiple_backslash_continuation.snap": "f5b282936dc6233e359c878aa7be36a019515edf3e92f60980a3fd71c2bbfdbe", + "src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap": "fc1540da1349ef7bd39b4653a07d24599e7eb165dbf4baca6ce96853d4522057", + "src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_mac_eol.snap": "6fd8554e7cd9ece7a9cfae1c17234688784b302b77da83711d27422fd122a73c", + "src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_unix_eol.snap": "70720dd73f05179d6f0259bd5f06893a0c3e6bbbdb32d93b0d7a8d05ee265d01", + "src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_windows_eol.snap": "ee74a2bb62cf30646c1b03df854ef8fa32e7d43c24232ae4614c3de46f54673c", + "src/snapshots/ruff_python_parser__lexer__tests__non_logical_newline_in_string_continuation.snap": "f9718dc4afbb549e8076b55f0c9676272f27f0dbadf982e841c0aa3816f667a0", + "src/snapshots/ruff_python_parser__lexer__tests__numbers.snap": "9080aea3a6a07a5b5236c2c3d435f6fc6243941cd2bb0152517c0de31326d33e", + "src/snapshots/ruff_python_parser__lexer__tests__operators.snap": "7a82cce74d331109e969d93eb6eb8144051fa23dd06bd67cd7345053cff0e184", + "src/snapshots/ruff_python_parser__lexer__tests__string.snap": "210d1c4735ac221036d21b2993e85b1feba2ad2916ce30558bfb893e8f5b8781", + "src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_mac_eol.snap": "1c8acd1e7a2a9fe705b036527a184fd92adafcc66cc5fa6682e0f14623babc35", + "src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_unix_eol.snap": "76b11b8814f80a40c7dc33dbc682043139d87de2a632678e2262da54daf77c11", + "src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_windows_eol.snap": "4161b25dfc792d567590c958b2449c3644f812d6b4b9e2d59c72f61a2ae13b6e", + "src/snapshots/ruff_python_parser__lexer__tests__tet_too_low_dedent.snap": "3d8f69a1853a9e2382fedac5357d94744ef22fff19087153a9c96272daf30dc0", + "src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_mac_eol.snap": "fac31867e510b73a67fe4bbc38e0ef06dec6bc7503eb57e50cd67d746e1f509c", + "src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_unix_eol.snap": "05c9ee21a9f63aeeaf33cc3a8c3ea8eac71d335d09939f3fc00a4a0d15c6c744", + "src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_windows_eol.snap": "7aa5ca0098d71807e6855f6e1d021b4cf23b39f8255b9f774d16859ac03c39c1", + "src/snapshots/ruff_python_parser__lexer__tests__tstring.snap": "54b78daa0fb973c10880f6c11e934976875bbc81f8d926236c5927b3b67911f2", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap": "ff0f6b415054298d1d1114fef25ac4a934b081d0fdfdef0fb2c94c59c180da84", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap": "e4fef621113219281e51616eca48d4ca55160dfcd473f76523d545b5f3e80e7d", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap": "1f047c7557cda4f0c8cdea7e413719990b08dd00f4a81d0318b127f21c108bf9", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap": "3a0ded42b7d554f650110e8bde8996c4a76986b013f8712a54ae0e667665685b", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap": "1b518f539dcb1f252af5bc8fb41992391814b600f44cad172f03a6f037eaf2e6", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap": "37b2bdfa2e59936ad50cf0c6e87ea129eba1394f492ceba5733cc3cc0f721c43", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap": "a0b93dabc2805b40b201ffff5f7ee7b9fd2c9adc540db3ec6635f15ce9f2feb4", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap": "f3d60cbdea5be208ee2623c434189abfaaa1956bcdd1972cc3ca696064b616fe", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap": "364c0aa854a1832bafe373291aab064fd49da3a9d6123dd1cfa162a914e02790", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap": "3d62c24efcb785a558b79ecdb6100f2a667dcea75dc2bcfb9a218656b8f96321", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_newline_format_spec.snap": "6f9b7eaaaeada86e7ef7b3428766b7de5b103dfb808c760600efe09176f6f041", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap": "b4a0ac7a5e497c260d4607abd7aa0b12f7bd5cc25a89570d77f4ac4162ab4272", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap": "4a8edd97e726282e3842da2c4e92ffce60967263a68239ff6535fbc004ac1ee2", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap": "b9376cc7ad42d32409d316579fdebdc0d3d3ffbd11785b0ac186b2c2589f4cf7", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap": "345c2d40459b06b23e3b9d8d5bf64befb44debe2aaadbf2e789ffa4306fc5a8a", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap": "b3dcecb1c19d745152217052a82d375e78f9a05717ee9f29ab715bf593463ee8", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap": "c8e77c49428147e477d141ee3fbcbec4dc9c9549bf12a7099026bd2a78123f7e", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap": "016d15ecd9ca5f25f69e4c5f72a2146f24cbd4b2df9f67ffeaa8527fbe4c4426", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap": "229ea461c953a8d6892f0bc785df3505353e969f72f044ae29da538abc999c98", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap": "a29e5b603c53e64c070870371a51a0d537a3a167448b7caf93fae05d74098d7a", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap": "bd052237d1848130485723870e5a1c4cc88645f8a272db8eebdc7697da6db43b", + "src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap": "20208e29546f545590dd0321e0a1a02b0531cc6b8f2dea5b6cf65835e34ade2d", + "src/snapshots/ruff_python_parser__string__tests__backspace_alias.snap": "e7e51f597394de93d5421dd7be43caca7e2112b14e8e6c192ec8f4e03e86f4d8", + "src/snapshots/ruff_python_parser__string__tests__bell_alias.snap": "567279925de27bea0c701b74fb707198d230b5bc379542977ea4a0e2e2277bb3", + "src/snapshots/ruff_python_parser__string__tests__carriage_return_alias.snap": "96b4e2728b3c953d0144bc91aaa96ee06a45a12b0d3b5172643a11f4ea90ea3e", + "src/snapshots/ruff_python_parser__string__tests__character_tabulation_with_justification_alias.snap": "8a01f55185287361e004942c761da7ecae8210fdc9c22c9d3a2e8214155761a3", + "src/snapshots/ruff_python_parser__string__tests__delete_alias.snap": "91ab41691392b1c9fb7ba86c7fa4b26e0fd8efb0046aaf95e7bb543d245bfa7d", + "src/snapshots/ruff_python_parser__string__tests__dont_panic_on_8_in_octal_escape.snap": "fca0fcecb408cd3cdaf6992135abf61573e8b78ba1db8c5739859f315631e369", + "src/snapshots/ruff_python_parser__string__tests__double_quoted_byte.snap": "dc4a2876f16cc943c52f681755d1b7019d5e8dbda5a1886269b0be70cf700705", + "src/snapshots/ruff_python_parser__string__tests__escape_alias.snap": "b8fa4cb792e031ebce36948df75fd8ec73ee3b05c87ce2d64232daa16f03bc54", + "src/snapshots/ruff_python_parser__string__tests__escape_char_in_byte_literal.snap": "ec5c3df3100a1cbdd4373ddd63066b183547c2f873c8885ef6d934542958d6ba", + "src/snapshots/ruff_python_parser__string__tests__escape_octet.snap": "cbe5e6017419c06bcc76321a640c7d116ddd822d0191ef1a677c7e9addde815d", + "src/snapshots/ruff_python_parser__string__tests__form_feed_alias.snap": "507c452b050a7f218cd80a17a55821bab7d1196e40b02aece7ec4e57716277e1", + "src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap": "33ec61d34bcfc89c8564e02527a225e9845d5132e941d5aadb8350f493fd21eb", + "src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap": "424c26c854c2a38f564d848362dcd996099d2c07641e8e8b0ef8eafe6be7c06d", + "src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap": "45b904a00da9f0c26780cd3944d53d4a90e4682f24079912a7cc45007e1fac32", + "src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap": "f6893eb6c7ed846447ef6a2a48b73ad894791c1925933a0953b6eb59a8467459", + "src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap": "57390c0d64cc1d2d6f67e0c00ca9ce14a2a277a14df246a66907cb1af4300844", + "src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap": "607a4667146a1cc9b7bf4ea2f598e2ef69b4c702f0d156a70257c998d3d5bd7e", + "src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap": "eb72b5724e4fd6eb940a8274057039afd21d3ba752950b122e8d25005431579d", + "src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap": "4b74dc2322e76518fb5fa18eea01d94fee8a9d4900a7dac354a5d5a2ee0aca7a", + "src/snapshots/ruff_python_parser__string__tests__hts_alias.snap": "68a92fa826c351c9f36e58fa75c42e254021a37cb5a75e126474e92818cdb998", + "src/snapshots/ruff_python_parser__string__tests__invalid_byte_literal_error.snap": "dea84344130d02a3ec8c8a97b98fffa7f1ef4427252966b10b0045bc7acc385e", + "src/snapshots/ruff_python_parser__string__tests__invalid_unicode_literal.snap": "7413bba4490c96a505c98a9a282c3f26c3a46771461b01796db3721b42d4f556", + "src/snapshots/ruff_python_parser__string__tests__invalid_unicode_name_error.snap": "f3d94167e53087f669248b0c8121474bdf3ba313715b012068054c872bdccacb", + "src/snapshots/ruff_python_parser__string__tests__missing_unicode_lbrace_error.snap": "54e96299a864efc7537db8c12dfa663ced1fb991ed054733d26fe513093ca5aa", + "src/snapshots/ruff_python_parser__string__tests__missing_unicode_rbrace_error.snap": "1330843027ecadbc9c764c379abb1b57bdc6f6872a58a3d9f9ecc58fe4e92f8b", + "src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap": "da4bb2b248dd1944506359d2d330450a5ba55a6d27338673bdf3a21a4df21b76", + "src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap": "aae82257aff5ed60cbd7e3ada0a55cbf29f47e94b94e2e09bb044292d211b405", + "src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap": "77c1cea432dfac04db94baac63116280e1be92e05406544120adb0c7d697f7ce", + "src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap": "77c1cea432dfac04db94baac63116280e1be92e05406544120adb0c7d697f7ce", + "src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap": "1583d0a92c06f2927dff006249f41e1d3dfc077c285df378562cf5b850f74b8f", + "src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap": "b177bc704436e31ac38b2f34915d103689ff438949cf80ed671d548ba279f7a1", + "src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1_error.snap": "fdc2ef580f0ec10a9c195ba798ef29567bc28feeb47c5508a801ff5006bdfe1c", + "src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2_error.snap": "92fdf69487cf600cf3352b4aa49bfb8991fc2aa9b7aa2fd3fd402752d89ba4a2", + "src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap": "ff0ca0134a6d4ed1247ae1705f3422a084f141d6df3f01a619adfbede7b07034", + "src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap": "da603effa180709cceab9ea2f4203609ff40bd469aef2105b6ff4551f8644d73", + "src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap": "50af3caaaf15178654d004ba3bbfe00c15277ef1475a1404bc7bf6e85367647c", + "src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap": "264fafb99495c5e315be4d6b034bb481ea0017d046462567b3fef4974dc1fd50", + "src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap": "1bfe31865ac371037ce03d2f5ea1065eb3cdb17f974ae58e8662149effdbbc5f", + "src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap": "4b1d2c2dcd4bde8fbee7c56a95b906c535659479acea743a9af87a5bb00a724c", + "src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap": "19ba09ad6757661af60be598babf1d1c6ed0ba15fe73d267c981451a116df094", + "src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap": "45879a270572248ec82ceafe109319518716d9e9fff0de2a559a29671f8b2274", + "src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap": "849885340d6caff83b53fac179ca8fab4f7164a19c4d013d072c663694f618b6", + "src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap": "93c7d3a280295a51e4c43adf00191ab184085ef21e98dfa90d2e545973cf53a6", + "src/snapshots/ruff_python_parser__string__tests__parse_string_concat.snap": "b272a7f3222f312ece05e24f4cf641a4e41f8ee46e4dc9e7c8f3bc72928f1f6c", + "src/snapshots/ruff_python_parser__string__tests__parse_string_triple_quotes_with_kind.snap": "128528d068a8df340d6a6fa9e8652e1c1bb6379bebc812ce561bf33c5bbf6fe2", + "src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1_error.snap": "887e1edd5e8a90527d50be19492177e3002309a38b63b0cc42e3bf15d7b9a385", + "src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2_error.snap": "887e1edd5e8a90527d50be19492177e3002309a38b63b0cc42e3bf15d7b9a385", + "src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3_error.snap": "92fdf69487cf600cf3352b4aa49bfb8991fc2aa9b7aa2fd3fd402752d89ba4a2", + "src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4_error.snap": "fa77ed8c10a09ca47a9f8d91791abb9565d3dc1acaa80b2ba9675e4c2f30e3f4", + "src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap": "318e407492edb059b432e7efb61e3065b341073f46eee29763565cb8bcf4b820", + "src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap": "a3bf70e3de855fd59b5197ad6cdd1a0d47ea0b685c23b313888254d433428b24", + "src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap": "a036dd08efca7869c54b16ee1b937b28e86b37a3f6c4dbbec544fbdda9e48980", + "src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap": "74e42a8ba2b0eaccb019d2fc35892f79d9130d271956ea60050115b2198c904e", + "src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap": "5708bfff7aa711d50d8058fa6f33532ecd1b61397f550840d7a063da7f220134", + "src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap": "7c249cf487082741c0f818e3f8fc6d5e907d2bf4e5806c2448cbe3a25a0730f9", + "src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap": "debcd6fb2721d51bd5eac5d47c97d51a7b4f6425edb2ea918741045185432529", + "src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap": "ede3a43a993daa416a2bcd13f990881fa6b7d422f7ef44eca77531aa36f04424", + "src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap": "0a7f4bb6dc36ded2d05245e4107ea3830b63ef27c530d0eb5d4fbc22df6bcfb5", + "src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap": "4f211dc909d6055b872818cad14a42eee45f17dd3dd223d4d579341201fd91f8", + "src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap": "d08c3d3ed129d2b740a38375281c31ab020682e9cc43f0ffc4d80bd17813b1ef", + "src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap": "48aad028b146c01e8a58724492d452cf74102b5243656a5cd8102eb1172c98ea", + "src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_1.snap": "015fa2d7b70a4d37d7f8d47b5838b015e0e719bc4054ff59d5ab3902b22b8efa", + "src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_2.snap": "3aa3721f2665a4ed7e5ce3008c9e686c07e2f5246d6c328674060979a84cb457", + "src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1_error.snap": "fdc2ef580f0ec10a9c195ba798ef29567bc28feeb47c5508a801ff5006bdfe1c", + "src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2_error.snap": "92fdf69487cf600cf3352b4aa49bfb8991fc2aa9b7aa2fd3fd402752d89ba4a2", + "src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_1.snap": "944d4e4d3f642df69cf7ad8aaf4b63b26bf958b4addfe7e184407b42ecb661c6", + "src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_2.snap": "f258afaa2ef264e3ba9069092329f27a26648c964fcc0399970b4f28e9d42f53", + "src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap": "f8f81ada44b466c18cebdbd3c55bfc02a2be8ce5e6ccdcd4e11cdeb724e1c0b6", + "src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap": "c73a94524c94c1a8b9137f7b646fc2e637b402202a9ec265664f6b74b0b2cdb3", + "src/snapshots/ruff_python_parser__string__tests__single_quoted_byte.snap": "426013ca86935edbb7c717177c9a1a1287c1ca8ce1527c92abd200aa61079e56", + "src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_mac_eol.snap": "4f1181eb540a75af8b097c19dbb4c4a15375fab6dc1d536511e1224645688683", + "src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_unix_eol.snap": "4f1181eb540a75af8b097c19dbb4c4a15375fab6dc1d536511e1224645688683", + "src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_windows_eol.snap": "1b9c7592a679fc26281aef52376f72364bed3b928cf2c01c7f3e472e83671bba", + "src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap": "4c9d8ded3dc415b46016d9500e0db19637ca4e5c8d7e027786507c094ba59840", + "src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap": "f47297ede21221672ed0570591eba0ba04e7c1c3aa786324fbfa57bdedad3b03", + "src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap": "030dd853a96c5eea9e9b0b3a0104e7b32877f51f8db2129d918278f8c422341d", + "src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap": "bd7589b52d311d3e0edf2ef362602908d93662b07995cb4fd805c61ea44af8bb", + "src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap": "af23917a0add36b89d0c49bafa0997fa4b6d886bf0ed31d3614586e4c493bb16", + "src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap": "643e81a2110bda7fda278ef2931e7a4433f61d247161df4cd4428ed08f4a032a", + "src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap": "6d8a2c7d6ad6d15f72f490e4731c4e2f3d81977c858dbc8bb4947fb731867496", + "src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap": "2584244694c5944bce984c6227d73c8ea6aaf3f77113168db0338419ecaa40cc", + "src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap": "ba91d07d62d5f52a45fb6b39d2e2f53ef4e4656db99f53692afdc4469a9534e6", + "src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap": "a327e8f5190a01a253e34fd7f3d604271c0de5b1c9d10b0cda5d518a517a1a7d", + "src/string.rs": "5b73f0786d5871bd8e28c810e17f0d44bb9b7f66920668cb87b60abc0e9e5751", + "src/token_set.rs": "7ce270836cd75b40aec058300a06406306180f502ac09cdec215deab16ff848b", + "src/token_source.rs": "ef40a3c6a7ce90570d69a83a982801c5158e11ab930b8413e0f6a86de63e2226", + "src/typing.rs": "39c53600f0416d7a6061f6cc7984a2bfeee9a69307e8e53135e754fbd30c1eba" +} diff --git a/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-concat-comma-diagnostics.patch b/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-concat-comma-diagnostics.patch new file mode 100644 index 00000000000..661a143d735 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-concat-comma-diagnostics.patch @@ -0,0 +1,130 @@ +--- a/vendor/rustpython-ruff_python_parser/src/error.rs ++++ b/vendor/rustpython-ruff_python_parser/src/error.rs +@@ -2,7 +2,7 @@ + + use ruff_python_ast::token::TokenKind; + use ruff_python_ast::{ConstantValue, Expr, PythonVersion}; +-use ruff_text_size::{Ranged, TextRange}; ++use ruff_text_size::{Ranged, TextRange, TextSize}; + + use crate::string::InterpolatedStringKind; + +@@ -200,6 +200,14 @@ + /// An unexpected error occurred. + OtherError(String), + ++ /// A missing-comma hint whose concatenated literal is displayed on its final line. ++ /// The original expression range controls error precedence; `diagnostic_start` can be ++ /// after the displayed range's end, which a `TextRange` cannot represent. ++ MissingCommaAfterConcatenatedLiteral { ++ expression_range: TextRange, ++ diagnostic_start: TextSize, ++ }, ++ + /// An error specific to stringified annotations occurred. + StringAnnotationError(&'static str), + +@@ -523,6 +531,9 @@ + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ParseErrorType::OtherError(msg) => f.write_str(msg), ++ ParseErrorType::MissingCommaAfterConcatenatedLiteral { .. } => { ++ f.write_str("invalid syntax. Perhaps you forgot a comma?") ++ } + ParseErrorType::ExpectedIndentedBlock { + clause: BlockClause::StandaloneCase, + .. +--- a/vendor/rustpython-ruff_python_parser/src/parser/expression.rs ++++ b/vendor/rustpython-ruff_python_parser/src/parser/expression.rs +@@ -451,10 +451,63 @@ + return; + }; + let range = TextRange::new(first.start(), second_end); ++ #[cfg(feature = "python315-diagnostics")] ++ if let Some((diagnostic_start, end)) = self.concatenated_literal_comma_range(first) { ++ self.add_error( ++ ParseErrorType::MissingCommaAfterConcatenatedLiteral { ++ expression_range: range, ++ diagnostic_start, ++ }, ++ TextRange::new(diagnostic_start.min(end), end), ++ ); ++ return; ++ } + self.add_error( + ParseErrorType::OtherError("invalid syntax. Perhaps you forgot a comma?".to_string()), + range, + ); ++ } ++ ++ /// Points a multiline concatenation's missing-comma hint at its final literal line, ++ /// retaining the first literal's column, as CPython does. ++ #[cfg(feature = "python315-diagnostics")] ++ fn concatenated_literal_comma_range(&self, first: &ParsedExpr) -> Option<(TextSize, TextSize)> { ++ let last = match &first.expr { ++ Expr::StringLiteral(string) if string.value.is_implicit_concatenated() => { ++ string.value.iter().next_back()?.range() ++ } ++ Expr::BytesLiteral(bytes) if bytes.value.is_implicit_concatenated() => { ++ bytes.value.iter().next_back()?.range() ++ } ++ Expr::FString(string) if string.value.is_implicit_concatenated() => { ++ string.value.iter().next_back()?.range() ++ } ++ Expr::TString(string) if string.value.is_implicit_concatenated() => { ++ string.value.iter().next_back()?.range() ++ } ++ _ => return None, ++ }; ++ let first_start = first.start().to_usize(); ++ let end = last.end().to_usize(); ++ let line_start = |offset| { ++ self.source[..offset] ++ .rfind(['\n', '\r']) ++ .map_or(0, |i| i + 1) ++ }; ++ let first_line = line_start(first_start); ++ let last_line = line_start(end); ++ if first_line == last_line { ++ return None; ++ } ++ let line_end = self.source[end..] ++ .find(['\n', '\r']) ++ .map_or(self.source.len(), |i| end + i); ++ let mut start = (last_line + first_start - first_line).min(line_end); ++ // Keep the projected byte column at a valid character boundary. ++ while !self.source.is_char_boundary(start) { ++ start -= 1; ++ } ++ Some((TextSize::try_from(start).unwrap(), last.end())) + } + + /// Reports the Python 2 `print` or `exec` statement named `keyword` that starts at `start` +--- a/vendor/rustpython-ruff_python_parser/src/parser/mod.rs ++++ b/vendor/rustpython-ruff_python_parser/src/parser/mod.rs +@@ -2175,11 +2175,20 @@ + #[cfg(feature = "python315-diagnostics")] + impl ReplacementFieldHint { + fn new(first: Option<&ParseError>) -> Self { +- let range = first.and_then(|error| { +- matches!(&error.error, ParseErrorType::OtherError(message) if matches!(message.as_str(), +- "invalid syntax. Perhaps you forgot a comma?" | +- "invalid syntax. Is this intended to be part of the string?")) +- .then_some(error.location) ++ let range = first.and_then(|error| match &error.error { ++ ParseErrorType::MissingCommaAfterConcatenatedLiteral { ++ expression_range, .. ++ } => Some(*expression_range), ++ ParseErrorType::OtherError(message) ++ if matches!( ++ message.as_str(), ++ "invalid syntax. Perhaps you forgot a comma?" ++ | "invalid syntax. Is this intended to be part of the string?" ++ ) => ++ { ++ Some(error.location) ++ } ++ _ => None, + }); + Self { + range, diff --git a/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-fstring-diagnostics.patch b/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-fstring-diagnostics.patch new file mode 100644 index 00000000000..65cdbcf5920 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-fstring-diagnostics.patch @@ -0,0 +1,268 @@ +--- a/vendor/rustpython-ruff_python_parser/src/error.rs ++++ b/vendor/rustpython-ruff_python_parser/src/error.rs +@@ -90,6 +90,8 @@ + pub enum InterpolatedStringErrorType { + /// Expected a right brace after an opened left brace. + UnclosedLbrace, ++ /// Expected a right brace for a replacement field opened on an earlier line. ++ UnclosedLbraceOnLine { opening_line: u32 }, + /// An invalid conversion flag was encountered. + InvalidConversionFlag, + /// A single right brace was encountered. +@@ -126,6 +128,9 @@ + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::UnclosedLbrace => f.write_str("expecting '}'"), ++ Self::UnclosedLbraceOnLine { opening_line } => { ++ write!(f, "expecting '}}' to close '{{' on line {opening_line}") ++ } + Self::InvalidConversionFlag => write!(f, "invalid conversion character"), + Self::SingleRbrace => f.write_str("single '}' is not allowed"), + Self::UnterminatedString { .. } => write!(f, "unterminated string"), +@@ -891,8 +896,14 @@ + matches!( + self, + Self::UnclosedStringError { .. } +- | Self::FStringError(InterpolatedStringErrorType::UnclosedLbrace) +- | Self::TStringError(InterpolatedStringErrorType::UnclosedLbrace) ++ | Self::FStringError( ++ InterpolatedStringErrorType::UnclosedLbrace ++ | InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } ++ ) ++ | Self::TStringError( ++ InterpolatedStringErrorType::UnclosedLbrace ++ | InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } ++ ) + ) + } + +@@ -914,6 +925,7 @@ + | InterpolatedStringErrorType::SingleRbrace + | InterpolatedStringErrorType::NewlineInFormatSpec + | InterpolatedStringErrorType::UnclosedLbrace ++ | InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } + | InterpolatedStringErrorType::UnmatchedBracket(_) + ), + Self::UnclosedStringError { .. } +--- a/vendor/rustpython-ruff_python_parser/src/lexer.rs ++++ b/vendor/rustpython-ruff_python_parser/src/lexer.rs +@@ -1126,7 +1126,7 @@ + == self.current_flags.is_triple_quoted() => + { + LexicalErrorType::from_interpolated_string_error( +- InterpolatedStringErrorType::UnclosedLbrace, ++ self.unclosed_replacement_field_error(), + interpolated_string.kind(), + ) + } +@@ -1160,6 +1160,32 @@ + }) + .count(); + u32::try_from(newlines + 1).unwrap() ++ } ++ ++ /// Returns the opening brace of the active replacement field and whether it is in a ++ /// format specifier. Brackets inside its expression are not replacement fields. ++ #[cfg(feature = "python315-diagnostics")] ++ pub(crate) fn replacement_field(&self) -> Option<(TextSize, bool)> { ++ let context = self.interpolated_strings.current()?; ++ if self.nesting <= context.nesting() { ++ return None; ++ } ++ let implicit = self.nesting as usize - self.brackets.len(); ++ let index = ++ (context.replacement_field_nesting(self.nesting) as usize).checked_sub(implicit)?; ++ let bracket = self.brackets.get(index)?; ++ (bracket.kind == '{').then_some((bracket.start, context.is_in_format_spec(self.nesting))) ++ } ++ ++ fn unclosed_replacement_field_error(&self) -> InterpolatedStringErrorType { ++ #[cfg(feature = "python315-diagnostics")] ++ if let Some((start, _)) = self.replacement_field() { ++ let opening_line = self.line_number(start); ++ if opening_line != self.line_number(self.token_range().start()) { ++ return InterpolatedStringErrorType::UnclosedLbraceOnLine { opening_line }; ++ } ++ } ++ InterpolatedStringErrorType::UnclosedLbrace + } + + /// Returns the error for reaching the end of the source unexpectedly at `range`: the innermost +--- a/vendor/rustpython-ruff_python_parser/src/lexer/interpolated_string.rs ++++ b/vendor/rustpython-ruff_python_parser/src/lexer/interpolated_string.rs +@@ -92,6 +92,12 @@ + current_nesting.saturating_sub(self.nesting) + } + ++ /// Returns the nesting level immediately outside the current replacement field. ++ #[cfg(feature = "python315-diagnostics")] ++ pub(crate) const fn replacement_field_nesting(&self, current_nesting: u32) -> u32 { ++ self.nesting + self.format_spec_depth - self.is_in_format_spec(current_nesting) as u32 ++ } ++ + /// Returns `true` if the lexer is in an f-string expression or t-string interpolation i.e., between + /// two curly braces. + pub(crate) const fn is_in_interpolation(&self, current_nesting: u32) -> bool { +--- a/vendor/rustpython-ruff_python_parser/src/parser/mod.rs ++++ b/vendor/rustpython-ruff_python_parser/src/parser/mod.rs +@@ -2023,7 +2023,13 @@ + let mut lexer = Lexer::new(source, mode, start_offset); + // The starts of the tokens at which parsing stops without reading ahead. + let mut stops = Vec::new(); ++ #[cfg(feature = "python315-diagnostics")] ++ let mut replacement_hint = ReplacementFieldHint::new(first.as_ref()); ++ #[cfg(feature = "python315-diagnostics")] ++ let mut replacement_hint_wins = false; + let (tokenizer_error, in_interpolated_string, detected) = loop { ++ #[cfg(feature = "python315-diagnostics")] ++ let replacement_field = lexer.replacement_field(); + let in_interpolated_string = lexer.in_interpolated_string(); + let previous_errors = lexer.errors().len(); + let kind = lexer.next_token(); +@@ -2031,21 +2037,30 @@ + .iter() + .find(|error| error.error().is_tokenizer_error()) + { ++ #[cfg(feature = "python315-diagnostics")] ++ { ++ replacement_hint_wins = ++ replacement_hint.is_unclosed_field_hint(error.error(), replacement_field); ++ } + // A missing closing brace or a bracket that closes a replacement field is found at + // the token that shows it. + let detected = match error.error() { + LexicalErrorType::FStringError( + InterpolatedStringErrorType::UnclosedLbrace ++ | InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } + | InterpolatedStringErrorType::UnmatchedBracket(_), + ) + | LexicalErrorType::TStringError( + InterpolatedStringErrorType::UnclosedLbrace ++ | InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } + | InterpolatedStringErrorType::UnmatchedBracket(_), + ) => error.location().start(), + _ => lexer.offset(), + }; + break (error.clone(), in_interpolated_string, detected); + } ++ #[cfg(feature = "python315-diagnostics")] ++ replacement_hint.token(kind, lexer.current_range(), replacement_field); + if matches!( + kind, + TokenKind::Colon | TokenKind::Semi | TokenKind::Unknown +@@ -2053,6 +2068,17 @@ + stops.push(lexer.current_range().start()); + } + if kind == TokenKind::EndOfFile { ++ #[cfg(feature = "python315-diagnostics")] ++ if let Some(start) = replacement_hint.unclosed_format_spec { ++ let error = LexicalError::new( ++ LexicalErrorType::UnclosedBracket { ++ opening: '{', ++ incomplete: false, ++ }, ++ TextRange::empty(start), ++ ); ++ break (error, false, lexer.offset()); ++ } + return; + } + }; +@@ -2076,7 +2102,10 @@ + LexicalErrorType::UnicodeEscapeError { .. } | LexicalErrorType::BytesEscapeError { .. } + ) + ); +- let wins = detected <= first_start ++ #[cfg(not(feature = "python315-diagnostics"))] ++ let replacement_hint_wins = false; ++ let wins = replacement_hint_wins ++ || detected <= first_start + || (!in_interpolated_string + && !escape_error + && match tokenizer_error.error() { +@@ -2113,6 +2142,85 @@ + move_to_front(errors, ParseError::from(front)); + } + ++/// Tracks the grammar boundaries after an expression hint while the unrecovered token stream ++/// is read. A replacement field keeps reading to its separator; an inner display or a debug / ++/// conversion separator can already establish the hint before a later missing brace is found. ++#[cfg(feature = "python315-diagnostics")] ++struct ReplacementFieldHint { ++ range: Option, ++ field: Option, ++ stopped: bool, ++ unclosed_format_spec: Option, ++} ++ ++#[cfg(feature = "python315-diagnostics")] ++impl ReplacementFieldHint { ++ fn new(first: Option<&ParseError>) -> Self { ++ let range = first.and_then(|error| { ++ matches!(&error.error, ParseErrorType::OtherError(message) if matches!(message.as_str(), ++ "invalid syntax. Perhaps you forgot a comma?" | ++ "invalid syntax. Is this intended to be part of the string?")) ++ .then_some(error.location) ++ }); ++ Self { ++ range, ++ field: None, ++ stopped: false, ++ unclosed_format_spec: None, ++ } ++ } ++ ++ fn token(&mut self, kind: TokenKind, range: TextRange, field: Option<(TextSize, bool)>) { ++ let Some(hint) = self.range else { ++ return; ++ }; ++ if range.contains(hint.start()) { ++ self.field = field.map(|(start, _)| start); ++ } ++ if range.start() < hint.end() || self.field.is_none() { ++ return; ++ } ++ // The hint covers both expressions. A later expression token establishes the hint; ++ // only the format-specifier and string-end tokens still belong to this field. ++ if !kind.is_trivia() ++ && !matches!( ++ kind, ++ TokenKind::Colon ++ | TokenKind::FStringMiddle ++ | TokenKind::TStringMiddle ++ | TokenKind::FStringEnd ++ | TokenKind::TStringEnd ++ | TokenKind::EndOfFile ++ ) ++ { ++ self.stopped = true; ++ } ++ if kind.is_interpolated_string_end() && !self.stopped { ++ self.unclosed_format_spec = field ++ .filter(|(start, format)| *format && Some(*start) == self.field) ++ .map(|(start, _)| start); ++ } ++ } ++ ++ fn is_unclosed_field_hint( ++ &self, ++ error: &LexicalErrorType, ++ field: Option<(TextSize, bool)>, ++ ) -> bool { ++ self.field.is_some() ++ && self.field == field.map(|(start, _)| start) ++ && !self.stopped ++ && matches!( ++ error, ++ LexicalErrorType::FStringError( ++ InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } ++ ) | LexicalErrorType::TStringError( ++ InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } ++ ) ++ ) ++ } ++} ++ + /// Moves `error` to the front of `errors`, removing an equal error elsewhere. + fn move_to_front(errors: &mut Vec, error: ParseError) { + errors.retain(|other| *other != error); diff --git a/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-parameter-diagnostics.patch b/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-parameter-diagnostics.patch new file mode 100644 index 00000000000..8426eb714c3 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-parameter-diagnostics.patch @@ -0,0 +1,120 @@ +--- a/vendor/rustpython-ruff_python_parser/Cargo.toml ++++ b/vendor/rustpython-ruff_python_parser/Cargo.toml +@@ -35,6 +35,10 @@ + [lib] + name = "ruff_python_parser" + path = "src/lib.rs" ++ ++# Local RustPython compatibility: wording only; independent of grammar target. ++[features] ++python315-diagnostics = [] + + [dependencies.bitflags] + version = "2.5.0" +--- a/vendor/rustpython-ruff_python_parser/Cargo.toml.orig ++++ b/vendor/rustpython-ruff_python_parser/Cargo.toml.orig +@@ -12,6 +12,10 @@ + + [lib] + name = "ruff_python_parser" ++ ++# Local RustPython compatibility: wording only; independent of grammar target. ++[features] ++python315-diagnostics = [] + + [dependencies] + ruff_python_ast = { workspace = true, features = ["get-size"] } +--- a/vendor/rustpython-ruff_python_parser/src/error.rs ++++ b/vendor/rustpython-ruff_python_parser/src/error.rs +@@ -609,22 +609,38 @@ + } + ParseErrorType::EmptyTypeParams => f.write_str("Type parameter list cannot be empty"), + ParseErrorType::ParamAfterVarKeywordParam => { +- f.write_str("arguments cannot follow var-keyword argument") ++ f.write_str(if cfg!(feature = "python315-diagnostics") { ++ "parameters cannot follow var-keyword parameter" ++ } else { ++ "arguments cannot follow var-keyword argument" ++ }) + } + ParseErrorType::NonDefaultParamAfterDefaultParam => { + f.write_str("parameter without a default follows parameter with a default") + } + ParseErrorType::ExpectedKeywordParam => { +- f.write_str("named arguments must follow bare *") ++ f.write_str(if cfg!(feature = "python315-diagnostics") { ++ "named parameters must follow bare *" ++ } else { ++ "named arguments must follow bare *" ++ }) + } + ParseErrorType::VarKeywordParameterWithDefault => { +- f.write_str("var-keyword argument cannot have default value") ++ f.write_str(if cfg!(feature = "python315-diagnostics") { ++ "var-keyword parameter cannot have default value" ++ } else { ++ "var-keyword argument cannot have default value" ++ }) + } + ParseErrorType::ExpectedColonAfterDictionaryKey => { + f.write_str("':' expected after dictionary key") + } + ParseErrorType::VarParameterWithDefault => { +- f.write_str("var-positional argument cannot have default value") ++ f.write_str(if cfg!(feature = "python315-diagnostics") { ++ "var-positional parameter cannot have default value" ++ } else { ++ "var-positional argument cannot have default value" ++ }) + } + ParseErrorType::InvalidStarPatternUsage => { + f.write_str("cannot use starred expression here") +--- a/vendor/rustpython-ruff_python_parser/src/parser/statement.rs ++++ b/vendor/rustpython-ruff_python_parser/src/parser/statement.rs +@@ -3652,10 +3652,13 @@ + // # def foo(a, *, b, c, *args): ... + // def foo(a, *args1, *args2, b): ... + // def foo(a, *args1, b, c, *args2): ... ++ let message = if cfg!(feature = "python315-diagnostics") { ++ "* may appear only once" ++ } else { ++ "* argument may appear only once" ++ }; + parser.add_error( +- ParseErrorType::OtherError( +- "* argument may appear only once".to_string(), +- ), ++ ParseErrorType::OtherError(message.to_string()), + star_range, + ); + } +@@ -3675,7 +3678,11 @@ + // def foo(a, *args, *, b): ... + // def foo(a, *args, b, c, *): ... + let message = if parser.at(TokenKind::Comma) { +- "* argument may appear only once" ++ if cfg!(feature = "python315-diagnostics") { ++ "* may appear only once" ++ } else { ++ "* argument may appear only once" ++ } + } else { + "invalid syntax" + }; +@@ -3759,10 +3766,13 @@ + // test_err params_no_arg_before_slash + // def foo(/): ... + // def foo(/, a): ... ++ let message = if cfg!(feature = "python315-diagnostics") { ++ "at least one parameter must precede /" ++ } else { ++ "at least one argument must precede /" ++ }; + parser.add_error( +- ParseErrorType::OtherError( +- "at least one argument must precede /".to_string(), +- ), ++ ParseErrorType::OtherError(message.to_string()), + slash_range, + ); + } diff --git a/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-starred-generator-operands.patch b/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-starred-generator-operands.patch new file mode 100644 index 00000000000..e6091f5e4d1 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-starred-generator-operands.patch @@ -0,0 +1,135 @@ +--- a/vendor/rustpython-ruff_python_parser/src/parser/expression.rs ++++ b/vendor/rustpython-ruff_python_parser/src/parser/expression.rs +@@ -3052,7 +3052,9 @@ + }; + } + let mut parsed_expr = self.parse_named_expression_or_higher( +- ExpressionContext::yield_or_starred_bitwise_or().with_for_excluded(), ++ ExpressionContext::yield_or_starred_bitwise_or() ++ .with_for_excluded() ++ .with_starred_generator_allowed(), + ); + + match self.current_token_kind() { +@@ -3621,9 +3623,22 @@ + context.disallow_starred_expressions(), + ), + StarredExpressionPrecedence::BitwiseOr => { ++ let errors_start = self.errors.len(); + let value = self + .parse_conditional_expression_without_comma_check(ExpressionContext::default()); ++ let operand_is_valid = self.errors.len() == errors_start; + self.validate_unpacking_precedence(&value, start, false); ++ // Generator elements use `starred_expression` (`* expression`). ++ // Keep a display-precedence error only for an overall parse failure. ++ if operand_is_valid ++ && context.is_starred_generator_allowed() ++ && matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For) ++ && self.errors.len() == errors_start + 1 ++ { ++ let error = self.errors.pop().expect("operand precedence error exists"); ++ self.deferred_starred_generator_errors ++ .push((errors_start, error)); ++ } + value + } + }; +@@ -4121,6 +4136,10 @@ + /// This flag is set when the `for` keyword, or `async` starting `async for`, should be + /// excluded from an expression. + const EXCLUDE_FOR = 1 << 4; ++ ++ /// The first element after `(` may become a generator expression. Its starred ++ /// operand can use full expression precedence if a comprehension clause follows. ++ const ALLOW_STARRED_GENERATOR = 1 << 5; + } + } + +@@ -4191,6 +4210,16 @@ + /// Returns a new [`ExpressionContext`] which excludes `for` from an expression. + fn with_for_excluded(self) -> Self { + ExpressionContext(self.0 | ExpressionContextFlags::EXCLUDE_FOR) ++ } ++ ++ /// Allow the first parenthesized starred element to become a generator element. ++ fn with_starred_generator_allowed(self) -> Self { ++ ExpressionContext(self.0 | ExpressionContextFlags::ALLOW_STARRED_GENERATOR) ++ } ++ ++ const fn is_starred_generator_allowed(self) -> bool { ++ self.0 ++ .contains(ExpressionContextFlags::ALLOW_STARRED_GENERATOR) + } + + /// Returns `true` if the `in` keyword should be excluded from a comparison expression. +--- a/vendor/rustpython-ruff_python_parser/src/parser/mod.rs ++++ b/vendor/rustpython-ruff_python_parser/src/parser/mod.rs +@@ -82,6 +82,10 @@ + /// Stores all the syntax errors found during the parsing. + errors: Vec, + ++ /// Display-precedence errors that are valid generator operands, retained for ++ /// the invalid grammar fallback if the overall parse fails. ++ deferred_starred_generator_errors: Vec<(usize, ParseError)>, ++ + /// Stores non-fatal syntax errors found during parsing, such as version-related errors. + unsupported_syntax_errors: Vec, + +@@ -163,6 +167,7 @@ + options, + source, + errors: Vec::new(), ++ deferred_starred_generator_errors: Vec::new(), + unsupported_syntax_errors: Vec::new(), + tokens, + name_interner: NameInterner::default(), +@@ -283,6 +288,14 @@ + ); + // TODO consider re-integrating lexical error handling into the parser? + let mut parse_errors = self.errors; ++ if !parse_errors.is_empty() ++ && let Some((0, error)) = self.deferred_starred_generator_errors.into_iter().next() ++ { ++ // CPython retries its invalid grammar rules only after a parse failure. ++ // Preserve encounter order, including nested operands whose ranges begin ++ // after the outer expression's later error. Semantic errors occur later. ++ parse_errors.insert(0, error); ++ } + let (tokens, lex_errors) = self.tokens.finish(); + + // Fast path for when there are no lex errors. +@@ -1163,6 +1176,9 @@ + ParserCheckpoint { + tokens: self.tokens.checkpoint(), + errors_position: self.errors.len(), ++ deferred_starred_generator_errors_position: self ++ .deferred_starred_generator_errors ++ .len(), + unsupported_syntax_errors_position: self.unsupported_syntax_errors.len(), + current_token_id: self.current_token_id, + prev_token_end: self.prev_token_end, +@@ -1175,6 +1191,7 @@ + let ParserCheckpoint { + tokens, + errors_position, ++ deferred_starred_generator_errors_position, + unsupported_syntax_errors_position, + current_token_id, + prev_token_end, +@@ -1183,6 +1200,8 @@ + + self.tokens.rewind(tokens); + self.errors.truncate(errors_position); ++ self.deferred_starred_generator_errors ++ .truncate(deferred_starred_generator_errors_position); + self.unsupported_syntax_errors + .truncate(unsupported_syntax_errors_position); + self.current_token_id = current_token_id; +@@ -1214,6 +1233,7 @@ + struct ParserCheckpoint { + tokens: TokenSourceCheckpoint, + errors_position: usize, ++ deferred_starred_generator_errors_position: usize, + unsupported_syntax_errors_position: usize, + current_token_id: TokenId, + prev_token_end: TextSize, diff --git a/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-statement-diagnostics.patch b/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-statement-diagnostics.patch new file mode 100644 index 00000000000..6c65122c614 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-statement-diagnostics.patch @@ -0,0 +1,709 @@ +--- a/vendor/rustpython-ruff_python_parser/src/error.rs ++++ b/vendor/rustpython-ruff_python_parser/src/error.rs +@@ -198,6 +198,17 @@ + /// An error specific to stringified annotations occurred. + StringAnnotationError(&'static str), + ++ /// A `raise from` statement has no exception expression. ++ MissingRaiseException, ++ /// A `raise` statement ends immediately after `from`. ++ MissingRaiseCause, ++ /// An unparenthesized `with` item list ends with a comma. ++ TrailingCommaInWith, ++ /// `lazy` follows the module name instead of preceding `from`. ++ MisplacedLazyImport, ++ /// An absolute future import is marked lazy. ++ LazyFutureImport, ++ + /// An empty slice was found during parsing, e.g `data[]`. + EmptySlice, + /// An empty global names list was found during parsing. +@@ -240,6 +251,8 @@ + InvalidStarPatternUsage, + /// An underscore was used as a binding target in a match pattern. + InvalidMatchPatternTarget, ++ /// A complete case clause was found outside a match statement. ++ CaseOutsideMatch, + + /// A parameter was found after a vararg. + ParamAfterVarKeywordParam, +@@ -259,6 +272,10 @@ + kind: ExpressionKind, + maybe_comparison: bool, + }, ++ /// A named expression is used in an assertion without its required parentheses. ++ NamedExpressionWithoutParentheses, ++ /// A mapping pattern contains an entry after its double-star rest pattern. ++ MappingRestPatternNotLast, + /// A name is assigned to where `==` or `:=` may have been meant. + AssignmentInsteadOfComparison, + /// A `yield` expression is assigned to. +@@ -456,6 +473,8 @@ + Finally, + Match, + Case, ++ /// A standalone case uses the generic invalid-block diagnostic. ++ StandaloneCase, + Class, + FunctionDef, + } +@@ -474,7 +493,7 @@ + Self::ExceptStar => "'except*' statement", + Self::Finally => "'finally' statement", + Self::Match => "'match' statement", +- Self::Case => "'case' statement", ++ Self::Case | Self::StandaloneCase => "'case' statement", + Self::Class => "class definition", + Self::FunctionDef => "function definition", + }) +@@ -499,6 +518,10 @@ + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ParseErrorType::OtherError(msg) => f.write_str(msg), ++ ParseErrorType::ExpectedIndentedBlock { ++ clause: BlockClause::StandaloneCase, ++ .. ++ } => f.write_str("expected an indented block"), + ParseErrorType::ExpectedIndentedBlock { clause, line } => { + write!( + f, +@@ -594,6 +617,21 @@ + ParseErrorType::PositionalAfterKeywordUnpacking => { + f.write_str("positional argument follows keyword argument unpacking") + } ++ ParseErrorType::MissingRaiseException => { ++ f.write_str("did you forget an expression between 'raise' and 'from'?") ++ } ++ ParseErrorType::MissingRaiseCause => { ++ f.write_str("did you forget an expression after 'from'?") ++ } ++ ParseErrorType::TrailingCommaInWith => { ++ f.write_str("the last 'with' item has a trailing comma") ++ } ++ ParseErrorType::MisplacedLazyImport => { ++ f.write_str("use 'lazy from ... ' instead of 'from ... lazy import'") ++ } ++ ParseErrorType::LazyFutureImport => { ++ f.write_str("lazy from __future__ import is not allowed") ++ } + ParseErrorType::EmptySlice => f.write_str("expected index or slice expression"), + ParseErrorType::EmptyGlobalNames => { + f.write_str("global statement must have at least one name") +@@ -646,6 +684,9 @@ + f.write_str("cannot use starred expression here") + } + ParseErrorType::InvalidMatchPatternTarget => f.write_str("cannot use '_' as a target"), ++ ParseErrorType::CaseOutsideMatch => { ++ f.write_str("case statement must be inside match statement") ++ } + ParseErrorType::ExpectedRealNumber => { + f.write_str("expected a real number in complex literal pattern") + } +@@ -666,6 +707,12 @@ + } => write!( + f, + "cannot assign to {kind} here. Maybe you meant '==' instead of '='?" ++ ), ++ ParseErrorType::NamedExpressionWithoutParentheses => { ++ f.write_str("cannot use named expression without parentheses here") ++ } ++ ParseErrorType::MappingRestPatternNotLast => f.write_str( ++ "double star pattern must be the last (right-most) subpattern in the mapping pattern", + ), + ParseErrorType::AssignmentInsteadOfComparison => { + f.write_str("invalid syntax. Maybe you meant '==' or ':=' instead of '='?") +--- a/vendor/rustpython-ruff_python_parser/src/parser/mod.rs ++++ b/vendor/rustpython-ruff_python_parser/src/parser/mod.rs +@@ -31,6 +31,8 @@ + + pub use crate::parser::options::ParseOptions; + ++#[cfg(feature = "python315-diagnostics")] ++mod case_diagnostics; + mod expression; + mod helpers; + mod options; +@@ -1146,12 +1148,29 @@ + + if let Some(trailing_comma_range) = trailing_comma_range { + if !recovery_context_kind.allow_trailing_comma() { +- self.add_error( ++ let error = if cfg!(feature = "python315-diagnostics") ++ && matches!(recovery_context_kind, RecoveryContextKind::WithItems(_)) ++ && self.at(TokenKind::Colon) ++ { ++ ParseErrorType::TrailingCommaInWith ++ } else { + ParseErrorType::OtherError( + "trailing comma not allowed without surrounding parentheses".to_string(), +- ), +- trailing_comma_range, +- ); ++ ) ++ }; ++ let range = if cfg!(feature = "python315-diagnostics") ++ && matches!( ++ recovery_context_kind, ++ RecoveryContextKind::ImportFromAsNames(Parenthesized::No) ++ ) ++ && self.at(TokenKind::Newline) ++ { ++ // The invalid import-target production diagnoses its NEWLINE. ++ self.current_token_range() ++ } else { ++ trailing_comma_range ++ }; ++ self.add_error(error, range); + } + } + +--- a/vendor/rustpython-ruff_python_parser/src/parser/statement.rs ++++ b/vendor/rustpython-ruff_python_parser/src/parser/statement.rs +@@ -111,8 +111,10 @@ + /// - + pub(super) fn parse_statement(&mut self) -> Stmt { + let start = self.node_start(); +- +- match self.current_token_kind() { ++ #[cfg(feature = "python315-diagnostics")] ++ let case_diagnostic = self.standalone_case_diagnostic(); ++ ++ let statement = match self.current_token_kind() { + TokenKind::If => Stmt::If(self.parse_if_statement()), + TokenKind::For => Stmt::For(self.parse_for_statement(start)), + TokenKind::While => Stmt::While(self.parse_while_statement()), +@@ -146,7 +148,18 @@ + + self.parse_single_simple_statement() + } +- } ++ }; ++ ++ #[cfg(feature = "python315-diagnostics")] ++ if !self.errors.is_empty() ++ && let Some(error) = case_diagnostic ++ { ++ // Only the invalid-grammar diagnostic changes. Keep the ordinary ++ // statement parser's AST, tokens, and recovery path intact. ++ self.errors.insert(0, error); ++ } ++ ++ statement + } + + /// Parses a single simple statement. +@@ -567,12 +580,19 @@ + // test_err raise_stmt_from_without_exc + // raise from exc + // raise from None +- self.add_error( +- ParseErrorType::OtherError( +- "exception missing in `raise` statement with cause".to_string(), +- ), +- self.current_token_range(), +- ); ++ if cfg!(feature = "python315-diagnostics") { ++ self.add_error( ++ ParseErrorType::MissingRaiseException, ++ TextRange::new(start, self.current_token_range().end()), ++ ); ++ } else { ++ self.add_error( ++ ParseErrorType::OtherError( ++ "exception missing in `raise` statement with cause".to_string(), ++ ), ++ self.current_token_range(), ++ ); ++ } + None + } + _ => { +@@ -598,7 +618,17 @@ + } + }; + ++ let from_range = self.current_token_range(); + let cause = self.eat(TokenKind::From).then(|| { ++ if cfg!(feature = "python315-diagnostics") ++ && exc.is_some() ++ && matches!( ++ self.current_token_kind(), ++ TokenKind::Newline | TokenKind::EndOfFile | TokenKind::Semi ++ ) ++ { ++ self.add_error(ParseErrorType::MissingRaiseCause, from_range); ++ } + // test_err raise_stmt_invalid_cause + // raise x from *y + // raise x from yield y +@@ -675,7 +705,7 @@ + if names.is_empty() { + // test_err import_stmt_empty + // import +- self.add_error(ParseErrorType::EmptyImportNames, self.current_token_range()); ++ self.add_empty_import_names_error(); + } + + ast::StmtImport { +@@ -718,6 +748,7 @@ + start: TextSize, + is_lazy: bool, + ) -> ast::StmtImportFrom { ++ let import_errors_start = self.errors.len(); + self.bump(TokenKind::From); + + let mut leading_dots = 0; +@@ -756,6 +787,13 @@ + None + }; + ++ if cfg!(feature = "python315-diagnostics") ++ && module.is_some() ++ && let Some(range) = self.misplaced_lazy_import_range() ++ { ++ self.add_error(ParseErrorType::MisplacedLazyImport, range); ++ } ++ + // test_ok from_import_no_space + // from.import x + // from...import x +@@ -766,6 +804,11 @@ + let mut seen_star_import = false; + + let parenthesized = Parenthesized::from(self.eat(TokenKind::Lpar)); ++ let lazy_future = cfg!(feature = "python315-diagnostics") ++ && is_lazy ++ && leading_dots == 0 ++ && module.as_ref().is_some_and(|name| name.id == "__future__"); ++ let mut incomplete_optional_alias = false; + + // test_err from_import_unparenthesized_trailing_comma + // from a import b, +@@ -778,6 +821,15 @@ + // from x import a. + // from x import a.b + // from x import a, b.c, d, e.f, g ++ // CPython's optional `as NAME` rolls back on a missing name, so an ++ // unparenthesized target list can finish before this recovery error. ++ incomplete_optional_alias = lazy_future ++ && !parenthesized.is_yes() ++ && parser.at_identifier_or_soft_keyword() ++ && matches!( ++ parser.peek2(), ++ (TokenKind::As, TokenKind::Newline | TokenKind::EndOfFile) ++ ); + let alias = parser.parse_alias(ImportStyle::ImportFrom); + seen_star_import |= alias.name.id == "*"; + parser.alias_scratch.push(alias); +@@ -790,7 +842,7 @@ + // from x import + // from x import () + // from x import ,, +- self.add_error(ParseErrorType::EmptyImportNames, self.current_token_range()); ++ self.add_empty_import_names_error(); + } + + if seen_star_import && parenthesized.is_yes() { +@@ -823,6 +875,24 @@ + self.expect(TokenKind::Rpar); + } + ++ if lazy_future ++ && !names.is_empty() ++ && (self.errors.len() == import_errors_start ++ || (!parenthesized.is_yes() ++ && incomplete_optional_alias ++ && self.errors.len() == import_errors_start + 1)) ++ { ++ // This grammar action precedes subsequent statements and the optional-as ++ // recovery above. Keep the recovered AST and tokens unchanged. ++ self.errors.insert( ++ import_errors_start, ++ crate::ParseError { ++ error: ParseErrorType::LazyFutureImport, ++ location: TextRange::at(start, TextSize::new(4)), ++ }, ++ ); ++ } ++ + ast::StmtImportFrom { + module, + names, +@@ -832,6 +902,51 @@ + node_index: AtomicNodeIndex::NONE, + runtime_level: None, + } ++ } ++ ++ /// Checks the misplaced `lazy` invalid production with the import-target parser. ++ /// Rewind afterward so the existing invalid-input recovery and AST stay intact. ++ fn misplaced_lazy_import_range(&mut self) -> Option { ++ if !self.at(TokenKind::Lazy) || self.peek() != TokenKind::Import { ++ return None; ++ } ++ let checkpoint = self.checkpoint(); ++ let errors_before = self.errors.len(); ++ let lazy_range = self.current_token_range(); ++ self.bump(TokenKind::Lazy); ++ self.bump(TokenKind::Import); ++ let parenthesized = Parenthesized::from(self.eat(TokenKind::Lpar)); ++ let names = self.parse_comma_separated_list_into_vec( ++ RecoveryContextKind::ImportFromAsNames(parenthesized), ++ |parser| parser.parse_alias(ImportStyle::ImportFrom), ++ ); ++ if parenthesized.is_yes() { ++ self.expect(TokenKind::Rpar); ++ } ++ let valid = !names.is_empty() ++ && self.errors.len() == errors_before ++ && (!names.iter().any(|alias| alias.name.id == "*") ++ || (names.len() == 1 && !parenthesized.is_yes())); ++ self.rewind(checkpoint); ++ valid.then_some(lazy_range) ++ } ++ ++ /// Empty import targets use a cursor diagnostic only at newline or EOF. ++ /// A concrete unexpected token keeps its ordinary syntax-error span. ++ fn add_empty_import_names_error(&mut self) { ++ let error = if cfg!(feature = "python315-diagnostics") ++ && !matches!( ++ self.current_token_kind(), ++ TokenKind::Newline | TokenKind::EndOfFile ++ ) { ++ ParseErrorType::ExpectedToken { ++ expected: TokenKind::Identifier, ++ found: self.current_token_kind(), ++ } ++ } else { ++ ParseErrorType::EmptyImportNames ++ }; ++ self.add_error(error, self.current_token_range()); + } + + /// Parses an `import` or `from` import name. +@@ -890,10 +1005,15 @@ + } else { + // test_err import_alias_missing_asname + // import x as +- self.add_error( +- ParseErrorType::OtherError("expected symbol after `as`".to_string()), +- self.current_token_range(), +- ); ++ let error = if cfg!(feature = "python315-diagnostics") { ++ ParseErrorType::ExpectedToken { ++ expected: TokenKind::Identifier, ++ found: self.current_token_kind(), ++ } ++ } else { ++ ParseErrorType::OtherError("expected symbol after `as`".to_string()) ++ }; ++ self.add_error(error, self.current_token_range()); + None + } + } else { +@@ -1014,7 +1134,11 @@ + // assert assert x + // assert yield x + // assert x := 1 ++ let test_errors_start = self.errors.len(); + let test = self.parse_conditional_expression_or_higher(); ++ if cfg!(feature = "python315-diagnostics") && self.errors.len() == test_errors_start { ++ self.check_assert_assignment(&test); ++ } + + let msg = if self.eat(TokenKind::Comma) { + if self.at_expr() { +@@ -1023,7 +1147,14 @@ + // assert False, assert x + // assert False, yield x + // assert False, x := 1 +- Some(Box::new(self.parse_conditional_expression_or_higher().expr)) ++ let message_errors_start = self.errors.len(); ++ let message = self.parse_conditional_expression_or_higher(); ++ if cfg!(feature = "python315-diagnostics") ++ && self.errors.len() == message_errors_start ++ { ++ self.check_assert_assignment(&message); ++ } ++ Some(Box::new(message.expr)) + } else { + // test_err assert_empty_msg + // assert x, +@@ -1042,6 +1173,39 @@ + msg, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, ++ } ++ } ++ ++ /// Diagnose an assignment after an otherwise valid assertion expression. Speculative ++ /// parsing supplies the expression span without consuming recovery tokens or changing ++ /// the recovered assertion AST. Errors inside parentheses retain their own diagnostics. ++ fn check_assert_assignment(&mut self, target: &ParsedExpr) { ++ let operator = self.current_token_kind(); ++ if !matches!(operator, TokenKind::Equal | TokenKind::ColonEqual) { ++ return; ++ } ++ ++ let checkpoint = self.checkpoint(); ++ let errors_start = self.errors.len(); ++ self.bump(operator); ++ let value_end = if self.at_expr() { ++ let value = self.parse_conditional_expression_or_higher(); ++ (self.errors.len() == errors_start).then_some(value.end()) ++ } else { ++ None ++ }; ++ self.rewind(checkpoint); ++ ++ if let Some(value_end) = value_end { ++ let error = if operator == TokenKind::ColonEqual { ++ ParseErrorType::NamedExpressionWithoutParentheses ++ } else { ++ ParseErrorType::InvalidAssignmentTarget { ++ kind: ExpressionKind::of(&target.expr), ++ maybe_comparison: true, ++ } ++ }; ++ self.add_error(error, TextRange::new(target.start(), value_end)); + } + } + +@@ -2471,12 +2635,14 @@ + /// See: + fn parse_with_items(&mut self) -> Vec { + if !self.at_expr() { +- self.add_error( ++ let error = if cfg!(feature = "python315-diagnostics") { ++ ParseErrorType::ExpectedExpression ++ } else { + ParseErrorType::OtherError( + "expected the start of an expression after `with` keyword".to_string(), +- ), +- self.current_token_range(), +- ); ++ ) ++ }; ++ self.add_error(error, self.current_token_range()); + return vec![]; + } + +@@ -3326,7 +3492,7 @@ + /// clause header or an indented block. + /// + /// `header_start` is the start of the clause's header keyword token. +- fn parse_body(&mut self, clause: BlockClause, header_start: TextSize) -> Suite { ++ pub(super) fn parse_body(&mut self, clause: BlockClause, header_start: TextSize) -> Suite { + // Note: The test cases in this method chooses a clause at random to test + // the error logic. + +--- a/vendor/rustpython-ruff_python_parser/src/parser/pattern.rs ++++ b/vendor/rustpython-ruff_python_parser/src/parser/pattern.rs +@@ -212,6 +212,8 @@ + + self.parse_comma_separated_list(RecoveryContextKind::MatchPatternMapping, |parser| { + let mapping_item_start = parser.node_start(); ++ let mapping_item_token = parser.current_token_range(); ++ let mapping_item_errors_start = parser.errors.len(); + + if parser.eat(TokenKind::DoubleStar) { + // A `_` rest target is not a capture target. +@@ -267,17 +269,32 @@ + }; + keys.push(key); + ++ let colon_errors_start = parser.errors.len(); + parser.expect(TokenKind::Colon); ++ if cfg!(feature = "python315-diagnostics") ++ && rest.is_some() ++ && parser.errors.len() > colon_errors_start ++ { ++ // An incomplete mapping entry does not match the invalid rest-order ++ // production. Report its first token, where the mapping stopped. ++ parser.errors[colon_errors_start].location = mapping_item_token; ++ } + + patterns.push(parser.parse_match_pattern(AllowStarPattern::No)); + +- if rest.is_some() { +- parser.add_error( +- ParseErrorType::OtherError( +- "pattern cannot follow a double star pattern".to_string(), +- ), +- parser.node_range(mapping_item_start), +- ); ++ if let Some(rest) = &rest { ++ if cfg!(feature = "python315-diagnostics") { ++ if parser.errors.len() == mapping_item_errors_start { ++ parser.add_error(ParseErrorType::MappingRestPatternNotLast, rest); ++ } ++ } else { ++ parser.add_error( ++ ParseErrorType::OtherError( ++ "pattern cannot follow a double star pattern".to_string(), ++ ), ++ parser.node_range(mapping_item_start), ++ ); ++ } + } + } + }); +--- /dev/null ++++ b/vendor/rustpython-ruff_python_parser/src/parser/case_diagnostics.rs +@@ -0,0 +1,156 @@ ++//! The invalid standalone-case production. This is diagnostic-only speculation: ++//! the caller always parses the original statement again for syntax and tokens. ++ ++use ruff_python_ast::token::TokenKind; ++use ruff_python_ast::{Expr, Pattern}; ++use ruff_text_size::TextRange; ++ ++use super::Parser; ++use super::expression::ExpressionContext; ++use crate::{BlockClause, ParseError, ParseErrorType}; ++ ++impl Parser<'_> { ++ pub(super) fn standalone_case_diagnostic(&mut self) -> Option { ++ if !self.at(TokenKind::Case) ++ || !self.errors.is_empty() ++ || self ++ .token_before(self.node_start()) ++ .is_some_and(|(kind, _)| { ++ !matches!( ++ kind, ++ TokenKind::Newline | TokenKind::Indent | TokenKind::Dedent ++ ) ++ }) ++ { ++ return None; ++ } ++ ++ let checkpoint = self.checkpoint(); ++ // These recovery fields are not covered by the normal parser checkpoint. ++ let unclosed_bracket = self.first_unclosed_bracket_recovery; ++ let escape_error = self.interpolated_string_escape_error.clone(); ++ let replacement_field = self.replacement_field; ++ let diagnostic = self.parse_invalid_standalone_case(); ++ self.rewind(checkpoint); ++ self.first_unclosed_bracket_recovery = unclosed_bracket; ++ self.interpolated_string_escape_error = escape_error; ++ self.replacement_field = replacement_field; ++ diagnostic ++ } ++ ++ fn parse_invalid_standalone_case(&mut self) -> Option { ++ let start = self.node_start(); ++ self.bump(TokenKind::Case); ++ let pattern = self.parse_match_patterns(); ++ if !self.errors.is_empty() { ++ return self.standalone_case_inner_error(); ++ } ++ if !pattern_has_valid_value_names(&pattern) { ++ return None; ++ } ++ ++ if self.eat(TokenKind::If) { ++ if !self.at_expr() { ++ return None; ++ } ++ self.parse_named_expression_or_higher(ExpressionContext::default()); ++ if !self.errors.is_empty() { ++ return self.standalone_case_inner_error(); ++ } ++ } ++ if !self.at(TokenKind::Colon) { ++ return None; ++ } ++ let header_range = TextRange::new(start, self.current_token_range().end()); ++ self.bump(TokenKind::Colon); ++ self.parse_body(BlockClause::StandaloneCase, start); ++ ++ if self ++ .tokens ++ .has_tokenizer_error_in(TextRange::new(start, self.current_token_range().end())) ++ { ++ return None; ++ } ++ if self.errors.is_empty() { ++ Some(ParseError { ++ error: ParseErrorType::CaseOutsideMatch, ++ location: header_range, ++ }) ++ } else { ++ self.standalone_case_inner_error() ++ } ++ } ++ ++ /// A malformed production does not establish a standalone case. An invalid ++ /// rule reached while parsing its pattern, guard, or body can still raise ++ /// its own diagnostic before the enclosing standalone-case rule fires. ++ fn standalone_case_inner_error(&self) -> Option { ++ self.errors ++ .first() ++ .filter(|error| { ++ matches!( ++ error.error, ++ ParseErrorType::CaseOutsideMatch ++ | ParseErrorType::ExpectedIndentedBlock { .. } ++ | ParseErrorType::InvalidMatchPatternTarget ++ | ParseErrorType::InvalidPatternTarget(_) ++ | ParseErrorType::MissingRaiseException ++ | ParseErrorType::MissingRaiseCause ++ | ParseErrorType::NamedExpressionWithoutParentheses ++ | ParseErrorType::MappingRestPatternNotLast ++ | ParseErrorType::TrailingCommaInWith ++ | ParseErrorType::MisplacedLazyImport ++ | ParseErrorType::LazyFutureImport ++ | ParseErrorType::InvalidAssignmentTarget { .. } ++ | ParseErrorType::InvalidAnnotatedAssignmentTarget ++ | ParseErrorType::InvalidAugmentedAssignmentTarget(_) ++ | ParseErrorType::InvalidDeleteTarget(_) ++ ) ++ }) ++ .cloned() ++ } ++} ++ ++/// The recovery parser permits an underscore at the root of a value or class ++/// pattern. In the grammar it commits to the wildcard instead, so `_.x` cannot ++/// complete the pattern even when the recovered AST is an attribute. ++fn pattern_has_valid_value_names(pattern: &Pattern) -> bool { ++ match pattern { ++ Pattern::MatchValue(pattern) => value_has_valid_root(&pattern.value), ++ Pattern::MatchSingleton(_) | Pattern::MatchStar(_) => true, ++ Pattern::MatchSequence(pattern) => { ++ pattern.patterns.iter().all(pattern_has_valid_value_names) ++ } ++ Pattern::MatchMapping(pattern) => { ++ pattern.patterns.iter().all(pattern_has_valid_value_names) ++ } ++ Pattern::MatchClass(pattern) => { ++ value_has_valid_root(&pattern.cls) ++ && pattern ++ .arguments ++ .patterns ++ .iter() ++ .all(pattern_has_valid_value_names) ++ && pattern ++ .arguments ++ .keywords ++ .iter() ++ .all(|keyword| pattern_has_valid_value_names(&keyword.pattern)) ++ } ++ Pattern::MatchAs(pattern) => pattern ++ .pattern ++ .as_deref() ++ .is_none_or(pattern_has_valid_value_names), ++ Pattern::MatchOr(pattern) => pattern.patterns.iter().all(pattern_has_valid_value_names), ++ } ++} ++ ++fn value_has_valid_root(mut value: &Expr) -> bool { ++ while let Expr::Attribute(attribute) = value { ++ value = &attribute.value; ++ } ++ match value { ++ Expr::Name(name) => name.id != "_", ++ _ => true, ++ } ++} diff --git a/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-unpack-diagnostics.patch b/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-unpack-diagnostics.patch new file mode 100644 index 00000000000..701751d3600 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/patches/rustpython-rc3-unpack-diagnostics.patch @@ -0,0 +1,559 @@ +--- a/vendor/rustpython-ruff_python_parser/src/error.rs ++++ b/vendor/rustpython-ruff_python_parser/src/error.rs +@@ -224,6 +224,18 @@ + InvalidYieldExpressionUsage, + /// An invalid usage of a starred expression was found. + InvalidStarredExpressionUsage, ++ /// An unpacked conditional expression is missing parentheses. ++ InvalidConditionalUnpacking { double_starred: bool }, ++ /// Only the else branch of a conditional expression is unpacked. ++ InvalidConditionalBranchUnpacking { double_starred: bool }, ++ /// Dictionary unpacking is used in a list or generator comprehension. ++ InvalidComprehensionDictUnpacking { generator: bool }, ++ /// An unpacked expression is used as a dictionary key. ++ InvalidDictKeyUnpacking { double_starred: bool }, ++ /// An unpacked expression is used as a dictionary value. ++ InvalidDictValueUnpacking { double_starred: bool }, ++ /// Dictionary unpacking is used outside a dictionary or call argument. ++ InvalidDictUnpacking, + /// A star pattern was found outside a sequence pattern. + InvalidStarPatternUsage, + /// An underscore was used as a binding target in a match pattern. +@@ -532,6 +544,49 @@ + ParseErrorType::InvalidLambdaExpressionUsage => { + f.write_str("lambda expression cannot be used here") + } ++ ParseErrorType::InvalidConditionalUnpacking { double_starred } => { ++ let kind = if *double_starred { ++ "double starred" ++ } else { ++ "starred" ++ }; ++ write!( ++ f, ++ "invalid {kind} expression. Did you forget to wrap the conditional expression in parentheses?" ++ ) ++ } ++ ParseErrorType::InvalidConditionalBranchUnpacking { double_starred } => { ++ if *double_starred { ++ f.write_str( ++ "cannot use dict unpacking on only part of a conditional expression", ++ ) ++ } else { ++ f.write_str("cannot unpack only part of a conditional expression") ++ } ++ } ++ ParseErrorType::InvalidComprehensionDictUnpacking { generator } => { ++ let kind = if *generator { ++ "generator expression" ++ } else { ++ "list comprehension" ++ }; ++ write!(f, "cannot use dict unpacking in {kind}") ++ } ++ ParseErrorType::InvalidDictKeyUnpacking { double_starred } ++ | ParseErrorType::InvalidDictValueUnpacking { double_starred } => { ++ let kind = if *double_starred { ++ "dict unpacking" ++ } else { ++ "a starred expression" ++ }; ++ let position = if matches!(self, ParseErrorType::InvalidDictKeyUnpacking { .. }) { ++ "key" ++ } else { ++ "value" ++ }; ++ write!(f, "cannot use {kind} in a dictionary {position}") ++ } ++ ParseErrorType::InvalidDictUnpacking => f.write_str("cannot use dict unpacking here"), + ParseErrorType::InvalidStarredExpressionUsage => f.write_str("invalid syntax"), + ParseErrorType::PositionalAfterKeywordArgument => { + f.write_str("positional argument follows keyword argument") +--- a/vendor/rustpython-ruff_python_parser/src/parser/expression.rs ++++ b/vendor/rustpython-ruff_python_parser/src/parser/expression.rs +@@ -687,22 +687,33 @@ + } + } + +- /// Parses an expression with a minimum precedence of bitwise `or`. +- /// +- /// This methods actually parses the expression using the `expression` rule +- /// of the [Python grammar] and then validates the parsed expression. In a +- /// sense, it matches the `bitwise_or` rule of the [Python grammar]. +- /// +- /// [Python grammar]: https://docs.python.org/3/reference/grammar.html +- fn parse_expression_with_bitwise_or_precedence(&mut self) -> ParsedExpr { +- let parsed_expr = +- self.parse_conditional_expression_without_comma_check(ExpressionContext::default()); +- ++ /// Checks the bitwise-or operand required by sequence and dictionary displays. ++ /// The caller can defer this check until a dictionary comprehension is ruled out. ++ fn validate_unpacking_precedence( ++ &mut self, ++ parsed_expr: &ParsedExpr, ++ start: TextSize, ++ double_starred: bool, ++ ) { + if parsed_expr.is_parenthesized { + // Parentheses resets the precedence, so we don't need to validate it. +- return parsed_expr; +- } +- ++ return; ++ } ++ if parsed_expr.expr.is_if_expr() { ++ self.add_error( ++ ParseErrorType::InvalidConditionalUnpacking { double_starred }, ++ TextRange::new(start, parsed_expr.end()), ++ ); ++ return; ++ } ++ ++ self.validate_bitwise_or_precedence(parsed_expr); ++ } ++ ++ fn validate_bitwise_or_precedence(&mut self, parsed_expr: &ParsedExpr) { ++ if parsed_expr.is_parenthesized { ++ return; ++ } + let expr_name = match parsed_expr.expr { + Expr::Compare(_) => "comparison", + Expr::BoolOp(_) +@@ -712,15 +723,13 @@ + }) => "boolean", + Expr::If(_) => "conditional", + Expr::Lambda(_) => "lambda", +- _ => return parsed_expr, ++ _ => return, + }; + + self.add_error( + ParseErrorType::OtherError(format!("{expr_name} expression cannot be used here")), +- &parsed_expr, ++ parsed_expr, + ); +- +- parsed_expr + } + + /// Parses a name. +@@ -1002,6 +1011,7 @@ + let keywords_snapshot = self.keyword_scratch.snapshot(); + let mut seen_keyword_argument = false; // foo = 1 + let mut seen_keyword_unpacking = false; // **foo ++ let mut first_argument = true; + // The first positional argument after a keyword argument, reported where the arguments + // end, and the number of errors before it. + let mut positional_after_keyword: Option<(ParseErrorType, usize)> = None; +@@ -1009,8 +1019,43 @@ + let has_trailing_comma = + self.parse_comma_separated_list(RecoveryContextKind::Arguments, |parser| { + let argument_start = parser.node_start(); ++ let is_first_argument = first_argument; ++ first_argument = false; + if parser.eat(TokenKind::DoubleStar) { + let mut value = parser.parse_conditional_expression_or_higher(); ++ if is_first_argument ++ && matches!(context, ArgumentsContext::Call) ++ && matches!( ++ parser.current_token_kind(), ++ TokenKind::Async | TokenKind::For ++ ) ++ { ++ let for_range = parser.current_token_range(); ++ let for_kind = parser.current_token_kind(); ++ let errors_start = parser.errors.len(); ++ let generators = parser.parse_generators(); ++ let (error, location) = if parser ++ .has_valid_first_comprehension_clause(&generators, errors_start) ++ { ++ ( ++ ParseErrorType::InvalidComprehensionDictUnpacking { ++ generator: true, ++ }, ++ TextRange::new(argument_start, value.end()), ++ ) ++ } else { ++ ( ++ ParseErrorType::ExpectedToken { ++ expected: TokenKind::Comma, ++ found: for_kind, ++ }, ++ for_range, ++ ) ++ }; ++ parser ++ .errors ++ .insert(errors_start, crate::ParseError { error, location }); ++ } + + if parser.eat(TokenKind::Equal) { + // test_err args_keyword_unpacking_assignment +@@ -2604,6 +2649,102 @@ + } + } + ++ /// An invalid comprehension only needs one complete target/iterable clause. ++ /// Later incomplete filters or clauses do not undo that first successful clause. ++ fn has_valid_first_comprehension_clause( ++ &self, ++ generators: &[ast::Comprehension], ++ errors_start: usize, ++ ) -> bool { ++ generators.first().is_some_and(|clause| { ++ self.errors[errors_start..] ++ .iter() ++ .all(|error| error.location.start() > clause.iter.end()) ++ }) ++ } ++ ++ /// Recovers a dictionary unpack in a sequence, using the same parse for diagnostics. ++ fn parse_invalid_sequence_dict_unpacking(&mut self, start: TextSize, generator: bool) -> Expr { ++ let stars = self.current_token_range(); ++ let errors_start = self.errors.len(); ++ self.bump(TokenKind::DoubleStar); ++ let has_bitwise_operand = self.at_ts(EXPR_SET) ++ && !matches!( ++ self.current_token_kind(), ++ TokenKind::Lambda ++ | TokenKind::Not ++ | TokenKind::Yield ++ | TokenKind::Star ++ | TokenKind::DoubleStar ++ ); ++ let value = self.parse_conditional_expression_or_higher(); ++ let value_end = value.end(); ++ let operand_is_valid = self.errors.len() == errors_start; ++ let is_group = generator && self.at(TokenKind::Rpar); ++ let clause_errors_start = self.errors.len(); ++ let (expression, valid_comprehension) = ++ if matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For) { ++ if generator { ++ let expression = ++ self.parse_generator_expression(value.expr, start, Parenthesized::Yes); ++ let valid = self.has_valid_first_comprehension_clause( ++ &expression.generators, ++ clause_errors_start, ++ ); ++ (Expr::Generator(expression), valid) ++ } else { ++ let expression = self.parse_list_comprehension_expression(value.expr, start); ++ let valid = self.has_valid_first_comprehension_clause( ++ &expression.generators, ++ clause_errors_start, ++ ); ++ (Expr::ListComp(expression), valid) ++ } ++ } else if !generator { ++ ( ++ Expr::List(self.parse_list_expression(value.expr, start)), ++ false, ++ ) ++ } else if self.at(TokenKind::Comma) { ++ let tuple = ++ self.parse_tuple_expression(value.expr, start, Parenthesized::Yes, |p| { ++ p.parse_named_expression_or_higher(ExpressionContext::starred_bitwise_or()) ++ }); ++ (Expr::Tuple(tuple), false) ++ } else { ++ self.expect(TokenKind::Rpar); ++ (value.expr, false) ++ }; ++ let invalid_target = self.errors[clause_errors_start..] ++ .iter() ++ .any(|error| matches!(error.error, ParseErrorType::InvalidAssignmentTarget { .. })); ++ let diagnostic = if invalid_target { ++ None ++ } else if operand_is_valid && valid_comprehension { ++ Some(( ++ ParseErrorType::InvalidComprehensionDictUnpacking { generator }, ++ TextRange::new(stars.start(), value_end), ++ )) ++ } else if operand_is_valid && has_bitwise_operand { ++ Some((ParseErrorType::InvalidDictUnpacking, stars)) ++ } else if operand_is_valid && is_group { ++ Some(( ++ ParseErrorType::OtherError("cannot use double starred expression here".to_string()), ++ stars, ++ )) ++ } else if !operand_is_valid || !has_bitwise_operand { ++ Some((ParseErrorType::ExpectedExpression, stars)) ++ } else { ++ None ++ }; ++ if let Some((error, location)) = diagnostic { ++ // The invalid production starts before any clause-recovery errors. ++ self.errors ++ .insert(errors_start, crate::ParseError { error, location }); ++ } ++ expression ++ } ++ + /// Parses a list or a list comprehension expression. + /// + /// # Panics +@@ -2636,6 +2777,9 @@ + } + + // Parse the first element with a more general rule and limit it later. ++ if self.at(TokenKind::DoubleStar) { ++ return self.parse_invalid_sequence_dict_unpacking(start, false); ++ } + let first_element = self.parse_named_expression_or_higher( + ExpressionContext::starred_bitwise_or().with_for_excluded(), + ); +@@ -2732,7 +2876,8 @@ + if self.eat(TokenKind::DoubleStar) { + // Handle dictionary unpacking. Here, the grammar is `'**' bitwise_or` + // which requires limiting the expression. +- let value = self.parse_expression_with_bitwise_or_precedence(); ++ let value = ++ self.parse_conditional_expression_without_comma_check(ExpressionContext::default()); + let unpack_range = TextRange::new(after_brace, value.range().end()); + + if matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For) { +@@ -2743,13 +2888,23 @@ + unpack_range, + ); + +- return Expr::DictComp( +- self.parse_dictionary_comprehension_expression(None, value.expr, start), ++ return Expr::DictComp(self.parse_dictionary_comprehension_expression( ++ None, ++ value.expr, ++ start, ++ Some(TextRange::at(after_brace, TextSize::new(2))), ++ )); ++ } ++ ++ self.validate_unpacking_precedence(&value, after_brace, true); ++ ++ if self.at(TokenKind::Colon) { ++ self.add_error( ++ ParseErrorType::InvalidDictKeyUnpacking { ++ double_starred: true, ++ }, ++ unpack_range, + ); +- } +- +- if self.at(TokenKind::Colon) { +- self.add_error(ParseErrorType::InvalidStarredExpressionUsage, unpack_range); + + self.bump(TokenKind::Colon); + let dict_value = self.parse_conditional_expression_or_higher(); +@@ -2759,6 +2914,7 @@ + Some(value.expr), + dict_value.expr, + start, ++ None, + )); + } + +@@ -2818,7 +2974,9 @@ + if !key_or_element.is_parenthesized { + match key_or_element.expr { + Expr::Starred(_) => self.add_error( +- ParseErrorType::InvalidStarredExpressionUsage, ++ ParseErrorType::InvalidDictKeyUnpacking { ++ double_starred: false, ++ }, + &key_or_element.expr, + ), + Expr::Named(_) => self.add_error( +@@ -2831,24 +2989,14 @@ + + let colon_range = self.current_token_range(); + self.bump(TokenKind::Colon); +- let value = if self.at(TokenKind::DoubleStar) { +- let unpack_start = self.node_start(); +- self.bump(TokenKind::DoubleStar); +- let value = self.parse_expression_with_bitwise_or_precedence(); +- self.add_error( +- ParseErrorType::InvalidStarredExpressionUsage, +- TextRange::new(unpack_start, value.range().end()), +- ); +- value +- } else { +- self.parse_dictionary_value(colon_range) +- }; ++ let value = self.parse_dictionary_value(colon_range); + + if matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For) { + Expr::DictComp(self.parse_dictionary_comprehension_expression( + Some(key_or_element.expr), + value.expr, + start, ++ None, + )) + } else { + Expr::Dict(self.parse_dictionary_expression( +@@ -2893,30 +3041,16 @@ + .into(); + } + +- if self.at(TokenKind::DoubleStar) { +- let double_star_range = self.current_token_range(); +- let checkpoint = self.checkpoint(); +- self.bump(TokenKind::DoubleStar); +- if self.at_expr() { +- let mut parsed_expr = self.parse_conditional_expression_or_higher(); +- if self.eat(TokenKind::Rpar) { +- // test_err parenthesized_double_starred_expr +- // (**x) +- self.add_error( +- ParseErrorType::OtherError( +- "cannot use double starred expression here".to_string(), +- ), +- double_star_range, +- ); +- parsed_expr.is_parenthesized = true; +- return parsed_expr; +- } +- } +- self.rewind(checkpoint); +- } +- + // Use the more general rule of the three to parse the first element + // and limit it later. ++ if self.at(TokenKind::DoubleStar) { ++ // test_err parenthesized_double_starred_expr ++ // (**x) ++ return ParsedExpr { ++ expr: self.parse_invalid_sequence_dict_unpacking(start, true), ++ is_parenthesized: true, ++ }; ++ } + let mut parsed_expr = self.parse_named_expression_or_higher( + ExpressionContext::yield_or_starred_bitwise_or().with_for_excluded(), + ); +@@ -3142,17 +3276,27 @@ + + self.parse_comma_separated_list(RecoveryContextKind::DictElements, |parser| { + if parser.eat(TokenKind::DoubleStar) { +- // Handle dictionary unpacking. Here, the grammar is `'**' bitwise_or` +- // which requires limiting the expression. ++ // Subsequent dictionary spreads retain the ordinary display grammar. ++ let value = parser ++ .parse_conditional_expression_without_comma_check(ExpressionContext::default()); ++ parser.validate_bitwise_or_precedence(&value); + items.push(ast::DictItem { + key: None, +- value: parser.parse_expression_with_bitwise_or_precedence().expr, ++ value: value.expr, + }); + } else { + // A missing comma is not reported in a key after the first item. +- let key = parser +- .parse_conditional_expression_without_comma_check(ExpressionContext::default()) +- .expr; ++ let key = parser.parse_conditional_expression_without_comma_check( ++ ExpressionContext::starred_bitwise_or(), ++ ); ++ if key.is_unparenthesized_starred_expr() { ++ parser.add_error( ++ ParseErrorType::InvalidDictKeyUnpacking { ++ double_starred: false, ++ }, ++ &key, ++ ); ++ } + if !parser.at(TokenKind::Colon) { + // test_err dict_missing_colon_after_key + // {a: 1, b} +@@ -3170,7 +3314,7 @@ + parser.expect(TokenKind::Colon); + + items.push(ast::DictItem { +- key: Some(key), ++ key: Some(key.expr), + value: parser.parse_dictionary_value(colon_range).expr, + }); + } +@@ -3200,18 +3344,23 @@ + ), + colon_range, + ); +- } else if self.at(TokenKind::Star) { ++ } else if matches!( ++ self.current_token_kind(), ++ TokenKind::Star | TokenKind::DoubleStar ++ ) { + // test_err dict_starred_value + // {a: *b} + // {a: 1, b: *c} +- let star_start = self.node_start(); +- self.bump(TokenKind::Star); +- let value = self.parse_expression_with_bitwise_or_precedence(); ++ let start = self.node_start(); ++ let double_starred = self.at(TokenKind::DoubleStar); ++ self.bump_any(); ++ let value = self.parse_binary_expression_or_higher( ++ OperatorPrecedence::ComparisonsMembershipIdentity, ++ ExpressionContext::default(), ++ ); + self.add_error( +- ParseErrorType::OtherError( +- "cannot use a starred expression in a dictionary value".to_string(), +- ), +- TextRange::new(star_start, value.end()), ++ ParseErrorType::InvalidDictValueUnpacking { double_starred }, ++ TextRange::new(start, value.end()), + ); + return value; + } +@@ -3376,9 +3525,15 @@ + key: Option, + value: Expr, + start: TextSize, ++ unpacking: Option, + ) -> ast::ExprDictComp { + let generators = self.parse_generators(); + ++ if self.at(TokenKind::Colon) ++ && let Some(stars) = unpacking ++ { ++ self.add_error(ParseErrorType::InvalidDictUnpacking, stars); ++ } + self.expect(TokenKind::Rbrace); + + ast::ExprDictComp { +@@ -3466,7 +3621,10 @@ + context.disallow_starred_expressions(), + ), + StarredExpressionPrecedence::BitwiseOr => { +- self.parse_expression_with_bitwise_or_precedence() ++ let value = self ++ .parse_conditional_expression_without_comma_check(ExpressionContext::default()); ++ self.validate_unpacking_precedence(&value, start, false); ++ value + } + }; + +@@ -3711,13 +3869,20 @@ + + let test = self.parse_simple_expression(ExpressionContext::default()); + +- if self.expect(TokenKind::Else) +- && (!self.at_expr() +- || matches!( +- self.current_token_kind(), +- TokenKind::Star | TokenKind::DoubleStar | TokenKind::Yield +- )) ++ let has_else = self.expect(TokenKind::Else); ++ if has_else ++ && matches!( ++ self.current_token_kind(), ++ TokenKind::Star | TokenKind::DoubleStar ++ ) + { ++ self.add_error( ++ ParseErrorType::InvalidConditionalBranchUnpacking { ++ double_starred: self.at(TokenKind::DoubleStar), ++ }, ++ self.current_token_range(), ++ ); ++ } else if has_else && (!self.at_expr() || self.at(TokenKind::Yield)) { + // test_err if_expr_statement_after_else + // x = 1 if 1 else pass + // x = 1 if 1 else diff --git a/vendor/rustpython-ruff_python_parser/src/error.rs b/vendor/rustpython-ruff_python_parser/src/error.rs new file mode 100644 index 00000000000..09957618f67 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/error.rs @@ -0,0 +1,1800 @@ +use std::fmt::{self, Display}; + +use ruff_python_ast::token::TokenKind; +use ruff_python_ast::{ConstantValue, Expr, PythonVersion}; +use ruff_text_size::{Ranged, TextRange, TextSize}; + +use crate::string::InterpolatedStringKind; + +/// Represents represent errors that occur during parsing and are +/// returned by the `parse_*` functions. +#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)] +pub struct ParseError { + pub error: ParseErrorType, + pub location: TextRange, +} + +impl std::ops::Deref for ParseError { + type Target = ParseErrorType; + + fn deref(&self) -> &Self::Target { + &self.error + } +} + +impl std::error::Error for ParseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.error) + } +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{} at byte range {:?}", self.error, self.location) + } +} + +impl From for ParseError { + fn from(error: LexicalError) -> Self { + ParseError { + location: error.location(), + error: ParseErrorType::Lexical(error.into_error()), + } + } +} + +impl Ranged for ParseError { + fn range(&self) -> TextRange { + self.location + } +} + +impl ParseError { + pub fn error(self) -> ParseErrorType { + self.error + } +} + +/// The reason an escape sequence in a string literal cannot be decoded. +#[derive(Debug, Clone, Copy, PartialEq, Eq, get_size2::GetSize)] +pub enum UnicodeEscapeErrorKind { + /// `\x` is not followed by two hexadecimal digits. + TruncatedHexByte, + /// `\u` is not followed by four hexadecimal digits. + TruncatedShortUnicode, + /// `\U` is not followed by eight hexadecimal digits. + TruncatedLongUnicode, + /// `\U` names a code point above `U+10FFFF`. + IllegalCharacter, + /// `\N` is not followed by a non-empty name in braces. + MalformedName, + /// `\N{...}` names no Unicode character. + UnknownName, +} + +impl Display for UnicodeEscapeErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::TruncatedHexByte => "truncated \\xXX escape", + Self::TruncatedShortUnicode => "truncated \\uXXXX escape", + Self::TruncatedLongUnicode => "truncated \\UXXXXXXXX escape", + Self::IllegalCharacter => "illegal Unicode character", + Self::MalformedName => "malformed \\N character escape", + Self::UnknownName => "unknown Unicode character name", + }) + } +} + +/// Represents the different types of errors that can occur during parsing of an f-string or t-string. +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] +pub enum InterpolatedStringErrorType { + /// Expected a right brace after an opened left brace. + UnclosedLbrace, + /// Expected a right brace for a replacement field opened on an earlier line. + UnclosedLbraceOnLine { opening_line: u32 }, + /// An invalid conversion flag was encountered. + InvalidConversionFlag, + /// A single right brace was encountered. + SingleRbrace, + /// Unterminated string, detected at the given one-based line. + UnterminatedString { detected_line: u32 }, + /// Unterminated triple-quoted string, detected at the given one-based line. + UnterminatedTripleQuotedString { detected_line: u32 }, + /// A lambda expression without parentheses was encountered. + LambdaWithoutParentheses, + /// Conversion flag does not immediately follow exclamation. + ConversionFlagNotImmediatelyAfterExclamation, + /// Newline inside of a format spec for a single quoted f- or t-string. + NewlineInFormatSpec, + /// A replacement field has no expression before the given separator. + ExpressionRequiredBefore(char), + /// A replacement field does not start with an expression. + ExpectedExpressionAfterLbrace, + /// The expression of a replacement field is not followed by `=`, `!`, `:` or `}`. + ExpectedSeparatorAfterExpression, + /// The `=` of a replacement field is not followed by `!`, `:` or `}`. + ExpectedSeparatorAfterDebug, + /// The conversion of a replacement field is not followed by `:` or `}`. + ExpectedSeparatorAfterConversion, + /// The format spec of a replacement field is not followed by `}`. + UnclosedFormatSpec, + /// A `!` is directly followed by `:` or `}`. + MissingConversionFlag, + /// A closing bracket closes the brace that opens a replacement field. + UnmatchedBracket(char), +} + +impl std::fmt::Display for InterpolatedStringErrorType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::UnclosedLbrace => f.write_str("expecting '}'"), + Self::UnclosedLbraceOnLine { opening_line } => { + write!(f, "expecting '}}' to close '{{' on line {opening_line}") + } + Self::InvalidConversionFlag => write!(f, "invalid conversion character"), + Self::SingleRbrace => f.write_str("single '}' is not allowed"), + Self::UnterminatedString { .. } => write!(f, "unterminated string"), + Self::UnterminatedTripleQuotedString { .. } => { + write!(f, "unterminated triple-quoted string") + } + Self::LambdaWithoutParentheses => { + write!(f, "lambda expressions are not allowed without parentheses") + } + Self::ConversionFlagNotImmediatelyAfterExclamation => write!( + f, + "conversion type must come right after the exclamation mark" + ), + Self::NewlineInFormatSpec => { + write!( + f, + "newlines are not allowed in format specifiers when using single quotes" + ) + } + Self::ExpressionRequiredBefore(separator) => { + write!(f, "valid expression required before '{separator}'") + } + Self::ExpectedExpressionAfterLbrace => { + f.write_str("expecting a valid expression after '{'") + } + Self::ExpectedSeparatorAfterExpression => { + f.write_str("expecting '=', or '!', or ':', or '}'") + } + Self::ExpectedSeparatorAfterDebug => f.write_str("expecting '!', or ':', or '}'"), + Self::ExpectedSeparatorAfterConversion => f.write_str("expecting ':' or '}'"), + Self::UnclosedFormatSpec => f.write_str("expecting '}', or format specs"), + Self::MissingConversionFlag => f.write_str("missing conversion character"), + Self::UnmatchedBracket(closing) => write!(f, "unmatched '{closing}'"), + } + } +} + +fn write_interpolated_string_error( + f: &mut std::fmt::Formatter, + kind: InterpolatedStringKind, + error: &InterpolatedStringErrorType, +) -> std::fmt::Result { + match error { + InterpolatedStringErrorType::UnterminatedString { detected_line } => { + write!( + f, + "unterminated {kind} literal (detected at line {detected_line})" + ) + } + InterpolatedStringErrorType::UnterminatedTripleQuotedString { detected_line } => { + write!( + f, + "unterminated triple-quoted {kind} literal (detected at line {detected_line})" + ) + } + InterpolatedStringErrorType::NewlineInFormatSpec => write!( + f, + "{kind}: newlines are not allowed in format specifiers for single quoted {kind}s" + ), + _ => write!(f, "{kind}: {error}"), + } +} + +/// Represents the different types of errors that can occur during parsing. +#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)] +pub enum ParseErrorType { + /// An unexpected error occurred. + OtherError(String), + + /// A missing-comma hint whose concatenated literal is displayed on its final line. + /// The original expression range controls error precedence; `diagnostic_start` can be + /// after the displayed range's end, which a `TextRange` cannot represent. + MissingCommaAfterConcatenatedLiteral { + expression_range: TextRange, + diagnostic_start: TextSize, + }, + + /// An error specific to stringified annotations occurred. + StringAnnotationError(&'static str), + + /// A `raise from` statement has no exception expression. + MissingRaiseException, + /// A `raise` statement ends immediately after `from`. + MissingRaiseCause, + /// An unparenthesized `with` item list ends with a comma. + TrailingCommaInWith, + /// `lazy` follows the module name instead of preceding `from`. + MisplacedLazyImport, + /// An absolute future import is marked lazy. + LazyFutureImport, + + /// An empty slice was found during parsing, e.g `data[]`. + EmptySlice, + /// An empty global names list was found during parsing. + EmptyGlobalNames, + /// An empty nonlocal names list was found during parsing. + EmptyNonlocalNames, + /// An empty delete targets list was found during parsing. + EmptyDeleteTargets, + /// An empty import names list was found during parsing. + EmptyImportNames, + /// An empty type parameter list was found during parsing. + EmptyTypeParams, + + /// An unparenthesized named expression was found where it is not allowed. + UnparenthesizedNamedExpression, + /// An unparenthesized tuple expression was found where it is not allowed. + UnparenthesizedTupleExpression, + /// An unparenthesized generator expression was found where it is not allowed. + UnparenthesizedGeneratorExpression, + + /// An invalid usage of a lambda expression was found. + InvalidLambdaExpressionUsage, + /// An invalid usage of a yield expression was found. + InvalidYieldExpressionUsage, + /// An invalid usage of a starred expression was found. + InvalidStarredExpressionUsage, + /// An unpacked conditional expression is missing parentheses. + InvalidConditionalUnpacking { double_starred: bool }, + /// Only the else branch of a conditional expression is unpacked. + InvalidConditionalBranchUnpacking { double_starred: bool }, + /// Dictionary unpacking is used in a list or generator comprehension. + InvalidComprehensionDictUnpacking { generator: bool }, + /// An unpacked expression is used as a dictionary key. + InvalidDictKeyUnpacking { double_starred: bool }, + /// An unpacked expression is used as a dictionary value. + InvalidDictValueUnpacking { double_starred: bool }, + /// Dictionary unpacking is used outside a dictionary or call argument. + InvalidDictUnpacking, + /// A star pattern was found outside a sequence pattern. + InvalidStarPatternUsage, + /// An underscore was used as a binding target in a match pattern. + InvalidMatchPatternTarget, + /// A complete case clause was found outside a match statement. + CaseOutsideMatch, + + /// A parameter was found after a vararg. + ParamAfterVarKeywordParam, + /// A non-default parameter follows a default parameter. + NonDefaultParamAfterDefaultParam, + /// A default value was found for a `*` or `**` parameter. + VarParameterWithDefault, + /// A default value was found for a `**` parameter. + VarKeywordParameterWithDefault, + /// A dictionary key after the first item is not followed by a `:`. The error range is the + /// last character of the key. + ExpectedColonAfterDictionaryKey, + + /// An invalid expression was found in the assignment target. `maybe_comparison` is set when + /// the `=` after it may have been meant as `==`. + InvalidAssignmentTarget { + kind: ExpressionKind, + maybe_comparison: bool, + }, + /// A named expression is used in an assertion without its required parentheses. + NamedExpressionWithoutParentheses, + /// A mapping pattern contains an entry after its double-star rest pattern. + MappingRestPatternNotLast, + /// A name is assigned to where `==` or `:=` may have been meant. + AssignmentInsteadOfComparison, + /// A `yield` expression is assigned to. + AssignmentToYield, + /// An invalid expression was found in the named assignment target. + InvalidNamedAssignmentTarget(ExpressionKind), + /// An invalid expression was found in the annotated assignment target. + InvalidAnnotatedAssignmentTarget, + /// An invalid expression was found in the augmented assignment target. + InvalidAugmentedAssignmentTarget(ExpressionKind), + /// An invalid expression was found in the delete target. + InvalidDeleteTarget(ExpressionKind), + /// An invalid expression was found after `as` in an import. + InvalidImportTarget(ExpressionKind), + /// An invalid expression was found after `as` in a pattern. + InvalidPatternTarget(ExpressionKind), + /// An invalid expression was found after `as` in an `except` or `except*` clause. + InvalidExceptTarget { kind: ExpressionKind, star: bool }, + + /// A positional argument was found after a keyword argument. + PositionalAfterKeywordArgument, + /// A positional argument was found after a keyword argument unpacking. + PositionalAfterKeywordUnpacking, + /// An iterable argument unpacking was found after keyword argument unpacking. + InvalidArgumentUnpackingOrder, + /// An invalid usage of iterable unpacking in a comprehension was found. + IterableUnpackingInComprehension, + + /// Multiple simple statements were found in the same line without a `;` separating them. + SimpleStatementsOnSameLine, + /// A simple statement and a compound statement was found in the same line. + SimpleAndCompoundStatementOnSameLine, + + /// Expected one or more keyword parameter after `*` separator. + ExpectedKeywordParam, + /// Expected a real number for a complex literal pattern. + ExpectedRealNumber, + /// Expected an imaginary number for a complex literal pattern. + ExpectedImaginaryNumber, + /// Expected an expression at the current parser location. + ExpectedExpression, + /// Expected an identifier at the current parser location. + ExpectedIdentifier, + /// The parser expected a specific token that was not found. + ExpectedToken { + expected: TokenKind, + found: TokenKind, + }, + + /// A compound statement header is not followed by an indented block. `line` is the line of + /// the header's keyword. + ExpectedIndentedBlock { clause: BlockClause, line: u32 }, + /// An unexpected indentation was found during parsing. + UnexpectedIndentation, + /// The statement being parsed cannot be `async`. + UnexpectedTokenAfterAsync(TokenKind), + /// Ipython escape command was found + UnexpectedIpythonEscapeCommand, + /// An unexpected token was found at the end of an expression parsing + UnexpectedExpressionToken, + + /// An f-string error containing the [`InterpolatedStringErrorType`]. + FStringError(InterpolatedStringErrorType), + /// A t-string error containing the [`InterpolatedStringErrorType`]. + TStringError(InterpolatedStringErrorType), + /// Parser encountered an error during lexing. + Lexical(LexicalErrorType), +} + +/// The kind of an expression, as syntax errors name it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum ExpressionKind { + Attribute, + Subscript, + Starred, + Name, + List, + Tuple, + Lambda, + FunctionCall, + Expression, + GeneratorExpression, + YieldExpression, + AwaitExpression, + ListComprehension, + SetComprehension, + DictComprehension, + DictLiteral, + SetDisplay, + FStringExpression, + TStringExpression, + None, + False, + True, + Ellipsis, + Literal, + Comparison, + ConditionalExpression, + NamedExpression, +} + +impl ExpressionKind { + pub fn of(expr: &Expr) -> Self { + match expr { + Expr::Attribute(_) => Self::Attribute, + Expr::Subscript(_) => Self::Subscript, + Expr::Starred(_) => Self::Starred, + Expr::Name(_) => Self::Name, + Expr::List(_) => Self::List, + Expr::Tuple(_) => Self::Tuple, + Expr::Lambda(_) => Self::Lambda, + Expr::Call(_) => Self::FunctionCall, + Expr::BoolOp(_) + | Expr::BinOp(_) + | Expr::UnaryOp(_) + | Expr::Slice(_) + | Expr::IpyEscapeCommand(_) => Self::Expression, + Expr::Generator(_) => Self::GeneratorExpression, + Expr::Yield(_) | Expr::YieldFrom(_) => Self::YieldExpression, + Expr::Await(_) => Self::AwaitExpression, + Expr::ListComp(_) => Self::ListComprehension, + Expr::SetComp(_) => Self::SetComprehension, + Expr::DictComp(_) => Self::DictComprehension, + Expr::Dict(_) => Self::DictLiteral, + Expr::Set(_) => Self::SetDisplay, + Expr::FString(_) => Self::FStringExpression, + Expr::TString(_) => Self::TStringExpression, + Expr::NoneLiteral(_) => Self::None, + Expr::BooleanLiteral(literal) if literal.value => Self::True, + Expr::BooleanLiteral(_) => Self::False, + Expr::EllipsisLiteral(_) => Self::Ellipsis, + Expr::StringLiteral(_) | Expr::BytesLiteral(_) | Expr::NumberLiteral(_) => { + Self::Literal + } + Expr::Compare(_) => Self::Comparison, + Expr::If(_) => Self::ConditionalExpression, + Expr::Named(_) => Self::NamedExpression, + Expr::Constant(constant) => match constant.value { + ConstantValue::None => Self::None, + ConstantValue::Boolean(true) => Self::True, + ConstantValue::Boolean(false) => Self::False, + ConstantValue::Ellipsis => Self::Ellipsis, + _ => Self::Literal, + }, + } + } +} + +impl std::fmt::Display for ExpressionKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str(match self { + Self::Attribute => "attribute", + Self::Subscript => "subscript", + Self::Starred => "starred", + Self::Name => "name", + Self::List => "list", + Self::Tuple => "tuple", + Self::Lambda => "lambda", + Self::FunctionCall => "function call", + Self::Expression => "expression", + Self::GeneratorExpression => "generator expression", + Self::YieldExpression => "yield expression", + Self::AwaitExpression => "await expression", + Self::ListComprehension => "list comprehension", + Self::SetComprehension => "set comprehension", + Self::DictComprehension => "dict comprehension", + Self::DictLiteral => "dict literal", + Self::SetDisplay => "set display", + Self::FStringExpression => "f-string expression", + Self::TStringExpression => "t-string expression", + Self::None => "None", + Self::False => "False", + Self::True => "True", + Self::Ellipsis => "ellipsis", + Self::Literal => "literal", + Self::Comparison => "comparison", + Self::ConditionalExpression => "conditional expression", + Self::NamedExpression => "named expression", + }) + } +} + +/// The compound statement clause whose header precedes an indented block. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum BlockClause { + If, + Elif, + Else, + For, + With, + While, + Try, + Except, + ExceptStar, + Finally, + Match, + Case, + /// A standalone case uses the generic invalid-block diagnostic. + StandaloneCase, + Class, + FunctionDef, +} + +impl std::fmt::Display for BlockClause { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str(match self { + Self::If => "'if' statement", + Self::Elif => "'elif' statement", + Self::Else => "'else' statement", + Self::For => "'for' statement", + Self::With => "'with' statement", + Self::While => "'while' statement", + Self::Try => "'try' statement", + Self::Except => "'except' statement", + Self::ExceptStar => "'except*' statement", + Self::Finally => "'finally' statement", + Self::Match => "'match' statement", + Self::Case | Self::StandaloneCase => "'case' statement", + Self::Class => "class definition", + Self::FunctionDef => "function definition", + }) + } +} + +impl ParseErrorType { + pub(crate) fn from_interpolated_string_error( + error: InterpolatedStringErrorType, + string_kind: InterpolatedStringKind, + ) -> Self { + match string_kind { + InterpolatedStringKind::FString => Self::FStringError(error), + InterpolatedStringKind::TString => Self::TStringError(error), + } + } +} + +impl std::error::Error for ParseErrorType {} + +impl std::fmt::Display for ParseErrorType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ParseErrorType::OtherError(msg) => f.write_str(msg), + ParseErrorType::MissingCommaAfterConcatenatedLiteral { .. } => { + f.write_str("invalid syntax. Perhaps you forgot a comma?") + } + ParseErrorType::ExpectedIndentedBlock { + clause: BlockClause::StandaloneCase, + .. + } => f.write_str("expected an indented block"), + ParseErrorType::ExpectedIndentedBlock { clause, line } => { + write!( + f, + "expected an indented block after {clause} on line {line}" + ) + } + ParseErrorType::StringAnnotationError(msg) => f.write_str(msg), + ParseErrorType::ExpectedToken { found, expected } => match (*expected, *found) { + (TokenKind::Colon, TokenKind::Newline) => f.write_str("expected ':'"), + (TokenKind::Lpar, _) => f.write_str("expected '('"), + (TokenKind::Else, found) if found != TokenKind::Colon => { + f.write_str("expected 'else' after 'if' expression") + } + _ => f.write_str("invalid syntax"), + }, + ParseErrorType::Lexical(lex_error) => write!(f, "{lex_error}"), + ParseErrorType::SimpleStatementsOnSameLine => f.write_str("invalid syntax"), + ParseErrorType::SimpleAndCompoundStatementOnSameLine => f.write_str("invalid syntax"), + ParseErrorType::UnexpectedTokenAfterAsync(kind) => { + write!( + f, + "expected `def`, `with` or `for` to follow `async`, found {kind}", + ) + } + ParseErrorType::InvalidArgumentUnpackingOrder => { + f.write_str("iterable argument unpacking follows keyword argument unpacking") + } + ParseErrorType::IterableUnpackingInComprehension => { + f.write_str("iterable unpacking cannot be used in a comprehension") + } + ParseErrorType::UnparenthesizedNamedExpression => { + f.write_str("unparenthesized named expression cannot be used here") + } + ParseErrorType::UnparenthesizedTupleExpression => { + f.write_str("unparenthesized tuple expression cannot be used here") + } + ParseErrorType::UnparenthesizedGeneratorExpression => { + f.write_str("Generator expression must be parenthesized") + } + ParseErrorType::InvalidYieldExpressionUsage => { + f.write_str("yield expression cannot be used here") + } + ParseErrorType::InvalidLambdaExpressionUsage => { + f.write_str("lambda expression cannot be used here") + } + ParseErrorType::InvalidConditionalUnpacking { double_starred } => { + let kind = if *double_starred { + "double starred" + } else { + "starred" + }; + write!( + f, + "invalid {kind} expression. Did you forget to wrap the conditional expression in parentheses?" + ) + } + ParseErrorType::InvalidConditionalBranchUnpacking { double_starred } => { + if *double_starred { + f.write_str( + "cannot use dict unpacking on only part of a conditional expression", + ) + } else { + f.write_str("cannot unpack only part of a conditional expression") + } + } + ParseErrorType::InvalidComprehensionDictUnpacking { generator } => { + let kind = if *generator { + "generator expression" + } else { + "list comprehension" + }; + write!(f, "cannot use dict unpacking in {kind}") + } + ParseErrorType::InvalidDictKeyUnpacking { double_starred } + | ParseErrorType::InvalidDictValueUnpacking { double_starred } => { + let kind = if *double_starred { + "dict unpacking" + } else { + "a starred expression" + }; + let position = if matches!(self, ParseErrorType::InvalidDictKeyUnpacking { .. }) { + "key" + } else { + "value" + }; + write!(f, "cannot use {kind} in a dictionary {position}") + } + ParseErrorType::InvalidDictUnpacking => f.write_str("cannot use dict unpacking here"), + ParseErrorType::InvalidStarredExpressionUsage => f.write_str("invalid syntax"), + ParseErrorType::PositionalAfterKeywordArgument => { + f.write_str("positional argument follows keyword argument") + } + ParseErrorType::PositionalAfterKeywordUnpacking => { + f.write_str("positional argument follows keyword argument unpacking") + } + ParseErrorType::MissingRaiseException => { + f.write_str("did you forget an expression between 'raise' and 'from'?") + } + ParseErrorType::MissingRaiseCause => { + f.write_str("did you forget an expression after 'from'?") + } + ParseErrorType::TrailingCommaInWith => { + f.write_str("the last 'with' item has a trailing comma") + } + ParseErrorType::MisplacedLazyImport => { + f.write_str("use 'lazy from ... ' instead of 'from ... lazy import'") + } + ParseErrorType::LazyFutureImport => { + f.write_str("lazy from __future__ import is not allowed") + } + ParseErrorType::EmptySlice => f.write_str("expected index or slice expression"), + ParseErrorType::EmptyGlobalNames => { + f.write_str("global statement must have at least one name") + } + ParseErrorType::EmptyNonlocalNames => { + f.write_str("nonlocal statement must have at least one name") + } + ParseErrorType::EmptyDeleteTargets => { + f.write_str("delete statement must have at least one target") + } + ParseErrorType::EmptyImportNames => { + f.write_str("Expected one or more names after 'import'") + } + ParseErrorType::EmptyTypeParams => f.write_str("Type parameter list cannot be empty"), + ParseErrorType::ParamAfterVarKeywordParam => { + f.write_str(if cfg!(feature = "python315-diagnostics") { + "parameters cannot follow var-keyword parameter" + } else { + "arguments cannot follow var-keyword argument" + }) + } + ParseErrorType::NonDefaultParamAfterDefaultParam => { + f.write_str("parameter without a default follows parameter with a default") + } + ParseErrorType::ExpectedKeywordParam => { + f.write_str(if cfg!(feature = "python315-diagnostics") { + "named parameters must follow bare *" + } else { + "named arguments must follow bare *" + }) + } + ParseErrorType::VarKeywordParameterWithDefault => { + f.write_str(if cfg!(feature = "python315-diagnostics") { + "var-keyword parameter cannot have default value" + } else { + "var-keyword argument cannot have default value" + }) + } + ParseErrorType::ExpectedColonAfterDictionaryKey => { + f.write_str("':' expected after dictionary key") + } + ParseErrorType::VarParameterWithDefault => { + f.write_str(if cfg!(feature = "python315-diagnostics") { + "var-positional parameter cannot have default value" + } else { + "var-positional argument cannot have default value" + }) + } + ParseErrorType::InvalidStarPatternUsage => { + f.write_str("cannot use starred expression here") + } + ParseErrorType::InvalidMatchPatternTarget => f.write_str("cannot use '_' as a target"), + ParseErrorType::CaseOutsideMatch => { + f.write_str("case statement must be inside match statement") + } + ParseErrorType::ExpectedRealNumber => { + f.write_str("expected a real number in complex literal pattern") + } + ParseErrorType::ExpectedImaginaryNumber => { + f.write_str("expected an imaginary number in complex literal pattern") + } + ParseErrorType::ExpectedExpression | ParseErrorType::ExpectedIdentifier => { + f.write_str("invalid syntax") + } + ParseErrorType::UnexpectedIndentation => f.write_str("unexpected indent"), + ParseErrorType::InvalidAssignmentTarget { + kind, + maybe_comparison: false, + } => write!(f, "cannot assign to {kind}"), + ParseErrorType::InvalidAssignmentTarget { + kind, + maybe_comparison: true, + } => write!( + f, + "cannot assign to {kind} here. Maybe you meant '==' instead of '='?" + ), + ParseErrorType::NamedExpressionWithoutParentheses => { + f.write_str("cannot use named expression without parentheses here") + } + ParseErrorType::MappingRestPatternNotLast => f.write_str( + "double star pattern must be the last (right-most) subpattern in the mapping pattern", + ), + ParseErrorType::AssignmentInsteadOfComparison => { + f.write_str("invalid syntax. Maybe you meant '==' or ':=' instead of '='?") + } + ParseErrorType::AssignmentToYield => { + f.write_str("assignment to yield expression not possible") + } + ParseErrorType::InvalidAnnotatedAssignmentTarget => { + f.write_str("illegal target for annotation") + } + ParseErrorType::InvalidNamedAssignmentTarget(kind) => { + write!(f, "cannot use assignment expressions with {kind}") + } + ParseErrorType::InvalidAugmentedAssignmentTarget(kind) => { + write!( + f, + "'{kind}' is an illegal expression for augmented assignment" + ) + } + ParseErrorType::InvalidDeleteTarget(kind) => write!(f, "cannot delete {kind}"), + ParseErrorType::InvalidImportTarget(kind) => { + write!(f, "cannot use {kind} as import target") + } + ParseErrorType::InvalidPatternTarget(kind) => { + write!(f, "cannot use {kind} as pattern target") + } + ParseErrorType::InvalidExceptTarget { kind, star } => { + let clause = if *star { "except*" } else { "except" }; + write!(f, "cannot use {clause} statement with {kind}") + } + ParseErrorType::UnexpectedIpythonEscapeCommand => { + f.write_str("IPython escape commands are only allowed in `Mode::Ipython`") + } + ParseErrorType::FStringError(error) => { + write_interpolated_string_error(f, InterpolatedStringKind::FString, error) + } + ParseErrorType::TStringError(error) => { + write_interpolated_string_error(f, InterpolatedStringKind::TString, error) + } + ParseErrorType::UnexpectedExpressionToken => f.write_str("invalid syntax"), + } + } +} + +/// Represents an error that occur during lexing and are +/// returned by the `parse_*` functions in the iterator in the +/// [lexer] implementation. +/// +/// [lexer]: crate::lexer +#[derive(Debug, Clone, PartialEq)] +pub struct LexicalError { + /// The type of error that occurred. + error: LexicalErrorType, + /// The location of the error. + location: TextRange, +} + +impl LexicalError { + /// Creates a new `LexicalError` with the given error type and location. + pub fn new(error: LexicalErrorType, location: TextRange) -> Self { + Self { error, location } + } + + pub fn error(&self) -> &LexicalErrorType { + &self.error + } + + pub fn into_error(self) -> LexicalErrorType { + self.error + } + + pub fn location(&self) -> TextRange { + self.location + } +} + +impl std::ops::Deref for LexicalError { + type Target = LexicalErrorType; + + fn deref(&self) -> &Self::Target { + self.error() + } +} + +impl std::error::Error for LexicalError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(self.error()) + } +} + +impl std::fmt::Display for LexicalError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{} at byte offset {}", + self.error(), + u32::from(self.location().start()) + ) + } +} + +/// Represents the different types of errors that can occur during lexing. +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] +pub enum LexicalErrorType { + // TODO: Can probably be removed, the places it is used seem to be able + // to use the `UnicodeError` variant instead. + #[doc(hidden)] + StringError, + /// A string literal without the closing quote, detected at the given one-based line. + UnclosedStringError { + triple_quoted: bool, + /// The literal contains a backslash followed by its quote character. + escaped_end_quote: bool, + detected_line: u32, + }, + /// An escape sequence in a string literal cannot be decoded. `start` and `end` are the + /// inclusive positions of the escape sequence in the string content, where a non-ASCII + /// character counts as its ten-character `\U` escape. + UnicodeEscapeError { + kind: UnicodeEscapeErrorKind, + start: u32, + end: u32, + }, + /// A `\x` escape in a bytes literal at the given position of its content is not followed by + /// two hexadecimal digits. + BytesEscapeError { position: u32 }, + /// A dedent does not match any outer indentation level. + IndentationError, + /// Tabs and spaces are mixed in a way that makes the indentation depend on the tab size. + TabError, + /// The indentation is nested too deeply. + TooDeepIndentation, + /// An unrecognized token was encountered. + UnrecognizedToken { tok: char }, + /// An f-string error containing the [`InterpolatedStringErrorType`]. + FStringError(InterpolatedStringErrorType), + /// A t-string error containing the [`InterpolatedStringErrorType`]. + TStringError(InterpolatedStringErrorType), + /// Invalid character encountered in a byte literal. + InvalidByteLiteral, + /// An unexpected character was encountered after a line continuation. + LineContinuationError, + /// An unexpected end of file was encountered. + Eof, + /// A closing bracket without an opening bracket. + UnmatchedBracket { closing: char }, + /// A closing bracket that does not match the innermost opening bracket. `opening_line` is the + /// line of the opening bracket if it is on another line. + MismatchedBracket { + closing: char, + opening: char, + opening_line: Option, + }, + /// An opening bracket that is not closed by the end of the source. + /// + /// `incomplete` is whether more input could still close it: the parser read to the end of the + /// source with the bracket open. Otherwise, the error is reported in place of an earlier syntax + /// error. + UnclosedBracket { opening: char, incomplete: bool }, + /// Brackets are nested too deeply. + TooDeeplyNestedBrackets, + /// A malformed number literal, or one directly followed by a name. + InvalidNumberLiteral { kind: NumberLiteralKind }, + /// A digit that is not valid in the octal or binary literal it appears in. + InvalidDigit { + digit: char, + kind: NumberLiteralKind, + }, + /// A decimal integer literal with leading zeros. + LeadingZerosInDecimalInteger, + /// A string prefix that combines incompatible prefixes, such as `ub`. + IncompatibleStringPrefixes { first: char, second: char }, + /// An unexpected error occurred. + OtherError(Box), +} + +impl std::error::Error for LexicalErrorType {} + +/// The kind of a number literal, as named in error messages. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum NumberLiteralKind { + Decimal, + Hexadecimal, + Octal, + Binary, + Imaginary, +} + +impl std::fmt::Display for NumberLiteralKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str(match self { + Self::Decimal => "decimal", + Self::Hexadecimal => "hexadecimal", + Self::Octal => "octal", + Self::Binary => "binary", + Self::Imaginary => "imaginary", + }) + } +} + +/// Whether `c` is printable: not a control, format, surrogate, private use, unassigned or +/// separator character, other than the ASCII space. +fn is_printable(c: char) -> bool { + use icu_properties::props::{EnumeratedProperty, GeneralCategory}; + + c == ' ' + || !matches!( + GeneralCategory::for_char(c), + GeneralCategory::SpaceSeparator + | GeneralCategory::LineSeparator + | GeneralCategory::ParagraphSeparator + | GeneralCategory::Control + | GeneralCategory::Format + | GeneralCategory::Surrogate + | GeneralCategory::PrivateUse + | GeneralCategory::Unassigned + ) +} + +impl LexicalErrorType { + /// Returns `true` if the error reports a string literal without its closing quotes. + pub(crate) fn is_unclosed_string_error(&self) -> bool { + matches!( + self, + Self::UnclosedStringError { .. } + | Self::FStringError( + InterpolatedStringErrorType::UnclosedLbrace + | InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } + ) + | Self::TStringError( + InterpolatedStringErrorType::UnclosedLbrace + | InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } + ) + ) + } + + /// Returns `true` if the error stops tokenization, as opposed to an error found while + /// decoding the content of a token. + pub fn is_tokenizer_error(&self) -> bool { + match self { + Self::StringError + | Self::UnicodeEscapeError { .. } + | Self::BytesEscapeError { .. } + | Self::InvalidByteLiteral + | Self::OtherError(_) => false, + // An ASCII punctuation character is a token that the parser rejects. + Self::UnrecognizedToken { tok } => !tok.is_ascii() || !is_printable(*tok), + Self::FStringError(error) | Self::TStringError(error) => matches!( + error, + InterpolatedStringErrorType::UnterminatedString { .. } + | InterpolatedStringErrorType::UnterminatedTripleQuotedString { .. } + | InterpolatedStringErrorType::SingleRbrace + | InterpolatedStringErrorType::NewlineInFormatSpec + | InterpolatedStringErrorType::UnclosedLbrace + | InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } + | InterpolatedStringErrorType::UnmatchedBracket(_) + ), + Self::UnclosedStringError { .. } + | Self::IndentationError + | Self::TabError + | Self::TooDeepIndentation + | Self::LineContinuationError + | Self::Eof + | Self::UnmatchedBracket { .. } + | Self::MismatchedBracket { .. } + | Self::UnclosedBracket { .. } + | Self::TooDeeplyNestedBrackets + | Self::InvalidNumberLiteral { .. } + | Self::InvalidDigit { .. } + | Self::LeadingZerosInDecimalInteger + | Self::IncompatibleStringPrefixes { .. } => true, + } + } + + pub(crate) fn from_interpolated_string_error( + error: InterpolatedStringErrorType, + string_kind: InterpolatedStringKind, + ) -> Self { + match string_kind { + InterpolatedStringKind::FString => Self::FStringError(error), + InterpolatedStringKind::TString => Self::TStringError(error), + } + } +} + +impl std::fmt::Display for LexicalErrorType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::StringError => write!(f, "got unexpected string"), + Self::FStringError(error) => { + write_interpolated_string_error(f, InterpolatedStringKind::FString, error) + } + Self::TStringError(error) => { + write_interpolated_string_error(f, InterpolatedStringKind::TString, error) + } + Self::InvalidByteLiteral => { + write!(f, "bytes can only contain ASCII literal characters") + } + Self::UnicodeEscapeError { kind, start, end } => write!( + f, + "(unicode error) 'unicodeescape' codec can't decode bytes in position {start}-{end}: {kind}" + ), + Self::BytesEscapeError { position } => { + write!(f, "(value error) invalid \\x escape at position {position}") + } + Self::IndentationError => { + write!(f, "unindent does not match any outer indentation level") + } + Self::TabError => write!(f, "inconsistent use of tabs and spaces in indentation"), + Self::TooDeepIndentation => write!(f, "too many levels of indentation"), + Self::UnrecognizedToken { tok } if !is_printable(*tok) => { + write!( + f, + "invalid non-printable character U+{:04X}", + u32::from(*tok) + ) + } + Self::UnrecognizedToken { tok } if !tok.is_ascii() => { + write!(f, "invalid character '{tok}' (U+{:04X})", u32::from(*tok)) + } + Self::UnrecognizedToken { .. } => f.write_str("invalid syntax"), + Self::InvalidNumberLiteral { kind } => write!(f, "invalid {kind} literal"), + Self::InvalidDigit { digit, kind } => { + write!(f, "invalid digit '{digit}' in {kind} literal") + } + Self::LeadingZerosInDecimalInteger => f.write_str( + "leading zeros in decimal integer literals are not permitted; use an 0o prefix for octal integers", + ), + Self::IncompatibleStringPrefixes { first, second } => { + write!(f, "'{first}' and '{second}' prefixes are incompatible") + } + Self::LineContinuationError => { + write!(f, "unexpected character after line continuation character") + } + Self::Eof => write!(f, "unexpected EOF while parsing"), + Self::UnmatchedBracket { closing } => write!(f, "unmatched '{closing}'"), + Self::MismatchedBracket { + closing, + opening, + opening_line, + } => { + write!( + f, + "closing parenthesis '{closing}' does not match opening parenthesis '{opening}'" + )?; + if let Some(line) = opening_line { + write!(f, " on line {line}")?; + } + Ok(()) + } + Self::UnclosedBracket { opening, .. } => write!(f, "'{opening}' was never closed"), + Self::TooDeeplyNestedBrackets => f.write_str("too many nested parentheses"), + Self::OtherError(msg) => write!(f, "{msg}"), + Self::UnclosedStringError { + triple_quoted: true, + detected_line, + .. + } => write!( + f, + "unterminated triple-quoted string literal (detected at line {detected_line})" + ), + Self::UnclosedStringError { + escaped_end_quote, + detected_line, + .. + } => { + write!( + f, + "unterminated string literal (detected at line {detected_line})" + )?; + if *escaped_end_quote { + f.write_str("; perhaps you escaped the end quote?")?; + } + Ok(()) + } + } + } +} + +/// Represents a version-related syntax error detected during parsing. +/// +/// An example of a version-related error is the use of a `match` statement before Python 3.10, when +/// it was first introduced. See [`UnsupportedSyntaxErrorKind`] for other kinds of errors. +#[derive(Debug, PartialEq, Clone, get_size2::GetSize)] +pub struct UnsupportedSyntaxError { + pub kind: UnsupportedSyntaxErrorKind, + pub range: TextRange, + /// The target [`PythonVersion`] for which this error was detected. + pub target_version: PythonVersion, +} + +impl Ranged for UnsupportedSyntaxError { + fn range(&self) -> TextRange { + self.range + } +} + +/// The type of tuple unpacking for [`UnsupportedSyntaxErrorKind::StarTuple`]. +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, get_size2::GetSize)] +pub enum StarTupleKind { + Return, + Yield, +} + +/// The type of PEP 701 f-string error for [`UnsupportedSyntaxErrorKind::Pep701FString`]. +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, get_size2::GetSize)] +pub enum FStringKind { + Backslash, + Comment, + LineBreak, + NestedQuote, +} + +/// The type of PEP 798 unpacking-comprehension error for +/// [`UnsupportedSyntaxErrorKind::UnpackingInComprehension`]. +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, get_size2::GetSize)] +pub enum ComprehensionUnpackingKind { + IterableInList, + IterableInSet, + IterableInGenerator, + DictInDict, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, get_size2::GetSize)] +pub enum UnparenthesizedNamedExprKind { + SequenceIndex, + SetLiteral, + SetComprehension, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, get_size2::GetSize)] +pub enum UnsupportedSyntaxErrorKind { + Match, + Walrus, + ExceptStar, + /// Represents the use of an unparenthesized named expression (`:=`) in a set literal, set + /// comprehension, or sequence index before Python 3.10. + /// + /// ## Examples + /// + /// These are allowed on Python 3.10: + /// + /// ```python + /// {x := 1, 2, 3} # set literal + /// {last := x for x in range(3)} # set comprehension + /// lst[x := 1] # sequence index + /// ``` + /// + /// But on Python 3.9 the named expression needs to be parenthesized: + /// + /// ```python + /// {(x := 1), 2, 3} # set literal + /// {(last := x) for x in range(3)} # set comprehension + /// lst[(x := 1)] # sequence index + /// ``` + /// + /// However, unparenthesized named expressions are never allowed in slices: + /// + /// ```python + /// lst[x:=1:-1] # syntax error + /// lst[1:x:=1] # syntax error + /// lst[1:3:x:=1] # syntax error + /// + /// lst[(x:=1):-1] # ok + /// lst[1:(x:=1)] # ok + /// lst[1:3:(x:=1)] # ok + /// ``` + /// + /// ## References + /// + /// - [Python 3.10 Other Language Changes](https://docs.python.org/3/whatsnew/3.10.html#other-language-changes) + UnparenthesizedNamedExpr(UnparenthesizedNamedExprKind), + + /// Represents the use of a parenthesized keyword argument name after Python 3.8. + /// + /// ## Example + /// + /// From [BPO 34641] it sounds like this was only accidentally supported and was removed when + /// noticed. Code like this used to be valid: + /// + /// ```python + /// f((a)=1) + /// ``` + /// + /// After Python 3.8, you have to omit the parentheses around `a`: + /// + /// ```python + /// f(a=1) + /// ``` + /// + /// [BPO 34641]: https://github.com/python/cpython/issues/78822 + ParenthesizedKeywordArgumentName, + + /// Represents the use of unparenthesized tuple unpacking in a `return` statement or `yield` + /// expression before Python 3.8. + /// + /// ## Examples + /// + /// Before Python 3.8, this syntax was allowed: + /// + /// ```python + /// rest = (4, 5, 6) + /// + /// def f(): + /// t = 1, 2, 3, *rest + /// return t + /// + /// def g(): + /// t = 1, 2, 3, *rest + /// yield t + /// ``` + /// + /// But this was not: + /// + /// ```python + /// rest = (4, 5, 6) + /// + /// def f(): + /// return 1, 2, 3, *rest + /// + /// def g(): + /// yield 1, 2, 3, *rest + /// ``` + /// + /// Instead, parentheses were required in the `return` and `yield` cases: + /// + /// ```python + /// rest = (4, 5, 6) + /// + /// def f(): + /// return (1, 2, 3, *rest) + /// + /// def g(): + /// yield (1, 2, 3, *rest) + /// ``` + /// + /// This was reported in [BPO 32117] and updated in Python 3.8 to allow the unparenthesized + /// form. + /// + /// [BPO 32117]: https://github.com/python/cpython/issues/76298 + StarTuple(StarTupleKind), + + /// Represents the use of a "relaxed" [PEP 614] decorator before Python 3.9. + /// + /// ## Examples + /// + /// Prior to Python 3.9, decorators were defined to be [`dotted_name`]s, optionally followed by + /// an argument list. For example: + /// + /// ```python + /// @buttons.clicked.connect + /// def foo(): ... + /// + /// @buttons.clicked.connect(1, 2, 3) + /// def foo(): ... + /// ``` + /// + /// As pointed out in the PEP, this prevented reasonable extensions like subscripts: + /// + /// ```python + /// buttons = [QPushButton(f'Button {i}') for i in range(10)] + /// + /// @buttons[0].clicked.connect + /// def spam(): ... + /// ``` + /// + /// Python 3.9 removed these restrictions and expanded the [decorator grammar] to include any + /// assignment expression and include cases like the example above. + /// + /// [PEP 614]: https://peps.python.org/pep-0614/ + /// [`dotted_name`]: https://docs.python.org/3.8/reference/compound_stmts.html#grammar-token-dotted-name + /// [decorator grammar]: https://docs.python.org/3/reference/compound_stmts.html#grammar-token-python-grammar-decorator + RelaxedDecorator(RelaxedDecoratorError), + + /// Represents the use of a [PEP 570] positional-only parameter before Python 3.8. + /// + /// ## Examples + /// + /// Python 3.8 added the `/` syntax for marking preceding parameters as positional-only: + /// + /// ```python + /// def foo(a, b, /, c): ... + /// ``` + /// + /// This means `a` and `b` in this case can only be provided by position, not by name. In other + /// words, this code results in a `TypeError` at runtime: + /// + /// ```pycon + /// >>> def foo(a, b, /, c): ... + /// ... + /// >>> foo(a=1, b=2, c=3) + /// Traceback (most recent call last): + /// File "", line 1, in + /// foo(a=1, b=2, c=3) + /// ~~~^^^^^^^^^^^^^^^ + /// TypeError: foo() got some positional-only arguments passed as keyword arguments: 'a, b' + /// ``` + /// + /// [PEP 570]: https://peps.python.org/pep-0570/ + PositionalOnlyParameter, + + /// Represents the use of a [type parameter list] before Python 3.12. + /// + /// ## Examples + /// + /// Before Python 3.12, generic parameters had to be declared separately using a class like + /// [`typing.TypeVar`], which could then be used in a function or class definition: + /// + /// ```python + /// from typing import Generic, TypeVar + /// + /// T = TypeVar("T") + /// + /// def f(t: T): ... + /// class C(Generic[T]): ... + /// ``` + /// + /// [PEP 695], included in Python 3.12, introduced the new type parameter syntax, which allows + /// these to be written more compactly and without a separate type variable: + /// + /// ```python + /// def f[T](t: T): ... + /// class C[T]: ... + /// ``` + /// + /// [type parameter list]: https://docs.python.org/3/reference/compound_stmts.html#type-parameter-lists + /// [PEP 695]: https://peps.python.org/pep-0695/ + /// [`typing.TypeVar`]: https://docs.python.org/3/library/typing.html#typevar + TypeParameterList, + LazyImportStatement, + TypeAliasStatement, + TypeParamDefault, + + /// Represents the use of a [PEP 701] f-string before Python 3.12. + /// + /// ## Examples + /// + /// As described in the PEP, each of these cases were invalid before Python 3.12: + /// + /// ```python + /// # nested quotes + /// f'Magic wand: { bag['wand'] }' + /// + /// # escape characters + /// f"{'\n'.join(a)}" + /// + /// # comments + /// f'''A complex trick: { + /// bag['bag'] # recursive bags! + /// }''' + /// + /// # line breaks in a non-triple-quoted replacement field + /// f"{ + /// 1 + /// }" + /// + /// # arbitrary nesting + /// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" + /// ``` + /// + /// These restrictions were lifted in Python 3.12, meaning that all of these examples are now + /// valid. + /// + /// [PEP 701]: https://peps.python.org/pep-0701/ + Pep701FString(FStringKind), + + /// Represents the use of a parenthesized `with` item before Python 3.9. + /// + /// ## Examples + /// + /// As described in [BPO 12782], `with` uses like this were not allowed on Python 3.8: + /// + /// ```python + /// with (open("a_really_long_foo") as foo, + /// open("a_really_long_bar") as bar): + /// pass + /// ``` + /// + /// because parentheses were not allowed within the `with` statement itself (see [this comment] + /// in particular). However, parenthesized expressions were still allowed, including the cases + /// below, so the issue can be pretty subtle and relates specifically to parenthesized items + /// with `as` bindings. + /// + /// ```python + /// with (foo, bar): ... # okay + /// with ( + /// open('foo.txt')) as foo: ... # also okay + /// with ( + /// foo, + /// bar, + /// baz, + /// ): ... # also okay, just a tuple + /// with ( + /// foo, + /// bar, + /// baz, + /// ) as tup: ... # also okay, binding the tuple + /// ``` + /// + /// This restriction was lifted in 3.9 but formally included in the [release notes] for 3.10. + /// + /// [BPO 12782]: https://github.com/python/cpython/issues/56991 + /// [this comment]: https://github.com/python/cpython/issues/56991#issuecomment-1093555141 + /// [release notes]: https://docs.python.org/3/whatsnew/3.10.html#summary-release-highlights + ParenthesizedContextManager, + + /// Represents the use of a [PEP 646] star expression in an index. + /// + /// ## Examples + /// + /// Before Python 3.11, star expressions were not allowed in index/subscript operations (within + /// square brackets). This restriction was lifted in [PEP 646] to allow for star-unpacking of + /// `typing.TypeVarTuple`s, also added in Python 3.11. As such, this is the primary motivating + /// example from the PEP: + /// + /// ```python + /// from typing import TypeVar, TypeVarTuple + /// + /// DType = TypeVar('DType') + /// Shape = TypeVarTuple('Shape') + /// + /// class Array(Generic[DType, *Shape]): ... + /// ``` + /// + /// But it applies to simple indexing as well: + /// + /// ```python + /// vector[*x] + /// array[a, *b] + /// ``` + /// + /// [PEP 646]: https://peps.python.org/pep-0646/#change-1-star-expressions-in-indexes + StarExpressionInIndex, + + /// Represents the use of a [PEP 646] star annotations in a function definition. + /// + /// ## Examples + /// + /// Before Python 3.11, star annotations were not allowed in function definitions. This + /// restriction was lifted in [PEP 646] to allow type annotations for `typing.TypeVarTuple`, + /// also added in Python 3.11: + /// + /// ```python + /// from typing import TypeVarTuple + /// + /// Ts = TypeVarTuple('Ts') + /// + /// def foo(*args: *Ts): ... + /// ``` + /// + /// Unlike [`UnsupportedSyntaxErrorKind::StarExpressionInIndex`], this does not include any + /// other annotation positions: + /// + /// ```python + /// x: *Ts # Syntax error + /// def foo(x: *Ts): ... # Syntax error + /// ``` + /// + /// [PEP 646]: https://peps.python.org/pep-0646/#change-2-args-as-a-typevartuple + StarAnnotation, + + /// Represents the use of iterable or dictionary unpacking inside a comprehension before Python + /// 3.15. + /// + /// ## Examples + /// + /// Before Python 3.15, comprehensions could not use iterable or dictionary unpacking in their + /// element expression: + /// + /// ```python + /// [*x for x in y] # SyntaxError + /// {*x for x in y} # SyntaxError + /// (*x for x in y) # SyntaxError + /// {**d for d in dicts} # SyntaxError + /// ``` + /// + /// Starting with Python 3.15, [PEP 798] allows unpacking within comprehensions: + /// + /// ```python + /// [*x for x in y] + /// {*x for x in y} + /// (*x for x in y) + /// {**d for d in dicts} + /// ``` + /// + /// [PEP 798]: https://peps.python.org/pep-0798/ + UnpackingInComprehension(ComprehensionUnpackingKind), + + /// Represents the use of tuple unpacking in a `for` statement iterator clause before Python + /// 3.9. + /// + /// ## Examples + /// + /// Like [`UnsupportedSyntaxErrorKind::StarTuple`] in `return` and `yield` statements, prior to + /// Python 3.9, tuple unpacking in the iterator clause of a `for` statement required + /// parentheses: + /// + /// ```python + /// # valid on Python 3.8 and earlier + /// for i in (*a, *b): ... + /// ``` + /// + /// Omitting the parentheses was invalid: + /// + /// ```python + /// for i in *a, *b: ... # SyntaxError + /// ``` + /// + /// This was changed as part of the [PEG parser rewrite] included in Python 3.9 but not + /// documented directly until the [Python 3.11 release]. + /// + /// [PEG parser rewrite]: https://peps.python.org/pep-0617/ + /// [Python 3.11 release]: https://docs.python.org/3/whatsnew/3.11.html#other-language-changes + UnparenthesizedUnpackInFor, + /// Represents the use of multiple exception names in an except clause without an `as` binding, before Python 3.14. + /// + /// ## Examples + /// Before Python 3.14, catching multiple exceptions required + /// parentheses like so: + /// + /// ```python + /// try: + /// ... + /// except (ExceptionA, ExceptionB, ExceptionC): + /// ... + /// ``` + /// + /// Starting with Python 3.14, thanks to [PEP 758], it was permitted + /// to omit the parentheses: + /// + /// ```python + /// try: + /// ... + /// except ExceptionA, ExceptionB, ExceptionC: + /// ... + /// ``` + /// + /// However, parentheses are still required in the presence of an `as`: + /// + /// ```python + /// try: + /// ... + /// except (ExceptionA, ExceptionB, ExceptionC) as e: + /// ... + /// ``` + /// + /// + /// [PEP 758]: https://peps.python.org/pep-0758/ + UnparenthesizedExceptionTypes, + /// Represents the use of a template string (t-string) + /// literal prior to the implementation of [PEP 750] + /// in Python 3.14. + /// + /// [PEP 750]: https://peps.python.org/pep-0750/ + TemplateStrings, + + /// Represents the use of a unary plus in a `match` literal pattern before Python 3.15. + /// + /// Before 3.15, unary minus was allowed but not plus: + /// + /// ```python + /// match foo: + /// case -1: ... # okay + /// case +1: ... # error before 3.15 + /// ``` + UnaryPlusMatchPattern, +} + +impl Display for UnsupportedSyntaxError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let kind = match self.kind { + UnsupportedSyntaxErrorKind::Match => "cannot use `match` statement", + UnsupportedSyntaxErrorKind::Walrus => "cannot use named assignment expression (`:=`)", + UnsupportedSyntaxErrorKind::ExceptStar => "cannot use `except*`", + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SequenceIndex, + ) => "cannot use unparenthesized assignment expression in a sequence index", + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SetLiteral, + ) => "cannot use unparenthesized assignment expression as an element in a set literal", + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SetComprehension, + ) => { + "cannot use unparenthesized assignment expression as an element in a set comprehension" + } + UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => { + "cannot use parenthesized keyword argument name" + } + UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Return) => { + "cannot use iterable unpacking in return statements" + } + UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Yield) => { + "cannot use iterable unpacking in yield expressions" + } + UnsupportedSyntaxErrorKind::RelaxedDecorator(relaxed_decorator_error) => { + return match relaxed_decorator_error { + RelaxedDecoratorError::CallExpression => { + write!( + f, + "cannot use a call expression in a decorator on Python {} \ + unless it is the top-level expression or it occurs \ + in the argument list of a top-level call expression \ + (relaxed decorator syntax was {changed})", + self.target_version, + changed = self.kind.changed_version(), + ) + } + RelaxedDecoratorError::Other(description) => write!( + f, + "cannot use {description} outside function call arguments in a decorator on Python {} \ + (syntax was {changed})", + self.target_version, + changed = self.kind.changed_version(), + ), + }; + } + UnsupportedSyntaxErrorKind::PositionalOnlyParameter => { + "cannot use positional-only parameter separator" + } + UnsupportedSyntaxErrorKind::TypeParameterList => "cannot use type parameter lists", + UnsupportedSyntaxErrorKind::LazyImportStatement => "cannot use `lazy` import statement", + UnsupportedSyntaxErrorKind::TypeAliasStatement => "cannot use `type` alias statement", + UnsupportedSyntaxErrorKind::TypeParamDefault => { + "cannot set default type for a type parameter" + } + UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Backslash) => { + "cannot use an escape sequence (backslash) in f-strings" + } + UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Comment) => { + "cannot use comments in f-strings" + } + UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::LineBreak) => { + "cannot use line breaks in non-triple-quoted f-string replacement fields" + } + UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::NestedQuote) => { + "cannot reuse outer quote character in f-strings" + } + UnsupportedSyntaxErrorKind::ParenthesizedContextManager => { + "cannot use parentheses within a `with` statement" + } + UnsupportedSyntaxErrorKind::StarExpressionInIndex => { + "cannot use star expression in index" + } + UnsupportedSyntaxErrorKind::StarAnnotation => "cannot use star annotation", + UnsupportedSyntaxErrorKind::UnpackingInComprehension( + ComprehensionUnpackingKind::IterableInList, + ) => "cannot use iterable unpacking in a list comprehension", + UnsupportedSyntaxErrorKind::UnpackingInComprehension( + ComprehensionUnpackingKind::IterableInSet, + ) => "cannot use iterable unpacking in a set comprehension", + UnsupportedSyntaxErrorKind::UnpackingInComprehension( + ComprehensionUnpackingKind::IterableInGenerator, + ) => "cannot use iterable unpacking in a generator expression", + UnsupportedSyntaxErrorKind::UnpackingInComprehension( + ComprehensionUnpackingKind::DictInDict, + ) => "cannot use dictionary unpacking in a dict comprehension", + UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor => { + "cannot use iterable unpacking in `for` statements" + } + UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes => { + "multiple exception types must be parenthesized" + } + UnsupportedSyntaxErrorKind::TemplateStrings => "cannot use t-strings", + UnsupportedSyntaxErrorKind::UnaryPlusMatchPattern => { + "unary '+' is not allowed in a literal pattern" + } + }; + + write!( + f, + "{kind} on Python {} (syntax was {changed})", + self.target_version, + changed = self.kind.changed_version(), + ) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum RelaxedDecoratorError { + CallExpression, + Other(&'static str), +} + +/// Represents the kind of change in Python syntax between versions. +enum Change { + Added(PythonVersion), + Removed(PythonVersion), +} + +impl Display for Change { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Change::Added(version) => write!(f, "added in Python {version}"), + Change::Removed(version) => write!(f, "removed in Python {version}"), + } + } +} + +impl UnsupportedSyntaxErrorKind { + /// Returns the Python version when the syntax associated with this error was changed, and the + /// type of [`Change`] (added or removed). + const fn changed_version(self) -> Change { + match self { + UnsupportedSyntaxErrorKind::Match => Change::Added(PythonVersion::PY310), + UnsupportedSyntaxErrorKind::Walrus => Change::Added(PythonVersion::PY38), + UnsupportedSyntaxErrorKind::ExceptStar => Change::Added(PythonVersion::PY311), + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr(_) => { + Change::Added(PythonVersion::PY39) + } + UnsupportedSyntaxErrorKind::StarTuple(_) => Change::Added(PythonVersion::PY38), + UnsupportedSyntaxErrorKind::RelaxedDecorator { .. } => { + Change::Added(PythonVersion::PY39) + } + UnsupportedSyntaxErrorKind::PositionalOnlyParameter => { + Change::Added(PythonVersion::PY38) + } + UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => { + Change::Removed(PythonVersion::PY38) + } + UnsupportedSyntaxErrorKind::TypeParameterList => Change::Added(PythonVersion::PY312), + UnsupportedSyntaxErrorKind::LazyImportStatement => Change::Added(PythonVersion::PY315), + UnsupportedSyntaxErrorKind::TypeAliasStatement => Change::Added(PythonVersion::PY312), + UnsupportedSyntaxErrorKind::TypeParamDefault => Change::Added(PythonVersion::PY313), + UnsupportedSyntaxErrorKind::Pep701FString(_) => Change::Added(PythonVersion::PY312), + UnsupportedSyntaxErrorKind::ParenthesizedContextManager => { + Change::Added(PythonVersion::PY39) + } + UnsupportedSyntaxErrorKind::StarExpressionInIndex => { + Change::Added(PythonVersion::PY311) + } + UnsupportedSyntaxErrorKind::StarAnnotation => Change::Added(PythonVersion::PY311), + UnsupportedSyntaxErrorKind::UnpackingInComprehension(_) => { + Change::Added(PythonVersion::PY315) + } + UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor => { + Change::Added(PythonVersion::PY39) + } + UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes => { + Change::Added(PythonVersion::PY314) + } + UnsupportedSyntaxErrorKind::TemplateStrings => Change::Added(PythonVersion::PY314), + UnsupportedSyntaxErrorKind::UnaryPlusMatchPattern => { + Change::Added(PythonVersion::PY315) + } + } + } + + /// Returns whether or not this kind of syntax is unsupported on `target_version`. + pub(crate) fn is_unsupported(self, target_version: PythonVersion) -> bool { + match self.changed_version() { + Change::Added(version) => target_version < version, + Change::Removed(version) => target_version >= version, + } + } + + /// Returns `true` if this kind of syntax is supported on `target_version`. + pub(crate) fn is_supported(self, target_version: PythonVersion) -> bool { + !self.is_unsupported(target_version) + } +} + +#[cfg(target_pointer_width = "64")] +mod sizes { + use crate::error::{LexicalError, LexicalErrorType}; + use static_assertions::assert_eq_size; + + assert_eq_size!(LexicalErrorType, [u8; 24]); + assert_eq_size!(LexicalError, [u8; 32]); +} diff --git a/vendor/rustpython-ruff_python_parser/src/lexer.rs b/vendor/rustpython-ruff_python_parser/src/lexer.rs new file mode 100644 index 00000000000..133016e023f --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/lexer.rs @@ -0,0 +1,3230 @@ +//! This module takes care of lexing Python source text. +//! +//! This means source code is scanned and translated into separate tokens. The rules +//! governing what is and is not a valid token are defined in the Python reference +//! guide section on [Lexical analysis]. +//! +//! [Lexical analysis]: https://docs.python.org/3/reference/lexical_analysis.html + +use std::cmp::Ordering; + +use unicode_ident::{is_xid_continue, is_xid_start}; + +use ruff_python_ast::StringFlags; +use ruff_python_ast::str_prefix::{AnyStringPrefix, StringLiteralPrefix}; +use ruff_python_ast::token::{TokenFlags, TokenKind}; +use ruff_python_trivia::is_python_whitespace; +use ruff_text_size::{TextLen, TextRange, TextSize}; + +use crate::Mode; +use crate::error::{ + InterpolatedStringErrorType, LexicalError, LexicalErrorType, NumberLiteralKind, +}; +use crate::lexer::cursor::{Cursor, EOF_CHAR}; +use crate::lexer::indentation::{ + Indentation, IndentationError, Indentations, IndentationsCheckpoint, +}; +use crate::lexer::interpolated_string::{ + InterpolatedStringContext, InterpolatedStrings, InterpolatedStringsCheckpoint, +}; +use crate::string::InterpolatedStringKind; + +mod cursor; +mod indentation; +mod interpolated_string; + +const BOM: char = '\u{feff}'; + +/// A lexer for Python source code. +#[derive(Debug)] +pub struct Lexer<'src> { + /// Source code to be lexed. + source: &'src str, + + /// A pointer to the current character of the source code which is being lexed. + cursor: Cursor<'src>, + + /// The kind of the current token. + current_kind: TokenKind, + + /// The range of the current token. + current_range: TextRange, + + /// Flags for the current token. + current_flags: TokenFlags, + + /// Lexer state. + state: State, + + /// Represents the current level of nesting in the lexer, indicating the depth of parentheses. + /// The lexer is within a parenthesized context if the value is greater than 0. + nesting: u32, + + /// The opening brackets that are not closed yet, innermost last. It holds the innermost + /// `nesting` brackets; the implicit outer parentheses of + /// [`Mode::ParenthesizedExpression`] have no entry. + brackets: Vec, + + /// A stack of indentation representing the current indentation level. + indentations: Indentations, + pending_indentation: Option, + + /// Lexer mode. + mode: Mode, + + /// F-string and t-string contexts. + interpolated_strings: InterpolatedStrings, + + /// Errors encountered while lexing. + errors: Vec, +} + +impl<'src> Lexer<'src> { + /// Create a new lexer for the given input source which starts at the given offset. + /// + /// If the start offset is greater than 0, the cursor is moved ahead that many bytes. + /// This means that the input source should be the complete source code and not the + /// sliced version. + pub(crate) fn new(source: &'src str, mode: Mode, start_offset: TextSize) -> Self { + assert!( + u32::try_from(source.len()).is_ok(), + "Lexer only supports files with a size up to 4GB" + ); + + let (state, nesting) = if mode == Mode::ParenthesizedExpression { + (State::Other, 1) + } else { + (State::AfterNewline, 0) + }; + + let mut lexer = Lexer { + source, + cursor: Cursor::new(source), + state, + current_kind: TokenKind::EndOfFile, + current_range: TextRange::empty(start_offset), + current_flags: TokenFlags::empty(), + nesting, + brackets: Vec::new(), + indentations: Indentations::default(), + pending_indentation: None, + mode, + interpolated_strings: InterpolatedStrings::default(), + errors: Vec::new(), + }; + + if start_offset == TextSize::new(0) { + // TODO: Handle possible mismatch between BOM and explicit encoding declaration. + lexer.cursor.eat_char(BOM); + } else { + lexer.cursor.skip_bytes(start_offset.to_usize()); + } + + lexer + } + + /// Returns the kind of the current token. + pub(crate) const fn current_kind(&self) -> TokenKind { + self.current_kind + } + + /// Returns the range of the current token. + #[doc(hidden)] + pub const fn current_range(&self) -> TextRange { + self.current_range + } + + /// Returns the flags for the current token. + #[doc(hidden)] + pub const fn current_flags(&self) -> TokenFlags { + self.current_flags + } + + /// Helper function to push the given error, updating the current range with the error location + /// and return the [`TokenKind::Unknown`] token. + fn push_error(&mut self, error: LexicalError) -> TokenKind { + self.current_range = error.location(); + self.errors.push(error); + TokenKind::Unknown + } + + /// Pushes an indentation error for the current line. + /// + /// An unmatched dedent is reported at the end of the line, tab and depth errors at its start. + fn push_indentation_error(&mut self, error: IndentationError) -> TokenKind { + let offset = self.offset().to_usize(); + let line_start = self.source[..offset] + .rfind(['\n', '\r']) + .map_or(0, |index| index + 1); + let line_end = self.source[offset..] + .find(['\n', '\r']) + .map_or(self.source.len(), |index| offset + index); + let (error, position) = match error { + IndentationError::UnmatchedDedent => (LexicalErrorType::IndentationError, line_end), + IndentationError::InconsistentTabs => (LexicalErrorType::TabError, line_start), + IndentationError::TooDeep => (LexicalErrorType::TooDeepIndentation, line_start), + }; + let range = TextRange::empty(TextSize::try_from(position).unwrap()); + self.push_error(LexicalError::new(error, range)); + // The token keeps covering the indentation. + self.current_range = self.token_range(); + TokenKind::Unknown + } + + /// Lex the next token. + pub fn next_token(&mut self) -> TokenKind { + // `lex_token` marks the start on the path that lexes each token. + self.current_flags = TokenFlags::empty(); + self.current_kind = self.lex_token(); + // For `Unknown` token, the `push_error` method updates the current range. + if !matches!(self.current_kind, TokenKind::Unknown) { + self.current_range = self.token_range(); + } + self.current_kind + } + + fn lex_token(&mut self) -> TokenKind { + if let Some(interpolated_string) = self.interpolated_strings.current() { + if !interpolated_string.is_in_interpolation(self.nesting) { + let nesting = interpolated_string.nesting(); + self.cursor.start_token(); + if let Some(token) = self.lex_interpolated_string_middle_or_end() { + if token.is_interpolated_string_end() { + // Brackets left open in a replacement field end with the string. + self.reset_nesting(nesting); + self.interpolated_strings.pop(); + } + return token; + } + } + } + // Return dedent tokens until the current indentation level matches the indentation of the next token. + // Avoid `Option::take` here: this check runs for every token, and `take` writes `None` + // even when there is no pending indentation. + else if let Some(indentation) = self.pending_indentation { + self.pending_indentation = None; + self.cursor.start_token(); + match self.indentations.current().try_compare(indentation) { + Ok(Ordering::Greater) => { + self.pending_indentation = Some(indentation); + if let Err(error) = self.indentations.dedent_one(indentation) { + return self.push_indentation_error(error); + } + return TokenKind::Dedent; + } + Ok(_) => {} + Err(error) => return self.push_indentation_error(error), + } + } + + if self.state.is_after_newline() { + // Indent and dedent tokens include leading whitespace in their ranges. + self.cursor.start_token(); + if let Some(indentation) = self.eat_indentation() { + return indentation; + } + } else { + if let Err(error) = self.skip_whitespace() { + return self.push_error(error); + } + } + + // Whitespace between tokens is not part of the next token's range. + self.cursor.start_token(); + + if let Some(c) = self.cursor.bump() { + if c.is_ascii() { + self.consume_ascii_character(c) + } else if is_unicode_identifier_start(c) { + let identifier = self.lex_identifier(c); + self.state = State::Other; + + identifier + } else { + self.push_error(LexicalError::new( + LexicalErrorType::UnrecognizedToken { tok: c }, + self.token_range(), + )) + } + } else { + // Reached the end of the file. Emit a trailing newline token if not at the beginning of a logical line, + // empty the dedent stack, and finally, return the EndOfFile token. + self.consume_end() + } + } + + fn eat_indentation(&mut self) -> Option { + let mut indentation = Indentation::root(); + + loop { + match self.cursor.first() { + ' ' => { + self.cursor.bump(); + indentation = indentation.add_space(); + } + '\t' => { + self.cursor.bump(); + indentation = indentation.add_tab(); + } + '\\' => { + self.cursor.bump(); + if self.cursor.eat_char('\r') { + self.cursor.eat_char('\n'); + } else if !self.cursor.eat_char('\n') { + return Some(self.push_error(LexicalError::new( + LexicalErrorType::LineContinuationError, + TextRange::empty(self.offset()), + ))); + } + if self.cursor.is_eof() { + let error = self.eof_error(self.token_range()); + return Some(self.push_error(error)); + } + // test_ok backslash_continuation_indentation + // if True: + // \ + // 1 + // \ + // 2 + // else:\ + // 3 + + // test_err backslash_continuation_indentation_error + // if True: + // 1 + // \ + // 2 + + // > Indentation cannot be split over multiple physical lines using backslashes; + // > the whitespace up to the first backslash determines the indentation. + // > + // > https://docs.python.org/3/reference/lexical_analysis.html#indentation + // + // Skip whitespace after the continuation-line without accumulating it into + // `indentation`. However, if the backslash is at column 0 (no prior + // indentation), let the loop continue so the next line's whitespace is + // accumulated normally. + // + // See also: https://github.com/python/cpython/issues/90249 + if indentation != Indentation::root() { + self.cursor.eat_while(is_python_whitespace); + } + } + // Form feed + '\x0C' => { + self.cursor.bump(); + indentation = Indentation::root(); + } + _ => break, + } + } + + // Handle indentation if this is a new, not all empty, logical line + if !matches!(self.cursor.first(), '\n' | '\r' | '#' | EOF_CHAR) { + self.state = State::NonEmptyLogicalLine; + + // Set to false so that we don't handle indentation on the next call. + return self.handle_indentation(indentation); + } + + None + } + + fn handle_indentation(&mut self, indentation: Indentation) -> Option { + match self.indentations.current().try_compare(indentation) { + // Dedent + Ok(Ordering::Greater) => { + self.pending_indentation = Some(indentation); + + if let Err(error) = self.indentations.dedent_one(indentation) { + return Some(self.push_indentation_error(error)); + } + + // The lexer might've eaten some whitespaces to calculate the `indentation`. For + // example: + // + // ```py + // if first: + // if second: + // pass + // foo + // # ^ + // ``` + // + // Here, the cursor is at `^` and the `indentation` contains the whitespaces before + // the `pass` token. + self.cursor.start_token(); + + Some(TokenKind::Dedent) + } + + Ok(Ordering::Equal) => None, + + // Indent + Ok(Ordering::Less) => { + if let Err(error) = self.indentations.indent(indentation) { + return Some(self.push_indentation_error(error)); + } + Some(TokenKind::Indent) + } + Err(error) => Some(self.push_indentation_error(error)), + } + } + + fn skip_whitespace(&mut self) -> Result<(), LexicalError> { + let whitespace_start = if matches!(self.cursor.first(), ' ' | '\t' | '\\' | '\x0C') { + self.offset() + } else { + return Ok(()); + }; + + loop { + match self.cursor.first() { + ' ' => { + self.cursor.bump(); + } + '\t' => { + self.cursor.bump(); + } + '\\' => { + self.cursor.bump(); + if self.cursor.eat_char('\r') { + self.cursor.eat_char('\n'); + } else if !self.cursor.eat_char('\n') { + return Err(LexicalError::new( + LexicalErrorType::LineContinuationError, + TextRange::empty(self.offset()), + )); + } + if self.cursor.is_eof() { + return Err(self.eof_error(TextRange::new(whitespace_start, self.offset()))); + } + } + // Form feed + '\x0C' => { + self.cursor.bump(); + } + _ => break, + } + } + + Ok(()) + } + + // Dispatch based on the given character. + fn consume_ascii_character(&mut self, c: char) -> TokenKind { + let token = match c { + c if is_ascii_identifier_start(c) => self.lex_identifier(c), + '0'..='9' => self.lex_number(c), + '#' => return self.lex_comment(), + '\'' | '"' => self.lex_string(c), + '=' => { + if self.cursor.eat_char('=') { + TokenKind::EqEqual + } else { + self.state = State::AfterEqual; + return TokenKind::Equal; + } + } + '+' => { + if self.cursor.eat_char('=') { + TokenKind::PlusEqual + } else { + TokenKind::Plus + } + } + '*' => { + if self.cursor.eat_char('=') { + TokenKind::StarEqual + } else if self.cursor.eat_char('*') { + if self.cursor.eat_char('=') { + TokenKind::DoubleStarEqual + } else { + TokenKind::DoubleStar + } + } else { + TokenKind::Star + } + } + + '%' | '!' + if self.mode == Mode::Ipython + && self.state.is_after_equal() + && self.nesting == 0 => + { + self.lex_ipython_escape_command() + } + + '%' | '!' | '?' | '/' | ';' | ',' + if self.mode == Mode::Ipython && self.state.is_new_logical_line() => + { + self.lex_ipython_escape_command() + } + + '?' if self.mode == Mode::Ipython => TokenKind::Question, + + '/' => { + if self.cursor.eat_char('=') { + TokenKind::SlashEqual + } else if self.cursor.eat_char('/') { + if self.cursor.eat_char('=') { + TokenKind::DoubleSlashEqual + } else { + TokenKind::DoubleSlash + } + } else { + TokenKind::Slash + } + } + '%' => { + if self.cursor.eat_char('=') { + TokenKind::PercentEqual + } else { + TokenKind::Percent + } + } + '|' => { + if self.cursor.eat_char('=') { + TokenKind::VbarEqual + } else { + TokenKind::Vbar + } + } + '^' => { + if self.cursor.eat_char('=') { + TokenKind::CircumflexEqual + } else { + TokenKind::CircumFlex + } + } + '&' => { + if self.cursor.eat_char('=') { + TokenKind::AmperEqual + } else { + TokenKind::Amper + } + } + '-' => { + if self.cursor.eat_char('=') { + TokenKind::MinusEqual + } else if self.cursor.eat_char('>') { + TokenKind::Rarrow + } else { + TokenKind::Minus + } + } + '@' => { + if self.cursor.eat_char('=') { + TokenKind::AtEqual + } else { + TokenKind::At + } + } + '!' => { + if self.cursor.eat_char('=') { + TokenKind::NotEqual + } else { + TokenKind::Exclamation + } + } + '~' => TokenKind::Tilde, + '(' => { + self.open_bracket('('); + TokenKind::Lpar + } + ')' => { + self.close_bracket(')'); + TokenKind::Rpar + } + '[' => { + self.open_bracket('['); + TokenKind::Lsqb + } + ']' => { + self.close_bracket(']'); + TokenKind::Rsqb + } + '{' => { + self.open_bracket('{'); + TokenKind::Lbrace + } + '}' => { + if let Some(interpolated_string) = self.interpolated_strings.current_mut() { + if interpolated_string.nesting() == self.nesting { + let error_type = LexicalErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::SingleRbrace, + interpolated_string.kind(), + ); + return self.push_error(LexicalError::new(error_type, self.token_range())); + } + interpolated_string.try_end_format_spec(self.nesting); + } + self.close_bracket('}'); + TokenKind::Rbrace + } + ':' => { + if self + .interpolated_strings + .current_mut() + .is_some_and(|interpolated_string| { + interpolated_string.try_start_format_spec(self.nesting) + }) + { + TokenKind::Colon + } else if self.cursor.eat_char('=') { + TokenKind::ColonEqual + } else { + TokenKind::Colon + } + } + ';' => TokenKind::Semi, + '<' => { + if self.cursor.eat_char('<') { + if self.cursor.eat_char('=') { + TokenKind::LeftShiftEqual + } else { + TokenKind::LeftShift + } + } else if self.cursor.eat_char('=') { + TokenKind::LessEqual + } else { + TokenKind::Less + } + } + '>' => { + if self.cursor.eat_char('>') { + if self.cursor.eat_char('=') { + TokenKind::RightShiftEqual + } else { + TokenKind::RightShift + } + } else if self.cursor.eat_char('=') { + TokenKind::GreaterEqual + } else { + TokenKind::Greater + } + } + ',' => TokenKind::Comma, + '.' => { + if self.cursor.first().is_ascii_digit() { + self.lex_decimal_number('.') + } else if self.cursor.eat_char2('.', '.') { + TokenKind::Ellipsis + } else { + TokenKind::Dot + } + } + '\n' => { + return if self.nesting == 0 && !self.state.is_new_logical_line() { + self.state = State::AfterNewline; + TokenKind::Newline + } else { + if let Some(interpolated_string) = self.interpolated_strings.current_mut() { + interpolated_string.try_end_format_spec(self.nesting); + } + TokenKind::NonLogicalNewline + }; + } + '\r' => { + self.cursor.eat_char('\n'); + + return if self.nesting == 0 && !self.state.is_new_logical_line() { + self.state = State::AfterNewline; + TokenKind::Newline + } else { + if let Some(interpolated_string) = self.interpolated_strings.current_mut() { + interpolated_string.try_end_format_spec(self.nesting); + } + TokenKind::NonLogicalNewline + }; + } + + _ => { + self.state = State::Other; + + return self.push_error(LexicalError::new( + LexicalErrorType::UnrecognizedToken { tok: c }, + self.token_range(), + )); + } + }; + + self.state = State::Other; + + token + } + + /// Lex an identifier. Also used for keywords and string/bytes literals with a prefix. + fn lex_identifier(&mut self, first: char) -> TokenKind { + if let Some(error) = self.incompatible_string_prefixes(first) { + self.cursor + .skip_bytes(error.location().len().to_usize() - first.len_utf8()); + self.errors.push(error); + self.current_range = self.token_range(); + return TokenKind::Unknown; + } + + // Detect potential string like rb'' b'' f'' t'' u'' r'' + let quote = if let Some(prefix) = single_char_prefix(first) { + match self.cursor.first() { + quote @ ('\'' | '"') => { + self.current_flags |= prefix; + self.cursor.bump(); + Some(quote) + } + second + if let quote = self.cursor.second() + && is_quote(quote) => + { + self.try_double_char_prefix([first, second]).then(|| { + self.cursor.bump(); + self.cursor.bump(); + quote + }) + } + _ => None, + } + } else { + None + }; + + if let Some(quote) = quote { + if self.current_flags.is_interpolated_string() { + if let Some(kind) = self.lex_interpolated_string_start(quote) { + return kind; + } + } + + return self.lex_string(quote); + } + + // Keep track of whether the identifier is ASCII-only or not. + // + // This is important because Python applies NFKC normalization to + // identifiers: https://docs.python.org/3/reference/lexical_analysis.html#identifiers. + // The parser needs to do the same when cooking the name, but applying + // NFKC normalization unconditionally is extremely expensive. If we know + // an identifier is ASCII-only (by far the most common case), the parser + // can skip NFKC normalization. + let mut is_ascii = first.is_ascii(); + self.cursor + .eat_while(|c| is_identifier_continuation(c, &mut is_ascii)); + + if !is_ascii { + self.current_flags |= TokenFlags::NON_ASCII_IDENTIFIER; + return TokenKind::Identifier; + } + + let text = self.token_text(); + + // No Python keyword is longer than eight bytes. + if text.len() > 8 { + return TokenKind::Identifier; + } + + match text.as_bytes() { + b"False" => TokenKind::False, + b"None" => TokenKind::None, + b"True" => TokenKind::True, + b"and" => TokenKind::And, + b"as" => TokenKind::As, + b"assert" => TokenKind::Assert, + b"async" => TokenKind::Async, + b"await" => TokenKind::Await, + b"break" => TokenKind::Break, + b"case" => TokenKind::Case, + b"class" => TokenKind::Class, + b"continue" => TokenKind::Continue, + b"def" => TokenKind::Def, + b"del" => TokenKind::Del, + b"elif" => TokenKind::Elif, + b"else" => TokenKind::Else, + b"except" => TokenKind::Except, + b"finally" => TokenKind::Finally, + b"for" => TokenKind::For, + b"from" => TokenKind::From, + b"global" => TokenKind::Global, + b"if" => TokenKind::If, + b"import" => TokenKind::Import, + b"in" => TokenKind::In, + b"is" => TokenKind::Is, + b"lazy" => TokenKind::Lazy, + b"lambda" => TokenKind::Lambda, + b"match" => TokenKind::Match, + b"nonlocal" => TokenKind::Nonlocal, + b"not" => TokenKind::Not, + b"or" => TokenKind::Or, + b"pass" => TokenKind::Pass, + b"raise" => TokenKind::Raise, + b"return" => TokenKind::Return, + b"try" => TokenKind::Try, + b"type" => TokenKind::Type, + b"while" => TokenKind::While, + b"with" => TokenKind::With, + b"yield" => TokenKind::Yield, + _ => TokenKind::Identifier, + } + } + + /// Returns the error for a string prefix starting with `first` that combines incompatible + /// prefixes, such as `ub''`. The error covers the prefix. + fn incompatible_string_prefixes(&self, first: char) -> Option { + const PREFIXES: [char; 5] = ['u', 'b', 'r', 'f', 't']; + const INCOMPATIBLE: [(char, char); 7] = [ + ('u', 'b'), + ('u', 'r'), + ('u', 'f'), + ('u', 't'), + ('b', 'f'), + ('b', 't'), + ('f', 't'), + ]; + + let bit = |prefix: char| PREFIXES.iter().position(|&c| c == prefix).map(|i| 1u8 << i); + let mut seen = 0u8; + let mut len = TextSize::new(0); + let mut chars = std::iter::once(first).chain(self.cursor.rest().chars()); + loop { + let c = chars.next()?; + if is_quote(c) { + break; + } + let prefix = bit(c.to_ascii_lowercase())?; + if seen & prefix != 0 { + return None; + } + seen |= prefix; + len += c.text_len(); + } + let has = |prefix: char| bit(prefix).is_some_and(|prefix| seen & prefix != 0); + let (first, second) = INCOMPATIBLE + .into_iter() + .find(|(first, second)| has(*first) && has(*second))?; + Some(LexicalError::new( + LexicalErrorType::IncompatibleStringPrefixes { first, second }, + TextRange::at(self.token_range().start(), len), + )) + } + + /// Try lexing the double character string prefix, updating the token flags accordingly. + /// Returns `true` if it matches. + fn try_double_char_prefix(&mut self, value: [char; 2]) -> bool { + match value { + ['r', 'f' | 'F'] | ['f' | 'F', 'r'] => { + self.current_flags |= TokenFlags::F_STRING | TokenFlags::RAW_STRING_LOWERCASE; + } + ['R', 'f' | 'F'] | ['f' | 'F', 'R'] => { + self.current_flags |= TokenFlags::F_STRING | TokenFlags::RAW_STRING_UPPERCASE; + } + ['r', 't' | 'T'] | ['t' | 'T', 'r'] => { + self.current_flags |= TokenFlags::T_STRING | TokenFlags::RAW_STRING_LOWERCASE; + } + ['R', 't' | 'T'] | ['t' | 'T', 'R'] => { + self.current_flags |= TokenFlags::T_STRING | TokenFlags::RAW_STRING_UPPERCASE; + } + ['r', 'b' | 'B'] | ['b' | 'B', 'r'] => { + self.current_flags |= TokenFlags::BYTE_STRING | TokenFlags::RAW_STRING_LOWERCASE; + } + ['R', 'b' | 'B'] | ['b' | 'B', 'R'] => { + self.current_flags |= TokenFlags::BYTE_STRING | TokenFlags::RAW_STRING_UPPERCASE; + } + _ => return false, + } + true + } + + /// Lex a f-string or t-string start token if positioned at the start of an f-string or t-string. + fn lex_interpolated_string_start(&mut self, quote: char) -> Option { + #[cfg(debug_assertions)] + debug_assert_eq!(self.cursor.previous(), quote); + + if quote == '"' { + self.current_flags |= TokenFlags::DOUBLE_QUOTES; + } + + if self.cursor.eat_char2(quote, quote) { + self.current_flags |= TokenFlags::TRIPLE_QUOTED_STRING; + } + + let ftcontext = InterpolatedStringContext::new( + self.current_flags, + self.nesting, + self.token_range().start(), + )?; + + let kind = ftcontext.kind(); + + self.interpolated_strings.push(ftcontext); + + Some(kind.start_token()) + } + + /// Lex an f-string or t-string middle or end token. + fn lex_interpolated_string_middle_or_end(&mut self) -> Option { + // SAFETY: Safe because the function is only called when `self.fstrings` is not empty. + let interpolated_string = self.interpolated_strings.current().unwrap(); + let string_kind = interpolated_string.kind(); + let interpolated_flags = interpolated_string.flags(); + + // Check if we're at the end of the f-string. + if interpolated_string.is_triple_quoted() { + let quote_char = interpolated_string.quote_char(); + if self.cursor.eat_char3(quote_char, quote_char, quote_char) { + self.current_flags = interpolated_string.flags(); + return Some(string_kind.end_token()); + } + } else if self.cursor.eat_char(interpolated_string.quote_char()) { + self.current_flags = interpolated_string.flags(); + return Some(string_kind.end_token()); + } + + // This isn't going to change for the duration of the loop. + let in_format_spec = interpolated_string.is_in_format_spec(self.nesting); + + let mut in_named_unicode = false; + + loop { + match self.cursor.first() { + // The condition is to differentiate between the `NUL` (`\0`) character + // in the source code and the one returned by `self.cursor.first()` when + // we reach the end of the source code. + EOF_CHAR if self.cursor.is_eof() => { + let detected_line = self.detected_line(); + let range = TextRange::empty(interpolated_string.start()); + let error = if interpolated_string.is_triple_quoted() { + InterpolatedStringErrorType::UnterminatedTripleQuotedString { + detected_line, + } + } else { + InterpolatedStringErrorType::UnterminatedString { detected_line } + }; + + self.reset_nesting(interpolated_string.nesting()); + self.interpolated_strings.pop(); + self.current_flags |= TokenFlags::UNCLOSED_STRING; + self.push_error(LexicalError::new( + LexicalErrorType::from_interpolated_string_error(error, string_kind), + range, + )); + + break; + } + '\n' | '\r' if !interpolated_string.is_triple_quoted() => { + // https://github.com/astral-sh/ruff/issues/18632 + + let (error_type, range) = if in_format_spec { + ( + InterpolatedStringErrorType::NewlineInFormatSpec, + self.token_range(), + ) + } else { + ( + InterpolatedStringErrorType::UnterminatedString { + detected_line: self.detected_line(), + }, + TextRange::empty(interpolated_string.start()), + ) + }; + + self.reset_nesting(interpolated_string.nesting()); + self.interpolated_strings.pop(); + self.current_flags |= TokenFlags::UNCLOSED_STRING; + + self.push_error(LexicalError::new( + LexicalErrorType::from_interpolated_string_error(error_type, string_kind), + range, + )); + + break; + } + '\\' => { + self.cursor.bump(); // '\' + if matches!(self.cursor.first(), '{' | '}') { + // Don't consume `{` or `}` as we want them to be emitted as tokens. + // They will be handled in the next iteration. + continue; + } else if !interpolated_string.is_raw_string() { + if self.cursor.eat_char2('N', '{') { + in_named_unicode = true; + continue; + } + } + // Consume the escaped character. + if self.cursor.eat_char('\r') { + self.cursor.eat_char('\n'); + } else { + self.cursor.bump(); + } + } + quote @ ('\'' | '"') if quote == interpolated_string.quote_char() => { + if let Some(triple_quotes) = interpolated_string.triple_quotes() { + if self.cursor.rest().starts_with(triple_quotes) { + break; + } + self.cursor.bump(); + } else { + break; + } + } + '{' => { + if self.cursor.second() == '{' && !in_format_spec { + self.cursor.bump(); + self.cursor.bump(); // Skip the second `{` + } else { + break; + } + } + '}' => { + if in_named_unicode { + in_named_unicode = false; + self.cursor.bump(); + } else if self.cursor.second() == '}' && !in_format_spec { + self.cursor.bump(); + self.cursor.bump(); // Skip the second `}` + } else { + break; + } + } + _ => { + self.cursor.bump(); + } + } + } + let range = self.token_range(); + if range.is_empty() { + return None; + } + + self.current_flags = interpolated_flags; + Some(string_kind.middle_token()) + } + + /// Lex a string literal. + fn lex_string(&mut self, quote: char) -> TokenKind { + #[cfg(debug_assertions)] + debug_assert_eq!(self.cursor.previous(), quote); + + if quote == '"' { + self.current_flags |= TokenFlags::DOUBLE_QUOTES; + } + + // If the next two characters are also the quote character, then we have a triple-quoted + // string; consume those two characters and ensure that we require a triple-quote to close + if self.cursor.eat_char2(quote, quote) { + self.current_flags |= TokenFlags::TRIPLE_QUOTED_STRING; + } + + let quote_byte = u8::try_from(quote).expect("char that fits in u8"); + if self.current_flags.is_triple_quoted() { + // For triple-quoted strings, scan until we find the closing quote (ignoring escaped + // quotes) or the end of the file. + loop { + let Some(index) = memchr::memchr(quote_byte, self.cursor.rest().as_bytes()) else { + self.cursor.skip_to_end(); + + self.current_flags |= TokenFlags::UNCLOSED_STRING; + self.push_unclosed_string_error(quote); + break; + }; + + // Rare case: if there are an odd number of backslashes before the quote, then + // the quote is escaped and we should continue scanning. + let num_backslashes = self.cursor.rest().as_bytes()[..index] + .iter() + .rev() + .take_while(|&&c| c == b'\\') + .count(); + + // Advance the cursor past the quote and continue scanning. + self.cursor.skip_bytes(index + 1); + + // If the character is escaped, continue scanning. + if num_backslashes % 2 == 1 { + continue; + } + + // Otherwise, if it's followed by two more quotes, then we're done. + if self.cursor.eat_char2(quote, quote) { + break; + } + } + } else { + // For non-triple-quoted strings, scan until we find the closing quote, but end early + // if we encounter a newline or the end of the file. + loop { + let Some(index) = + memchr::memchr3(quote_byte, b'\r', b'\n', self.cursor.rest().as_bytes()) + else { + self.cursor.skip_to_end(); + self.current_flags |= TokenFlags::UNCLOSED_STRING; + self.push_unclosed_string_error(quote); + + break; + }; + + // Rare case: if there are an odd number of backslashes before the quote, then + // the quote is escaped and we should continue scanning. + let num_backslashes = self.cursor.rest().as_bytes()[..index] + .iter() + .rev() + .take_while(|&&c| c == b'\\') + .count(); + + // Skip up to the current character. + self.cursor.skip_bytes(index); + + // Lookahead because we want to bump only if it's a quote or being escaped. + let quote_or_newline = self.cursor.first(); + + // If the character is escaped, continue scanning. + if num_backslashes % 2 == 1 { + self.cursor.bump(); + if quote_or_newline == '\r' { + self.cursor.eat_char('\n'); + } + continue; + } + + match quote_or_newline { + '\r' | '\n' => { + self.current_flags |= TokenFlags::UNCLOSED_STRING; + self.push_unclosed_string_error(quote); + break; + } + ch if ch == quote => { + self.cursor.bump(); + break; + } + _ => unreachable!("memchr2 returned an index that is not a quote or a newline"), + } + } + } + + TokenKind::String + } + + /// Pushes an [`LexicalErrorType::UnclosedStringError`] for the string token lexed so far, + /// reported at the start of the token. + fn push_unclosed_string_error(&mut self, quote: char) { + let range = self.token_range(); + let body = &self.source[range]; + let body = &body[body.find(quote).unwrap_or(0)..]; + let mut escaped_end_quote = false; + let mut chars = body.chars(); + while let Some(c) = chars.next() { + if c == '\\' && chars.next() == Some(quote) { + escaped_end_quote = true; + break; + } + } + let error = match self.interpolated_strings.current() { + // A string in a replacement field that opens with the quotes of the enclosing + // f-string or t-string is its end without the closing brace. + Some(interpolated_string) + if interpolated_string.quote_char() == quote + && interpolated_string.is_triple_quoted() + == self.current_flags.is_triple_quoted() => + { + LexicalErrorType::from_interpolated_string_error( + self.unclosed_replacement_field_error(), + interpolated_string.kind(), + ) + } + _ => LexicalErrorType::UnclosedStringError { + triple_quoted: self.current_flags.is_triple_quoted(), + escaped_end_quote, + detected_line: self.detected_line(), + }, + }; + self.push_error(LexicalError::new(error, TextRange::empty(range.start()))); + } + + /// Returns the one-based line number of the cursor, where a newline that ends the source + /// belongs to the line it terminates. + fn detected_line(&self) -> u32 { + let mut offset = self.offset(); + if offset.to_usize() == self.source.len() && self.source.ends_with(['\n', '\r']) { + offset -= TextSize::new(1); + } + self.line_number(offset) + } + + /// Returns the one-based line number of `offset`. + fn line_number(&self, offset: TextSize) -> u32 { + let source = self.source.as_bytes(); + let newlines = source[..offset.to_usize()] + .iter() + .enumerate() + .filter(|&(index, &byte)| { + byte == b'\n' || (byte == b'\r' && source.get(index + 1) != Some(&b'\n')) + }) + .count(); + u32::try_from(newlines + 1).unwrap() + } + + /// Returns the opening brace of the active replacement field and whether it is in a + /// format specifier. Brackets inside its expression are not replacement fields. + #[cfg(feature = "python315-diagnostics")] + pub(crate) fn replacement_field(&self) -> Option<(TextSize, bool)> { + let context = self.interpolated_strings.current()?; + if self.nesting <= context.nesting() { + return None; + } + let implicit = self.nesting as usize - self.brackets.len(); + let index = + (context.replacement_field_nesting(self.nesting) as usize).checked_sub(implicit)?; + let bracket = self.brackets.get(index)?; + (bracket.kind == '{').then_some((bracket.start, context.is_in_format_spec(self.nesting))) + } + + fn unclosed_replacement_field_error(&self) -> InterpolatedStringErrorType { + #[cfg(feature = "python315-diagnostics")] + if let Some((start, _)) = self.replacement_field() { + let opening_line = self.line_number(start); + if opening_line != self.line_number(self.token_range().start()) { + return InterpolatedStringErrorType::UnclosedLbraceOnLine { opening_line }; + } + } + InterpolatedStringErrorType::UnclosedLbrace + } + + /// Returns the error for reaching the end of the source unexpectedly at `range`: the innermost + /// bracket that is not closed, if any. + fn eof_error(&self, range: TextRange) -> LexicalError { + match self.brackets.last() { + Some(bracket) => LexicalError::new( + LexicalErrorType::UnclosedBracket { + opening: bracket.kind, + incomplete: true, + }, + TextRange::empty(bracket.start), + ), + None => LexicalError::new(LexicalErrorType::Eof, range), + } + } + + /// Enters the opening bracket `kind` at the current token. + fn open_bracket(&mut self, kind: char) { + let start = self.token_range().start(); + if self.nesting >= MAX_NESTING { + self.errors.push(LexicalError::new( + LexicalErrorType::TooDeeplyNestedBrackets, + TextRange::empty(start), + )); + } + self.nesting += 1; + self.brackets.push(OpenBracket { kind, start }); + } + + /// Leaves the innermost bracket at the closing bracket `closing`, reporting a closing bracket + /// that has no opening bracket or does not match it. + fn close_bracket(&mut self, closing: char) { + let start = self.token_range().start(); + let at_interpolation_brace = + self.interpolated_strings + .current() + .filter(|interpolated_string| { + interpolated_string.is_at_interpolation_brace(self.nesting) + }); + let error = match self.brackets.pop() { + Some(open) if open.kind == opening_bracket(closing) => None, + Some(open) if open.kind == '{' && at_interpolation_brace.is_some() => { + Some(LexicalErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::UnmatchedBracket(closing), + at_interpolation_brace.unwrap().kind(), + )) + } + Some(open) => { + let opening_line = self.line_number(open.start); + Some(LexicalErrorType::MismatchedBracket { + closing, + opening: open.kind, + opening_line: (opening_line != self.line_number(start)).then_some(opening_line), + }) + } + None if self.nesting == 0 => Some(LexicalErrorType::UnmatchedBracket { closing }), + None => None, + }; + if let Some(error) = error { + self.errors + .push(LexicalError::new(error, TextRange::empty(start))); + } + self.nesting = self.nesting.saturating_sub(1); + } + + /// Sets the nesting level to `nesting`, dropping the brackets inside it. + fn reset_nesting(&mut self, nesting: u32) { + let implicit = self.nesting as usize - self.brackets.len(); + self.brackets + .truncate((nesting as usize).saturating_sub(implicit)); + self.nesting = nesting; + } + + /// Numeric lexing. The feast can start! + fn lex_number(&mut self, first: char) -> TokenKind { + if first == '0' { + if self.cursor.eat_if(|c| matches!(c, 'x' | 'X')).is_some() { + self.lex_number_radix(Radix::Hex) + } else if self.cursor.eat_if(|c| matches!(c, 'o' | 'O')).is_some() { + self.lex_number_radix(Radix::Octal) + } else if self.cursor.eat_if(|c| matches!(c, 'b' | 'B')).is_some() { + self.lex_number_radix(Radix::Binary) + } else { + self.lex_decimal_number(first) + } + } else { + self.lex_decimal_number(first) + } + } + + /// Lex a hex/octal/binary number after its `0x`, `0o` or `0b` prefix. + fn lex_number_radix(&mut self, radix: Radix) -> TokenKind { + #[cfg(debug_assertions)] + { + use std::debug_assert_matches; + debug_assert_matches!(self.cursor.previous().to_ascii_lowercase(), 'x' | 'o' | 'b'); + } + + loop { + self.cursor.eat_char('_'); + if !radix.is_digit(self.cursor.first()) { + return self.push_invalid_radix_digit(radix); + } + self.cursor.eat_while(|c| radix.is_digit(c)); + if self.cursor.first() != '_' { + break; + } + } + if self.cursor.first().is_ascii_digit() && radix != Radix::Hex { + return self.push_invalid_radix_digit(radix); + } + self.verify_end_of_number(radix.kind()); + TokenKind::Int + } + + /// Pushes the error for a character after the prefix or an `_` of a hex/octal/binary number + /// that is not one of its digits. + fn push_invalid_radix_digit(&mut self, radix: Radix) -> TokenKind { + let kind = radix.kind(); + let digit = self.cursor.first(); + let error = if digit.is_ascii_digit() && radix != Radix::Hex { + self.cursor.bump(); + LexicalErrorType::InvalidDigit { digit, kind } + } else { + LexicalErrorType::InvalidNumberLiteral { kind } + }; + self.push_number_error(error) + } + + /// Lex a decimal number, starting with a digit or with a `.` followed by a digit. + fn lex_decimal_number(&mut self, first_digit_or_dot: char) -> TokenKind { + #[cfg(debug_assertions)] + debug_assert!(self.cursor.previous().is_ascii_digit() || self.cursor.previous() == '.'); + + let mut fraction = first_digit_or_dot == '.'; + if first_digit_or_dot == '0' { + // Zeros, which may be followed by other digits only in a float or imaginary literal. + loop { + if self.cursor.eat_char('_') && !self.cursor.first().is_ascii_digit() { + return self.push_number_error(LexicalErrorType::InvalidNumberLiteral { + kind: NumberLiteralKind::Decimal, + }); + } + if !self.cursor.eat_char('0') { + break; + } + } + let nonzero_start = self.offset(); + let nonzero = self.cursor.first().is_ascii_digit(); + if nonzero && !self.decimal_tail() { + return TokenKind::Unknown; + } + match self.cursor.first() { + '.' => { + self.cursor.bump(); + fraction = true; + } + 'e' | 'E' | 'j' | 'J' => {} + _ if nonzero => { + self.errors.push(LexicalError::new( + LexicalErrorType::LeadingZerosInDecimalInteger, + TextRange::new(self.token_range().start(), nonzero_start), + )); + self.current_range = self.token_range(); + return TokenKind::Unknown; + } + _ => {} + } + } else if first_digit_or_dot != '.' { + if !self.decimal_tail() { + return TokenKind::Unknown; + } + fraction = self.cursor.eat_char('.'); + } + + let mut is_float = fraction; + if fraction && self.cursor.first().is_ascii_digit() && !self.decimal_tail() { + return TokenKind::Unknown; + } + + if matches!(self.cursor.first(), 'e' | 'E') { + match self.cursor.second() { + '+' | '-' => { + self.cursor.bump(); + self.cursor.bump(); + if !self.cursor.first().is_ascii_digit() { + return self.push_number_error(LexicalErrorType::InvalidNumberLiteral { + kind: NumberLiteralKind::Decimal, + }); + } + } + second if !second.is_ascii_digit() => { + // The `e` isn't part of the number. + self.verify_end_of_number(NumberLiteralKind::Decimal); + return if is_float { + TokenKind::Float + } else { + TokenKind::Int + }; + } + _ => { + self.cursor.bump(); + } + } + if !self.decimal_tail() { + return TokenKind::Unknown; + } + is_float = true; + } + + if self.cursor.eat_if(|c| matches!(c, 'j' | 'J')).is_some() { + self.verify_end_of_number(NumberLiteralKind::Imaginary); + TokenKind::Complex + } else { + self.verify_end_of_number(NumberLiteralKind::Decimal); + if is_float { + TokenKind::Float + } else { + TokenKind::Int + } + } + } + + /// Consumes decimal digits, which may be separated by single underscores. Pushes an error and + /// returns `false` for an underscore that is not followed by a digit. + fn decimal_tail(&mut self) -> bool { + loop { + self.cursor.eat_while(|c| c.is_ascii_digit()); + if !self.cursor.eat_char('_') { + return true; + } + if !self.cursor.first().is_ascii_digit() { + self.push_number_error(LexicalErrorType::InvalidNumberLiteral { + kind: NumberLiteralKind::Decimal, + }); + return false; + } + } + } + + /// Pushes an error for the number literal at its last consumed character, ending the token. + fn push_number_error(&mut self, error: LexicalErrorType) -> TokenKind { + self.errors.push(LexicalError::new( + error, + TextRange::empty(self.offset() - TextSize::new(1)), + )); + self.current_range = self.token_range(); + TokenKind::Unknown + } + + /// Pushes an error if the number literal is directly followed by a name, unless the name starts + /// with a keyword that can follow a number in valid code. The token is kept as a number. + fn verify_end_of_number(&mut self, kind: NumberLiteralKind) { + let rest = self.cursor.rest(); + let before_keyword = ["and", "else", "for", "if", "in", "is", "not", "or"] + .iter() + .any(|keyword| rest.starts_with(keyword)); + if !before_keyword + && rest + .chars() + .next() + .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') + { + self.errors.push(LexicalError::new( + LexicalErrorType::InvalidNumberLiteral { kind }, + TextRange::empty(self.offset() - TextSize::new(1)), + )); + } + } + + /// Lex a single comment. + fn lex_comment(&mut self) -> TokenKind { + #[cfg(debug_assertions)] + debug_assert_eq!(self.cursor.previous(), '#'); + + let bytes = self.cursor.rest().as_bytes(); + let offset = memchr::memchr2(b'\n', b'\r', bytes).unwrap_or(bytes.len()); + self.cursor.skip_bytes(offset); + + TokenKind::Comment + } + + /// Lex a single IPython escape command. + fn lex_ipython_escape_command(&mut self) -> TokenKind { + loop { + match self.cursor.first() { + '\\' => { + // Consume an escaped newline so it doesn't terminate the command. A normal + // backslash is consumed by itself and remains part of the token range. + self.cursor.bump(); + self.cursor.eat_char('\r'); + self.cursor.eat_char('\n'); + } + '\n' | '\r' | EOF_CHAR => return TokenKind::IpyEscapeCommand, + _ => { + self.cursor.bump(); + } + } + } + } + + fn consume_end(&mut self) -> TokenKind { + // We reached end of file. + + // First, finish any unterminated interpolated-strings. + while let Some(interpolated_string) = self.interpolated_strings.pop() { + // An unterminated replacement field reports its innermost open bracket. + let error = match self.brackets.last() { + Some(bracket) if self.nesting > interpolated_string.nesting() => LexicalError::new( + LexicalErrorType::UnclosedBracket { + opening: bracket.kind, + incomplete: true, + }, + TextRange::empty(bracket.start), + ), + _ => LexicalError::new( + LexicalErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::UnterminatedString { + detected_line: self.detected_line(), + }, + interpolated_string.kind(), + ), + TextRange::empty(interpolated_string.start()), + ), + }; + self.reset_nesting(interpolated_string.nesting()); + self.push_error(error); + } + + // Second, finish all nestings. + // For Mode::ParenthesizedExpression we start with nesting level 1. + // So we check if we end with that level. + let init_nesting = u32::from(self.mode == Mode::ParenthesizedExpression); + + if self.nesting > init_nesting { + let error = self.eof_error(self.token_range()); + // Reset the nesting to avoid going into infinite loop. + self.nesting = 0; + self.brackets.clear(); + self.push_error(error); + self.current_range = self.token_range(); + return TokenKind::Unknown; + } + + // Next, insert a trailing newline, if required. + if !self.state.is_new_logical_line() { + self.state = State::AfterNewline; + TokenKind::Newline + } + // Next, flush the indentation stack to zero. + else if self.indentations.dedent().is_some() { + TokenKind::Dedent + } else { + TokenKind::EndOfFile + } + } + + /// Re-lex the [`NonLogicalNewline`] token at the given position in the context of a logical + /// line. + /// + /// Returns a boolean indicating whether the lexer's position has changed. This could result + /// into the new current token being different than the previous current token but is not + /// necessarily true. If the return value is `true` then the caller is responsible for updating + /// it's state accordingly. + /// + /// This method is a no-op if the lexer isn't in a parenthesized context. + /// + /// ## Explanation + /// + /// The lexer emits two different kinds of newline token based on the context. If it's in a + /// parenthesized context, it'll emit a [`NonLogicalNewline`] token otherwise it'll emit a + /// regular [`Newline`] token. Based on the type of newline token, the lexer will consume and + /// emit the indentation tokens appropriately which affects the structure of the code. + /// + /// For example: + /// ```py + /// if call(foo + /// def bar(): + /// pass + /// ``` + /// + /// Here, the lexer emits a [`NonLogicalNewline`] token after `foo` which means that the lexer + /// doesn't emit an `Indent` token before the `def` keyword. This leads to an AST which + /// considers the function `bar` as part of the module block and the `if` block remains empty. + /// + /// This method is to facilitate the parser if it recovers from these kind of scenarios so that + /// the lexer can then re-lex a [`NonLogicalNewline`] token to a [`Newline`] token which in + /// turn helps the parser to build the correct AST. + /// + /// In the above snippet, it would mean that this method would move the lexer back to the + /// newline character after the `foo` token and emit it as a [`Newline`] token instead of + /// [`NonLogicalNewline`]. This means that the next token emitted by the lexer would be an + /// `Indent` token. + /// + /// There are cases where the lexer's position will change but the re-lexed token will remain + /// the same. This is to help the parser to add the error message at an appropriate location. + /// Consider the following example: + /// + /// ```py + /// if call(foo, [a, b + /// def bar(): + /// pass + /// ``` + /// + /// Here, the parser recovers from two unclosed parenthesis. The inner unclosed `[` will call + /// into the re-lexing logic and reduce the nesting level from 2 to 1. And, the re-lexing logic + /// will move the lexer at the newline after `b` but still emit a [`NonLogicalNewline`] token. + /// Only after the parser recovers from the outer unclosed `(` does the re-lexing logic emit + /// the [`Newline`] token. + /// + /// [`Newline`]: TokenKind::Newline + /// [`NonLogicalNewline`]: TokenKind::NonLogicalNewline + pub(crate) fn re_lex_logical_token( + &mut self, + non_logical_newline_start: Option, + ) -> bool { + if self.nesting == 0 { + return false; + } + + // Reduce the nesting level because the parser recovered from an error inside list parsing + // i.e., it recovered from an unclosed parenthesis (`(`, `[`, or `{`). + self.nesting -= 1; + let unclosed = self.brackets.pop(); + + // The lexer can't be moved back for a triple-quoted f/t-string because the newlines are + // part of the f/t-string itself, so there is no newline token to be emitted. + if self.current_flags.is_triple_quoted_interpolated_string() { + return false; + } + + let Some(new_position) = non_logical_newline_start else { + return false; + }; + + // Earlier we reduced the nesting level unconditionally. Now that we know the lexer's + // position is going to be moved back, the lexer needs to be put back into a + // parenthesized context if the current token is a closing parenthesis. + // + // ```py + // (a, [b, + // c + // ) + // ``` + // + // Here, the parser would request to re-lex the token when it's at `)` and can recover + // from an unclosed `[`. This method will move the lexer back to the newline character + // after `c` which means it goes back into parenthesized context. + if matches!( + self.current_kind, + TokenKind::Rpar | TokenKind::Rsqb | TokenKind::Rbrace + ) { + self.nesting += 1; + self.brackets.extend(unclosed); + } + + self.cursor = Cursor::new(self.source); + self.cursor.skip_bytes(new_position.to_usize()); + self.state = State::Other; + self.next_token(); + true + } + + /// Re-lexes an unclosed string token in the context of an interpolated string element. + /// + /// ```py + /// f'{a' + /// ``` + /// + /// This method re-lexes the trailing `'` as the end of the f-string rather than the + /// start of a new string token for better error recovery. + pub(crate) fn re_lex_string_token_in_interpolation_element( + &mut self, + kind: InterpolatedStringKind, + ) { + let Some(interpolated_string) = self.interpolated_strings.current() else { + return; + }; + + let current_string_flags = self.current_flags().as_any_string_flags(); + + // Only unclosed strings, that have the same quote character + if !matches!(self.current_kind, TokenKind::String) + || !self.current_flags.is_unclosed() + || current_string_flags.prefix() != AnyStringPrefix::Regular(StringLiteralPrefix::Empty) + || current_string_flags.quote_style().as_char() != interpolated_string.quote_char() + || current_string_flags.is_triple_quoted() != interpolated_string.is_triple_quoted() + { + return; + } + + // Only if the string's first line only contains whitespace, + // or ends in a comment (not `f"{"abc`) + let first_line = &self.source + [(self.current_range.start() + current_string_flags.quote_len()).to_usize()..]; + + for c in first_line.chars() { + if matches!(c, '\n' | '\r' | '#') { + break; + } + + // `f'{'ab`, we want to parse `ab` as a normal string and not the closing element of the f-string + if !is_python_whitespace(c) { + return; + } + } + + if self.errors.last().is_some_and(|error| { + error.location().start() == self.current_range.start() + && error.error().is_unclosed_string_error() + }) { + self.errors.pop(); + } + + self.current_range = + TextRange::at(self.current_range.start(), self.current_flags.quote_len()); + self.current_kind = kind.end_token(); + self.current_flags = TokenFlags::empty(); + + self.reset_nesting(interpolated_string.nesting()); + self.interpolated_strings.pop(); + + self.cursor = Cursor::new(self.source); + self.cursor.skip_bytes(self.current_range.end().to_usize()); + } + + /// Re-lex `r"` in a format specifier position. + /// + /// `r"` in a format specifier position is unlikely to be the start of a raw string. + /// Instead, it's the format specifier `!r` followed by the closing quote of the f-string, + /// when the `}` is missing. + /// + /// ```py + /// f"{test!r" + /// ``` + /// + /// This function re-lexes the `r"` as `r` (an identifier token). The next `next_token` call will + /// return a unclosed string token for `"`, which [`Self::re_lex_string_token_in_interpolation_element`] + /// can then re-lex as the end of the f-string. + pub(crate) fn re_lex_raw_string_in_format_spec(&mut self) { + // Re-lex `r"` as `NAME r` followed by an unclosed string + // `f"{test!r"` -> `f"{test!`, `r`, `"` + if matches!(self.current_kind, TokenKind::String) + && self.current_flags.is_unclosed() + && self.current_flags.prefix() + == AnyStringPrefix::Regular(StringLiteralPrefix::Raw { uppercase: false }) + { + if self.errors.last().is_some_and(|error| { + error.location().start() == self.current_range.start() + && error.error().is_unclosed_string_error() + }) { + self.errors.pop(); + } + + self.current_range = TextRange::at(self.current_range.start(), 'r'.text_len()); + self.current_kind = TokenKind::Identifier; + self.current_flags = TokenFlags::empty(); + self.cursor = Cursor::new(self.source); + self.cursor.skip_bytes(self.current_range.end().to_usize()); + } + } + + #[inline] + fn token_range(&self) -> TextRange { + let end = self.offset(); + let len = self.cursor.token_len(); + + TextRange::at(end - len, len) + } + + #[inline] + fn token_text(&self) -> &'src str { + &self.source[self.token_range()] + } + + /// Retrieves the current offset of the cursor within the source code. + // SAFETY: Lexer doesn't allow files larger than 4GB + #[expect(clippy::cast_possible_truncation)] + #[inline] + pub(crate) fn offset(&self) -> TextSize { + TextSize::new(self.source.len() as u32) - self.cursor.text_len() + } + + /// Creates a checkpoint to which the lexer can later return to using [`Self::rewind`]. + pub(crate) fn checkpoint(&self) -> LexerCheckpoint { + LexerCheckpoint { + current_kind: self.current_kind, + current_range: self.current_range, + current_flags: self.current_flags, + cursor_offset: self.offset(), + state: self.state, + nesting: self.nesting, + brackets: self.brackets.clone(), + indentations_checkpoint: self.indentations.checkpoint(), + pending_indentation: self.pending_indentation, + interpolated_strings_checkpoint: self.interpolated_strings.checkpoint(), + errors_position: self.errors.len(), + } + } + + /// Restore the lexer to the given checkpoint. + pub(crate) fn rewind(&mut self, checkpoint: LexerCheckpoint) { + let LexerCheckpoint { + current_kind, + current_range, + current_flags, + cursor_offset, + state, + nesting, + brackets, + indentations_checkpoint, + pending_indentation, + interpolated_strings_checkpoint, + errors_position, + } = checkpoint; + + let mut cursor = Cursor::new(self.source); + // We preserve the previous char using this method. + cursor.skip_bytes(cursor_offset.to_usize()); + + self.current_kind = current_kind; + self.current_range = current_range; + self.current_flags = current_flags; + self.cursor = cursor; + self.state = state; + self.nesting = nesting; + self.brackets = brackets; + self.indentations.rewind(indentations_checkpoint); + self.pending_indentation = pending_indentation; + self.interpolated_strings + .rewind(interpolated_strings_checkpoint); + self.errors.truncate(errors_position); + } + + pub(crate) fn finish(self) -> Vec { + self.errors + } + + /// Returns the errors found so far. + pub(crate) fn errors(&self) -> &[LexicalError] { + &self.errors + } + + /// Returns `true` if the lexer is inside an f-string or t-string. + pub(crate) fn in_interpolated_string(&self) -> bool { + self.interpolated_strings.current().is_some() + } +} + +/// The maximum number of nested brackets. +const MAX_NESTING: u32 = 200; + +/// An opening bracket that is not closed yet. +#[derive(Copy, Clone, Debug)] +struct OpenBracket { + kind: char, + start: TextSize, +} + +/// Returns the opening bracket for the closing bracket `closing`. +const fn opening_bracket(closing: char) -> char { + match closing { + ')' => '(', + ']' => '[', + _ => '{', + } +} + +pub(crate) struct LexerCheckpoint { + current_kind: TokenKind, + current_range: TextRange, + current_flags: TokenFlags, + cursor_offset: TextSize, + state: State, + nesting: u32, + brackets: Vec, + indentations_checkpoint: IndentationsCheckpoint, + pending_indentation: Option, + interpolated_strings_checkpoint: InterpolatedStringsCheckpoint, + errors_position: usize, +} + +#[derive(Copy, Clone, Debug)] +enum State { + /// Lexer is right at the beginning of the file or after a `Newline` token. + AfterNewline, + + /// The lexer is at the start of a new logical line but **after** the indentation + NonEmptyLogicalLine, + + /// Lexer is right after an equal token + AfterEqual, + + /// Inside of a logical line + Other, +} + +impl State { + const fn is_after_newline(self) -> bool { + matches!(self, State::AfterNewline) + } + + const fn is_new_logical_line(self) -> bool { + matches!(self, State::AfterNewline | State::NonEmptyLogicalLine) + } + + const fn is_after_equal(self) -> bool { + matches!(self, State::AfterEqual) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum Radix { + Binary, + Octal, + Hex, +} + +impl Radix { + const fn kind(self) -> NumberLiteralKind { + match self { + Radix::Binary => NumberLiteralKind::Binary, + Radix::Octal => NumberLiteralKind::Octal, + Radix::Hex => NumberLiteralKind::Hexadecimal, + } + } + + const fn is_digit(self, c: char) -> bool { + match self { + Radix::Binary => matches!(c, '0'..='1'), + Radix::Octal => matches!(c, '0'..='7'), + Radix::Hex => c.is_ascii_hexdigit(), + } + } +} + +const fn is_quote(c: char) -> bool { + matches!(c, '\'' | '"') +} + +fn single_char_prefix(c: char) -> Option { + Some(match c { + 'f' | 'F' => TokenFlags::F_STRING, + 't' | 'T' => TokenFlags::T_STRING, + 'u' | 'U' => TokenFlags::UNICODE_STRING, + 'b' | 'B' => TokenFlags::BYTE_STRING, + 'r' => TokenFlags::RAW_STRING_LOWERCASE, + 'R' => TokenFlags::RAW_STRING_UPPERCASE, + _ => return None, + }) +} + +const fn is_ascii_identifier_start(c: char) -> bool { + matches!(c, 'a'..='z' | 'A'..='Z' | '_') +} + +// Checks if the character c is a valid starting character as described +// in https://docs.python.org/3/reference/lexical_analysis.html#identifiers +fn is_unicode_identifier_start(c: char) -> bool { + is_xid_start(c) +} + +/// Checks if the character c is a valid continuation character as described +/// in . +/// +/// Additionally, this function also keeps track of whether or not the total +/// identifier is ASCII-only or not by mutably altering a reference to a +/// boolean value passed in. +fn is_identifier_continuation(c: char, identifier_is_ascii_only: &mut bool) -> bool { + // Arrange things such that ASCII codepoints never + // result in the slower `is_xid_continue` getting called. + if c.is_ascii() { + matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '0'..='9') + } else { + let is_continuation = is_xid_continue(c); + if is_continuation { + *identifier_is_ascii_only = false; + } + is_continuation + } +} + +/// Create a new [`Lexer`] for the given source code and [`Mode`]. +#[doc(hidden)] +pub fn lex(source: &str, mode: Mode) -> Lexer<'_> { + Lexer::new(source, mode, TextSize::default()) +} + +#[cfg(test)] +mod tests { + use std::fmt::Write; + + use insta::assert_snapshot; + use ruff_python_ast::token::Token; + use ruff_text_size::Ranged; + + use super::*; + + const WINDOWS_EOL: &str = "\r\n"; + const MAC_EOL: &str = "\r"; + const UNIX_EOL: &str = "\n"; + + struct LexerOutput { + tokens: Vec, + errors: Vec, + } + + impl std::fmt::Display for LexerOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "## Tokens")?; + writeln!(f, "```\n{:#?}\n```", self.tokens)?; + if !self.errors.is_empty() { + writeln!(f, "## Errors")?; + writeln!(f, "```\n{:#?}\n```", self.errors)?; + } + Ok(()) + } + } + + fn lex(source: &str, mode: Mode, start_offset: TextSize) -> LexerOutput { + let mut lexer = Lexer::new(source, mode, start_offset); + let mut tokens = Vec::new(); + loop { + let kind = lexer.next_token(); + if kind.is_eof() { + break; + } + tokens.push(Token::new( + kind, + lexer.current_range(), + lexer.current_flags(), + )); + } + LexerOutput { + tokens, + errors: lexer.finish(), + } + } + + #[track_caller] + fn lex_valid(source: &str, mode: Mode, start_offset: TextSize) -> LexerOutput { + let output = lex(source, mode, start_offset); + + if !output.errors.is_empty() { + let mut message = "Unexpected lexical errors for a valid source:\n".to_string(); + for error in &output.errors { + writeln!(&mut message, "{error:?}").unwrap(); + } + writeln!(&mut message, "Source:\n{source}").unwrap(); + panic!("{message}"); + } + + output + } + + #[track_caller] + fn lex_invalid(source: &str, mode: Mode) -> LexerOutput { + let output = lex(source, mode, TextSize::default()); + + assert!( + !output.errors.is_empty(), + "Expected lexer to generate at least one error for the following source:\n{source}" + ); + + output + } + + #[track_caller] + fn lex_source(source: &str) -> LexerOutput { + lex_valid(source, Mode::Module, TextSize::default()) + } + + #[track_caller] + fn lex_source_with_offset(source: &str, start_offset: TextSize) -> LexerOutput { + lex_valid(source, Mode::Module, start_offset) + } + + #[track_caller] + fn lex_jupyter_source(source: &str) -> LexerOutput { + lex_valid(source, Mode::Ipython, TextSize::default()) + } + + #[test] + fn bom() { + let source = "\u{feff}x = 1"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn bom_with_offset() { + let source = "\u{feff}x + y + z"; + assert_snapshot!(lex_source_with_offset(source, TextSize::new(7))); + } + + #[test] + fn bom_with_offset_edge() { + // BOM offsets the first token by 3, so make sure that lexing from offset 11 (variable z) + // doesn't panic. Refer https://github.com/astral-sh/ruff/issues/11731 + let source = "\u{feff}x + y + z"; + assert_snapshot!(lex_source_with_offset(source, TextSize::new(11))); + } + + fn ipython_escape_command_line_continuation_eol(eol: &str) -> LexerOutput { + let source = format!("%matplotlib \\{eol} --inline"); + lex_jupyter_source(&source) + } + + #[test] + fn test_ipython_escape_command_line_continuation_unix_eol() { + assert_snapshot!(ipython_escape_command_line_continuation_eol(UNIX_EOL)); + } + + #[test] + fn test_ipython_escape_command_line_continuation_mac_eol() { + assert_snapshot!(ipython_escape_command_line_continuation_eol(MAC_EOL)); + } + + #[test] + fn test_ipython_escape_command_line_continuation_windows_eol() { + assert_snapshot!(ipython_escape_command_line_continuation_eol(WINDOWS_EOL)); + } + + fn ipython_escape_command_line_continuation_with_eol_and_eof(eol: &str) -> LexerOutput { + let source = format!("%matplotlib \\{eol}"); + lex_jupyter_source(&source) + } + + #[test] + fn test_ipython_escape_command_line_continuation_with_unix_eol_and_eof() { + assert_snapshot!(ipython_escape_command_line_continuation_with_eol_and_eof( + UNIX_EOL + )); + } + + #[test] + fn test_ipython_escape_command_line_continuation_with_mac_eol_and_eof() { + assert_snapshot!(ipython_escape_command_line_continuation_with_eol_and_eof( + MAC_EOL + )); + } + + #[test] + fn test_ipython_escape_command_line_continuation_with_windows_eol_and_eof() { + assert_snapshot!(ipython_escape_command_line_continuation_with_eol_and_eof( + WINDOWS_EOL + )); + } + + #[test] + fn test_empty_ipython_escape_command() { + let source = "%\n%%\n!\n!!\n?\n??\n/\n,\n;"; + assert_snapshot!(lex_jupyter_source(source)); + } + + #[test] + fn test_ipython_escape_command() { + let source = r" +?foo +??foo +%timeit a = b +%timeit a % 3 +%matplotlib \ + --inline +!pwd \ + && ls -a | sed 's/^/\\ /' +!!cd /Users/foo/Library/Application\ Support/ +/foo 1 2 +,foo 1 2 +;foo 1 2 +!ls +" + .trim(); + assert_snapshot!(lex_jupyter_source(source)); + } + + #[test] + fn test_ipython_help_end_escape_command() { + let source = r" +?foo? +?? foo? +?? foo ? +?foo?? +??foo?? +???foo? +???foo?? +??foo??? +???foo??? +?? \ + foo? +?? \ +? +???? +%foo? +%foo?? +%%foo??? +!pwd?" + .trim(); + assert_snapshot!(lex_jupyter_source(source)); + } + + #[test] + fn test_ipython_escape_command_indentation() { + let source = r" +if True: + %matplotlib \ + --inline" + .trim(); + assert_snapshot!(lex_jupyter_source(source)); + } + + #[test] + fn test_ipython_escape_command_assignment() { + let source = r" +pwd = !pwd +foo = %timeit a = b +bar = %timeit a % 3 +baz = %matplotlib \ + inline +qux = %foo? +quux = !pwd?" + .trim(); + assert_snapshot!(lex_jupyter_source(source)); + } + + fn assert_no_ipython_escape_command(tokens: &[Token]) { + for token in tokens { + if matches!(token.kind(), TokenKind::IpyEscapeCommand) { + panic!("Unexpected escape command token at {:?}", token.range()) + } + } + } + + #[test] + fn test_ipython_escape_command_not_an_assignment() { + let source = r" +# Other escape kinds are not valid here (can't test `foo = ?str` because '?' is not a valid token) +foo = /func +foo = ;func +foo = ,func + +(foo == %timeit a = b) +(foo := %timeit a = b) +def f(arg=%timeit a = b): + pass" + .trim(); + let output = lex(source, Mode::Ipython, TextSize::default()); + assert!(output.errors.is_empty()); + assert_no_ipython_escape_command(&output.tokens); + } + + #[test] + fn test_numbers() { + let source = "0x2f 0o12 0b1101 0 123 123_45_67_890 0.2 1e+2 2.1e3 2j 2.2j 000 0x995DC9BBDF1939FA 0x995DC9BBDF1939FA995DC9BBDF1939FA"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_invalid_leading_zero_small() { + let source = "025"; + assert_snapshot!(lex_invalid(source, Mode::Module)); + } + + #[test] + fn test_invalid_leading_zero_big() { + let source = + "0252222222222222522222222222225222222222222252222222222222522222222222225222222222222"; + assert_snapshot!(lex_invalid(source, Mode::Module)); + } + + #[test] + fn test_line_comment_long() { + let source = "99232 # foo".to_string(); + assert_snapshot!(lex_source(&source)); + } + + #[test] + fn test_line_comment_whitespace() { + let source = "99232 # ".to_string(); + assert_snapshot!(lex_source(&source)); + } + + #[test] + fn test_line_comment_single_whitespace() { + let source = "99232 # ".to_string(); + assert_snapshot!(lex_source(&source)); + } + + #[test] + fn test_line_comment_empty() { + let source = "99232 #".to_string(); + assert_snapshot!(lex_source(&source)); + } + + fn comment_until_eol(eol: &str) -> LexerOutput { + let source = format!("123 # Foo{eol}456"); + lex_source(&source) + } + + #[test] + fn test_comment_until_unix_eol() { + assert_snapshot!(comment_until_eol(UNIX_EOL)); + } + + #[test] + fn test_comment_until_mac_eol() { + assert_snapshot!(comment_until_eol(MAC_EOL)); + } + + #[test] + fn test_comment_until_windows_eol() { + assert_snapshot!(comment_until_eol(WINDOWS_EOL)); + } + + #[test] + fn test_assignment() { + let source = r"a_variable = 99 + 2-0"; + assert_snapshot!(lex_source(source)); + } + + fn indentation_with_eol(eol: &str) -> LexerOutput { + let source = format!("def foo():{eol} return 99{eol}{eol}"); + lex_source(&source) + } + + #[test] + fn test_indentation_with_unix_eol() { + assert_snapshot!(indentation_with_eol(UNIX_EOL)); + } + + #[test] + fn test_indentation_with_mac_eol() { + assert_snapshot!(indentation_with_eol(MAC_EOL)); + } + + #[test] + fn test_indentation_with_windows_eol() { + assert_snapshot!(indentation_with_eol(WINDOWS_EOL)); + } + + fn double_dedent_with_eol(eol: &str) -> LexerOutput { + let source = format!("def foo():{eol} if x:{eol}{eol} return 99{eol}{eol}"); + lex_source(&source) + } + + #[test] + fn test_double_dedent_with_unix_eol() { + assert_snapshot!(double_dedent_with_eol(UNIX_EOL)); + } + + #[test] + fn test_double_dedent_with_mac_eol() { + assert_snapshot!(double_dedent_with_eol(MAC_EOL)); + } + + #[test] + fn test_double_dedent_with_windows_eol() { + assert_snapshot!(double_dedent_with_eol(WINDOWS_EOL)); + } + + fn double_dedent_with_tabs_eol(eol: &str) -> LexerOutput { + let source = format!("def foo():{eol}\tif x:{eol}{eol}\t\t return 99{eol}{eol}"); + lex_source(&source) + } + + #[test] + fn test_double_dedent_with_tabs_unix_eol() { + assert_snapshot!(double_dedent_with_tabs_eol(UNIX_EOL)); + } + + #[test] + fn test_double_dedent_with_tabs_mac_eol() { + assert_snapshot!(double_dedent_with_tabs_eol(MAC_EOL)); + } + + #[test] + fn test_double_dedent_with_tabs_windows_eol() { + assert_snapshot!(double_dedent_with_tabs_eol(WINDOWS_EOL)); + } + + #[test] + fn dedent_after_whitespace() { + let source = "\ +if first: + if second: + pass + foo +"; + assert_snapshot!(lex_source(source)); + } + + fn newline_in_brackets_eol(eol: &str) -> LexerOutput { + let source = r"x = [ + + 1,2 +,(3, +4, +), { +5, +6,\ +7}] +" + .replace('\n', eol); + lex_source(&source) + } + + #[test] + fn test_newline_in_brackets_unix_eol() { + assert_snapshot!(newline_in_brackets_eol(UNIX_EOL)); + } + + #[test] + fn test_newline_in_brackets_mac_eol() { + assert_snapshot!(newline_in_brackets_eol(MAC_EOL)); + } + + #[test] + fn test_newline_in_brackets_windows_eol() { + assert_snapshot!(newline_in_brackets_eol(WINDOWS_EOL)); + } + + #[test] + fn test_non_logical_newline_in_string_continuation() { + let source = r"( + 'a' + 'b' + + 'c' \ + 'd' +)"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_logical_newline_line_comment() { + let source = "#Hello\n#World\n"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_operators() { + let source = "//////=/ /"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_string() { + let source = r#""double" 'single' 'can\'t' "\\\"" '\t\r\n' '\g' r'raw\'' '\420' '\200\0a'"#; + assert_snapshot!(lex_source(source)); + } + + fn string_continuation_with_eol(eol: &str) -> LexerOutput { + let source = format!("\"abc\\{eol}def\""); + lex_source(&source) + } + + #[test] + fn test_string_continuation_with_unix_eol() { + assert_snapshot!(string_continuation_with_eol(UNIX_EOL)); + } + + #[test] + fn test_string_continuation_with_mac_eol() { + assert_snapshot!(string_continuation_with_eol(MAC_EOL)); + } + + #[test] + fn test_string_continuation_with_windows_eol() { + assert_snapshot!(string_continuation_with_eol(WINDOWS_EOL)); + } + + #[test] + fn test_escape_unicode_name() { + let source = r#""\N{EN SPACE}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn non_ascii_identifier_flag() { + let mut lexer = Lexer::new("a€\naβ = β\nascii", Mode::Module, TextSize::default()); + let mut flags = Vec::new(); + loop { + let kind = lexer.next_token(); + if kind.is_eof() { + break; + } + if kind == TokenKind::Identifier { + flags.push(lexer.current_flags().is_non_ascii_identifier()); + } + } + + assert_eq!(lexer.finish().len(), 1); + assert_eq!(flags, [false, true, true, false]); + } + + fn triple_quoted_eol(eol: &str) -> LexerOutput { + let source = format!("\"\"\"{eol} test string{eol} \"\"\""); + lex_source(&source) + } + + #[test] + fn test_triple_quoted_unix_eol() { + assert_snapshot!(triple_quoted_eol(UNIX_EOL)); + } + + #[test] + fn test_triple_quoted_mac_eol() { + assert_snapshot!(triple_quoted_eol(MAC_EOL)); + } + + #[test] + fn test_triple_quoted_windows_eol() { + assert_snapshot!(triple_quoted_eol(WINDOWS_EOL)); + } + + fn line_continuation_at_eof_after_newline(eol: &str) -> LexerOutput { + let source = format!(r"\{eol}"); + lex_invalid(&source, Mode::Module) + } + + #[test] + fn test_line_continuation_at_eof_after_newline_unix_eol() { + assert_snapshot!(line_continuation_at_eof_after_newline(UNIX_EOL)); + } + + #[test] + fn test_line_continuation_at_eof_after_newline_mac_eol() { + assert_snapshot!(line_continuation_at_eof_after_newline(MAC_EOL)); + } + + #[test] + fn test_line_continuation_at_eof_after_newline_windows_eol() { + assert_snapshot!(line_continuation_at_eof_after_newline(WINDOWS_EOL)); + } + + fn line_continuation_at_eof(eol: &str) -> LexerOutput { + let source = format!(r"1, \{eol}"); + lex_invalid(&source, Mode::Module) + } + + #[test] + fn test_line_continuation_at_eof_unix_eol() { + assert_snapshot!(line_continuation_at_eof(UNIX_EOL)); + } + + #[test] + fn test_line_continuation_at_eof_mac_eol() { + assert_snapshot!(line_continuation_at_eof(MAC_EOL)); + } + + #[test] + fn test_line_continuation_at_eof_windows_eol() { + assert_snapshot!(line_continuation_at_eof(WINDOWS_EOL)); + } + + // This test case is to just make sure that the lexer doesn't go into + // infinite loop on invalid input. + #[test] + fn test_infinite_loop() { + let source = "[1"; + lex_invalid(source, Mode::Module); + } + + /// Emoji identifiers are a non-standard python feature and are not supported by our lexer. + #[test] + fn test_emoji_identifier() { + let source = "🐦"; + assert_snapshot!(lex_invalid(source, Mode::Module)); + } + + #[test] + fn tet_too_low_dedent() { + let source = "if True: + pass + pass"; + assert_snapshot!(lex_invalid(source, Mode::Module)); + } + + #[test] + fn test_empty_fstrings() { + let source = r#"f"" "" F"" f'' '' f"""""" f''''''"#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_prefix() { + let source = r#"f"" F"" rf"" rF"" Rf"" RF"" fr"" Fr"" fR"" FR"""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring() { + let source = r#"f"normal {foo} {{another}} {bar} {{{three}}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_parentheses() { + let source = r#"f"{}" f"{{}}" f" {}" f"{{{}}}" f"{{{{}}}}" f" {} {{}} {{{}}} {{{{}}}} ""#; + assert_snapshot!(lex_source(source)); + } + + fn fstring_single_quote_escape_eol(eol: &str) -> LexerOutput { + let source = format!(r"f'text \{eol} more text'"); + lex_source(&source) + } + + #[test] + fn test_fstring_single_quote_escape_unix_eol() { + assert_snapshot!(fstring_single_quote_escape_eol(UNIX_EOL)); + } + + #[test] + fn test_fstring_single_quote_escape_mac_eol() { + assert_snapshot!(fstring_single_quote_escape_eol(MAC_EOL)); + } + + #[test] + fn test_fstring_single_quote_escape_windows_eol() { + assert_snapshot!(fstring_single_quote_escape_eol(WINDOWS_EOL)); + } + + #[test] + fn test_fstring_escape() { + let source = r#"f"\{x:\"\{x}} \"\"\ + end""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_escape_braces() { + let source = r"f'\{foo}' f'\\{foo}' f'\{{foo}}' f'\\{{foo}}'"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_escape_raw() { + let source = r#"rf"\{x:\"\{x}} \"\"\ + end""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_named_unicode() { + let source = r#"f"\N{BULLET} normal \Nope \N""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_named_unicode_raw() { + let source = r#"rf"\N{BULLET} normal""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_with_named_expression() { + let source = r#"f"{x:=10} {(x:=10)} {x,{y:=10}} {[x:=10]}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_with_format_spec() { + let source = r#"f"{foo:} {x=!s:.3f} {x:.{y}f} {'':*^{1:{1}}} {x:{{1}.pop()}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_with_multiline_format_spec() { + // The last f-string is invalid syntactically but we should still lex it. + // Note that the `b` is an `Identifier` token and not a `FStringMiddle` token. + let source = r"f'''__{ + x:d +}__''' +f'''__{ + x:a + b + c +}__''' +"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_newline_format_spec() { + let source = r" +f'__{ + x:d +}__' +f'__{ + x:a + b +}__' +"; + assert_snapshot!(lex_invalid(source, Mode::Module)); + } + + #[test] + fn test_fstring_conversion() { + let source = r#"f"{x!s} {x=!r} {x:.3f!r} {{x!r}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_nested() { + let source = r#"f"foo {f"bar {x + f"{wow}"}"} baz" f'foo {f'bar'} some {f"another"}'"#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_expression_multiline() { + let source = r#"f"first { + x + * + y +} second""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_multiline() { + let source = r#"f""" +hello + world +""" f''' + world +hello +''' f"some {f"""multiline +allowed {x}"""} string""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_comments() { + let source = r#"f""" +# not a comment { # comment { + x +} # not a comment +""""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_with_ipy_escape_command() { + let source = r#"f"foo {!pwd} bar""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_with_lambda_expression() { + let source = r#" +f"{lambda x:{x}}" +f"{(lambda x:{x})}" +"# + .trim(); + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_with_nul_char() { + let source = r"f'\0'"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_empty_tstrings() { + let source = r#"t"" "" t"" t'' '' t"""""" t''''''"#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_prefix() { + let source = r#"t"" t"" rt"" rt"" Rt"" Rt"" tr"" Tr"" tR"" TR"""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring() { + let source = r#"t"normal {foo} {{another}} {bar} {{{three}}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_parentheses() { + let source = r#"t"{}" t"{{}}" t" {}" t"{{{}}}" t"{{{{}}}}" t" {} {{}} {{{}}} {{{{}}}} ""#; + assert_snapshot!(lex_source(source)); + } + + fn tstring_single_quote_escape_eol(eol: &str) -> LexerOutput { + let source = format!(r"t'text \{eol} more text'"); + lex_source(&source) + } + + #[test] + fn test_tstring_single_quote_escape_unix_eol() { + assert_snapshot!(tstring_single_quote_escape_eol(UNIX_EOL)); + } + + #[test] + fn test_tstring_single_quote_escape_mac_eol() { + assert_snapshot!(tstring_single_quote_escape_eol(MAC_EOL)); + } + + #[test] + fn test_tstring_single_quote_escape_windows_eol() { + assert_snapshot!(tstring_single_quote_escape_eol(WINDOWS_EOL)); + } + + #[test] + fn test_tstring_escape() { + let source = r#"t"\{x:\"\{x}} \"\"\ + end""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_escape_braces() { + let source = r"t'\{foo}' t'\\{foo}' t'\{{foo}}' t'\\{{foo}}'"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_escape_raw() { + let source = r#"rt"\{x:\"\{x}} \"\"\ + end""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_named_unicode() { + let source = r#"t"\N{BULLET} normal \Nope \N""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_named_unicode_raw() { + let source = r#"rt"\N{BULLET} normal""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_named_expression() { + let source = r#"t"{x:=10} {(x:=10)} {x,{y:=10}} {[x:=10]}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_format_spec() { + let source = r#"t"{foo:} {x=!s:.3f} {x:.{y}f} {'':*^{1:{1}}} {x:{{1}.pop()}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_multiline_format_spec() { + // The last t-string is invalid syntactically but we should still lex it. + // Note that the `b` is an `Identifier` token and not a `TStringMiddle` token. + let source = r"t'''__{ + x:d +}__''' +t'''__{ + x:a + b + c +}__''' +"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_newline_format_spec() { + let source = r" +t'__{ + x:d +}__' +t'__{ + x:a + b +}__' +"; + assert_snapshot!(lex_invalid(source, Mode::Module)); + } + + #[test] + fn test_tstring_conversion() { + let source = r#"t"{x!s} {x=!r} {x:.3f!r} {{x!r}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_nested() { + let source = r#"t"foo {t"bar {x + t"{wow}"}"} baz" t'foo {t'bar'} some {t"another"}'"#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_expression_multiline() { + let source = r#"t"first { + x + * + y +} second""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_multiline() { + let source = r#"t""" +hello + world +""" t''' + world +hello +''' t"some {t"""multiline +allowed {x}"""} string""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_comments() { + let source = r#"t""" +# not a comment { # comment { + x +} # not a comment +""""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_ipy_escape_command() { + let source = r#"t"foo {!pwd} bar""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_lambda_expression() { + let source = r#" +t"{lambda x:{x}}" +t"{(lambda x:{x})}" +"# + .trim(); + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_nul_char() { + let source = r"t'\0'"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_nested_t_and_fstring() { + let source = r#"t"foo {f"bar {x + t"{wow}"}"} baz" f'foo {t'bar'!r} some {f"another"}'"#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_match_softkeyword_in_notebook() { + let source = r"match foo: + case bar: + pass"; + assert_snapshot!(lex_jupyter_source(source)); + } + + fn lex_fstring_error(source: &str) -> InterpolatedStringErrorType { + let output = lex(source, Mode::Module, TextSize::default()); + match output + .errors + .into_iter() + .next() + .expect("lexer should give at least one error") + .into_error() + { + LexicalErrorType::FStringError(error) => error, + err => panic!("Expected FStringError: {err:?}"), + } + } + + #[test] + fn test_fstring_error() { + use InterpolatedStringErrorType::{ + SingleRbrace, UnterminatedString, UnterminatedTripleQuotedString, + }; + + assert_eq!(lex_fstring_error("f'}'"), SingleRbrace); + assert_eq!(lex_fstring_error("f'{{}'"), SingleRbrace); + assert_eq!(lex_fstring_error("f'{{}}}'"), SingleRbrace); + assert_eq!(lex_fstring_error("f'foo}'"), SingleRbrace); + assert_eq!(lex_fstring_error(r"f'\u007b}'"), SingleRbrace); + assert_eq!(lex_fstring_error("f'{a:b}}'"), SingleRbrace); + assert_eq!(lex_fstring_error("f'{3:}}>10}'"), SingleRbrace); + assert_eq!(lex_fstring_error(r"f'\{foo}\}'"), SingleRbrace); + + assert!(matches!( + lex_fstring_error(r#"f""#), + UnterminatedString { .. } + )); + assert!(matches!( + lex_fstring_error(r"f'"), + UnterminatedString { .. } + )); + + assert!(matches!( + lex_fstring_error(r#"f""""#), + UnterminatedTripleQuotedString { .. } + )); + assert!(matches!( + lex_fstring_error(r"f'''"), + UnterminatedTripleQuotedString { .. } + )); + assert!(matches!( + lex_fstring_error(r#"f"""""#), + UnterminatedTripleQuotedString { .. } + )); + assert!(matches!( + lex_fstring_error(r#"f""""""#), + UnterminatedTripleQuotedString { .. } + )); + } + + fn lex_tstring_error(source: &str) -> InterpolatedStringErrorType { + let output = lex(source, Mode::Module, TextSize::default()); + match output + .errors + .into_iter() + .next() + .expect("lexer should give at least one error") + .into_error() + { + LexicalErrorType::TStringError(error) => error, + err => panic!("Expected TStringError: {err:?}"), + } + } + + #[test] + fn lex_fstring_unclosed() { + let source = r#"f"hello"#; + + assert_snapshot!(lex_invalid(source, Mode::Module), @" + ## Tokens + ``` + [ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 2..7 (flags = DOUBLE_QUOTES | F_STRING), + Newline 7..7, + ] + ``` + ## Errors + ``` + [ + LexicalError { + error: FStringError( + UnterminatedString { + detected_line: 1, + }, + ), + location: 0..0, + }, + ] + ``` + "); + } + + #[test] + fn lex_fstring_missing_brace() { + let source = "f'{'"; + + assert_snapshot!(lex_invalid(source, Mode::Module), @" + ## Tokens + ``` + [ + FStringStart 0..2 (flags = F_STRING), + Lbrace 2..3, + String 3..4 (flags = UNCLOSED_STRING), + Newline 4..4, + ] + ``` + ## Errors + ``` + [ + LexicalError { + error: FStringError( + UnclosedLbrace, + ), + location: 3..3, + }, + LexicalError { + error: UnclosedBracket { + opening: '{', + incomplete: true, + }, + location: 2..2, + }, + ] + ``` + "); + } + + #[test] + fn lex_fstring_missing_brace_after_format_spec() { + let source = r#"f"{foo!r""#; + + assert_snapshot!(lex_invalid(source, Mode::Module), @" + ## Tokens + ``` + [ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 2..3, + Identifier 3..6, + Exclamation 6..7, + String 7..9 (flags = DOUBLE_QUOTES | RAW_STRING_LOWERCASE | UNCLOSED_STRING), + Newline 9..9, + ] + ``` + ## Errors + ``` + [ + LexicalError { + error: FStringError( + UnclosedLbrace, + ), + location: 7..7, + }, + LexicalError { + error: UnclosedBracket { + opening: '{', + incomplete: true, + }, + location: 2..2, + }, + ] + ``` + "); + } + + #[test] + fn test_tstring_error() { + use InterpolatedStringErrorType::{ + SingleRbrace, UnterminatedString, UnterminatedTripleQuotedString, + }; + + assert_eq!(lex_tstring_error("t'}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{{}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{{}}}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'foo}'"), SingleRbrace); + assert_eq!(lex_tstring_error(r"t'\u007b}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{a:b}}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{3:}}>10}'"), SingleRbrace); + assert_eq!(lex_tstring_error(r"t'\{foo}\}'"), SingleRbrace); + + assert!(matches!( + lex_tstring_error(r#"t""#), + UnterminatedString { .. } + )); + assert!(matches!( + lex_tstring_error(r"t'"), + UnterminatedString { .. } + )); + + assert!(matches!( + lex_tstring_error(r#"t""""#), + UnterminatedTripleQuotedString { .. } + )); + assert!(matches!( + lex_tstring_error(r"t'''"), + UnterminatedTripleQuotedString { .. } + )); + assert!(matches!( + lex_tstring_error(r#"t"""""#), + UnterminatedTripleQuotedString { .. } + )); + assert!(matches!( + lex_tstring_error(r#"t""""""#), + UnterminatedTripleQuotedString { .. } + )); + } + + #[test] + fn backslash_continuation_indentation() { + // The first `\` has 4 spaces before it which matches the indentation level at that point, + // so the whitespace before `2` is irrelevant and shouldn't produce an indentation error. + // Similarly, the second `\` is also at the same indentation level, so the `3` line is also + // valid. + let source = r"if True: + 1 + \ + 2 + \ +3 +else: + pass +" + .to_string(); + assert_snapshot!(lex_source(&source)); + } + + #[test] + fn backslash_continuation_at_root() { + // But, it's a different when the backslash character itself is at the root indentation + // level. Then, the whitespaces following it determines the indentation level of the next + // line, so `1` is indented with 4 spaces and `2` is indented with 8 spaces, and `3` is + // indented with 4 spaces, all of which are valid. + let source = r"if True: +\ + 1 + if True: +\ + 2 +else:\ + 3 +" + .to_string(); + assert_snapshot!(lex_source(&source)); + } + + #[test] + fn multiple_backslash_continuation() { + // It's only the first backslash character that determines the indentation level of the next + // line, so all the lines after the first `\` are indented with 4 spaces, and the remaining + // backslashes are just ignored and don't affect the indentation level. + let source = r"if True: + 1 + \ + \ + \ + \ + 2 +" + .to_string(); + assert_snapshot!(lex_source(&source)); + } + + #[test] + fn backslash_continuation_mismatch_indentation() { + // Indentation doesn't match any previous indentation level + let source = r"if True: + 1 + \ + 2 +" + .to_string(); + assert_snapshot!(lex_invalid(&source, Mode::Module)); + } +} diff --git a/vendor/rustpython-ruff_python_parser/src/lexer/cursor.rs b/vendor/rustpython-ruff_python_parser/src/lexer/cursor.rs new file mode 100644 index 00000000000..413d648ca00 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/lexer/cursor.rs @@ -0,0 +1,178 @@ +use std::str::Chars; + +use ruff_text_size::{TextLen, TextSize}; + +pub(crate) const EOF_CHAR: char = '\0'; + +/// A cursor represents a pointer in the source code. +/// +/// Based on [`rustc`'s `Cursor`](https://github.com/rust-lang/rust/blob/d1b7355d3d7b4ead564dbecb1d240fcc74fff21b/compiler/rustc_lexer/src/cursor.rs) +#[derive(Clone, Debug)] +pub(super) struct Cursor<'src> { + /// An iterator over the [`char`]'s of the source code. + chars: Chars<'src>, + + /// Length of the source code. This is used as a marker to indicate the start of the current + /// token which is being lexed. + source_length: TextSize, + + /// Stores the previous character for debug assertions. + #[cfg(debug_assertions)] + prev_char: char, +} + +impl<'src> Cursor<'src> { + pub(crate) fn new(source: &'src str) -> Self { + Self { + source_length: source.text_len(), + chars: source.chars(), + #[cfg(debug_assertions)] + prev_char: EOF_CHAR, + } + } + + /// Returns the previous character. Useful for debug assertions. + #[cfg(debug_assertions)] + pub(super) const fn previous(&self) -> char { + self.prev_char + } + + /// Peeks the next character from the input stream without consuming it. + /// Returns [`EOF_CHAR`] if the position is past the end of the file. + pub(super) fn first(&self) -> char { + self.chars.clone().next().unwrap_or(EOF_CHAR) + } + + /// Peeks the second character from the input stream without consuming it. + /// Returns [`EOF_CHAR`] if the position is past the end of the file. + pub(super) fn second(&self) -> char { + let mut chars = self.chars.clone(); + chars.next(); + chars.next().unwrap_or(EOF_CHAR) + } + + /// Returns the remaining text to lex. + /// + /// Use [`Cursor::text_len`] to get the length of the remaining text. + pub(super) fn rest(&self) -> &'src str { + self.chars.as_str() + } + + /// Returns the length of the remaining text. + /// + /// Use [`Cursor::rest`] to get the remaining text. + // SAFETY: The `source.text_len` call in `new` would panic if the string length is larger than a `u32`. + #[expect(clippy::cast_possible_truncation)] + pub(super) fn text_len(&self) -> TextSize { + TextSize::new(self.chars.as_str().len() as u32) + } + + /// Returns the length of the current token length. + /// + /// This is to be used after setting the start position of the token using + /// [`Cursor::start_token`]. + pub(super) fn token_len(&self) -> TextSize { + self.source_length - self.text_len() + } + + /// Mark the current position of the cursor as the start of the token which is going to be + /// lexed. + /// + /// Use [`Cursor::token_len`] to get the length of the lexed token. + pub(super) fn start_token(&mut self) { + self.source_length = self.text_len(); + } + + /// Returns `true` if the cursor is at the end of file. + pub(super) fn is_eof(&self) -> bool { + self.chars.as_str().is_empty() + } + + /// Moves the cursor to the next character, returning the previous character. + /// Returns [`None`] if there is no next character. + pub(super) fn bump(&mut self) -> Option { + let prev = self.chars.next()?; + + #[cfg(debug_assertions)] + { + self.prev_char = prev; + } + + Some(prev) + } + + pub(super) fn eat_char(&mut self, c: char) -> bool { + if self.first() == c { + self.bump(); + true + } else { + false + } + } + + pub(super) fn eat_char2(&mut self, c1: char, c2: char) -> bool { + let mut chars = self.chars.clone(); + if chars.next() == Some(c1) && chars.next() == Some(c2) { + self.bump(); + self.bump(); + true + } else { + false + } + } + + pub(super) fn eat_char3(&mut self, c1: char, c2: char, c3: char) -> bool { + let mut chars = self.chars.clone(); + if chars.next() == Some(c1) && chars.next() == Some(c2) && chars.next() == Some(c3) { + self.bump(); + self.bump(); + self.bump(); + true + } else { + false + } + } + + pub(super) fn eat_if(&mut self, mut predicate: F) -> Option + where + F: FnMut(char) -> bool, + { + if predicate(self.first()) && !self.is_eof() { + self.bump() + } else { + None + } + } + + /// Eats symbols while predicate returns true or until the end of file is reached. + #[inline] + pub(super) fn eat_while(&mut self, mut predicate: impl FnMut(char) -> bool) { + // It was tried making optimized version of this for eg. line comments, but + // LLVM can inline all of this and compile it down to fast iteration over bytes. + while predicate(self.first()) && !self.is_eof() { + self.bump(); + } + } + + /// Skips the next `count` bytes. + /// + /// ## Panics + /// - If `count` is larger than the remaining bytes in the input stream. + /// - If `count` indexes into a multi-byte character. + pub(super) fn skip_bytes(&mut self, count: usize) { + #[cfg(debug_assertions)] + { + self.prev_char = self.chars.as_str()[..count] + .chars() + .next_back() + .unwrap_or('\0'); + } + + self.chars = self.chars.as_str()[count..].chars(); + } + + /// Skips to the end of the input stream. + pub(super) fn skip_to_end(&mut self) { + self.chars = "".chars(); + } +} diff --git a/vendor/rustpython-ruff_python_parser/src/lexer/indentation.rs b/vendor/rustpython-ruff_python_parser/src/lexer/indentation.rs new file mode 100644 index 00000000000..89071578e59 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/lexer/indentation.rs @@ -0,0 +1,202 @@ +use static_assertions::assert_eq_size; +use std::cmp::Ordering; +use std::fmt::Debug; + +use ruff_python_trivia::tab_offset_u32; + +/// The column index of an indentation. +/// +/// A space increments the column by one. A tab advances the column to the next multiple of 8. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default)] +pub(super) struct Column(u32); + +impl Column { + const fn new(column: u32) -> Self { + Self(column) + } +} + +/// The number of characters in an indentation. Each character accounts for 1. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default)] +pub(super) struct Character(u32); + +impl Character { + const fn new(characters: u32) -> Self { + Self(characters) + } +} + +/// The [Indentation](https://docs.python.org/3/reference/lexical_analysis.html#indentation) of a logical line. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] +pub(super) struct Indentation { + column: Column, + character: Character, +} + +impl Indentation { + const TAB_SIZE: u32 = 8; + + pub(super) const fn root() -> Self { + Self { + column: Column::new(0), + character: Character::new(0), + } + } + + #[cfg(test)] + const fn new(column: Column, character: Character) -> Self { + Self { column, character } + } + + #[must_use] + pub(super) fn add_space(self) -> Self { + Self { + character: Character(self.character.0 + 1), + column: Column(self.column.0 + 1), + } + } + + #[must_use] + pub(super) fn add_tab(self) -> Self { + Self { + character: Character(self.character.0 + 1), + // Compute the column index: + // * Adds `TAB_SIZE` if `column` is a multiple of `TAB_SIZE` + // * Rounds `column` up to the next multiple of `TAB_SIZE` otherwise. + // https://github.com/python/cpython/blob/2cf99026d6320f38937257da1ab014fc873a11a6/Parser/tokenizer.c#L1818 + column: Column(self.column.0 + tab_offset_u32(self.column.0, Self::TAB_SIZE)), + } + } + + /// Compares the current indentation `self` with the indentation `other` of a new line by + /// column. + /// + /// A deeper line must also have more characters, and a line at the same column the same + /// number of characters; otherwise tabs and spaces are mixed inconsistently. A shallower line + /// is checked against the level it dedents to, in [`Indentations::dedent_one`]. + pub(super) fn try_compare(self, other: Indentation) -> Result { + let ordering = self.column.cmp(&other.column); + let consistent = match ordering { + Ordering::Less => self.character < other.character, + Ordering::Equal => self.character == other.character, + Ordering::Greater => true, + }; + + if consistent { + Ok(ordering) + } else { + Err(IndentationError::InconsistentTabs) + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub(super) enum IndentationError { + /// Tabs and spaces are mixed in a way that makes the indentation depend on the tab size. + InconsistentTabs, + /// A dedent does not match any outer indentation level. + UnmatchedDedent, + /// Indenting would exceed [`Indentations::MAX_DEPTH`]. + TooDeep, +} + +/// The indentations stack is used to keep track of the current indentation level +/// [See Indentation](docs.python.org/3/reference/lexical_analysis.html#indentation). +#[derive(Debug, Clone, Default)] +pub(super) struct Indentations { + stack: Vec, +} + +impl Indentations { + /// The maximum number of nested indentation levels. + const MAX_DEPTH: usize = 99; + + pub(super) fn indent(&mut self, indent: Indentation) -> Result<(), IndentationError> { + debug_assert_eq!(self.current().try_compare(indent), Ok(Ordering::Less)); + + if self.stack.len() >= Self::MAX_DEPTH { + return Err(IndentationError::TooDeep); + } + self.stack.push(indent); + Ok(()) + } + + /// Dedent one level to eventually reach `new_indentation`. + /// + /// Returns `Err` if the `new_indentation` is greater than the new current indentation level. + pub(super) fn dedent_one( + &mut self, + new_indentation: Indentation, + ) -> Result, IndentationError> { + let previous = self.dedent(); + let current = *self.current(); + + match new_indentation.column.cmp(¤t.column) { + Ordering::Less => Ok(previous), + Ordering::Equal if new_indentation.character == current.character => Ok(previous), + Ordering::Equal => Err(IndentationError::InconsistentTabs), + // ```python + // if True: + // pass + // pass <- The indentation is greater than the expected indent of 0. + // ``` + Ordering::Greater => Err(IndentationError::UnmatchedDedent), + } + } + + pub(super) fn dedent(&mut self) -> Option { + self.stack.pop() + } + + pub(super) fn current(&self) -> &Indentation { + static ROOT: Indentation = Indentation::root(); + self.stack.last().unwrap_or(&ROOT) + } + + pub(crate) fn checkpoint(&self) -> IndentationsCheckpoint { + IndentationsCheckpoint(self.stack.clone()) + } + + pub(crate) fn rewind(&mut self, checkpoint: IndentationsCheckpoint) { + self.stack = checkpoint.0; + } +} + +#[derive(Debug, Clone)] +pub(crate) struct IndentationsCheckpoint(Vec); + +assert_eq_size!(Indentation, u64); + +#[cfg(test)] +mod tests { + use super::{Character, Column, Indentation, IndentationError}; + use std::cmp::Ordering; + + #[test] + fn indentation_try_compare() { + let tab = Indentation::new(Column::new(8), Character::new(1)); + + assert_eq!(tab.try_compare(tab), Ok(Ordering::Equal)); + + let two_tabs = Indentation::new(Column::new(16), Character::new(2)); + assert_eq!(two_tabs.try_compare(tab), Ok(Ordering::Greater)); + assert_eq!(tab.try_compare(two_tabs), Ok(Ordering::Less)); + } + + #[test] + fn indentation_try_compare_mixed_tabs() { + let tab = Indentation::new(Column::new(8), Character::new(1)); + let eight_spaces = Indentation::new(Column::new(8), Character::new(8)); + let tab_and_space = Indentation::new(Column::new(9), Character::new(2)); + + assert_eq!( + tab.try_compare(eight_spaces), + Err(IndentationError::InconsistentTabs) + ); + assert_eq!( + eight_spaces.try_compare(tab_and_space), + Err(IndentationError::InconsistentTabs) + ); + assert_eq!(tab.try_compare(tab_and_space), Ok(Ordering::Less)); + } +} diff --git a/vendor/rustpython-ruff_python_parser/src/lexer/interpolated_string.rs b/vendor/rustpython-ruff_python_parser/src/lexer/interpolated_string.rs new file mode 100644 index 00000000000..44174dbe2ee --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/lexer/interpolated_string.rs @@ -0,0 +1,179 @@ +use ruff_python_ast::StringFlags; +use ruff_text_size::TextSize; + +use crate::string::InterpolatedStringKind; + +use super::TokenFlags; + +/// The context representing the current f-string or t-string that the lexer is in. +#[derive(Clone, Debug)] +pub(crate) struct InterpolatedStringContext { + flags: TokenFlags, + + /// The level of nesting for the lexer when it entered the current f/t-string. + /// The nesting level includes all kinds of parentheses i.e., round, square, + /// and curly. + nesting: u32, + + /// The current depth of format spec for the current f/t-string. This is because + /// there can be multiple format specs nested for the same f-string. + /// For example, `{a:{b:{c}}}` has 3 format specs. + format_spec_depth: u32, + + /// The start of the f/t-string, including its prefix. + start: TextSize, +} + +impl InterpolatedStringContext { + pub(crate) const fn new(flags: TokenFlags, nesting: u32, start: TextSize) -> Option { + if flags.is_interpolated_string() { + Some(Self { + flags, + nesting, + format_spec_depth: 0, + start, + }) + } else { + None + } + } + + pub(crate) fn kind(&self) -> InterpolatedStringKind { + if self.flags.is_f_string() { + InterpolatedStringKind::FString + } else if self.flags.is_t_string() { + InterpolatedStringKind::TString + } else { + unreachable!("Can only be constructed when f-string or t-string flag is present") + } + } + + pub(crate) const fn flags(&self) -> TokenFlags { + self.flags + } + + pub(crate) const fn nesting(&self) -> u32 { + self.nesting + } + + /// Returns the start of the f/t-string, including its prefix. + pub(crate) const fn start(&self) -> TextSize { + self.start + } + + /// Returns the quote character for the current f-string. + pub(crate) fn quote_char(&self) -> char { + self.flags.quote_style().as_char() + } + + /// Returns the triple quotes for the current f-string if it is a triple-quoted + /// f-string, `None` otherwise. + pub(crate) fn triple_quotes(&self) -> Option<&'static str> { + if self.is_triple_quoted() { + Some(self.flags.quote_str()) + } else { + None + } + } + + /// Returns `true` if the current f-string is a raw f-string. + pub(crate) fn is_raw_string(&self) -> bool { + self.flags.is_raw_string() + } + + /// Returns `true` if the current f-string is a triple-quoted f-string. + pub(crate) fn is_triple_quoted(&self) -> bool { + self.flags.is_triple_quoted() + } + + /// Calculates the number of open parentheses for the current f-string + /// based on the current level of nesting for the lexer. + const fn open_parentheses_count(&self, current_nesting: u32) -> u32 { + current_nesting.saturating_sub(self.nesting) + } + + /// Returns the nesting level immediately outside the current replacement field. + #[cfg(feature = "python315-diagnostics")] + pub(crate) const fn replacement_field_nesting(&self, current_nesting: u32) -> u32 { + self.nesting + self.format_spec_depth - self.is_in_format_spec(current_nesting) as u32 + } + + /// Returns `true` if the lexer is in an f-string expression or t-string interpolation i.e., between + /// two curly braces. + pub(crate) const fn is_in_interpolation(&self, current_nesting: u32) -> bool { + self.open_parentheses_count(current_nesting) > self.format_spec_depth + } + + /// Returns `true` if the innermost open bracket is the brace that opens the current + /// expression or interpolation. + pub(crate) const fn is_at_interpolation_brace(&self, current_nesting: u32) -> bool { + self.open_parentheses_count(current_nesting) == self.format_spec_depth + 1 + } + + /// Returns `true` if the lexer is in a f-string format spec i.e., after a colon. + pub(crate) const fn is_in_format_spec(&self, current_nesting: u32) -> bool { + self.format_spec_depth > 0 && !self.is_in_interpolation(current_nesting) + } + + /// Returns `true` if the context is in a valid position to start format spec + /// i.e., at the same level of nesting as the opening parentheses token. + /// Increments the format spec depth if it is. + /// + /// This assumes that the current character for the lexer is a colon (`:`). + pub(crate) fn try_start_format_spec(&mut self, current_nesting: u32) -> bool { + if self + .open_parentheses_count(current_nesting) + .saturating_sub(self.format_spec_depth) + == 1 + { + self.format_spec_depth += 1; + true + } else { + false + } + } + + /// Decrements the format spec depth if the current f-string is in a format + /// spec. + pub(crate) fn try_end_format_spec(&mut self, current_nesting: u32) { + if self.is_in_format_spec(current_nesting) { + self.format_spec_depth = self.format_spec_depth.saturating_sub(1); + } + } +} + +/// The interpolated strings stack is used to keep track of all the f-strings and t-strings that the +/// lexer encounters. This is necessary because f-strings and t-strings can be nested. +#[derive(Debug, Default)] +pub(crate) struct InterpolatedStrings { + stack: Vec, +} + +impl InterpolatedStrings { + pub(crate) fn push(&mut self, context: InterpolatedStringContext) { + self.stack.push(context); + } + + pub(crate) fn pop(&mut self) -> Option { + self.stack.pop() + } + + pub(crate) fn current(&self) -> Option<&InterpolatedStringContext> { + self.stack.last() + } + + pub(crate) fn current_mut(&mut self) -> Option<&mut InterpolatedStringContext> { + self.stack.last_mut() + } + + pub(crate) fn checkpoint(&self) -> InterpolatedStringsCheckpoint { + InterpolatedStringsCheckpoint(self.stack.clone()) + } + + pub(crate) fn rewind(&mut self, checkpoint: InterpolatedStringsCheckpoint) { + self.stack = checkpoint.0; + } +} + +#[derive(Debug, Clone)] +pub(crate) struct InterpolatedStringsCheckpoint(Vec); diff --git a/vendor/rustpython-ruff_python_parser/src/lib.rs b/vendor/rustpython-ruff_python_parser/src/lib.rs new file mode 100644 index 00000000000..9577be1f7ec --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/lib.rs @@ -0,0 +1,626 @@ +//! This crate can be used to parse Python source code into an Abstract +//! Syntax Tree. +//! +//! ## Overview +//! +//! The process by which source code is parsed into an AST can be broken down +//! into two general stages: [lexical analysis] and [parsing]. +//! +//! During lexical analysis, the source code is converted into a stream of lexical +//! tokens that represent the smallest meaningful units of the language. For example, +//! the source code `print("Hello world")` would _roughly_ be converted into the following +//! stream of tokens: +//! +//! ```text +//! Name("print"), LeftParen, String("Hello world"), RightParen +//! ``` +//! +//! These tokens are then consumed by the `ruff_python_parser`, which matches them against a set of +//! grammar rules to verify that the source code is syntactically valid and to construct +//! an AST that represents the source code. +//! +//! During parsing, the `ruff_python_parser` consumes the tokens generated by the lexer and constructs +//! a tree representation of the source code. The tree is made up of nodes that represent +//! the different syntactic constructs of the language. If the source code is syntactically +//! invalid, parsing fails and an error is returned. After a successful parse, the AST can +//! be used to perform further analysis on the source code. Continuing with the example +//! above, the AST generated by the `ruff_python_parser` would _roughly_ look something like this: +//! +//! ```text +//! node: Expr { +//! value: { +//! node: Call { +//! func: { +//! node: Name { +//! id: "print", +//! ctx: Load, +//! }, +//! }, +//! args: [ +//! node: Constant { +//! value: Str("Hello World"), +//! kind: None, +//! }, +//! ], +//! keywords: [], +//! }, +//! }, +//! }, +//!``` +//! +//! **Note:** The Tokens/ASTs shown above are not the exact tokens/ASTs generated by the `ruff_python_parser`. +//! Refer to the [playground](https://play.ruff.rs) for the correct representation. +//! +//! ## Source code layout +//! +//! The functionality of this crate is split into several modules: +//! +//! - [lexer]: This module contains the lexer and is responsible for generating the tokens. +//! - parser: This module contains an interface to the [Parsed] and is responsible for generating the AST. +//! - mode: This module contains the definition of the different modes that the `ruff_python_parser` can be in. +//! +//! [lexical analysis]: https://en.wikipedia.org/wiki/Lexical_analysis +//! [parsing]: https://en.wikipedia.org/wiki/Parsing +//! [lexer]: crate::lexer + +pub use crate::error::{ + BlockClause, ExpressionKind, InterpolatedStringErrorType, LexicalErrorType, NumberLiteralKind, + ParseError, ParseErrorType, UnicodeEscapeErrorKind, UnsupportedSyntaxError, + UnsupportedSyntaxErrorKind, +}; +pub use crate::parser::ParseOptions; + +use crate::parser::Parser; + +use ruff_python_ast::token::{Token, TokenFlags, TokenKind, Tokens}; +use ruff_python_ast::{ + AtomicNodeIndex, Expr, Mod, ModExpression, ModModule, PySourceType, StringFlags, StringLiteral, + Suite, +}; +use ruff_text_size::{Ranged, TextRange}; + +mod error; +pub mod lexer; +mod parser; +pub mod semantic_errors; +mod string; +mod token_set; +mod token_source; +pub mod typing; + +/// Parse a full Python module usually consisting of multiple lines. +/// +/// This is a convenience function that can be used to parse a full Python program without having to +/// specify the [`Mode`] or the location. It is probably what you want to use most of the time. +/// +/// # Example +/// +/// For example, parsing a simple function definition and a call to that function: +/// +/// ``` +/// use ruff_python_parser::parse_module; +/// +/// let source = r#" +/// def foo(): +/// return 42 +/// +/// print(foo()) +/// "#; +/// +/// let module = parse_module(source); +/// assert!(module.is_ok()); +/// ``` +pub fn parse_module(source: &str) -> Result, ParseError> { + Parser::new(source, ParseOptions::from(Mode::Module)) + .parse() + .try_into_module() + .unwrap() + .into_result() +} + +/// Parses a single Python expression. +/// +/// This convenience function can be used to parse a single expression without having to +/// specify the Mode or the location. +/// +/// # Example +/// +/// For example, parsing a single expression denoting the addition of two numbers: +/// +/// ``` +/// use ruff_python_parser::parse_expression; +/// +/// let expr = parse_expression("1 + 2"); +/// assert!(expr.is_ok()); +/// ``` +pub fn parse_expression(source: &str) -> Result, ParseError> { + Parser::new(source, ParseOptions::from(Mode::Expression)) + .parse() + .try_into_expression() + .unwrap() + .into_result() +} + +/// Parses a Python expression for the given range in the source. +/// +/// This function allows to specify the range of the expression in the source code, other than +/// that, it behaves exactly like [`parse_expression`]. +/// +/// # Example +/// +/// Parsing one of the numeric literal which is part of an addition expression: +/// +/// ``` +/// use ruff_python_parser::parse_expression_range; +/// # use ruff_text_size::{TextRange, TextSize}; +/// +/// let parsed = parse_expression_range("11 + 22 + 33", TextRange::new(TextSize::new(5), TextSize::new(7))); +/// assert!(parsed.is_ok()); +/// ``` +pub fn parse_expression_range( + source: &str, + range: TextRange, +) -> Result, ParseError> { + let source = &source[..range.end().to_usize()]; + Parser::new_starts_at(source, range.start(), ParseOptions::from(Mode::Expression)) + .parse() + .try_into_expression() + .unwrap() + .into_result() +} + +/// Parses a Python expression as if it is parenthesized. +/// +/// It behaves similarly to [`parse_expression_range`] but allows what would be valid within parenthesis +/// +/// # Example +/// +/// Parsing an expression that would be valid within parenthesis: +/// +/// ``` +/// use ruff_python_parser::parse_parenthesized_expression_range; +/// # use ruff_text_size::{TextRange, TextSize}; +/// +/// let parsed = parse_parenthesized_expression_range("'''\n int | str'''", TextRange::new(TextSize::new(3), TextSize::new(14))); +/// assert!(parsed.is_ok()); +pub fn parse_parenthesized_expression_range( + source: &str, + range: TextRange, +) -> Result, ParseError> { + let source = &source[..range.end().to_usize()]; + let parsed = Parser::new_starts_at( + source, + range.start(), + ParseOptions::from(Mode::ParenthesizedExpression), + ) + .parse(); + parsed.try_into_expression().unwrap().into_result() +} + +/// Parses a Python expression from a string annotation. +/// +/// # Example +/// +/// Parsing a string annotation: +/// +/// ``` +/// use ruff_python_parser::parse_string_annotation; +/// use ruff_python_ast::{StringLiteral, StringLiteralFlags, AtomicNodeIndex}; +/// use ruff_text_size::{TextRange, TextSize}; +/// +/// let string = StringLiteral { +/// value: "'''\n int | str'''".to_string().into_boxed_str(), +/// flags: StringLiteralFlags::empty(), +/// range: TextRange::new(TextSize::new(0), TextSize::new(16)), +/// node_index: AtomicNodeIndex::NONE +/// }; +/// let parsed = parse_string_annotation("'''\n int | str'''", &string); +/// assert!(!parsed.is_ok()); +/// ``` +pub fn parse_string_annotation( + source: &str, + string: &StringLiteral, +) -> Result, ParseError> { + let range = string + .range() + .add_start(string.flags.opener_len()) + .sub_end(string.flags.closer_len()); + let source = &source[..range.end().to_usize()]; + if string.flags.is_triple_quoted() { + parse_parenthesized_expression_range(source, range) + } else { + parse_expression_range(source, range) + } +} + +/// Parse the given Python source code using the specified [`ParseOptions`]. +/// +/// This function is the most general function to parse Python code. Based on the [`Mode`] supplied +/// via the [`ParseOptions`], it can be used to parse a single expression, a full Python program, +/// an interactive expression or a Python program containing IPython escape commands. +/// +/// # Example +/// +/// If we want to parse a simple expression, we can use the [`Mode::Expression`] mode during +/// parsing: +/// +/// ``` +/// use ruff_python_parser::{parse, Mode, ParseOptions}; +/// +/// let parsed = parse("1 + 2", ParseOptions::from(Mode::Expression)); +/// assert!(parsed.is_ok()); +/// ``` +/// +/// Alternatively, we can parse a full Python program consisting of multiple lines: +/// +/// ``` +/// use ruff_python_parser::{parse, Mode, ParseOptions}; +/// +/// let source = r#" +/// class Greeter: +/// +/// def greet(self): +/// print("Hello, world!") +/// "#; +/// let parsed = parse(source, ParseOptions::from(Mode::Module)); +/// assert!(parsed.is_ok()); +/// ``` +/// +/// Additionally, we can parse a Python program containing IPython escapes: +/// +/// ``` +/// use ruff_python_parser::{parse, Mode, ParseOptions}; +/// +/// let source = r#" +/// %timeit 1 + 2 +/// ?str.replace +/// !ls +/// "#; +/// let parsed = parse(source, ParseOptions::from(Mode::Ipython)); +/// assert!(parsed.is_ok()); +/// ``` +pub fn parse(source: &str, options: ParseOptions) -> Result, ParseError> { + parse_unchecked(source, options).into_result() +} + +/// Parse the given Python source code using the specified [`ParseOptions`]. +/// +/// This is same as the [`parse`] function except that it doesn't check for any [`ParseError`] +/// and returns the [`Parsed`] as is. +pub fn parse_unchecked(source: &str, options: ParseOptions) -> Parsed { + Parser::new(source, options).parse() +} + +/// Parse the given Python source code using the specified [`PySourceType`]. +pub fn parse_unchecked_source(source: &str, source_type: PySourceType) -> Parsed { + // SAFETY: Safe because `PySourceType` always parses to a `ModModule` + Parser::new(source, ParseOptions::from(source_type)) + .parse() + .try_into_module() + .unwrap() +} + +/// Parses each `range` of `source` as an independent module and concatenates the results into a +/// single [`Parsed`] whose nodes keep their offsets into `source`. +/// +/// This validates sources such as Jupyter notebooks, where each cell must be syntactically valid on +/// its own while later cells can still reference earlier definitions. +/// The `ranges` must be ordered and non-overlapping. +/// +/// Consecutive ranges must be separated by a single-byte `\n`: each range ends just before the +/// separator and the next range starts just after it, as Ruff's notebook cells are. A syntax error +/// anchored at a cell's trailing offset then lands on that separator, the cell's own last line, so +/// it is attributed to that cell rather than to the following one. +pub fn parse_cells_unchecked( + source: &str, + ranges: impl IntoIterator, + options: &ParseOptions, +) -> Parsed { + let mut ranges = ranges.into_iter().peekable(); + let mut body = Suite::new(); + let mut tokens = Vec::new(); + let mut errors = Vec::new(); + let mut unsupported_syntax_errors = Vec::new(); + let mut module_range: Option = None; + + while let Some(range) = ranges.next() { + if let Some(previous) = module_range { + assert!(previous.end() <= range.start()); + } + + // The cell is lexed from `range.start()`, so the slice must keep the leading text to + // preserve absolute offsets into the concatenated source. + let cell_source = &source[TextRange::up_to(range.end())]; + let Parsed { + syntax, + tokens: cell_tokens, + errors: cell_errors, + unsupported_syntax_errors: cell_unsupported_syntax_errors, + } = Parser::new_starts_at(cell_source, range.start(), options.clone()) + .parse() + .try_into_module() + .expect("module options should parse into a module"); + + body.extend(syntax.body); + tokens.extend(cell_tokens); + errors.extend(cell_errors); + unsupported_syntax_errors.extend(cell_unsupported_syntax_errors); + + // Each range excludes its trailing `\n` separator (see the doc comment above), leaving a + // one-byte gap in the token stream. Cover it with a `NonLogicalNewline` so token-based + // checks don't treat the separator as another logical line terminator. The final cell's + // separator is the file-final newline and is deliberately left uncovered. + if let Some(next) = ranges.peek() { + let separator = TextRange::new(range.end(), next.start()); + assert_eq!(&source[separator], "\n"); + tokens.push(Token::new( + TokenKind::NonLogicalNewline, + separator, + TokenFlags::empty(), + )); + } + + module_range = Some(match module_range { + Some(previous) => TextRange::new(previous.start(), range.end()), + None => range, + }); + } + + body.shrink_to_fit(); + tokens.shrink_to_fit(); + errors.shrink_to_fit(); + unsupported_syntax_errors.shrink_to_fit(); + + Parsed { + syntax: ModModule { + node_index: AtomicNodeIndex::NONE, + range: module_range.unwrap_or_default(), + body, + runtime_body: None, + }, + tokens: Tokens::new(tokens), + errors, + unsupported_syntax_errors, + } +} + +/// Represents the parsed source code. +#[derive(Debug, PartialEq, Clone, get_size2::GetSize)] +pub struct Parsed { + syntax: T, + tokens: Tokens, + errors: Vec, + unsupported_syntax_errors: Vec, +} + +impl Parsed { + /// Returns the syntax node represented by this parsed output. + pub fn syntax(&self) -> &T { + &self.syntax + } + + /// Returns all the tokens for the parsed output. + pub fn tokens(&self) -> &Tokens { + &self.tokens + } + + /// Returns a list of syntax errors found during parsing. + pub fn errors(&self) -> &[ParseError] { + &self.errors + } + + /// Returns a list of version-related syntax errors found during parsing. + pub fn unsupported_syntax_errors(&self) -> &[UnsupportedSyntaxError] { + &self.unsupported_syntax_errors + } + + /// Consumes the [`Parsed`] output and returns the contained syntax node. + pub fn into_syntax(self) -> T { + self.syntax + } + + /// Consumes the [`Parsed`] output and returns a list of syntax errors found during parsing. + fn into_errors(self) -> Vec { + self.errors + } + + /// Returns `true` if the parsed source code is valid i.e., it has no [`ParseError`]s. + /// + /// Note that this does not include version-related [`UnsupportedSyntaxError`]s. + /// + /// See [`Parsed::has_no_syntax_errors`] for a version that takes these into account. + pub fn has_valid_syntax(&self) -> bool { + self.errors.is_empty() + } + + /// Returns `true` if the parsed source code is invalid i.e., it has [`ParseError`]s. + /// + /// Note that this does not include version-related [`UnsupportedSyntaxError`]s. + /// + /// See [`Parsed::has_no_syntax_errors`] for a version that takes these into account. + pub fn has_invalid_syntax(&self) -> bool { + !self.has_valid_syntax() + } + + /// Returns `true` if the parsed source code does not contain any [`ParseError`]s *or* + /// [`UnsupportedSyntaxError`]s. + /// + /// See [`Parsed::has_valid_syntax`] for a version specific to [`ParseError`]s. + pub fn has_no_syntax_errors(&self) -> bool { + self.has_valid_syntax() && self.unsupported_syntax_errors.is_empty() + } + + /// Returns `true` if the parsed source code contains any [`ParseError`]s *or* + /// [`UnsupportedSyntaxError`]s. + /// + /// See [`Parsed::has_invalid_syntax`] for a version specific to [`ParseError`]s. + pub fn has_syntax_errors(&self) -> bool { + !self.has_no_syntax_errors() + } + + /// Returns the [`Parsed`] output as a [`Result`], returning [`Ok`] if it has no syntax errors, + /// or [`Err`] containing the first [`ParseError`] encountered. + /// + /// Note that any [`unsupported_syntax_errors`](Parsed::unsupported_syntax_errors) will not + /// cause [`Err`] to be returned. + pub fn as_result(&self) -> Result<&Parsed, &[ParseError]> { + if self.has_valid_syntax() { + Ok(self) + } else { + Err(&self.errors) + } + } + + /// Consumes the [`Parsed`] output and returns a [`Result`] which is [`Ok`] if it has no syntax + /// errors, or [`Err`] containing the first [`ParseError`] encountered. + /// + /// Note that any [`unsupported_syntax_errors`](Parsed::unsupported_syntax_errors) will not + /// cause [`Err`] to be returned. + fn into_result(self) -> Result, ParseError> { + if self.has_valid_syntax() { + Ok(self) + } else { + Err(self.into_errors().into_iter().next().unwrap()) + } + } +} + +impl Parsed { + /// Attempts to convert the [`Parsed`] into a [`Parsed`]. + /// + /// This method checks if the `syntax` field of the output is a [`Mod::Module`]. If it is, the + /// method returns [`Some(Parsed)`] with the contained module. Otherwise, it + /// returns [`None`]. + /// + /// [`Some(Parsed)`]: Some + pub fn try_into_module(self) -> Option> { + match self.syntax { + Mod::Module(module) => Some(Parsed { + syntax: module, + tokens: self.tokens, + errors: self.errors, + unsupported_syntax_errors: self.unsupported_syntax_errors, + }), + Mod::Expression(_) => None, + } + } + + /// Attempts to convert the [`Parsed`] into a [`Parsed`]. + /// + /// This method checks if the `syntax` field of the output is a [`Mod::Expression`]. If it is, + /// the method returns [`Some(Parsed)`] with the contained expression. + /// Otherwise, it returns [`None`]. + /// + /// [`Some(Parsed)`]: Some + fn try_into_expression(self) -> Option> { + match self.syntax { + Mod::Module(_) => None, + Mod::Expression(expression) => Some(Parsed { + syntax: expression, + tokens: self.tokens, + errors: self.errors, + unsupported_syntax_errors: self.unsupported_syntax_errors, + }), + } + } +} + +impl Parsed { + /// Returns the module body contained in this parsed output as a [`Suite`]. + pub fn suite(&self) -> &Suite { + &self.syntax.body + } + + /// Consumes the [`Parsed`] output and returns the module body as a [`Suite`]. + pub fn into_suite(self) -> Suite { + self.syntax.body + } +} + +impl Parsed { + /// Returns the expression contained in this parsed output. + pub fn expr(&self) -> &Expr { + &self.syntax.body + } + + /// Returns a mutable reference to the expression contained in this parsed output. + fn expr_mut(&mut self) -> &mut Expr { + &mut self.syntax.body + } + + /// Consumes the [`Parsed`] output and returns the contained [`Expr`]. + pub fn into_expr(self) -> Expr { + *self.syntax.body + } +} + +/// Control in the different modes by which a source file can be parsed. +/// +/// The mode argument specifies in what way code must be parsed. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub enum Mode { + /// The code consists of a sequence of statements. + Module, + + /// The code consists of a single expression. + Expression, + + /// The code consists of a single expression and is parsed as if it is parenthesized. The parentheses themselves aren't required. + /// This allows for having valid multiline expression without the need of parentheses + /// and is specifically useful for parsing string annotations. + ParenthesizedExpression, + + /// The code consists of a sequence of statements which can include the + /// escape commands that are part of IPython syntax. + /// + /// ## Supported escape commands: + /// + /// - [Magic command system] which is limited to [line magics] and can start + /// with `?` or `??`. + /// - [Dynamic object information] which can start with `?` or `??`. + /// - [System shell access] which can start with `!` or `!!`. + /// - [Automatic parentheses and quotes] which can start with `/`, `;`, or `,`. + /// + /// [Magic command system]: https://ipython.readthedocs.io/en/stable/interactive/reference.html#magic-command-system + /// [line magics]: https://ipython.readthedocs.io/en/stable/interactive/magics.html#line-magics + /// [Dynamic object information]: https://ipython.readthedocs.io/en/stable/interactive/reference.html#dynamic-object-information + /// [System shell access]: https://ipython.readthedocs.io/en/stable/interactive/reference.html#system-shell-access + /// [Automatic parentheses and quotes]: https://ipython.readthedocs.io/en/stable/interactive/reference.html#automatic-parentheses-and-quotes + Ipython, +} + +impl std::str::FromStr for Mode { + type Err = ModeParseError; + fn from_str(s: &str) -> Result { + match s { + "exec" | "single" => Ok(Mode::Module), + "eval" => Ok(Mode::Expression), + "ipython" => Ok(Mode::Ipython), + _ => Err(ModeParseError), + } + } +} + +/// A type that can be represented as [Mode]. +pub trait AsMode { + fn as_mode(&self) -> Mode; +} + +impl AsMode for PySourceType { + fn as_mode(&self) -> Mode { + match self { + PySourceType::Python | PySourceType::Stub => Mode::Module, + PySourceType::Ipynb => Mode::Ipython, + } + } +} + +/// Returned when a given mode is not valid. +#[derive(Debug)] +pub struct ModeParseError; + +impl std::fmt::Display for ModeParseError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, r#"mode must be "exec", "eval", "ipython", or "single""#) + } +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/case_diagnostics.rs b/vendor/rustpython-ruff_python_parser/src/parser/case_diagnostics.rs new file mode 100644 index 00000000000..9005ce03d40 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/case_diagnostics.rs @@ -0,0 +1,156 @@ +//! The invalid standalone-case production. This is diagnostic-only speculation: +//! the caller always parses the original statement again for syntax and tokens. + +use ruff_python_ast::token::TokenKind; +use ruff_python_ast::{Expr, Pattern}; +use ruff_text_size::TextRange; + +use super::Parser; +use super::expression::ExpressionContext; +use crate::{BlockClause, ParseError, ParseErrorType}; + +impl Parser<'_> { + pub(super) fn standalone_case_diagnostic(&mut self) -> Option { + if !self.at(TokenKind::Case) + || !self.errors.is_empty() + || self + .token_before(self.node_start()) + .is_some_and(|(kind, _)| { + !matches!( + kind, + TokenKind::Newline | TokenKind::Indent | TokenKind::Dedent + ) + }) + { + return None; + } + + let checkpoint = self.checkpoint(); + // These recovery fields are not covered by the normal parser checkpoint. + let unclosed_bracket = self.first_unclosed_bracket_recovery; + let escape_error = self.interpolated_string_escape_error.clone(); + let replacement_field = self.replacement_field; + let diagnostic = self.parse_invalid_standalone_case(); + self.rewind(checkpoint); + self.first_unclosed_bracket_recovery = unclosed_bracket; + self.interpolated_string_escape_error = escape_error; + self.replacement_field = replacement_field; + diagnostic + } + + fn parse_invalid_standalone_case(&mut self) -> Option { + let start = self.node_start(); + self.bump(TokenKind::Case); + let pattern = self.parse_match_patterns(); + if !self.errors.is_empty() { + return self.standalone_case_inner_error(); + } + if !pattern_has_valid_value_names(&pattern) { + return None; + } + + if self.eat(TokenKind::If) { + if !self.at_expr() { + return None; + } + self.parse_named_expression_or_higher(ExpressionContext::default()); + if !self.errors.is_empty() { + return self.standalone_case_inner_error(); + } + } + if !self.at(TokenKind::Colon) { + return None; + } + let header_range = TextRange::new(start, self.current_token_range().end()); + self.bump(TokenKind::Colon); + self.parse_body(BlockClause::StandaloneCase, start); + + if self + .tokens + .has_tokenizer_error_in(TextRange::new(start, self.current_token_range().end())) + { + return None; + } + if self.errors.is_empty() { + Some(ParseError { + error: ParseErrorType::CaseOutsideMatch, + location: header_range, + }) + } else { + self.standalone_case_inner_error() + } + } + + /// A malformed production does not establish a standalone case. An invalid + /// rule reached while parsing its pattern, guard, or body can still raise + /// its own diagnostic before the enclosing standalone-case rule fires. + fn standalone_case_inner_error(&self) -> Option { + self.errors + .first() + .filter(|error| { + matches!( + error.error, + ParseErrorType::CaseOutsideMatch + | ParseErrorType::ExpectedIndentedBlock { .. } + | ParseErrorType::InvalidMatchPatternTarget + | ParseErrorType::InvalidPatternTarget(_) + | ParseErrorType::MissingRaiseException + | ParseErrorType::MissingRaiseCause + | ParseErrorType::NamedExpressionWithoutParentheses + | ParseErrorType::MappingRestPatternNotLast + | ParseErrorType::TrailingCommaInWith + | ParseErrorType::MisplacedLazyImport + | ParseErrorType::LazyFutureImport + | ParseErrorType::InvalidAssignmentTarget { .. } + | ParseErrorType::InvalidAnnotatedAssignmentTarget + | ParseErrorType::InvalidAugmentedAssignmentTarget(_) + | ParseErrorType::InvalidDeleteTarget(_) + ) + }) + .cloned() + } +} + +/// The recovery parser permits an underscore at the root of a value or class +/// pattern. In the grammar it commits to the wildcard instead, so `_.x` cannot +/// complete the pattern even when the recovered AST is an attribute. +fn pattern_has_valid_value_names(pattern: &Pattern) -> bool { + match pattern { + Pattern::MatchValue(pattern) => value_has_valid_root(&pattern.value), + Pattern::MatchSingleton(_) | Pattern::MatchStar(_) => true, + Pattern::MatchSequence(pattern) => { + pattern.patterns.iter().all(pattern_has_valid_value_names) + } + Pattern::MatchMapping(pattern) => { + pattern.patterns.iter().all(pattern_has_valid_value_names) + } + Pattern::MatchClass(pattern) => { + value_has_valid_root(&pattern.cls) + && pattern + .arguments + .patterns + .iter() + .all(pattern_has_valid_value_names) + && pattern + .arguments + .keywords + .iter() + .all(|keyword| pattern_has_valid_value_names(&keyword.pattern)) + } + Pattern::MatchAs(pattern) => pattern + .pattern + .as_deref() + .is_none_or(pattern_has_valid_value_names), + Pattern::MatchOr(pattern) => pattern.patterns.iter().all(pattern_has_valid_value_names), + } +} + +fn value_has_valid_root(mut value: &Expr) -> bool { + while let Expr::Attribute(attribute) = value { + value = &attribute.value; + } + match value { + Expr::Name(name) => name.id != "_", + _ => true, + } +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/expression.rs b/vendor/rustpython-ruff_python_parser/src/parser/expression.rs new file mode 100644 index 00000000000..e7741edba27 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/expression.rs @@ -0,0 +1,4341 @@ +use std::ops::Deref; + +use bitflags::bitflags; +use thin_vec::ThinVec; + +use ruff_python_ast::name::Name; +use ruff_python_ast::token::TokenKind; +use ruff_python_ast::{ + self as ast, AnyStringFlags, AtomicNodeIndex, BoolOp, CmpOp, ConversionFlag, Expr, ExprContext, + FString, InterpolatedStringElement, InterpolatedStringElements, IpyEscapeKind, Number, + Operator, OperatorPrecedence, PythonVersion, StringFlags, TString, UnaryOp, +}; +use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; + +use crate::error::{ + ComprehensionUnpackingKind, FStringKind, StarTupleKind, UnparenthesizedNamedExprKind, +}; +use crate::parser::progress::ParserProgress; +use crate::parser::{FunctionKind, IpyEscapeContext, Parser, helpers}; +use crate::string::{ + InterpolatedStringKind, StringType, parse_interpolated_string_literal_element, + parse_string_literal, +}; +use crate::token_set::TokenSet; +use crate::{ + ExpressionKind, InterpolatedStringErrorType, Mode, ParseError, ParseErrorType, + UnsupportedSyntaxError, UnsupportedSyntaxErrorKind, +}; + +use super::{Bracket, InterpolatedStringElementsKind, Parenthesized, RecoveryContextKind}; + +/// The message for a keyword argument whose name is not a plain name. +const EXPRESSION_CONTAINS_ASSIGNMENT: &str = + "expression cannot contain assignment, perhaps you meant \"==\"?"; + +/// A token set consisting of a newline or end of file. +const NEWLINE_EOF_SET: TokenSet = TokenSet::new([TokenKind::Newline, TokenKind::EndOfFile]); + +/// Tokens that represents a literal expression. +const LITERAL_SET: TokenSet = TokenSet::new([ + TokenKind::Int, + TokenKind::Float, + TokenKind::Complex, + TokenKind::String, + TokenKind::Ellipsis, + TokenKind::True, + TokenKind::False, + TokenKind::None, +]); + +/// Tokens that represents either an expression or the start of one. +pub(super) const EXPR_SET: TokenSet = TokenSet::new([ + TokenKind::Identifier, + TokenKind::Minus, + TokenKind::Plus, + TokenKind::Tilde, + TokenKind::Star, + TokenKind::DoubleStar, + TokenKind::Lpar, + TokenKind::Lbrace, + TokenKind::Lsqb, + TokenKind::Lambda, + TokenKind::Await, + TokenKind::Not, + TokenKind::Yield, + TokenKind::FStringStart, + TokenKind::TStringStart, + TokenKind::IpyEscapeCommand, +]) +.union(LITERAL_SET); + +/// Tokens that can appear after an expression. +const END_EXPR_SET: TokenSet = TokenSet::new([ + // Ex) `expr` (without a newline) + TokenKind::EndOfFile, + // Ex) `expr` + TokenKind::Newline, + // Ex) `expr;` + TokenKind::Semi, + // Ex) `data[expr:]` + // Ex) `def foo() -> expr:` + // Ex) `{expr: expr}` + TokenKind::Colon, + // Ex) `{expr}` + TokenKind::Rbrace, + // Ex) `[expr]` + TokenKind::Rsqb, + // Ex) `(expr)` + TokenKind::Rpar, + // Ex) `expr,` + TokenKind::Comma, + // Ex) + // + // if True: + // expr + // # <- Dedent + // x + TokenKind::Dedent, + // Ex) `expr if expr else expr` + TokenKind::If, + TokenKind::Else, + // Ex) `with expr as target:` + // Ex) `except expr as NAME:` + TokenKind::As, + // Ex) `raise expr from expr` + TokenKind::From, + // Ex) `[expr for expr in iter]` + TokenKind::For, + // Ex) `[expr async for expr in iter]` + TokenKind::Async, + // Ex) `expr in expr` + TokenKind::In, + // Ex) `name: expr = expr` + // Ex) `f"{expr=}"` + TokenKind::Equal, + // Ex) `f"{expr!s}"` + TokenKind::Exclamation, +]); + +/// Tokens that can appear at the end of a sequence. +const END_SEQUENCE_SET: TokenSet = END_EXPR_SET.remove(TokenKind::Comma); + +impl<'src> Parser<'src> { + /// Returns `true` if the parser is at an identifier or keyword (including soft keyword) token. + pub(super) fn at_identifier_or_keyword(&self) -> bool { + self.at(TokenKind::Identifier) || self.current_token_kind().is_keyword() + } + + /// Returns `true` if the parser is at an identifier or soft keyword token. + pub(super) fn at_identifier_or_soft_keyword(&self) -> bool { + self.at(TokenKind::Identifier) || self.at_soft_keyword() + } + + /// Returns `true` if the parser is at a soft keyword token. + pub(super) fn at_soft_keyword(&self) -> bool { + self.current_token_kind().is_soft_keyword() + } + + /// Returns `true` if the current token is the start of an expression. + pub(super) fn at_expr(&self) -> bool { + self.at_ts(EXPR_SET) || self.at_soft_keyword() + } + + /// Returns `true` if the current token ends a sequence. + pub(super) fn at_sequence_end(&self) -> bool { + self.at_ts(END_SEQUENCE_SET) + } + + /// Parses every Python expression. + /// + /// Matches the `expressions` rule in the [Python grammar]. The [`ExpressionContext`] can be + /// used to match the `star_expressions` rule. + /// + /// [Python grammar]: https://docs.python.org/3/reference/grammar.html + pub(super) fn parse_expression_list(&mut self, context: ExpressionContext) -> ParsedExpr { + let start = self.node_start(); + let parsed_expr = self.parse_conditional_expression_or_higher_impl(context); + + if self.at(TokenKind::Comma) { + let subsequent_context = context.disallow_yield_expressions(); + Expr::Tuple(self.parse_tuple_expression( + parsed_expr.expr, + start, + Parenthesized::No, + |p| p.parse_conditional_expression_or_higher_impl(subsequent_context), + )) + .into() + } else { + parsed_expr + } + } + + /// Parses every Python expression except unparenthesized tuple. + /// + /// Matches the `named_expression` rule in the [Python grammar]. The [`ExpressionContext`] can + /// be used to match the `star_named_expression` rule. + /// + /// NOTE: If you have expressions separated by commas and want to parse them individually + /// instead of as a tuple, as done by [`Parser::parse_expression_list`], use this function. + /// + /// [Python grammar]: https://docs.python.org/3/reference/grammar.html + pub(super) fn parse_named_expression_or_higher( + &mut self, + context: ExpressionContext, + ) -> ParsedExpr { + let parsed_expr = self.parse_named_expression_without_assignment_check(context); + self.check_assignment_instead_of_comparison(&parsed_expr); + parsed_expr + } + + /// Parses a named expression or higher without reporting an `=` after it, which starts a + /// keyword argument in a call. + fn parse_named_expression_without_assignment_check( + &mut self, + context: ExpressionContext, + ) -> ParsedExpr { + let start = self.node_start(); + let parsed_expr = self.parse_conditional_expression_or_higher_impl(context); + + if self.at(TokenKind::ColonEqual) { + Expr::Named(self.parse_named_expression(parsed_expr.expr, start)).into() + } else { + parsed_expr + } + } + + /// Parses every Python expression except unparenthesized tuple and named expressions. + /// + /// Matches the `expression` rule in the [Python grammar]. + /// + /// This uses the default [`ExpressionContext`]. Use + /// [`Parser::parse_conditional_expression_or_higher_impl`] if you prefer to pass in the + /// context. + /// + /// NOTE: If you have expressions separated by commas and want to parse them individually + /// instead of as a tuple, as done by [`Parser::parse_expression_list`] use this function. + /// + /// [Python grammar]: https://docs.python.org/3/reference/grammar.html + pub(super) fn parse_conditional_expression_or_higher(&mut self) -> ParsedExpr { + self.parse_conditional_expression_or_higher_impl(ExpressionContext::default()) + } + + pub(super) fn parse_conditional_expression_or_higher_impl( + &mut self, + context: ExpressionContext, + ) -> ParsedExpr { + if matches!( + self.current_token_kind(), + TokenKind::Pass | TokenKind::Break | TokenKind::Continue + ) { + self.check_statement_before_if_expression(); + } + let parsed_expr = self.parse_conditional_expression_without_comma_check(context); + if !self.check_expression_between_strings(&parsed_expr) { + self.check_missing_comma(&parsed_expr); + } + parsed_expr + } + + /// Reports a `pass`, `break` or `continue` statement used as the body of an `if` + /// expression, as in `pass if x else y`. + pub(super) fn check_statement_before_if_expression(&mut self) { + // test_err statement_before_if_expression + // x = pass if 1 else 1 + // pass if 1 else pass + if self.peek() != TokenKind::If { + return; + } + let statement_range = self.current_token_range(); + let checkpoint = self.checkpoint(); + self.bump(self.current_token_kind()); + self.bump(TokenKind::If); + self.parse_simple_expression(ExpressionContext::default()); + let found = self.eat(TokenKind::Else) + && (self.at_expr() + || matches!( + self.current_token_kind(), + TokenKind::Pass + | TokenKind::Break + | TokenKind::Continue + | TokenKind::Return + | TokenKind::Raise + | TokenKind::Global + | TokenKind::Nonlocal + | TokenKind::Del + | TokenKind::Assert + | TokenKind::Import + | TokenKind::From + )); + self.rewind(checkpoint); + if found { + self.add_error( + ParseErrorType::OtherError( + "expected expression before 'if', but statement is given".to_string(), + ), + statement_range, + ); + } + } + + /// Reports expressions between two string literals, as in `"a" b "c"`, and returns `true` if + /// it did. + fn check_expression_between_strings(&mut self, first: &ParsedExpr) -> bool { + // test_err expression_between_strings + // "a" b "c" + // x = "a" 1 + 2 c "d" + // ["a" b "c"] + let single_string = match &first.expr { + Expr::StringLiteral(string) => !string.value.is_implicit_concatenated(), + Expr::BytesLiteral(bytes) => !bytes.value.is_implicit_concatenated(), + _ => false, + }; + if !single_string || first.is_parenthesized || !self.at_expr() || self.at(TokenKind::String) + { + return false; + } + + let checkpoint = self.checkpoint(); + let start = self.node_start(); + let mut end = start; + while self.at_expr() && !self.at(TokenKind::String) { + end = self + .parse_conditional_expression_without_comma_check(ExpressionContext::default()) + .end(); + } + let found = self.at(TokenKind::String); + self.rewind(checkpoint); + if found { + self.add_error( + ParseErrorType::OtherError( + "invalid syntax. Is this intended to be part of the string?".to_string(), + ), + TextRange::new(start, end), + ); + } + found + } + + /// Reports an `=` after a named expression where `==` or `:=` may have been meant. + fn check_assignment_instead_of_comparison(&mut self, target: &ParsedExpr) { + // test_err assignment_instead_of_comparison + // if x = 3: ... + // while x.a = 3: ... + // [x, y = 1] + // if x = 3 = 4: ... + if !self.at(TokenKind::Equal) + || !self.is_bitwise_or(&target.expr) + || self.starts_with_display_or_constant(&target.expr) + { + return; + } + + let checkpoint = self.checkpoint(); + self.bump(TokenKind::Equal); + let value_end = if self.at_expr() { + let value = self.parse_simple_expression(ExpressionContext::default()); + self.bitwise_or_prefix(&value.expr) + .filter(|value| { + !matches!( + self.token_kind_after(self.parenthesized_end(value.range())), + TokenKind::Equal | TokenKind::ColonEqual + ) + }) + .map(Ranged::end) + } else { + None + }; + self.rewind(checkpoint); + + let Some(value_end) = value_end else { + return; + }; + if target.expr.is_name_expr() && !target.is_parenthesized { + self.add_error( + ParseErrorType::AssignmentInsteadOfComparison, + TextRange::new(target.start(), value_end), + ); + } else { + self.add_error( + ParseErrorType::InvalidAssignmentTarget { + kind: ExpressionKind::of(&target.expr), + maybe_comparison: true, + }, + target.expr.range(), + ); + } + } + + fn parse_conditional_expression_without_comma_check( + &mut self, + context: ExpressionContext, + ) -> ParsedExpr { + if self.at(TokenKind::Lambda) { + Expr::Lambda(self.parse_lambda_expr(false)).into() + } else { + let start = self.node_start(); + let parsed_expr = self.parse_simple_expression(context); + + if self.at(TokenKind::If) { + Expr::If(self.parse_if_expression(parsed_expr.expr, start)).into() + } else { + parsed_expr + } + } + } + + /// Reports a missing comma when an expression inside brackets is directly followed by + /// another expression. + /// + /// The error is not reported when the first expression is a name followed by a string (an + /// invalid string prefix) or starts with a soft keyword (including `_`). The name `print` or + /// `exec` followed by an expression is reported as a call missing its parentheses instead. + fn check_missing_comma(&mut self, first: &ParsedExpr) { + // test_err missing_comma_between_expressions + // [a b] + // f(x, a.b c) + // x[1 + 2 3] + // f"{a b}" + // A tokenizer error in or right after the first expression comes first. + if !self.at_expr() + || self.at(TokenKind::Yield) + || self.at_tokenizer_error() + || self.tokens.has_tokenizer_error_in(TextRange::new( + first.start(), + self.current_token_range().end(), + )) + || matches!( + first.expr, + Expr::Starred(_) + | Expr::Yield(_) + | Expr::YieldFrom(_) + | Expr::Lambda(_) + | Expr::If(_) + | Expr::Named(_) + ) + { + return; + } + if let Expr::Name(name) = &first.expr + && !first.is_parenthesized + { + if matches!(name.id.as_str(), "print" | "exec") { + self.report_legacy_statement(first.start(), name.id.as_str()); + return; + } + if matches!( + self.current_token_kind(), + TokenKind::String | TokenKind::FStringStart | TokenKind::TStringStart + ) { + return; + } + } + // Soft keywords are bumped as names. + let (first_kind, first_range) = self.token_after(first.start()); + if (first_kind == TokenKind::Identifier + && matches!(self.src_text(first_range), "_" | "case" | "match" | "type")) + || self.bracket_level() == 0 + { + return; + } + + // test_err missing_comma_before_invalid_expression + // [a ~] + // [a b +] + let checkpoint = self.checkpoint(); + let errors_before = self.errors.len(); + let second = self.parse_conditional_expression_or_higher(); + let second_end = self.complete_prefix_end(&second.expr, errors_before); + self.rewind(checkpoint); + let Some(second_end) = second_end else { + return; + }; + let range = TextRange::new(first.start(), second_end); + #[cfg(feature = "python315-diagnostics")] + if let Some((diagnostic_start, end)) = self.concatenated_literal_comma_range(first) { + self.add_error( + ParseErrorType::MissingCommaAfterConcatenatedLiteral { + expression_range: range, + diagnostic_start, + }, + TextRange::new(diagnostic_start.min(end), end), + ); + return; + } + self.add_error( + ParseErrorType::OtherError("invalid syntax. Perhaps you forgot a comma?".to_string()), + range, + ); + } + + /// Points a multiline concatenation's missing-comma hint at its final literal line, + /// retaining the first literal's column, as CPython does. + #[cfg(feature = "python315-diagnostics")] + fn concatenated_literal_comma_range(&self, first: &ParsedExpr) -> Option<(TextSize, TextSize)> { + let last = match &first.expr { + Expr::StringLiteral(string) if string.value.is_implicit_concatenated() => { + string.value.iter().next_back()?.range() + } + Expr::BytesLiteral(bytes) if bytes.value.is_implicit_concatenated() => { + bytes.value.iter().next_back()?.range() + } + Expr::FString(string) if string.value.is_implicit_concatenated() => { + string.value.iter().next_back()?.range() + } + Expr::TString(string) if string.value.is_implicit_concatenated() => { + string.value.iter().next_back()?.range() + } + _ => return None, + }; + let first_start = first.start().to_usize(); + let end = last.end().to_usize(); + let line_start = |offset| { + self.source[..offset] + .rfind(['\n', '\r']) + .map_or(0, |i| i + 1) + }; + let first_line = line_start(first_start); + let last_line = line_start(end); + if first_line == last_line { + return None; + } + let line_end = self.source[end..] + .find(['\n', '\r']) + .map_or(self.source.len(), |i| end + i); + let mut start = (last_line + first_start - first_line).min(line_end); + // Keep the projected byte column at a valid character boundary. + while !self.source.is_char_boundary(start) { + start -= 1; + } + Some((TextSize::try_from(start).unwrap(), last.end())) + } + + /// Reports the Python 2 `print` or `exec` statement named `keyword` that starts at `start` + /// and whose operands start at the current token, unless no prefix of the operands is a + /// complete expression. + fn report_legacy_statement(&mut self, start: TextSize, keyword: &str) { + // test_err legacy_print_statement + // print 'hello' + // [print x, y] + // exec code in ns + + // test_err legacy_print_statement_invalid_operand + // print {1:(foo.)} + // [print 1 +] + let checkpoint = self.checkpoint(); + let errors_before = self.errors.len(); + let operands = self.parse_expression_list(ExpressionContext::starred_bitwise_or()); + let end = self.complete_prefix_end(&operands.expr, errors_before); + self.rewind(checkpoint); + let Some(end) = end else { + return; + }; + self.add_error( + ParseErrorType::OtherError(format!( + "Missing parentheses in call to '{keyword}'. Did you mean {keyword}(...)?" + )), + TextRange::new(start, end), + ); + } + + /// Parses every Python expression except unparenthesized tuples, named expressions, + /// and `if` expression. + /// + /// This is a combination of the `disjunction`, `starred_expression`, `yield_expr` + /// and `lambdef` rules of the [Python grammar]. + /// + /// Note that this function parses lambda expression but reports an error as they're not + /// allowed in this context. This is done for better error recovery. + /// Use [`Parser::parse_conditional_expression_or_higher`] or any methods which calls into the + /// specified method to allow parsing lambda expression. + /// + /// [Python grammar]: https://docs.python.org/3/reference/grammar.html + fn parse_simple_expression(&mut self, context: ExpressionContext) -> ParsedExpr { + self.parse_binary_expression_or_higher(OperatorPrecedence::None, context) + } + + /// Parses a binary expression using the [Pratt parsing algorithm]. + /// + /// [Pratt parsing algorithm]: https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html + fn parse_binary_expression_or_higher( + &mut self, + left_precedence: OperatorPrecedence, + context: ExpressionContext, + ) -> ParsedExpr { + self.with_recursion(|parser| { + let start = parser.node_start(); + let lhs = parser.parse_lhs_expression(left_precedence, context); + parser.parse_binary_expression_or_higher_recursive(lhs, left_precedence, context, start) + }) + } + + fn parse_binary_expression_or_higher_recursive( + &mut self, + mut left: ParsedExpr, + left_precedence: OperatorPrecedence, + context: ExpressionContext, + start: TextSize, + ) -> ParsedExpr { + let mut progress = ParserProgress::default(); + + loop { + progress.assert_progressing(self); + + let current_token = self.current_token_kind(); + + if matches!(current_token, TokenKind::In) && context.is_in_excluded() { + // Omit the `in` keyword when parsing the target expression in a comprehension or + // a `for` statement. + break; + } + + let next_token = + matches!(current_token, TokenKind::Is | TokenKind::Not).then(|| self.peek()); + let Some(operator) = BinaryLikeOperator::try_from_tokens(current_token, next_token) + else { + // Not an operator. + break; + }; + + let new_precedence = operator.precedence(); + + let stop_at_current_operator = if new_precedence.is_right_associative() { + new_precedence < left_precedence + } else { + new_precedence <= left_precedence + }; + + if stop_at_current_operator { + break; + } + + left.expr = match operator { + BinaryLikeOperator::Boolean(bool_op) => { + Expr::BoolOp(self.parse_boolean_expression(left.expr, start, bool_op, context)) + } + BinaryLikeOperator::Comparison(cmp_op) => Expr::Compare( + self.parse_comparison_expression(left.expr, start, cmp_op, context), + ), + BinaryLikeOperator::Binary(bin_op) => { + self.bump(TokenKind::from(bin_op)); + + let right = self.parse_binary_expression_or_higher(new_precedence, context); + + Expr::BinOp(ast::ExprBinOp { + left: Box::new(left.expr), + op: bin_op, + right: Box::new(right.expr), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + }; + } + + left + } + + /// Parses the left-hand side of an expression. + /// + /// This includes prefix expressions such as unary operators, boolean `not`, + /// `await`, `lambda`. It also parses atoms and postfix expressions. + /// + /// The given [`OperatorPrecedence`] is used to determine if the parsed expression + /// is valid in that context. For example, a unary operator is not valid + /// in an `await` expression in which case the `left_precedence` would + /// be [`OperatorPrecedence::Await`]. + fn parse_lhs_expression( + &mut self, + left_precedence: OperatorPrecedence, + context: ExpressionContext, + ) -> ParsedExpr { + let token = self.current_token_kind(); + let start = self.node_start(); + + if let Some(unary_op) = token.as_unary_operator() { + let expr = self.parse_unary_expression(unary_op, context); + + if matches!(unary_op, UnaryOp::Not) { + if left_precedence > OperatorPrecedence::Not { + self.add_error( + ParseErrorType::OtherError( + "'not' after an operator must be parenthesized".to_string(), + ), + &expr, + ); + } + } else { + // > The power operator `**` binds less tightly than an arithmetic + // > or bitwise unary operator on its right, that is, 2**-1 is 0.5. + // + // Reference: https://docs.python.org/3/reference/expressions.html#id21 + if left_precedence > OperatorPrecedence::PosNegBitNot + && left_precedence != OperatorPrecedence::Exponent + { + self.add_error( + ParseErrorType::OtherError(format!( + "unary '{unary_op}' expression cannot be used here", + )), + &expr, + ); + } + } + + return Expr::UnaryOp(expr).into(); + } + + match token { + TokenKind::Star => { + let starred_expr = self.parse_starred_expression(context); + + if left_precedence > OperatorPrecedence::None + || !context.is_starred_expression_allowed() + { + self.add_error(ParseErrorType::InvalidStarredExpressionUsage, &starred_expr); + } + + return Expr::Starred(starred_expr).into(); + } + TokenKind::Await => { + let await_expr = self.parse_await_expression(); + + // `await` expressions cannot be nested + if left_precedence >= OperatorPrecedence::Await { + self.add_error( + ParseErrorType::OtherError( + "await expression cannot be used here".to_string(), + ), + &await_expr, + ); + } + + return Expr::Await(await_expr).into(); + } + TokenKind::Lambda => { + // Lambda expression isn't allowed in this context but we'll still parse it and + // report an error for better recovery. + let lambda_expr = self.parse_lambda_expr(true); + self.add_error(ParseErrorType::InvalidLambdaExpressionUsage, &lambda_expr); + return Expr::Lambda(lambda_expr).into(); + } + TokenKind::Yield => { + let expr = self.parse_yield_expression(); + + if left_precedence > OperatorPrecedence::None + || !context.is_yield_expression_allowed() + { + self.add_error(ParseErrorType::InvalidYieldExpressionUsage, &expr); + } + + return expr.into(); + } + _ => {} + } + + let lhs = self.parse_atom(context); + + ParsedExpr { + expr: self.parse_postfix_expression(lhs.expr, start, context), + is_parenthesized: lhs.is_parenthesized, + } + } + + /// Checks the bitwise-or operand required by sequence and dictionary displays. + /// The caller can defer this check until a dictionary comprehension is ruled out. + fn validate_unpacking_precedence( + &mut self, + parsed_expr: &ParsedExpr, + start: TextSize, + double_starred: bool, + ) { + if parsed_expr.is_parenthesized { + // Parentheses resets the precedence, so we don't need to validate it. + return; + } + if parsed_expr.expr.is_if_expr() { + self.add_error( + ParseErrorType::InvalidConditionalUnpacking { double_starred }, + TextRange::new(start, parsed_expr.end()), + ); + return; + } + + self.validate_bitwise_or_precedence(parsed_expr); + } + + fn validate_bitwise_or_precedence(&mut self, parsed_expr: &ParsedExpr) { + if parsed_expr.is_parenthesized { + return; + } + let expr_name = match parsed_expr.expr { + Expr::Compare(_) => "comparison", + Expr::BoolOp(_) + | Expr::UnaryOp(ast::ExprUnaryOp { + op: ast::UnaryOp::Not, + .. + }) => "boolean", + Expr::If(_) => "conditional", + Expr::Lambda(_) => "lambda", + _ => return, + }; + + self.add_error( + ParseErrorType::OtherError(format!("{expr_name} expression cannot be used here")), + parsed_expr, + ); + } + + /// Parses a name. + /// + /// For an invalid name, the `id` field will be an empty string and the `ctx` + /// field will be [`ExprContext::Invalid`]. + /// + /// See: + pub(super) fn parse_name(&mut self, context: ExpressionContext) -> ast::ExprName { + let identifier = self.parse_identifier_with_context(context); + + let ctx = if identifier.is_valid() { + ExprContext::Load + } else { + ExprContext::Invalid + }; + + ast::ExprName { + range: identifier.range, + id: identifier.id, + ctx, + node_index: AtomicNodeIndex::NONE, + } + } + + pub(super) fn parse_missing_name(&mut self) -> ast::ExprName { + let identifier = self.parse_missing_identifier(); + + ast::ExprName { + range: identifier.range, + id: identifier.id, + ctx: ExprContext::Invalid, + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses an identifier. + /// + /// For an invalid identifier, the `id` field will be an empty string. + /// + /// See: + pub(super) fn parse_identifier(&mut self) -> ast::Identifier { + self.parse_identifier_with_context(ExpressionContext::default()) + } + + fn parse_identifier_with_context(&mut self, context: ExpressionContext) -> ast::Identifier { + let range = self.current_token_range(); + + if self.at(TokenKind::Identifier) { + let name = self.bump_identifier(); + return ast::Identifier { + id: name, + range, + node_index: AtomicNodeIndex::NONE, + }; + } + + if self.current_token_kind().is_soft_keyword() { + let text = self.src_text(range); + let id = self.intern_name(text); + self.bump_soft_keyword_as_identifier(); + return ast::Identifier { + id, + range, + node_index: AtomicNodeIndex::NONE, + }; + } + + // test_err incomplete_attribute_before_for_in_delimiter + // [item. for item in xs] + // [item. async for item in xs] + // {item. for item in xs} + // (item. for item in xs) + // [item for item. in xs] + // for item. in xs: ... + if (context.is_for_excluded()) + && (self.at(TokenKind::For) + || (self.at(TokenKind::Async) && self.peek() == TokenKind::For)) + || (context.is_in_excluded() && self.at(TokenKind::In)) + { + return self.parse_missing_identifier(); + } + + if self.current_token_kind().is_keyword() { + // Non-soft keyword + self.add_error(ParseErrorType::ExpectedIdentifier, range); + + let text = self.src_text(range); + let id = self.intern_name(text); + self.bump_any(); + ast::Identifier { + id, + range, + node_index: AtomicNodeIndex::NONE, + } + } else { + self.parse_missing_identifier() + } + } + + fn parse_missing_identifier(&mut self) -> ast::Identifier { + self.add_error( + ParseErrorType::ExpectedIdentifier, + self.current_token_range(), + ); + + ast::Identifier { + id: Name::empty(), + range: self.missing_node_range(), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses an atom. + /// + /// See: + fn parse_atom(&mut self, context: ExpressionContext) -> ParsedExpr { + let start = self.node_start(); + + let lhs = match self.current_token_kind() { + TokenKind::Float => { + let value = self.bump_float(); + + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: Number::Float(value), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::Complex => { + let (real, imag) = self.bump_complex(); + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: Number::Complex { real, imag }, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::Int => { + let value = self.bump_int(); + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: Number::Int(value), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::True => { + self.bump(TokenKind::True); + Expr::BooleanLiteral(ast::ExprBooleanLiteral { + value: true, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::False => { + self.bump(TokenKind::False); + Expr::BooleanLiteral(ast::ExprBooleanLiteral { + value: false, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::None => { + self.bump(TokenKind::None); + Expr::NoneLiteral(ast::ExprNoneLiteral { + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::Ellipsis => { + self.bump(TokenKind::Ellipsis); + Expr::EllipsisLiteral(ast::ExprEllipsisLiteral { + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::Identifier => Expr::Name(self.parse_name(context)), + TokenKind::IpyEscapeCommand => { + Expr::IpyEscapeCommand(self.parse_ipython_escape_command_expression()) + } + TokenKind::String | TokenKind::FStringStart | TokenKind::TStringStart => { + self.parse_strings() + } + TokenKind::Lpar => { + return self.parse_parenthesized_expression(); + } + TokenKind::Lsqb => self.parse_list_like_expression(), + TokenKind::Lbrace => self.parse_set_or_dict_like_expression(), + + kind => { + if kind.is_keyword() { + Expr::Name(self.parse_name(context)) + } else { + self.add_error( + ParseErrorType::ExpectedExpression, + self.current_token_range(), + ); + Expr::Name(ast::ExprName { + range: self.missing_node_range(), + id: Name::empty(), + ctx: ExprContext::Invalid, + node_index: AtomicNodeIndex::NONE, + }) + } + } + }; + + lhs.into() + } + + /// Parses a postfix expression in a loop until there are no postfix expressions left to parse. + /// + /// For a given left-hand side, a postfix expression can begin with either `(` for a call + /// expression, `[` for a subscript expression, or `.` for an attribute expression. + /// + /// This method does nothing if the current token is not a candidate for a postfix expression. + fn parse_postfix_expression( + &mut self, + mut lhs: Expr, + start: TextSize, + context: ExpressionContext, + ) -> Expr { + loop { + lhs = match self.current_token_kind() { + TokenKind::Lpar => Expr::Call(self.parse_call_expression(lhs, start)), + TokenKind::Lsqb => Expr::Subscript(self.parse_subscript_expression(lhs, start)), + TokenKind::Dot => { + Expr::Attribute(self.parse_attribute_expression(lhs, start, context)) + } + _ => break lhs, + }; + } + } + + /// Parse a call expression. + /// + /// The function name is parsed by the caller and passed as `func` along with + /// the `start` position of the call expression. + /// + /// # Panics + /// + /// If the parser isn't position at a `(` token. + /// + /// See: + fn parse_call_expression(&mut self, func: Expr, start: TextSize) -> ast::ExprCall { + let arguments = self.parse_arguments(ArgumentsContext::Call); + debug_assert_eq!(self.node_range(start).end(), arguments.end()); + + ast::ExprCall { + func: Box::new(func), + arguments, + range_start: start, + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses an argument list. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `(` token. + /// + /// See: + pub(super) fn parse_arguments(&mut self, context: ArgumentsContext) -> ast::Arguments { + let start = self.node_start(); + self.bump(TokenKind::Lpar); + + if self.eat(TokenKind::Rpar) { + return ast::Arguments { + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + args: ThinVec::new(), + keywords: ThinVec::default(), + runtime_args: None, + runtime_bases: None, + }; + } + + let args_snapshot = self.expr_scratch.snapshot(); + let keywords_snapshot = self.keyword_scratch.snapshot(); + let mut seen_keyword_argument = false; // foo = 1 + let mut seen_keyword_unpacking = false; // **foo + let mut first_argument = true; + // The first positional argument after a keyword argument, reported where the arguments + // end, and the number of errors before it. + let mut positional_after_keyword: Option<(ParseErrorType, usize)> = None; + + let has_trailing_comma = + self.parse_comma_separated_list(RecoveryContextKind::Arguments, |parser| { + let argument_start = parser.node_start(); + let is_first_argument = first_argument; + first_argument = false; + if parser.eat(TokenKind::DoubleStar) { + let mut value = parser.parse_conditional_expression_or_higher(); + if is_first_argument + && matches!(context, ArgumentsContext::Call) + && matches!( + parser.current_token_kind(), + TokenKind::Async | TokenKind::For + ) + { + let for_range = parser.current_token_range(); + let for_kind = parser.current_token_kind(); + let errors_start = parser.errors.len(); + let generators = parser.parse_generators(); + let (error, location) = if parser + .has_valid_first_comprehension_clause(&generators, errors_start) + { + ( + ParseErrorType::InvalidComprehensionDictUnpacking { + generator: true, + }, + TextRange::new(argument_start, value.end()), + ) + } else { + ( + ParseErrorType::ExpectedToken { + expected: TokenKind::Comma, + found: for_kind, + }, + for_range, + ) + }; + parser + .errors + .insert(errors_start, crate::ParseError { error, location }); + } + + if parser.eat(TokenKind::Equal) { + // test_err args_keyword_unpacking_assignment + // f(**a=1) + value = parser.parse_conditional_expression_or_higher(); + parser.add_error( + ParseErrorType::OtherError( + "cannot assign to keyword argument unpacking".to_string(), + ), + TextRange::new(argument_start, value.end()), + ); + } + + parser.keyword_scratch.push(ast::Keyword { + arg: None, + value: value.expr, + range: parser.node_range(argument_start), + node_index: AtomicNodeIndex::NONE, + }); + + seen_keyword_unpacking = true; + } else { + let start = parser.node_start(); + let mut parsed_expr = parser.parse_named_expression_without_assignment_check( + ExpressionContext::starred_conditional(), + ); + + match parser.current_token_kind() { + TokenKind::Async | TokenKind::For => { + if parsed_expr.is_unparenthesized_starred_expr() { + parser.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnpackingInComprehension( + ComprehensionUnpackingKind::IterableInGenerator, + ), + parsed_expr.range(), + ); + } + + parsed_expr = Expr::Generator(parser.parse_generator_expression( + parsed_expr.expr, + start, + Parenthesized::No, + )) + .into(); + } + _ => { + if seen_keyword_unpacking + && parsed_expr.is_unparenthesized_starred_expr() + { + // The error starts at the comma before the argument. + let start = parser + .token_before(parsed_expr.start()) + .filter(|(kind, _)| *kind == TokenKind::Comma) + .map_or(parsed_expr.start(), |(_, range)| range.start()); + parser.add_error( + ParseErrorType::InvalidArgumentUnpackingOrder, + TextRange::new(start, parsed_expr.end()), + ); + } + } + } + + let arg_range = parser.node_range(start); + let equal_range = parser.current_token_range(); + if parser.eat(TokenKind::Equal) { + seen_keyword_argument = true; + let keyword_target_range = + TextRange::new(parsed_expr.start(), equal_range.end()); + if parsed_expr.is_parenthesized + && parsed_expr.is_name_expr() + && parser.options.target_version >= PythonVersion::PY38 + { + // test_err args_parenthesized_keyword_name + // f((a)=1) + parser.add_error( + ParseErrorType::OtherError( + EXPRESSION_CONTAINS_ASSIGNMENT.to_string(), + ), + keyword_target_range, + ); + } + let unpacking = parsed_expr.is_unparenthesized_starred_expr(); + let arg = if let ParsedExpr { + expr: Expr::Name(ident_expr), + is_parenthesized, + } = parsed_expr + { + // test_ok parenthesized_kwarg_py37 + // # parse_options: {"target-version": "3.7"} + // f((a)=1) + + // test_err parenthesized_kwarg_py38 + // # parse_options: {"target-version": "3.8"} + // f((a)=1) + // f((a) = 1) + // f( ( a ) = 1) + + if is_parenthesized { + parser.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName, + arg_range, + ); + } + + ast::Identifier { + id: ident_expr.id, + range: ident_expr.range, + node_index: AtomicNodeIndex::NONE, + } + } else { + // TODO(dhruvmanila): Parser shouldn't drop the `parsed_expr` if it's + // not a name expression. We could add the expression into `args` but + // that means the error is a missing comma instead. + + // test_err args_invalid_keyword_name + // f(a.b=1) + // f(True=1) + // f(*a=1) + let message = match &parsed_expr.expr { + _ if unpacking => None, + Expr::BooleanLiteral(_) | Expr::NoneLiteral(_) + if !parsed_expr.is_parenthesized => + { + Some(format!( + "cannot assign to {}", + ExpressionKind::of(&parsed_expr.expr) + )) + } + _ => Some(EXPRESSION_CONTAINS_ASSIGNMENT.to_string()), + }; + if let Some(message) = message { + parser.add_error( + ParseErrorType::OtherError(message), + keyword_target_range, + ); + } + ast::Identifier { + id: Name::empty(), + range: parsed_expr.range(), + node_index: AtomicNodeIndex::NONE, + } + }; + + if arg.is_valid() + && (parser.at(TokenKind::Comma) || parser.at(TokenKind::Rpar)) + { + // test_err args_missing_keyword_value + // f(a=) + // f(a, b=, c) + parser.add_error( + ParseErrorType::OtherError( + "expected argument value expression".to_string(), + ), + TextRange::new(arg.start(), equal_range.end()), + ); + } + + let value = parser.parse_conditional_expression_or_higher(); + + if unpacking { + parser.add_error( + ParseErrorType::OtherError( + "cannot assign to iterable argument unpacking".to_string(), + ), + TextRange::new(arg.start(), value.end()), + ); + } else if arg.is_valid() + && matches!( + parser.current_token_kind(), + TokenKind::For | TokenKind::Async + ) + { + // test_err args_keyword_generator + // f(a=x for x in y) + parser.add_error( + ParseErrorType::AssignmentInsteadOfComparison, + TextRange::new(arg.start(), equal_range.end()), + ); + } + + parser.keyword_scratch.push(ast::Keyword { + arg: Some(arg), + value: value.expr, + range: parser.node_range(argument_start), + node_index: AtomicNodeIndex::NONE, + }); + } else { + if !parsed_expr.is_unparenthesized_starred_expr() + && positional_after_keyword.is_none() + { + if seen_keyword_unpacking { + positional_after_keyword = Some(( + ParseErrorType::PositionalAfterKeywordUnpacking, + parser.errors.len(), + )); + } else if seen_keyword_argument { + positional_after_keyword = Some(( + ParseErrorType::PositionalAfterKeywordArgument, + parser.errors.len(), + )); + } + } + parser.expr_scratch.push(parsed_expr.expr); + } + } + }); + + if let Some((error, index)) = positional_after_keyword { + // test_err args_positional_after_keyword + // f(a=1, b) + // f(**a, b, **c) + self.errors.insert( + index, + ParseError { + error, + location: self.current_token_range(), + }, + ); + } + + self.expect(TokenKind::Rpar); + + let keywords = self.keyword_scratch.take_thin_vec(keywords_snapshot); + let arguments = ast::Arguments { + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + args: self.expr_scratch.take_thin_vec(args_snapshot), + keywords, + runtime_args: None, + runtime_bases: None, + }; + + self.validate_arguments(&arguments, has_trailing_comma, context); + + arguments + } + + /// Parses a subscript expression. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `[` token. + /// + /// See: + fn parse_subscript_expression( + &mut self, + mut value: Expr, + start: TextSize, + ) -> ast::ExprSubscript { + self.bump(TokenKind::Lsqb); + + // To prevent the `value` context from being `Del` within a `del` statement, + // we set the context as `Load` here. + helpers::set_expr_ctx(&mut value, ExprContext::Load); + + // Slice range doesn't include the `[` token. + let slice_start = self.node_start(); + + // Create an error when receiving an empty slice to parse, e.g. `x[]` + if self.eat(TokenKind::Rsqb) { + let slice_range = self.node_range(slice_start); + self.add_error(ParseErrorType::EmptySlice, slice_range); + + return ast::ExprSubscript { + value: Box::new(value), + slice: Box::new(Expr::Name(ast::ExprName { + range: slice_range, + id: Name::empty(), + ctx: ExprContext::Invalid, + node_index: AtomicNodeIndex::NONE, + })), + ctx: ExprContext::Load, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }; + } + + let mut slice = self.parse_slice(); + + // If there are more than one element in the slice, we need to create a tuple + // expression to represent it. + if self.eat(TokenKind::Comma) { + let slices_snapshot = self.expr_scratch.snapshot(); + self.expr_scratch.push(slice); + + self.parse_comma_separated_list(RecoveryContextKind::Slices, |parser| { + let slice = parser.parse_slice(); + parser.expr_scratch.push(slice); + }); + + slice = Expr::Tuple(ast::ExprTuple { + elts: self.expr_scratch.take(slices_snapshot), + ctx: ExprContext::Load, + range: self.node_range(slice_start), + parenthesized: false, + node_index: AtomicNodeIndex::NONE, + runtime_elts: None, + }); + } else if slice.is_starred_expr() { + // If the only slice element is a starred expression, that is represented + // using a tuple expression with a single element. This is the second case + // in the `slices` rule in the Python grammar. + slice = Expr::Tuple(ast::ExprTuple { + elts: vec![slice], + ctx: ExprContext::Load, + range: self.node_range(slice_start), + parenthesized: false, + node_index: AtomicNodeIndex::NONE, + runtime_elts: None, + }); + } + + self.expect(TokenKind::Rsqb); + + // test_ok star_index_py311 + // # parse_options: {"target-version": "3.11"} + // lst[*index] # simple index + // class Array(Generic[DType, *Shape]): ... # motivating example from the PEP + // lst[a, *b, c] # different positions + // lst[a, b, *c] # different positions + // lst[*a, *b] # multiple unpacks + // array[3:5, *idxs] # mixed with slices + + // test_err star_index_py310 + // # parse_options: {"target-version": "3.10"} + // lst[*index] # simple index + // class Array(Generic[DType, *Shape]): ... # motivating example from the PEP + // lst[a, *b, c] # different positions + // lst[a, b, *c] # different positions + // lst[*a, *b] # multiple unpacks + // array[3:5, *idxs] # mixed with slices + + // test_err star_slices + // array[*start:*end] + + // test_ok parenthesized_star_index_py310 + // # parse_options: {"target-version": "3.10"} + // out[(*(slice(None) for _ in range(2)), *ind)] = 1 + if let Expr::Tuple(ast::ExprTuple { + elts, + parenthesized: false, + .. + }) = &slice + { + for elt in elts.iter().filter(|elt| elt.is_starred_expr()) { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::StarExpressionInIndex, + elt.range(), + ); + } + } + + ast::ExprSubscript { + value: Box::new(value), + slice: Box::new(slice), + ctx: ExprContext::Load, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a slice expression. + /// + /// See: + fn parse_slice(&mut self) -> Expr { + const UPPER_END_SET: TokenSet = + TokenSet::new([TokenKind::Comma, TokenKind::Colon, TokenKind::Rsqb]) + .union(NEWLINE_EOF_SET); + const STEP_END_SET: TokenSet = + TokenSet::new([TokenKind::Comma, TokenKind::Rsqb]).union(NEWLINE_EOF_SET); + + // test_err named_expr_slice + // # even after 3.9, an unparenthesized named expression is not allowed in a slice + // lst[x:=1:-1] + // lst[1:x:=1] + // lst[1:3:x:=1] + + // test_err named_expr_slice_parse_error + // # parse_options: {"target-version": "3.8"} + // # before 3.9, only emit the parse error, not the unsupported syntax error + // lst[x:=1:-1] + + let start = self.node_start(); + + let lower = if self.at_expr() { + let lower = + self.parse_named_expression_or_higher(ExpressionContext::starred_conditional()); + + // This means we're in a subscript. + if self.at_ts(NEWLINE_EOF_SET.union([TokenKind::Rsqb, TokenKind::Comma].into())) { + // test_ok parenthesized_named_expr_index_py38 + // # parse_options: {"target-version": "3.8"} + // lst[(x:=1)] + + // test_ok unparenthesized_named_expr_index_py39 + // # parse_options: {"target-version": "3.9"} + // lst[x:=1] + + // test_err unparenthesized_named_expr_index_py38 + // # parse_options: {"target-version": "3.8"} + // lst[x:=1] + if lower.is_unparenthesized_named_expr() { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SequenceIndex, + ), + lower.range(), + ); + } + return lower.expr; + } + + // Now we know we're in a slice. + if !lower.is_parenthesized { + match lower.expr { + Expr::Starred(_) => { + self.add_error(ParseErrorType::InvalidStarredExpressionUsage, &lower); + } + Expr::Named(_) => { + self.add_error(ParseErrorType::UnparenthesizedNamedExpression, &lower); + } + _ => {} + } + } + + Some(lower.expr) + } else { + None + }; + + self.expect(TokenKind::Colon); + + let lower = lower.map(Box::new); + let upper = if self.at_ts(UPPER_END_SET) { + None + } else { + Some(Box::new(self.parse_conditional_expression_or_higher().expr)) + }; + + let step = if self.eat(TokenKind::Colon) { + if self.at_ts(STEP_END_SET) { + None + } else { + Some(Box::new(self.parse_conditional_expression_or_higher().expr)) + } + } else { + None + }; + + Expr::Slice(ast::ExprSlice { + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + lower, + upper, + step, + }) + } + + /// Parses a unary expression. + /// + /// This includes the unary arithmetic `+` and `-`, bitwise `~`, and the + /// boolean `not` operators. + /// + /// # Panics + /// + /// If the parser isn't positioned at any of the unary operators. + /// + /// See: + pub(super) fn parse_unary_expression( + &mut self, + op: UnaryOp, + context: ExpressionContext, + ) -> ast::ExprUnaryOp { + let start = self.node_start(); + self.bump(TokenKind::from(op)); + + let operand = self.parse_binary_expression_or_higher(OperatorPrecedence::from(op), context); + + ast::ExprUnaryOp { + op, + operand: Box::new(operand.expr), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses an attribute expression. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `.` token. + /// + /// See: + pub(super) fn parse_attribute_expression( + &mut self, + value: Expr, + start: TextSize, + context: ExpressionContext, + ) -> ast::ExprAttribute { + self.bump(TokenKind::Dot); + + let attr = self.parse_identifier_with_context(context); + + ast::ExprAttribute { + value: Box::new(value), + attr, + ctx: ExprContext::Load, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a boolean operation expression. + /// + /// Note that the boolean `not` operator is parsed as a unary expression and + /// not as a boolean expression. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `or` or `and` token. + /// + /// See: + fn parse_boolean_expression( + &mut self, + lhs: Expr, + start: TextSize, + op: BoolOp, + context: ExpressionContext, + ) -> ast::ExprBoolOp { + self.bump(TokenKind::from(op)); + + let values_snapshot = self.expr_scratch.snapshot(); + self.expr_scratch.push(lhs); + let mut progress = ParserProgress::default(); + + // Keep adding the expression to `values` until we see a different + // token than `operator_token`. + loop { + progress.assert_progressing(self); + + let parsed_expr = + self.parse_binary_expression_or_higher(OperatorPrecedence::from(op), context); + self.expr_scratch.push(parsed_expr.expr); + + if !self.eat(TokenKind::from(op)) { + break; + } + } + + ast::ExprBoolOp { + values: self.expr_scratch.take(values_snapshot), + op, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_values: None, + } + } + + /// Bump the appropriate token(s) for the given comparison operator. + fn bump_cmp_op(&mut self, op: CmpOp) { + let (first, second) = match op { + CmpOp::Eq => (TokenKind::EqEqual, None), + CmpOp::NotEq => (TokenKind::NotEqual, None), + CmpOp::Lt => (TokenKind::Less, None), + CmpOp::LtE => (TokenKind::LessEqual, None), + CmpOp::Gt => (TokenKind::Greater, None), + CmpOp::GtE => (TokenKind::GreaterEqual, None), + CmpOp::Is => (TokenKind::Is, None), + CmpOp::IsNot => (TokenKind::Is, Some(TokenKind::Not)), + CmpOp::In => (TokenKind::In, None), + CmpOp::NotIn => (TokenKind::Not, Some(TokenKind::In)), + }; + + self.bump(first); + if let Some(second) = second { + self.bump(second); + } + } + + /// Parse a comparison expression. + /// + /// This includes the following operators: + /// - Value comparisons: `==`, `!=`, `<`, `<=`, `>`, and `>=`. + /// - Membership tests: `in` and `not in`. + /// - Identity tests: `is` and `is not`. + /// + /// # Panics + /// + /// If the parser isn't positioned at any of the comparison operators. + /// + /// See: + fn parse_comparison_expression( + &mut self, + lhs: Expr, + start: TextSize, + op: CmpOp, + context: ExpressionContext, + ) -> ast::ExprCompare { + self.bump_cmp_op(op); + + let operands_snapshot = self.expr_scratch.snapshot(); + self.expr_scratch.push(lhs); + let mut operators = vec![op]; + + let mut progress = ParserProgress::default(); + + loop { + progress.assert_progressing(self); + + let comparator = self + .parse_binary_expression_or_higher( + OperatorPrecedence::ComparisonsMembershipIdentity, + context, + ) + .expr; + self.expr_scratch.push(comparator); + + let next_token = self.current_token_kind(); + if matches!(next_token, TokenKind::In) && context.is_in_excluded() { + break; + } + + let next_next_token = + matches!(next_token, TokenKind::Is | TokenKind::Not).then(|| self.peek()); + let Some(next_op) = helpers::token_kind_to_cmp_op(next_token, next_next_token) else { + break; + }; + + self.bump_cmp_op(next_op); + operators.push(next_op); + } + + ast::ExprCompare { + ops: operators.into_boxed_slice(), + operands: self.expr_scratch.take(operands_snapshot), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_comparators: None, + } + } + + /// Parses all kinds of strings and implicitly concatenated strings. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `String`, `FStringStart`, or `TStringStart` token. + /// + /// See: (Search "strings:") + pub(super) fn parse_strings(&mut self) -> Expr { + const STRING_START_SET: TokenSet = TokenSet::new([ + TokenKind::String, + TokenKind::FStringStart, + TokenKind::TStringStart, + ]); + + let start = self.node_start(); + let first = self.parse_string(); + + if !self.at_ts(STRING_START_SET) { + return first.into(); + } + + let mut strings = Vec::with_capacity(2); + strings.push(first); + + let mut progress = ParserProgress::default(); + + while self.at_ts(STRING_START_SET) { + progress.assert_progressing(self); + strings.push(self.parse_string()); + } + + let range = self.node_range(start); + self.handle_implicitly_concatenated_strings(strings, range) + } + + /// Parses a single string, byte literal, f-string, or t-string. + fn parse_string(&mut self) -> StringType { + if self.at(TokenKind::String) { + self.parse_string_or_byte_literal() + } else if self.at(TokenKind::FStringStart) { + StringType::FString( + self.parse_interpolated_string(InterpolatedStringKind::FString) + .into(), + ) + } else if self.at(TokenKind::TStringStart) { + // test_ok template_strings_py314 + // # parse_options: {"target-version": "3.14"} + // t"{hey}" + // t'{there}' + // t"""what's + // happening?""" + + // test_err template_strings_py313 + // # parse_options: {"target-version": "3.13"} + // t"{hey}" + // t'{there}' + // t"""what's + // happening?""" + let string_type = StringType::TString( + self.parse_interpolated_string(InterpolatedStringKind::TString) + .into(), + ); + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::TemplateStrings, + string_type.range(), + ); + string_type + } else { + unreachable!("Expected to parse a string") + } + } + + /// Handles implicitly concatenated strings. + /// + /// # Panics + /// + /// If the length of `strings` is less than 2. + fn handle_implicitly_concatenated_strings( + &mut self, + strings: Vec, + range: TextRange, + ) -> Expr { + assert!(strings.len() > 1); + + let mut has_fstring = false; + let mut byte_literal_count = 0; + let mut tstring_count = 0; + for string in &strings { + match string { + StringType::FString(_) => has_fstring = true, + StringType::TString(_) => tstring_count += 1, + StringType::Bytes(_) => byte_literal_count += 1, + StringType::Str(_) => {} + } + } + let has_bytes = byte_literal_count > 0; + let has_tstring = tstring_count > 0; + + if has_bytes { + if byte_literal_count < strings.len() { + // TODO(dhruvmanila): This is not an ideal recovery because the parser + // replaces the byte literals with an invalid string literal node. Any + // downstream tools can extract the raw bytes from the range. + // + // We could convert the node into a string and mark it as invalid + // and would be clever to mark the type which is fewer in quantity. + + // test_err mixed_tstring_and_bytes_literals + // t'first' b'second' + // b'first' t'second' + // t'first' br'second' + // 'first' b'second' t'third' + // b'first' 'second' t'third' + // 'first' t'second' 'third' b'fourth' + // b'first' t'second' f'third' + + // test_err mixed_bytes_and_non_bytes_literals + // 'first' b'second' + // f'first' b'second' + // 'first' f'second' b'third' + self.report_mixed_string_literal_error(&strings, range); + } + // Only construct a byte expression if all the literals are bytes + // otherwise, we'll try either string, t-string, or f-string. This is to retain + // as much information as possible. + else { + let mut values = Vec::with_capacity(strings.len()); + for string in strings { + values.push(match string { + StringType::Bytes(value) => value, + _ => unreachable!("Expected `StringType::Bytes`"), + }); + } + return Expr::from(ast::ExprBytesLiteral { + value: ast::BytesLiteralValue::concatenated(values), + range, + node_index: AtomicNodeIndex::NONE, + }); + } + } else if has_tstring { + if tstring_count < strings.len() { + self.report_mixed_string_literal_error(&strings, range); + } + // Only construct a t-string expression if all the literals are t-strings + // otherwise, we'll try either string or f-string. This is to retain + // as much information as possible. + else { + let mut values = Vec::with_capacity(strings.len()); + for string in strings { + values.push(match string { + StringType::TString(value) => value, + _ => unreachable!("Expected `StringType::TString`"), + }); + } + return Expr::from(ast::ExprTString { + value: ast::TStringValue::concatenated(values), + range, + node_index: AtomicNodeIndex::NONE, + runtime_template_str: None, + runtime_values: None, + }); + } + } + + // TODO(dhruvmanila): Parser drops unterminated strings here as well + // because the lexer doesn't emit them. + + // test_err implicitly_concatenated_unterminated_string + // 'hello' 'world + // 1 + 1 + // 'hello' f'world {x} + // 2 + 2 + + // test_err implicitly_concatenated_unterminated_string_multiline + // ( + // 'hello' + // f'world {x} + // ) + // 1 + 1 + // ( + // 'first' + // 'second + // f'third' + // ) + // 2 + 2 + + if !has_fstring && !has_tstring { + let mut values = Vec::with_capacity(strings.len()); + for string in strings { + values.push(match string { + StringType::Str(value) => value, + _ => ast::StringLiteral::invalid(string.range()), + }); + } + return Expr::from(ast::ExprStringLiteral { + value: ast::StringLiteralValue::concatenated(values), + range, + node_index: AtomicNodeIndex::NONE, + }); + } + + let mut parts = Vec::with_capacity(strings.len()); + for string in strings { + match string { + StringType::FString(fstring) => parts.push(ast::FStringPart::FString(fstring)), + StringType::Str(string) => parts.push(ast::FStringPart::Literal(string)), + // Bytes and Template strings are invalid at this point + // and stored as invalid string literal parts in the + // f-string + StringType::TString(tstring) => parts.push(ast::FStringPart::Literal( + ast::StringLiteral::invalid(tstring.range()), + )), + StringType::Bytes(bytes) => parts.push(ast::FStringPart::Literal( + ast::StringLiteral::invalid(bytes.range()), + )), + } + } + + Expr::from(ast::ExprFString { + value: ast::FStringValue::concatenated(parts), + range, + node_index: AtomicNodeIndex::NONE, + runtime_joined_str: None, + runtime_values: None, + }) + } + + fn report_mixed_string_literal_error(&mut self, strings: &[StringType], range: TextRange) { + // CPython reports the first incompatible pair. A t-string mismatch takes + // precedence over a bytes mismatch within that pair. + for pair in strings.windows(2) { + let message = match pair { + [StringType::TString(_), StringType::TString(_)] + | [StringType::Bytes(_), StringType::Bytes(_)] => continue, + [StringType::TString(_), _] | [_, StringType::TString(_)] => { + "cannot mix t-string literals with string or bytes literals" + } + [StringType::Bytes(_), _] | [_, StringType::Bytes(_)] => { + "cannot mix bytes and nonbytes literals" + } + _ => continue, + }; + self.add_error(ParseErrorType::OtherError(message.to_string()), range); + break; + } + } + + /// Parses a single string or byte literal. + /// + /// This does not handle implicitly concatenated strings. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `String` token. + /// + /// See: + fn parse_string_or_byte_literal(&mut self) -> StringType { + let range = self.current_token_range(); + let flags = self.tokens.current_flags().as_any_string_flags(); + + let value = self.bump_string_value(); + + match parse_string_literal(value, flags, range) { + Ok(string) => string, + Err(error) => { + let location = error.location(); + self.add_error(ParseErrorType::Lexical(error.into_error()), location); + + if flags.is_byte_string() { + // test_err invalid_byte_literal + // b'123a𝐁c' + // rb"a𝐁c123" + // b"""123a𝐁c""" + StringType::Bytes(ast::BytesLiteral { + value: Box::new([]), + range, + flags: ast::BytesLiteralFlags::from(flags).with_invalid(), + node_index: AtomicNodeIndex::NONE, + }) + } else { + // test_err invalid_string_literal + // 'hello \N{INVALID} world' + // """hello \N{INVALID} world""" + StringType::Str(ast::StringLiteral { + value: "".into(), + range, + flags: ast::StringLiteralFlags::from(flags).with_invalid(), + node_index: AtomicNodeIndex::NONE, + }) + } + } + } + } + + /// Parses an f/t-string. + /// + /// This does not handle implicitly concatenated strings. + /// + /// # Panics + /// + /// If the parser isn't positioned at an `FStringStart` or + /// `TStringStart` token. + /// + /// See: (Search "fstring:" or "tstring:") + /// See: + fn parse_interpolated_string( + &mut self, + kind: InterpolatedStringKind, + ) -> InterpolatedStringData { + let start = self.node_start(); + let mut flags = self.tokens.current_flags().as_any_string_flags(); + + self.bump(kind.start_token()); + let outer_escape_error = self.interpolated_string_escape_error.take(); + let elements = self.parse_interpolated_string_elements( + flags, + InterpolatedStringElementsKind::Regular(kind), + kind, + ); + + let end_range = self.current_token_range(); + if !self.expect(kind.end_token()) { + flags = flags.with_unclosed(true); + } + // test_err interpolated_string_escape_error + // f'a\N b{x}' + // t'{x}\x1' 'c' + if let Some(error) = std::mem::replace( + &mut self.interpolated_string_escape_error, + outer_escape_error, + ) { + self.add_error(ParseErrorType::Lexical(error), end_range); + } + + InterpolatedStringData { + elements, + range: self.node_range(start), + flags, + } + } + + /// Check `range` for comment tokens, report an `UnsupportedSyntaxError` for each one found, + /// and return whether any comments were found. + fn check_fstring_comments(&mut self, range: TextRange) -> bool { + let mut has_comments = false; + + self.unsupported_syntax_errors.extend( + self.tokens + .in_range(range) + .iter() + .filter(|token| token.kind().is_comment()) + .map(|token| { + has_comments = true; + UnsupportedSyntaxError { + kind: UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Comment), + range: token.range(), + target_version: self.options.target_version, + } + }), + ); + + has_comments + } + + /// Parses a list of f/t-string elements. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `{`, `FStringMiddle`, + /// or `TStringMiddle` token. + fn parse_interpolated_string_elements( + &mut self, + flags: ast::AnyStringFlags, + elements_kind: InterpolatedStringElementsKind, + string_kind: InterpolatedStringKind, + ) -> ast::InterpolatedStringElements { + let mut elements = vec![]; + let middle_token_kind = string_kind.middle_token(); + + self.parse_list( + RecoveryContextKind::InterpolatedStringElements(elements_kind), + |parser| { + let element = match parser.current_token_kind() { + TokenKind::Lbrace => ast::InterpolatedStringElement::from( + parser.parse_interpolated_element(flags, string_kind), + ), + tok if tok == middle_token_kind => { + let range = parser.current_token_range(); + let value = parser.current_token_text(); + parser.bump(middle_token_kind); + InterpolatedStringElement::Literal( + parse_interpolated_string_literal_element(value, flags, range) + .unwrap_or_else(|lex_error| { + // test_err invalid_fstring_literal_element + // f'hello \N{INVALID} world' + // f"""hello \N{INVALID} world""" + parser + .interpolated_string_escape_error + .get_or_insert(lex_error.into_error()); + ast::InterpolatedStringLiteralElement { + value: "".into(), + range, + node_index: AtomicNodeIndex::NONE, + } + }), + ) + } + // `Invalid` tokens are created when there's a lexical error, so + // we ignore it here to avoid creating unexpected token errors + TokenKind::Unknown => { + parser.bump_any(); + return; + } + tok => { + // This should never happen because the list parsing will only + // call this closure for the above token kinds which are the same + // as in the FIRST set. + unreachable!( + "{}: unexpected token `{tok:?}` at {:?}", + string_kind, + parser.current_token_range() + ); + } + }; + elements.push(element); + }, + ); + + ast::InterpolatedStringElements::from(elements) + } + + /// Parses an f/t-string expression element. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `{` token. + /// Returns `true` if the tokenizer stops at the current token. + fn at_tokenizer_error(&self) -> bool { + self.tokens + .has_tokenizer_error_in(self.current_token_range()) + } + + /// Reports a replacement field expression that does not start with a valid expression, or + /// whose longest valid prefix is not followed by `=`, `!`, `:` or `}`. The expression starts + /// at `expression_start`, and its errors follow the first `errors_before` errors. + fn check_replacement_field_expression( + &mut self, + expr: &Expr, + expression_start: TextRange, + errors_before: usize, + string_kind: InterpolatedStringKind, + ) { + // test_err f_string_invalid_expression_start + // f"{.}" + // f"{**x}" + // f"{+;}" + // t"{x:{/5}}" + + // test_err f_string_expected_separator_after_expression + // f"{x;y}" + // f"{1 +}" + // f"{a, b and}" + // f"{x.}" + // A tokenizer error up to where the expression fails comes first. + let failure_end = self + .errors + .get(errors_before) + .map_or(self.current_token_range(), |error| error.location) + .end(); + if self + .tokens + .has_tokenizer_error_in(TextRange::new(expression_start.start(), failure_end)) + { + return; + } + let (error, range) = match self.errors.get(errors_before) { + Some(first) + if matches!( + first.error, + ParseErrorType::ExpectedExpression | ParseErrorType::ExpectedIdentifier + ) => + { + match self.complete_prefix_end(expr, errors_before) { + None => ( + InterpolatedStringErrorType::ExpectedExpressionAfterLbrace, + expression_start, + ), + Some(end) => ( + InterpolatedStringErrorType::ExpectedSeparatorAfterExpression, + self.token_after(end).1, + ), + } + } + Some(_) => return, + None if replacement_field_separator(self.current_token_kind()).is_some() => return, + None => ( + InterpolatedStringErrorType::ExpectedSeparatorAfterExpression, + self.current_token_range(), + ), + }; + self.errors.insert( + errors_before, + ParseError { + error: ParseErrorType::from_interpolated_string_error(error, string_kind), + location: range, + }, + ); + } + + /// Returns the end of the longest prefix of `expr` that is a complete expression, where an + /// expression is incomplete if a missing expression or identifier after the first + /// `errors_before` errors is inside it, or `None` if no prefix is complete. + fn complete_prefix_end(&self, expr: &Expr, errors_before: usize) -> Option { + let is_complete = |expr: &Expr| { + !self.errors[errors_before..].iter().any(|error| { + matches!( + error.error, + ParseErrorType::ExpectedExpression | ParseErrorType::ExpectedIdentifier + ) && expr.range().contains_inclusive(error.location.start()) + }) + }; + if is_complete(expr) { + return Some(expr.end()); + } + let operands: Vec<&Expr> = match expr { + Expr::BinOp(ast::ExprBinOp { left, right, .. }) => vec![left, right], + Expr::BoolOp(ast::ExprBoolOp { values, .. }) => values.iter().collect(), + Expr::Compare(ast::ExprCompare { operands, .. }) => operands.iter().collect(), + Expr::Tuple(ast::ExprTuple { + elts, + parenthesized: false, + .. + }) => elts.iter().collect(), + Expr::Attribute(ast::ExprAttribute { value, .. }) + | Expr::Subscript(ast::ExprSubscript { value, .. }) => vec![value], + Expr::Call(ast::ExprCall { func, .. }) => vec![func], + Expr::If(ast::ExprIf { body, .. }) => vec![body], + _ => return None, + }; + let mut end = None; + for operand in operands { + if is_complete(operand) { + end = Some(operand.end()); + continue; + } + if let Some(operand_end) = self.complete_prefix_end(operand, errors_before) { + return Some(operand_end); + } + break; + } + // A trailing comma ends a valid tuple. + if expr.is_tuple_expr() + && let Some(end) = end + { + let (kind, range) = self.token_after(end); + if kind == TokenKind::Comma { + return Some(range.end()); + } + } + end + } + + fn parse_interpolated_element( + &mut self, + flags: ast::AnyStringFlags, + string_kind: InterpolatedStringKind, + ) -> ast::InterpolatedElement { + let start = self.node_start(); + self.bump(TokenKind::Lbrace); + + self.tokens + .re_lex_string_token_in_interpolation_element(string_kind); + + // test_err f_string_empty_expression + // f"{}" + // f"{ }" + + // test_err t_string_empty_expression + // # parse_options: {"target-version": "3.14"} + // t"{}" + // t"{ }" + + // test_err f_string_invalid_starred_expr + // # Starred expression inside f-string has a minimum precedence of bitwise or. + // f"{*}" + // f"{*x and y}" + // f"{*yield x}" + + // test_err t_string_invalid_starred_expr + // # parse_options: {"target-version": "3.14"} + // # Starred expression inside t-string has a minimum precedence of bitwise or. + // t"{*}" + // t"{*x and y}" + // t"{*yield x}" + + // test_err f_string_expression_required_before_separator + // f"{=x}" + // f"{!r}" + // t"{:x}" + let expression_start = self.current_token_range(); + let errors_before = self.errors.len(); + if let Some(separator) = replacement_field_separator(self.current_token_kind()) { + self.add_error( + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::ExpressionRequiredBefore(separator), + string_kind, + ), + expression_start, + ); + } + + // test_err f_string_lambda_without_parentheses + // f"{lambda x: x}" + + // test_err t_string_lambda_without_parentheses + // # parse_options: {"target-version": "3.14"} + // t"{lambda x: x}" + let outer_field = self + .replacement_field + .replace((string_kind, self.bracket_level())); + let value = self.parse_expression_list(ExpressionContext::yield_or_starred_bitwise_or()); + self.replacement_field = outer_field; + self.check_replacement_field_expression( + &value.expr, + expression_start, + errors_before, + string_kind, + ); + + let debug_text = if self.eat(TokenKind::Equal) { + // test_err f_string_expected_separator_after_debug + // f"{x=y}" + if !self.at_ts(TokenSet::new([ + TokenKind::Exclamation, + TokenKind::Colon, + TokenKind::Rbrace, + ])) && !self.at_tokenizer_error() + { + self.add_error( + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::ExpectedSeparatorAfterDebug, + string_kind, + ), + self.current_token_range(), + ); + } + let leading_range = TextRange::new(start + "{".text_len(), value.start()); + let trailing_range = TextRange::new(value.end(), self.current_token_range().start()); + Some(ast::DebugText::new( + self.src_text(leading_range), + self.src_text(value.range()), + self.src_text(trailing_range), + )) + } else { + None + }; + + let conversion = if self.eat(TokenKind::Exclamation) { + // Ensure that the `r` is lexed as an identifier token instead of a raw string + // in `f{abc!r"` (note the missing `}`). + self.tokens.re_lex_raw_string_in_format_spec(); + + let conversion_flag_range = self.current_token_range(); + if self.at(TokenKind::Colon) || self.at(TokenKind::Rbrace) { + // test_err f_string_missing_conversion_flag + // f"{x!}" + // f"{x=!:}" + self.add_error( + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::MissingConversionFlag, + string_kind, + ), + conversion_flag_range, + ); + ConversionFlag::None + } else if self.at(TokenKind::Identifier) { + // test_err f_string_conversion_follows_exclamation + // f"{x! s}" + // t"{x! s}" + // f"{x! z}" + if self.prev_token_end != conversion_flag_range.start() { + self.add_error( + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::ConversionFlagNotImmediatelyAfterExclamation, + string_kind, + ), + TextRange::new(self.prev_token_end, conversion_flag_range.start()), + ); + } + let name = self.bump_identifier(); + let flag = match &*name { + "s" => ConversionFlag::Str, + "r" => ConversionFlag::Repr, + "a" => ConversionFlag::Ascii, + _ => { + // test_err f_string_invalid_conversion_flag_name_tok + // f"{x!z}" + + // test_err t_string_invalid_conversion_flag_name_tok + // # parse_options: {"target-version": "3.14"} + // t"{x!z}" + self.add_error( + ParseErrorType::OtherError(format!( + "{string_kind}: invalid conversion character '{name}': expected 's', 'r', or 'a'" + )), + conversion_flag_range, + ); + ConversionFlag::None + } + }; + // test_err f_string_expected_separator_after_conversion + // f"{x!s!}" + // f"{x!r{y}}" + if !self.at(TokenKind::Colon) + && !self.at(TokenKind::Rbrace) + && !self.at_tokenizer_error() + { + self.add_error( + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::ExpectedSeparatorAfterConversion, + string_kind, + ), + self.current_token_range(), + ); + } + flag + } else { + // test_err f_string_invalid_conversion_flag_other_tok + // f"{x!123}" + // f"{x!'a'}" + + // test_err t_string_invalid_conversion_flag_other_tok + // # parse_options: {"target-version": "3.14"} + // t"{x!123}" + // t"{x!'a'}" + self.add_error( + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::InvalidConversionFlag, + string_kind, + ), + conversion_flag_range, + ); + // TODO(dhruvmanila): Avoid dropping this token + self.bump_any(); + ConversionFlag::None + } + } else { + ConversionFlag::None + }; + + let has_format_spec = self.at(TokenKind::Colon); + let format_spec = if self.eat(TokenKind::Colon) { + let spec_start = self.node_start(); + let elements = self.with_recursion(|parser| { + parser.parse_interpolated_string_elements( + flags, + InterpolatedStringElementsKind::FormatSpec(string_kind), + string_kind, + ) + }); + Some(Box::new(ast::InterpolatedStringFormatSpec { + range: self.node_range(spec_start), + elements, + node_index: AtomicNodeIndex::NONE, + })) + } else { + None + }; + + self.tokens + .re_lex_string_token_in_interpolation_element(string_kind); + + // We're using `eat` here instead of `expect` to use the f-string specific error type. + if !self.eat(TokenKind::Rbrace) { + // TODO(dhruvmanila): This requires some changes in the lexer. One of them + // would be to emit `FStringEnd`. Currently, the following test cases doesn't + // really work as expected. Refer https://github.com/astral-sh/ruff/pull/10372 + + // test_err f_string_unclosed_lbrace + // f"{" + // f"{foo!r" + // f"{foo=" + // f"{" + // f"""{""" + + // test_err t_string_unclosed_lbrace + // # parse_options: {"target-version": "3.14"} + // t"{" + // t"{foo!r" + // t"{foo=" + // t"{" + // t"""{""" + + // The lexer does emit `FStringEnd` for the following test cases: + + // test_err f_string_unclosed_lbrace_in_format_spec + // f"hello {x:" + // f"hello {x:.3f" + + // test_err t_string_unclosed_lbrace_in_format_spec + // # parse_options: {"target-version": "3.14"} + // t"hello {x:" + // t"hello {x:.3f" + + // test_err f_string_unclosed_format_spec + // f"{x!s:" + let error = if has_format_spec { + InterpolatedStringErrorType::UnclosedFormatSpec + } else { + InterpolatedStringErrorType::UnclosedLbrace + }; + self.add_error( + ParseErrorType::from_interpolated_string_error(error, string_kind), + self.current_token_range(), + ); + } + + // test_ok pep701_f_string_py312 + // # parse_options: {"target-version": "3.12"} + // f'Magic wand: { bag['wand'] }' # nested quotes + // f"{'\n'.join(a)}" # escape sequence + // f'''A complex trick: { + // bag['bag'] # comment + // }''' + // f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting + // f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes + // f"{ + // 1 + // }" + // f"test {a \ + // } more" # line continuation + + // test_ok pep750_t_string_py314 + // # parse_options: {"target-version": "3.14"} + // t'Magic wand: { bag['wand'] }' # nested quotes + // t"{'\n'.join(a)}" # escape sequence + // t'''A complex trick: { + // bag['bag'] # comment + // }''' + // t"{t"{t"{t"{t"{t"{1+1}"}"}"}"}"}" # arbitrary nesting + // t"{t'''{"nested"} inner'''} outer" # nested (triple) quotes + // t"test {a \ + // } more" # line continuation + + // test_ok pep701_f_string_py311 + // # parse_options: {"target-version": "3.11"} + // f"outer {'# not a comment'}" + // f'outer {x:{"# not a comment"} }' + // f"""{f'''{f'{"# not a comment"}'}'''}""" + // f"""{f'''# before expression {f'# aro{f"#{1+1}#"}und #'}'''} # after expression""" + // f"""{ + // 1 + // }""" + // f"escape outside of \t {expr}\n" + // f"test\"abcd" + // f"{1:\x64}" # escapes are valid in the format spec + // f"{1:\"d\"}" # this also means that escaped outer quotes are valid + + // test_err pep701_f_string_py311 + // # parse_options: {"target-version": "3.11"} + // f'Magic wand: { bag['wand'] }' # nested quotes + // f"{'\n'.join(a)}" # escape sequence + // f'''A complex trick: { + // bag['bag'] # comment + // }''' + // f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting + // f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes + // f"{ + // 1 + // }" + // f"test {a \ + // } more" # line continuation + // f"""{f"""{x}"""}""" # mark the whole triple quote + // f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors + + // test_err pep701_nested_interpolation_py311 + // # parse_options: {"target-version": "3.11"} + // # nested interpolations also need to be checked + // f'{1: abcd "{'aa'}" }' + // f'{1: abcd "{"\n"}" }' + + // test_err nested_quote_in_format_spec_py312 + // # parse_options: {"target-version": "3.12"} + // f"{1:""}" # this is a ParseError on all versions + + // test_ok non_nested_quote_in_format_spec_py311 + // # parse_options: {"target-version": "3.11"} + // f"{1:''}" # but this is okay on all versions + let range = self.node_range(start); + + if !self.options.target_version.supports_pep_701() + && matches!(string_kind, InterpolatedStringKind::FString) + { + // We need to check the whole expression range, including any leading or trailing + // debug text, but exclude the format spec, where escapes and escaped, reused quotes + // are allowed. + let range = format_spec + .as_ref() + .map(|format_spec| TextRange::new(range.start(), format_spec.start())) + .unwrap_or(range); + + let quote_bytes = flags.quote_str().as_bytes(); + let quote_len = flags.quote_len(); + let mut has_backslash_or_comment = false; + + for slash_position in memchr::memchr_iter(b'\\', self.source[range].as_bytes()) { + has_backslash_or_comment = true; + let slash_position = TextSize::try_from(slash_position).unwrap(); + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Backslash), + TextRange::at(range.start() + slash_position, '\\'.text_len()), + ); + } + + if let Some(quote_position) = + memchr::memmem::find(self.source[range].as_bytes(), quote_bytes) + { + let quote_position = TextSize::try_from(quote_position).unwrap(); + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::NestedQuote), + TextRange::at(range.start() + quote_position, quote_len), + ); + } + + has_backslash_or_comment |= self.check_fstring_comments(range); + + // Before Python 3.12, replacement fields could only span physical lines when the + // outer f-string was triple-quoted. + if !flags.is_triple_quoted() + && !has_backslash_or_comment + && memchr::memchr2(b'\n', b'\r', self.source[range].as_bytes()).is_some() + { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::LineBreak), + TextRange::at(range.start(), '{'.text_len()), + ); + } + } + + ast::InterpolatedElement { + expression: Box::new(value.expr), + debug_text, + conversion, + format_spec, + range, + node_index: AtomicNodeIndex::NONE, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + } + } + + /// An invalid comprehension only needs one complete target/iterable clause. + /// Later incomplete filters or clauses do not undo that first successful clause. + fn has_valid_first_comprehension_clause( + &self, + generators: &[ast::Comprehension], + errors_start: usize, + ) -> bool { + generators.first().is_some_and(|clause| { + self.errors[errors_start..] + .iter() + .all(|error| error.location.start() > clause.iter.end()) + }) + } + + /// Recovers a dictionary unpack in a sequence, using the same parse for diagnostics. + fn parse_invalid_sequence_dict_unpacking(&mut self, start: TextSize, generator: bool) -> Expr { + let stars = self.current_token_range(); + let errors_start = self.errors.len(); + self.bump(TokenKind::DoubleStar); + let has_bitwise_operand = self.at_ts(EXPR_SET) + && !matches!( + self.current_token_kind(), + TokenKind::Lambda + | TokenKind::Not + | TokenKind::Yield + | TokenKind::Star + | TokenKind::DoubleStar + ); + let value = self.parse_conditional_expression_or_higher(); + let value_end = value.end(); + let operand_is_valid = self.errors.len() == errors_start; + let is_group = generator && self.at(TokenKind::Rpar); + let clause_errors_start = self.errors.len(); + let (expression, valid_comprehension) = + if matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For) { + if generator { + let expression = + self.parse_generator_expression(value.expr, start, Parenthesized::Yes); + let valid = self.has_valid_first_comprehension_clause( + &expression.generators, + clause_errors_start, + ); + (Expr::Generator(expression), valid) + } else { + let expression = self.parse_list_comprehension_expression(value.expr, start); + let valid = self.has_valid_first_comprehension_clause( + &expression.generators, + clause_errors_start, + ); + (Expr::ListComp(expression), valid) + } + } else if !generator { + ( + Expr::List(self.parse_list_expression(value.expr, start)), + false, + ) + } else if self.at(TokenKind::Comma) { + let tuple = + self.parse_tuple_expression(value.expr, start, Parenthesized::Yes, |p| { + p.parse_named_expression_or_higher(ExpressionContext::starred_bitwise_or()) + }); + (Expr::Tuple(tuple), false) + } else { + self.expect(TokenKind::Rpar); + (value.expr, false) + }; + let invalid_target = self.errors[clause_errors_start..] + .iter() + .any(|error| matches!(error.error, ParseErrorType::InvalidAssignmentTarget { .. })); + let diagnostic = if invalid_target { + None + } else if operand_is_valid && valid_comprehension { + Some(( + ParseErrorType::InvalidComprehensionDictUnpacking { generator }, + TextRange::new(stars.start(), value_end), + )) + } else if operand_is_valid && has_bitwise_operand { + Some((ParseErrorType::InvalidDictUnpacking, stars)) + } else if operand_is_valid && is_group { + Some(( + ParseErrorType::OtherError("cannot use double starred expression here".to_string()), + stars, + )) + } else if !operand_is_valid || !has_bitwise_operand { + Some((ParseErrorType::ExpectedExpression, stars)) + } else { + None + }; + if let Some((error, location)) = diagnostic { + // The invalid production starts before any clause-recovery errors. + self.errors + .insert(errors_start, crate::ParseError { error, location }); + } + expression + } + + /// Parses a list or a list comprehension expression. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `[` token. + /// + /// See: + fn parse_list_like_expression(&mut self) -> Expr { + let start = self.node_start(); + + self.bump(TokenKind::Lsqb); + + // Nice error message when having a unclosed open bracket `[` + if self.at_ts(NEWLINE_EOF_SET) { + self.add_error( + ParseErrorType::OtherError("missing closing bracket `]`".to_string()), + self.current_token_range(), + ); + } + + // Return an empty `ListExpr` when finding a `]` right after the `[` + if self.eat(TokenKind::Rsqb) { + return Expr::List(ast::ExprList { + elts: vec![], + ctx: ExprContext::Load, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_elts: None, + }); + } + + // Parse the first element with a more general rule and limit it later. + if self.at(TokenKind::DoubleStar) { + return self.parse_invalid_sequence_dict_unpacking(start, false); + } + let first_element = self.parse_named_expression_or_higher( + ExpressionContext::starred_bitwise_or().with_for_excluded(), + ); + + match self.current_token_kind() { + TokenKind::Async | TokenKind::For => { + // Parenthesized starred expression isn't allowed either but that is + // handled by the `parse_parenthesized_expression` method. + + // test_ok starred_list_comp_py315 + // # parse_options: {"target-version": "3.15"} + // [*x for x in y] + // [*factor.dims for factor in bases] + + // test_err starred_list_comp_py314 + // # parse_options: {"target-version": "3.14"} + // [*x for x in y] + if first_element.is_unparenthesized_starred_expr() { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnpackingInComprehension( + ComprehensionUnpackingKind::IterableInList, + ), + first_element.range(), + ); + } + + Expr::ListComp(self.parse_list_comprehension_expression(first_element.expr, start)) + } + _ => Expr::List(self.parse_list_expression(first_element.expr, start)), + } + } + + /// Parses a set, dict, set comprehension, or dict comprehension. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `{` token. + /// + /// See: + /// - + /// - + /// - + fn parse_set_or_dict_like_expression(&mut self) -> Expr { + // test_ok pep_798_unpacking_comprehensions_py315 + // # parse_options: {"target-version": "3.15"} + // [*x for x in y] + // {*x for x in y} + // {**x for x in y} + // (*x for x in y) + // f(*x for x in y) + // [*x async for x in y] + // {*x async for x in y} + // {**x async for x in y} + // (*x async for x in y) + + // test_err pep_798_unpacking_comprehensions_py314 + // # parse_options: {"target-version": "3.14"} + // [*x for x in y] + // {*x for x in y} + // {**x for x in y} + // (*x for x in y) + // f(*x for x in y) + + // test_err pep_798_invalid_dict_unpacking_comprehensions_py315 + // # parse_options: {"target-version": "3.15"} + // {*k: v for k, v in items} + // {k: *v for k, v in items} + // {**k: v for k, v in items} + // {k: **v for k, v in items} + + let start = self.node_start(); + self.bump(TokenKind::Lbrace); + + // Nice error message when having a unclosed open brace `{` + if self.at_ts(NEWLINE_EOF_SET) { + self.add_error( + ParseErrorType::OtherError("missing closing brace `}`".to_string()), + self.current_token_range(), + ); + } + + // Return an empty `DictExpr` when finding a `}` right after the `{` + if self.eat(TokenKind::Rbrace) { + return Expr::Dict(ast::ExprDict { + items: vec![], + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_values: None, + }); + } + + let after_brace = self.node_start(); + + if self.eat(TokenKind::DoubleStar) { + // Handle dictionary unpacking. Here, the grammar is `'**' bitwise_or` + // which requires limiting the expression. + let value = + self.parse_conditional_expression_without_comma_check(ExpressionContext::default()); + let unpack_range = TextRange::new(after_brace, value.range().end()); + + if matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For) { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnpackingInComprehension( + ComprehensionUnpackingKind::DictInDict, + ), + unpack_range, + ); + + return Expr::DictComp(self.parse_dictionary_comprehension_expression( + None, + value.expr, + start, + Some(TextRange::at(after_brace, TextSize::new(2))), + )); + } + + self.validate_unpacking_precedence(&value, after_brace, true); + + if self.at(TokenKind::Colon) { + self.add_error( + ParseErrorType::InvalidDictKeyUnpacking { + double_starred: true, + }, + unpack_range, + ); + + self.bump(TokenKind::Colon); + let dict_value = self.parse_conditional_expression_or_higher(); + + if matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For) { + return Expr::DictComp(self.parse_dictionary_comprehension_expression( + Some(value.expr), + dict_value.expr, + start, + None, + )); + } + + return Expr::Dict(self.parse_dictionary_expression( + Some(value.expr), + dict_value.expr, + start, + )); + } + + return Expr::Dict(self.parse_dictionary_expression(None, value.expr, start)); + } + + // For dictionary expressions, the key uses the `expression` rule while for + // set expressions, the element uses the `star_expression` rule. So, use the + // one that is more general and limit it later. + let key_or_element = self.parse_named_expression_or_higher( + ExpressionContext::starred_bitwise_or().with_for_excluded(), + ); + + match self.current_token_kind() { + TokenKind::Async | TokenKind::For => { + if key_or_element.is_unparenthesized_starred_expr() { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnpackingInComprehension( + ComprehensionUnpackingKind::IterableInSet, + ), + key_or_element.range(), + ); + } else if key_or_element.is_unparenthesized_named_expr() { + // test_ok parenthesized_named_expr_py38 + // # parse_options: {"target-version": "3.8"} + // {(x := 1), 2, 3} + // {(last := x) for x in range(3)} + + // test_ok unparenthesized_named_expr_py39 + // # parse_options: {"target-version": "3.9"} + // {x := 1, 2, 3} + // {last := x for x in range(3)} + + // test_err unparenthesized_named_expr_set_comp_py38 + // # parse_options: {"target-version": "3.8"} + // {last := x for x in range(3)} + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SetComprehension, + ), + key_or_element.range(), + ); + } + + Expr::SetComp(self.parse_set_comprehension_expression(key_or_element.expr, start)) + } + TokenKind::Colon => { + // Now, we know that it's either a dictionary expression or a dictionary comprehension. + // In either case, the key is limited to an `expression`. + if !key_or_element.is_parenthesized { + match key_or_element.expr { + Expr::Starred(_) => self.add_error( + ParseErrorType::InvalidDictKeyUnpacking { + double_starred: false, + }, + &key_or_element.expr, + ), + Expr::Named(_) => self.add_error( + ParseErrorType::UnparenthesizedNamedExpression, + &key_or_element, + ), + _ => {} + } + } + + let colon_range = self.current_token_range(); + self.bump(TokenKind::Colon); + let value = self.parse_dictionary_value(colon_range); + + if matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For) { + Expr::DictComp(self.parse_dictionary_comprehension_expression( + Some(key_or_element.expr), + value.expr, + start, + None, + )) + } else { + Expr::Dict(self.parse_dictionary_expression( + Some(key_or_element.expr), + value.expr, + start, + )) + } + } + _ => Expr::Set(self.parse_set_expression(key_or_element, start)), + } + } + + /// Parses an expression in parentheses, a tuple expression, or a generator expression. + /// + /// Matches the `(tuple | group | genexp)` rule in the [Python grammar]. + /// + /// [Python grammar]: https://docs.python.org/3/reference/grammar.html + fn parse_parenthesized_expression(&mut self) -> ParsedExpr { + let start = self.node_start(); + self.bump(TokenKind::Lpar); + + // Nice error message when having a unclosed open parenthesis `(` + if self.at_ts(NEWLINE_EOF_SET) { + let range = self.current_token_range(); + self.add_error( + ParseErrorType::OtherError("missing closing parenthesis `)`".to_string()), + range, + ); + } + + // Return an empty `TupleExpr` when finding a `)` right after the `(` + if self.eat(TokenKind::Rpar) { + return Expr::Tuple(ast::ExprTuple { + elts: vec![], + ctx: ExprContext::Load, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + parenthesized: true, + runtime_elts: None, + }) + .into(); + } + + // Use the more general rule of the three to parse the first element + // and limit it later. + if self.at(TokenKind::DoubleStar) { + // test_err parenthesized_double_starred_expr + // (**x) + return ParsedExpr { + expr: self.parse_invalid_sequence_dict_unpacking(start, true), + is_parenthesized: true, + }; + } + let mut parsed_expr = self.parse_named_expression_or_higher( + ExpressionContext::yield_or_starred_bitwise_or() + .with_for_excluded() + .with_starred_generator_allowed(), + ); + + match self.current_token_kind() { + TokenKind::Comma => { + // grammar: `tuple` + let tuple = + self.parse_tuple_expression(parsed_expr.expr, start, Parenthesized::Yes, |p| { + p.parse_named_expression_or_higher(ExpressionContext::starred_bitwise_or()) + }); + + ParsedExpr { + expr: tuple.into(), + is_parenthesized: false, + } + } + TokenKind::Async | TokenKind::For => { + // grammar: `genexp` + if parsed_expr.is_unparenthesized_starred_expr() { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnpackingInComprehension( + ComprehensionUnpackingKind::IterableInGenerator, + ), + parsed_expr.range(), + ); + } + + let generator = Expr::Generator(self.parse_generator_expression( + parsed_expr.expr, + start, + Parenthesized::Yes, + )); + + ParsedExpr { + expr: generator, + is_parenthesized: false, + } + } + _ => { + // grammar: `group` + if parsed_expr.expr.is_starred_expr() { + if self.at(TokenKind::Rpar) { + // test_err parenthesized_starred_expr + // (*x) + self.add_error( + ParseErrorType::OtherError( + "cannot use starred expression here".to_string(), + ), + &parsed_expr, + ); + } else { + self.add_error(ParseErrorType::InvalidStarredExpressionUsage, &parsed_expr); + } + } + + self.expect(TokenKind::Rpar); + + parsed_expr.is_parenthesized = true; + parsed_expr + } + } + } + + /// Parses multiple items separated by a comma into a tuple expression. + /// + /// Uses the `parse_func` to parse each item in the tuple. + pub(super) fn parse_tuple_expression( + &mut self, + first_element: Expr, + start: TextSize, + parenthesized: Parenthesized, + mut parse_func: impl FnMut(&mut Parser<'src>) -> ParsedExpr, + ) -> ast::ExprTuple { + // TODO(dhruvmanila): Can we remove `parse_func` and use `parenthesized` to + // determine the parsing function? + + if !self.at_sequence_end() { + self.expect(TokenKind::Comma); + } + + let elts_snapshot = self.expr_scratch.snapshot(); + self.expr_scratch.push(first_element); + + self.parse_comma_separated_list(RecoveryContextKind::TupleElements(parenthesized), |p| { + let element = parse_func(p).expr; + p.expr_scratch.push(element); + }); + + if parenthesized.is_yes() { + self.expect(TokenKind::Rpar); + } + + ast::ExprTuple { + elts: self.expr_scratch.take(elts_snapshot), + ctx: ExprContext::Load, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + parenthesized: parenthesized.is_yes(), + runtime_elts: None, + } + } + + /// Parses a list expression. + /// + /// See: + fn parse_list_expression(&mut self, first_element: Expr, start: TextSize) -> ast::ExprList { + if !self.at_sequence_end() { + self.expect(TokenKind::Comma); + } + + let elts_snapshot = self.expr_scratch.snapshot(); + self.expr_scratch.push(first_element); + + self.parse_comma_separated_list(RecoveryContextKind::ListElements, |parser| { + let element = parser + .parse_named_expression_or_higher(ExpressionContext::starred_bitwise_or()) + .expr; + parser.expr_scratch.push(element); + }); + self.check_unparenthesized_comprehension_target(start); + + self.expect(TokenKind::Rsqb); + + ast::ExprList { + elts: self.expr_scratch.take(elts_snapshot), + ctx: ExprContext::Load, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_elts: None, + } + } + + /// Reports a comprehension whose target is an unparenthesized tuple, as in + /// `[x, y for x, y in z]`. The elements of the list or set starting at `start` have been + /// parsed. + fn check_unparenthesized_comprehension_target(&mut self, start: TextSize) { + // test_err comprehension_unparenthesized_target + // [x, y for x, y in z] + // {x, for x in z} + if !matches!(self.current_token_kind(), TokenKind::For | TokenKind::Async) { + return; + } + let first_start = self.token_after(start + TextSize::from(1)).1.start(); + let end = self + .token_before(self.current_token_range().start()) + .map_or(first_start, |(_, range)| range.end()); + self.add_error( + ParseErrorType::OtherError( + "did you forget parentheses around the comprehension target?".to_string(), + ), + TextRange::new(first_start, end), + ); + } + + /// Parses a set expression. + /// + /// See: + fn parse_set_expression(&mut self, first_element: ParsedExpr, start: TextSize) -> ast::ExprSet { + if !self.at_sequence_end() { + self.expect(TokenKind::Comma); + } + + // test_err unparenthesized_named_expr_set_literal_py38 + // # parse_options: {"target-version": "3.8"} + // {x := 1, 2, 3} + // {1, x := 2, 3} + // {1, 2, x := 3} + + if first_element.is_unparenthesized_named_expr() { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SetLiteral, + ), + first_element.range(), + ); + } + + let elts_snapshot = self.expr_scratch.snapshot(); + self.expr_scratch.push(first_element.expr); + + self.parse_comma_separated_list(RecoveryContextKind::SetElements, |parser| { + let parsed_expr = + parser.parse_named_expression_or_higher(ExpressionContext::starred_bitwise_or()); + + if parsed_expr.is_unparenthesized_named_expr() { + parser.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SetLiteral, + ), + parsed_expr.range(), + ); + } + + parser.expr_scratch.push(parsed_expr.expr); + }); + self.check_unparenthesized_comprehension_target(start); + + self.expect(TokenKind::Rbrace); + + ast::ExprSet { + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + elts: self.expr_scratch.take(elts_snapshot), + runtime_elts: None, + } + } + + /// Parses a dictionary expression. + /// + /// See: + fn parse_dictionary_expression( + &mut self, + key: Option, + value: Expr, + start: TextSize, + ) -> ast::ExprDict { + if !self.at_sequence_end() { + self.expect(TokenKind::Comma); + } + + let mut items = vec![ast::DictItem { key, value }]; + + self.parse_comma_separated_list(RecoveryContextKind::DictElements, |parser| { + if parser.eat(TokenKind::DoubleStar) { + // Subsequent dictionary spreads retain the ordinary display grammar. + let value = parser + .parse_conditional_expression_without_comma_check(ExpressionContext::default()); + parser.validate_bitwise_or_precedence(&value); + items.push(ast::DictItem { + key: None, + value: value.expr, + }); + } else { + // A missing comma is not reported in a key after the first item. + let key = parser.parse_conditional_expression_without_comma_check( + ExpressionContext::starred_bitwise_or(), + ); + if key.is_unparenthesized_starred_expr() { + parser.add_error( + ParseErrorType::InvalidDictKeyUnpacking { + double_starred: false, + }, + &key, + ); + } + if !parser.at(TokenKind::Colon) { + // test_err dict_missing_colon_after_key + // {a: 1, b} + // {a: 1, b c: 2} + // {**a, b + c} + let end = key.end(); + let last_char = parser.src_text(key.range()).chars().last(); + let start = end - last_char.map_or(TextSize::default(), TextSize::of); + parser.add_error( + ParseErrorType::ExpectedColonAfterDictionaryKey, + TextRange::new(start, end), + ); + } + let colon_range = parser.current_token_range(); + parser.expect(TokenKind::Colon); + + items.push(ast::DictItem { + key: Some(key.expr), + value: parser.parse_dictionary_value(colon_range).expr, + }); + } + }); + + self.expect(TokenKind::Rbrace); + + items.shrink_to_fit(); + + ast::ExprDict { + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + items, + runtime_values: None, + } + } + + /// Parses the value of a dictionary item after the `:` at `colon_range`. + fn parse_dictionary_value(&mut self, colon_range: TextRange) -> ParsedExpr { + if self.at_ts(TokenSet::new([TokenKind::Rbrace, TokenKind::Comma])) { + // test_err dict_missing_value + // {a: } + // {a: 1, b: , c: 2} + self.add_error( + ParseErrorType::OtherError( + "expression expected after dictionary key and ':'".to_string(), + ), + colon_range, + ); + } else if matches!( + self.current_token_kind(), + TokenKind::Star | TokenKind::DoubleStar + ) { + // test_err dict_starred_value + // {a: *b} + // {a: 1, b: *c} + let start = self.node_start(); + let double_starred = self.at(TokenKind::DoubleStar); + self.bump_any(); + let value = self.parse_binary_expression_or_higher( + OperatorPrecedence::ComparisonsMembershipIdentity, + ExpressionContext::default(), + ); + self.add_error( + ParseErrorType::InvalidDictValueUnpacking { double_starred }, + TextRange::new(start, value.end()), + ); + return value; + } + self.parse_conditional_expression_or_higher() + } + + /// Parses a list of comprehension generators. + /// + /// These are the `for` and `async for` clauses in a comprehension, optionally + /// followed by `if` clauses. + /// + /// See: + fn parse_generators(&mut self) -> Vec { + const GENERATOR_SET: TokenSet = TokenSet::new([TokenKind::For, TokenKind::Async]); + + let mut generators = Vec::with_capacity(1); + let mut progress = ParserProgress::default(); + + while self.at_ts(GENERATOR_SET) { + progress.assert_progressing(self); + generators.push(self.parse_comprehension()); + } + + generators.shrink_to_fit(); + + generators + } + + /// Parses a comprehension. + /// + /// # Panics + /// + /// If the parser isn't positioned at an `async` or `for` token. + /// + /// See: + fn parse_comprehension(&mut self) -> ast::Comprehension { + let start = self.node_start(); + + let is_async = self.eat(TokenKind::Async); + + if is_async { + // test_err comprehension_missing_for_after_async + // (async) + // (x async x in iter) + self.expect(TokenKind::For); + } else { + self.bump(TokenKind::For); + } + + // The targets are a list of `bitwise_or` expressions that must be followed by `in`. + let checkpoint = self.checkpoint(); + while self.at_expr() { + self.eat(TokenKind::Star); + self.parse_simple_expression(ExpressionContext::default().with_in_excluded()); + if !self.eat(TokenKind::Comma) { + break; + } + } + let missing_in = (!self.at(TokenKind::In)).then(|| self.current_token_range()); + self.rewind(checkpoint); + let errors_before_target = self.errors.len(); + + let mut target = + self.parse_expression_list(ExpressionContext::starred_conditional().with_in_excluded()); + + if let Some(range) = missing_in { + // test_err comprehension_missing_in + // [x for x if y] + // [x for a, b y] + self.errors.insert( + errors_before_target, + ParseError { + error: ParseErrorType::OtherError( + "'in' expected after for-loop variables".to_string(), + ), + location: range, + }, + ); + } + + helpers::set_expr_ctx(&mut target.expr, ExprContext::Store); + self.validate_for_target(&target.expr); + + self.expect(TokenKind::In); + let iter = self.parse_simple_expression(ExpressionContext::default()); + + let ifs_snapshot = self.expr_scratch.snapshot(); + let mut progress = ParserProgress::default(); + + while self.eat(TokenKind::If) { + progress.assert_progressing(self); + + let parsed_expr = self.parse_simple_expression(ExpressionContext::default()); + + self.expr_scratch.push(parsed_expr.expr); + } + + ast::Comprehension { + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + target: target.expr, + iter: iter.expr, + ifs: self.expr_scratch.take(ifs_snapshot), + is_async, + runtime_ifs: None, + runtime_is_async: None, + } + } + + /// Parses a generator expression. + /// + /// The given `start` offset is the start of either the opening parenthesis if the generator is + /// parenthesized or the first token of the expression. + /// + /// See: + fn parse_generator_expression( + &mut self, + element: Expr, + start: TextSize, + parenthesized: Parenthesized, + ) -> ast::ExprGenerator { + let generators = self.parse_generators(); + + if parenthesized.is_yes() { + self.expect(TokenKind::Rpar); + } + + ast::ExprGenerator { + elt: Box::new(element), + generators, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + parenthesized: parenthesized.is_yes(), + } + } + + /// Parses a list comprehension expression. + /// + /// See: + fn parse_list_comprehension_expression( + &mut self, + element: Expr, + start: TextSize, + ) -> ast::ExprListComp { + let generators = self.parse_generators(); + + self.expect(TokenKind::Rsqb); + + ast::ExprListComp { + elt: Box::new(element), + generators, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a dictionary comprehension expression. + /// + /// See: + fn parse_dictionary_comprehension_expression( + &mut self, + key: Option, + value: Expr, + start: TextSize, + unpacking: Option, + ) -> ast::ExprDictComp { + let generators = self.parse_generators(); + + if self.at(TokenKind::Colon) + && let Some(stars) = unpacking + { + self.add_error(ParseErrorType::InvalidDictUnpacking, stars); + } + self.expect(TokenKind::Rbrace); + + ast::ExprDictComp { + key: key.map(Box::new), + value: Box::new(value), + generators: generators.into_boxed_slice(), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a set comprehension expression. + /// + /// See: + fn parse_set_comprehension_expression( + &mut self, + element: Expr, + start: TextSize, + ) -> ast::ExprSetComp { + let generators = self.parse_generators(); + + self.expect(TokenKind::Rbrace); + + ast::ExprSetComp { + elt: Box::new(element), + generators, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Returns `true` if a `*` starting at `star_start` is at the start of a parenthesized + /// expression, list, set or dictionary, or of a call argument or subscript slice. + fn is_star_expression_position(&self, star_start: TextSize) -> bool { + let previous = self.token_before(star_start).map(|(kind, _)| kind); + match self.innermost_bracket() { + Some(bracket) => { + matches!( + previous, + Some(TokenKind::Lpar | TokenKind::Lsqb | TokenKind::Lbrace) + ) || (bracket == Bracket::CallOrSubscript && previous == Some(TokenKind::Comma)) + } + None => false, + } + } + + /// Parses a starred expression with the given precedence. + /// + /// The expression is parsed with the highest precedence. If the precedence + /// of the parsed expression is lower than the given precedence, an error + /// is reported. + /// + /// For example, if the given precedence is [`StarredExpressionPrecedence::BitOr`], + /// the comparison expression is not allowed. + /// + /// Refer to the [Python grammar] for more information. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `*` token. + /// + /// [Python grammar]: https://docs.python.org/3/reference/grammar.html + fn parse_starred_expression(&mut self, context: ExpressionContext) -> ast::ExprStarred { + let start = self.node_start(); + self.bump(TokenKind::Star); + + if (!self.at_expr() || self.at(TokenKind::Star)) && self.is_star_expression_position(start) + { + // test_err starred_missing_expression_in_brackets + // f(x, *) + // [*, 1] + self.add_error( + ParseErrorType::OtherError("Invalid star expression".to_string()), + self.current_token_range(), + ); + } + + let parsed_expr = match context.starred_expression_precedence() { + StarredExpressionPrecedence::Conditional => self + .parse_conditional_expression_or_higher_impl( + // test_err starred_starred_expression + // print(* + // *[]) + // print(* *[]) + context.disallow_starred_expressions(), + ), + StarredExpressionPrecedence::BitwiseOr => { + let errors_start = self.errors.len(); + let value = self + .parse_conditional_expression_without_comma_check(ExpressionContext::default()); + let operand_is_valid = self.errors.len() == errors_start; + self.validate_unpacking_precedence(&value, start, false); + // Generator elements use `starred_expression` (`* expression`). + // Keep a display-precedence error only for an overall parse failure. + if operand_is_valid + && context.is_starred_generator_allowed() + && matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For) + && self.errors.len() == errors_start + 1 + { + let error = self.errors.pop().expect("operand precedence error exists"); + self.deferred_starred_generator_errors + .push((errors_start, error)); + } + value + } + }; + + ast::ExprStarred { + value: Box::new(parsed_expr.expr), + ctx: ExprContext::Load, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses an `await` expression. + /// + /// # Panics + /// + /// If the parser isn't positioned at an `await` token. + /// + /// See: + fn parse_await_expression(&mut self) -> ast::ExprAwait { + let start = self.node_start(); + self.bump(TokenKind::Await); + + let parsed_expr = self.parse_binary_expression_or_higher( + OperatorPrecedence::Await, + ExpressionContext::default(), + ); + + ast::ExprAwait { + value: Box::new(parsed_expr.expr), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a `yield` expression. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `yield` token. + /// + /// See: + fn parse_yield_expression(&mut self) -> Expr { + let start = self.node_start(); + self.bump(TokenKind::Yield); + + if self.eat(TokenKind::From) { + return self.parse_yield_from_expression(start); + } + + let value = self.at_expr().then(|| { + let parsed_expr = self.parse_expression_list(ExpressionContext::starred_bitwise_or()); + + // test_ok iter_unpack_yield_py37 + // # parse_options: {"target-version": "3.7"} + // rest = (4, 5, 6) + // def g(): yield (1, 2, 3, *rest) + + // test_ok iter_unpack_yield_py38 + // # parse_options: {"target-version": "3.8"} + // rest = (4, 5, 6) + // def g(): yield 1, 2, 3, *rest + // def h(): yield 1, (yield 2, *rest), 3 + + // test_err iter_unpack_yield_py37 + // # parse_options: {"target-version": "3.7"} + // rest = (4, 5, 6) + // def g(): yield 1, 2, 3, *rest + // def h(): yield 1, (yield 2, *rest), 3 + self.check_tuple_unpacking( + &parsed_expr, + UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Yield), + ); + + Box::new(parsed_expr.expr) + }); + + Expr::Yield(ast::ExprYield { + value, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + + /// Parses a `yield from` expression. + /// + /// This method should not be used directly. Use [`Parser::parse_yield_expression`] + /// even when parsing a `yield from` expression. + /// + /// See: + fn parse_yield_from_expression(&mut self, start: TextSize) -> Expr { + // Grammar: + // 'yield' 'from' expression + // + // Here, a tuple expression isn't allowed without the parentheses. But, we + // allow it here to report better error message. + // + // Now, this also solves another problem. Take the following example: + // + // ```python + // yield from x, y + // ``` + // + // If we didn't use the `parse_expression_list` method here, the parser + // would have stopped at the comma. Then, the outer expression would + // have been a tuple expression with two elements: `yield from x` and `y`. + let expr = self + .parse_expression_list(ExpressionContext::default()) + .expr; + + match &expr { + Expr::Tuple(tuple) if !tuple.parenthesized => { + self.add_error(ParseErrorType::UnparenthesizedTupleExpression, &expr); + } + _ => {} + } + + Expr::YieldFrom(ast::ExprYieldFrom { + value: Box::new(expr), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + + /// Parses a named expression (`:=`). + /// + /// # Panics + /// + /// If the parser isn't positioned at a `:=` token. + /// + /// See: + fn parse_named_expression(&mut self, mut target: Expr, start: TextSize) -> ast::ExprNamed { + self.bump(TokenKind::ColonEqual); + + if !target.is_name_expr() { + self.add_error( + ParseErrorType::InvalidNamedAssignmentTarget(ExpressionKind::of(&target)), + target.range(), + ); + } + helpers::set_expr_ctx(&mut target, ExprContext::Store); + + let value = self.parse_conditional_expression_or_higher(); + + let range = self.node_range(start); + + // test_err walrus_py37 + // # parse_options: { "target-version": "3.7" } + // (x := 1) + + // test_ok walrus_py38 + // # parse_options: { "target-version": "3.8" } + // (x := 1) + + self.add_unsupported_syntax_error(UnsupportedSyntaxErrorKind::Walrus, range); + + ast::ExprNamed { + target: Box::new(target), + value: Box::new(value.expr), + range, + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a lambda expression. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `lambda` token. + /// + /// See: + /// Parses a lambda expression, which is an operand of another expression if `is_operand`. + fn parse_lambda_expr(&mut self, is_operand: bool) -> ast::ExprLambda { + let start = self.node_start(); + self.bump(TokenKind::Lambda); + + let parameters = if self.at(TokenKind::Colon) { + // test_ok lambda_with_no_parameters + // lambda: 1 + None + } else { + Some(Box::new(self.parse_parameters(FunctionKind::Lambda))) + }; + + // A `:` at the level of a replacement field starts its format spec. + if !is_operand + && self.at(TokenKind::Colon) + && let Some((kind, level)) = self.replacement_field + && level == self.bracket_level() + { + // test_err lambda_in_replacement_field + // f"{lambda x:x}" + // f"{1, lambda:x}" + // t"{lambda x:}" + self.add_error( + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::LambdaWithoutParentheses, + kind, + ), + TextRange::new(start, self.current_token_range().end()), + ); + } + self.expect(TokenKind::Colon); + + // test_ok lambda_with_valid_body + // lambda x: x + // lambda x: x if True else y + // lambda x: await x + // lambda x: lambda y: x + y + // lambda x: (yield x) # Parenthesized `yield` is fine + // lambda x: x, *y + + // test_err lambda_body_with_starred_expr + // lambda x: *y + // lambda x: *y, + // lambda x: *y, z + // lambda x: *y and z + + // test_err lambda_body_with_yield_expr + // lambda x: yield y + // lambda x: yield from y + + // Lambda bodies recurse through the conditional layer without entering the binary parser. + let body = self.with_recursion(Self::parse_conditional_expression_or_higher); + + ast::ExprLambda { + body: Box::new(body.expr), + parameters, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses an `if` expression. + /// + /// # Panics + /// + /// If the parser isn't positioned at an `if` token. + /// + /// See: + fn parse_if_expression(&mut self, body: Expr, start: TextSize) -> ast::ExprIf { + self.bump(TokenKind::If); + + let test = self.parse_simple_expression(ExpressionContext::default()); + + let has_else = self.expect(TokenKind::Else); + if has_else + && matches!( + self.current_token_kind(), + TokenKind::Star | TokenKind::DoubleStar + ) + { + self.add_error( + ParseErrorType::InvalidConditionalBranchUnpacking { + double_starred: self.at(TokenKind::DoubleStar), + }, + self.current_token_range(), + ); + } else if has_else && (!self.at_expr() || self.at(TokenKind::Yield)) { + // test_err if_expr_statement_after_else + // x = 1 if 1 else pass + // x = 1 if 1 else + // x = 1 if 1 else yield 2 + self.add_error( + ParseErrorType::OtherError( + "expected expression after 'else', but statement is given".to_string(), + ), + self.current_token_range(), + ); + } + + // The binary-expression guard has already returned before parsing the `else` branch. + let orelse = self.with_recursion(Self::parse_conditional_expression_or_higher); + + ast::ExprIf { + body: Box::new(body), + test: Box::new(test.expr), + orelse: Box::new(orelse.expr), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses an IPython escape command at the expression level. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `IpyEscapeCommand` token. + /// If the escape command kind is not `%` or `!`. + fn parse_ipython_escape_command_expression(&mut self) -> ast::ExprIpyEscapeCommand { + let start = self.node_start(); + + let (value, kind) = self.bump_ipython_escape_command(IpyEscapeContext::Assignment); + + if !matches!(kind, IpyEscapeKind::Magic | IpyEscapeKind::Shell) { + // This should never occur as the lexer won't allow it. + unreachable!("IPython escape command expression is only allowed for % and !"); + } + + let command = ast::ExprIpyEscapeCommand { + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + kind, + value, + }; + + if self.options.mode != Mode::Ipython { + self.add_error(ParseErrorType::UnexpectedIpythonEscapeCommand, &command); + } + + command + } + + /// Performs the following validations on the arguments: + /// - Generator expressions are parenthesized when required by the argument context. + fn validate_arguments( + &mut self, + arguments: &ast::Arguments, + has_trailing_comma: bool, + context: ArgumentsContext, + ) { + let generator_must_be_parenthesized = match context { + ArgumentsContext::Call => has_trailing_comma || arguments.len() > 1, + // CPython rejects an unparenthesized generator expression as a class base even though + // this restriction isn't specified in the class definition grammar. + ArgumentsContext::ClassDefinition => true, + }; + + if generator_must_be_parenthesized { + for arg in &*arguments.args { + if let Some(ast::ExprGenerator { + range, + parenthesized: false, + .. + }) = arg.as_generator_expr() + { + // test_ok args_unparenthesized_generator + // zip((x for x in range(10)), (y for y in range(10))) + // sum(x for x in range(10)) + // sum((x for x in range(10)),) + + // test_err args_unparenthesized_generator + // sum(x for x in range(10), 5) + // total(1, 2, x for x in range(5), 6) + // sum(x for x in range(10),) + if matches!(context, ArgumentsContext::ClassDefinition) + && !has_trailing_comma + && arguments.len() == 1 + { + // A class definition with only the generator expression is reported at + // its `for`. + let elt_end = arg.as_generator_expr().map_or(range.start(), |generator| { + self.parenthesized_end(generator.elt.range()) + }); + let (_, for_range) = self.token_after(elt_end); + self.add_error( + ParseErrorType::OtherError("invalid syntax".to_owned()), + for_range, + ); + } else { + self.add_error(ParseErrorType::UnparenthesizedGeneratorExpression, range); + } + } + } + } + } +} + +/// Identifies the syntactic context for an argument list. +/// +/// Unlike calls, class definitions require a sole generator expression to be parenthesized: +/// +/// ```python +/// f(x for x in xs) +/// class C((x for x in xs)): ... +/// ``` +#[derive(Debug, Copy, Clone)] +pub(super) enum ArgumentsContext { + Call, + ClassDefinition, +} + +#[derive(Debug)] +pub(super) struct ParsedExpr { + pub(super) expr: Expr, + pub(super) is_parenthesized: bool, +} + +impl ParsedExpr { + #[inline] + pub(super) const fn is_unparenthesized_starred_expr(&self) -> bool { + !self.is_parenthesized && self.expr.is_starred_expr() + } + + #[inline] + const fn is_unparenthesized_named_expr(&self) -> bool { + !self.is_parenthesized && self.expr.is_named_expr() + } +} + +impl From for ParsedExpr { + #[inline] + fn from(expr: Expr) -> Self { + ParsedExpr { + expr, + is_parenthesized: false, + } + } +} + +impl Deref for ParsedExpr { + type Target = Expr; + + fn deref(&self) -> &Self::Target { + &self.expr + } +} + +impl Ranged for ParsedExpr { + #[inline] + fn range(&self) -> TextRange { + self.expr.range() + } +} + +#[derive(Debug)] +enum BinaryLikeOperator { + Boolean(BoolOp), + Comparison(CmpOp), + Binary(Operator), +} + +impl BinaryLikeOperator { + /// Attempts to convert the token into the corresponding binary-like operator. `next` is + /// required to distinguish `is not` and `not in` from their one-token alternatives. + /// Returns [None] if it's not a binary-like operator. + fn try_from_tokens(current: TokenKind, next: Option) -> Option { + if let Some(bool_op) = current.as_bool_operator() { + Some(BinaryLikeOperator::Boolean(bool_op)) + } else if let Some(bin_op) = current.as_binary_operator() { + Some(BinaryLikeOperator::Binary(bin_op)) + } else { + helpers::token_kind_to_cmp_op(current, next).map(BinaryLikeOperator::Comparison) + } + } + + /// Returns the [`OperatorPrecedence`] for the given operator token or [None] if the token + /// isn't an operator token. + fn precedence(&self) -> OperatorPrecedence { + match self { + BinaryLikeOperator::Boolean(bool_op) => OperatorPrecedence::from(*bool_op), + BinaryLikeOperator::Comparison(_) => OperatorPrecedence::ComparisonsMembershipIdentity, + BinaryLikeOperator::Binary(bin_op) => OperatorPrecedence::from(*bin_op), + } + } +} + +/// Represents the precedence used for parsing the value part of a starred expression. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum StarredExpressionPrecedence { + /// Matches `'*' bitwise_or` which is part of the `star_expression` rule in the + /// [Python grammar](https://docs.python.org/3/reference/grammar.html). + BitwiseOr, + + /// Matches `'*' expression` which is part of the `starred_expression` rule in the + /// [Python grammar](https://docs.python.org/3/reference/grammar.html). + Conditional, +} + +/// Represents the expression parsing context. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +pub(super) struct ExpressionContext(ExpressionContextFlags); + +bitflags! { + #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] + struct ExpressionContextFlags: u8 { + /// This flag is set when the `in` keyword should be excluded from a comparison expression. + /// It is to avoid ambiguity in `for ... in ...` statements. + const EXCLUDE_IN = 1 << 0; + + /// This flag is set when a starred expression should be allowed. This doesn't affect the + /// parsing of a starred expression as it will be parsed nevertheless. But, if it is not + /// allowed, an error is reported. + const ALLOW_STARRED_EXPRESSION = 1 << 1; + + /// This flag is set when the value of a starred expression should be limited to bitwise OR + /// precedence. Matches the `* bitwise_or` grammar rule if set. + const STARRED_BITWISE_OR_PRECEDENCE = 1 << 2; + + /// This flag is set when a yield expression should be allowed. This doesn't affect the + /// parsing of a yield expression as it will be parsed nevertheless. But, if it is not + /// allowed, an error is reported. + const ALLOW_YIELD_EXPRESSION = 1 << 3; + + /// This flag is set when the `for` keyword, or `async` starting `async for`, should be + /// excluded from an expression. + const EXCLUDE_FOR = 1 << 4; + + /// The first element after `(` may become a generator expression. Its starred + /// operand can use full expression precedence if a comprehension clause follows. + const ALLOW_STARRED_GENERATOR = 1 << 5; + } +} + +/// Returns the character of `kind` if it separates the parts of a replacement field. +fn replacement_field_separator(kind: TokenKind) -> Option { + match kind { + TokenKind::Equal => Some('='), + TokenKind::Exclamation => Some('!'), + TokenKind::Colon => Some(':'), + TokenKind::Rbrace => Some('}'), + _ => None, + } +} + +impl ExpressionContext { + /// Create a new context allowing starred expression at conditional precedence. + pub(super) fn starred_conditional() -> Self { + ExpressionContext::default() + .with_starred_expression_allowed(StarredExpressionPrecedence::Conditional) + } + + /// Create a new context allowing starred expression at bitwise OR precedence. + pub(super) fn starred_bitwise_or() -> Self { + ExpressionContext::default() + .with_starred_expression_allowed(StarredExpressionPrecedence::BitwiseOr) + } + + /// Create a new context allowing starred expression at bitwise OR precedence or yield + /// expression. + pub(super) fn yield_or_starred_bitwise_or() -> Self { + ExpressionContext::starred_bitwise_or().with_yield_expression_allowed() + } + + fn disallow_starred_expressions(self) -> Self { + let flags = self.0 & !ExpressionContextFlags::ALLOW_STARRED_EXPRESSION; + ExpressionContext(flags) + } + + fn disallow_yield_expressions(self) -> Self { + ExpressionContext(self.0 & !ExpressionContextFlags::ALLOW_YIELD_EXPRESSION) + } + + /// Returns a new [`ExpressionContext`] which allows starred expression with the given + /// precedence. + fn with_starred_expression_allowed(self, precedence: StarredExpressionPrecedence) -> Self { + let mut flags = self.0 | ExpressionContextFlags::ALLOW_STARRED_EXPRESSION; + match precedence { + StarredExpressionPrecedence::BitwiseOr => { + flags |= ExpressionContextFlags::STARRED_BITWISE_OR_PRECEDENCE; + } + StarredExpressionPrecedence::Conditional => { + flags -= ExpressionContextFlags::STARRED_BITWISE_OR_PRECEDENCE; + } + } + ExpressionContext(flags) + } + + /// Returns a new [`ExpressionContext`] which allows yield expression. + fn with_yield_expression_allowed(self) -> Self { + ExpressionContext(self.0 | ExpressionContextFlags::ALLOW_YIELD_EXPRESSION) + } + + /// Returns a new [`ExpressionContext`] which excludes `in` as part of a comparison expression. + pub(super) fn with_in_excluded(self) -> Self { + ExpressionContext(self.0 | ExpressionContextFlags::EXCLUDE_IN) + } + + /// Returns a new [`ExpressionContext`] which excludes `for` from an expression. + fn with_for_excluded(self) -> Self { + ExpressionContext(self.0 | ExpressionContextFlags::EXCLUDE_FOR) + } + + /// Allow the first parenthesized starred element to become a generator element. + fn with_starred_generator_allowed(self) -> Self { + ExpressionContext(self.0 | ExpressionContextFlags::ALLOW_STARRED_GENERATOR) + } + + const fn is_starred_generator_allowed(self) -> bool { + self.0 + .contains(ExpressionContextFlags::ALLOW_STARRED_GENERATOR) + } + + /// Returns `true` if the `in` keyword should be excluded from a comparison expression. + const fn is_in_excluded(self) -> bool { + self.0.contains(ExpressionContextFlags::EXCLUDE_IN) + } + + /// Returns `true` if starred expressions are allowed. + const fn is_starred_expression_allowed(self) -> bool { + self.0 + .contains(ExpressionContextFlags::ALLOW_STARRED_EXPRESSION) + } + + /// Returns `true` if yield expressions are allowed. + const fn is_yield_expression_allowed(self) -> bool { + self.0 + .contains(ExpressionContextFlags::ALLOW_YIELD_EXPRESSION) + } + + /// Returns `true` if `for` should be excluded from the expression. + const fn is_for_excluded(self) -> bool { + self.0.contains(ExpressionContextFlags::EXCLUDE_FOR) + } + + /// Returns the [`StarredExpressionPrecedence`] for the context, regardless of whether starred + /// expressions are allowed or not. + const fn starred_expression_precedence(self) -> StarredExpressionPrecedence { + if self + .0 + .contains(ExpressionContextFlags::STARRED_BITWISE_OR_PRECEDENCE) + { + StarredExpressionPrecedence::BitwiseOr + } else { + StarredExpressionPrecedence::Conditional + } + } +} + +#[derive(Debug)] +struct InterpolatedStringData { + elements: InterpolatedStringElements, + range: TextRange, + flags: AnyStringFlags, +} + +impl From for FString { + fn from(value: InterpolatedStringData) -> Self { + Self { + elements: value.elements, + range: value.range, + flags: value.flags.into(), + node_index: AtomicNodeIndex::NONE, + } + } +} + +impl From for TString { + fn from(value: InterpolatedStringData) -> Self { + Self { + elements: value.elements, + range: value.range, + flags: value.flags.into(), + node_index: AtomicNodeIndex::NONE, + } + } +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/helpers.rs b/vendor/rustpython-ruff_python_parser/src/parser/helpers.rs new file mode 100644 index 00000000000..562cf0fc48b --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/helpers.rs @@ -0,0 +1,112 @@ +use ruff_python_ast::token::TokenKind; +use ruff_python_ast::{self as ast, CmpOp, Expr, ExprContext, Number}; +use ruff_text_size::{Ranged, TextRange}; + +use crate::error::RelaxedDecoratorError; + +/// Set the `ctx` for `Expr::Id`, `Expr::Attribute`, `Expr::Subscript`, `Expr::Starred`, +/// `Expr::Tuple` and `Expr::List`. If `expr` is either `Expr::Tuple` or `Expr::List`, +/// recursively sets the `ctx` for their elements. +pub(super) fn set_expr_ctx(expr: &mut Expr, new_ctx: ExprContext) { + match expr { + Expr::Name(ast::ExprName { ctx, .. }) + | Expr::Attribute(ast::ExprAttribute { ctx, .. }) + | Expr::Subscript(ast::ExprSubscript { ctx, .. }) => *ctx = new_ctx, + Expr::Starred(ast::ExprStarred { value, ctx, .. }) => { + *ctx = new_ctx; + set_expr_ctx(value, new_ctx); + } + Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => { + set_expr_ctx(operand, new_ctx); + } + Expr::List(ast::ExprList { elts, ctx, .. }) + | Expr::Tuple(ast::ExprTuple { elts, ctx, .. }) => { + *ctx = new_ctx; + for element in elts.iter_mut() { + set_expr_ctx(element, new_ctx); + } + } + _ => {} + } +} + +/// Converts a [`TokenKind`] into its corresponding [`CmpOp`]. +/// +/// `next` is only needed to distinguish `is not` and `not in`. +pub(super) const fn token_kind_to_cmp_op( + current: TokenKind, + next: Option, +) -> Option { + Some(match (current, next) { + (TokenKind::Is, Some(TokenKind::Not)) => CmpOp::IsNot, + (TokenKind::Is, _) => CmpOp::Is, + (TokenKind::Not, Some(TokenKind::In)) => CmpOp::NotIn, + (TokenKind::In, _) => CmpOp::In, + (TokenKind::EqEqual, _) => CmpOp::Eq, + (TokenKind::NotEqual, _) => CmpOp::NotEq, + (TokenKind::Less, _) => CmpOp::Lt, + (TokenKind::LessEqual, _) => CmpOp::LtE, + (TokenKind::Greater, _) => CmpOp::Gt, + (TokenKind::GreaterEqual, _) => CmpOp::GtE, + _ => return None, + }) +} + +/// Helper for `parse_decorators` to determine if `expr` is a [`dotted_name`] from the decorator +/// grammar before Python 3.9. +/// +/// Returns `Some((error, range))` if `expr` is not a `dotted_name`, or `None` if it is a `dotted_name`. +/// +/// [`dotted_name`]: https://docs.python.org/3.8/reference/compound_stmts.html#grammar-token-dotted-name +pub(super) fn detect_invalid_pre_py39_decorator_node( + expr: &Expr, +) -> Option<(RelaxedDecoratorError, TextRange)> { + let description = match expr { + Expr::Name(_) => return None, + + Expr::Attribute(attribute) => { + return detect_invalid_pre_py39_decorator_node(&attribute.value); + } + + Expr::Call(_) => return Some((RelaxedDecoratorError::CallExpression, expr.range())), + + Expr::NumberLiteral(number) => match &number.value { + Number::Int(_) => "an int literal", + Number::Float(_) => "a float literal", + Number::Complex { .. } => "a complex literal", + }, + Expr::Constant(_) => "a constant literal", + + Expr::BoolOp(_) => "boolean expression", + Expr::BinOp(_) => "binary-operation expression", + Expr::UnaryOp(_) => "unary-operation expression", + Expr::Await(_) => "`await` expression", + Expr::Lambda(_) => "lambda expression", + Expr::If(_) => "conditional expression", + Expr::Dict(_) => "a dict literal", + Expr::Set(_) => "a set literal", + Expr::List(_) => "a list literal", + Expr::Tuple(_) => "a tuple literal", + Expr::Starred(_) => "starred expression", + Expr::Slice(_) => "slice expression", + Expr::BytesLiteral(_) => "a bytes literal", + Expr::StringLiteral(_) => "a string literal", + Expr::EllipsisLiteral(_) => "an ellipsis literal", + Expr::NoneLiteral(_) => "a `None` literal", + Expr::BooleanLiteral(_) => "a boolean literal", + Expr::ListComp(_) => "a list comprehension", + Expr::SetComp(_) => "a set comprehension", + Expr::DictComp(_) => "a dict comprehension", + Expr::Generator(_) => "generator expression", + Expr::Yield(_) => "`yield` expression", + Expr::YieldFrom(_) => "`yield from` expression", + Expr::Compare(_) => "comparison expression", + Expr::FString(_) => "f-string", + Expr::TString(_) => "t-string", + Expr::Named(_) => "assignment expression", + Expr::Subscript(_) => "subscript expression", + Expr::IpyEscapeCommand(_) => "IPython escape command", + }; + + Some((RelaxedDecoratorError::Other(description), expr.range())) +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/mod.rs b/vendor/rustpython-ruff_python_parser/src/parser/mod.rs new file mode 100644 index 00000000000..3deba72c5a7 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/mod.rs @@ -0,0 +1,2256 @@ +use std::borrow::Cow; +use std::cmp::Ordering; +use std::str::FromStr; + +use bitflags::bitflags; +use hashbrown::HashSet; +use ruff_python_ast::name::Name; +use ruff_python_ast::token::TokenKind; +use ruff_python_ast::{ + Alias, AtomicNodeIndex, ElifElseClause, Expr, Int, IpyEscapeKind, Keyword, Mod, ModExpression, + ModModule, ParameterWithDefault, Stmt, StringFlags, +}; +use ruff_python_trivia::is_python_whitespace; +use ruff_text_size::{Ranged, TextRange, TextSize}; +use rustc_hash::FxBuildHasher; +use thin_vec::ThinVec; +use unicode_normalization::UnicodeNormalization; + +use crate::error::{ + InterpolatedStringErrorType, LexicalError, LexicalErrorType, UnsupportedSyntaxError, +}; +use crate::lexer::Lexer; +use crate::parser::expression::ExpressionContext; +use crate::parser::progress::{ParserProgress, TokenId}; +use crate::parser::scratch_buffer::ScratchBuffer; +use crate::string::InterpolatedStringKind; +use crate::token_set::TokenSet; +use crate::token_source::{TokenSource, TokenSourceCheckpoint}; +use crate::{Mode, ParseError, ParseErrorType, UnsupportedSyntaxErrorKind}; +use crate::{Parsed, Tokens}; + +pub use crate::parser::options::ParseOptions; + +#[cfg(feature = "python315-diagnostics")] +mod case_diagnostics; +mod expression; +mod helpers; +mod options; +mod pattern; +mod progress; +mod recovery; +mod scratch_buffer; +mod statement; +#[cfg(test)] +mod tests; + +#[derive(Debug, Default)] +struct NameInterner { + names: HashSet, +} + +impl NameInterner { + /// Returns an inline name directly, or a shared clone of a heap-allocated name. + fn intern(&mut self, text: &str) -> Name { + if let Some(name) = Name::new_inline(text) { + return name; + } + + self.names + .get_or_insert_with(text, |text| Name::new_heap(text)) + .clone() + } +} + +// Stack probes access thread-local state, so avoid them while recursive parser calls remain +// shallow. `STACK_RED_ZONE` must cover the stack used before the first deferred probe. +const STACK_RED_ZONE: usize = 100 * 1024; +const STACK_SIZE: usize = 1024 * 1024; +const MAX_UNCHECKED_RECURSION_DEPTH: usize = 20; + +#[derive(Debug)] +pub(crate) struct Parser<'src> { + source: &'src str, + + /// Token source for the parser that skips over any non-trivia token. + tokens: TokenSource<'src>, + + /// Deduplicates the backing allocations for repeated names that do not fit inline. + name_interner: NameInterner, + + /// Reusable storage for names that need to be constructed by the parser. + name_buffer: String, + + /// Stores all the syntax errors found during the parsing. + errors: Vec, + + /// Display-precedence errors that are valid generator operands, retained for + /// the invalid grammar fallback if the overall parse fails. + deferred_starred_generator_errors: Vec<(usize, ParseError)>, + + /// Stores non-fatal syntax errors found during parsing, such as version-related errors. + unsupported_syntax_errors: Vec, + + /// Options for how the code will be parsed. + options: ParseOptions, + + /// The ID of the current token. This is used to track the progress of the parser + /// to avoid infinite loops when the parser is stuck. + current_token_id: TokenId, + + /// The end of the previous token processed. This is used to determine a node's end. + prev_token_end: TextSize, + + /// The recovery context in which the parser is currently in. + recovery_context: RecoveryContext, + + /// The start offset in the source code from which to start parsing at. + start_offset: TextSize, + + /// The start of the token at which the parser first recovered from an unclosed bracket, if + /// that happened before any error was found. The error itself is reported at the end of the + /// bracket's logical line. + first_unclosed_bracket_recovery: Option, + + /// The first escape sequence error in a literal part of the f-string or t-string being + /// parsed. It is reported at the closing quote once the string ends. + interpolated_string_escape_error: Option, + + /// The kind of the innermost f-string or t-string whose replacement field expression is + /// being parsed, and the bracket level inside its brace, where a `:` starts the format spec. + replacement_field: Option<(InterpolatedStringKind, usize)>, + + /// Number of active recursive statement, expression, and pattern parsing operations. + recursion_depth: usize, + + /// Reusable, nesting-safe scratch storage for expression lists. + expr_scratch: ScratchBuffer, + + /// Reusable, nesting-safe scratch storage for call keywords. + keyword_scratch: ScratchBuffer, + + /// Reusable, nesting-safe scratch storage for function and lambda parameters. + parameter_scratch: ScratchBuffer, + + /// Reusable, nesting-safe scratch storage for statement lists. + stmt_scratch: ScratchBuffer, + + /// Reusable scratch storage for import aliases. + alias_scratch: ScratchBuffer, + + /// Reusable, nesting-safe scratch storage for `elif` and `else` clauses. + elif_else_scratch: ScratchBuffer, +} + +/// A bracket that encloses part of an expression. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Bracket { + /// The arguments of a call or the slices of a subscript. + CallOrSubscript, + /// A parenthesized expression, list, set or dictionary. + Display, +} + +impl<'src> Parser<'src> { + /// Create a new parser for the given source code. + pub(crate) fn new(source: &'src str, options: ParseOptions) -> Self { + Parser::new_starts_at(source, TextSize::new(0), options) + } + + /// Create a new parser for the given source code which starts parsing at the given offset. + pub(crate) fn new_starts_at( + source: &'src str, + start_offset: TextSize, + options: ParseOptions, + ) -> Self { + let tokens = TokenSource::from_source(source, options.mode, start_offset); + + Parser { + options, + source, + errors: Vec::new(), + deferred_starred_generator_errors: Vec::new(), + unsupported_syntax_errors: Vec::new(), + tokens, + name_interner: NameInterner::default(), + name_buffer: String::new(), + recovery_context: RecoveryContext::empty(), + prev_token_end: TextSize::new(0), + start_offset, + first_unclosed_bracket_recovery: None, + interpolated_string_escape_error: None, + replacement_field: None, + recursion_depth: 0, + current_token_id: TokenId::default(), + expr_scratch: ScratchBuffer::with_capacity(16), + keyword_scratch: ScratchBuffer::new(), + parameter_scratch: ScratchBuffer::new(), + stmt_scratch: ScratchBuffer::with_capacity(32), + alias_scratch: ScratchBuffer::new(), + elif_else_scratch: ScratchBuffer::new(), + } + } + + /// Grows the stack for recursive parser calls only after shallow nesting is exceeded. + #[inline] + fn with_recursion(&mut self, f: impl FnOnce(&mut Self) -> T) -> T { + self.recursion_depth += 1; + + let result = if self.recursion_depth > MAX_UNCHECKED_RECURSION_DEPTH { + self.grow_stack(f) + } else { + f(self) + }; + + self.recursion_depth -= 1; + result + } + + #[cold] + fn grow_stack(&mut self, f: impl FnOnce(&mut Self) -> T) -> T { + stacker::maybe_grow(STACK_RED_ZONE, STACK_SIZE, || f(self)) + } + + /// Consumes the [`Parser`] and returns the parsed [`Parsed`]. + pub(crate) fn parse(mut self) -> Parsed { + let syntax = stacker::maybe_grow(STACK_RED_ZONE, STACK_SIZE, || match self.options.mode { + Mode::Expression | Mode::ParenthesizedExpression => { + Mod::Expression(self.parse_single_expression()) + } + Mode::Module | Mode::Ipython => Mod::Module(self.parse_module()), + }); + + self.finish(syntax) + } + + /// Parses a single expression. + /// + /// This is to be used for [`Mode::Expression`]. + /// + /// ## Recovery + /// + /// After parsing a single expression, an error is reported and all remaining tokens are + /// dropped by the parser. + fn parse_single_expression(&mut self) -> ModExpression { + let start = self.node_start(); + let parsed_expr = self.parse_expression_list(ExpressionContext::default()); + + // All remaining newlines are actually going to be non-logical newlines. + self.eat(TokenKind::Newline); + + if !self.at(TokenKind::EndOfFile) { + self.add_error( + ParseErrorType::UnexpectedExpressionToken, + self.current_token_range(), + ); + + // TODO(dhruvmanila): How should error recovery work here? Just truncate after the expression? + let mut progress = ParserProgress::default(); + loop { + progress.assert_progressing(self); + if self.at(TokenKind::EndOfFile) { + break; + } + self.bump_any(); + } + } + + self.bump(TokenKind::EndOfFile); + + ModExpression { + body: Box::new(parsed_expr.expr), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a Python module. + /// + /// This is to be used for [`Mode::Module`] and [`Mode::Ipython`]. + fn parse_module(&mut self) -> ModModule { + let body = self.parse_list_into_thin_vec(RecoveryContextKind::ModuleStatements, |p| { + p.parse_statement() + }); + + self.bump(TokenKind::EndOfFile); + + ModModule { + body, + range: TextRange::new(self.start_offset, self.current_token_range().end()), + node_index: AtomicNodeIndex::NONE, + runtime_body: None, + } + } + + fn finish(self, syntax: Mod) -> Parsed { + assert_eq!( + self.current_token_kind(), + TokenKind::EndOfFile, + "Parser should be at the end of the file." + ); + // TODO consider re-integrating lexical error handling into the parser? + let mut parse_errors = self.errors; + if !parse_errors.is_empty() + && let Some((0, error)) = self.deferred_starred_generator_errors.into_iter().next() + { + // CPython retries its invalid grammar rules only after a parse failure. + // Preserve encounter order, including nested operands whose ranges begin + // after the outer expression's later error. Semantic errors occur later. + parse_errors.insert(0, error); + } + let (tokens, lex_errors) = self.tokens.finish(); + + // Fast path for when there are no lex errors. + // There's no fast path for when there are no parse errors because a lex error + // always results in a parse error. + if lex_errors.is_empty() { + prioritize_tokenizer_error( + &mut parse_errors, + self.source, + self.options.mode, + self.start_offset, + self.first_unclosed_bracket_recovery, + ); + return Parsed { + syntax, + tokens: Tokens::new(tokens), + errors: parse_errors, + unsupported_syntax_errors: self.unsupported_syntax_errors, + }; + } + + let mut merged = Vec::with_capacity(parse_errors.len().saturating_add(lex_errors.len())); + + let mut parse_errors = parse_errors.into_iter().peekable(); + let mut lex_errors = lex_errors.into_iter().peekable(); + + while let (Some(parse_error), Some(lex_error)) = (parse_errors.peek(), lex_errors.peek()) { + match parse_error + .location + .start() + .cmp(&lex_error.location().start()) + { + Ordering::Less => merged.push(parse_errors.next().unwrap()), + // A replacement field error at a token the tokenizer accepts replaces the lex + // error of that token. + Ordering::Equal + if matches!( + parse_error.error, + ParseErrorType::FStringError(_) | ParseErrorType::TStringError(_) + ) && !lex_error.error().is_tokenizer_error() => + { + lex_errors.next().unwrap(); + merged.push(parse_errors.next().unwrap()); + } + Ordering::Equal => { + // Skip the parse error if we already have a lex error at the same location.. + parse_errors.next().unwrap(); + merged.push(lex_errors.next().unwrap().into()); + } + Ordering::Greater => merged.push(lex_errors.next().unwrap().into()), + } + } + + merged.extend(parse_errors); + merged.extend(lex_errors.map(ParseError::from)); + prioritize_tokenizer_error( + &mut merged, + self.source, + self.options.mode, + self.start_offset, + self.first_unclosed_bracket_recovery, + ); + + Parsed { + syntax, + tokens: Tokens::new(tokens), + errors: merged, + unsupported_syntax_errors: self.unsupported_syntax_errors, + } + } + + /// Returns the start position for a node that starts at the current token. + fn node_start(&self) -> TextSize { + self.current_token_range().start() + } + + /// Returns the 1-based line number of `offset` in the source. + /// + /// `\n`, `\r\n`, and a lone `\r` each count as one line break. + pub(super) fn line_number(&self, offset: TextSize) -> u32 { + line_number(self.source, offset) + } + + /// Returns `true` if the already parsed expression at `range` is enclosed in parentheses of + /// its own. + fn is_parenthesized(&self, range: TextRange) -> bool { + self.enclosing_parentheses(range).is_some() + } + + /// Returns the end of the already parsed expression at `range`, including the parentheses + /// that enclose only it. + fn parenthesized_end(&self, mut range: TextRange) -> TextSize { + while let Some(parenthesized) = self.enclosing_parentheses(range) { + range = parenthesized; + } + range.end() + } + + /// Returns the range from the `(` directly before `range` to the `)` directly after it, if + /// both exist. + fn enclosing_parentheses(&self, range: TextRange) -> Option { + let tokens = self.tokens.bumped(); + let before = tokens[..tokens.partition_point(|token| token.start() < range.start())] + .iter() + .rev() + .find(|token| !token.kind().is_trivia()) + .filter(|token| token.kind() == TokenKind::Lpar)?; + let (after_kind, after_range) = self.token_after(range.end()); + (after_kind == TokenKind::Rpar).then(|| TextRange::new(before.start(), after_range.end())) + } + + /// Returns the kind and range of the last non-trivia token that starts before `offset`. + fn token_before(&self, offset: TextSize) -> Option<(TokenKind, TextRange)> { + let tokens = self.tokens.bumped(); + tokens[..tokens.partition_point(|token| token.start() < offset)] + .iter() + .rev() + .find(|token| !token.kind().is_trivia()) + .map(|token| (token.kind(), token.range())) + } + + /// Returns how many brackets enclose the tokens bumped so far. + fn bracket_level(&self) -> usize { + let mut level = 0usize; + for token in self.tokens.bumped() { + match token.kind() { + TokenKind::Lpar | TokenKind::Lsqb | TokenKind::Lbrace => level += 1, + TokenKind::Rpar | TokenKind::Rsqb | TokenKind::Rbrace => { + level = level.saturating_sub(1); + } + _ => {} + } + } + level + } + + /// Returns the innermost bracket enclosing the tokens bumped so far, or `None` if there is + /// none or it starts an f-string or t-string replacement field. + fn innermost_bracket(&self) -> Option { + #[derive(PartialEq)] + enum Opener { + Bracket(Bracket), + InterpolatedString, + ReplacementField, + } + let mut openers = Vec::new(); + let mut previous = TokenKind::Unknown; + for token in self.tokens.bumped() { + if token.kind().is_trivia() { + continue; + } + match token.kind() { + TokenKind::FStringStart | TokenKind::TStringStart => { + openers.push(Opener::InterpolatedString); + } + TokenKind::Lbrace if openers.last() == Some(&Opener::InterpolatedString) => { + openers.push(Opener::ReplacementField); + } + TokenKind::Lpar | TokenKind::Lsqb + if matches!( + previous, + TokenKind::Identifier + | TokenKind::String + | TokenKind::FStringEnd + | TokenKind::TStringEnd + | TokenKind::Rpar + | TokenKind::Rsqb + | TokenKind::Rbrace + ) || previous.is_soft_keyword() => + { + openers.push(Opener::Bracket(Bracket::CallOrSubscript)); + } + TokenKind::Lpar | TokenKind::Lsqb | TokenKind::Lbrace => { + openers.push(Opener::Bracket(Bracket::Display)); + } + TokenKind::Rpar + | TokenKind::Rsqb + | TokenKind::Rbrace + | TokenKind::FStringEnd + | TokenKind::TStringEnd => { + openers.pop(); + } + _ => {} + } + previous = token.kind(); + } + match openers.pop() { + Some(Opener::Bracket(bracket)) => Some(bracket), + _ => None, + } + } + + /// Returns the kind of the first non-trivia token that starts at or after `offset`. + fn token_kind_after(&self, offset: TextSize) -> TokenKind { + self.token_after(offset).0 + } + + /// Returns the kind and range of the first non-trivia token that starts at or after + /// `offset`. + fn token_after(&self, offset: TextSize) -> (TokenKind, TextRange) { + let tokens = self.tokens.bumped(); + tokens[tokens.partition_point(|token| token.start() < offset)..] + .iter() + .find(|token| !token.kind().is_trivia()) + .map_or_else( + || (self.current_token_kind(), self.current_token_range()), + |token| (token.kind(), token.range()), + ) + } + + fn node_range(&self, start: TextSize) -> TextRange { + // It's possible during error recovery that the parsing didn't consume any tokens. In that + // case, `last_token_end` still points to the end of the previous token but `start` is the + // start of the current token. Calling `TextRange::new(start, self.last_token_end)` would + // panic in that case because `start > end`. This path "detects" this case and creates an + // empty range instead. + // + // The reason it's `<=` instead of just `==` is because there could be whitespaces between + // the two tokens. For example: + // + // ```python + // # last token end + // # | current token (newline) start + // # v v + // def foo \n + // # ^ + // # assume there's trailing whitespace here + // ``` + // + // Or, there could tokens that are considered "trivia" and thus aren't emitted by the token + // source. These are comments and non-logical newlines. For example: + // + // ```python + // # last token end + // # v + // def foo # comment\n + // # ^ current token (newline) start + // ``` + // + // In either of the above cases, there's a "gap" between the end of the last token and start + // of the current token. + if self.prev_token_end <= start { + // We need to create an empty range at the last token end instead of the start because + // otherwise this node range will fall outside the range of it's parent node. Taking + // the above example: + // + // ```python + // if True: + // # function start + // # | function end + // # v v + // def foo # comment + // # ^ current token start + // ``` + // + // Here, the current token start is the start of parameter range but the function ends + // at `foo`. Even if there's a function body, the range of parameters would still be + // before the comment. + + // test_err node_range_with_gaps + // def foo # comment + // def bar(): ... + // def baz + TextRange::empty(self.prev_token_end) + } else { + TextRange::new(start, self.prev_token_end) + } + } + + fn missing_node_range(&self) -> TextRange { + // TODO(dhruvmanila): This range depends on whether the missing node is + // on the leftmost or the rightmost of the expression. It's incorrect for + // the leftmost missing node because the range is outside the expression + // range. For example, + // + // ```python + // value = ** y + // # ^^^^ expression range + // # ^ last token end + // ``` + TextRange::empty(self.prev_token_end) + } + + /// Moves the parser to the next token. + fn do_bump(&mut self, kind: TokenKind) { + if match self.current_token_kind() { + // TODO explore including everything up to the dedent as part of the body. + TokenKind::Dedent => false, + + // Don't include newlines in the body + TokenKind::Newline => false, + + // TODO(micha): Including the semi feels more correct but it isn't compatible with lalrpop and breaks the + // formatters semicolon detection. Exclude it for now + TokenKind::Semi => false, + + _ => true, + } { + self.prev_token_end = self.current_token_range().end(); + } + + self.tokens.bump(kind); + self.current_token_id.increment(); + } + + /// Returns the next token kind without consuming it. + fn peek(&mut self) -> TokenKind { + self.tokens.peek() + } + + /// Returns the next two token kinds without consuming it. + fn peek2(&mut self) -> (TokenKind, TokenKind) { + self.tokens.peek2() + } + + /// Returns the current token kind. + #[inline] + fn current_token_kind(&self) -> TokenKind { + self.tokens.current_kind() + } + + /// Returns the range of the current token. + #[inline] + fn current_token_range(&self) -> TextRange { + self.tokens.current_range() + } + + /// Returns the current token ID. + #[inline] + fn current_token_id(&self) -> TokenId { + self.current_token_id + } + + /// Bumps the current token assuming it is of the given kind. + /// + /// # Panics + /// + /// If the current token is not of the given kind. + fn bump(&mut self, kind: TokenKind) { + assert_eq!(self.current_token_kind(), kind); + + self.do_bump(kind); + } + + fn bump_identifier(&mut self) -> Name { + let text = self.current_token_text(); + let name = if !self.tokens.current_flags().is_non_ascii_identifier() { + self.intern_name(text) + } else { + self.intern_normalized_name(text) + }; + self.bump(TokenKind::Identifier); + name + } + + fn intern_name(&mut self, text: &str) -> Name { + self.name_interner.intern(text) + } + + fn intern_normalized_name(&mut self, text: &str) -> Name { + let snapshot = self.name_buffer.len(); + self.name_buffer.extend(text.nfkc()); + + let name = self.name_interner.intern(&self.name_buffer[snapshot..]); + + self.name_buffer.truncate(snapshot); + name + } + + fn bump_int(&mut self) -> Int { + let text = self.current_token_text(); + let value = if let Some(digits) = + text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) + { + Int::from_str_radix(&strip_underscores(digits), 16, text) + } else if let Some(digits) = text.strip_prefix("0o").or_else(|| text.strip_prefix("0O")) { + Int::from_str_radix(&strip_underscores(digits), 8, text) + } else if let Some(digits) = text.strip_prefix("0b").or_else(|| text.strip_prefix("0B")) { + Int::from_str_radix(&strip_underscores(digits), 2, text) + } else { + Int::from_str(&strip_underscores(text)) + } + .expect("lexer validated integer literal"); + self.bump(TokenKind::Int); + value + } + + fn bump_float(&mut self) -> f64 { + let value = f64::from_str(&strip_underscores(self.current_token_text())) + .expect("lexer validated float literal"); + self.bump(TokenKind::Float); + value + } + + fn bump_complex(&mut self) -> (f64, f64) { + let text = self.current_token_text(); + let value = f64::from_str(&strip_underscores(&text[..text.len() - 1])) + .expect("lexer validated complex literal"); + self.bump(TokenKind::Complex); + (0.0, value) + } + + fn bump_string_value(&mut self) -> &'src str { + let range = self.current_token_range(); + let flags = self.tokens.current_flags().as_any_string_flags(); + let value_range = TextRange::new( + range.start() + flags.opener_len(), + range.end() - flags.closer_len(), + ); + let value = &self.source[value_range]; + self.bump(TokenKind::String); + value + } + + fn bump_ipython_escape_command( + &mut self, + context: IpyEscapeContext, + ) -> (Box, IpyEscapeKind) { + let (value, kind) = self.parse_ipython_escape_command_value(context); + self.bump(TokenKind::IpyEscapeCommand); + (value, kind) + } + + fn current_token_text(&self) -> &'src str { + self.src_text(self.current_token_range()) + } + + fn parse_ipython_escape_command_value( + &self, + context: IpyEscapeContext, + ) -> (Box, IpyEscapeKind) { + let raw = self.current_token_text(); + let initial_kind = if context.is_logical_line_start() + && let Ok(kind) = IpyEscapeKind::try_from([ + raw.as_bytes()[0] as char, + raw[1..].chars().next().unwrap_or('\0'), + ]) { + kind + } else { + IpyEscapeKind::try_from(raw.as_bytes()[0] as char).expect("IPython escape token") + }; + + let mut value = String::new(); + let mut chars = raw[initial_kind.as_str().len()..].chars().peekable(); + while let Some(c) = chars.next() { + match c { + '\\' if matches!(chars.peek(), Some('\r' | '\n')) => { + if chars.next() == Some('\r') && matches!(chars.peek(), Some('\n')) { + chars.next(); + } + } + '?' => { + let mut question_count = 1; + while matches!(chars.peek(), Some('?')) { + chars.next(); + question_count += 1; + } + + // At logical-line start, IPython treats one or two terminal `?` after a + // nonempty help or magic command as the command kind. Strict `foo?` syntax + // is parsed separately. + // https://github.com/ipython/ipython/blob/292e3a23459ca965b8c1bfe2c3707044c510209a/IPython/core/inputtransformer2.py#L454-L462 + if !context.is_logical_line_start() + || !(initial_kind.is_magic() || initial_kind.is_help()) + || question_count > 2 + || value.chars().last().is_none_or(is_python_whitespace) + || !matches!(chars.peek(), None | Some('\n' | '\r')) + { + value.extend(std::iter::repeat_n('?', question_count)); + continue; + } + + let kind = if question_count == 1 { + IpyEscapeKind::Help + } else { + IpyEscapeKind::Help2 + }; + + // A help suffix replaces a leading help prefix, but takes precedence over a + // magic prefix, which remains part of the value (`%foo?` becomes help for + // `%foo`). + if initial_kind.is_help() { + value = value.trim_start_matches([' ', '?']).to_string(); + } else { + value.insert_str(0, initial_kind.as_str()); + } + + return (value.into_boxed_str(), kind); + } + '\n' | '\r' => break, + c => value.push(c), + } + } + + (value.into_boxed_str(), initial_kind) + } + + /// Bumps the current token assuming it is found in the given token set. + /// + /// # Panics + /// + /// If the current token is not found in the given token set. + fn bump_ts(&mut self, ts: TokenSet) { + let kind = self.current_token_kind(); + assert!(ts.contains(kind)); + + self.do_bump(kind); + } + + /// Bumps the current token regardless of its kind and advances to the next token. + /// + /// # Panics + /// + /// If the parser is at end of file. + fn bump_any(&mut self) { + let kind = self.current_token_kind(); + assert_ne!(kind, TokenKind::EndOfFile); + + self.do_bump(kind); + } + + /// Bumps the soft keyword token as an `Identifier` token. + /// + /// # Panics + /// + /// If the current token is not a soft keyword. + fn bump_soft_keyword_as_identifier(&mut self) { + assert!(self.at_soft_keyword()); + + self.do_bump(TokenKind::Identifier); + } + + /// Consume the current token if it is of the given kind. Returns `true` if it matches, `false` + /// otherwise. + fn eat(&mut self, kind: TokenKind) -> bool { + if self.at(kind) { + self.do_bump(kind); + true + } else { + false + } + } + + /// Eat the current token if its of the expected kind, otherwise adds an appropriate error. + fn expect(&mut self, expected: TokenKind) -> bool { + if self.eat(expected) { + return true; + } + + self.add_error( + ParseErrorType::ExpectedToken { + found: self.current_token_kind(), + expected, + }, + self.current_token_range(), + ); + + false + } + + /// Re-lexes the current token in the context of a logical line to recover from an unclosed + /// bracket. + fn re_lex_logical_token(&mut self) { + if self.errors.is_empty() && self.first_unclosed_bracket_recovery.is_none() { + self.first_unclosed_bracket_recovery = Some(self.current_token_range().start()); + } + self.tokens.re_lex_logical_token(); + } + + fn add_error(&mut self, error: ParseErrorType, ranged: T) + where + T: Ranged, + { + fn inner(errors: &mut Vec, error: ParseErrorType, range: TextRange) { + // Avoid flagging multiple errors at the same location + let is_same_location = errors + .last() + .is_some_and(|last| last.location.start() == range.start()); + + if !is_same_location { + errors.push(ParseError { + location: reported_range(&error, range), + error, + }); + } + } + + fn reported_range(error: &ParseErrorType, range: TextRange) -> TextRange { + match error { + // A misplaced starred expression or star pattern is reported at its `*`. + ParseErrorType::InvalidStarredExpressionUsage + | ParseErrorType::InvalidStarPatternUsage => { + TextRange::at(range.start(), TextSize::new(1)) + } + // An unexpected indent is reported at the last character of the indentation. + ParseErrorType::UnexpectedIndentation if !range.is_empty() => { + TextRange::at(range.end() - TextSize::new(1), TextSize::new(1)) + } + _ => range, + } + } + + // The lexer already reported an error for the current token, which comes first. A + // string literal that fails to decode was read before it. + if self.at(TokenKind::Unknown) && !matches!(error, ParseErrorType::Lexical(_)) { + return; + } + + inner(&mut self.errors, error, ranged.range()); + } + + /// Add an [`UnsupportedSyntaxError`] with the given [`UnsupportedSyntaxErrorKind`] and + /// [`TextRange`] if its minimum version is less than [`Parser::target_version`]. + fn add_unsupported_syntax_error(&mut self, kind: UnsupportedSyntaxErrorKind, range: TextRange) { + if kind.is_unsupported(self.options.target_version) { + self.unsupported_syntax_errors.push(UnsupportedSyntaxError { + kind, + range, + target_version: self.options.target_version, + }); + } + } + + /// Returns `true` if the current token is of the given kind. + fn at(&self, kind: TokenKind) -> bool { + self.current_token_kind() == kind + } + + /// Returns `true` if the current token is found in the given token set. + fn at_ts(&self, ts: TokenSet) -> bool { + ts.contains(self.current_token_kind()) + } + + fn src_text(&self, ranged: T) -> &'src str + where + T: Ranged, + { + &self.source[ranged.range()] + } + + /// Parses a list of elements into a thin vector where each element is parsed using + /// the given `parse_element` function. + fn parse_list_into_thin_vec( + &mut self, + recovery_context_kind: RecoveryContextKind, + parse_element: impl Fn(&mut Parser<'src>) -> T, + ) -> ThinVec { + let mut elements = ThinVec::new(); + self.parse_list(recovery_context_kind, |p| elements.push(parse_element(p))); + elements.shrink_to_fit(); + elements + } + + /// Parses a list of elements where each element is parsed using the given + /// `parse_element` function. + fn parse_list( + &mut self, + recovery_context_kind: RecoveryContextKind, + mut parse_element: impl FnMut(&mut Parser<'src>), + ) { + let mut progress = ParserProgress::default(); + let mut unexpected_indents = 0; + + let saved_context = self.recovery_context; + self.recovery_context = self + .recovery_context + .union(RecoveryContext::from_kind(recovery_context_kind)); + + loop { + progress.assert_progressing(self); + + if 0 < unexpected_indents && self.at(TokenKind::Dedent) { + // Ignore this `Dedent` like we ignored the `Indent`, avoiding extra errors from + // being imbalanced + unexpected_indents -= 1; + self.bump(TokenKind::Dedent); + } else if recovery_context_kind.is_list_element(self) { + parse_element(self); + } else if recovery_context_kind.is_regular_list_terminator(self) { + break; + } else { + // Run the error recovery: If the token is recognised as an element or terminator + // of an enclosing list, then we try to re-lex in the context of a logical line and + // break out of list parsing. + if self.is_enclosing_list_element_or_terminator() { + self.re_lex_logical_token(); + break; + } + + self.add_error( + recovery_context_kind.create_error(self), + self.current_token_range(), + ); + + if matches!( + recovery_context_kind, + RecoveryContextKind::ModuleStatements | RecoveryContextKind::BlockStatements + ) && self.at(TokenKind::Indent) + { + // For this invalid `Indent`, ensure the matching `Dedent` gets consumed as well + unexpected_indents += 1; + } + self.bump_any(); + } + } + + self.recovery_context = saved_context; + } + + /// Parses a comma separated list of elements into a vector where each element + /// is parsed using the given `parse_element` function. + fn parse_comma_separated_list_into_vec( + &mut self, + recovery_context_kind: RecoveryContextKind, + parse_element: impl Fn(&mut Parser<'src>) -> T, + ) -> Vec { + self.parse_comma_separated_list_into_vec_with_capacity( + recovery_context_kind, + parse_element, + 0, + ) + } + + /// Parses a comma separated list of elements into a vector with an initial capacity. + fn parse_comma_separated_list_into_vec_with_capacity( + &mut self, + recovery_context_kind: RecoveryContextKind, + parse_element: impl Fn(&mut Parser<'src>) -> T, + capacity: usize, + ) -> Vec { + let mut elements = Vec::with_capacity(capacity); + self.parse_comma_separated_list(recovery_context_kind, |p| elements.push(parse_element(p))); + elements + } + + /// Parses a comma separated list of elements where each element is parsed + /// using the given `parse_element` function. + /// + /// The difference between this function and `parse_comma_separated_list_into_vec` + /// is that this function does not return the parsed elements. Instead, it is the + /// caller's responsibility to handle the parsed elements. This is the reason + /// that the `parse_element` parameter is bound to [`FnMut`] instead of [`Fn`]. + /// + /// Returns `true` if there is a trailing comma present. + fn parse_comma_separated_list( + &mut self, + recovery_context_kind: RecoveryContextKind, + mut parse_element: impl FnMut(&mut Parser<'src>), + ) -> bool { + let mut progress = ParserProgress::default(); + + let saved_context = self.recovery_context; + self.recovery_context = self + .recovery_context + .union(RecoveryContext::from_kind(recovery_context_kind)); + + let mut first_element = true; + let mut trailing_comma_range: Option = None; + + loop { + progress.assert_progressing(self); + + if recovery_context_kind.is_list_element(self) { + parse_element(self); + + // Only unset this when we've completely parsed a single element. This is mainly to + // raise the correct error in case the first element isn't valid and the current + // token isn't a comma. Without this knowledge, the parser would later expect a + // comma instead of raising the context error. + first_element = false; + + let maybe_comma_range = self.current_token_range(); + if self.eat(TokenKind::Comma) { + trailing_comma_range = Some(maybe_comma_range); + continue; + } + trailing_comma_range = None; + } + + // test_ok comma_separated_regular_list_terminator + // # The first element is parsed by `parse_list_like_expression` and the comma after + // # the first element is expected by `parse_list_expression` + // [0] + // [0, 1] + // [0, 1,] + // [0, 1, 2] + // [0, 1, 2,] + if recovery_context_kind.is_regular_list_terminator(self) { + break; + } + + // test_err comma_separated_missing_comma_between_elements + // # The comma between the first two elements is expected in `parse_list_expression`. + // [0, 1 2] + if recovery_context_kind.is_list_element(self) { + // This is a special case to expect a comma between two elements and should be + // checked before running the error recovery. This is because the error recovery + // will always run as the parser is currently at a list element. + self.expect(TokenKind::Comma); + continue; + } + + // Run the error recovery: If the token is recognised as an element or terminator of an + // enclosing list, then we try to re-lex in the context of a logical line and break out + // of list parsing. + if self.is_enclosing_list_element_or_terminator() { + self.re_lex_logical_token(); + break; + } + + if first_element || self.at(TokenKind::Comma) { + // There are two conditions when we need to add the recovery context error: + // + // 1. If the parser is at a comma which means that there's a missing element + // otherwise the comma would've been consumed by the first `eat` call above. + // And, the parser doesn't take the re-lexing route on a comma token. + // 2. If it's the first element and the current token is not a comma which means + // that it's an invalid element. + + // test_err comma_separated_missing_element_between_commas + // [0, 1, , 2] + + // test_err comma_separated_missing_first_element + // call(= 1) + self.add_error( + recovery_context_kind.create_error(self), + self.current_token_range(), + ); + + trailing_comma_range = if self.at(TokenKind::Comma) { + Some(self.current_token_range()) + } else { + None + }; + } else { + // Otherwise, there should've been a comma at this position. This could be because + // the element isn't consumed completely by `parse_element`. + + // test_err comma_separated_missing_comma + // call(**x := 1) + self.expect(TokenKind::Comma); + + trailing_comma_range = None; + } + + self.bump_any(); + } + + if let Some(trailing_comma_range) = trailing_comma_range { + if !recovery_context_kind.allow_trailing_comma() { + let error = if cfg!(feature = "python315-diagnostics") + && matches!(recovery_context_kind, RecoveryContextKind::WithItems(_)) + && self.at(TokenKind::Colon) + { + ParseErrorType::TrailingCommaInWith + } else { + ParseErrorType::OtherError( + "trailing comma not allowed without surrounding parentheses".to_string(), + ) + }; + let range = if cfg!(feature = "python315-diagnostics") + && matches!( + recovery_context_kind, + RecoveryContextKind::ImportFromAsNames(Parenthesized::No) + ) + && self.at(TokenKind::Newline) + { + // The invalid import-target production diagnoses its NEWLINE. + self.current_token_range() + } else { + trailing_comma_range + }; + self.add_error(error, range); + } + } + + self.recovery_context = saved_context; + + trailing_comma_range.is_some() + } + + #[cold] + fn is_enclosing_list_element_or_terminator(&self) -> bool { + for context in self.recovery_context.kind_iter() { + if context.is_list_terminator(self) || context.is_list_element(self) { + return true; + } + } + + false + } + + /// Creates a checkpoint to which the parser can later return to using [`Self::rewind`]. + fn checkpoint(&self) -> ParserCheckpoint { + ParserCheckpoint { + tokens: self.tokens.checkpoint(), + errors_position: self.errors.len(), + deferred_starred_generator_errors_position: self + .deferred_starred_generator_errors + .len(), + unsupported_syntax_errors_position: self.unsupported_syntax_errors.len(), + current_token_id: self.current_token_id, + prev_token_end: self.prev_token_end, + recovery_context: self.recovery_context, + } + } + + /// Restore the parser to the given checkpoint. + fn rewind(&mut self, checkpoint: ParserCheckpoint) { + let ParserCheckpoint { + tokens, + errors_position, + deferred_starred_generator_errors_position, + unsupported_syntax_errors_position, + current_token_id, + prev_token_end, + recovery_context, + } = checkpoint; + + self.tokens.rewind(tokens); + self.errors.truncate(errors_position); + self.deferred_starred_generator_errors + .truncate(deferred_starred_generator_errors_position); + self.unsupported_syntax_errors + .truncate(unsupported_syntax_errors_position); + self.current_token_id = current_token_id; + self.prev_token_end = prev_token_end; + self.recovery_context = recovery_context; + } +} + +fn strip_underscores(text: &str) -> Cow<'_, str> { + if text.as_bytes().contains(&b'_') { + Cow::Owned(text.chars().filter(|&c| c != '_').collect()) + } else { + Cow::Borrowed(text) + } +} + +#[derive(Copy, Clone)] +enum IpyEscapeContext { + Assignment, + LogicalLineStart, +} + +impl IpyEscapeContext { + const fn is_logical_line_start(self) -> bool { + matches!(self, Self::LogicalLineStart) + } +} + +struct ParserCheckpoint { + tokens: TokenSourceCheckpoint, + errors_position: usize, + deferred_starred_generator_errors_position: usize, + unsupported_syntax_errors_position: usize, + current_token_id: TokenId, + prev_token_end: TextSize, + recovery_context: RecoveryContext, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum SequenceMatchPatternParentheses { + Tuple, + List, +} + +impl SequenceMatchPatternParentheses { + /// Returns the token kind that closes the parentheses. + const fn closing_kind(self) -> TokenKind { + match self { + SequenceMatchPatternParentheses::Tuple => TokenKind::Rpar, + SequenceMatchPatternParentheses::List => TokenKind::Rsqb, + } + } + + /// Returns `true` if the parentheses are for a list pattern e.g., `case [a, b]: ...`. + const fn is_list(self) -> bool { + matches!(self, SequenceMatchPatternParentheses::List) + } +} + +#[derive(Debug, PartialEq, Copy, Clone)] +enum FunctionKind { + /// A lambda expression, e.g., `lambda x: x` + Lambda, + /// A function definition, e.g., `def f(x): ...` + FunctionDef, +} + +impl FunctionKind { + /// Returns the token that terminates a list of parameters. + const fn list_terminator(self) -> TokenKind { + match self { + FunctionKind::Lambda => TokenKind::Colon, + FunctionKind::FunctionDef => TokenKind::Rpar, + } + } +} + +#[derive(Debug, PartialEq, Copy, Clone)] +enum WithItemKind { + /// A list of `with` items that are surrounded by parentheses. + /// + /// ```python + /// with (item1, item2): ... + /// with (item1, item2 as foo): ... + /// ``` + /// + /// The parentheses belongs to the `with` statement. + Parenthesized, + + /// The `with` item has a parenthesized expression. + /// + /// ```python + /// with (item) as foo: ... + /// ``` + /// + /// The parentheses belongs to the context expression. + ParenthesizedExpression, + + /// The `with` items aren't parenthesized in any way. + /// + /// ```python + /// with item: ... + /// with item as foo: ... + /// with item1, item2: ... + /// ``` + /// + /// There are no parentheses around the items. + Unparenthesized, +} + +impl WithItemKind { + /// Returns `true` if the with items are parenthesized. + const fn is_parenthesized(self) -> bool { + matches!(self, WithItemKind::Parenthesized) + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum InterpolatedStringElementsKind { + /// The regular f-string elements. + /// + /// For example, the `"hello "`, `x`, and `" world"` elements in: + /// ```py + /// f"hello {x:.2f} world" + /// ``` + Regular(InterpolatedStringKind), + + /// The f-string elements are part of the format specifier. + /// + /// For example, the `.2f` in: + /// ```py + /// f"hello {x:.2f} world" + /// ``` + FormatSpec(InterpolatedStringKind), +} + +impl InterpolatedStringElementsKind { + const fn list_terminator(self) -> TokenKind { + match self { + InterpolatedStringElementsKind::Regular(string_kind) => string_kind.end_token(), + // test_ok fstring_format_spec_terminator + // f"hello {x:} world" + // f"hello {x:.3f} world" + InterpolatedStringElementsKind::FormatSpec(_) => TokenKind::Rbrace, + } + } +} + +#[derive(Debug, PartialEq, Copy, Clone)] +enum Parenthesized { + /// The elements are parenthesized, e.g., `(a, b)`. + Yes, + /// The elements are not parenthesized, e.g., `a, b`. + No, +} + +impl From for Parenthesized { + fn from(value: bool) -> Self { + if value { + Parenthesized::Yes + } else { + Parenthesized::No + } + } +} + +impl Parenthesized { + /// Returns `true` if the parenthesized value is `Yes`. + const fn is_yes(self) -> bool { + matches!(self, Parenthesized::Yes) + } +} + +#[derive(Copy, Clone, Debug)] +enum ListTerminatorKind { + /// The current token terminates the list. + Regular, + /// The current token doesn't terminate the list, but is useful for better error recovery. + ErrorRecovery, +} + +#[derive(Copy, Clone, Debug)] +enum RecoveryContextKind { + /// When parsing a list of statements at the module level i.e., at the top level of a file. + ModuleStatements, + + /// When parsing a list of statements in a block e.g., the body of a function or a class. + BlockStatements, + + /// The `elif` clauses of an `if` statement + Elif, + + /// The `except` clauses of a `try` statement + Except, + + /// When parsing a list of assignment targets + AssignmentTargets, + + /// When parsing a list of type parameters + TypeParams, + + /// When parsing a list of names in a `from ... import ...` statement + ImportFromAsNames(Parenthesized), + + /// When parsing a list of names in an `import` statement + ImportNames, + + /// When parsing a list of slice elements e.g., `data[1, 2]`. + /// + /// This is different from `ListElements` as the surrounding context is + /// different in that the list is part of a subscript expression. + Slices, + + /// When parsing a list of elements in a list expression e.g., `[1, 2]` + ListElements, + + /// When parsing a list of elements in a set expression e.g., `{1, 2}` + SetElements, + + /// When parsing a list of elements in a dictionary expression e.g., `{1: "a", **data}` + DictElements, + + /// When parsing a list of elements in a tuple expression e.g., `(1, 2)` + TupleElements(Parenthesized), + + /// When parsing a list of patterns in a match statement with an optional + /// parentheses, e.g., `case a, b: ...`, `case (a, b): ...`, `case [a, b]: ...` + SequenceMatchPattern(Option), + + /// When parsing a mapping pattern in a match statement + MatchPatternMapping, + + /// When parsing a list of arguments in a class pattern for the match statement + MatchPatternClassArguments, + + /// When parsing a list of arguments in a function call or a class definition + Arguments, + + /// When parsing a `del` statement + DeleteTargets, + + /// When parsing a list of identifiers + Identifiers, + + /// When parsing a list of parameters in a function definition which can be + /// either a function definition or a lambda expression. + Parameters(FunctionKind), + + /// When parsing a list of items in a `with` statement + WithItems(WithItemKind), + + /// When parsing a list of f-string or t-string elements which are either literal elements, expressions, or interpolations. + InterpolatedStringElements(InterpolatedStringElementsKind), +} + +impl RecoveryContextKind { + /// Returns `true` if a trailing comma is allowed in the current context. + const fn allow_trailing_comma(self) -> bool { + match self { + RecoveryContextKind::Slices + | RecoveryContextKind::TupleElements(_) + | RecoveryContextKind::SetElements + | RecoveryContextKind::ListElements + | RecoveryContextKind::DictElements + | RecoveryContextKind::Arguments + | RecoveryContextKind::MatchPatternMapping + | RecoveryContextKind::SequenceMatchPattern(_) + | RecoveryContextKind::MatchPatternClassArguments + | RecoveryContextKind::Parameters(_) + | RecoveryContextKind::TypeParams + | RecoveryContextKind::DeleteTargets + | RecoveryContextKind::ImportFromAsNames(Parenthesized::Yes) => true, + + // Only allow a trailing comma if the with item itself is parenthesized + RecoveryContextKind::WithItems(WithItemKind::Parenthesized) => true, + + _ => false, + } + } + + /// Returns `true` if the parser is at a token that terminates the list as per the context. + /// + /// This token could either end the list or is only present for better error recovery. Refer to + /// [`is_regular_list_terminator`] to only check against the former. + /// + /// [`is_regular_list_terminator`]: RecoveryContextKind::is_regular_list_terminator + fn is_list_terminator(self, p: &Parser) -> bool { + self.list_terminator_kind(p).is_some() + } + + /// Returns `true` if the parser is at a token that terminates the list as per the context but + /// the token isn't part of the error recovery set. + #[expect(clippy::inline_always, reason = "reduces list-parser branch misses")] + #[inline(always)] + fn is_regular_list_terminator(self, p: &Parser) -> bool { + matches!( + self.list_terminator_kind(p), + Some(ListTerminatorKind::Regular) + ) + } + + /// Checks the current token the parser is at and returns the list terminator kind if the token + /// terminates the list as per the context. + #[expect(clippy::inline_always, reason = "reduces list-parser branch misses")] + #[inline(always)] + fn list_terminator_kind(self, p: &Parser) -> Option { + // The end of file marker ends all lists. + if p.at(TokenKind::EndOfFile) { + return Some(ListTerminatorKind::Regular); + } + + match self { + // The parser must consume all tokens until the end + RecoveryContextKind::ModuleStatements => None, + RecoveryContextKind::BlockStatements => p + .at(TokenKind::Dedent) + .then_some(ListTerminatorKind::Regular), + + RecoveryContextKind::Elif => { + p.at(TokenKind::Else).then_some(ListTerminatorKind::Regular) + } + RecoveryContextKind::Except => { + matches!(p.current_token_kind(), TokenKind::Finally | TokenKind::Else) + .then_some(ListTerminatorKind::Regular) + } + RecoveryContextKind::AssignmentTargets => { + // test_ok assign_targets_terminator + // x = y = z = 1; a, b + // x = y = z = 1 + // a, b + matches!(p.current_token_kind(), TokenKind::Newline | TokenKind::Semi) + .then_some(ListTerminatorKind::Regular) + } + + // Tokens other than `]` are for better error recovery. For example, recover when we + // find the `:` of a clause header or the equal of a type assignment. + RecoveryContextKind::TypeParams => { + if p.at(TokenKind::Rsqb) { + Some(ListTerminatorKind::Regular) + } else { + matches!( + p.current_token_kind(), + TokenKind::Newline | TokenKind::Colon | TokenKind::Equal | TokenKind::Lpar + ) + .then_some(ListTerminatorKind::ErrorRecovery) + } + } + // The names of an import statement cannot be parenthesized, so `)` is not a + // terminator. + RecoveryContextKind::ImportNames => { + // test_ok import_stmt_terminator + // import a, b; import c, d + // import a, b + // c, d + matches!(p.current_token_kind(), TokenKind::Semi | TokenKind::Newline) + .then_some(ListTerminatorKind::Regular) + } + RecoveryContextKind::ImportFromAsNames(_) => { + // test_ok from_import_stmt_terminator + // from a import (b, c) + // from a import (b, c); x, y + // from a import b, c; x, y + // from a import b, c + // x, y + matches!( + p.current_token_kind(), + TokenKind::Rpar | TokenKind::Semi | TokenKind::Newline + ) + .then_some(ListTerminatorKind::Regular) + } + // The elements in a container expression cannot end with a newline + // as all of them are actually non-logical newlines. + RecoveryContextKind::Slices | RecoveryContextKind::ListElements => { + p.at(TokenKind::Rsqb).then_some(ListTerminatorKind::Regular) + } + RecoveryContextKind::SetElements | RecoveryContextKind::DictElements => p + .at(TokenKind::Rbrace) + .then_some(ListTerminatorKind::Regular), + RecoveryContextKind::TupleElements(parenthesized) => { + if parenthesized.is_yes() { + p.at(TokenKind::Rpar).then_some(ListTerminatorKind::Regular) + } else { + // test_err aug_assign_stmt_unparenthesized_tuple_target + // a, b += 1 + (p.at_sequence_end() + || p.current_token_kind() + .as_augmented_assign_operator() + .is_some()) + .then_some(ListTerminatorKind::Regular) + } + } + RecoveryContextKind::SequenceMatchPattern(parentheses) => match parentheses { + None => { + // test_ok match_sequence_pattern_terminator + // match subject: + // case a if x: ... + // case a, b: ... + // case a, b if x: ... + // case a: ... + matches!(p.current_token_kind(), TokenKind::Colon | TokenKind::If) + .then_some(ListTerminatorKind::Regular) + } + Some(parentheses) => { + // test_ok match_sequence_pattern_parentheses_terminator + // match subject: + // case [a, b]: ... + // case (a, b): ... + p.at(parentheses.closing_kind()) + .then_some(ListTerminatorKind::Regular) + } + }, + RecoveryContextKind::MatchPatternMapping => p + .at(TokenKind::Rbrace) + .then_some(ListTerminatorKind::Regular), + RecoveryContextKind::MatchPatternClassArguments => { + p.at(TokenKind::Rpar).then_some(ListTerminatorKind::Regular) + } + RecoveryContextKind::Arguments => { + p.at(TokenKind::Rpar).then_some(ListTerminatorKind::Regular) + } + RecoveryContextKind::DeleteTargets | RecoveryContextKind::Identifiers => { + // test_ok del_targets_terminator + // del a, b; c, d + // del a, b + // c, d + matches!(p.current_token_kind(), TokenKind::Semi | TokenKind::Newline) + .then_some(ListTerminatorKind::Regular) + } + RecoveryContextKind::Parameters(function_kind) => { + // `lambda x, y: ...` or `def f(x, y): ...` + if p.at(function_kind.list_terminator()) { + Some(ListTerminatorKind::Regular) + } else { + // To recover from missing closing parentheses + (p.at(TokenKind::Rarrow) || p.at_compound_stmt()) + .then_some(ListTerminatorKind::ErrorRecovery) + } + } + RecoveryContextKind::WithItems(with_item_kind) => match with_item_kind { + WithItemKind::Parenthesized => match p.current_token_kind() { + TokenKind::Rpar => Some(ListTerminatorKind::Regular), + TokenKind::Colon => Some(ListTerminatorKind::ErrorRecovery), + _ => None, + }, + // test_err ipython_help_escape_command_error_recovery_1 + // # parse_options: {"mode": "ipython"} + // with (a, ?b) + // ? + + // test_err ipython_help_escape_command_error_recovery_2 + // # parse_options: {"mode": "ipython"} + // with (a, ?b + // ? + + // test_err ipython_help_escape_command_error_recovery_3 + // # parse_options: {"mode": "ipython"} + // with a, ?b + // ? + // x = 1 + WithItemKind::Unparenthesized => matches!( + p.current_token_kind(), + TokenKind::Colon | TokenKind::Newline + ) + .then_some(ListTerminatorKind::Regular), + WithItemKind::ParenthesizedExpression => p + .at(TokenKind::Colon) + .then_some(ListTerminatorKind::Regular), + }, + RecoveryContextKind::InterpolatedStringElements(kind) => { + if p.at(kind.list_terminator()) { + Some(ListTerminatorKind::Regular) + } else { + // test_err unterminated_fstring_newline_recovery + // f"hello + // 1 + 1 + // f"hello {x + // 2 + 2 + // f"hello {x: + // 3 + 3 + // f"hello {x} + // 4 + 4 + p.at(TokenKind::Newline) + .then_some(ListTerminatorKind::ErrorRecovery) + } + } + } + } + + #[expect(clippy::inline_always, reason = "reduces list-parser branch misses")] + #[inline(always)] + fn is_list_element(self, p: &Parser) -> bool { + match self { + RecoveryContextKind::ModuleStatements => p.at_stmt(), + RecoveryContextKind::BlockStatements => p.at_stmt(), + RecoveryContextKind::Elif => p.at(TokenKind::Elif), + RecoveryContextKind::Except => p.at(TokenKind::Except), + RecoveryContextKind::AssignmentTargets => p.at(TokenKind::Equal), + RecoveryContextKind::TypeParams => p.at_type_param(), + RecoveryContextKind::ImportNames => p.at_identifier_or_soft_keyword(), + RecoveryContextKind::ImportFromAsNames(_) => { + p.at(TokenKind::Star) || p.at_identifier_or_soft_keyword() + } + RecoveryContextKind::Slices => p.at(TokenKind::Colon) || p.at_expr(), + RecoveryContextKind::ListElements + | RecoveryContextKind::SetElements + | RecoveryContextKind::TupleElements(_) => p.at_expr(), + RecoveryContextKind::DictElements => p.at(TokenKind::DoubleStar) || p.at_expr(), + RecoveryContextKind::SequenceMatchPattern(_) => p.at_pattern_start(), + RecoveryContextKind::MatchPatternMapping => { + // A star pattern is invalid as a mapping key and is here only for + // better error recovery. + p.at(TokenKind::Star) || p.at_mapping_pattern_start() + } + RecoveryContextKind::MatchPatternClassArguments => p.at_pattern_start(), + RecoveryContextKind::Arguments => p.at_expr(), + RecoveryContextKind::DeleteTargets => p.at_expr(), + RecoveryContextKind::Identifiers => p.at_identifier_or_soft_keyword(), + RecoveryContextKind::Parameters(_) => { + // A `(` is an element only to report parenthesized parameters. + matches!( + p.current_token_kind(), + TokenKind::Star | TokenKind::DoubleStar | TokenKind::Slash | TokenKind::Lpar + ) || p.at_identifier_or_soft_keyword() + } + RecoveryContextKind::WithItems(_) => p.at_expr(), + RecoveryContextKind::InterpolatedStringElements(elements_kind) => match elements_kind { + InterpolatedStringElementsKind::Regular(interpolated_string_kind) + | InterpolatedStringElementsKind::FormatSpec(interpolated_string_kind) => { + p.current_token_kind() == interpolated_string_kind.middle_token() + || p.current_token_kind() == TokenKind::Lbrace + } + }, + } + } + + fn create_error(self, p: &Parser) -> ParseErrorType { + match self { + RecoveryContextKind::ModuleStatements | RecoveryContextKind::BlockStatements => { + if p.at(TokenKind::Indent) { + ParseErrorType::UnexpectedIndentation + } else { + ParseErrorType::OtherError("invalid syntax".to_string()) + } + } + RecoveryContextKind::Elif + | RecoveryContextKind::Except + | RecoveryContextKind::AssignmentTargets + | RecoveryContextKind::TypeParams + | RecoveryContextKind::ImportFromAsNames(_) + | RecoveryContextKind::ImportNames + | RecoveryContextKind::Slices + | RecoveryContextKind::ListElements + | RecoveryContextKind::SetElements + | RecoveryContextKind::DictElements + | RecoveryContextKind::TupleElements(_) + | RecoveryContextKind::SequenceMatchPattern(_) + | RecoveryContextKind::MatchPatternMapping + | RecoveryContextKind::MatchPatternClassArguments + | RecoveryContextKind::Arguments + | RecoveryContextKind::DeleteTargets + | RecoveryContextKind::Identifiers + | RecoveryContextKind::Parameters(_) + | RecoveryContextKind::WithItems(_) => { + ParseErrorType::OtherError("invalid syntax".to_string()) + } + RecoveryContextKind::InterpolatedStringElements(kind) => match kind { + InterpolatedStringElementsKind::Regular(string_kind) => ParseErrorType::OtherError( + format!("expected an element of or the end of the {string_kind}"), + ), + InterpolatedStringElementsKind::FormatSpec(string_kind) => { + ParseErrorType::OtherError(format!( + "expected an {string_kind} element or a '}}'" + )) + } + }, + } + } +} + +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +struct RecoveryContext(u32); + +bitflags! { + impl RecoveryContext: u32 { + const MODULE_STATEMENTS = 1 << 0; + const BLOCK_STATEMENTS = 1 << 1; + const ELIF = 1 << 2; + const EXCEPT = 1 << 3; + const ASSIGNMENT_TARGETS = 1 << 4; + const TYPE_PARAMS = 1 << 5; + const IMPORT_FROM_AS_NAMES_PARENTHESIZED = 1 << 6; + const IMPORT_FROM_AS_NAMES_UNPARENTHESIZED = 1 << 7; + const IMPORT_NAMES = 1 << 8; + const SLICES = 1 << 9; + const LIST_ELEMENTS = 1 << 10; + const SET_ELEMENTS = 1 << 11; + const DICT_ELEMENTS = 1 << 12; + const TUPLE_ELEMENTS_PARENTHESIZED = 1 << 13; + const TUPLE_ELEMENTS_UNPARENTHESIZED = 1 << 14; + const SEQUENCE_MATCH_PATTERN = 1 << 15; + const SEQUENCE_MATCH_PATTERN_LIST = 1 << 16; + const SEQUENCE_MATCH_PATTERN_TUPLE = 1 << 17; + const MATCH_PATTERN_MAPPING = 1 << 18; + const MATCH_PATTERN_CLASS_ARGUMENTS = 1 << 19; + const ARGUMENTS = 1 << 20; + const DELETE = 1 << 21; + const IDENTIFIERS = 1 << 22; + const FUNCTION_PARAMETERS = 1 << 23; + const LAMBDA_PARAMETERS = 1 << 24; + const WITH_ITEMS_PARENTHESIZED = 1 << 25; + const WITH_ITEMS_PARENTHESIZED_EXPRESSION = 1 << 26; + const WITH_ITEMS_UNPARENTHESIZED = 1 << 27; + const F_STRING_ELEMENTS = 1 << 28; + const T_STRING_ELEMENTS = 1 << 29; + const F_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 30; + const T_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 31; + } +} + +impl RecoveryContext { + const fn from_kind(kind: RecoveryContextKind) -> Self { + match kind { + RecoveryContextKind::ModuleStatements => RecoveryContext::MODULE_STATEMENTS, + RecoveryContextKind::BlockStatements => RecoveryContext::BLOCK_STATEMENTS, + RecoveryContextKind::Elif => RecoveryContext::ELIF, + RecoveryContextKind::Except => RecoveryContext::EXCEPT, + RecoveryContextKind::AssignmentTargets => RecoveryContext::ASSIGNMENT_TARGETS, + RecoveryContextKind::TypeParams => RecoveryContext::TYPE_PARAMS, + RecoveryContextKind::ImportFromAsNames(parenthesized) => match parenthesized { + Parenthesized::Yes => RecoveryContext::IMPORT_FROM_AS_NAMES_PARENTHESIZED, + Parenthesized::No => RecoveryContext::IMPORT_FROM_AS_NAMES_UNPARENTHESIZED, + }, + RecoveryContextKind::ImportNames => RecoveryContext::IMPORT_NAMES, + RecoveryContextKind::Slices => RecoveryContext::SLICES, + RecoveryContextKind::ListElements => RecoveryContext::LIST_ELEMENTS, + RecoveryContextKind::SetElements => RecoveryContext::SET_ELEMENTS, + RecoveryContextKind::DictElements => RecoveryContext::DICT_ELEMENTS, + RecoveryContextKind::TupleElements(parenthesized) => match parenthesized { + Parenthesized::Yes => RecoveryContext::TUPLE_ELEMENTS_PARENTHESIZED, + Parenthesized::No => RecoveryContext::TUPLE_ELEMENTS_UNPARENTHESIZED, + }, + RecoveryContextKind::SequenceMatchPattern(parentheses) => match parentheses { + None => RecoveryContext::SEQUENCE_MATCH_PATTERN, + Some(SequenceMatchPatternParentheses::List) => { + RecoveryContext::SEQUENCE_MATCH_PATTERN_LIST + } + Some(SequenceMatchPatternParentheses::Tuple) => { + RecoveryContext::SEQUENCE_MATCH_PATTERN_TUPLE + } + }, + RecoveryContextKind::MatchPatternMapping => RecoveryContext::MATCH_PATTERN_MAPPING, + RecoveryContextKind::MatchPatternClassArguments => { + RecoveryContext::MATCH_PATTERN_CLASS_ARGUMENTS + } + RecoveryContextKind::Arguments => RecoveryContext::ARGUMENTS, + RecoveryContextKind::DeleteTargets => RecoveryContext::DELETE, + RecoveryContextKind::Identifiers => RecoveryContext::IDENTIFIERS, + RecoveryContextKind::Parameters(function_kind) => match function_kind { + FunctionKind::Lambda => RecoveryContext::LAMBDA_PARAMETERS, + FunctionKind::FunctionDef => RecoveryContext::FUNCTION_PARAMETERS, + }, + RecoveryContextKind::WithItems(with_item_kind) => match with_item_kind { + WithItemKind::Parenthesized => RecoveryContext::WITH_ITEMS_PARENTHESIZED, + WithItemKind::ParenthesizedExpression => { + RecoveryContext::WITH_ITEMS_PARENTHESIZED_EXPRESSION + } + WithItemKind::Unparenthesized => RecoveryContext::WITH_ITEMS_UNPARENTHESIZED, + }, + RecoveryContextKind::InterpolatedStringElements(kind) => match kind { + InterpolatedStringElementsKind::Regular(InterpolatedStringKind::FString) => { + RecoveryContext::F_STRING_ELEMENTS + } + InterpolatedStringElementsKind::Regular(InterpolatedStringKind::TString) => { + RecoveryContext::T_STRING_ELEMENTS + } + + InterpolatedStringElementsKind::FormatSpec(InterpolatedStringKind::FString) => { + RecoveryContext::F_STRING_ELEMENTS_IN_FORMAT_SPEC + } + InterpolatedStringElementsKind::FormatSpec(InterpolatedStringKind::TString) => { + RecoveryContext::T_STRING_ELEMENTS_IN_FORMAT_SPEC + } + }, + } + } + + /// Safe conversion to the corresponding [`RecoveryContextKind`] (inverse of [`Self::from_kind`]). + /// + /// Returns `None` if the `RecoveryContext` is empty or has multiple flags set. + const fn to_kind(self) -> Option { + Some(match self { + RecoveryContext::MODULE_STATEMENTS => RecoveryContextKind::ModuleStatements, + RecoveryContext::BLOCK_STATEMENTS => RecoveryContextKind::BlockStatements, + RecoveryContext::ELIF => RecoveryContextKind::Elif, + RecoveryContext::EXCEPT => RecoveryContextKind::Except, + RecoveryContext::ASSIGNMENT_TARGETS => RecoveryContextKind::AssignmentTargets, + RecoveryContext::TYPE_PARAMS => RecoveryContextKind::TypeParams, + RecoveryContext::IMPORT_FROM_AS_NAMES_PARENTHESIZED => { + RecoveryContextKind::ImportFromAsNames(Parenthesized::Yes) + } + RecoveryContext::IMPORT_FROM_AS_NAMES_UNPARENTHESIZED => { + RecoveryContextKind::ImportFromAsNames(Parenthesized::No) + } + RecoveryContext::IMPORT_NAMES => RecoveryContextKind::ImportNames, + RecoveryContext::SLICES => RecoveryContextKind::Slices, + RecoveryContext::LIST_ELEMENTS => RecoveryContextKind::ListElements, + RecoveryContext::SET_ELEMENTS => RecoveryContextKind::SetElements, + RecoveryContext::DICT_ELEMENTS => RecoveryContextKind::DictElements, + RecoveryContext::TUPLE_ELEMENTS_PARENTHESIZED => { + RecoveryContextKind::TupleElements(Parenthesized::Yes) + } + RecoveryContext::TUPLE_ELEMENTS_UNPARENTHESIZED => { + RecoveryContextKind::TupleElements(Parenthesized::No) + } + RecoveryContext::SEQUENCE_MATCH_PATTERN => { + RecoveryContextKind::SequenceMatchPattern(None) + } + RecoveryContext::SEQUENCE_MATCH_PATTERN_LIST => { + RecoveryContextKind::SequenceMatchPattern(Some( + SequenceMatchPatternParentheses::List, + )) + } + RecoveryContext::SEQUENCE_MATCH_PATTERN_TUPLE => { + RecoveryContextKind::SequenceMatchPattern(Some( + SequenceMatchPatternParentheses::Tuple, + )) + } + RecoveryContext::MATCH_PATTERN_MAPPING => RecoveryContextKind::MatchPatternMapping, + RecoveryContext::MATCH_PATTERN_CLASS_ARGUMENTS => { + RecoveryContextKind::MatchPatternClassArguments + } + RecoveryContext::ARGUMENTS => RecoveryContextKind::Arguments, + RecoveryContext::DELETE => RecoveryContextKind::DeleteTargets, + RecoveryContext::IDENTIFIERS => RecoveryContextKind::Identifiers, + RecoveryContext::FUNCTION_PARAMETERS => { + RecoveryContextKind::Parameters(FunctionKind::FunctionDef) + } + RecoveryContext::LAMBDA_PARAMETERS => { + RecoveryContextKind::Parameters(FunctionKind::Lambda) + } + RecoveryContext::WITH_ITEMS_PARENTHESIZED => { + RecoveryContextKind::WithItems(WithItemKind::Parenthesized) + } + RecoveryContext::WITH_ITEMS_PARENTHESIZED_EXPRESSION => { + RecoveryContextKind::WithItems(WithItemKind::ParenthesizedExpression) + } + RecoveryContext::WITH_ITEMS_UNPARENTHESIZED => { + RecoveryContextKind::WithItems(WithItemKind::Unparenthesized) + } + RecoveryContext::F_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements( + InterpolatedStringElementsKind::Regular(InterpolatedStringKind::FString), + ), + RecoveryContext::T_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements( + InterpolatedStringElementsKind::Regular(InterpolatedStringKind::TString), + ), + RecoveryContext::F_STRING_ELEMENTS_IN_FORMAT_SPEC => { + RecoveryContextKind::InterpolatedStringElements( + InterpolatedStringElementsKind::FormatSpec(InterpolatedStringKind::FString), + ) + } + RecoveryContext::T_STRING_ELEMENTS_IN_FORMAT_SPEC => { + RecoveryContextKind::InterpolatedStringElements( + InterpolatedStringElementsKind::FormatSpec(InterpolatedStringKind::TString), + ) + } + _ => return None, + }) + } + + fn kind_iter(self) -> impl Iterator { + self.iter().map(|context| { + context + .to_kind() + .expect("Expected context to be of a single kind.") + }) + } +} + +/// Returns the 1-based line number of `offset` in `source`. +/// +/// `\n`, `\r\n`, and a lone `\r` each count as one line break. +fn line_number(source: &str, offset: TextSize) -> u32 { + let text = &source[..offset.to_usize()]; + let newlines = + text.matches('\n').count() + text.matches('\r').count() - text.matches("\r\n").count(); + u32::try_from(newlines + 1).unwrap() +} + +/// Moves the error that stops parsing first to the front of `errors`. +/// +/// The source is tokenized again without the parser's error recovery, and its first tokenizer +/// error is compared with the first error found by the parser, other than a tokenizer error. A +/// tokenizer error detected before that error stops parsing there. One detected after it is still +/// reported, because the rest of the source is tokenized once parsing failed, except: +/// - an error detected inside an f-string or t-string keeps the parser's error; +/// - an unexpected indent is reported without tokenizing the rest of the source; +/// - a bracket that is never closed wins over an error on a later line. On the same line, it wins +/// over an error at or after it when parsing reads ahead from the error to the end of the +/// source, which it does unless a `:`, a `;` or an unknown token follows. An error found by +/// recovering from an unclosed bracket is placed at the token that started the recovery, +/// `unclosed_bracket_recovery`. +fn prioritize_tokenizer_error( + errors: &mut Vec, + source: &str, + mode: Mode, + start_offset: TextSize, + unclosed_bracket_recovery: Option, +) { + // The lexer reports every tokenizer error, so a source without errors has none to find. + if errors.is_empty() { + return; + } + let is_tokenizer_error = |error: &ParseError| matches!(&error.error, ParseErrorType::Lexical(lexical) if lexical.is_tokenizer_error()); + let first = errors + .iter() + .find(|error| !is_tokenizer_error(error)) + .cloned(); + + let mut lexer = Lexer::new(source, mode, start_offset); + // The starts of the tokens at which parsing stops without reading ahead. + let mut stops = Vec::new(); + #[cfg(feature = "python315-diagnostics")] + let mut replacement_hint = ReplacementFieldHint::new(first.as_ref()); + #[cfg(feature = "python315-diagnostics")] + let mut replacement_hint_wins = false; + let (tokenizer_error, in_interpolated_string, detected) = loop { + #[cfg(feature = "python315-diagnostics")] + let replacement_field = lexer.replacement_field(); + let in_interpolated_string = lexer.in_interpolated_string(); + let previous_errors = lexer.errors().len(); + let kind = lexer.next_token(); + if let Some(error) = lexer.errors()[previous_errors..] + .iter() + .find(|error| error.error().is_tokenizer_error()) + { + #[cfg(feature = "python315-diagnostics")] + { + replacement_hint_wins = + replacement_hint.is_unclosed_field_hint(error.error(), replacement_field); + } + // A missing closing brace or a bracket that closes a replacement field is found at + // the token that shows it. + let detected = match error.error() { + LexicalErrorType::FStringError( + InterpolatedStringErrorType::UnclosedLbrace + | InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } + | InterpolatedStringErrorType::UnmatchedBracket(_), + ) + | LexicalErrorType::TStringError( + InterpolatedStringErrorType::UnclosedLbrace + | InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } + | InterpolatedStringErrorType::UnmatchedBracket(_), + ) => error.location().start(), + _ => lexer.offset(), + }; + break (error.clone(), in_interpolated_string, detected); + } + #[cfg(feature = "python315-diagnostics")] + replacement_hint.token(kind, lexer.current_range(), replacement_field); + if matches!( + kind, + TokenKind::Colon | TokenKind::Semi | TokenKind::Unknown + ) { + stops.push(lexer.current_range().start()); + } + if kind == TokenKind::EndOfFile { + #[cfg(feature = "python315-diagnostics")] + if let Some(start) = replacement_hint.unclosed_format_spec { + let error = LexicalError::new( + LexicalErrorType::UnclosedBracket { + opening: '{', + incomplete: false, + }, + TextRange::empty(start), + ); + break (error, false, lexer.offset()); + } + return; + } + }; + + let Some(first) = first else { + move_to_front(errors, ParseError::from(tokenizer_error)); + return; + }; + let first_start = first.location.start(); + let error_start = match tokenizer_error.error() { + LexicalErrorType::UnclosedBracket { .. } => { + unclosed_bracket_recovery.map_or(first_start, |start| start.max(first_start)) + } + _ => first_start, + }; + // An escape sequence error is found when its string is parsed, before the end of the + // source shows an unclosed bracket. + let escape_error = matches!( + first.error, + ParseErrorType::Lexical( + LexicalErrorType::UnicodeEscapeError { .. } | LexicalErrorType::BytesEscapeError { .. } + ) + ); + #[cfg(not(feature = "python315-diagnostics"))] + let replacement_hint_wins = false; + let wins = replacement_hint_wins + || detected <= first_start + || (!in_interpolated_string + && !escape_error + && match tokenizer_error.error() { + LexicalErrorType::UnclosedBracket { .. } => { + let bracket_start = tokenizer_error.location().start(); + bracket_start <= error_start + && (line_number(source, bracket_start) < line_number(source, error_start) + || stops.iter().all(|stop| *stop < first_start)) + } + _ => !matches!(first.error, ParseErrorType::UnexpectedIndentation), + }); + if !wins { + move_to_front(errors, first); + return; + } + // A bracket reported in place of an error before the end of the source can't be closed by + // more input. + let front = match *tokenizer_error.error() { + LexicalErrorType::UnclosedBracket { + opening, + incomplete: true, + } if error_start.to_usize() < source.trim_end().len() => { + errors.retain(|error| *error != ParseError::from(tokenizer_error.clone())); + LexicalError::new( + LexicalErrorType::UnclosedBracket { + opening, + incomplete: false, + }, + tokenizer_error.location(), + ) + } + _ => tokenizer_error, + }; + move_to_front(errors, ParseError::from(front)); +} + +/// Tracks the grammar boundaries after an expression hint while the unrecovered token stream +/// is read. A replacement field keeps reading to its separator; an inner display or a debug / +/// conversion separator can already establish the hint before a later missing brace is found. +#[cfg(feature = "python315-diagnostics")] +struct ReplacementFieldHint { + range: Option, + field: Option, + stopped: bool, + unclosed_format_spec: Option, +} + +#[cfg(feature = "python315-diagnostics")] +impl ReplacementFieldHint { + fn new(first: Option<&ParseError>) -> Self { + let range = first.and_then(|error| match &error.error { + ParseErrorType::MissingCommaAfterConcatenatedLiteral { + expression_range, .. + } => Some(*expression_range), + ParseErrorType::OtherError(message) + if matches!( + message.as_str(), + "invalid syntax. Perhaps you forgot a comma?" + | "invalid syntax. Is this intended to be part of the string?" + ) => + { + Some(error.location) + } + _ => None, + }); + Self { + range, + field: None, + stopped: false, + unclosed_format_spec: None, + } + } + + fn token(&mut self, kind: TokenKind, range: TextRange, field: Option<(TextSize, bool)>) { + let Some(hint) = self.range else { + return; + }; + if range.contains(hint.start()) { + self.field = field.map(|(start, _)| start); + } + if range.start() < hint.end() || self.field.is_none() { + return; + } + // The hint covers both expressions. A later expression token establishes the hint; + // only the format-specifier and string-end tokens still belong to this field. + if !kind.is_trivia() + && !matches!( + kind, + TokenKind::Colon + | TokenKind::FStringMiddle + | TokenKind::TStringMiddle + | TokenKind::FStringEnd + | TokenKind::TStringEnd + | TokenKind::EndOfFile + ) + { + self.stopped = true; + } + if kind.is_interpolated_string_end() && !self.stopped { + self.unclosed_format_spec = field + .filter(|(start, format)| *format && Some(*start) == self.field) + .map(|(start, _)| start); + } + } + + fn is_unclosed_field_hint( + &self, + error: &LexicalErrorType, + field: Option<(TextSize, bool)>, + ) -> bool { + self.field.is_some() + && self.field == field.map(|(start, _)| start) + && !self.stopped + && matches!( + error, + LexicalErrorType::FStringError( + InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } + ) | LexicalErrorType::TStringError( + InterpolatedStringErrorType::UnclosedLbraceOnLine { .. } + ) + ) + } +} + +/// Moves `error` to the front of `errors`, removing an equal error elsewhere. +fn move_to_front(errors: &mut Vec, error: ParseError) { + errors.retain(|other| *other != error); + errors.insert(0, error); +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/options.rs b/vendor/rustpython-ruff_python_parser/src/parser/options.rs new file mode 100644 index 00000000000..ec87a72d1e4 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/options.rs @@ -0,0 +1,59 @@ +use ruff_python_ast::{PySourceType, PythonVersion}; + +use crate::{AsMode, Mode}; + +/// Options for controlling how a source file is parsed. +/// +/// You can construct a [`ParseOptions`] directly from a [`Mode`]: +/// +/// ``` +/// use ruff_python_parser::{Mode, ParseOptions}; +/// +/// let options = ParseOptions::from(Mode::Module); +/// ``` +/// +/// or from a [`PySourceType`] +/// +/// ``` +/// use ruff_python_ast::PySourceType; +/// use ruff_python_parser::ParseOptions; +/// +/// let options = ParseOptions::from(PySourceType::Python); +/// ``` +#[derive(Clone, Debug)] +pub struct ParseOptions { + /// Specify the mode in which the code will be parsed. + pub(crate) mode: Mode, + /// Target version for detecting version-related syntax errors. + pub(crate) target_version: PythonVersion, +} + +impl ParseOptions { + #[must_use] + pub fn with_target_version(mut self, target_version: PythonVersion) -> Self { + self.target_version = target_version; + self + } + + pub fn target_version(&self) -> PythonVersion { + self.target_version + } +} + +impl From for ParseOptions { + fn from(mode: Mode) -> Self { + Self { + mode, + target_version: PythonVersion::default(), + } + } +} + +impl From for ParseOptions { + fn from(source_type: PySourceType) -> Self { + Self { + mode: source_type.as_mode(), + target_version: PythonVersion::default(), + } + } +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/pattern.rs b/vendor/rustpython-ruff_python_parser/src/parser/pattern.rs new file mode 100644 index 00000000000..44618b73721 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/pattern.rs @@ -0,0 +1,889 @@ +use ruff_python_ast::name::Name; +use ruff_python_ast::token::TokenKind; +use ruff_python_ast::{ + self as ast, AtomicNodeIndex, Expr, ExprContext, Number, Operator, Pattern, PatternKeys, + Patterns, Singleton, +}; +use ruff_text_size::{Ranged, TextSize}; + +use crate::parser::progress::ParserProgress; +use crate::parser::{Parser, RecoveryContextKind, SequenceMatchPatternParentheses, recovery}; +use crate::token_set::TokenSet; +use crate::{ExpressionKind, ParseErrorType, UnsupportedSyntaxErrorKind}; + +use super::expression::ExpressionContext; +use super::statement::invalid_target_identifier; + +/// The set of tokens that can start a literal pattern. +const LITERAL_PATTERN_START_SET: TokenSet = TokenSet::new([ + TokenKind::None, + TokenKind::True, + TokenKind::False, + TokenKind::String, + TokenKind::Int, + TokenKind::Float, + TokenKind::Complex, + TokenKind::Minus, // Unary minus + TokenKind::Plus, // Unary plus +]); + +/// The set of tokens that can start a pattern. +const PATTERN_START_SET: TokenSet = TokenSet::new([ + // Star pattern + TokenKind::Star, + // Capture pattern + // Wildcard pattern ('_' is an identifier token) + // Value pattern (name or attribute) + // Class pattern + TokenKind::Identifier, + // Group pattern + TokenKind::Lpar, + // Sequence pattern + TokenKind::Lsqb, + // Mapping pattern + TokenKind::Lbrace, +]) +.union(LITERAL_PATTERN_START_SET); + +/// The set of tokens that can start a mapping pattern. +const MAPPING_PATTERN_START_SET: TokenSet = TokenSet::new([ + // Double star pattern + TokenKind::DoubleStar, + // Value pattern + TokenKind::Identifier, +]) +.union(LITERAL_PATTERN_START_SET); + +impl Parser<'_> { + /// Returns `true` if the current token is a valid start of a pattern. + pub(super) fn at_pattern_start(&self) -> bool { + self.at_ts(PATTERN_START_SET) || self.at_soft_keyword() + } + + /// Returns `true` if the current token is a valid start of a mapping pattern. + pub(super) fn at_mapping_pattern_start(&self) -> bool { + self.at_ts(MAPPING_PATTERN_START_SET) || self.at_soft_keyword() + } + + /// Entry point to start parsing a pattern. + /// + /// See: + pub(super) fn parse_match_patterns(&mut self) -> Pattern { + let start = self.node_start(); + + // We don't yet know if it's a sequence pattern or a single pattern, so + // we need to allow star pattern here. + let pattern = self.parse_match_pattern(AllowStarPattern::Yes); + + if self.at(TokenKind::Comma) { + Pattern::MatchSequence(self.parse_sequence_match_pattern(pattern, start, None)) + } else { + // We know it's not a sequence pattern now, so check for star pattern usage. + if pattern.is_match_star() { + self.add_error(ParseErrorType::InvalidStarPatternUsage, &pattern); + } + pattern + } + } + + /// Parses an `or_pattern` or an `as_pattern`. + /// + /// See: + fn parse_match_pattern(&mut self, allow_star_pattern: AllowStarPattern) -> Pattern { + let start = self.node_start(); + + // We don't yet know if it's an or pattern or an as pattern, so use whatever + // was passed in. + let mut lhs = self.parse_match_pattern_lhs(allow_star_pattern); + + // Or pattern + if self.at(TokenKind::Vbar) { + // We know it's an `or` pattern now, so check for star pattern usage. + if lhs.is_match_star() { + self.add_error(ParseErrorType::InvalidStarPatternUsage, &lhs); + } + + let mut patterns = vec![lhs]; + let mut progress = ParserProgress::default(); + + while self.eat(TokenKind::Vbar) { + progress.assert_progressing(self); + let pattern = self.parse_match_pattern_lhs(AllowStarPattern::No); + patterns.push(pattern); + } + + lhs = Pattern::MatchOr(ast::PatternMatchOr { + range: self.node_range(start), + patterns, + node_index: AtomicNodeIndex::NONE, + runtime_patterns: None, + }); + } + + // As pattern + if self.eat(TokenKind::As) { + // We know it's an `as` pattern now, so check for star pattern usage. + if lhs.is_match_star() { + self.add_error(ParseErrorType::InvalidStarPatternUsage, &lhs); + } + + let ident = if self.at_expr() + && !(self.at_identifier_or_soft_keyword() + && (self.src_text(self.current_token_range()) == "_" + || !matches!( + self.peek(), + TokenKind::Dot | TokenKind::Lpar | TokenKind::Equal + ))) { + // test_err match_as_pattern_invalid_target + // match x: + // case y as z.w: ... + // case y as f(): ... + let target = self.parse_conditional_expression_or_higher(); + self.add_error( + ParseErrorType::InvalidPatternTarget(ExpressionKind::of(&target.expr)), + &target, + ); + invalid_target_identifier(&target.expr) + } else { + self.parse_match_pattern_target() + }; + lhs = Pattern::MatchAs(ast::PatternMatchAs { + range: self.node_range(start), + name: Some(ident), + pattern: Some(Box::new(lhs)), + node_index: AtomicNodeIndex::NONE, + }); + } + + lhs + } + + /// Parses a pattern. + /// + /// See: + fn parse_match_pattern_lhs(&mut self, allow_star_pattern: AllowStarPattern) -> Pattern { + self.with_recursion(|parser| { + let start = parser.node_start(); + + let mut lhs = match parser.current_token_kind() { + TokenKind::Lbrace => Pattern::MatchMapping(parser.parse_match_pattern_mapping()), + TokenKind::Star => { + let star_pattern = parser.parse_match_pattern_star(); + if allow_star_pattern.is_no() { + parser.add_error(ParseErrorType::InvalidStarPatternUsage, &star_pattern); + } + Pattern::MatchStar(star_pattern) + } + TokenKind::Lpar | TokenKind::Lsqb => { + parser.parse_parenthesized_or_sequence_pattern() + } + _ => parser.parse_match_pattern_literal(), + }; + + if parser.at(TokenKind::Lpar) { + lhs = Pattern::MatchClass(parser.parse_match_pattern_class(lhs, start)); + } + + if matches!( + parser.current_token_kind(), + TokenKind::Plus | TokenKind::Minus + ) { + lhs = Pattern::MatchValue(parser.parse_complex_literal_pattern(lhs, start)); + } + + lhs + }) + } + + /// Parses a mapping pattern. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `{` token. + /// + /// See: + fn parse_match_pattern_mapping(&mut self) -> ast::PatternMatchMapping { + let start = self.node_start(); + self.bump(TokenKind::Lbrace); + + let mut keys = PatternKeys::new(); + let mut patterns = Patterns::new(); + let mut rest = None; + + self.parse_comma_separated_list(RecoveryContextKind::MatchPatternMapping, |parser| { + let mapping_item_start = parser.node_start(); + let mapping_item_token = parser.current_token_range(); + let mapping_item_errors_start = parser.errors.len(); + + if parser.eat(TokenKind::DoubleStar) { + // A `_` rest target is not a capture target. + let identifier = parser.parse_identifier(); + if identifier.is_valid() && identifier.id == "_" { + parser.add_error( + ParseErrorType::OtherError("invalid syntax".to_string()), + &identifier, + ); + } + if rest.is_some() { + parser.add_error( + ParseErrorType::OtherError( + "only one double star pattern is allowed".to_string(), + ), + parser.node_range(mapping_item_start), + ); + } + // TODO(dhruvmanila): It's not possible to retain multiple double starred + // patterns because of the way the mapping node is represented in the grammar. + // The last value will always win. Update the AST representation. + // See: https://github.com/astral-sh/ruff/pull/10477#discussion_r1535143536 + rest = Some(identifier); + } else { + let key = match parser.parse_match_pattern_lhs(AllowStarPattern::No) { + Pattern::MatchValue(ast::PatternMatchValue { value, .. }) => *value, + Pattern::MatchSingleton(ast::PatternMatchSingleton { + value, + range, + node_index, + }) => match value { + Singleton::None => { + Expr::NoneLiteral(ast::ExprNoneLiteral { range, node_index }) + } + Singleton::True => Expr::BooleanLiteral(ast::ExprBooleanLiteral { + value: true, + range, + node_index, + }), + Singleton::False => Expr::BooleanLiteral(ast::ExprBooleanLiteral { + value: false, + range, + node_index, + }), + }, + pattern => { + parser.add_error( + ParseErrorType::OtherError("invalid mapping pattern key".to_string()), + &pattern, + ); + recovery::pattern_to_expr(pattern) + } + }; + keys.push(key); + + let colon_errors_start = parser.errors.len(); + parser.expect(TokenKind::Colon); + if cfg!(feature = "python315-diagnostics") + && rest.is_some() + && parser.errors.len() > colon_errors_start + { + // An incomplete mapping entry does not match the invalid rest-order + // production. Report its first token, where the mapping stopped. + parser.errors[colon_errors_start].location = mapping_item_token; + } + + patterns.push(parser.parse_match_pattern(AllowStarPattern::No)); + + if let Some(rest) = &rest { + if cfg!(feature = "python315-diagnostics") { + if parser.errors.len() == mapping_item_errors_start { + parser.add_error(ParseErrorType::MappingRestPatternNotLast, rest); + } + } else { + parser.add_error( + ParseErrorType::OtherError( + "pattern cannot follow a double star pattern".to_string(), + ), + parser.node_range(mapping_item_start), + ); + } + } + } + }); + + self.expect(TokenKind::Rbrace); + + ast::PatternMatchMapping { + range: self.node_range(start), + keys, + patterns, + rest, + node_index: AtomicNodeIndex::NONE, + runtime_keys: None, + runtime_patterns: None, + } + } + + /// Parses a star pattern. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `*` token. + /// + /// See: + fn parse_match_pattern_star(&mut self) -> ast::PatternMatchStar { + let start = self.node_start(); + self.bump(TokenKind::Star); + + let ident = self.parse_identifier(); + + ast::PatternMatchStar { + range: self.node_range(start), + name: if ident.is_valid() && ident.id == "_" { + None + } else { + Some(ident) + }, + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a binding target in an `as` or mapping pattern. + fn parse_match_pattern_target(&mut self) -> ast::Identifier { + // test_err invalid_match_pattern_target + // match value: + // case 1 as _: ... + // case {**_}: ... + // after = 1 + let identifier = self.parse_identifier(); + if identifier.is_valid() && identifier.id == "_" { + self.add_error(ParseErrorType::InvalidMatchPatternTarget, &identifier); + } + identifier + } + + /// Parses a parenthesized pattern or a sequence pattern. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `(` or `[` token. + /// + /// See: + fn parse_parenthesized_or_sequence_pattern(&mut self) -> Pattern { + let start = self.node_start(); + let parentheses = if self.eat(TokenKind::Lpar) { + SequenceMatchPatternParentheses::Tuple + } else { + self.bump(TokenKind::Lsqb); + SequenceMatchPatternParentheses::List + }; + + if matches!( + self.current_token_kind(), + TokenKind::Newline | TokenKind::Colon + ) { + // TODO(dhruvmanila): This recovery isn't possible currently because + // of the soft keyword transformer. If there's a missing closing + // parenthesis, it'll consider `case` an identifier token instead. + self.add_error( + ParseErrorType::OtherError(format!( + "missing '{closing}'", + closing = if parentheses.is_list() { "]" } else { ")" } + )), + self.current_token_range(), + ); + } + + if self.eat(parentheses.closing_kind()) { + return Pattern::MatchSequence(ast::PatternMatchSequence { + patterns: Vec::new(), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_patterns: None, + }); + } + + let mut pattern = self.parse_match_pattern(AllowStarPattern::Yes); + + if parentheses.is_list() || self.at(TokenKind::Comma) { + pattern = Pattern::MatchSequence(self.parse_sequence_match_pattern( + pattern, + start, + Some(parentheses), + )); + } else { + self.expect(parentheses.closing_kind()); + } + + pattern + } + + /// Parses the rest of a sequence pattern, given the first element. + /// + /// If the `parentheses` is `None`, it is an [open sequence pattern]. + /// + /// See: + /// + /// [open sequence pattern]: https://docs.python.org/3/reference/compound_stmts.html#grammar-token-python-grammar-open_sequence_pattern + fn parse_sequence_match_pattern( + &mut self, + first_element: Pattern, + start: TextSize, + parentheses: Option, + ) -> ast::PatternMatchSequence { + if parentheses.is_some_and(|parentheses| { + self.at(parentheses.closing_kind()) || self.peek() == parentheses.closing_kind() + }) { + // The comma is optional if it is a single-element sequence + self.eat(TokenKind::Comma); + } else { + self.expect(TokenKind::Comma); + } + + let mut patterns = vec![first_element]; + + self.parse_comma_separated_list( + RecoveryContextKind::SequenceMatchPattern(parentheses), + |parser| patterns.push(parser.parse_match_pattern(AllowStarPattern::Yes)), + ); + + if let Some(parentheses) = parentheses { + self.expect(parentheses.closing_kind()); + } + + ast::PatternMatchSequence { + range: self.node_range(start), + patterns, + node_index: AtomicNodeIndex::NONE, + runtime_patterns: None, + } + } + + /// Parses a literal pattern. + /// + /// See: + fn parse_match_pattern_literal(&mut self) -> Pattern { + let start = self.node_start(); + match self.current_token_kind() { + TokenKind::None => { + self.bump(TokenKind::None); + Pattern::MatchSingleton(ast::PatternMatchSingleton { + value: Singleton::None, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::True => { + self.bump(TokenKind::True); + Pattern::MatchSingleton(ast::PatternMatchSingleton { + value: Singleton::True, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::False => { + self.bump(TokenKind::False); + Pattern::MatchSingleton(ast::PatternMatchSingleton { + value: Singleton::False, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::String | TokenKind::FStringStart | TokenKind::TStringStart => { + let str = self.parse_strings(); + + Pattern::MatchValue(ast::PatternMatchValue { + value: Box::new(str), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::Complex => { + let (real, imag) = self.bump_complex(); + let range = self.node_range(start); + + Pattern::MatchValue(ast::PatternMatchValue { + value: Box::new(Expr::NumberLiteral(ast::ExprNumberLiteral { + value: Number::Complex { real, imag }, + range, + node_index: AtomicNodeIndex::NONE, + })), + range, + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::Int => { + let value = self.bump_int(); + let range = self.node_range(start); + + Pattern::MatchValue(ast::PatternMatchValue { + value: Box::new(Expr::NumberLiteral(ast::ExprNumberLiteral { + value: Number::Int(value), + range, + node_index: AtomicNodeIndex::NONE, + })), + range, + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::Float => { + let value = self.bump_float(); + let range = self.node_range(start); + + Pattern::MatchValue(ast::PatternMatchValue { + value: Box::new(Expr::NumberLiteral(ast::ExprNumberLiteral { + value: Number::Float(value), + range, + node_index: AtomicNodeIndex::NONE, + })), + range, + node_index: AtomicNodeIndex::NONE, + }) + } + kind => { + if let Some(unary_arithmetic_op) = kind.as_unary_arithmetic_operator() { + if matches!( + self.peek(), + TokenKind::Int | TokenKind::Float | TokenKind::Complex + ) { + let unary_expr = self.parse_unary_expression( + unary_arithmetic_op, + ExpressionContext::default(), + ); + + // test_err signed_pattern_non_literal_operand + // # parse_options: {"target-version": "3.15"} + // match value: + // case -1**2: ... + // case -1 .real: ... + // case -1[0]: ... + // case -1(): ... + // case {+1**2: _}: ... + + // Parse the full operand for error recovery, but only numeric literals + // are valid after a sign in a literal pattern. + if !unary_expr.operand.is_number_literal_expr() { + self.add_error( + ParseErrorType::OtherError( + "expected a numeric literal after unary operator".to_string(), + ), + unary_expr.operand.range(), + ); + } + + // test_ok unary_plus_py315 + // # parse_options: {"target-version": "3.15"} + // match foo: + // case +1: ... + // # this is also now valid inside more complicated patterns + // case {+1: 2}: ... + + // test_err unary_plus_py314 + // # parse_options: {"target-version": "3.14"} + // match foo: + // case +1: ... + // case {+1: 2}: ... + if unary_expr.op.is_u_add() { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnaryPlusMatchPattern, + unary_expr.range, + ); + } + + return Pattern::MatchValue(ast::PatternMatchValue { + value: Box::new(Expr::UnaryOp(unary_expr)), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }); + } + } + + if self.at_identifier_or_keyword() { + if self.peek() == TokenKind::Dot { + // test_ok match_attr_pattern_soft_keyword + // match foo: + // case match.bar: ... + // case case.bar: ... + // case type.bar: ... + // case match.case.type.bar.type.case.match: ... + let id = Expr::Name(self.parse_name(ExpressionContext::default())); + + let attribute = self.parse_attr_expr_for_match_pattern(id, start); + + Pattern::MatchValue(ast::PatternMatchValue { + value: Box::new(attribute), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } else { + // test_ok match_as_pattern_soft_keyword + // match foo: + // case case: ... + // match foo: + // case match: ... + // match foo: + // case type: ... + let ident = self.parse_identifier(); + + // test_ok match_as_pattern + // match foo: + // case foo_bar: ... + // match foo: + // case _: ... + Pattern::MatchAs(ast::PatternMatchAs { + range: ident.range, + pattern: None, + name: if &ident == "_" { None } else { Some(ident) }, + node_index: AtomicNodeIndex::NONE, + }) + } + } else { + // Upon encountering an unexpected token, return a `Pattern::MatchValue` containing + // an empty `Expr::Name`. + self.add_error( + ParseErrorType::OtherError("expected a pattern".to_string()), + self.current_token_range(), + ); + let invalid_node = Expr::Name(ast::ExprName { + range: self.missing_node_range(), + id: Name::empty(), + ctx: ExprContext::Invalid, + node_index: AtomicNodeIndex::NONE, + }); + Pattern::MatchValue(ast::PatternMatchValue { + range: invalid_node.range(), + value: Box::new(invalid_node), + node_index: AtomicNodeIndex::NONE, + }) + } + } + } + } + + /// Parses a complex literal pattern, given the `lhs` pattern and the `start` + /// position of the pattern. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `+` or `-` token. + /// + /// See: + fn parse_complex_literal_pattern( + &mut self, + lhs: Pattern, + start: TextSize, + ) -> ast::PatternMatchValue { + let operator = if self.eat(TokenKind::Plus) { + Operator::Add + } else { + self.bump(TokenKind::Minus); + Operator::Sub + }; + + let lhs_value = if let Pattern::MatchValue(lhs) = lhs { + if !is_real_number(&lhs.value) { + self.add_error(ParseErrorType::ExpectedRealNumber, &lhs); + } + lhs.value + } else { + self.add_error(ParseErrorType::ExpectedRealNumber, &lhs); + Box::new(recovery::pattern_to_expr(lhs)) + }; + + let rhs_pattern = self.parse_match_pattern_lhs(AllowStarPattern::No); + let rhs_value = if let Pattern::MatchValue(rhs) = rhs_pattern { + if !is_complex_number(&rhs.value) { + self.add_error(ParseErrorType::ExpectedImaginaryNumber, &rhs); + } + rhs.value + } else { + self.add_error(ParseErrorType::ExpectedImaginaryNumber, &rhs_pattern); + Box::new(recovery::pattern_to_expr(rhs_pattern)) + }; + + let range = self.node_range(start); + + ast::PatternMatchValue { + value: Box::new(Expr::BinOp(ast::ExprBinOp { + left: lhs_value, + op: operator, + right: rhs_value, + range, + node_index: AtomicNodeIndex::NONE, + })), + range, + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses an attribute expression until the current token is not a `.`. + fn parse_attr_expr_for_match_pattern(&mut self, mut lhs: Expr, start: TextSize) -> Expr { + while self.current_token_kind() == TokenKind::Dot { + lhs = Expr::Attribute(self.parse_attribute_expression( + lhs, + start, + ExpressionContext::default(), + )); + } + + lhs + } + + /// Parses the [pattern arguments] in a class pattern. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `(` token. + /// + /// See: + /// + /// [pattern arguments]: https://docs.python.org/3/reference/compound_stmts.html#grammar-token-python-grammar-pattern_arguments + fn parse_match_pattern_class( + &mut self, + cls: Pattern, + start: TextSize, + ) -> ast::PatternMatchClass { + let arguments_start = self.node_start(); + + let cls = match cls { + Pattern::MatchAs(ast::PatternMatchAs { + pattern: None, + name: Some(ident), + .. + }) => { + if ident.is_valid() { + Box::new(Expr::Name(ast::ExprName { + range: ident.range(), + id: ident.id, + ctx: ExprContext::Load, + node_index: AtomicNodeIndex::NONE, + })) + } else { + Box::new(Expr::Name(ast::ExprName { + range: ident.range(), + id: Name::empty(), + ctx: ExprContext::Invalid, + node_index: AtomicNodeIndex::NONE, + })) + } + } + Pattern::MatchValue(ast::PatternMatchValue { value, .. }) + if matches!(&*value, Expr::Attribute(_)) => + { + value + } + pattern => { + self.add_error( + ParseErrorType::OtherError("invalid value for a class pattern".to_string()), + &pattern, + ); + Box::new(recovery::pattern_to_expr(pattern)) + } + }; + + self.bump(TokenKind::Lpar); + + let mut patterns = Patterns::new(); + let mut keywords = vec![]; + let mut has_seen_pattern = false; + let mut has_seen_keyword_pattern = false; + + self.parse_comma_separated_list( + RecoveryContextKind::MatchPatternClassArguments, + |parser| { + let pattern_start = parser.node_start(); + let pattern = parser.parse_match_pattern(AllowStarPattern::No); + + if parser.eat(TokenKind::Equal) { + has_seen_pattern = false; + has_seen_keyword_pattern = true; + + let key = if let Pattern::MatchAs(ast::PatternMatchAs { + pattern: None, + name: Some(name), + .. + }) = pattern + { + name + } else { + parser.add_error( + ParseErrorType::OtherError("invalid syntax".to_string()), + &pattern, + ); + ast::Identifier { + id: Name::empty(), + range: parser.missing_node_range(), + node_index: AtomicNodeIndex::NONE, + } + }; + + let value_pattern = parser.parse_match_pattern(AllowStarPattern::No); + + keywords.push(ast::PatternKeyword { + attr: key, + pattern: value_pattern, + range: parser.node_range(pattern_start), + node_index: AtomicNodeIndex::NONE, + }); + } else { + has_seen_pattern = true; + patterns.push(pattern); + } + + if has_seen_keyword_pattern && has_seen_pattern { + parser.add_error( + ParseErrorType::OtherError( + "positional patterns follow keyword patterns".to_string(), + ), + parser.node_range(pattern_start), + ); + } + }, + ); + + self.expect(TokenKind::Rpar); + + ast::PatternMatchClass { + cls, + arguments: ast::PatternArguments { + patterns, + keywords, + range: self.node_range(arguments_start), + node_index: AtomicNodeIndex::NONE, + }, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_patterns: None, + runtime_kwd_attrs: None, + runtime_kwd_patterns: None, + } + } +} + +#[derive(Debug, Clone, Copy)] +enum AllowStarPattern { + Yes, + No, +} + +impl AllowStarPattern { + const fn is_no(self) -> bool { + matches!(self, AllowStarPattern::No) + } +} + +/// Returns `true` if the given expression is a real number literal or a unary +/// addition or subtraction of a real number literal. +const fn is_real_number(expr: &Expr) -> bool { + match expr { + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(_) | ast::Number::Float(_), + .. + }) => true, + Expr::UnaryOp(ast::ExprUnaryOp { + op: ast::UnaryOp::UAdd | ast::UnaryOp::USub, + operand, + .. + }) => is_real_number(operand), + _ => false, + } +} + +/// Returns `true` if the given expression is a complex number literal. +const fn is_complex_number(expr: &Expr) -> bool { + matches!( + expr, + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Complex { .. }, + .. + }) + ) +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/progress.rs b/vendor/rustpython-ruff_python_parser/src/parser/progress.rs new file mode 100644 index 00000000000..ad6f3884cbc --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/progress.rs @@ -0,0 +1,46 @@ +use crate::parser::Parser; + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub(super) struct TokenId(u32); + +impl TokenId { + /// Increments the value of the token ID. + pub(super) fn increment(&mut self) { + // It's fine to just wrap around because the main purpose is to check whether + // the previous token ID is different from the current token ID. + self.0 = self.0.wrapping_add(1); + } +} + +/// Captures the progress of the parser and allows to test if the parsing is still making progress +#[derive(Debug, Copy, Clone, Default)] +pub(super) struct ParserProgress(Option); + +impl ParserProgress { + /// Returns true if the parser has passed this position + #[inline] + fn has_progressed(self, p: &Parser) -> bool { + match self.0 { + None => true, + Some(prev_token_id) => prev_token_id != p.current_token_id(), + } + } + + /// Asserts that the parsing is still making progress. + /// + /// # Panics + /// + /// Panics if the parser hasn't progressed since the last call. + #[inline] + pub(super) fn assert_progressing(&mut self, p: &Parser) { + assert!( + self.has_progressed(p), + "The parser is no longer progressing. Stuck at '{}' {:?}:{:?}", + p.src_text(p.current_token_range()), + p.current_token_kind(), + p.current_token_range(), + ); + + self.0 = Some(p.current_token_id()); + } +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/recovery.rs b/vendor/rustpython-ruff_python_parser/src/parser/recovery.rs new file mode 100644 index 00000000000..0f9fc3edf03 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/recovery.rs @@ -0,0 +1,219 @@ +use ruff_python_ast::name::Name; +use ruff_python_ast::{self as ast, Expr, ExprContext, Pattern}; +use ruff_text_size::{Ranged, TextLen, TextRange}; + +/// Convert the given [`Pattern`] to an [`Expr`]. +/// +/// This is used to convert an invalid use of pattern to their equivalent expression +/// to preserve the structure of the pattern. +/// +/// The conversion is done as follows: +/// - `PatternMatchSingleton`: Boolean and None literals +/// - `PatternMatchValue`: The value itself +/// - `PatternMatchSequence`: List literal +/// - `PatternMatchMapping`: Dictionary literal +/// - `PatternMatchClass`: Call expression +/// - `PatternMatchStar`: Starred expression +/// - `PatternMatchAs`: The pattern itself or the name +/// - `PatternMatchOr`: Binary expression with `|` operator +/// +/// Note that the sequence pattern is always converted to a list literal even +/// if it was surrounded by parentheses. +/// +/// # Note +/// +/// This function returns an invalid [`ast::ExprName`] if the given pattern is a [`Pattern::MatchAs`] +/// with both the pattern and name present. This is because it cannot be converted to an expression +/// without dropping one of them as there's no way to represent `x as y` as a valid expression. +pub(super) fn pattern_to_expr(pattern: Pattern) -> Expr { + match pattern { + Pattern::MatchSingleton(ast::PatternMatchSingleton { + range, + node_index, + value, + }) => match value { + ast::Singleton::True => Expr::BooleanLiteral(ast::ExprBooleanLiteral { + value: true, + range, + node_index, + }), + ast::Singleton::False => Expr::BooleanLiteral(ast::ExprBooleanLiteral { + value: false, + range, + node_index, + }), + ast::Singleton::None => Expr::NoneLiteral(ast::ExprNoneLiteral { range, node_index }), + }, + Pattern::MatchValue(ast::PatternMatchValue { value, .. }) => *value, + // We don't know which kind of sequence this is: `case [1, 2]:` or `case (1, 2):`. + Pattern::MatchSequence(ast::PatternMatchSequence { + range, + node_index, + patterns, + .. + }) => Expr::List(ast::ExprList { + elts: patterns.into_iter().map(pattern_to_expr).collect(), + ctx: ExprContext::Store, + range, + node_index, + runtime_elts: None, + }), + Pattern::MatchMapping(ast::PatternMatchMapping { + range, + node_index, + keys, + patterns, + rest, + .. + }) => { + let mut items: Vec = keys + .into_iter() + .zip(patterns) + .map(|(key, pattern)| ast::DictItem { + key: Some(key), + value: pattern_to_expr(pattern), + }) + .collect(); + if let Some(rest) = rest { + let value = Expr::Name(ast::ExprName { + range: rest.range, + node_index: node_index.clone(), + id: rest.id, + ctx: ExprContext::Store, + }); + items.push(ast::DictItem { key: None, value }); + } + items.shrink_to_fit(); + + Expr::Dict(ast::ExprDict { + range, + node_index, + items, + runtime_values: None, + }) + } + Pattern::MatchClass(ast::PatternMatchClass { + range, + node_index, + cls, + arguments, + .. + }) => { + debug_assert_eq!(range.end(), arguments.end()); + + Expr::Call(ast::ExprCall { + range_start: range.start(), + node_index: node_index.clone(), + func: cls, + arguments: ast::Arguments { + range: arguments.range, + node_index: node_index.clone(), + args: arguments + .patterns + .into_iter() + .map(pattern_to_expr) + .collect(), + keywords: arguments + .keywords + .into_iter() + .map(|keyword_pattern| ast::Keyword { + range: keyword_pattern.range, + node_index: node_index.clone(), + arg: Some(keyword_pattern.attr), + value: pattern_to_expr(keyword_pattern.pattern), + }) + .collect(), + runtime_args: None, + runtime_bases: None, + }, + }) + } + Pattern::MatchStar(ast::PatternMatchStar { + range, + node_index, + name, + }) => { + if let Some(name) = name { + Expr::Starred(ast::ExprStarred { + range, + node_index: node_index.clone(), + value: Box::new(Expr::Name(ast::ExprName { + range: name.range, + node_index, + id: name.id, + ctx: ExprContext::Store, + })), + ctx: ExprContext::Store, + }) + } else { + Expr::Starred(ast::ExprStarred { + range, + node_index: node_index.clone(), + value: Box::new(Expr::Name(ast::ExprName { + range: TextRange::new(range.end() - "_".text_len(), range.end()), + id: Name::new_static("_"), + ctx: ExprContext::Store, + node_index, + })), + ctx: ExprContext::Store, + }) + } + } + Pattern::MatchAs(ast::PatternMatchAs { + range, + node_index, + pattern, + name, + }) => match (pattern, name) { + (Some(_), Some(_)) => Expr::Name(ast::ExprName { + range, + node_index, + id: Name::empty(), + ctx: ExprContext::Invalid, + }), + (Some(pattern), None) => pattern_to_expr(*pattern), + (None, Some(name)) => Expr::Name(ast::ExprName { + range: name.range, + node_index, + id: name.id, + ctx: ExprContext::Store, + }), + (None, None) => Expr::Name(ast::ExprName { + range, + node_index, + id: Name::new_static("_"), + ctx: ExprContext::Store, + }), + }, + Pattern::MatchOr(ast::PatternMatchOr { + patterns, + node_index, + .. + }) => { + let to_bin_expr = |left: Pattern, right: Pattern| ast::ExprBinOp { + range: TextRange::new(left.start(), right.end()), + left: Box::new(pattern_to_expr(left)), + op: ast::Operator::BitOr, + right: Box::new(pattern_to_expr(right)), + node_index: node_index.clone(), + }; + + let mut iter = patterns.into_iter(); + + match (iter.next(), iter.next()) { + (Some(left), Some(right)) => { + Expr::BinOp(iter.fold(to_bin_expr(left, right), |expr_bin_op, pattern| { + ast::ExprBinOp { + range: TextRange::new(expr_bin_op.start(), pattern.end()), + left: Box::new(Expr::BinOp(expr_bin_op)), + op: ast::Operator::BitOr, + right: Box::new(pattern_to_expr(pattern)), + node_index: node_index.clone(), + } + })) + } + _ => unreachable!("Or patterns can only be formed with at least two patterns."), + } + } + } +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/scratch_buffer.rs b/vendor/rustpython-ruff_python_parser/src/parser/scratch_buffer.rs new file mode 100644 index 00000000000..9f3acb015ea --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/scratch_buffer.rs @@ -0,0 +1,136 @@ +use std::vec::Drain; + +use drop_bomb::DebugDropBomb; +use thin_vec::ThinVec; + +/// Reusable scratch storage that preserves entries belonging to outer parser frames. +#[derive(Debug)] +pub(super) struct ScratchBuffer { + buffer: Vec, +} + +impl ScratchBuffer { + pub(super) fn new() -> Self { + Self { buffer: Vec::new() } + } + + pub(super) fn with_capacity(capacity: usize) -> Self { + Self { + buffer: Vec::with_capacity(capacity), + } + } + + #[inline] + pub(super) fn push(&mut self, value: T) { + self.buffer.push(value); + } + + #[inline] + pub(super) fn is_empty(&self, snapshot: &ScratchSnapshot) -> bool { + debug_assert!( + self.buffer.len() >= snapshot.len, + "Scratch buffer snapshots must be restored in reverse order of creation." + ); + self.buffer.len() == snapshot.len + } + + #[inline] + pub(super) fn snapshot(&self) -> ScratchSnapshot { + ScratchSnapshot { + len: self.buffer.len(), + bomb: DebugDropBomb::new("Scratch buffer snapshots must be restored."), + } + } + + #[inline] + pub(super) fn take>(&mut self, snapshot: ScratchSnapshot) -> C { + self.drain_snapshot(snapshot).collect() + } + + #[inline] + pub(super) fn take_thin_vec(&mut self, mut snapshot: ScratchSnapshot) -> ThinVec { + if self.is_empty(&snapshot) { + snapshot.bomb.defuse(); + return ThinVec::new(); + } + + let drain = self.drain_snapshot(snapshot); + let mut result = ThinVec::with_capacity(drain.len()); + result.extend(drain); + result + } + + #[inline] + fn drain_snapshot(&mut self, mut snapshot: ScratchSnapshot) -> Drain<'_, T> { + debug_assert!( + self.buffer.len() >= snapshot.len, + "Scratch buffer snapshots must be restored in reverse order of creation." + ); + snapshot.bomb.defuse(); + self.buffer.drain(snapshot.len..) + } +} + +impl Drop for ScratchBuffer { + fn drop(&mut self) { + debug_assert!( + self.buffer.is_empty() || std::thread::panicking(), + "Scratch buffers must be empty when dropped." + ); + } +} + +pub(super) struct ScratchSnapshot { + len: usize, + bomb: DebugDropBomb, +} + +#[cfg(test)] +mod tests { + use super::ScratchBuffer; + + #[test] + #[cfg(debug_assertions)] + #[should_panic(expected = "Scratch buffers must be empty when dropped.")] + fn buffer_must_be_empty_when_dropped() { + let mut buffer = ScratchBuffer::new(); + buffer.push(1); + } + + #[test] + #[cfg(debug_assertions)] + #[should_panic(expected = "Scratch buffer snapshots must be restored.")] + fn snapshot_must_be_restored() { + let buffer = ScratchBuffer::::new(); + let _snapshot = buffer.snapshot(); + } + + #[test] + #[cfg(debug_assertions)] + #[should_panic( + expected = "Scratch buffer snapshots must be restored in reverse order of creation." + )] + fn snapshot_must_be_restored_in_reverse_order() { + let mut buffer = ScratchBuffer::new(); + buffer.push(1); + let snapshot = buffer.snapshot(); + buffer.buffer.clear(); + + let _: Vec<_> = buffer.take(snapshot); + } + + #[test] + fn snapshot_is_empty_relative_to_its_buffer() { + let mut buffer = ScratchBuffer::new(); + let outer_snapshot = buffer.snapshot(); + buffer.push(1); + let snapshot = buffer.snapshot(); + assert!(buffer.is_empty(&snapshot)); + + buffer.push(2); + assert!(!buffer.is_empty(&snapshot)); + + let _: Vec<_> = buffer.take(snapshot); + let _: Vec<_> = buffer.take(outer_snapshot); + } +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax1.snap b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax1.snap new file mode 100644 index 00000000000..a3b74e9fadb --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax1.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_python_parser/src/parser/tests.rs +expression: error +--- +ParseError { + error: UnexpectedExpressionToken, + location: 6..12, +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax2.snap b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax2.snap new file mode 100644 index 00000000000..ba26114dd84 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax2.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_python_parser/src/parser/tests.rs +expression: error +--- +ParseError { + error: UnexpectedExpressionToken, + location: 7..13, +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax3.snap b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax3.snap new file mode 100644 index 00000000000..ba26114dd84 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_invalid_syntax3.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_python_parser/src/parser/tests.rs +expression: error +--- +ParseError { + error: UnexpectedExpressionToken, + location: 7..13, +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_valid_syntax.snap b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_valid_syntax.snap new file mode 100644 index 00000000000..714127298bf --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_valid_syntax.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_python_parser/src/parser/tests.rs +expression: parsed.expr() +--- +Name( + ExprName { + node_index: NodeIndex(None), + range: 0..5, + id: Name("first"), + ctx: Load, + }, +) diff --git a/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__fstring_expr_inner_line_continuation_and_t_string.snap b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__fstring_expr_inner_line_continuation_and_t_string.snap new file mode 100644 index 00000000000..3bf63899e39 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__fstring_expr_inner_line_continuation_and_t_string.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_parser/src/parser/tests.rs +expression: error +--- +ParseError { + error: Lexical( + LineContinuationError, + ), + location: 4..4, +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__fstring_expr_inner_line_continuation_newline_t_string.snap b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__fstring_expr_inner_line_continuation_newline_t_string.snap new file mode 100644 index 00000000000..36cd0363825 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__fstring_expr_inner_line_continuation_newline_t_string.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_python_parser/src/parser/tests.rs +expression: error +--- +ParseError { + error: Lexical( + TStringError( + SingleRbrace, + ), + ), + location: 8..9, +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__ipython_escape_commands.snap b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__ipython_escape_commands.snap new file mode 100644 index 00000000000..903a0c5236b --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__ipython_escape_commands.snap @@ -0,0 +1,532 @@ +--- +source: crates/ruff_python_parser/src/parser/tests.rs +expression: parsed.syntax() +--- +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..953, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 21..42, + value: BinOp( + ExprBinOp { + node_index: NodeIndex(None), + range: 27..40, + left: Name( + ExprName { + node_index: NodeIndex(None), + range: 27..28, + id: Name("a"), + ctx: Load, + }, + ), + op: Mod, + right: Name( + ExprName { + node_index: NodeIndex(None), + range: 39..40, + id: Name("b"), + ctx: Load, + }, + ), + }, + ), + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 66..73, + kind: Help2, + value: "a.foo", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 74..80, + kind: Help, + value: "a.foo", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 81..88, + kind: Help, + value: "a.foo", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 89..100, + kind: Help2, + value: "a.foo()", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 115..128, + kind: Magic, + value: "timeit a = b", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 129..147, + kind: Magic, + value: "timeit foo(b) % 3", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 148..176, + kind: Magic, + value: "alias showPath pwd && ls -a", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 177..205, + kind: Magic, + value: "timeit a = foo(b); b = 2", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 206..226, + kind: Magic, + value: "matplotlib --inline", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 227..253, + kind: Magic, + value: "matplotlib --inline", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 277..309, + kind: Shell, + value: "pwd && ls -a | sed 's/^/\\ /'", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 310..347, + kind: Shell, + value: "pwd && ls -a | sed 's/^/\\\\ /'", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 348..393, + kind: ShCap, + value: "cd /Users/foo/Library/Application\\ Support/", + }, + ), + FunctionDef( + StmtFunctionDef { + node_index: NodeIndex(None), + range: 566..626, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("foo"), + range: 570..573, + node_index: NodeIndex(None), + }, + type_params: None, + parameters: Parameters { + range: 573..575, + node_index: NodeIndex(None), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + runtime_defaults: None, + }, + returns: None, + body: [ + Return( + StmtReturn { + node_index: NodeIndex(None), + range: 581..626, + value: Some( + Compare( + ExprCompare { + node_index: NodeIndex(None), + range: 598..620, + ops: [ + NotEq, + ], + operands: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 598..599, + id: Name("a"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: NodeIndex(None), + range: 619..620, + id: Name("b"), + ctx: Load, + }, + ), + ], + runtime_comparators: None, + }, + ), + ), + }, + ), + ], + runtime_decorator_list: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + runtime_body: None, + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 656..664, + kind: Paren, + value: "foo 1 2", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 665..673, + kind: Quote2, + value: "foo 1 2", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 674..682, + kind: Quote, + value: "foo 1 2", + }, + ), + For( + StmtFor { + node_index: NodeIndex(None), + range: 711..737, + is_async: false, + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 715..716, + id: Name("a"), + ctx: Store, + }, + ), + iter: Call( + ExprCall { + node_index: NodeIndex(None), + range: 720..728, + func: Name( + ExprName { + node_index: NodeIndex(None), + range: 720..725, + id: Name("range"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 725..728, + node_index: NodeIndex(None), + args: [ + NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 726..727, + value: Int( + 5, + ), + }, + ), + ], + keywords: [], + runtime_args: None, + runtime_bases: None, + }, + }, + ), + body: [ + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 734..737, + kind: Shell, + value: "ls", + }, + ), + ], + orelse: [], + runtime_type_comment: None, + runtime_type_comment_bytes: None, + runtime_body: None, + runtime_orelse: None, + }, + ), + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 739..748, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 739..741, + id: Name("p1"), + ctx: Store, + }, + ), + ], + value: IpyEscapeCommand( + ExprIpyEscapeCommand { + node_index: NodeIndex(None), + range: 744..748, + kind: Shell, + value: "pwd", + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), + AnnAssign( + StmtAnnAssign { + node_index: NodeIndex(None), + range: 749..763, + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 749..751, + id: Name("p2"), + ctx: Store, + }, + ), + annotation: Name( + ExprName { + node_index: NodeIndex(None), + range: 753..756, + id: Name("str"), + ctx: Load, + }, + ), + value: Some( + IpyEscapeCommand( + ExprIpyEscapeCommand { + node_index: NodeIndex(None), + range: 759..763, + kind: Shell, + value: "pwd", + }, + ), + ), + simple: true, + runtime_simple: None, + }, + ), + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 764..784, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 764..767, + id: Name("foo"), + ctx: Store, + }, + ), + ], + value: IpyEscapeCommand( + ExprIpyEscapeCommand { + node_index: NodeIndex(None), + range: 770..784, + kind: Magic, + value: "foo bar", + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 785..796, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 785..788, + id: Name("bar"), + ctx: Store, + }, + ), + ], + value: IpyEscapeCommand( + ExprIpyEscapeCommand { + node_index: NodeIndex(None), + range: 791..796, + kind: Magic, + value: "foo?", + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 797..808, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 797..800, + id: Name("baz"), + ctx: Store, + }, + ), + ], + value: IpyEscapeCommand( + ExprIpyEscapeCommand { + node_index: NodeIndex(None), + range: 803..808, + kind: Shell, + value: "pwd?", + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 810..815, + kind: Magic, + value: " foo", + }, + ), + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 816..837, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 816..819, + id: Name("foo"), + ctx: Store, + }, + ), + ], + value: IpyEscapeCommand( + ExprIpyEscapeCommand { + node_index: NodeIndex(None), + range: 822..837, + kind: Magic, + value: "foo # comment", + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 862..866, + kind: Help, + value: "foo", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 867..876, + kind: Help2, + value: "foo.bar", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 877..889, + kind: Help, + value: "foo.bar.baz", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 890..898, + kind: Help2, + value: "foo[0]", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 899..909, + kind: Help, + value: "foo[0][1]", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 910..929, + kind: Help2, + value: "foo.bar[0].baz[1]", + }, + ), + IpyEscapeCommand( + StmtIpyEscapeCommand { + node_index: NodeIndex(None), + range: 930..953, + kind: Help2, + value: "foo.bar[0].baz[2].egg", + }, + ), + ], + runtime_body: None, + }, +) diff --git a/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle.snap b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle.snap new file mode 100644 index 00000000000..45027e17661 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_python_parser/src/parser/tests.rs +expression: error +--- +ParseError { + error: TStringError( + ExpressionRequiredBefore( + ':', + ), + ), + location: 3..4, +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle_fuzzer.snap b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle_fuzzer.snap new file mode 100644 index 00000000000..e26bcbc33e9 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle_fuzzer.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_python_parser/src/parser/tests.rs +expression: error +--- +ParseError { + error: Lexical( + UnrecognizedToken { + tok: '\0', + }, + ), + location: 5..6, +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__unicode_aliases.snap b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__unicode_aliases.snap new file mode 100644 index 00000000000..5824cc5f4fc --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__unicode_aliases.snap @@ -0,0 +1,46 @@ +--- +source: crates/ruff_python_parser/src/parser/tests.rs +expression: suite +--- +[ + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 0..37, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 0..1, + id: Name("x"), + ctx: Store, + }, + ), + ], + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 4..37, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 4..37, + node_index: NodeIndex(None), + value: "\u{8}another cool trick", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/parser/statement.rs b/vendor/rustpython-ruff_python_parser/src/parser/statement.rs new file mode 100644 index 00000000000..0e00e79ef18 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/statement.rs @@ -0,0 +1,4921 @@ +use std::fmt::Write; + +use ruff_python_ast::name::Name; +use ruff_python_ast::token::TokenKind; +use ruff_python_ast::{ + self as ast, AtomicNodeIndex, DecoratorList, ExceptHandler, Expr, ExprContext, IpyEscapeKind, + Operator, PythonVersion, Stmt, Suite, UnaryOp, WithItem, +}; +use ruff_text_size::{Ranged, TextRange, TextSize}; + +use crate::error::StarTupleKind; +use crate::parser::expression::{ArgumentsContext, EXPR_SET, ParsedExpr}; +use crate::parser::progress::ParserProgress; +use crate::parser::{ + FunctionKind, IpyEscapeContext, Parser, RecoveryContext, RecoveryContextKind, WithItemKind, + helpers, +}; +use crate::token_set::TokenSet; +use crate::{BlockClause, ExpressionKind, Mode, ParseErrorType, UnsupportedSyntaxErrorKind}; + +use super::Parenthesized; +use super::expression::ExpressionContext; + +/// Tokens that represent compound statements. +const COMPOUND_STMT_SET: TokenSet = TokenSet::new([ + TokenKind::Match, + TokenKind::If, + TokenKind::With, + TokenKind::While, + TokenKind::For, + TokenKind::Try, + TokenKind::Def, + TokenKind::Class, + TokenKind::Async, + TokenKind::At, +]); + +/// Tokens that represent simple statements, but doesn't include expressions. +const SIMPLE_STMT_SET: TokenSet = TokenSet::new([ + TokenKind::Pass, + TokenKind::Return, + TokenKind::Break, + TokenKind::Continue, + TokenKind::Global, + TokenKind::Nonlocal, + TokenKind::Assert, + TokenKind::Yield, + TokenKind::Del, + TokenKind::Raise, + TokenKind::Import, + TokenKind::From, + TokenKind::Type, + TokenKind::IpyEscapeCommand, +]); + +/// Tokens that represent simple statements, including expressions. +const SIMPLE_STMT_WITH_EXPR_SET: TokenSet = SIMPLE_STMT_SET.union(EXPR_SET); + +/// Tokens that represents all possible statements, including simple, compound, +/// and expression statements. +const STMTS_SET: TokenSet = SIMPLE_STMT_WITH_EXPR_SET.union(COMPOUND_STMT_SET); + +/// Tokens that represent operators that can be used in augmented assignments. +const AUGMENTED_ASSIGN_SET: TokenSet = TokenSet::new([ + TokenKind::PlusEqual, + TokenKind::MinusEqual, + TokenKind::StarEqual, + TokenKind::DoubleStarEqual, + TokenKind::SlashEqual, + TokenKind::DoubleSlashEqual, + TokenKind::PercentEqual, + TokenKind::AtEqual, + TokenKind::AmperEqual, + TokenKind::VbarEqual, + TokenKind::CircumflexEqual, + TokenKind::LeftShiftEqual, + TokenKind::RightShiftEqual, +]); + +impl<'src> Parser<'src> { + /// Returns `true` if the current token is the start of a compound statement. + pub(super) fn at_compound_stmt(&self) -> bool { + self.at_ts(COMPOUND_STMT_SET) + } + + /// Returns `true` if the current token is the start of a simple statement, + /// including expressions. + fn at_simple_stmt(&self) -> bool { + self.at_ts(SIMPLE_STMT_WITH_EXPR_SET) || self.at_soft_keyword() + } + + /// Returns `true` if the current token is the start of a simple, compound or expression + /// statement. + pub(super) fn at_stmt(&self) -> bool { + self.at_ts(STMTS_SET) || self.at_soft_keyword() + } + + /// Checks if the parser is currently positioned at the start of a type parameter. + pub(super) fn at_type_param(&self) -> bool { + let token = self.current_token_kind(); + matches!( + token, + TokenKind::Star | TokenKind::DoubleStar | TokenKind::Identifier + ) || token.is_keyword() + } + + /// Parses a compound or a single simple statement. + /// + /// See: + /// - + /// - + pub(super) fn parse_statement(&mut self) -> Stmt { + let start = self.node_start(); + #[cfg(feature = "python315-diagnostics")] + let case_diagnostic = self.standalone_case_diagnostic(); + + let statement = match self.current_token_kind() { + TokenKind::If => Stmt::If(self.parse_if_statement()), + TokenKind::For => Stmt::For(self.parse_for_statement(start)), + TokenKind::While => Stmt::While(self.parse_while_statement()), + TokenKind::Def => { + Stmt::FunctionDef(self.parse_function_definition(DecoratorList::new(), start)) + } + TokenKind::Class => { + Stmt::ClassDef(self.parse_class_definition(DecoratorList::new(), start)) + } + TokenKind::Try => Stmt::Try(self.parse_try_statement()), + TokenKind::With => Stmt::With(self.parse_with_statement(start)), + TokenKind::At => self.parse_decorators(), + TokenKind::Async => self.parse_async_statement(), + token => { + if token == TokenKind::Match { + // Match is considered a soft keyword, so we will treat it as an identifier if + // it's followed by an unexpected token. + + match self.classify_match_token() { + MatchTokenKind::Keyword => { + return Stmt::Match(self.parse_match_statement()); + } + MatchTokenKind::KeywordOrIdentifier => { + if let Some(match_stmt) = self.try_parse_match_statement() { + return Stmt::Match(match_stmt); + } + } + MatchTokenKind::Identifier => {} + } + } + + self.parse_single_simple_statement() + } + }; + + #[cfg(feature = "python315-diagnostics")] + if !self.errors.is_empty() + && let Some(error) = case_diagnostic + { + // Only the invalid-grammar diagnostic changes. Keep the ordinary + // statement parser's AST, tokens, and recovery path intact. + self.errors.insert(0, error); + } + + statement + } + + /// Parses a single simple statement. + /// + /// This statement must be terminated by a newline or semicolon. + /// + /// Use [`Parser::parse_simple_statements`] to parse a sequence of simple statements. + fn parse_single_simple_statement(&mut self) -> Stmt { + let stmt = self.parse_simple_statement(); + + // The order of the token is important here. + let has_eaten_semicolon = self.eat(TokenKind::Semi); + let has_eaten_newline = self.eat(TokenKind::Newline); + + if !has_eaten_newline { + if !has_eaten_semicolon && self.at_simple_stmt() { + // test_err simple_stmts_on_same_line + // a b + // a + b c + d + // break; continue pass; continue break + self.add_error( + ParseErrorType::SimpleStatementsOnSameLine, + self.current_token_range(), + ); + } else if self.at_compound_stmt() { + // test_err simple_and_compound_stmt_on_same_line + // a; if b: pass; b + self.add_error( + ParseErrorType::SimpleAndCompoundStatementOnSameLine, + self.current_token_range(), + ); + } + } + + stmt + } + + /// Parses a sequence of simple statements. + /// + /// If there is more than one statement in this sequence, it is expected to be separated by a + /// semicolon. The sequence can optionally end with a semicolon, but regardless of whether + /// a semicolon is present or not, it is expected to end with a newline. + /// + /// Matches the `simple_stmts` rule in the [Python grammar]. + /// + /// [Python grammar]: https://docs.python.org/3/reference/grammar.html + fn parse_simple_statements(&mut self) -> Suite { + let stmts_snapshot = self.stmt_scratch.snapshot(); + let mut progress = ParserProgress::default(); + + loop { + progress.assert_progressing(self); + + let stmt = self.parse_simple_statement(); + self.stmt_scratch.push(stmt); + + if !self.eat(TokenKind::Semi) { + if self.at_simple_stmt() { + // test_err simple_stmts_on_same_line_in_block + // if True: break; continue pass; continue break + self.add_error( + ParseErrorType::SimpleStatementsOnSameLine, + self.current_token_range(), + ); + } else { + // test_ok simple_stmts_in_block + // if True: pass + // if True: pass; + // if True: pass; continue + // if True: pass; continue; + // x = 1 + break; + } + } + + if !self.at_simple_stmt() { + break; + } + } + + // Ideally, we should use `expect` here but we use `eat` for better error message. Later, + // if the parser isn't at the start of a compound statement, we'd `expect` a newline. + if !self.eat(TokenKind::Newline) { + if self.at_compound_stmt() { + // test_err simple_and_compound_stmt_on_same_line_in_block + // if True: pass if False: pass + // if True: pass; if False: pass + self.add_error( + ParseErrorType::SimpleAndCompoundStatementOnSameLine, + self.current_token_range(), + ); + } else { + // test_err multiple_clauses_on_same_line + // if True: pass elif False: pass else: pass + // if True: pass; elif False: pass; else: pass + // for x in iter: break else: pass + // for x in iter: break; else: pass + // try: pass except exc: pass else: pass finally: pass + // try: pass; except exc: pass; else: pass; finally: pass + self.add_error( + ParseErrorType::ExpectedToken { + found: self.current_token_kind(), + expected: TokenKind::Newline, + }, + self.current_token_range(), + ); + } + } + + // test_ok simple_stmts_with_semicolons + // return; import a; from x import y; z; type T = int + self.stmt_scratch.take_thin_vec(stmts_snapshot) + } + + /// Parses a simple statement. + /// + /// See: + fn parse_simple_statement(&mut self) -> Stmt { + if matches!( + self.current_token_kind(), + TokenKind::Pass | TokenKind::Break | TokenKind::Continue + ) { + self.check_statement_before_if_expression(); + } + match self.current_token_kind() { + TokenKind::Return => Stmt::Return(self.parse_return_statement()), + TokenKind::Import => { + let start = self.node_start(); + Stmt::Import(self.parse_import_statement(start, false)) + } + TokenKind::From => { + let start = self.node_start(); + Stmt::ImportFrom(self.parse_from_import_statement(start, false)) + } + TokenKind::Pass => Stmt::Pass(self.parse_pass_statement()), + TokenKind::Continue => Stmt::Continue(self.parse_continue_statement()), + TokenKind::Break => Stmt::Break(self.parse_break_statement()), + TokenKind::Raise => Stmt::Raise(self.parse_raise_statement()), + TokenKind::Del => Stmt::Delete(self.parse_delete_statement()), + TokenKind::Assert => Stmt::Assert(self.parse_assert_statement()), + TokenKind::Global => Stmt::Global(self.parse_global_statement()), + TokenKind::Nonlocal => Stmt::Nonlocal(self.parse_nonlocal_statement()), + TokenKind::IpyEscapeCommand => { + Stmt::IpyEscapeCommand(self.parse_ipython_escape_command_statement()) + } + token => { + if token == TokenKind::Lazy { + let start = self.node_start(); + let lazy_range = self.current_token_range(); + + match self.peek() { + // test_ok lazy_import_stmt_py315 + // # parse_options: {"target-version": "3.15"} + // lazy import foo + // lazy import foo as bar + // lazy from bar import baz + // lazy from sys import x as y + // lazy = 1 + // import foo as lazy + // from lazy import qux + + // test_ok lazy_import_relative_py315 + // # parse_options: {"target-version": "3.15"} + // lazy from . import basic2 + // lazy from .basic2 import x, f + // lazy from . import b, x + + // test_ok lazy_import_soft_keyword_split_py315 + // # parse_options: {"target-version": "3.15"} + // lazy + // import os + // + // lazy # comment + // from sys import path + + // test_err lazy_import_stmt_py314 + // # parse_options: {"target-version": "3.14"} + // lazy import foo + // lazy from bar import baz + TokenKind::Import => { + self.bump(TokenKind::Lazy); + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::LazyImportStatement, + lazy_range, + ); + return Stmt::Import(self.parse_import_statement(start, true)); + } + TokenKind::From => { + self.bump(TokenKind::Lazy); + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::LazyImportStatement, + lazy_range, + ); + return Stmt::ImportFrom(self.parse_from_import_statement(start, true)); + } + _ => {} + } + } + + if token == TokenKind::Type { + // Type is considered a soft keyword, so we will treat it as an identifier if + // it's followed by an unexpected token. + let (first, second) = self.peek2(); + + if (first == TokenKind::Identifier || first.is_soft_keyword()) + && matches!(second, TokenKind::Lsqb | TokenKind::Equal) + { + return Stmt::TypeAlias(self.parse_type_alias_statement()); + } + } + + let start = self.node_start(); + + // test_err yield_after_comma + // def f(): 1, yield 1 + + // test_ok yield_after_comma_parenthesized + // def f(): 1, (yield 1) + + // simple_stmt: `... | yield_stmt | star_expressions | ...` + let parsed_expr = + self.parse_expression_list(ExpressionContext::yield_or_starred_bitwise_or()); + + if self.at(TokenKind::ColonEqual) { + // test_err named_expr_statement_invalid_target + // x + y := 1 + // a.b := 1 + let target = match &parsed_expr.expr { + Expr::Tuple(tuple) if !parsed_expr.is_parenthesized => tuple.elts.last(), + expr => Some(expr), + }; + if let Some(target) = target.filter(|target| { + !matches!( + target, + Expr::Name(_) | Expr::Starred(_) | Expr::Yield(_) | Expr::YieldFrom(_) + ) + }) { + self.add_error( + ParseErrorType::InvalidNamedAssignmentTarget(ExpressionKind::of( + target, + )), + target.range(), + ); + } + } + + if self.at(TokenKind::Equal) { + Stmt::Assign(self.parse_assign_statement(parsed_expr, start)) + } else if self.at(TokenKind::Colon) { + Stmt::AnnAssign(self.parse_annotated_assignment_statement(parsed_expr, start)) + } else if let Some(op) = self.current_token_kind().as_augmented_assign_operator() { + Stmt::AugAssign(self.parse_augmented_assignment_statement( + parsed_expr, + op, + start, + )) + } else if self.options.mode == Mode::Ipython && self.at(TokenKind::Question) { + Stmt::IpyEscapeCommand( + self.parse_ipython_help_end_escape_command_statement(&parsed_expr), + ) + } else { + Stmt::Expr(ast::StmtExpr { + range: self.node_range(start), + value: Box::new(parsed_expr.expr), + node_index: AtomicNodeIndex::NONE, + }) + } + } + } + } + + /// Parses a delete statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `del` token. + /// + /// See: + fn parse_delete_statement(&mut self) -> ast::StmtDelete { + let start = self.node_start(); + self.bump(TokenKind::Del); + + // test_err del_incomplete_target + // del x, y. + // z + // del x, y[ + // z + let targets = self.parse_comma_separated_list_into_vec( + RecoveryContextKind::DeleteTargets, + |parser| { + // Allow starred expression to raise a better error message for + // an invalid delete target later. + let mut target = parser.parse_conditional_expression_or_higher_impl( + ExpressionContext::starred_conditional(), + ); + helpers::set_expr_ctx(&mut target.expr, ExprContext::Del); + + // test_err invalid_del_target + // del x + 1 + // del {'x': 1} + // del {'x', 'y'} + // del None, True, False, 1, 1.0, "abc" + parser.validate_delete_target(&target.expr); + + target.expr + }, + ); + + if targets.is_empty() { + // test_err del_stmt_empty + // del + self.add_error( + ParseErrorType::EmptyDeleteTargets, + self.current_token_range(), + ); + } + + ast::StmtDelete { + targets, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_targets: None, + } + } + + /// Parses a `return` statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `return` token. + /// + /// See: + fn parse_return_statement(&mut self) -> ast::StmtReturn { + let start = self.node_start(); + self.bump(TokenKind::Return); + + // test_err return_stmt_invalid_expr + // return * + // return yield x + // return yield from x + // return x := 1 + // return *x and y + let value = self.at_expr().then(|| { + let parsed_expr = self.parse_expression_list(ExpressionContext::starred_bitwise_or()); + + // test_ok iter_unpack_return_py37 + // # parse_options: {"target-version": "3.7"} + // rest = (4, 5, 6) + // def f(): return (1, 2, 3, *rest) + + // test_ok iter_unpack_return_py38 + // # parse_options: {"target-version": "3.8"} + // rest = (4, 5, 6) + // def f(): return 1, 2, 3, *rest + + // test_err iter_unpack_return_py37 + // # parse_options: {"target-version": "3.7"} + // rest = (4, 5, 6) + // def f(): return 1, 2, 3, *rest + self.check_tuple_unpacking( + &parsed_expr, + UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Return), + ); + + Box::new(parsed_expr.expr) + }); + + ast::StmtReturn { + range: self.node_range(start), + value, + node_index: AtomicNodeIndex::NONE, + } + } + + /// Report [`UnsupportedSyntaxError`]s for each starred element in `expr` if it is an + /// unparenthesized tuple. + /// + /// This method can be used to check for tuple unpacking in `return`, `yield`, and `for` + /// statements, which are only allowed after [Python 3.8] and [Python 3.9], respectively. + /// + /// [Python 3.8]: https://github.com/python/cpython/issues/76298 + /// [Python 3.9]: https://github.com/python/cpython/issues/90881 + pub(super) fn check_tuple_unpacking(&mut self, expr: &Expr, kind: UnsupportedSyntaxErrorKind) { + if kind.is_supported(self.options.target_version) { + return; + } + + let Expr::Tuple(ast::ExprTuple { + elts, + parenthesized: false, + .. + }) = expr + else { + return; + }; + + for elt in elts { + if elt.is_starred_expr() { + self.add_unsupported_syntax_error(kind, elt.range()); + } + } + } + + /// Parses a `raise` statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `raise` token. + /// + /// See: + fn parse_raise_statement(&mut self) -> ast::StmtRaise { + let start = self.node_start(); + self.bump(TokenKind::Raise); + + let exc = match self.current_token_kind() { + TokenKind::Newline => None, + TokenKind::From => { + // test_err raise_stmt_from_without_exc + // raise from exc + // raise from None + if cfg!(feature = "python315-diagnostics") { + self.add_error( + ParseErrorType::MissingRaiseException, + TextRange::new(start, self.current_token_range().end()), + ); + } else { + self.add_error( + ParseErrorType::OtherError( + "exception missing in `raise` statement with cause".to_string(), + ), + self.current_token_range(), + ); + } + None + } + _ => { + // test_err raise_stmt_invalid_exc + // raise *x + // raise yield x + // raise x := 1 + let exc = self.parse_expression_list(ExpressionContext::default()); + + if let Some(ast::ExprTuple { + parenthesized: false, + .. + }) = exc.as_tuple_expr() + { + // test_err raise_stmt_unparenthesized_tuple_exc + // raise x, + // raise x, y + // raise x, y from z + self.add_error(ParseErrorType::UnparenthesizedTupleExpression, &exc); + } + + Some(Box::new(exc.expr)) + } + }; + + let from_range = self.current_token_range(); + let cause = self.eat(TokenKind::From).then(|| { + if cfg!(feature = "python315-diagnostics") + && exc.is_some() + && matches!( + self.current_token_kind(), + TokenKind::Newline | TokenKind::EndOfFile | TokenKind::Semi + ) + { + self.add_error(ParseErrorType::MissingRaiseCause, from_range); + } + // test_err raise_stmt_invalid_cause + // raise x from *y + // raise x from yield y + // raise x from y := 1 + let cause = self.parse_expression_list(ExpressionContext::default()); + + if let Some(ast::ExprTuple { + parenthesized: false, + .. + }) = cause.as_tuple_expr() + { + // test_err raise_stmt_unparenthesized_tuple_cause + // raise x from y, + // raise x from y, z + self.add_error(ParseErrorType::UnparenthesizedTupleExpression, &cause); + } + + Box::new(cause.expr) + }); + + ast::StmtRaise { + range: self.node_range(start), + exc, + cause, + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses an import statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at an `import` token. + /// + /// See: + fn parse_import_statement(&mut self, start: TextSize, is_lazy: bool) -> ast::StmtImport { + self.bump(TokenKind::Import); + + // test_err import_stmt_parenthesized_names + // import (a) + // import (a, b) + + // test_err import_stmt_star_import + // import * + // import x, *, y + + // test_err import_stmt_trailing_comma + // import , + // import x, y, + + let names_snapshot = self.alias_scratch.snapshot(); + self.parse_comma_separated_list(RecoveryContextKind::ImportNames, |parser| { + let alias = parser.parse_alias(ImportStyle::Import); + parser.alias_scratch.push(alias); + }); + let names: Vec<_> = self.alias_scratch.take(names_snapshot); + + if self.at(TokenKind::From) + && !names.is_empty() + && names.iter().all(|alias| alias.asname.is_none()) + && let Some(end) = self.misplaced_from_error_end() + { + // test_err import_stmt_from_after_names + // import a from b + // import a.b, c from d.e as f + self.add_error( + ParseErrorType::OtherError( + "Did you mean to use 'from ... import ...' instead?".to_string(), + ), + TextRange::new(start, end), + ); + } + + if names.is_empty() { + // test_err import_stmt_empty + // import + self.add_empty_import_names_error(); + } + + ast::StmtImport { + names, + is_lazy, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Returns where the error for `from` after the names of an `import` statement ends, if the + /// `from` is followed by a dotted name. The error ends one character before the end of the + /// token after the dotted name. + fn misplaced_from_error_end(&mut self) -> Option { + let checkpoint = self.checkpoint(); + self.bump(TokenKind::From); + let mut found_name = false; + while self.at_identifier_or_soft_keyword() { + found_name = true; + self.bump_any(); + if !self.eat(TokenKind::Dot) { + break; + } + } + let next = self.current_token_range(); + let end = found_name.then(|| next.start().max(next.end() - TextSize::from(1))); + self.rewind(checkpoint); + end + } + + /// Parses a `from` import statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `from` token. + /// + /// See: + fn parse_from_import_statement( + &mut self, + start: TextSize, + is_lazy: bool, + ) -> ast::StmtImportFrom { + let import_errors_start = self.errors.len(); + self.bump(TokenKind::From); + + let mut leading_dots = 0; + let mut progress = ParserProgress::default(); + + loop { + progress.assert_progressing(self); + + if self.eat(TokenKind::Dot) { + leading_dots += 1; + } else if self.eat(TokenKind::Ellipsis) { + leading_dots += 3; + } else { + break; + } + } + + let module = if self.at_identifier_or_soft_keyword() { + // test_ok from_import_soft_keyword_module_name + // from match import pattern + // from type import bar + // from case import pattern + // from lazy import qux + // from match.type.case import foo + Some(self.parse_dotted_name()) + } else { + if leading_dots == 0 { + // test_err from_import_missing_module + // from + // from import x + self.add_error( + ParseErrorType::OtherError("expected a module name".to_string()), + self.current_token_range(), + ); + } + None + }; + + if cfg!(feature = "python315-diagnostics") + && module.is_some() + && let Some(range) = self.misplaced_lazy_import_range() + { + self.add_error(ParseErrorType::MisplacedLazyImport, range); + } + + // test_ok from_import_no_space + // from.import x + // from...import x + self.expect(TokenKind::Import); + + let names_start = self.node_start(); + let names_snapshot = self.alias_scratch.snapshot(); + let mut seen_star_import = false; + + let parenthesized = Parenthesized::from(self.eat(TokenKind::Lpar)); + let lazy_future = cfg!(feature = "python315-diagnostics") + && is_lazy + && leading_dots == 0 + && module.as_ref().is_some_and(|name| name.id == "__future__"); + let mut incomplete_optional_alias = false; + + // test_err from_import_unparenthesized_trailing_comma + // from a import b, + // from a import b as c, + // from a import b, c, + self.parse_comma_separated_list( + RecoveryContextKind::ImportFromAsNames(parenthesized), + |parser| { + // test_err from_import_dotted_names + // from x import a. + // from x import a.b + // from x import a, b.c, d, e.f, g + // CPython's optional `as NAME` rolls back on a missing name, so an + // unparenthesized target list can finish before this recovery error. + incomplete_optional_alias = lazy_future + && !parenthesized.is_yes() + && parser.at_identifier_or_soft_keyword() + && matches!( + parser.peek2(), + (TokenKind::As, TokenKind::Newline | TokenKind::EndOfFile) + ); + let alias = parser.parse_alias(ImportStyle::ImportFrom); + seen_star_import |= alias.name.id == "*"; + parser.alias_scratch.push(alias); + }, + ); + let names: Vec<_> = self.alias_scratch.take(names_snapshot); + + if names.is_empty() { + // test_err from_import_empty_names + // from x import + // from x import () + // from x import ,, + self.add_empty_import_names_error(); + } + + if seen_star_import && parenthesized.is_yes() { + // test_err from_import_parenthesized_star + // from x import (*) + self.add_error( + ParseErrorType::OtherError("star import cannot be parenthesized".to_string()), + self.node_range(names_start), + ); + } + + if seen_star_import && names.len() > 1 { + // test_err from_import_star_with_other_names + // from x import *, a + // from x import a, *, b + // from x import *, a as b + // from x import *, *, a + self.add_error( + ParseErrorType::OtherError("star import must be the only import".to_string()), + self.node_range(names_start), + ); + } + + if parenthesized.is_yes() { + // test_err from_import_missing_rpar + // from x import (a, b + // 1 + 1 + // from x import (a, b, + // 2 + 2 + self.expect(TokenKind::Rpar); + } + + if lazy_future + && !names.is_empty() + && (self.errors.len() == import_errors_start + || (!parenthesized.is_yes() + && incomplete_optional_alias + && self.errors.len() == import_errors_start + 1)) + { + // This grammar action precedes subsequent statements and the optional-as + // recovery above. Keep the recovered AST and tokens unchanged. + self.errors.insert( + import_errors_start, + crate::ParseError { + error: ParseErrorType::LazyFutureImport, + location: TextRange::at(start, TextSize::new(4)), + }, + ); + } + + ast::StmtImportFrom { + module, + names, + level: leading_dots, + is_lazy, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_level: None, + } + } + + /// Checks the misplaced `lazy` invalid production with the import-target parser. + /// Rewind afterward so the existing invalid-input recovery and AST stay intact. + fn misplaced_lazy_import_range(&mut self) -> Option { + if !self.at(TokenKind::Lazy) || self.peek() != TokenKind::Import { + return None; + } + let checkpoint = self.checkpoint(); + let errors_before = self.errors.len(); + let lazy_range = self.current_token_range(); + self.bump(TokenKind::Lazy); + self.bump(TokenKind::Import); + let parenthesized = Parenthesized::from(self.eat(TokenKind::Lpar)); + let names = self.parse_comma_separated_list_into_vec( + RecoveryContextKind::ImportFromAsNames(parenthesized), + |parser| parser.parse_alias(ImportStyle::ImportFrom), + ); + if parenthesized.is_yes() { + self.expect(TokenKind::Rpar); + } + let valid = !names.is_empty() + && self.errors.len() == errors_before + && (!names.iter().any(|alias| alias.name.id == "*") + || (names.len() == 1 && !parenthesized.is_yes())); + self.rewind(checkpoint); + valid.then_some(lazy_range) + } + + /// Empty import targets use a cursor diagnostic only at newline or EOF. + /// A concrete unexpected token keeps its ordinary syntax-error span. + fn add_empty_import_names_error(&mut self) { + let error = if cfg!(feature = "python315-diagnostics") + && !matches!( + self.current_token_kind(), + TokenKind::Newline | TokenKind::EndOfFile + ) { + ParseErrorType::ExpectedToken { + expected: TokenKind::Identifier, + found: self.current_token_kind(), + } + } else { + ParseErrorType::EmptyImportNames + }; + self.add_error(error, self.current_token_range()); + } + + /// Parses an `import` or `from` import name. + /// + /// See: + /// - + /// - + fn parse_alias(&mut self, style: ImportStyle) -> ast::Alias { + let start = self.node_start(); + if self.eat(TokenKind::Star) { + let range = self.node_range(start); + return ast::Alias { + name: ast::Identifier { + id: Name::new_static("*"), + range, + node_index: AtomicNodeIndex::NONE, + }, + asname: None, + range, + node_index: AtomicNodeIndex::NONE, + }; + } + + let name = match style { + ImportStyle::Import => self.parse_dotted_name(), + ImportStyle::ImportFrom => self.parse_identifier(), + }; + + let asname = if self.eat(TokenKind::As) { + if self.at_identifier_or_soft_keyword() + && matches!( + self.peek(), + TokenKind::Comma + | TokenKind::Rpar + | TokenKind::Semi + | TokenKind::Newline + | TokenKind::EndOfFile + ) + { + // test_ok import_as_name_soft_keyword + // import foo as match + // import bar as case + // import baz as type + // import qux as lazy + Some(self.parse_identifier()) + } else if self.at_expr() { + // test_err import_alias_invalid_asname + // import x as y.z + // from x import (y as z()) + let target = self.parse_conditional_expression_or_higher(); + self.add_error( + ParseErrorType::InvalidImportTarget(ExpressionKind::of(&target.expr)), + &target, + ); + Some(invalid_target_identifier(&target.expr)) + } else { + // test_err import_alias_missing_asname + // import x as + let error = if cfg!(feature = "python315-diagnostics") { + ParseErrorType::ExpectedToken { + expected: TokenKind::Identifier, + found: self.current_token_kind(), + } + } else { + ParseErrorType::OtherError("expected symbol after `as`".to_string()) + }; + self.add_error(error, self.current_token_range()); + None + } + } else { + None + }; + + ast::Alias { + range: self.node_range(start), + name, + asname, + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a dotted name. + /// + /// A dotted name is a sequence of identifiers separated by a single dot. + fn parse_dotted_name(&mut self) -> ast::Identifier { + let start = self.node_start(); + + let first = self.parse_identifier(); + if !self.at(TokenKind::Dot) { + return first; + } + + let snapshot = self.name_buffer.len(); + self.name_buffer.push_str(&first.id); + let mut progress = ParserProgress::default(); + + while self.eat(TokenKind::Dot) { + progress.assert_progressing(self); + + // test_err dotted_name_multiple_dots + // import a..b + // import a...b + self.name_buffer.push('.'); + let identifier = self.parse_identifier(); + self.name_buffer.push_str(&identifier.id); + } + + let id = self.name_interner.intern(&self.name_buffer[snapshot..]); + self.name_buffer.truncate(snapshot); + + // test_ok dotted_name_normalized_spaces + // import a.b.c + // import a . b . c + ast::Identifier { + id, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a `pass` statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `pass` token. + /// + /// See: + fn parse_pass_statement(&mut self) -> ast::StmtPass { + let start = self.node_start(); + self.bump(TokenKind::Pass); + ast::StmtPass { + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a `continue` statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `continue` token. + /// + /// See: + fn parse_continue_statement(&mut self) -> ast::StmtContinue { + let start = self.node_start(); + self.bump(TokenKind::Continue); + ast::StmtContinue { + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a `break` statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `break` token. + /// + /// See: + fn parse_break_statement(&mut self) -> ast::StmtBreak { + let start = self.node_start(); + self.bump(TokenKind::Break); + ast::StmtBreak { + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses an `assert` statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at an `assert` token. + /// + /// See: + fn parse_assert_statement(&mut self) -> ast::StmtAssert { + let start = self.node_start(); + self.bump(TokenKind::Assert); + + // test_err assert_empty_test + // assert + + // test_err assert_invalid_test_expr + // assert *x + // assert assert x + // assert yield x + // assert x := 1 + let test_errors_start = self.errors.len(); + let test = self.parse_conditional_expression_or_higher(); + if cfg!(feature = "python315-diagnostics") && self.errors.len() == test_errors_start { + self.check_assert_assignment(&test); + } + + let msg = if self.eat(TokenKind::Comma) { + if self.at_expr() { + // test_err assert_invalid_msg_expr + // assert False, *x + // assert False, assert x + // assert False, yield x + // assert False, x := 1 + let message_errors_start = self.errors.len(); + let message = self.parse_conditional_expression_or_higher(); + if cfg!(feature = "python315-diagnostics") + && self.errors.len() == message_errors_start + { + self.check_assert_assignment(&message); + } + Some(Box::new(message.expr)) + } else { + // test_err assert_empty_msg + // assert x, + self.add_error( + ParseErrorType::ExpectedExpression, + self.current_token_range(), + ); + None + } + } else { + None + }; + + ast::StmtAssert { + test: Box::new(test.expr), + msg, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Diagnose an assignment after an otherwise valid assertion expression. Speculative + /// parsing supplies the expression span without consuming recovery tokens or changing + /// the recovered assertion AST. Errors inside parentheses retain their own diagnostics. + fn check_assert_assignment(&mut self, target: &ParsedExpr) { + let operator = self.current_token_kind(); + if !matches!(operator, TokenKind::Equal | TokenKind::ColonEqual) { + return; + } + + let checkpoint = self.checkpoint(); + let errors_start = self.errors.len(); + self.bump(operator); + let value_end = if self.at_expr() { + let value = self.parse_conditional_expression_or_higher(); + (self.errors.len() == errors_start).then_some(value.end()) + } else { + None + }; + self.rewind(checkpoint); + + if let Some(value_end) = value_end { + let error = if operator == TokenKind::ColonEqual { + ParseErrorType::NamedExpressionWithoutParentheses + } else { + ParseErrorType::InvalidAssignmentTarget { + kind: ExpressionKind::of(&target.expr), + maybe_comparison: true, + } + }; + self.add_error(error, TextRange::new(target.start(), value_end)); + } + } + + /// Parses a global statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `global` token. + /// + /// See: + fn parse_global_statement(&mut self) -> ast::StmtGlobal { + let start = self.node_start(); + self.bump(TokenKind::Global); + + // test_err global_stmt_trailing_comma + // global , + // global x, + // global x, y, + + // test_err global_stmt_expression + // global x + 1 + let names = self.parse_comma_separated_list_into_vec( + RecoveryContextKind::Identifiers, + Parser::parse_identifier, + ); + + if names.is_empty() { + // test_err global_stmt_empty + // global + self.add_error(ParseErrorType::EmptyGlobalNames, self.current_token_range()); + } + + // test_ok global_stmt + // global x + // global x, y, z + ast::StmtGlobal { + range: self.node_range(start), + names, + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a nonlocal statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `nonlocal` token. + /// + /// See: + fn parse_nonlocal_statement(&mut self) -> ast::StmtNonlocal { + let start = self.node_start(); + self.bump(TokenKind::Nonlocal); + + // test_err nonlocal_stmt_trailing_comma + // def _(): + // nonlocal , + // nonlocal x, + // nonlocal x, y, + + // test_err nonlocal_stmt_expression + // def _(): + // nonlocal x + 1 + let names = self.parse_comma_separated_list_into_vec( + RecoveryContextKind::Identifiers, + Parser::parse_identifier, + ); + + if names.is_empty() { + // test_err nonlocal_stmt_empty + // def _(): + // nonlocal + self.add_error( + ParseErrorType::EmptyNonlocalNames, + self.current_token_range(), + ); + } + + // test_ok nonlocal_stmt + // def _(): + // nonlocal x + // nonlocal x, y, z + ast::StmtNonlocal { + range: self.node_range(start), + names, + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a type alias statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `type` token. + /// + /// See: + fn parse_type_alias_statement(&mut self) -> ast::StmtTypeAlias { + let start = self.node_start(); + let type_range = self.current_token_range(); + self.bump(TokenKind::Type); + + // test_ok type_stmt_py312 + // # parse_options: {"target-version": "3.12"} + // type x = int + + // test_err type_stmt_py311 + // # parse_options: {"target-version": "3.11"} + // type x = int + + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::TypeAliasStatement, + type_range, + ); + + let mut name = Expr::Name(self.parse_name(ExpressionContext::default())); + helpers::set_expr_ctx(&mut name, ExprContext::Store); + + let type_params = self.try_parse_type_params(); + + self.expect(TokenKind::Equal); + + // test_err type_alias_incomplete_stmt + // type + // type x + // type x = + + // test_err type_alias_invalid_value_expr + // type x = *y + // type x = yield y + // type x = yield from y + // type x = x := 1 + let value = self.parse_conditional_expression_or_higher(); + + ast::StmtTypeAlias { + name: Box::new(name), + type_params: type_params.map(Box::new), + value: Box::new(value.expr), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses an IPython escape command at the statement level. + /// + /// # Panics + /// + /// If the parser isn't positioned at an `IpyEscapeCommand` token. + fn parse_ipython_escape_command_statement(&mut self) -> ast::StmtIpyEscapeCommand { + let start = self.node_start(); + + let (value, kind) = self.bump_ipython_escape_command(IpyEscapeContext::LogicalLineStart); + + let range = self.node_range(start); + if self.options.mode != Mode::Ipython { + self.add_error(ParseErrorType::UnexpectedIpythonEscapeCommand, range); + } + + ast::StmtIpyEscapeCommand { + range, + kind, + value, + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses an IPython help end escape command at the statement level. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `?` token. + fn parse_ipython_help_end_escape_command_statement( + &mut self, + parsed_expr: &ParsedExpr, + ) -> ast::StmtIpyEscapeCommand { + // We are permissive than the original implementation because we would allow whitespace + // between the expression and the suffix while the IPython implementation doesn't allow it. + // For example, `foo ?` would be valid in our case but invalid for IPython. + fn unparse_expr(parser: &mut Parser, expr: &Expr, buffer: &mut String) { + match expr { + Expr::Name(ast::ExprName { id, .. }) => { + buffer.push_str(id.as_str()); + } + Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { + unparse_expr(parser, value, buffer); + buffer.push('['); + + if let Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(integer), + .. + }) = &**slice + { + let _ = write!(buffer, "{integer}"); + } else { + parser.add_error( + ParseErrorType::OtherError( + "only integer literals are allowed in subscript expressions \ + in help end escape command" + .to_string(), + ), + slice.range(), + ); + buffer.push_str(parser.src_text(slice.range())); + } + + buffer.push(']'); + } + Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => { + unparse_expr(parser, value, buffer); + buffer.push('.'); + buffer.push_str(attr.as_str()); + } + _ => { + parser.add_error( + ParseErrorType::OtherError( + "expected name, subscript or attribute expression \ + in help end escape command" + .to_string(), + ), + expr, + ); + } + } + } + + let start = self.node_start(); + self.bump(TokenKind::Question); + + let kind = if self.eat(TokenKind::Question) { + IpyEscapeKind::Help2 + } else { + IpyEscapeKind::Help + }; + + if parsed_expr.is_parenthesized { + let token_range = self.node_range(start); + self.add_error( + ParseErrorType::OtherError( + "help end escape command cannot be applied on a parenthesized expression" + .to_string(), + ), + token_range, + ); + } + + if self.at(TokenKind::Question) { + self.add_error( + ParseErrorType::OtherError( + "maximum of 2 `?` tokens are allowed in help end escape command".to_string(), + ), + self.current_token_range(), + ); + } + + let mut value = String::new(); + unparse_expr(self, &parsed_expr.expr, &mut value); + + ast::StmtIpyEscapeCommand { + value: value.into_boxed_str(), + kind, + range: self.node_range(parsed_expr.start()), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parse an assignment statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at an `=` token. + /// + /// See: + fn parse_assign_statement(&mut self, target: ParsedExpr, start: TextSize) -> ast::StmtAssign { + self.bump(TokenKind::Equal); + + let mut targets = vec![target.expr]; + // Errors in the targets are reported before the errors in the expressions after them. + let value_errors_start = self.errors.len(); + + // test_err assign_stmt_missing_rhs + // x = + // 1 + 1 + // x = y = + // 2 + 2 + // x = = y + // 3 + 3 + + // test_err assign_stmt_keyword_target + // a = pass = c + // a + b + // a = b = pass = c + // a + b + + // test_err assign_stmt_invalid_value_expr + // x = (*a and b,) + // x = (42, *yield x) + // x = (42, *yield from x) + // x = (*lambda x: x,) + // x = x := 1 + + let mut value = + self.parse_expression_list(ExpressionContext::yield_or_starred_bitwise_or()); + + if self.at(TokenKind::Equal) { + // This path is only taken when there are more than one assignment targets. + self.parse_list(RecoveryContextKind::AssignmentTargets, |parser| { + parser.bump(TokenKind::Equal); + + let mut parsed_expr = + parser.parse_expression_list(ExpressionContext::yield_or_starred_bitwise_or()); + + std::mem::swap(&mut value, &mut parsed_expr); + + targets.push(parsed_expr.expr); + }); + } + + for target in &mut targets { + helpers::set_expr_ctx(target, ExprContext::Store); + } + + // test_err assign_stmt_invalid_target + // 1 = 1 + // x = 1 = 2 + // x = 1 = y = 2 = z + // ["a", "b"] = ["a", "b"] + if targets + .iter() + .any(|target| invalid_assignment_target(target).is_some()) + { + let value_errors = self.errors.split_off(value_errors_start); + if let Some((target, rhs_end)) = self.comparison_like_assignment(&targets, &value.expr) + { + // test_err assign_stmt_comparison_like_target + // f() = 1 + // f(), b = 1 + // a, (x < y) = 1 if z else 2 + if target.is_name_expr() { + self.add_error( + ParseErrorType::AssignmentInsteadOfComparison, + TextRange::new(target.start(), rhs_end), + ); + } else { + self.add_error( + ParseErrorType::InvalidAssignmentTarget { + kind: ExpressionKind::of(target), + maybe_comparison: true, + }, + target.range(), + ); + } + } else { + for target in &targets { + // test_err assign_stmt_yield_target + // yield x = 1 + // y = yield = 1 + if matches!(target, Expr::Yield(_) | Expr::YieldFrom(_)) + && !self.is_parenthesized(target.range()) + { + self.add_error(ParseErrorType::AssignmentToYield, target); + } else { + self.validate_assignment_target(target); + } + } + } + self.errors.extend(value_errors); + } + + ast::StmtAssign { + targets, + value: Box::new(value.expr), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + } + } + + /// Parses an annotated assignment statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `:` token. + /// + /// See: + fn parse_annotated_assignment_statement( + &mut self, + mut target: ParsedExpr, + start: TextSize, + ) -> ast::StmtAnnAssign { + self.bump(TokenKind::Colon); + + // test_err ann_assign_stmt_invalid_target + // "abc": str = "def" + // call(): str = "no" + // *x: int = 1, 2 + // # Tuple assignment + // x,: int = 1 + // x, y: int = 1, 2 + // (x, y): int = 1, 2 + // # List assignment + // [x]: int = 1 + // [x, y]: int = 1, 2 + self.validate_annotated_assignment_target(&target.expr); + + helpers::set_expr_ctx(&mut target.expr, ExprContext::Store); + + // test_ok ann_assign_stmt_simple_target + // a: int # simple + // (a): int + // a.b: int + // a[0]: int + let simple = target.is_name_expr() && !target.is_parenthesized; + + // test_err ann_assign_stmt_invalid_annotation + // x: *int = 1 + // x: yield a = 1 + // x: yield from b = 1 + // x: y := int = 1 + + // test_err ann_assign_stmt_type_alias_annotation + // a: type X = int + // lambda: type X = int + let annotation = self.parse_conditional_expression_or_higher(); + + let value = if self.eat(TokenKind::Equal) { + if self.at_expr() { + // test_err ann_assign_stmt_invalid_value + // x: Any = *a and b + // x: Any = x := 1 + // x: list = [x, *a | b, *a or b] + Some(Box::new( + self.parse_expression_list(ExpressionContext::yield_or_starred_bitwise_or()) + .expr, + )) + } else { + // test_err ann_assign_stmt_missing_rhs + // x: int = + self.add_error( + ParseErrorType::ExpectedExpression, + self.current_token_range(), + ); + None + } + } else { + None + }; + + ast::StmtAnnAssign { + target: Box::new(target.expr), + annotation: Box::new(annotation.expr), + value, + simple, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_simple: None, + } + } + + /// Parses an augmented assignment statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at an augmented assignment token. + /// + /// See: + fn parse_augmented_assignment_statement( + &mut self, + mut target: ParsedExpr, + op: Operator, + start: TextSize, + ) -> ast::StmtAugAssign { + // Consume the operator + self.bump_ts(AUGMENTED_ASSIGN_SET); + + if !matches!( + &target.expr, + Expr::Name(_) | Expr::Attribute(_) | Expr::Subscript(_) + ) { + // test_err aug_assign_stmt_invalid_target + // 1 += 1 + // "a" += "b" + // *x += 1 + // pass += 1 + // x += pass + // (x + y) += 1 + self.add_error( + ParseErrorType::InvalidAugmentedAssignmentTarget(ExpressionKind::of(&target.expr)), + &target, + ); + } + + helpers::set_expr_ctx(&mut target.expr, ExprContext::Store); + + // test_err aug_assign_stmt_missing_rhs + // x += + // 1 + 1 + // x += y += + // 2 + 2 + + // test_err aug_assign_stmt_invalid_value + // x += *a and b + // x += *yield x + // x += *yield from x + // x += *lambda x: x + // x += y := 1 + let value = self.parse_expression_list(ExpressionContext::yield_or_starred_bitwise_or()); + + ast::StmtAugAssign { + target: Box::new(target.expr), + op, + value: Box::new(value.expr), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses an `if` statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at an `if` token. + /// + /// See: + fn parse_if_statement(&mut self) -> ast::StmtIf { + let start = self.node_start(); + self.bump(TokenKind::If); + + // test_err if_stmt_invalid_test_expr + // if *x: ... + // if yield x: ... + // if yield from x: ... + + // test_err if_stmt_missing_test + // if : ... + let test = self.parse_named_expression_or_higher(ExpressionContext::default()); + + // test_err if_stmt_missing_colon + // if x + // if x + // pass + // a = 1 + self.expect(TokenKind::Colon); + + // test_err if_stmt_empty_body + // if True: + // 1 + 1 + let body = self.parse_body(BlockClause::If, start); + + // test_err if_stmt_misspelled_elif + // if True: + // pass + // elf: + // pass + // else: + // pass + let elif_else_snapshot = self.elif_else_scratch.snapshot(); + self.parse_clauses(Clause::ElIf, |parser| { + let clause = parser.parse_elif_or_else_clause(ElifOrElse::Elif); + parser.elif_else_scratch.push(clause); + }); + + if self.at(TokenKind::Else) { + let clause = self.parse_elif_or_else_clause(ElifOrElse::Else); + self.elif_else_scratch.push(clause); + + if self.at(TokenKind::Elif) { + // test_err if_stmt_elif_after_else + // if x: + // pass + // else: + // pass + // elif y: + // pass + self.add_error( + ParseErrorType::OtherError("'elif' block follows an 'else' block".to_string()), + self.current_token_range(), + ); + } + } + + ast::StmtIf { + test: Box::new(test.expr), + body, + elif_else_clauses: self.elif_else_scratch.take(elif_else_snapshot), + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_body: None, + } + } + + /// Parses an `elif` or `else` clause. + /// + /// # Panics + /// + /// If the parser isn't positioned at an `elif` or `else` token. + fn parse_elif_or_else_clause(&mut self, kind: ElifOrElse) -> ast::ElifElseClause { + let start = self.node_start(); + self.bump(kind.as_token_kind()); + + let test = if kind.is_elif() { + // test_err if_stmt_invalid_elif_test_expr + // if x: + // pass + // elif *x: + // pass + // elif yield x: + // pass + Some( + self.parse_named_expression_or_higher(ExpressionContext::default()) + .expr, + ) + } else { + None + }; + + // test_err if_stmt_elif_missing_colon + // if x: + // pass + // elif y + // pass + // else: + // pass + self.expect(TokenKind::Colon); + + let body = self.parse_body(kind.as_block_clause(), start); + + ast::ElifElseClause { + test, + body, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_body: None, + runtime_orelse: None, + } + } + + /// Parses a `try` statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `try` token. + /// + /// See: + fn parse_try_statement(&mut self) -> ast::StmtTry { + let try_start = self.node_start(); + self.bump(TokenKind::Try); + self.expect(TokenKind::Colon); + + let mut is_star: Option = None; + + let try_body = self.parse_body(BlockClause::Try, try_start); + + let has_except = self.at(TokenKind::Except); + + // test_err try_stmt_mixed_except_kind + // try: + // pass + // except: + // pass + // except* ExceptionGroup: + // pass + // try: + // pass + // except* ExceptionGroup: + // pass + // except: + // pass + // try: + // pass + // except: + // pass + // except: + // pass + // except* ExceptionGroup: + // pass + // except* ExceptionGroup: + // pass + let mut mixed_except_ranges = Vec::new(); + let mut handlers = Vec::new(); + self.parse_clauses(Clause::Except, |p| { + let (handler, kind) = p.parse_except_clause(); + if let ExceptClauseKind::Star(range) = kind { + p.add_unsupported_syntax_error(UnsupportedSyntaxErrorKind::ExceptStar, range); + } + if is_star.is_none() { + is_star = Some(kind.is_star()); + } else if is_star != Some(kind.is_star()) { + // The error covers the `except` keyword and the star. + let except_end = match kind { + ExceptClauseKind::Star(star_range) => star_range.end(), + ExceptClauseKind::Normal => handler.start() + TextSize::of("except"), + }; + mixed_except_ranges.push(TextRange::new(handler.start(), except_end)); + } + if handlers.is_empty() { + handlers.reserve_exact(1); + } + handlers.push(handler); + }); + handlers.shrink_to_fit(); + + // Empty handler has `is_star` false. + let is_star = is_star.unwrap_or_default(); + for handler_err_range in mixed_except_ranges { + self.add_error( + ParseErrorType::OtherError( + "cannot have both 'except' and 'except*' on the same 'try'".to_string(), + ), + handler_err_range, + ); + } + + // test_err try_stmt_misspelled_except + // try: + // pass + // exept: # spellchecker:disable-line + // pass + // finally: + // pass + // a = 1 + // try: + // pass + // except: + // pass + // exept: # spellchecker:disable-line + // pass + // b = 1 + + let orelse = if self.at(TokenKind::Else) { + let else_start = self.current_token_range().start(); + self.bump(TokenKind::Else); + self.expect(TokenKind::Colon); + self.parse_body(BlockClause::Else, else_start) + } else { + Suite::new() + }; + + let (finalbody, has_finally) = if self.at(TokenKind::Finally) { + let finally_start = self.current_token_range().start(); + self.bump(TokenKind::Finally); + self.expect(TokenKind::Colon); + (self.parse_body(BlockClause::Finally, finally_start), true) + } else { + (Suite::new(), false) + }; + + if !has_except && !has_finally { + // test_err try_stmt_missing_except_finally + // try: + // pass + // try: + // pass + // else: + // pass + self.add_error( + ParseErrorType::OtherError("expected 'except' or 'finally' block".to_string()), + self.current_token_range(), + ); + } + + if has_finally && self.at(TokenKind::Else) { + // test_err try_stmt_invalid_order + // try: + // pass + // finally: + // pass + // else: + // pass + self.add_error( + ParseErrorType::OtherError( + "`else` block must come before `finally` block".to_string(), + ), + self.current_token_range(), + ); + } + + // test_ok except_star_py311 + // # parse_options: {"target-version": "3.11"} + // try: ... + // except* ValueError: ... + + // test_err except_star_py310 + // # parse_options: {"target-version": "3.10"} + // try: ... + // except* ValueError: ... + // except* KeyError: ... + // except * Error: ... + + ast::StmtTry { + body: try_body, + handlers, + orelse, + finalbody, + is_star, + range: self.node_range(try_start), + node_index: AtomicNodeIndex::NONE, + runtime_body: None, + runtime_handlers: None, + runtime_orelse: None, + runtime_finalbody: None, + } + } + + /// Parses an `except` clause of a `try` statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at an `except` token. + fn parse_except_clause(&mut self) -> (ExceptHandler, ExceptClauseKind) { + let start = self.node_start(); + self.bump(TokenKind::Except); + + let star_token_range = self.current_token_range(); + let block_kind = if self.eat(TokenKind::Star) { + ExceptClauseKind::Star(star_token_range) + } else { + ExceptClauseKind::Normal + }; + + let mut unparenthesized_types_start = None; + let type_ = if self.at_expr() { + // test_err except_stmt_invalid_expression + // try: + // pass + // except yield x: + // pass + // try: + // pass + // except* *x: + // pass + let parsed_expr = self.parse_expression_list(ExpressionContext::default()); + if matches!( + parsed_expr.expr, + Expr::Tuple(ast::ExprTuple { + parenthesized: false, + .. + }) + ) { + if self.at(TokenKind::As) { + // test_err except_stmt_unparenthesized_tuple_as + // try: + // pass + // except x, y as exc: + // pass + // try: + // pass + // except* x, y as eg: + // pass + unparenthesized_types_start = Some(parsed_expr.start()); + } else { + // test_err except_stmt_unparenthesized_tuple_no_as_py313 + // # parse_options: {"target-version": "3.13"} + // try: + // pass + // except x, y: + // pass + // try: + // pass + // except* x, y: + // pass + + // test_ok except_stmt_unparenthesized_tuple_no_as_py314 + // # parse_options: {"target-version": "3.14"} + // try: + // pass + // except x, y: + // pass + // try: + // pass + // except* x, y: + // pass + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes, + parsed_expr.range(), + ); + } + } + Some(Box::new(parsed_expr.expr)) + } else { + if block_kind.is_star() || self.at(TokenKind::As) { + // test_err except_stmt_missing_exception + // try: + // pass + // except as exc: + // pass + // # If a '*' is present then exception type is required + // try: + // pass + // except*: + // pass + // except* + // pass + // except* as exc: + // pass + self.add_error( + ParseErrorType::OtherError("expected one or more exception types".to_string()), + self.current_token_range(), + ); + } + None + }; + + let name = if self.eat(TokenKind::As) { + let checkpoint = self.checkpoint(); + // An unparenthesized tuple of exception types is reported on its own. + let invalid_target = if unparenthesized_types_start.is_none() + && self.at_expr() + && !(self.at_identifier_or_soft_keyword() && self.peek() == TokenKind::Colon) + { + let target = self.parse_conditional_expression_or_higher(); + if self.at(TokenKind::Colon) { + Some(target.expr) + } else { + self.rewind(checkpoint); + None + } + } else { + None + }; + if let Some(target) = invalid_target { + // test_err except_stmt_invalid_as_name + // try: ... + // except Exception as x.y: ... + // except* Exception as f(): ... + self.add_error( + ParseErrorType::InvalidExceptTarget { + kind: ExpressionKind::of(&target), + star: block_kind.is_star(), + }, + &target, + ); + Some(invalid_target_identifier(&target)) + } else if self.at_identifier_or_soft_keyword() { + // test_ok except_stmt_as_name_soft_keyword + // try: ... + // except Exception as match: ... + // except Exception as case: ... + // except Exception as type: ... + Some(self.parse_identifier()) + } else { + // test_err except_stmt_missing_as_name + // try: + // pass + // except Exception as: + // pass + // except Exception as + // pass + self.add_error( + ParseErrorType::OtherError("expected name after `as`".to_string()), + self.current_token_range(), + ); + None + } + } else { + None + }; + + // The error is raised only once the whole `as NAME :` part has matched, and covers + // the exception types through `as NAME`, stopping just before the `:`. + + // test_err except_stmt_unparenthesized_tuple_as_incomplete + // try: + // pass + // except x, y as (exc): + // pass + // try: + // pass + // except x, y as exc + // pass + if let Some(types_start) = unparenthesized_types_start + && name.is_some() + && self.at(TokenKind::Colon) + { + self.add_error( + ParseErrorType::OtherError( + "multiple exception types must be parenthesized when using 'as'".to_string(), + ), + TextRange::new(types_start, self.current_token_range().start()), + ); + } + + // test_err except_stmt_missing_exception_and_as_name + // try: + // pass + // except as: + // pass + + self.expect(TokenKind::Colon); + + let except_clause = if block_kind.is_star() { + BlockClause::ExceptStar + } else { + BlockClause::Except + }; + let except_body = self.parse_body(except_clause, start); + + ( + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + type_, + name, + body: except_body, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_body: None, + }), + block_kind, + ) + } + + /// Parses a `for` statement. + /// + /// The given `start` offset is the start of either the `for` token or the + /// `async` token if it's an async for statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `for` token. + /// + /// See: + fn parse_for_statement(&mut self, start: TextSize) -> ast::StmtFor { + let for_start = self.current_token_range().start(); + self.bump(TokenKind::For); + + // test_err for_stmt_missing_target + // for in x: ... + + // test_ok for_in_target_valid_expr + // for d[x in y] in target: ... + // for (x in y)[0] in iter: ... + // for (x in y).attr in iter: ... + + // test_err for_stmt_invalid_target_in_keyword + // for d(x in y) in target: ... + // for (x in y)() in iter: ... + // for (x in y) in iter: ... + // for (x in y, z) in iter: ... + // for [x in y, z] in iter: ... + // for {x in y, z} in iter: ... + + // test_err for_stmt_invalid_target_binary_expr + // for x not in y in z: ... + // for x == y in z: ... + // for x or y in z: ... + // for -x in y: ... + // for not x in y: ... + // for x | y in z: ... + let mut target = + self.parse_expression_list(ExpressionContext::starred_conditional().with_in_excluded()); + + helpers::set_expr_ctx(&mut target.expr, ExprContext::Store); + + // test_err for_stmt_invalid_target + // for 1 in x: ... + // for "a" in x: ... + // for *x and y in z: ... + // for *x | y in z: ... + // for await x in z: ... + // for yield x in y: ... + // for [x, 1, y, *["a"]] in z: ... + self.validate_for_target(&target.expr); + + // test_err for_stmt_missing_in_keyword + // for a b: ... + // for a: ... + self.expect(TokenKind::In); + + // test_err for_stmt_missing_iter + // for x in: + // a = 1 + + // test_err for_stmt_invalid_iter_expr + // for x in *a and b: ... + // for x in yield a: ... + // for target in x := 1: ... + let iter = self.parse_expression_list(ExpressionContext::starred_bitwise_or()); + + // test_ok for_iter_unpack_py39 + // # parse_options: {"target-version": "3.9"} + // for x in *a, b: ... + // for x in a, *b: ... + // for x in *a, *b: ... + + // test_ok for_iter_unpack_py38 + // # parse_options: {"target-version": "3.8"} + // for x in (*a, b): ... + // for x in ( a, *b): ... + // for x in (*a, *b): ... + + // test_err for_iter_unpack_py38 + // # parse_options: {"target-version": "3.8"} + // for x in *a, b: ... + // for x in a, *b: ... + // for x in *a, *b: ... + self.check_tuple_unpacking( + &iter, + UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor, + ); + + self.expect(TokenKind::Colon); + + let body = self.parse_body(BlockClause::For, for_start); + + let orelse = if self.at(TokenKind::Else) { + let else_start = self.current_token_range().start(); + self.bump(TokenKind::Else); + self.expect(TokenKind::Colon); + self.parse_body(BlockClause::Else, else_start) + } else { + Suite::new() + }; + + ast::StmtFor { + target: Box::new(target.expr), + iter: Box::new(iter.expr), + is_async: false, + body, + orelse, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + runtime_body: None, + runtime_orelse: None, + } + } + + /// Parses a `while` statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `while` token. + /// + /// See: + fn parse_while_statement(&mut self) -> ast::StmtWhile { + let start = self.node_start(); + self.bump(TokenKind::While); + + // test_err while_stmt_missing_test + // while : ... + // while : + // a = 1 + + // test_err while_stmt_invalid_test_expr + // while *x: ... + // while yield x: ... + // while a, b: ... + // while a := 1, b: ... + let test = self.parse_named_expression_or_higher(ExpressionContext::default()); + + // test_err while_stmt_missing_colon + // while ( + // a < 30 # comment + // ) + // pass + self.expect(TokenKind::Colon); + + let body = self.parse_body(BlockClause::While, start); + + let orelse = if self.at(TokenKind::Else) { + let else_start = self.current_token_range().start(); + self.bump(TokenKind::Else); + self.expect(TokenKind::Colon); + self.parse_body(BlockClause::Else, else_start) + } else { + Suite::new() + }; + + ast::StmtWhile { + test: Box::new(test.expr), + body, + orelse, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_body: None, + runtime_orelse: None, + } + } + + /// Parses a function definition. + /// + /// The given `start` offset is the start of either of the following: + /// - `def` token + /// - `async` token if it's an asynchronous function definition with no decorators + /// - `@` token if the function definition has decorators + /// + /// # Panics + /// + /// If the parser isn't positioned at a `def` token. + /// + /// See: + fn parse_function_definition( + &mut self, + decorator_list: DecoratorList, + start: TextSize, + ) -> ast::StmtFunctionDef { + let def_start = self.current_token_range().start(); + self.bump(TokenKind::Def); + + // test_err function_def_missing_identifier + // def (): ... + // def () -> int: ... + let name = self.parse_identifier(); + + // test_err function_def_unclosed_type_param_list + // def foo[T1, *T2(a, b): + // return a + b + // x = 10 + let type_params = self.try_parse_type_params(); + + // test_ok function_type_params_py312 + // # parse_options: {"target-version": "3.12"} + // def foo[T](): ... + + // test_err function_type_params_py311 + // # parse_options: {"target-version": "3.11"} + // def foo[T](): ... + // def foo[](): ... + if let Some(ast::TypeParams { range, .. }) = &type_params { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::TypeParameterList, + *range, + ); + } + + // test_ok function_def_parameter_range + // def foo( + // first: int, + // second: int, + // ) -> int: ... + + // test_err function_def_unclosed_parameter_list + // def foo(a: int, b: + // def foo(): + // return 42 + // def foo(a: int, b: str + // x = 10 + let parameters = self.parse_parameters(FunctionKind::FunctionDef); + + let returns = if self.eat(TokenKind::Rarrow) { + if self.at_expr() { + // test_ok function_def_valid_return_expr + // def foo() -> int | str: ... + // def foo() -> lambda x: x: ... + // def foo() -> int if True else str: ... + + // test_err function_def_invalid_return_expr + // def foo() -> *int: ... + // def foo() -> (*int): ... + // def foo() -> yield x: ... + let returns = self.parse_expression_list(ExpressionContext::default()); + + if matches!( + returns.expr, + Expr::Tuple(ast::ExprTuple { + parenthesized: false, + .. + }) + ) { + // test_ok function_def_parenthesized_return_types + // def foo() -> (int,): ... + // def foo() -> (int, str): ... + + // test_err function_def_unparenthesized_return_types + // def foo() -> int,: ... + // def foo() -> int, str: ... + self.add_error( + ParseErrorType::OtherError( + "multiple return types must be parenthesized".to_string(), + ), + returns.range(), + ); + } + + Some(Box::new(returns.expr)) + } else { + // test_err function_def_missing_return_type + // def foo() -> : ... + self.add_error( + ParseErrorType::ExpectedExpression, + self.current_token_range(), + ); + + None + } + } else { + None + }; + + self.expect(TokenKind::Colon); + + // test_err function_def_empty_body + // def foo(): + // def foo() -> int: + // x = 42 + let body = self.parse_body(BlockClause::FunctionDef, def_start); + + ast::StmtFunctionDef { + name, + type_params: type_params.map(Box::new), + parameters: Box::new(parameters), + body, + decorator_list, + is_async: false, + returns, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_decorator_list: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + runtime_body: None, + } + } + + /// Parses a class definition. + /// + /// The given `start` offset is the start of either the `def` token or the + /// `@` token if the class definition has decorators. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `class` token. + /// + /// See: + fn parse_class_definition( + &mut self, + decorator_list: DecoratorList, + start: TextSize, + ) -> ast::StmtClassDef { + let class_start = self.current_token_range().start(); + self.bump(TokenKind::Class); + + // test_err class_def_missing_name + // class : ... + // class (): ... + // class (metaclass=ABC): ... + let name = self.parse_identifier(); + + // test_err class_def_unclosed_type_param_list + // class Foo[T1, *T2(a, b): + // pass + // x = 10 + let type_params = self.try_parse_type_params(); + + // test_ok class_type_params_py312 + // # parse_options: {"target-version": "3.12"} + // class Foo[S: (str, bytes), T: float, *Ts, **P]: ... + + // test_err class_type_params_py311 + // # parse_options: {"target-version": "3.11"} + // class Foo[S: (str, bytes), T: float, *Ts, **P]: ... + // class Foo[]: ... + if let Some(ast::TypeParams { range, .. }) = &type_params { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::TypeParameterList, + *range, + ); + } + + // test_ok class_def_arguments + // class Foo: ... + // class Foo(): ... + // class Foo((base for base in bases)): ... + // class Foo(*(base for base in bases)): ... + + // test_err class_def_unparenthesized_generator_argument + // class Foo(base for base in bases): ... + let arguments = self + .at(TokenKind::Lpar) + .then(|| Box::new(self.parse_arguments(ArgumentsContext::ClassDefinition))); + + self.expect(TokenKind::Colon); + + // test_err class_def_empty_body + // class Foo: + // class Foo(): + // x = 42 + let body = self.parse_body(BlockClause::Class, class_start); + + ast::StmtClassDef { + range: self.node_range(start), + decorator_list, + name, + type_params: type_params.map(Box::new), + arguments, + body, + node_index: AtomicNodeIndex::NONE, + runtime_decorator_list: None, + runtime_body: None, + } + } + + /// Parses a `with` statement + /// + /// The given `start` offset is the start of either the `with` token or the + /// `async` token if it's an async with statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `with` token. + /// + /// See: + fn parse_with_statement(&mut self, start: TextSize) -> ast::StmtWith { + let with_start = self.current_token_range().start(); + self.bump(TokenKind::With); + + let mut items = self.parse_with_items(); + items.shrink_to_fit(); + + self.expect(TokenKind::Colon); + + let body = self.parse_body(BlockClause::With, with_start); + + ast::StmtWith { + items, + body, + is_async: false, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + runtime_body: None, + } + } + + /// Parses a list of with items. + /// + /// See: + fn parse_with_items(&mut self) -> Vec { + if !self.at_expr() { + let error = if cfg!(feature = "python315-diagnostics") { + ParseErrorType::ExpectedExpression + } else { + ParseErrorType::OtherError( + "expected the start of an expression after `with` keyword".to_string(), + ) + }; + self.add_error(error, self.current_token_range()); + return vec![]; + } + + let open_paren_range = self.current_token_range(); + + if self.at(TokenKind::Lpar) { + if let (Some(items), has_trailing_comma) = self.try_parse_parenthesized_with_items() { + // test_ok tuple_context_manager_py38 + // # parse_options: {"target-version": "3.8"} + // with ( + // foo, + // bar, + // baz, + // ) as tup: ... + + // test_ok single_parenthesized_item_context_manager_py38 + // # parse_options: {"target-version": "3.8"} + // with ( + // open('foo.txt')) as foo: ... + // with ( + // open('foo.txt')): ... + + // test_err tuple_context_manager_py38 + // # parse_options: {"target-version": "3.8"} + // # these cases are _syntactically_ valid before Python 3.9 because the `with` item + // # is parsed as a tuple, but this will always cause a runtime error, so we flag it + // # anyway + // with (foo, bar): ... + // with ( + // foo, + // bar, + // baz, + // ): ... + // with (foo,): ... + + // test_ok parenthesized_context_manager_py39 + // # parse_options: {"target-version": "3.9"} + // with (foo as x, bar as y): ... + // with (foo, bar as y): ... + // with (foo as x, bar): ... + + // test_err parenthesized_context_manager_py38 + // # parse_options: {"target-version": "3.8"} + // with (foo as x, bar as y): ... + // with (foo, bar as y): ... + // with (foo as x, bar): ... + if items.len() > 1 || has_trailing_comma { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::ParenthesizedContextManager, + open_paren_range, + ); + } + + self.expect(TokenKind::Rpar); + items + } else { + // test_ok ambiguous_lpar_with_items_if_expr + // with (x) if True else y: ... + // with (x for x in iter) if True else y: ... + // with (x async for x in iter) if True else y: ... + // with (x)[0] if True else y: ... + + // test_ok ambiguous_lpar_with_items_binary_expr + // # It doesn't matter what's inside the parentheses, these tests need to make sure + // # all binary expressions parses correctly. + // with (a) and b: ... + // with (a) is not b: ... + // # Make sure precedence works + // with (a) or b and c: ... + // with (a) and b or c: ... + // with (a | b) << c | d: ... + // # Postfix should still be parsed first + // with (a)[0] + b * c: ... + self.parse_comma_separated_list_into_vec_with_capacity( + RecoveryContextKind::WithItems(WithItemKind::ParenthesizedExpression), + |p| p.parse_with_item(WithItemParsingState::Regular).item, + 1, + ) + } + } else { + self.parse_comma_separated_list_into_vec_with_capacity( + RecoveryContextKind::WithItems(WithItemKind::Unparenthesized), + |p| p.parse_with_item(WithItemParsingState::Regular).item, + 1, + ) + } + } + + /// Try parsing with-items coming after an ambiguous `(` token. + /// + /// To understand the ambiguity, consider the following example: + /// + /// ```python + /// with (item1, item2): ... # Parenthesized with items + /// with (item1, item2) as f: ... # Parenthesized expression + /// ``` + /// + /// When the parser is at the `(` token after the `with` keyword, it doesn't know if `(` is + /// used to parenthesize the with items or if it's part of a parenthesized expression of the + /// first with item. The challenge here is that until the parser sees the matching `)` token, + /// it can't resolve the ambiguity. + /// + /// This method resolves the ambiguity using speculative parsing. It starts with an assumption + /// that it's a parenthesized with items. Then, once it finds the matching `)`, it checks if + /// the assumption still holds true. If the initial assumption was correct, this will return + /// the parsed with items. Otherwise, rewind the parser back to the starting `(` token, + /// returning [`None`]. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `(` token. + /// + /// See: + fn try_parse_parenthesized_with_items(&mut self) -> (Option>, bool) { + let checkpoint = self.checkpoint(); + + // We'll start with the assumption that the with items are parenthesized. + let mut with_item_kind = WithItemKind::Parenthesized; + + self.bump(TokenKind::Lpar); + + let mut parsed_with_items = Vec::with_capacity(1); + let mut has_optional_vars = false; + + // test_err with_items_parenthesized_missing_comma + // with (item1 item2): ... + // with (item1 as f1 item2): ... + // with (item1, item2 item3, item4): ... + // with (item1, item2 as f1 item3, item4): ... + // with (item1, item2: ... + let has_trailing_comma = + self.parse_comma_separated_list(RecoveryContextKind::WithItems(with_item_kind), |p| { + let parsed_with_item = p.parse_with_item(WithItemParsingState::Speculative); + has_optional_vars |= parsed_with_item.item.optional_vars.is_some(); + parsed_with_items.push(parsed_with_item); + }); + + // Check if our assumption is incorrect and it's actually a parenthesized expression. + if has_optional_vars { + // If any of the with item has optional variables, then our assumption is correct + // and it is a parenthesized with items. Now, we need to restrict the grammar for a + // with item's context expression which is: + // + // with_item: expression ... + // + // So, named, starred and yield expressions not allowed. + for parsed_with_item in &parsed_with_items { + if parsed_with_item.is_parenthesized { + // Parentheses resets the precedence. + continue; + } + let error = match parsed_with_item.item.context_expr { + Expr::Named(_) => ParseErrorType::UnparenthesizedNamedExpression, + Expr::Starred(_) => ParseErrorType::InvalidStarredExpressionUsage, + Expr::Yield(_) | Expr::YieldFrom(_) => { + ParseErrorType::InvalidYieldExpressionUsage + } + _ => continue, + }; + self.add_error(error, &parsed_with_item.item.context_expr); + } + } else if self.at(TokenKind::Rpar) + && ( + // test_err with_items_parenthesized_missing_colon + // # `)` followed by a newline + // with (item1, item2) + // pass + matches!(self.peek(), TokenKind::Colon | TokenKind::Newline) + ) + { + if parsed_with_items.is_empty() { + // No with items, treat it as a parenthesized expression to create an empty + // tuple expression. + with_item_kind = WithItemKind::ParenthesizedExpression; + } else { + // These expressions, if unparenthesized, are only allowed if it's a + // parenthesized expression and none of the with items have an optional + // variable. + if parsed_with_items.iter().any(|parsed_with_item| { + !parsed_with_item.is_parenthesized + && matches!( + parsed_with_item.item.context_expr, + Expr::Named(_) | Expr::Starred(_) | Expr::Yield(_) | Expr::YieldFrom(_) + ) + }) { + with_item_kind = WithItemKind::ParenthesizedExpression; + } + } + } else { + // For any other token followed by `)`, if any of the items has an optional + // variables (`as ...`), then our assumption is correct. Otherwise, treat + // it as a parenthesized expression. For example: + // + // ```python + // with (item1, item2 as f): ... + // ``` + // + // This also helps in raising the correct syntax error for the following + // case: + // ```python + // with (item1, item2 as f) as x: ... + // # ^^ + // # Expecting `:` but got `as` + // ``` + with_item_kind = WithItemKind::ParenthesizedExpression; + } + + let with_items = if with_item_kind.is_parenthesized() { + let with_items: Vec<_> = parsed_with_items + .into_iter() + .map(|parsed_with_item| parsed_with_item.item) + .collect(); + + Some(with_items) + } else { + self.rewind(checkpoint); + + None + }; + + (with_items, has_trailing_comma) + } + + /// Parses a single `with` item. + /// + /// See: + fn parse_with_item(&mut self, state: WithItemParsingState) -> ParsedWithItem { + let start = self.node_start(); + + // The grammar for the context expression of a with item depends on the state + // of with item parsing. + let context_expr = match state { + WithItemParsingState::Speculative => { + // If it's in a speculative state, the parenthesis (`(`) could be part of any of the + // following expression: + // + // Tuple expression - star_named_expression + // Generator expression - named_expression + // Parenthesized expression - (yield_expr | named_expression) + // Parenthesized with items - expression + // + // Here, the right side specifies the grammar for an element corresponding to the + // expression mentioned in the left side. + // + // So, the grammar used should be able to parse an element belonging to any of the + // above expression. At a later point, once the parser understands where the + // parenthesis belongs to, it'll validate and report errors for any invalid expression + // usage. + // + // Thus, we can conclude that the grammar used should be: + // (yield_expr | star_named_expression) + self.parse_named_expression_or_higher( + ExpressionContext::yield_or_starred_bitwise_or(), + ) + } + WithItemParsingState::Regular => self.parse_conditional_expression_or_higher(), + }; + + let optional_vars = self + .at(TokenKind::As) + .then(|| Box::new(self.parse_with_item_optional_vars().expr)); + + ParsedWithItem { + is_parenthesized: context_expr.is_parenthesized, + item: ast::WithItem { + range: self.node_range(start), + context_expr: context_expr.expr, + optional_vars, + node_index: AtomicNodeIndex::NONE, + }, + } + } + + /// Parses the optional variables in a `with` item. + /// + /// # Panics + /// + /// If the parser isn't positioned at an `as` token. + fn parse_with_item_optional_vars(&mut self) -> ParsedExpr { + self.bump(TokenKind::As); + + let mut target = self + .parse_conditional_expression_or_higher_impl(ExpressionContext::starred_conditional()); + + // This has the same semantics as an assignment target. + self.validate_assignment_target(&target.expr); + + helpers::set_expr_ctx(&mut target.expr, ExprContext::Store); + + target + } + + /// Try parsing a `match` statement. + /// + /// This uses speculative parsing to remove the ambiguity of whether the `match` token is used + /// as a keyword or an identifier. This ambiguity arises only in if the `match` token is + /// followed by certain tokens. For example, if `match` is followed by `[`, we can't know if + /// it's used in the context of a subscript expression or as a list expression: + /// + /// ```python + /// # Subscript expression; `match` is an identifier + /// match[x] + /// + /// # List expression; `match` is a keyword + /// match [x, y]: + /// case [1, 2]: + /// pass + /// ``` + /// + /// This is done by parsing the subject expression considering `match` as a keyword token. + /// Then, based on certain heuristics we'll determine if our assumption is true. If so, we'll + /// continue parsing the entire match statement. Otherwise, return `None`. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `match` token. + /// + /// See: + fn try_parse_match_statement(&mut self) -> Option { + let checkpoint = self.checkpoint(); + + let start = self.node_start(); + self.bump(TokenKind::Match); + + let subject = self.parse_match_subject_expression(); + + match self.current_token_kind() { + // test_ok match_annotated_assignment + // match[0]: int + // match [x, y, z]: dict + TokenKind::Colon if self.peek() == TokenKind::Newline => { + // `match` is a keyword — colon followed by newline confirms + // this is a match statement, not an annotated assignment like + // `match [x, y, z]: {dict}` or `match[0]: int`. + self.bump(TokenKind::Colon); + + let cases = self.parse_match_body(start); + + Some(ast::StmtMatch { + subject: Box::new(subject), + cases, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + TokenKind::Newline if matches!(self.peek2(), (TokenKind::Indent, TokenKind::Case)) => { + // `match` is a keyword + + // test_err match_expected_colon + // match [1, 2] + // case _: ... + self.add_error( + ParseErrorType::ExpectedToken { + found: self.current_token_kind(), + expected: TokenKind::Colon, + }, + self.current_token_range(), + ); + + let cases = self.parse_match_body(start); + + Some(ast::StmtMatch { + subject: Box::new(subject), + cases, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + }) + } + _ => { + // `match` is an identifier + self.rewind(checkpoint); + + None + } + } + } + + /// Parses a match statement. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `match` token. + /// + /// See: + fn parse_match_statement(&mut self) -> ast::StmtMatch { + let start = self.node_start(); + self.bump(TokenKind::Match); + + let match_range = self.node_range(start); + + let subject = self.parse_match_subject_expression(); + self.expect(TokenKind::Colon); + + let cases = self.parse_match_body(start); + + // test_err match_before_py310 + // # parse_options: { "target-version": "3.9" } + // match 2: + // case 1: + // pass + + // test_ok match_after_py310 + // # parse_options: { "target-version": "3.10" } + // match 2: + // case 1: + // pass + + self.add_unsupported_syntax_error(UnsupportedSyntaxErrorKind::Match, match_range); + + ast::StmtMatch { + subject: Box::new(subject), + cases, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses the subject expression for a `match` statement. + fn parse_match_subject_expression(&mut self) -> Expr { + let start = self.node_start(); + + // Subject expression grammar is: + // + // subject_expr: + // | star_named_expression ',' star_named_expressions? + // | named_expression + // + // First try with `star_named_expression`, then if there's no comma, + // we'll restrict it to `named_expression`. + let subject = + self.parse_named_expression_or_higher(ExpressionContext::starred_bitwise_or()); + + // test_ok match_stmt_subject_expr + // match x := 1: + // case _: ... + // match (x := 1): + // case _: ... + // # Starred expressions are only allowed in tuple expression + // match *x | y, z: + // case _: ... + // match await x: + // case _: ... + + // test_err match_stmt_invalid_subject_expr + // match (*x): + // case _: ... + // # Starred expression precedence test + // match *x and y, z: + // case _: ... + // match yield x: + // case _: ... + if self.at(TokenKind::Comma) { + let tuple = self.parse_tuple_expression(subject.expr, start, Parenthesized::No, |p| { + p.parse_named_expression_or_higher(ExpressionContext::starred_bitwise_or()) + }); + + Expr::Tuple(tuple) + } else { + if subject.is_unparenthesized_starred_expr() { + // test_err match_stmt_single_starred_subject + // match *foo: + // case _: ... + self.add_error(ParseErrorType::InvalidStarredExpressionUsage, &subject); + } + subject.expr + } + } + + /// Parses the body of a `match` statement. + /// + /// This method expects that the parser is positioned at a `Newline` token. If not, it adds a + /// syntax error and continues parsing. + /// + /// `header_start` is the start of the `match` keyword. + fn parse_match_body(&mut self, header_start: TextSize) -> Vec { + // test_err match_stmt_no_newline_before_case + // match foo: case _: ... + self.expect(TokenKind::Newline); + + // Use `eat` instead of `expect` for better error message. + if !self.eat(TokenKind::Indent) { + // test_err match_stmt_expect_indented_block + // match foo: + // case _: ... + self.add_error( + ParseErrorType::ExpectedIndentedBlock { + clause: BlockClause::Match, + line: self.line_number(header_start), + }, + self.current_token_range(), + ); + } + + let cases = self.parse_match_case_blocks(); + + // TODO(dhruvmanila): Should we expect `Dedent` only if there was an `Indent` present? + self.expect(TokenKind::Dedent); + + cases + } + + /// Parses a list of match case blocks. + fn parse_match_case_blocks(&mut self) -> Vec { + let mut cases = vec![]; + + if !self.at(TokenKind::Case) { + // test_err match_stmt_expected_case_block + // match x: + // x = 1 + // match x: + // match y: + // case _: ... + self.add_error( + ParseErrorType::OtherError("expected `case` block".to_string()), + self.current_token_range(), + ); + return cases; + } + + let mut progress = ParserProgress::default(); + + while self.at(TokenKind::Case) { + progress.assert_progressing(self); + cases.push(self.parse_match_case()); + } + + cases + } + + /// Parses a single match case block. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `case` token. + /// + /// See: + fn parse_match_case(&mut self) -> ast::MatchCase { + let start = self.node_start(); + self.bump(TokenKind::Case); + + // test_err match_stmt_missing_pattern + // match x: + // case : ... + let pattern = self.parse_match_patterns(); + + let guard = if self.eat(TokenKind::If) { + if self.at_expr() { + // test_ok match_stmt_valid_guard_expr + // match x: + // case y if a := 1: ... + // match x: + // case y if a if True else b: ... + // match x: + // case y if lambda a: b: ... + // match x: + // case y if (yield x): ... + + // test_err match_stmt_invalid_guard_expr + // match x: + // case y if *a: ... + // match x: + // case y if (*a): ... + // match x: + // case y if yield x: ... + Some(Box::new( + self.parse_named_expression_or_higher(ExpressionContext::default()) + .expr, + )) + } else { + // test_err match_stmt_missing_guard_expr + // match x: + // case y if: ... + self.add_error( + ParseErrorType::ExpectedExpression, + self.current_token_range(), + ); + None + } + } else { + None + }; + + self.expect(TokenKind::Colon); + + // test_err case_expect_indented_block + // match subject: + // case 1: + // case 2: ... + let body = self.parse_body(BlockClause::Case, start); + + ast::MatchCase { + pattern, + guard, + body, + range: self.node_range(start), + node_index: AtomicNodeIndex::NONE, + runtime_body: None, + } + } + + /// Parses a statement that is valid after an `async` token. + /// + /// If the statement is not a valid `async` statement, an error will be reported + /// and it will be parsed as a statement. + /// + /// See: + /// - + /// - + /// - + fn parse_async_statement(&mut self) -> Stmt { + let async_start = self.node_start(); + self.bump(TokenKind::Async); + + match self.current_token_kind() { + // test_ok async_function_definition + // async def foo(): ... + TokenKind::Def => Stmt::FunctionDef(ast::StmtFunctionDef { + is_async: true, + ..self.parse_function_definition(DecoratorList::new(), async_start) + }), + + // test_ok async_with_statement + // async with item: ... + TokenKind::With => Stmt::With(ast::StmtWith { + is_async: true, + ..self.parse_with_statement(async_start) + }), + + // test_ok async_for_statement + // async for target in iter: ... + TokenKind::For => Stmt::For(ast::StmtFor { + is_async: true, + ..self.parse_for_statement(async_start) + }), + + kind => { + // test_err async_unexpected_token + // async class Foo: ... + // async while test: ... + // async x = 1 + // async async def foo(): ... + // async match test: + // case _: ... + self.add_error( + ParseErrorType::UnexpectedTokenAfterAsync(kind), + self.current_token_range(), + ); + + // Although this statement is not a valid `async` statement, + // we still parse it. Guard the recursive recovery path so + // `async async async ...` cannot overflow the parser stack. + self.with_recursion(Self::parse_statement) + } + } + } + + /// Parses a decorator list followed by a class, function or async function definition. + /// + /// See: + fn parse_decorators(&mut self) -> Stmt { + let start = self.node_start(); + + let mut decorators = DecoratorList::new(); + let mut progress = ParserProgress::default(); + + // test_err decorator_missing_expression + // @def foo(): ... + // @ + // def foo(): ... + // @@ + // def foo(): ... + // @test + // @ + // class Test + while self.at(TokenKind::At) { + progress.assert_progressing(self); + + let decorator_start = self.node_start(); + self.bump(TokenKind::At); + + let parsed_expr = if self.at(TokenKind::Def) || self.at(TokenKind::Class) { + Expr::Name(self.parse_missing_name()).into() + } else { + self.parse_named_expression_or_higher(ExpressionContext::default()) + }; + + if self.options.target_version < PythonVersion::PY39 { + // test_ok decorator_expression_dotted_ident_py38 + // # parse_options: { "target-version": "3.8" } + // @buttons.clicked.connect + // def spam(): ... + + // test_ok decorator_expression_identity_hack_py38 + // # parse_options: { "target-version": "3.8" } + // def _(x): return x + // @_(buttons[0].clicked.connect) + // def spam(): ... + + // test_ok decorator_expression_eval_hack_py38 + // # parse_options: { "target-version": "3.8" } + // @eval("buttons[0].clicked.connect") + // def spam(): ... + + // test_ok decorator_expression_py39 + // # parse_options: { "target-version": "3.9" } + // @buttons[0].clicked.connect + // def spam(): ... + // @(x := lambda x: x)(foo) + // def bar(): ... + + // test_err decorator_expression_py38 + // # parse_options: { "target-version": "3.8" } + // @buttons[0].clicked.connect + // def spam(): ... + + // test_err decorator_named_expression_py37 + // # parse_options: { "target-version": "3.7" } + // @(x := lambda x: x)(foo) + // def bar(): ... + + // test_err decorator_dict_literal_py38 + // # parse_options: { "target-version": "3.8" } + // @{3: 3} + // def bar(): ... + + // test_err decorator_float_literal_py38 + // # parse_options: { "target-version": "3.8" } + // @3.14 + // def bar(): ... + + // test_ok decorator_await_expression_py39 + // # parse_options: { "target-version": "3.9" } + // async def foo(): + // @await bar + // def baz(): ... + + // test_err decorator_await_expression_py38 + // # parse_options: { "target-version": "3.8" } + // async def foo(): + // @await bar + // def baz(): ... + + // test_err decorator_non_toplevel_call_expression_py38 + // # parse_options: { "target-version": "3.8" } + // @foo().bar() + // def baz(): ... + + let relaxed_decorator_error = match &parsed_expr.expr { + Expr::Call(expr_call) => { + helpers::detect_invalid_pre_py39_decorator_node(&expr_call.func) + } + expr => helpers::detect_invalid_pre_py39_decorator_node(expr), + }; + + if let Some((error, range)) = relaxed_decorator_error { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::RelaxedDecorator(error), + range, + ); + } + } + + // test_err decorator_invalid_expression + // @*x + // @(*x) + // @((*x)) + // @yield x + // @yield from x + // def foo(): ... + + decorators.push(ast::Decorator { + expression: parsed_expr.expr, + range: self.node_range(decorator_start), + node_index: AtomicNodeIndex::NONE, + }); + + // test_err decorator_missing_newline + // @x def foo(): ... + // @x async def foo(): ... + // @x class Foo: ... + self.expect(TokenKind::Newline); + } + + decorators.shrink_to_fit(); + + match self.current_token_kind() { + TokenKind::Def => Stmt::FunctionDef(self.parse_function_definition(decorators, start)), + TokenKind::Class => Stmt::ClassDef(self.parse_class_definition(decorators, start)), + TokenKind::Async if self.peek() == TokenKind::Def => { + self.bump(TokenKind::Async); + + // test_ok decorator_async_function + // @decorator + // async def foo(): ... + Stmt::FunctionDef(ast::StmtFunctionDef { + is_async: true, + ..self.parse_function_definition(decorators, start) + }) + } + _ => { + // test_err decorator_unexpected_token + // @foo + // async with x: ... + // @foo + // x = 1 + self.add_error( + ParseErrorType::OtherError( + "expected class, function definition or async function definition \ + after decorator" + .to_string(), + ), + self.current_token_range(), + ); + + let range = self.node_range(start); + + ast::StmtFunctionDef { + node_index: AtomicNodeIndex::default(), + range, + is_async: false, + decorator_list: decorators, + name: ast::Identifier { + id: Name::empty(), + range: self.missing_node_range(), + node_index: AtomicNodeIndex::NONE, + }, + type_params: None, + parameters: Box::new(ast::Parameters { + range: self.missing_node_range(), + ..ast::Parameters::default() + }), + returns: None, + body: Suite::new(), + runtime_decorator_list: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + runtime_body: None, + } + .into() + } + } + } + + /// Parses the body of the given [`BlockClause`]. + /// + /// This could either be a single statement that's on the same line as the + /// clause header or an indented block. + /// + /// `header_start` is the start of the clause's header keyword token. + pub(super) fn parse_body(&mut self, clause: BlockClause, header_start: TextSize) -> Suite { + // Note: The test cases in this method chooses a clause at random to test + // the error logic. + + let newline_range = self.current_token_range(); + if self.eat(TokenKind::Newline) { + if self.at(TokenKind::Indent) { + return self.parse_block(); + } + // test_err clause_expect_indented_block + // # Here, the error is highlighted at the `pass` token + // if True: + // pass + // # The parser is at the end of the program, so let's highlight + // # at the newline token after `:` + // if True: + self.add_error( + ParseErrorType::ExpectedIndentedBlock { + clause, + line: self.line_number(header_start), + }, + if self.current_token_range().is_empty() { + newline_range + } else { + self.current_token_range() + }, + ); + } else { + if self.at_simple_stmt() { + return self.parse_simple_statements(); + } + // test_err clause_expect_single_statement + // if True: if True: pass + self.add_error( + ParseErrorType::OtherError("expected a simple statement".to_string()), + self.current_token_range(), + ); + } + + Suite::new() + } + + /// Parses a block of statements. + /// + /// # Panics + /// + /// If the parser isn't positioned at an `Indent` token. + fn parse_block(&mut self) -> Suite { + self.bump(TokenKind::Indent); + + let statements = self.with_recursion(|parser| { + let snapshot = parser.stmt_scratch.snapshot(); + parser.parse_list(RecoveryContextKind::BlockStatements, |parser| { + let statement = parser.parse_statement(); + parser.stmt_scratch.push(statement); + }); + + parser.stmt_scratch.take_thin_vec(snapshot) + }); + + self.expect(TokenKind::Dedent); + + statements + } + + /// Parses a single parameter for the given function kind. + /// + /// Matches either the `param_no_default_star_annotation` or `param_no_default` + /// rule in the [Python grammar] depending on whether star annotation is allowed + /// or not. + /// + /// Use [`Parser::parse_parameter_with_default`] to allow parameter with default + /// values. + /// + /// [Python grammar]: https://docs.python.org/3/reference/grammar.html + fn parse_parameter( + &mut self, + start: TextSize, + function_kind: FunctionKind, + allow_star_annotation: AllowStarAnnotation, + ) -> ast::Parameter { + let name = self.parse_identifier(); + + // Annotations are only allowed for function definition. For lambda expression, + // the `:` token would indicate its body. + let annotation = match function_kind { + FunctionKind::FunctionDef if self.eat(TokenKind::Colon) => { + if self.at_expr() { + let parsed_expr = match allow_star_annotation { + AllowStarAnnotation::Yes => { + // test_ok param_with_star_annotation + // def foo(*args: *int | str): ... + // def foo(*args: *(int or str)): ... + + // test_err param_with_invalid_star_annotation + // def foo(*args: *): ... + // def foo(*args: (*tuple[int])): ... + // def foo(*args: *int or str): ... + // def foo(*args: *yield x): ... + // # def foo(*args: **int): ... + let parsed_expr = self.parse_conditional_expression_or_higher_impl( + ExpressionContext::starred_bitwise_or(), + ); + + // test_ok param_with_star_annotation_py311 + // # parse_options: {"target-version": "3.11"} + // def foo(*args: *Ts): ... + + // test_ok param_with_star_annotation_py310 + // # parse_options: {"target-version": "3.10"} + // # regression tests for https://github.com/astral-sh/ruff/issues/16874 + // # starred parameters are fine, just not the annotation + // from typing import Annotated, Literal + // def foo(*args: Ts): ... + // def foo(*x: Literal["this should allow arbitrary strings"]): ... + // def foo(*x: Annotated[str, "this should allow arbitrary strings"]): ... + // def foo(*args: str, **kwds: int): ... + // def union(*x: A | B): ... + + // test_err param_with_star_annotation_py310 + // # parse_options: {"target-version": "3.10"} + // def foo(*args: *Ts): ... + if parsed_expr.is_starred_expr() { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::StarAnnotation, + parsed_expr.range(), + ); + } + + parsed_expr + } + AllowStarAnnotation::No => { + // test_ok param_with_annotation + // def foo(arg: int): ... + // def foo(arg: lambda x: x): ... + + // test_err param_with_invalid_annotation + // def foo(arg: *int): ... + // def foo(arg: yield int): ... + // def foo(arg: x := int): ... + self.parse_conditional_expression_or_higher() + } + }; + Some(Box::new(parsed_expr.expr)) + } else { + // test_err param_missing_annotation + // def foo(x:): ... + // def foo(x:,): ... + self.add_error( + ParseErrorType::ExpectedExpression, + self.current_token_range(), + ); + None + } + } + _ => None, + }; + + ast::Parameter { + range: self.node_range(start), + name, + annotation, + node_index: AtomicNodeIndex::NONE, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + } + } + + /// Parses a parameter with an optional default expression. + /// + /// Matches the `param_maybe_default` rule in the [Python grammar]. + /// + /// This method doesn't allow star annotation. Use [`Parser::parse_parameter`] + /// instead. + /// + /// [Python grammar]: https://docs.python.org/3/reference/grammar.html + fn parse_parameter_with_default( + &mut self, + start: TextSize, + function_kind: FunctionKind, + ) -> ast::ParameterWithDefault { + let parameter = self.parse_parameter(start, function_kind, AllowStarAnnotation::No); + + let equal_range = self.current_token_range(); + let default = if self.eat(TokenKind::Equal) { + if self.at_expr() { + // test_ok param_with_default + // def foo(x=lambda y: y): ... + // def foo(x=1 if True else 2): ... + // def foo(x=await y): ... + // def foo(x=(yield y)): ... + + // test_err param_with_invalid_default + // def foo(x=*int): ... + // def foo(x=(*int)): ... + // def foo(x=yield y): ... + Some(Box::new(self.parse_conditional_expression_or_higher().expr)) + } else { + // test_err param_missing_default + // def foo(x=): ... + // def foo(x: int = ): ... + if self.at(TokenKind::Rpar) || self.at(TokenKind::Comma) { + self.add_error( + ParseErrorType::OtherError("expected default value expression".to_string()), + equal_range, + ); + } else { + self.add_error( + ParseErrorType::ExpectedExpression, + self.current_token_range(), + ); + } + None + } + } else { + None + }; + + ast::ParameterWithDefault { + range: self.node_range(start), + parameter, + default, + node_index: AtomicNodeIndex::NONE, + } + } + + /// Parses a parameter list for the given function kind. + /// + /// See: + pub(super) fn parse_parameters(&mut self, function_kind: FunctionKind) -> ast::Parameters { + let start = self.node_start(); + + if matches!(function_kind, FunctionKind::FunctionDef) { + self.expect(TokenKind::Lpar); + } + + // TODO(dhruvmanila): This has the same problem as `parse_match_pattern_mapping` + // has where if there are multiple kwarg or vararg, the last one will win and + // the parser will drop the previous ones. Another thing is the vararg and kwarg + // uses `Parameter` (not `ParameterWithDefault`) which means that the parser cannot + // recover well from `*args=(1, 2)`. + let mut parameters = ast::Parameters::default(); + let parameters_snapshot = self.parameter_scratch.snapshot(); + let mut args_snapshot = None; + let mut kwonlyargs_snapshot = None; + + let mut seen_default_param = false; // `a=10` + let mut seen_positional_only_separator = false; // `/` + let mut seen_keyword_only_separator = false; // `*` + let mut seen_keyword_only_param_after_separator = false; + + // Whether only parameters without a default have been seen. + let mut only_parameters_without_default = true; + + self.parse_comma_separated_list(RecoveryContextKind::Parameters(function_kind), |parser| { + let param_start = parser.node_start(); + + if parameters.kwarg.is_some() { + // test_err params_follows_var_keyword_param + // def foo(**kwargs, a, /, b=10, *, *args): ... + parser.add_error( + ParseErrorType::ParamAfterVarKeywordParam, + parser.current_token_range(), + ); + } + + let only_parameters_without_default_before = only_parameters_without_default; + only_parameters_without_default = false; + + match parser.current_token_kind() { + TokenKind::Lpar => { + // test_err params_parenthesized + // def foo(a, (b, c)): ... + // def foo((a)): ... + // lambda (a,): ... + let range = parser + .parenthesized_parameters_range(function_kind) + .filter(|_| only_parameters_without_default_before); + let message = if range.is_none() { + "invalid syntax" + } else if matches!(function_kind, FunctionKind::Lambda) { + "Lambda expression parameters cannot be parenthesized" + } else { + "Function parameters cannot be parenthesized" + }; + let lpar_range = parser.current_token_range(); + parser.add_error( + ParseErrorType::OtherError(message.to_string()), + range.unwrap_or(lpar_range), + ); + parser.bump(TokenKind::Lpar); + } + TokenKind::Star => { + let star_range = parser.current_token_range(); + parser.bump(TokenKind::Star); + + kwonlyargs_snapshot.get_or_insert_with(|| parser.parameter_scratch.snapshot()); + + if parser.at_identifier_or_soft_keyword() { + let param = parser.parse_parameter( + param_start, + function_kind, + AllowStarAnnotation::Yes, + ); + + // A starred annotation is not an annotation the rule accepts. + if parser.at(TokenKind::Equal) + && !param + .annotation + .as_deref() + .is_some_and(ast::Expr::is_starred_expr) + { + // test_err params_var_positional_with_default + // def foo(a, *args=(1, 2)): ... + parser.add_error( + ParseErrorType::VarParameterWithDefault, + parser.current_token_range(), + ); + } + + if seen_keyword_only_separator || parameters.vararg.is_some() { + // test_err params_multiple_varargs + // def foo(a, *, *args, b): ... + // # def foo(a, *, b, c, *args): ... + // def foo(a, *args1, *args2, b): ... + // def foo(a, *args1, b, c, *args2): ... + let message = if cfg!(feature = "python315-diagnostics") { + "* may appear only once" + } else { + "* argument may appear only once" + }; + parser.add_error( + ParseErrorType::OtherError(message.to_string()), + star_range, + ); + } + + // TODO(dhruvmanila): The AST doesn't allow multiple `vararg`, so let's + // choose to keep the first one so that the parameters remain in preorder. + if parameters.vararg.is_none() { + parameters.vararg = Some(Box::new(param)); + } + } else { + if seen_keyword_only_separator || parameters.vararg.is_some() { + // test_err params_multiple_star_separator + // def foo(a, *, *, b): ... + // def foo(a, *, b, c, *): ... + + // test_err params_star_separator_after_star_param + // def foo(a, *args, *, b): ... + // def foo(a, *args, b, c, *): ... + let message = if parser.at(TokenKind::Comma) { + if cfg!(feature = "python315-diagnostics") { + "* may appear only once" + } else { + "* argument may appear only once" + } + } else { + "invalid syntax" + }; + parser.add_error( + ParseErrorType::OtherError(message.to_string()), + star_range, + ); + } else if let Some(range) = + parser.bare_star_error_range(function_kind, star_range) + { + // test_err params_expected_after_star_separator + // def foo(*): ... + // def foo(*,): ... + // def foo(a, *): ... + // def foo(a, *,): ... + // def foo(*, **kwargs): ... + parser.add_error(ParseErrorType::ExpectedKeywordParam, range); + } + + seen_keyword_only_separator = true; + } + } + TokenKind::DoubleStar => { + let double_star_range = parser.current_token_range(); + parser.bump(TokenKind::DoubleStar); + + let param = + parser.parse_parameter(param_start, function_kind, AllowStarAnnotation::No); + let param_double_star_range = parser.node_range(double_star_range.start()); + + if parameters.kwarg.is_some() { + // test_err params_multiple_kwargs + // def foo(a, **kwargs1, **kwargs2): ... + parser.add_error( + ParseErrorType::OtherError( + "only one '**' parameter allowed".to_string(), + ), + param_double_star_range, + ); + } + + if parser.at(TokenKind::Equal) { + // test_err params_var_keyword_with_default + // def foo(a, **kwargs={'b': 1, 'c': 2}): ... + parser.add_error( + ParseErrorType::VarKeywordParameterWithDefault, + parser.current_token_range(), + ); + } + + // test_ok params_seen_keyword_only_param_after_star + // def foo(*, a, **kwargs): ... + // def foo(*, a=10, **kwargs): ... + + // test_err params_kwarg_after_star_separator + // def foo(*, **kwargs): ... + parameters.kwarg = Some(Box::new(param)); + } + TokenKind::Slash => { + let slash_range = parser.current_token_range(); + parser.bump(TokenKind::Slash); + + if parser.at(TokenKind::Star) + && !parser.parameter_scratch.is_empty(¶meters_snapshot) + { + // test_err params_star_after_slash_without_comma + // def foo(a, /*, b): ... + // lambda a, /*, b: ... + parser.add_error( + ParseErrorType::OtherError( + "expected comma between / and *".to_string(), + ), + parser.current_token_range(), + ); + } + + if parser.parameter_scratch.is_empty(¶meters_snapshot) + && parameters.vararg.is_none() + && parameters.kwarg.is_none() + { + // test_err params_no_arg_before_slash + // def foo(/): ... + // def foo(/, a): ... + let message = if cfg!(feature = "python315-diagnostics") { + "at least one parameter must precede /" + } else { + "at least one argument must precede /" + }; + parser.add_error( + ParseErrorType::OtherError(message.to_string()), + slash_range, + ); + } + + if seen_positional_only_separator { + // test_err params_multiple_slash_separator + // def foo(a, /, /, b): ... + // def foo(a, /, b, c, /): ... + parser.add_error( + ParseErrorType::OtherError("/ may appear only once".to_string()), + slash_range, + ); + } + + if seen_keyword_only_separator || parameters.vararg.is_some() { + // test_err params_star_after_slash + // def foo(*a, /): ... + // def foo(a, *args, b, /): ... + // def foo(a, *, /, b): ... + // def foo(a, *, b, c, /, d): ... + parser.add_error( + ParseErrorType::OtherError("/ must be ahead of *".to_string()), + slash_range, + ); + } + + if !seen_positional_only_separator { + // We should only split if we're seeing the separator for the + // first time, otherwise it's a user error. + if kwonlyargs_snapshot.is_none() { + args_snapshot = Some(parser.parameter_scratch.snapshot()); + } + seen_positional_only_separator = true; + + // test_ok pos_only_py38 + // # parse_options: {"target-version": "3.8"} + // def foo(a, /): ... + + // test_err pos_only_py37 + // # parse_options: {"target-version": "3.7"} + // def foo(a, /): ... + // def foo(a, /, b, /): ... + // def foo(a, *args, /, b): ... + // def foo(a, //): ... + parser.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::PositionalOnlyParameter, + slash_range, + ); + } + } + _ if parser.at_identifier_or_soft_keyword() => { + let param = parser.parse_parameter_with_default(param_start, function_kind); + + // TODO(dhruvmanila): Pyright seems to only highlight the first non-default argument + // https://github.com/microsoft/pyright/blob/3b70417dd549f6663b8f86a76f75d8dfd450f4a8/packages/pyright-internal/src/parser/parser.ts#L2038-L2042 + if param.default.is_none() + && seen_default_param + && !seen_keyword_only_separator + && parameters.vararg.is_none() + { + // test_ok params_non_default_after_star + // def foo(a=10, *, b, c=11, d): ... + // def foo(a=10, *args, b, c=11, d): ... + + // test_err params_non_default_after_default + // def foo(a=10, b, c: int): ... + parser.add_error(ParseErrorType::NonDefaultParamAfterDefaultParam, ¶m); + } + + seen_default_param |= param.default.is_some(); + only_parameters_without_default = + only_parameters_without_default_before && param.default.is_none(); + + if seen_keyword_only_separator { + seen_keyword_only_param_after_separator = true; + } + + parser.parameter_scratch.push(param); + } + _ => { + // This corresponds to the expected token kinds for `is_list_element`. + unreachable!("Expected identifier, '*', '**', or '/'"); + } + } + }); + + if matches!(function_kind, FunctionKind::FunctionDef) { + self.expect(TokenKind::Rpar); + } + + if let Some(kwonlyargs_snapshot) = kwonlyargs_snapshot { + parameters.kwonlyargs = self.parameter_scratch.take_thin_vec(kwonlyargs_snapshot); + } + if let Some(args_snapshot) = args_snapshot { + parameters.args = self.parameter_scratch.take_thin_vec(args_snapshot); + parameters.posonlyargs = self.parameter_scratch.take_thin_vec(parameters_snapshot); + } else if seen_positional_only_separator { + parameters.posonlyargs = self.parameter_scratch.take_thin_vec(parameters_snapshot); + } else { + parameters.args = self.parameter_scratch.take_thin_vec(parameters_snapshot); + } + + parameters.range = self.node_range(start); + + parameters + } + + /// Parses and reports a bound or constraints after a `TypeVarTuple` or `ParamSpec` name, if + /// there is one. The AST has no place for them. + fn parse_variadic_type_param_bound(&mut self, kind: &str) { + if !self.at(TokenKind::Colon) + || !(EXPR_SET.contains(self.peek()) || self.peek().is_soft_keyword()) + { + return; + } + let start = self.current_token_range().start(); + self.bump(TokenKind::Colon); + let bound = self.parse_conditional_expression_or_higher(); + let what = if bound.expr.is_tuple_expr() { + "constraints" + } else { + "bound" + }; + self.add_error( + ParseErrorType::OtherError(format!("cannot use {what} with {kind}")), + TextRange::new(start, bound.end()), + ); + } + + /// Returns the range of the error for a bare `*` separator not followed by a keyword-only + /// parameter, if there is one. The parser is positioned right after the `*`. + fn bare_star_error_range( + &mut self, + function_kind: FunctionKind, + star_range: TextRange, + ) -> Option { + let terminator = function_kind.list_terminator(); + if self.at(terminator) { + return Some(match function_kind { + FunctionKind::FunctionDef => star_range, + FunctionKind::Lambda => self.current_token_range(), + }); + } + if !self.at(TokenKind::Comma) + || !matches!(self.peek(), kind if kind == terminator || kind == TokenKind::DoubleStar) + { + return None; + } + Some(match function_kind { + FunctionKind::FunctionDef => star_range, + FunctionKind::Lambda => { + let checkpoint = self.checkpoint(); + self.bump(TokenKind::Comma); + let range = self.current_token_range(); + self.rewind(checkpoint); + range + } + }) + } + + /// Returns the range from the current `(` to its `)` if they enclose only parameters + /// without a default (or names, for a lambda), separated by commas. + fn parenthesized_parameters_range(&mut self, function_kind: FunctionKind) -> Option { + let checkpoint = self.checkpoint(); + let start = self.current_token_range().start(); + self.bump(TokenKind::Lpar); + let mut range = None; + while self.at_identifier_or_soft_keyword() { + self.bump_any(); + if matches!(function_kind, FunctionKind::FunctionDef) && self.eat(TokenKind::Colon) { + if !self.at_expr() { + break; + } + self.parse_conditional_expression_or_higher(); + } + let comma = self.eat(TokenKind::Comma); + if self.at(TokenKind::Rpar) { + range = Some(TextRange::new(start, self.current_token_range().end())); + break; + } + if !comma { + break; + } + } + self.rewind(checkpoint); + range + } + + /// Try to parse a type parameter list. If the parser is not at the start of a + /// type parameter list, return `None`. + /// + /// See: + fn try_parse_type_params(&mut self) -> Option { + self.at(TokenKind::Lsqb).then(|| self.parse_type_params()) + } + + /// Parses a type parameter list. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `[` token. + /// + /// See: + fn parse_type_params(&mut self) -> ast::TypeParams { + let start = self.node_start(); + self.bump(TokenKind::Lsqb); + + let type_params = self.parse_comma_separated_list_into_vec( + RecoveryContextKind::TypeParams, + Parser::parse_type_param, + ); + + if type_params.is_empty() { + // test_err type_params_empty + // def foo[](): + // pass + // type ListOrSet[] = list | set + self.add_error(ParseErrorType::EmptyTypeParams, self.current_token_range()); + } + + self.expect(TokenKind::Rsqb); + + ast::TypeParams { + range: self.node_range(start), + type_params, + node_index: AtomicNodeIndex::NONE, + runtime_type_params: None, + } + } + + /// Parses a type parameter. + /// + /// See: + fn parse_type_param(&mut self) -> ast::TypeParam { + let start = self.node_start(); + + // TODO(dhruvmanila): CPython throws an error if `TypeVarTuple` or `ParamSpec` + // has bounds: + // + // type X[*T: int] = int + // ^^^^^ + // SyntaxError: cannot use bound with TypeVarTuple + // + // We should do the same but currently we can't without throwing away the parsed + // expression because the AST can't contain it. + + // test_ok type_param_type_var_tuple + // type X[*Ts] = int + // type X[*Ts = int] = int + // type X[*Ts = *int] = int + // type X[T, *Ts] = int + // type X[T, *Ts = int] = int + if self.eat(TokenKind::Star) { + let name = self.parse_identifier(); + self.parse_variadic_type_param_bound("TypeVarTuple"); + + let default = if self.eat(TokenKind::Equal) { + if self.at_expr() { + // test_err type_param_type_var_tuple_invalid_default_expr + // type X[*Ts = *int] = int + // type X[*Ts = *int or str] = int + // type X[*Ts = yield x] = int + // type X[*Ts = yield from x] = int + // type X[*Ts = x := int] = int + Some(Box::new( + self.parse_conditional_expression_or_higher_impl( + ExpressionContext::starred_bitwise_or(), + ) + .expr, + )) + } else { + // test_err type_param_type_var_tuple_missing_default + // type X[*Ts =] = int + // type X[*Ts =, T2] = int + self.add_error( + ParseErrorType::ExpectedExpression, + self.current_token_range(), + ); + None + } + } else { + None + }; + + // test_err type_param_type_var_tuple_bound + // type X[*T: int] = int + ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { + range: self.node_range(start), + name, + default, + node_index: AtomicNodeIndex::NONE, + }) + + // test_ok type_param_param_spec + // type X[**P] = int + // type X[**P = int] = int + // type X[T, **P] = int + // type X[T, **P = int] = int + } else if self.eat(TokenKind::DoubleStar) { + let name = self.parse_identifier(); + self.parse_variadic_type_param_bound("ParamSpec"); + + let default = if self.eat(TokenKind::Equal) { + if self.at_expr() { + // test_err type_param_param_spec_invalid_default_expr + // type X[**P = *int] = int + // type X[**P = yield x] = int + // type X[**P = yield from x] = int + // type X[**P = x := int] = int + // type X[**P = *int] = int + Some(Box::new(self.parse_conditional_expression_or_higher().expr)) + } else { + // test_err type_param_param_spec_missing_default + // type X[**P =] = int + // type X[**P =, T2] = int + self.add_error( + ParseErrorType::ExpectedExpression, + self.current_token_range(), + ); + None + } + } else { + None + }; + + // test_err type_param_param_spec_bound + // type X[**T: int] = int + ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { + range: self.node_range(start), + name, + default, + node_index: AtomicNodeIndex::NONE, + }) + // test_ok type_param_type_var + // type X[T] = int + // type X[T = int] = int + // type X[T: int = int] = int + // type X[T: (int, int) = int] = int + // type X[T: int = int, U: (int, int) = int] = int + } else { + let name = self.parse_identifier(); + + let bound = if self.eat(TokenKind::Colon) { + if self.at_expr() { + // test_err type_param_invalid_bound_expr + // type X[T: *int] = int + // type X[T: yield x] = int + // type X[T: yield from x] = int + // type X[T: x := int] = int + Some(Box::new(self.parse_conditional_expression_or_higher().expr)) + } else { + // test_err type_param_missing_bound + // type X[T: ] = int + // type X[T1: , T2] = int + self.add_error( + ParseErrorType::ExpectedExpression, + self.current_token_range(), + ); + None + } + } else { + None + }; + + let equal_token_start = self.node_start(); + let default = if self.eat(TokenKind::Equal) { + if self.at_expr() { + // test_err type_param_type_var_invalid_default_expr + // type X[T = *int] = int + // type X[T = yield x] = int + // type X[T = (yield x)] = int + // type X[T = yield from x] = int + // type X[T = x := int] = int + // type X[T: int = *int] = int + Some(Box::new(self.parse_conditional_expression_or_higher().expr)) + } else { + // test_err type_param_type_var_missing_default + // type X[T =] = int + // type X[T: int =] = int + // type X[T1 =, T2] = int + self.add_error( + ParseErrorType::ExpectedExpression, + self.current_token_range(), + ); + None + } + } else { + None + }; + + // test_ok type_param_default_py313 + // # parse_options: {"target-version": "3.13"} + // type X[T = int] = int + // def f[T = int](): ... + // class C[T = int](): ... + + // test_err type_param_default_py312 + // # parse_options: {"target-version": "3.12"} + // type X[T = int] = int + // def f[T = int](): ... + // class C[T = int](): ... + // class D[S, T = int, U = uint](): ... + + if default.is_some() { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::TypeParamDefault, + self.node_range(equal_token_start), + ); + } + + ast::TypeParam::TypeVar(ast::TypeParamTypeVar { + range: self.node_range(start), + name, + bound, + default, + node_index: AtomicNodeIndex::NONE, + }) + } + } + + /// Returns the target of an invalid assignment that is reported as a possible comparison, and + /// the end of the expression it would be compared with. + /// + /// This is the last element of the first target (or the first target itself) when it is a + /// `bitwise_or` expression directly followed by `=`, and the expression after that `=` starts + /// with a `bitwise_or` expression that is not followed by `=` or `:=`. + fn comparison_like_assignment<'a>( + &self, + targets: &'a [Expr], + value: &Expr, + ) -> Option<(&'a Expr, TextSize)> { + let first = targets.first()?; + let target = match first { + Expr::Tuple(tuple) if !tuple.parenthesized => tuple.elts.last()?, + _ => first, + }; + if self.token_kind_after(self.parenthesized_end(target.range())) != TokenKind::Equal + || !self.is_bitwise_or(target) + || self.starts_with_display_or_constant(target) + { + return None; + } + let rhs = self.bitwise_or_prefix(targets.get(1).unwrap_or(value))?; + if matches!( + self.token_kind_after(self.parenthesized_end(rhs.range())), + TokenKind::Equal | TokenKind::ColonEqual + ) { + return None; + } + Some((target, rhs.end())) + } + + /// Returns `true` if `expr` is a `bitwise_or` expression or any parenthesized expression. + pub(super) fn is_bitwise_or(&self, expr: &Expr) -> bool { + if self.is_parenthesized(expr.range()) { + return true; + } + match expr { + Expr::Compare(_) + | Expr::BoolOp(_) + | Expr::Lambda(_) + | Expr::If(_) + | Expr::Named(_) + | Expr::Yield(_) + | Expr::YieldFrom(_) + | Expr::Starred(_) => false, + Expr::UnaryOp(unary) => unary.op != UnaryOp::Not, + Expr::Tuple(tuple) => tuple.parenthesized, + Expr::Generator(generator) => generator.parenthesized, + _ => true, + } + } + + /// Returns `true` if `expr` starts with a list or tuple display, a parenthesized generator + /// expression, `True`, `False` or `None`. + pub(super) fn starts_with_display_or_constant(&self, mut expr: &Expr) -> bool { + loop { + if self.is_parenthesized(expr.range()) { + return false; + } + expr = match expr { + Expr::Call(call) => &call.func, + Expr::Attribute(attribute) => &attribute.value, + Expr::Subscript(subscript) => &subscript.value, + Expr::BinOp(bin_op) => &bin_op.left, + Expr::List(_) | Expr::BooleanLiteral(_) | Expr::NoneLiteral(_) => return true, + Expr::Tuple(tuple) => return tuple.parenthesized, + Expr::Generator(generator) => return generator.parenthesized, + _ => return false, + }; + } + } + + /// Returns the `bitwise_or` expression that `expr` starts with, if any. + pub(super) fn bitwise_or_prefix<'a>(&self, mut expr: &'a Expr) -> Option<&'a Expr> { + loop { + if self.is_parenthesized(expr.range()) { + return Some(expr); + } + expr = match expr { + Expr::Tuple(tuple) if !tuple.parenthesized => tuple.elts.first()?, + Expr::Compare(compare) => compare.operands.first()?, + Expr::BoolOp(bool_op) => bool_op.values.first()?, + Expr::If(if_expr) => &if_expr.body, + _ if self.is_bitwise_or(expr) => return Some(expr), + _ => return None, + }; + } + } + + /// Validate that the given expression is a valid assignment target. + /// + /// If the expression is a list or tuple, then validate each element in the list. + /// If it's a starred expression, then validate the value of the starred expression. + /// + /// Report an error for each invalid assignment expression found. + pub(super) fn validate_assignment_target(&mut self, expr: &Expr) { + match expr { + Expr::Starred(ast::ExprStarred { value, .. }) => self.validate_assignment_target(value), + Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => { + for expr in elts { + self.validate_assignment_target(expr); + } + } + Expr::Name(_) | Expr::Attribute(_) | Expr::Subscript(_) => {} + _ => self.add_error( + ParseErrorType::InvalidAssignmentTarget { + kind: ExpressionKind::of(expr), + maybe_comparison: false, + }, + expr.range(), + ), + } + } + + /// Validate that the given expression is a valid target of a `for` statement or + /// comprehension. + /// + /// A comparison is not reported as an invalid target, because the `in` that follows the + /// target could belong to it. Without an invalid target, the token where the target stops + /// being one is reported instead. + pub(super) fn validate_for_target(&mut self, target: &Expr) { + let elements = match target { + Expr::Tuple(tuple) if !tuple.parenthesized => &tuple.elts[..], + _ => std::slice::from_ref(target), + }; + let mut comparison_end = None; + for element in elements { + match invalid_for_target(element) { + ForTarget::Valid => {} + ForTarget::Invalid(expr) => { + self.add_error( + ParseErrorType::InvalidAssignmentTarget { + kind: ExpressionKind::of(expr), + maybe_comparison: false, + }, + expr, + ); + return; + } + ForTarget::Comparison(compare) => { + // test_err for_stmt_comparison_target + // for x == y in z: ... + // for (x in y), z in w: ... + comparison_end.get_or_insert_with(|| match &compare.operands[..] { + [left, ..] + if compare.range() == element.range() + && !self.is_parenthesized(element.range()) => + { + self.parenthesized_end(left.range()) + } + _ => self.parenthesized_end(element.range()), + }); + } + } + } + if let Some(end) = comparison_end { + let (_, range) = self.token_after(end); + self.add_error( + ParseErrorType::OtherError("invalid syntax".to_string()), + range, + ); + } + } + + /// Validate that the given expression is a valid annotated assignment target. + /// + /// Unlike [`Parser::validate_assignment_target`], starred, list and tuple + /// expressions aren't allowed here. + fn validate_annotated_assignment_target(&mut self, expr: &Expr) { + match expr { + Expr::List(_) => self.add_error( + ParseErrorType::OtherError( + "only single target (not list) can be annotated".to_string(), + ), + expr, + ), + // An unparenthesized tuple is reported at its first element. + Expr::Tuple(tuple) => self.add_error( + ParseErrorType::OtherError( + "only single target (not tuple) can be annotated".to_string(), + ), + match tuple.elts.first() { + Some(first) if !tuple.parenthesized => first.range(), + _ => tuple.range(), + }, + ), + Expr::Name(_) | Expr::Attribute(_) | Expr::Subscript(_) => {} + _ => self.add_error(ParseErrorType::InvalidAnnotatedAssignmentTarget, expr), + } + } + + /// Validate that the given expression is a valid delete target. + /// + /// If the expression is a list or tuple, then validate each element in the list. + /// + /// See: + fn validate_delete_target(&mut self, expr: &Expr) { + match expr { + Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => { + for expr in elts { + self.validate_delete_target(expr); + } + } + Expr::Name(_) | Expr::Attribute(_) | Expr::Subscript(_) => {} + _ => self.add_error( + ParseErrorType::InvalidDeleteTarget(ExpressionKind::of(expr)), + expr, + ), + } + } + + /// Classify the `match` soft keyword token. + /// + /// # Panics + /// + /// If the parser isn't positioned at a `match` token. + fn classify_match_token(&mut self) -> MatchTokenKind { + assert_eq!(self.current_token_kind(), TokenKind::Match); + + let (first, second) = self.peek2(); + + match first { + // test_ok match_classify_as_identifier_1 + // match not in case + TokenKind::Not if second == TokenKind::In => MatchTokenKind::Identifier, + + // test_ok match_classify_as_keyword_1 + // match foo: + // case _: ... + // match 1: + // case _: ... + // match 1.0: + // case _: ... + // match 1j: + // case _: ... + // match "foo": + // case _: ... + // match f"foo {x}": + // case _: ... + // match {1, 2}: + // case _: ... + // match ~foo: + // case _: ... + // match ...: + // case _: ... + // match not foo: + // case _: ... + // match await foo(): + // case _: ... + // match lambda foo: foo: + // case _: ... + + // test_err match_classify_as_keyword + // match yield foo: + // case _: ... + TokenKind::Identifier + | TokenKind::Int + | TokenKind::Float + | TokenKind::Complex + | TokenKind::String + | TokenKind::FStringStart + | TokenKind::TStringStart + | TokenKind::Lbrace + | TokenKind::Tilde + | TokenKind::Ellipsis + | TokenKind::Not + | TokenKind::Await + | TokenKind::Yield + | TokenKind::Lambda => MatchTokenKind::Keyword, + + // test_ok match_classify_as_keyword_or_identifier + // match (1, 2) # Identifier + // match (1, 2): # Keyword + // case _: ... + // match [1:] # Identifier + // match [1, 2]: # Keyword + // case _: ... + // match * foo # Identifier + // match - foo # Identifier + // match -foo: # Keyword + // case _: ... + + // test_err match_classify_as_keyword_or_identifier + // match *foo: # Keyword + // case _: ... + TokenKind::Lpar + | TokenKind::Lsqb + | TokenKind::Star + | TokenKind::Plus + | TokenKind::Minus => MatchTokenKind::KeywordOrIdentifier, + + _ => { + if first.is_soft_keyword() || first.is_singleton() { + // test_ok match_classify_as_keyword_2 + // match match: + // case _: ... + // match case: + // case _: ... + // match type: + // case _: ... + // match None: + // case _: ... + // match True: + // case _: ... + // match False: + // case _: ... + MatchTokenKind::Keyword + } else { + // test_ok match_classify_as_identifier_2 + // match + // match != foo + // (foo, match) + // [foo, match] + // {foo, match} + // match; + // match: int + // match, + // match.foo + // match / foo + // match << foo + // match and foo + // match is not foo + MatchTokenKind::Identifier + } + } + } + } + + /// Parses a sequence of clauses. + /// + /// The parser only continues for as long as it sees the token indicating the start of the + /// specific clause. Unlike [`Parser::parse_list`], this method does not perform error recovery + /// when the next token is not a list terminator or the start of a list element. + /// + /// The special method is necessary because Python uses indentation over explicit delimiters to + /// indicate the end of a clause. + /// + /// ```python + /// if True: ... + /// elif False: ... + /// elf x: .... + /// else: ... + /// ``` + /// + /// It would be nice if the above example would recover and either skip over the `elf x: ...` + /// or parse it as a nested statement so that the parser recognises the `else` clause. But + /// Python makes this hard (without writing custom error recovery logic) because `elf x: ` + /// could also be an annotated assignment that went wrong ;) + /// + /// For now, don't recover when parsing clause headers, but add the terminator tokens (e.g. + /// `Else`) to the recovery context so that expression recovery stops when it encounters an + /// `else` token. + fn parse_clauses(&mut self, clause: Clause, mut parse_clause: impl FnMut(&mut Parser<'src>)) { + let mut progress = ParserProgress::default(); + + let recovery_kind = match clause { + Clause::ElIf => RecoveryContextKind::Elif, + Clause::Except => RecoveryContextKind::Except, + }; + + let saved_context = self.recovery_context; + self.recovery_context = self + .recovery_context + .union(RecoveryContext::from_kind(recovery_kind)); + + while recovery_kind.is_list_element(self) { + progress.assert_progressing(self); + + parse_clause(self); + } + + self.recovery_context = saved_context; + } +} + +/// Returns the identifier that stands in for an invalid binding target expression. +pub(super) fn invalid_target_identifier(target: &Expr) -> ast::Identifier { + ast::Identifier { + id: match target { + Expr::Name(name) => name.id.clone(), + _ => Name::empty(), + }, + range: target.range(), + node_index: AtomicNodeIndex::NONE, + } +} + +/// How an expression stands as the target of a `for` statement or comprehension. +enum ForTarget<'a> { + Valid, + Invalid(&'a Expr), + /// The target has no invalid expression, but contains this comparison. + Comparison(&'a ast::ExprCompare), +} + +/// Returns the first expression in `expr` that is not a valid `for` target. +/// +/// A comparison whose first operator is `in` is checked through its left operand. +fn invalid_for_target(expr: &Expr) -> ForTarget<'_> { + match expr { + Expr::Starred(ast::ExprStarred { value, .. }) => invalid_for_target(value), + Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => { + let mut result = ForTarget::Valid; + for elt in elts { + match invalid_for_target(elt) { + ForTarget::Valid => {} + invalid @ ForTarget::Invalid(_) => return invalid, + comparison @ ForTarget::Comparison(_) => { + if matches!(result, ForTarget::Valid) { + result = comparison; + } + } + } + } + result + } + Expr::Compare(compare) => match (compare.ops.first(), compare.operands.first()) { + (Some(ast::CmpOp::In), Some(left)) => match invalid_for_target(left) { + ForTarget::Valid => ForTarget::Comparison(compare), + other => other, + }, + _ => ForTarget::Comparison(compare), + }, + Expr::Name(_) | Expr::Attribute(_) | Expr::Subscript(_) => ForTarget::Valid, + _ => ForTarget::Invalid(expr), + } +} + +/// Returns the first expression in `expr` that is not a valid assignment target. +fn invalid_assignment_target(expr: &Expr) -> Option<&Expr> { + match expr { + Expr::Starred(ast::ExprStarred { value, .. }) => invalid_assignment_target(value), + Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => { + elts.iter().find_map(invalid_assignment_target) + } + Expr::Name(_) | Expr::Attribute(_) | Expr::Subscript(_) => None, + _ => Some(expr), + } +} + +#[derive(Copy, Clone)] +enum Clause { + ElIf, + Except, +} + +/// The classification of the `match` token. +/// +/// The `match` token is a soft keyword which means, depending on the context, it can be used as a +/// keyword or an identifier. +#[derive(Debug, Clone, Copy)] +enum MatchTokenKind { + /// The `match` token is used as a keyword. + /// + /// For example: + /// ```python + /// match foo: + /// case _: + /// pass + /// ``` + Keyword, + + /// The `match` token is used as an identifier. + /// + /// For example: + /// ```python + /// match.values() + /// match is None + /// ```` + Identifier, + + /// The `match` token is used as either a keyword or an identifier. + /// + /// For example: + /// ```python + /// # Used as a keyword + /// match [x, y]: + /// case [1, 2]: + /// pass + /// + /// # Used as an identifier + /// match[x] + /// ``` + KeywordOrIdentifier, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum WithItemParsingState { + /// Parsing the with items without any ambiguity. + Regular, + + /// Parsing the with items in a speculative mode. + Speculative, +} + +#[derive(Debug)] +struct ParsedWithItem { + /// The contained with item. + item: WithItem, + /// If the context expression of the item is parenthesized. + is_parenthesized: bool, +} + +#[derive(Debug, Copy, Clone)] +enum ElifOrElse { + Elif, + Else, +} + +impl ElifOrElse { + const fn is_elif(self) -> bool { + matches!(self, ElifOrElse::Elif) + } + + const fn as_token_kind(self) -> TokenKind { + match self { + ElifOrElse::Elif => TokenKind::Elif, + ElifOrElse::Else => TokenKind::Else, + } + } + + const fn as_block_clause(self) -> BlockClause { + match self { + ElifOrElse::Elif => BlockClause::Elif, + ElifOrElse::Else => BlockClause::Else, + } + } +} + +/// The kind of the except clause. +#[derive(Debug, Copy, Clone)] +enum ExceptClauseKind { + /// A normal except clause e.g., `except Exception as e: ...`. + Normal, + /// An except clause with a star e.g., `except *: ...`. + /// + /// Contains the star's [`TextRange`] for error reporting. + Star(TextRange), +} + +impl ExceptClauseKind { + const fn is_star(self) -> bool { + matches!(self, ExceptClauseKind::Star(..)) + } +} + +#[derive(Debug, Copy, Clone)] +enum AllowStarAnnotation { + Yes, + No, +} + +#[derive(Debug, Copy, Clone)] +enum ImportStyle { + /// E.g., `import foo, bar` + Import, + /// E.g., `from foo import bar, baz` + ImportFrom, +} diff --git a/vendor/rustpython-ruff_python_parser/src/parser/tests.rs b/vendor/rustpython-ruff_python_parser/src/parser/tests.rs new file mode 100644 index 00000000000..d3947f71700 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/parser/tests.rs @@ -0,0 +1,623 @@ +use std::assert_matches; + +use ruff_python_ast::{Expr, InterpolatedStringElement, IpyEscapeKind, ModModule, Number, Stmt}; + +use crate::{ + LexicalErrorType, Mode, ParseErrorType, ParseOptions, Parsed, parse, parse_expression, + parse_module, parse_unchecked, +}; + +// Keep recursive ASTs shallow enough for Windows's 1 MiB test-thread stacks. +const RECURSIVE_AST_TEST_DEPTH: usize = 1_000; + +#[test] +fn test_modes() { + let source = "a[0][1][2][3][4]"; + + assert!(parse(source, ParseOptions::from(Mode::Expression)).is_ok()); + assert!(parse(source, ParseOptions::from(Mode::Module)).is_ok()); +} + +#[test] +fn test_expr_mode_invalid_syntax1() { + let source = "first second"; + let error = parse_expression(source).unwrap_err(); + + insta::assert_debug_snapshot!(error); +} + +#[test] +fn test_expr_mode_invalid_syntax2() { + let source = r"first + +second +"; + let error = parse_expression(source).unwrap_err(); + + insta::assert_debug_snapshot!(error); +} + +#[test] +fn test_expr_mode_invalid_syntax3() { + let source = r"first + +second + +third +"; + let error = parse_expression(source).unwrap_err(); + + insta::assert_debug_snapshot!(error); +} + +#[test] +fn test_expr_mode_valid_syntax() { + let source = "first + +"; + let parsed = parse_expression(source).unwrap(); + + insta::assert_debug_snapshot!(parsed.expr()); +} + +#[test] +fn test_unicode_aliases() { + // https://github.com/RustPython/RustPython/issues/4566 + let source = r#"x = "\N{BACKSPACE}another cool trick""#; + let suite = parse_module(source).unwrap().into_suite(); + + insta::assert_debug_snapshot!(suite); +} + +#[test] +fn nfkc_normalizes_names() { + let parsed = parse_expression("𝒞").unwrap(); + let Expr::Name(name) = parsed.expr() else { + panic!("expected name expression, got {:?}", parsed.expr()); + }; + + assert_eq!(name.id.as_str(), "C"); +} + +#[test] +fn nfkc_normalizes_dotted_names() { + let suite = parse_module("import 𝒞.𝒟").unwrap().into_suite(); + let [Stmt::Import(import)] = suite.as_slice() else { + panic!("expected a single import statement, got {suite:?}"); + }; + let [alias] = import.names.as_slice() else { + panic!("expected a single import alias, got {:?}", import.names); + }; + + assert_eq!(alias.name.id.as_str(), "C.D"); +} + +#[test] +fn number_values() { + let cases = [ + ("1E400", Number::Float(f64::INFINITY)), + ( + "1E400J", + Number::Complex { + real: 0.0, + imag: f64::INFINITY, + }, + ), + ( + "123_456_789_123_456_789_123_456_789_123_456_789", + Number::Int("123456789123456789123456789123456789".parse().unwrap()), + ), + ( + "000_123_456_789_123_456_789_123_456_789_123_456_789J", + Number::Complex { + real: 0.0, + imag: 1.234_567_891_234_567_8e35, + }, + ), + ]; + + for (source, expected) in cases { + let parsed = parse_expression(source).unwrap(); + let Expr::NumberLiteral(number) = parsed.expr() else { + panic!( + "expected number expression for {source:?}, got {:?}", + parsed.expr() + ); + }; + + assert_eq!(number.value, expected, "source: {source:?}"); + } +} + +#[test] +fn malformed_radix_literals() { + for source in ["0x", "0o", "0b", "0x_", "0x__1"] { + assert!(parse_expression(source).is_err(), "source: {source:?}"); + } +} + +#[test] +fn interpolated_string_escaped_brace_values() { + let cases = [ + (r"f'\{{1}}'", r"\{1}"), + (r"f'\}}'", r"\}"), + (r"f'\\{{1}}'", r"\{1}"), + (r"f'\\\{{1}}'", r"\\{1}"), + (r"t'\{{1}}'", r"\{1}"), + (r"t'\}}'", r"\}"), + (r"t'\\{{1}}'", r"\{1}"), + (r"t'\\\{{1}}'", r"\\{1}"), + (r"rf'\{{1}}'", r"\{1}"), + (r"rt'\{{1}}'", r"\{1}"), + ]; + + for (source, expected) in cases { + let parsed = parse_expression(source).unwrap(); + let elements = match parsed.expr() { + Expr::FString(string) => &string.as_single_part_fstring().unwrap().elements, + Expr::TString(string) => &string.as_single_part_tstring().unwrap().elements, + expression => panic!("expected interpolated string for {source:?}, got {expression:?}"), + }; + let [InterpolatedStringElement::Literal(literal)] = &**elements else { + panic!("expected one literal element for {source:?}"); + }; + + assert_eq!(&*literal.value, expected, "source: {source:?}"); + } +} + +#[test] +fn ipython_escape_command_values() { + let cases = [ + ("?foo?", IpyEscapeKind::Help, "foo"), + ("?? foo?", IpyEscapeKind::Help, "foo"), + ("?? foo ?", IpyEscapeKind::Help2, " foo ?"), + ("?foo??", IpyEscapeKind::Help2, "foo"), + ("%foo?", IpyEscapeKind::Help, "%foo"), + ("%foo??", IpyEscapeKind::Help2, "%foo"), + ("%%foo???", IpyEscapeKind::Magic2, "foo???"), + ("!pwd?", IpyEscapeKind::Shell, "pwd?"), + ("?? \\\n foo?", IpyEscapeKind::Help, "foo"), + ("?? \\\r foo?", IpyEscapeKind::Help, "foo"), + ("?? \\\r\n foo?", IpyEscapeKind::Help, "foo"), + ]; + + for (source, expected_kind, expected_value) in cases { + let suite = parse(source, ParseOptions::from(Mode::Ipython)) + .unwrap() + .try_into_module() + .unwrap() + .into_suite(); + let [Stmt::IpyEscapeCommand(command)] = suite.as_slice() else { + panic!("expected one IPython escape command for {source:?}, got {suite:?}"); + }; + + assert_eq!(command.kind, expected_kind, "source: {source:?}"); + assert_eq!(&*command.value, expected_value, "source: {source:?}"); + } +} + +#[test] +fn ipython_escape_command_expression_values() { + let cases = [ + ("x = !!foo", IpyEscapeKind::Shell, "!foo"), + ("x = %%foo", IpyEscapeKind::Magic, "%foo"), + ]; + + for (source, expected_kind, expected_value) in cases { + let suite = parse(source, ParseOptions::from(Mode::Ipython)) + .unwrap() + .try_into_module() + .unwrap() + .into_suite(); + let [Stmt::Assign(assign)] = suite.as_slice() else { + panic!("expected one assignment for {source:?}, got {suite:?}"); + }; + let Expr::IpyEscapeCommand(command) = assign.value.as_ref() else { + panic!( + "expected an IPython escape command for {source:?}, got {:?}", + assign.value + ); + }; + + assert_eq!(command.kind, expected_kind, "source: {source:?}"); + assert_eq!(&*command.value, expected_value, "source: {source:?}"); + } +} + +#[test] +fn test_ipython_escape_commands() { + let parsed = parse( + r" +# Normal Python code +( + a + % + b +) + +# Dynamic object info +??a.foo +?a.foo +?a.foo? +??a.foo()?? + +# Line magic +%timeit a = b +%timeit foo(b) % 3 +%alias showPath pwd && ls -a +%timeit a =\ + foo(b); b = 2 +%matplotlib --inline +%matplotlib \ + --inline + +# System shell access +!pwd && ls -a | sed 's/^/\ /' +!pwd \ + && ls -a | sed 's/^/\\ /' +!!cd /Users/foo/Library/Application\ Support/ + +# Let's add some Python code to make sure that earlier escapes were handled +# correctly and that we didn't consume any of the following code as a result +# of the escapes. +def foo(): + return ( + a + != + b + ) + +# Transforms into `foo(..)` +/foo 1 2 +;foo 1 2 +,foo 1 2 + +# Indented escape commands +for a in range(5): + !ls + +p1 = !pwd +p2: str = !pwd +foo = %foo \ + bar +bar = %foo? +baz = !pwd? + +% foo +foo = %foo # comment + +# Help end line magics +foo? +foo.bar?? +foo.bar.baz? +foo[0]?? +foo[0][1]? +foo.bar[0].baz[1]?? +foo.bar[0].baz[2].egg?? +" + .trim(), + ParseOptions::from(Mode::Ipython), + ) + .unwrap(); + insta::assert_debug_snapshot!(parsed.syntax()); +} + +#[test] +fn test_fstring_expr_inner_line_continuation_and_t_string() { + let source = r#"f'{\t"i}'"#; + + let parsed = parse_expression(source); + + let error = parsed.unwrap_err(); + + insta::assert_debug_snapshot!(error); +} + +#[test] +fn test_fstring_expr_inner_line_continuation_newline_t_string() { + let source = r#"f'{\ +t"i}'"#; + + let parsed = parse_expression(source); + + let error = parsed.unwrap_err(); + + insta::assert_debug_snapshot!(error); +} + +#[test] +fn test_tstring_fstring_middle() { + let source = "t'{:{F'{\0}F"; + let parsed = parse_expression(source); + + let error = parsed.unwrap_err(); + + insta::assert_debug_snapshot!(error); +} + +#[test] +fn test_tstring_fstring_middle_fuzzer() { + let source = "A1[A\u{c}\0:+,>1t'{:f\0:{f\"f\0:\0{fm\0:{f:\u{10}\0\0\0:bb\0{@f>f\u{1}'\0f"; + let parsed = parse_expression(source); + + let error = parsed.unwrap_err(); + + insta::assert_debug_snapshot!(error); +} + +/// Parses the module `source`, which nests brackets deeper than allowed, and asserts that this is +/// its only error. +fn parse_too_deeply_nested(source: &str) -> Parsed { + let parsed = parse_unchecked(source, ParseOptions::from(Mode::Module)) + .try_into_module() + .unwrap(); + assert!(!parsed.errors().is_empty()); + for error in parsed.errors() { + assert_eq!( + error.error, + ParseErrorType::Lexical(LexicalErrorType::TooDeeplyNestedBrackets) + ); + } + parsed +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn nested_parens_grow_stack() { + let src = format!("{}1{}", "(".repeat(1_000), ")".repeat(1_000)); + stacker::grow(32 * 1024, || parse_too_deeply_nested(&src)); +} + +#[test] +fn normal_python_unaffected() { + let src = format!("x = {}1{}", "(".repeat(50), ")".repeat(50)); + parse_module(&src).unwrap(); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn deep_nesting_preserves_surrounding_statements() { + let src = format!( + "before = 1\n{}1{}\nafter = 2\n", + "(".repeat(1_000), + ")".repeat(1_000), + ); + let parsed = parse_too_deeply_nested(&src); + + assert_matches!(parsed.suite().first(), Some(Stmt::Assign(_))); + assert_matches!(parsed.suite().last(), Some(Stmt::Assign(_))); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn nested_def_blocks_grow_stack() { + // Each nested function crosses the suite boundary where the parser rechecks the stack. + // Indentation stops at 99 levels, so the deeper blocks are reported instead of parsed. + let depth = RECURSIVE_AST_TEST_DEPTH; + let mut src = String::new(); + for i in 0..depth { + src.push_str(&"\t".repeat(i)); + src.push_str("def f():\n"); + } + src.push_str(&"\t".repeat(depth)); + src.push_str("pass\n"); + let error = parse_module(&src).unwrap_err(); + assert_eq!( + error.error, + ParseErrorType::Lexical(LexicalErrorType::TooDeepIndentation) + ); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn nested_lists_grow_stack() { + let src = format!("{}1{}", "[".repeat(1_000), "]".repeat(1_000)); + parse_too_deeply_nested(&src); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn nested_calls_grow_stack() { + let src = format!("x = {}1{}", "f(".repeat(1_000), ")".repeat(1_000)); + parse_too_deeply_nested(&src); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn nested_subscripts_grow_stack() { + let src = format!("x = {}1{}", "a[".repeat(1_000), "]".repeat(1_000)); + parse_too_deeply_nested(&src); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn nested_match_patterns_grow_stack() { + // Deeply parenthesised match patterns — exercises pattern-parsing + // instrumentation in addition to statement / expression paths. + let mut src = String::from("match x:\n case "); + for _ in 0..600 { + src.push('('); + } + src.push('y'); + for _ in 0..600 { + src.push(')'); + } + src.push_str(": pass\n"); + parse_too_deeply_nested(&src); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn nested_invalid_mapping_pattern_keys_grow_stack() { + let depth = 512; + let src = format!( + "match value:\n case {}0{}:\n pass\n", + "{".repeat(depth), + ": 0}".repeat(depth) + ); + let parsed = crate::parse_unchecked(&src, ParseOptions::from(Mode::Module)); + assert!(!parsed.errors().is_empty()); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn binary_paren_interplay_grows_stack() { + // `1+(1+(1+(1+...)))` — each level alternates a binary operator and a + // parenthesised sub-expression, exactly like the pattern described in + // the tracking issue. + let depth = RECURSIVE_AST_TEST_DEPTH; + let mut src = String::new(); + for _ in 0..depth { + src.push_str("1+("); + } + src.push('1'); + for _ in 0..depth { + src.push(')'); + } + parse_too_deeply_nested(&src); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn right_assoc_pow_chain_grows_stack() { + // `1**1**1**...**1` — `**` is right-associative, so the right operand + // is parsed by a recursive `parse_binary_expression_or_higher` call + // *without* any intervening parentheses or atom nesting. This exercises + // the binary-expression recursion path directly, unlike the + // `1+(1+(...))` interplay test which recurses through parenthesised + // atoms. + let depth = RECURSIVE_AST_TEST_DEPTH; + let mut src = String::with_capacity(depth * 3 + 1); + for _ in 0..depth { + src.push_str("1**"); + } + src.push('1'); + parse_module(&src).unwrap(); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn ternary_else_chain_grows_stack() { + // `1 if 1 else 1 if 1 else ...` — the `else` operand recurses at the + // conditional layer (`parse_if_expression` -> `orelse`), which is not + // covered by the binary-expression guard. + let depth = RECURSIVE_AST_TEST_DEPTH; + let mut src = String::with_capacity(depth * 12 + 1); + for _ in 0..depth { + src.push_str("1 if 1 else "); + } + src.push('1'); + parse_module(&src).unwrap(); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn nested_lambda_chain_grows_stack() { + // `lambda: lambda: lambda: ...` — the lambda body recurses at the + // conditional layer (`parse_lambda_expr` -> body), bypassing the + // binary-expression guard entirely. + let depth = RECURSIVE_AST_TEST_DEPTH; + let mut src = String::from("x = "); + for _ in 0..depth { + src.push_str("lambda: "); + } + src.push('1'); + parse_module(&src).unwrap(); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn invalid_async_chain_grows_stack() { + let source = format!("{}x = 1\n", "async ".repeat(5_000)); + let parsed = crate::parse_unchecked(&source, ParseOptions::from(Mode::Module)); + assert!(!parsed.errors().is_empty()); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[test] +fn nested_unary_chains_grow_stack() { + let depth = 300; + let source = format!("{}1\n", "-~+".repeat(depth)); + parse_module(&source).unwrap(); + + let source = format!("{}True\n", "not ".repeat(depth)); + parse_module(&source).unwrap(); +} + +fn first_error(source: &str) -> ParseErrorType { + parse_unchecked(source, ParseOptions::from(Mode::Module)).errors()[0] + .error + .clone() +} + +#[test] +fn unclosed_bracket_at_end_is_incomplete() { + assert_eq!( + first_error("x = [\n1,\n"), + ParseErrorType::Lexical(LexicalErrorType::UnclosedBracket { + opening: '[', + incomplete: true, + }) + ); +} + +#[test] +fn unclosed_bracket_in_place_of_earlier_error_is_complete() { + assert_eq!( + first_error("x = [\nx for x\nin raise(3)\nof x\n"), + ParseErrorType::Lexical(LexicalErrorType::UnclosedBracket { + opening: '[', + incomplete: false, + }) + ); +} + +#[test] +fn unclosed_bracket_after_same_line_error() { + assert_eq!(first_error("def f(:\n").to_string(), "invalid syntax"); +} + +#[test] +fn number_literal_errors() { + for (source, message) in [ + ("0x\n", "invalid hexadecimal literal"), + ("0o9\n", "invalid digit '9' in octal literal"), + ("0b1_2\n", "invalid digit '2' in binary literal"), + ("1_\n", "invalid decimal literal"), + ("1e+x\n", "invalid decimal literal"), + ("1abc\n", "invalid decimal literal"), + ("1jx\n", "invalid imaginary literal"), + ( + "0012\n", + "leading zeros in decimal integer literals are not permitted; use an 0o prefix for octal integers", + ), + ] { + assert_eq!(first_error(source).to_string(), message, "{source:?}"); + } +} + +#[test] +fn number_literal_before_keyword() { + assert!(parse_unchecked("1if x else 2\n", ParseOptions::from(Mode::Module)).has_valid_syntax()); +} + +#[test] +fn incompatible_string_prefixes() { + assert_eq!( + first_error("x = bu''\n"), + ParseErrorType::Lexical(LexicalErrorType::IncompatibleStringPrefixes { + first: 'u', + second: 'b', + }) + ); + assert!(parse_unchecked("ub = 1\n", ParseOptions::from(Mode::Module)).has_valid_syntax()); +} + +#[test] +fn non_printable_character() { + assert_eq!( + first_error("x = \u{a0}\n").to_string(), + "invalid non-printable character U+00A0" + ); +} diff --git a/vendor/rustpython-ruff_python_parser/src/semantic_errors.rs b/vendor/rustpython-ruff_python_parser/src/semantic_errors.rs new file mode 100644 index 00000000000..a8f1a8ddf91 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/semantic_errors.rs @@ -0,0 +1,2668 @@ +//! [`SemanticSyntaxChecker`] for AST-based syntax errors. +//! +//! This checker is not responsible for traversing the AST itself. Instead, its +//! [`SemanticSyntaxChecker::visit_stmt`] and [`SemanticSyntaxChecker::visit_expr`] methods should +//! be called in a parent `Visitor`'s `visit_stmt` and `visit_expr` methods, respectively. + +use ruff_python_ast::{ + self as ast, Expr, ExprContext, IrrefutablePatternKind, Pattern, PythonVersion, Stmt, StmtExpr, + StmtFunctionDef, StmtImportFrom, + comparable::HashableExpr, + helpers, + visitor::{Visitor, walk_expr, walk_stmt}, +}; +use ruff_text_size::{Ranged, TextRange, TextSize}; +use rustc_hash::{FxBuildHasher, FxHashSet}; +use std::fmt::Display; + +#[derive(Debug, Default)] +pub struct SemanticSyntaxChecker { + /// The checker has traversed past the `__future__` import boundary. + /// + /// For example, the checker could be visiting `x` in: + /// + /// ```python + /// from __future__ import annotations + /// + /// import os + /// + /// x: int = 1 + /// ``` + /// + /// Python considers it a syntax error to import from `__future__` after any other + /// non-`__future__`-importing statements. + seen_futures_boundary: bool, + + /// The checker has traversed past the module docstring boundary (i.e. seen any statement in the + /// module). + seen_module_docstring_boundary: bool, +} + +impl SemanticSyntaxChecker { + pub fn new() -> Self { + Self::default() + } +} + +impl SemanticSyntaxChecker { + fn add_error( + context: &Ctx, + kind: SemanticSyntaxErrorKind, + range: TextRange, + ) { + context.report_semantic_error(SemanticSyntaxError { + kind, + range, + python_version: context.python_version(), + }); + } + + fn check_lazy_import_context( + ctx: &Ctx, + range: TextRange, + kind: LazyImportKind, + ) -> bool { + if let Some(context) = ctx.lazy_import_context() { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::LazyImportNotAllowed { context, kind }, + range, + ); + return true; + } + false + } + + fn check_stmt(&mut self, stmt: &ast::Stmt, ctx: &Ctx) { + match stmt { + Stmt::ImportFrom(StmtImportFrom { + range, + module, + level, + names, + is_lazy, + .. + }) => { + let mut handled_lazy_error = false; + + if *is_lazy { + // test_ok lazy_import_semantic_ok_py315 + // # parse_options: {"target-version": "3.15"} + // import contextlib + // with contextlib.nullcontext(): + // lazy import os + // with contextlib.nullcontext(): + // lazy from sys import path + + // test_err lazy_import_invalid_context_py315 + // # parse_options: {"target-version": "3.15"} + // try: + // lazy import os + // except: + // pass + // + // try: + // x + // except* Exception: + // lazy import sys + // + // def func(): + // lazy import math + // + // async def async_func(): + // lazy from json import loads + // + // class MyClass: + // lazy import typing + // + // def outer(): + // class Inner: + // lazy import json + if Self::check_lazy_import_context(ctx, *range, LazyImportKind::ImportFrom) { + handled_lazy_error = true; + } else if names.iter().any(|alias| alias.name.as_str() == "*") { + // test_err lazy_import_invalid_from_py315 + // # parse_options: {"target-version": "3.15"} + // lazy from os import * + // lazy from __future__ import annotations + // + // def func(): + // lazy from sys import * + Self::add_error(ctx, SemanticSyntaxErrorKind::LazyImportStar, *range); + handled_lazy_error = true; + } else if matches!(module.as_deref(), Some("__future__")) { + Self::add_error(ctx, SemanticSyntaxErrorKind::LazyFutureImport, *range); + handled_lazy_error = true; + } + } + + if handled_lazy_error { + // Skip the regular `from`-import validations after reporting the lazy-specific + // syntax error with the highest precedence. + } else if matches!(module.as_deref(), Some("__future__")) { + for name in names { + if !is_known_future_feature(&name.name) { + // test_ok valid_future_feature + // from __future__ import annotations + + // test_err invalid_future_feature + // from __future__ import invalid_feature + // from __future__ import annotations, invalid_feature + // from __future__ import invalid_feature_1, invalid_feature_2 + Self::add_error( + ctx, + SemanticSyntaxErrorKind::FutureFeatureNotDefined( + name.name.to_string(), + ), + name.range, + ); + } + } + if self.seen_futures_boundary { + Self::add_error(ctx, SemanticSyntaxErrorKind::LateFutureImport, *range); + } + } + for alias in names { + if alias.name.as_str() == "*" && !ctx.in_module_scope() { + // test_err import_from_star + // def f1(): + // from module import * + // class C: + // from module import * + // def f2(): + // from ..module import * + // def f3(): + // from module import *, * + + // test_ok import_from_star + // from module import * + Self::add_error( + ctx, + SemanticSyntaxErrorKind::NonModuleImportStar( + helpers::format_import_from(*level, module.as_deref()).to_string(), + ), + *range, + ); + break; + } + } + } + Stmt::Import(ast::StmtImport { + range, + is_lazy: true, + .. + }) => { + Self::check_lazy_import_context(ctx, *range, LazyImportKind::Import); + } + Stmt::Match(match_stmt) => { + Self::irrefutable_match_case(match_stmt, ctx); + for case in &match_stmt.cases { + let mut visitor = MatchPatternVisitor { + names: FxHashSet::default(), + ctx, + }; + visitor.visit_pattern(&case.pattern); + } + } + Stmt::FunctionDef(ast::StmtFunctionDef { + type_params, + parameters, + .. + }) => { + if let Some(type_params) = type_params { + Self::duplicate_type_parameter_name(type_params, ctx); + Self::type_parameter_default_order(type_params, ctx); + } + Self::duplicate_parameter_name(parameters, ctx); + } + Stmt::Global(ast::StmtGlobal { names, .. }) => { + for name in names { + if ctx.is_bound_parameter(name) { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::GlobalParameter(name.to_string()), + name.range, + ); + } + } + } + Stmt::ClassDef(ast::StmtClassDef { + type_params, + arguments, + .. + }) => { + if let Some(type_params) = type_params { + Self::duplicate_type_parameter_name(type_params, ctx); + Self::type_parameter_default_order(type_params, ctx); + } + if let Some(arguments) = arguments { + Self::duplicate_keyword_args(arguments, ctx); + } + } + Stmt::TypeAlias(ast::StmtTypeAlias { + type_params: Some(type_params), + .. + }) => { + Self::duplicate_type_parameter_name(type_params, ctx); + Self::type_parameter_default_order(type_params, ctx); + } + Stmt::Assign(ast::StmtAssign { targets, value, .. }) => { + if let [Expr::Starred(ast::ExprStarred { range, .. })] = targets.as_slice() { + // test_ok single_starred_assignment_target + // (*a,) = (1,) + // *a, = (1,) + // [*a] = (1,) + + // test_err single_starred_assignment_target + // *a = (1,) + Self::add_error( + ctx, + SemanticSyntaxErrorKind::SingleStarredAssignment, + *range, + ); + } + + // test_ok assign_stmt_starred_expr_value + // _ = 4 + // _ = [4] + // _ = (*[1],) + // _ = *[1], + + // test_err assign_stmt_starred_expr_value + // _ = *[42] + // _ = *{42} + // _ = *list() + // _ = *(p + q) + Self::invalid_star_expression(value, ctx); + } + Stmt::Return(ast::StmtReturn { + value, + range, + node_index: _, + }) => { + if let Some(value) = value { + // test_err single_star_return + // def f(): return *x + Self::invalid_star_expression(value, ctx); + } + if !ctx.in_function_scope() { + Self::add_error(ctx, SemanticSyntaxErrorKind::ReturnOutsideFunction, *range); + } + } + Stmt::For(ast::StmtFor { + target, + iter, + is_async, + .. + }) => { + // test_err single_star_for + // for _ in *x: ... + // for *x in xs: ... + Self::invalid_star_expression(target, ctx); + Self::invalid_star_expression(iter, ctx); + if *is_async { + Self::await_outside_async_function( + ctx, + stmt, + AwaitOutsideAsyncFunctionKind::AsyncFor, + ); + } + } + Stmt::With(ast::StmtWith { is_async: true, .. }) => { + Self::await_outside_async_function( + ctx, + stmt, + AwaitOutsideAsyncFunctionKind::AsyncWith, + ); + } + Stmt::Nonlocal(ast::StmtNonlocal { names, range, .. }) => { + // test_ok nonlocal_declaration_at_module_level + // def _(): + // nonlocal x + + // test_err nonlocal_declaration_at_module_level + // nonlocal x + // nonlocal x, y + if ctx.in_module_scope() { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel, + *range, + ); + } + + if !ctx.in_module_scope() { + for name in names { + if ctx.is_bound_parameter(name) { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::NonlocalParameter(name.to_string()), + name.range, + ); + } else if !ctx.has_nonlocal_binding(name) { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::NonlocalWithoutBinding(name.to_string()), + name.range, + ); + } + } + } + } + Stmt::Break(ast::StmtBreak { range, .. }) if !ctx.in_loop_context() => { + Self::add_error(ctx, SemanticSyntaxErrorKind::BreakOutsideLoop, *range); + } + Stmt::Continue(ast::StmtContinue { range, .. }) if !ctx.in_loop_context() => { + Self::add_error(ctx, SemanticSyntaxErrorKind::ContinueOutsideLoop, *range); + } + _ => {} + } + + Self::debug_shadowing(stmt, ctx); + Self::check_annotation(stmt, ctx); + } + + fn check_annotation(stmt: &ast::Stmt, ctx: &Ctx) { + match stmt { + Stmt::AnnAssign(ast::StmtAnnAssign { + target, annotation, .. + }) => { + if ctx.python_version() > PythonVersion::PY313 { + // test_ok valid_annotation_py313 + // # parse_options: {"target-version": "3.13"} + // a: (x := 1) + // def outer(): + // b: (yield 1) + // c: (yield from 1) + // async def outer(): + // d: (await 1) + + // test_err invalid_annotation_py314 + // # parse_options: {"target-version": "3.14"} + // a: (x := 1) + // def outer(): + // b: (yield 1) + // c: (yield from 1) + // async def outer(): + // d: (await 1) + let mut visitor = InvalidExpressionVisitor { + position: InvalidExpressionPosition::TypeAnnotation, + ctx, + }; + visitor.visit_expr(annotation); + } + if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { + if let Some(global_stmt) = ctx.global(id.as_str()) { + let global_start = global_stmt.start(); + if !ctx.in_module_scope() || target.start() < global_start { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::AnnotatedGlobal(id.to_string()), + target.range(), + ); + } + } + } + } + Stmt::FunctionDef(ast::StmtFunctionDef { + type_params, + parameters, + returns, + .. + }) => { + // test_ok valid_annotation_function_py313 + // # parse_options: {"target-version": "3.13"} + // def f() -> (y := 3): ... + // def g(arg: (x := 1)): ... + // def outer(): + // def i(x: (yield 1)): ... + // def k() -> (yield 1): ... + // def m(x: (yield from 1)): ... + // def o() -> (yield from 1): ... + // async def outer(): + // def f() -> (await 1): ... + // def g(arg: (await 1)): ... + + // test_err invalid_annotation_function_py314 + // # parse_options: {"target-version": "3.14"} + // def f() -> (y := 3): ... + // def g(arg: (x := 1)): ... + // def outer(): + // def i(x: (yield 1)): ... + // def k() -> (yield 1): ... + // def m(x: (yield from 1)): ... + // def o() -> (yield from 1): ... + // async def outer(): + // def f() -> (await 1): ... + // def g(arg: (await 1)): ... + + // test_err invalid_annotation_function + // def d[T]() -> (await 1): ... + // def e[T](arg: (await 1)): ... + // def f[T]() -> (y := 3): ... + // def g[T](arg: (x := 1)): ... + // def h[T](x: (yield 1)): ... + // def j[T]() -> (yield 1): ... + // def l[T](x: (yield from 1)): ... + // def n[T]() -> (yield from 1): ... + // def p[T: (yield 1)](): ... # yield in TypeVar bound + // def q[T = (yield 1)](): ... # yield in TypeVar default + // def r[*Ts = (yield 1)](): ... # yield in TypeVarTuple default + // def s[**Ts = (yield 1)](): ... # yield in ParamSpec default + // def t[T: (x := 1)](): ... # named expr in TypeVar bound + // def u[T = (x := 1)](): ... # named expr in TypeVar default + // def v[*Ts = (x := 1)](): ... # named expr in TypeVarTuple default + // def w[**Ts = (x := 1)](): ... # named expr in ParamSpec default + // def t[T: (await 1)](): ... # await in TypeVar bound + // def u[T = (await 1)](): ... # await in TypeVar default + // def v[*Ts = (await 1)](): ... # await in TypeVarTuple default + // def w[**Ts = (await 1)](): ... # await in ParamSpec default + let mut visitor = InvalidExpressionVisitor { + position: InvalidExpressionPosition::TypeAnnotation, + ctx, + }; + if let Some(type_params) = type_params { + visitor.visit_type_params(type_params); + } + // the __future__ annotation error takes precedence over the generic error + if ctx.future_annotations_or_stub() || ctx.python_version() > PythonVersion::PY313 { + visitor.position = InvalidExpressionPosition::TypeAnnotation; + } else if type_params.is_some() { + visitor.position = InvalidExpressionPosition::GenericDefinition; + } else { + return; + } + for param in parameters + .iter() + .filter_map(ast::AnyParameterRef::annotation) + { + visitor.visit_expr(param); + } + if let Some(returns) = returns { + visitor.visit_expr(returns); + } + } + Stmt::ClassDef(ast::StmtClassDef { + type_params: Some(type_params), + arguments, + .. + }) => { + // test_ok valid_annotation_class + // class F(y := list): ... + // def f(): + // class G((yield 1)): ... + // class H((yield from 1)): ... + // async def f(): + // class G((await 1)): ... + + // test_err invalid_annotation_class + // class F[T](y := list): ... + // class I[T]((yield 1)): ... + // class J[T]((yield from 1)): ... + // class K[T: (yield 1)]: ... # yield in TypeVar + // class L[T: (x := 1)]: ... # named expr in TypeVar + // class M[T]((await 1)): ... + // class N[T: (await 1)]: ... + let mut visitor = InvalidExpressionVisitor { + position: InvalidExpressionPosition::TypeAnnotation, + ctx, + }; + visitor.visit_type_params(type_params); + if let Some(arguments) = arguments { + visitor.position = InvalidExpressionPosition::GenericDefinition; + visitor.visit_arguments(arguments); + } + } + Stmt::TypeAlias(ast::StmtTypeAlias { + type_params, value, .. + }) => { + // test_err invalid_annotation_type_alias + // type X[T: (yield 1)] = int # TypeVar bound + // type X[T = (yield 1)] = int # TypeVar default + // type X[*Ts = (yield 1)] = int # TypeVarTuple default + // type X[**Ts = (yield 1)] = int # ParamSpec default + // type Y = (yield 1) # yield in value + // type Y = (x := 1) # named expr in value + // type Y[T: (await 1)] = int # await in bound + // type Y = (await 1) # await in value + let mut visitor = InvalidExpressionVisitor { + position: InvalidExpressionPosition::TypeAlias, + ctx, + }; + visitor.visit_expr(value); + if let Some(type_params) = type_params { + visitor.visit_type_params(type_params); + } + } + _ => {} + } + } + + /// Emit a [`SemanticSyntaxErrorKind::InvalidStarExpression`] if `expr` is starred. + fn invalid_star_expression(expr: &Expr, ctx: &Ctx) { + // test_ok single_star_in_tuple + // def f(): yield (*x,) + // def f(): return (*x,) + // for _ in (*x,): ... + // for (*x,) in xs: ... + if expr.is_starred_expr() { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::InvalidStarExpression, + expr.range(), + ); + } + } + + fn multiple_star_expression( + ctx: &Ctx, + expr_ctx: ExprContext, + elts: &[Expr], + range: TextRange, + ) { + if expr_ctx.is_store() { + let mut has_starred = false; + for elt in elts { + if elt.is_starred_expr() { + if has_starred { + // test_err multiple_starred_assignment_target + // (*a, *b) = (1, 2) + // [*a, *b] = (1, 2) + // (*a, *b, c) = (1, 2, 3) + // [*a, *b, c] = (1, 2, 3) + // (*a, *b, (*c, *d)) = (1, 2) + + // test_ok multiple_starred_assignment_target + // (*a, b) = (1, 2) + // (*_, normed), *_ = [(1,), 2] + Self::add_error( + ctx, + SemanticSyntaxErrorKind::MultipleStarredExpressions, + range, + ); + return; + } + has_starred = true; + } + } + } + } + + /// Check for [`SemanticSyntaxErrorKind::WriteToDebug`] in `stmt`. + fn debug_shadowing(stmt: &ast::Stmt, ctx: &Ctx) { + match stmt { + Stmt::FunctionDef(ast::StmtFunctionDef { + name, + type_params, + parameters, + .. + }) => { + // test_err debug_shadow_function + // def __debug__(): ... # function name + // def f[__debug__](): ... # type parameter name + // def f(__debug__): ... # parameter name + // lambda __debug__: 0 # lambda parameter name + Self::check_identifier(name, ctx); + if let Some(type_params) = type_params { + for type_param in type_params.iter() { + Self::check_identifier(type_param.name(), ctx); + } + } + for parameter in parameters { + Self::check_identifier(parameter.name(), ctx); + } + } + Stmt::ClassDef(ast::StmtClassDef { + name, type_params, .. + }) => { + // test_err debug_shadow_class + // class __debug__: ... # class name + // class C[__debug__]: ... # type parameter name + Self::check_identifier(name, ctx); + if let Some(type_params) = type_params { + for type_param in type_params.iter() { + Self::check_identifier(type_param.name(), ctx); + } + } + } + Stmt::TypeAlias(ast::StmtTypeAlias { + type_params: Some(type_params), + .. + }) => { + // test_err debug_shadow_type_alias + // type __debug__ = list[int] # visited as an Expr but still flagged + // type Debug[__debug__] = str + for type_param in type_params.iter() { + Self::check_identifier(type_param.name(), ctx); + } + } + Stmt::Import(ast::StmtImport { names, .. }) + | Stmt::ImportFrom(ast::StmtImportFrom { names, .. }) => { + // test_err debug_shadow_import + // import __debug__ + // import debug as __debug__ + // from x import __debug__ + // from x import debug as __debug__ + + // test_ok debug_rename_import + // import __debug__ as debug + // from __debug__ import Some + // from x import __debug__ as debug + for name in names { + match &name.asname { + Some(asname) => Self::check_identifier(asname, ctx), + None => Self::check_identifier(&name.name, ctx), + } + } + } + Stmt::Try(ast::StmtTry { handlers, .. }) => { + // test_err debug_shadow_try + // try: ... + // except Exception as __debug__: ... + for handler in handlers + .iter() + .filter_map(ast::ExceptHandler::as_except_handler) + { + if let Some(name) = &handler.name { + Self::check_identifier(name, ctx); + } + } + } + // test_err debug_shadow_with + // with open("foo.txt") as __debug__: ... + _ => {} + } + } + + /// Check if `ident` is equal to `__debug__` and emit a + /// [`SemanticSyntaxErrorKind::WriteToDebug`] if so. + fn check_identifier(ident: &ast::Identifier, ctx: &Ctx) { + if ident.id == "__debug__" { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::WriteToDebug(WriteToDebugKind::Store), + ident.range, + ); + } + } + + fn duplicate_type_parameter_name( + type_params: &ast::TypeParams, + ctx: &Ctx, + ) { + if type_params.len() < 2 { + return; + } + + for (i, type_param) in type_params.iter().enumerate() { + if type_params + .iter() + .take(i) + .any(|t| t.name().id == type_param.name().id) + { + // test_ok non_duplicate_type_parameter_names + // type Alias[T] = list[T] + // def f[T](t: T): ... + // class C[T]: ... + // class C[T, U, V]: ... + // type Alias[T, U: str, V: (str, bytes), *Ts, **P, D = default] = ... + + // test_err duplicate_type_parameter_names + // type Alias[T, T] = ... + // def f[T, T](t: T): ... + // class C[T, T]: ... + // type Alias[T, U: str, V: (str, bytes), *Ts, **P, T = default] = ... + // def f[T, T, T](): ... # two errors + // def f[T, *T](): ... # star is still duplicate + // def f[T, **T](): ... # as is double star + Self::add_error( + ctx, + SemanticSyntaxErrorKind::DuplicateTypeParameter, + type_param.range(), + ); + } + } + } + + fn type_parameter_default_order( + type_params: &ast::TypeParams, + ctx: &Ctx, + ) { + let mut seen_default = false; + for type_param in type_params { + let has_default = match type_param { + ast::TypeParam::TypeVar(ast::TypeParamTypeVar { default, .. }) + | ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { default, .. }) + | ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { default, .. }) => { + default.is_some() + } + }; + + if seen_default && !has_default { + // test_err type_parameter_default_order + // class C[T = int, U]: ... + // class C[T1, T2 = int, T3, T4]: ... + // def f[T = int, U](): ... + // type Alias[T = int, U] = ... + Self::add_error( + ctx, + SemanticSyntaxErrorKind::TypeParameterDefaultOrder( + type_param.name().id.to_string(), + ), + type_param.range(), + ); + } + if has_default { + seen_default = true; + } + } + } + + fn duplicate_parameter_name( + parameters: &ast::Parameters, + ctx: &Ctx, + ) { + if parameters.len() < 2 { + return; + } + + let mut all_arg_names = + FxHashSet::with_capacity_and_hasher(parameters.len(), FxBuildHasher); + + for parameter in parameters { + let range = parameter.name().range(); + let param_name = parameter.name().as_str(); + if !all_arg_names.insert(param_name) { + // test_err params_duplicate_names + // def foo(a, a=10, *a, a, a: str, **a): ... + Self::add_error( + ctx, + SemanticSyntaxErrorKind::DuplicateParameter(param_name.to_string()), + range, + ); + } + } + } + + fn duplicate_keyword_args(args: &ast::Arguments, ctx: &Ctx) { + if args.keywords.len() < 2 { + return; + } + + let mut all_arg_names = + FxHashSet::with_capacity_and_hasher(args.keywords.len(), FxBuildHasher); + + for (ident, range) in args + .keywords + .iter() + .filter_map(|keyword| keyword.arg.as_ref().map(|arg| (arg, keyword.range))) + { + if !all_arg_names.insert(ident.as_str()) { + // test_err duplicate_keyword_args + // def foo(x): ... + // foo(x=1, x=2) + // def baz(x, y, z): ... + // baz(x, y=1, z=3, y=4) + + // test_ok non_duplicate_keyword_args + // def foo(x): ... + // foo(x=1) + // def bar(x, y, z): ... + // foo(x="a", y=1, z=True) + Self::add_error( + ctx, + SemanticSyntaxErrorKind::DuplicateKeywordArgument(ident.to_string()), + range, + ); + } + } + } + + fn irrefutable_match_case(stmt: &ast::StmtMatch, ctx: &Ctx) { + // test_ok irrefutable_case_pattern_at_end + // match x: + // case 2: ... + // case var: ... + // match x: + // case 2: ... + // case _: ... + // match x: + // case var if True: ... # don't try to refute a guarded pattern + // case 2: ... + + // test_err irrefutable_case_pattern + // match x: + // case var: ... # capture pattern + // case 2: ... + // match x: + // case _: ... + // case 2: ... # wildcard pattern + // match x: + // case var1 as var2: ... # as pattern with irrefutable left-hand side + // case 2: ... + // match x: + // case enum.variant | var: ... # or pattern with irrefutable part + // case 2: ... + for case in stmt + .cases + .iter() + .rev() + .skip(1) + .filter_map(|case| match case.guard { + Some(_) => None, + None => case.pattern.irrefutable_pattern(), + }) + { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::IrrefutableCasePattern(case.kind), + case.range, + ); + } + } + + /// Check `stmt` for semantic syntax errors and update the checker's internal state. + /// + /// Note that this method should only be called when traversing `stmt` *and* its children. For + /// example, if traversal of function bodies needs to be deferred, avoid calling `visit_stmt` on + /// the function itself until the deferred body is visited too. Failing to defer `visit_stmt` in + /// this case will break any internal state that depends on function scopes, such as `async` + /// context detection. + pub fn visit_stmt(&mut self, stmt: &ast::Stmt, ctx: &Ctx) { + // check for errors + self.check_stmt(stmt, ctx); + + // update internal state + match stmt { + Stmt::Expr(StmtExpr { value, .. }) + if !self.seen_module_docstring_boundary && value.is_string_literal_expr() => {} + Stmt::ImportFrom(StmtImportFrom { + module, is_lazy, .. + }) => { + // Allow eager `__future__` imports until we see any other import. Lazy imports, + // including `lazy from __future__ import ...`, always close the boundary. + if *is_lazy || !matches!(module.as_deref(), Some("__future__")) { + self.seen_futures_boundary = true; + } + } + Stmt::FunctionDef(StmtFunctionDef { is_async, body, .. }) => { + if *is_async { + let mut visitor = ReturnVisitor::default(); + visitor.visit_body(body); + + if visitor.has_yield { + if let Some(return_range) = visitor.return_range { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::ReturnInGenerator, + return_range, + ); + } + } + } + self.seen_futures_boundary = true; + } + _ => { + self.seen_futures_boundary = true; + } + } + + self.seen_module_docstring_boundary = true; + } + + /// Check `expr` for semantic syntax errors and update the checker's internal state. + pub fn visit_expr(&mut self, expr: &Expr, ctx: &Ctx) { + match expr { + Expr::ListComp(ast::ExprListComp { + elt, generators, .. + }) + | Expr::SetComp(ast::ExprSetComp { + elt, generators, .. + }) => { + Self::check_generator_expr(elt, generators, ctx); + Self::check_generator_clauses(generators, ctx); + Self::async_comprehension_in_sync_comprehension(ctx, generators); + for generator in generators.iter().filter(|g| g.is_async) { + Self::await_outside_async_function( + ctx, + generator, + AwaitOutsideAsyncFunctionKind::AsyncComprehension, + ); + } + } + Expr::DictComp(ast::ExprDictComp { + key, + value, + generators, + .. + }) => { + if let Some(key) = key { + Self::check_generator_expr(key, generators, ctx); + } + Self::check_generator_expr(value, generators, ctx); + Self::check_generator_clauses(generators, ctx); + Self::async_comprehension_in_sync_comprehension(ctx, generators); + for generator in generators.iter().filter(|g| g.is_async) { + Self::await_outside_async_function( + ctx, + generator, + AwaitOutsideAsyncFunctionKind::AsyncComprehension, + ); + } + } + Expr::Generator(ast::ExprGenerator { + elt, generators, .. + }) => { + Self::check_generator_expr(elt, generators, ctx); + Self::check_generator_clauses(generators, ctx); + // Note that `await_outside_async_function` is not called here because generators + // are evaluated lazily. See the note in the function for more details. + } + Expr::Name(ast::ExprName { + range, + id, + ctx: expr_ctx, + node_index: _, + }) => { + // test_err write_to_debug_expr + // del __debug__ + // del x, y, __debug__, z + // __debug__ = 1 + // x, y, __debug__, z = 1, 2, 3, 4 + + // test_err del_debug_py39 + // # parse_options: {"target-version": "3.9"} + // del __debug__ + + // test_ok del_debug_py38 + // # parse_options: {"target-version": "3.8"} + // del __debug__ + + // test_ok read_from_debug + // if __debug__: ... + // x = __debug__ + if id == "__debug__" { + match expr_ctx { + ExprContext::Store => Self::add_error( + ctx, + SemanticSyntaxErrorKind::WriteToDebug(WriteToDebugKind::Store), + *range, + ), + ExprContext::Del => { + let version = ctx.python_version(); + if version >= PythonVersion::PY39 { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::WriteToDebug( + WriteToDebugKind::Delete(version), + ), + *range, + ); + } + } + _ => {} + } + } + + // PLE0118 + if let Some(stmt) = ctx.global(id) { + let start = stmt.start(); + if expr.start() < start { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { + name: id.to_string(), + start, + }, + expr.range(), + ); + } + } + } + Expr::Yield(ast::ExprYield { value, .. }) => { + if let Some(value) = value { + // test_err single_star_yield + // def f(): yield *x + Self::invalid_star_expression(value, ctx); + } + Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Yield); + } + Expr::YieldFrom(_) => { + Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::YieldFrom); + if ctx.in_function_scope() && ctx.in_async_context() { + // test_err yield_from_in_async_function + // async def f(): yield from x + + Self::add_error( + ctx, + SemanticSyntaxErrorKind::YieldFromInAsyncFunction, + expr.range(), + ); + } + } + Expr::Await(_) => { + Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await); + Self::await_outside_async_function(ctx, expr, AwaitOutsideAsyncFunctionKind::Await); + } + Expr::Tuple(ast::ExprTuple { + elts, + ctx: expr_ctx, + range, + .. + }) + | Expr::List(ast::ExprList { + elts, + ctx: expr_ctx, + range, + .. + }) => { + Self::multiple_star_expression(ctx, *expr_ctx, elts, *range); + } + Expr::Lambda(ast::ExprLambda { + parameters: Some(parameters), + .. + }) => { + for parameter in parameters { + Self::check_identifier(parameter.name(), ctx); + } + Self::duplicate_parameter_name(parameters, ctx); + } + Expr::Call(ast::ExprCall { arguments, .. }) => { + Self::duplicate_keyword_args(arguments, ctx); + } + _ => {} + } + } + + /// PLE1142 + fn await_outside_async_function( + ctx: &Ctx, + node: Node, + kind: AwaitOutsideAsyncFunctionKind, + ) { + if ctx.in_async_context() { + return; + } + // `await` is allowed at the top level of a Jupyter notebook. + // See: https://ipython.readthedocs.io/en/stable/interactive/autoawait.html. + if ctx.in_module_scope() && ctx.in_notebook() { + return; + } + // Generators are evaluated lazily, so you can use `await` in them. For example: + // + // ```python + // # This is valid + // def f(): + // (await x for x in y) + // (x async for x in y) + // + // # This is invalid + // def f(): + // (x for x in await y) + // [await x for x in y] + // ``` + // + // This check is required in addition to avoiding calling this function in `visit_expr` + // because the generator scope applies to nested parts of the `Expr::Generator` that are + // visited separately. + if ctx.in_generator_context() { + return; + } + Self::add_error( + ctx, + SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(kind), + node.range(), + ); + } + + /// F704 + fn yield_outside_function( + ctx: &Ctx, + expr: &Expr, + kind: YieldOutsideFunctionKind, + ) { + // We are intentionally not inspecting the async status of the scope for now to mimic F704. + // await-outside-async is PLE1142 instead, so we'll end up emitting both syntax errors for + // cases that trigger F704 + + if ctx.in_function_scope() { + return; + } + + if kind.is_await() { + // `await` is allowed at the top level of a Jupyter notebook. + // See: https://ipython.readthedocs.io/en/stable/interactive/autoawait.html. + if ctx.in_module_scope() && ctx.in_notebook() { + return; + } + if ctx.in_await_allowed_context() { + return; + } + } else if ctx.in_yield_allowed_context() { + return; + } + + Self::add_error( + ctx, + SemanticSyntaxErrorKind::YieldOutsideFunction(kind), + expr.range(), + ); + } + + /// Add a [`SemanticSyntaxErrorKind::ReboundComprehensionVariable`] if `expr` rebinds an + /// iteration variable in `generators`. + fn check_generator_expr( + expr: &Expr, + comprehensions: &[ast::Comprehension], + ctx: &Ctx, + ) { + Self::check_rebound_variables( + expr, + comprehension_target_names(comprehensions), + FxHashSet::default(), + ctx, + ); + Self::check_class_body_expr(expr, ctx); + } + + fn check_generator_clauses( + generators: &[ast::Comprehension], + ctx: &Ctx, + ) { + // test_ok starred_comprehension_target + // [item for (*items,) in source] + + // test_err starred_comprehension_target + // [item for *items in source] + for (index, generator) in generators.iter().enumerate() { + Self::invalid_star_expression(&generator.target, ctx); + + for if_expr in &generator.ifs { + Self::check_rebound_variables( + if_expr, + comprehension_target_names(&generators[..=index]), + comprehension_target_names(&generators[index + 1..]), + ctx, + ); + Self::check_class_body_expr(if_expr, ctx); + } + + let mut visitor = ComprehensionIterableNamedExpressionVisitor::default(); + visitor.visit_expr(&generator.iter); + for range in visitor.ranges { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::NamedExpressionInComprehensionIterable, + range, + ); + } + } + } + + fn check_rebound_variables<'a, Ctx: SemanticSyntaxContext>( + expr: &Expr, + targets: FxHashSet<&'a ast::name::Name>, + direct_targets: FxHashSet<&'a ast::name::Name>, + ctx: &Ctx, + ) { + let mut visitor = ReboundComprehensionVisitor { + targets, + direct_targets, + ranges: Vec::new(), + }; + visitor.visit_expr(expr); + + // TODO(brent): With multiple diagnostic ranges, mark both the named expression target + // (currently reported) and the comprehension target it rebinds. + for range in visitor.ranges { + // test_err rebound_comprehension_variable + // [(a := 0) for a in range(0)] + // {(a := 0) for a in range(0)} + // {(a := 0): val for a in range(0)} + // {key: (a := 0) for a in range(0)} + // ((a := 0) for a in range(0)) + // [[(a := 0)] for a in range(0)] + // [(a := 0) for b in range (0) for a in range(0)] + // [(a := 0) for a in range (0) for b in range(0)] + // [((a := 0), (b := 1)) for a in range (0) for b in range(0)] + + // test_ok non_rebound_comprehension_variable + // [a := 0 for x in range(0)] + Self::add_error( + ctx, + SemanticSyntaxErrorKind::ReboundComprehensionVariable, + range, + ); + } + } + + fn check_class_body_expr(expr: &Expr, ctx: &Ctx) { + if !ctx.in_class_body_comprehension() { + return; + } + + let mut visitor = ClassBodyNamedExpressionVisitor::default(); + visitor.visit_expr(expr); + for range in visitor.ranges { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::NamedExpressionInClassBodyComprehension, + range, + ); + } + } + + fn async_comprehension_in_sync_comprehension( + ctx: &Ctx, + generators: &[ast::Comprehension], + ) { + let python_version = ctx.python_version(); + if python_version >= PythonVersion::PY311 { + return; + } + // async allowed at notebook top-level + if ctx.in_notebook() && ctx.in_module_scope() { + return; + } + if !ctx.in_sync_comprehension() { + return; + } + for generator in generators.iter().filter(|generator| generator.is_async) { + // test_ok nested_async_comprehension_py311 + // # parse_options: {"target-version": "3.11"} + // async def f(): return [[x async for x in foo(n)] for n in range(3)] # list + // async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict + // async def h(): return [{x async for x in foo(n)} for n in range(3)] # set + + // test_ok nested_async_comprehension_py310 + // # parse_options: {"target-version": "3.10"} + // async def f(): + // [_ for n in range(3)] + // [_ async for n in range(3)] + // async def f(): + // def g(): ... + // [_ async for n in range(3)] + + // test_ok all_async_comprehension_py310 + // # parse_options: {"target-version": "3.10"} + // async def test(): return [[x async for x in elements(n)] async for n in range(3)] + + // test_err nested_async_comprehension_py310 + // # parse_options: {"target-version": "3.10"} + // async def f(): return [[x async for x in foo(n)] for n in range(3)] # list + // async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict + // async def h(): return [{x async for x in foo(n)} for n in range(3)] # set + // async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)] + // async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)] + Self::add_error( + ctx, + SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(python_version), + generator.range, + ); + } + } +} + +fn is_known_future_feature(name: &str) -> bool { + matches!( + name, + "nested_scopes" + | "generators" + | "division" + | "absolute_import" + | "with_statement" + | "print_function" + | "unicode_literals" + | "barry_as_FLUFL" + | "generator_stop" + | "annotations" + ) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum LazyImportKind { + Import, + ImportFrom, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum LazyImportContext { + Function, + Class, + TryExceptBlocks, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] +pub struct SemanticSyntaxError { + pub kind: SemanticSyntaxErrorKind, + pub range: TextRange, + pub python_version: PythonVersion, +} + +impl Display for SemanticSyntaxError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.kind { + SemanticSyntaxErrorKind::LateFutureImport => { + f.write_str("__future__ imports must be at the top of the file") + } + SemanticSyntaxErrorKind::NamedExpressionInComprehensionIterable => f.write_str( + "assignment expression cannot be used in a comprehension iterable expression", + ), + SemanticSyntaxErrorKind::NamedExpressionInClassBodyComprehension => f.write_str( + "assignment expression within a comprehension cannot be used in a class body", + ), + SemanticSyntaxErrorKind::ReboundComprehensionVariable => { + f.write_str("assignment expression cannot rebind comprehension variable") + } + SemanticSyntaxErrorKind::DuplicateTypeParameter => { + f.write_str("duplicate type parameter") + } + SemanticSyntaxErrorKind::TypeParameterDefaultOrder(name) => { + write!( + f, + "non default type parameter `{name}` follows default type parameter" + ) + } + SemanticSyntaxErrorKind::MultipleCaseAssignment(name) => { + write!(f, "multiple assignments to name `{name}` in pattern") + } + SemanticSyntaxErrorKind::MultipleStarredNamesInSequencePattern => { + f.write_str("multiple starred names in sequence pattern") + } + SemanticSyntaxErrorKind::IrrefutableCasePattern(kind) => match kind { + // These error messages are taken from CPython's syntax errors + IrrefutablePatternKind::Name(name) => { + write!( + f, + "name capture `{name}` makes remaining patterns unreachable" + ) + } + IrrefutablePatternKind::Wildcard => { + f.write_str("wildcard makes remaining patterns unreachable") + } + }, + SemanticSyntaxErrorKind::SingleStarredAssignment => { + f.write_str("starred assignment target must be in a list or tuple") + } + SemanticSyntaxErrorKind::WriteToDebug(kind) => match kind { + WriteToDebugKind::Store => f.write_str("cannot assign to `__debug__`"), + WriteToDebugKind::Delete(python_version) => { + write!( + f, + "cannot delete `__debug__` on Python {python_version} (syntax was removed in 3.9)" + ) + } + }, + SemanticSyntaxErrorKind::InvalidExpression(kind, position) => { + write!(f, "{kind} cannot be used within a {position}") + } + SemanticSyntaxErrorKind::DuplicateMatchKey(key) => { + write!( + f, + "mapping pattern checks duplicate key `{}`", + EscapeDefault(key) + ) + } + SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(name) => { + write!(f, "attribute name `{name}` repeated in class pattern") + } + SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { name, start: _ } => { + write!(f, "name `{name}` is used prior to global declaration") + } + SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { name, start: _ } => { + write!(f, "name `{name}` is used prior to nonlocal declaration") + } + SemanticSyntaxErrorKind::InvalidStarExpression => { + f.write_str("Starred expression cannot be used here") + } + SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(python_version) => { + write!( + f, + "cannot use an asynchronous comprehension inside of a synchronous comprehension \ + on Python {python_version} (syntax was added in 3.11)", + ) + } + SemanticSyntaxErrorKind::YieldOutsideFunction(kind) => { + write!(f, "`{kind}` statement outside of a function") + } + SemanticSyntaxErrorKind::ReturnOutsideFunction => { + f.write_str("`return` statement outside of a function") + } + SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(kind) => { + write!(f, "{kind} outside of an asynchronous function") + } + SemanticSyntaxErrorKind::DuplicateParameter(name) => { + write!(f, r#"Duplicate parameter "{name}""#) + } + SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => { + write!(f, "nonlocal declaration not allowed at module level") + } + SemanticSyntaxErrorKind::DuplicateKeywordArgument(name) => { + write!(f, "Duplicate keyword argument `{name}`") + } + SemanticSyntaxErrorKind::NonlocalAndGlobal(name) => { + write!(f, "name `{name}` is nonlocal and global") + } + SemanticSyntaxErrorKind::AnnotatedGlobal(name) => { + write!(f, "annotated name `{name}` can't be global") + } + SemanticSyntaxErrorKind::AnnotatedNonlocal(name) => { + write!(f, "annotated name `{name}` can't be nonlocal") + } + SemanticSyntaxErrorKind::YieldFromInAsyncFunction => { + f.write_str("`yield from` statement in async function; use `async for` instead") + } + SemanticSyntaxErrorKind::NonModuleImportStar(name) => { + write!(f, "`from {name} import *` only allowed at module level") + } + SemanticSyntaxErrorKind::MultipleStarredExpressions => { + write!(f, "Two starred expressions in assignment") + } + SemanticSyntaxErrorKind::FutureFeatureNotDefined(name) => { + write!(f, "Future feature `{name}` is not defined") + } + SemanticSyntaxErrorKind::LazyImportNotAllowed { context, kind } => { + let statement = match kind { + LazyImportKind::Import => "lazy import", + LazyImportKind::ImportFrom => "lazy from ... import", + }; + let location = match context { + LazyImportContext::Function => "functions", + LazyImportContext::Class => "classes", + LazyImportContext::TryExceptBlocks => "try/except blocks", + }; + write!(f, "{statement} not allowed inside {location}") + } + SemanticSyntaxErrorKind::LazyImportStar => { + f.write_str("lazy from ... import * is not allowed") + } + SemanticSyntaxErrorKind::LazyFutureImport => { + f.write_str("lazy from __future__ import is not allowed") + } + SemanticSyntaxErrorKind::BreakOutsideLoop => f.write_str("`break` outside loop"), + SemanticSyntaxErrorKind::ContinueOutsideLoop => f.write_str("`continue` outside loop"), + SemanticSyntaxErrorKind::GlobalParameter(name) => { + write!( + f, + "name `{name}` cannot refer to a parameter and a global variable" + ) + } + SemanticSyntaxErrorKind::NonlocalParameter(name) => { + write!( + f, + "name `{name}` cannot refer to a parameter and a nonlocal variable" + ) + } + SemanticSyntaxErrorKind::DifferentMatchPatternBindings => { + write!(f, "alternative patterns bind different names") + } + SemanticSyntaxErrorKind::NonlocalWithoutBinding(name) => { + write!(f, "no binding for nonlocal `{name}` found") + } + SemanticSyntaxErrorKind::ReturnInGenerator => { + write!(f, "`return` with value in async generator") + } + } + } +} + +impl Ranged for SemanticSyntaxError { + fn range(&self) -> TextRange { + self.range + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum SemanticSyntaxErrorKind { + /// Represents a `lazy` import statement in an invalid context. + LazyImportNotAllowed { + context: LazyImportContext, + kind: LazyImportKind, + }, + + /// Represents the use of `lazy from ... import *`. + LazyImportStar, + + /// Represents the use of `lazy from __future__ import ...`. + LazyFutureImport, + + /// Represents the use of a `__future__` import after the beginning of a file. + /// + /// ## Examples + /// + /// ```python + /// from pathlib import Path + /// + /// from __future__ import annotations + /// ``` + /// + /// This corresponds to the [`late-future-import`] (`F404`) rule in ruff. + /// + /// [`late-future-import`]: https://docs.astral.sh/ruff/rules/late-future-import/ + LateFutureImport, + + /// Represents the use of an assignment expression within a comprehension iterable clause. + /// + /// ## Examples + /// + /// ```python + /// [x for x in (y := range(3))] + /// [x for x in [z for z in range(3) if (y := z)]] + /// ``` + NamedExpressionInComprehensionIterable, + + /// Represents the use of an assignment expression within a comprehension nested directly or + /// indirectly in a class body. + /// + /// ## Examples + /// + /// ```python + /// class C: + /// [(x := y) for y in range(3)] + /// ``` + NamedExpressionInClassBodyComprehension, + + /// Represents the rebinding of the iteration variable of a list, set, or dict comprehension or + /// a generator expression. + /// + /// ## Examples + /// + /// ```python + /// [(a := 0) for a in range(0)] + /// {(a := 0) for a in range(0)} + /// {(a := 0): val for a in range(0)} + /// {key: (a := 0) for a in range(0)} + /// ((a := 0) for a in range(0)) + /// ``` + ReboundComprehensionVariable, + + /// Represents a duplicate type parameter name in a function definition, class definition, or + /// type alias statement. + /// + /// ## Examples + /// + /// ```python + /// type Alias[T, T] = ... + /// def f[T, T](t: T): ... + /// class C[T, T]: ... + /// ``` + DuplicateTypeParameter, + + /// Represents a duplicate binding in a `case` pattern of a `match` statement. + /// + /// ## Examples + /// + /// ```python + /// match x: + /// case [x, y, x]: ... + /// case x as x: ... + /// case Class(x=1, x=2): ... + /// ``` + MultipleCaseAssignment(ast::name::Name), + + /// Represents multiple starred names in a sequence pattern. + /// + /// ## Examples + /// + /// ```python + /// match x: + /// case [*head, middle, *tail]: ... + /// ``` + MultipleStarredNamesInSequencePattern, + + /// Represents an irrefutable `case` pattern before the last `case` in a `match` statement. + /// + /// According to the [Python reference], "a match statement may have at most one irrefutable + /// case block, and it must be last." + /// + /// ## Examples + /// + /// ```python + /// match x: + /// case value: ... # irrefutable capture pattern + /// case other: ... + /// + /// match x: + /// case _: ... # irrefutable wildcard pattern + /// case other: ... + /// ``` + /// + /// [Python reference]: https://docs.python.org/3/reference/compound_stmts.html#irrefutable-case-blocks + IrrefutableCasePattern(IrrefutablePatternKind), + + /// Represents a single starred assignment target outside of a tuple or list. + /// + /// ## Examples + /// + /// ```python + /// *a = (1,) # SyntaxError + /// ``` + /// + /// A starred assignment target can only occur within a tuple or list: + /// + /// ```python + /// b, *a = 1, 2, 3 + /// (*a,) = 1, 2, 3 + /// [*a] = 1, 2, 3 + /// ``` + SingleStarredAssignment, + + /// Represents a write to `__debug__`. This includes simple assignments and deletions as well + /// other kinds of statements that can introduce bindings, such as type parameters in functions, + /// classes, and aliases, `match` arms, and imports, among others. + /// + /// ## Examples + /// + /// ```python + /// del __debug__ + /// __debug__ = False + /// def f(__debug__): ... + /// class C[__debug__]: ... + /// ``` + /// + /// See [BPO 45000] for more information. + /// + /// [BPO 45000]: https://github.com/python/cpython/issues/89163 + WriteToDebug(WriteToDebugKind), + + /// Represents the use of an invalid expression kind in one of several locations. + /// + /// The kinds include `yield` and `yield from` expressions and named expressions, and locations + /// include type parameter bounds and defaults, type annotations, type aliases, and base class + /// lists. + /// + /// ## Examples + /// + /// ```python + /// type X[T: (yield 1)] = int + /// type Y = (yield 1) + /// def f[T](x: int) -> (y := 3): return x + /// ``` + InvalidExpression(InvalidExpressionKind, InvalidExpressionPosition), + + /// Represents a duplicate key in a `match` mapping pattern. + /// + /// The [CPython grammar] allows keys in mapping patterns to be literals or attribute accesses: + /// + /// ```text + /// key_value_pattern: + /// | (literal_expr | attr) ':' pattern + /// ``` + /// + /// But only literals are checked for duplicates: + /// + /// ```pycon + /// >>> match x: + /// ... case {"x": 1, "x": 2}: ... + /// ... + /// File "", line 2 + /// case {"x": 1, "x": 2}: ... + /// ^^^^^^^^^^^^^^^^ + /// SyntaxError: mapping pattern checks duplicate key ('x') + /// >>> match x: + /// ... case {x.a: 1, x.a: 2}: ... + /// ... + /// >>> + /// ``` + /// + /// ## Examples + /// + /// ```python + /// match x: + /// case {"x": 1, "x": 2}: ... + /// ``` + /// + /// [CPython grammar]: https://docs.python.org/3/reference/grammar.html + DuplicateMatchKey(String), + + /// Represents a duplicate attribute name in a `match` class pattern. + /// + /// ## Examples + /// + /// ```python + /// match x: + /// case Class(x=1, x=2): ... + /// ``` + DuplicateMatchClassAttribute(ast::name::Name), + + /// Represents the use of a `global` variable before its `global` declaration. + /// + /// ## Examples + /// + /// ```python + /// counter = 1 + /// def increment(): + /// print(f"Adding 1 to {counter}") + /// global counter + /// counter += 1 + /// ``` + /// + /// ## Known Issues + /// + /// Note that the order in which the parts of a `try` statement are visited was changed in 3.13, + /// as tracked in Python issue [#111123]. For example, this code was valid on Python 3.12: + /// + /// ```python + /// a = 10 + /// def g(): + /// try: + /// 1 / 0 + /// except: + /// a = 1 + /// else: + /// global a + /// ``` + /// + /// While this more intuitive behavior aligned with the textual order was a syntax error: + /// + /// ```python + /// a = 10 + /// def f(): + /// try: + /// pass + /// except: + /// global a + /// else: + /// a = 1 # SyntaxError: name 'a' is assigned to before global declaration + /// ``` + /// + /// This was reversed in version 3.13 to make the second case valid and the first case a syntax + /// error. We intentionally enforce the 3.13 ordering, regardless of the Python version, which + /// will lead to both false positives and false negatives on 3.12 code that takes advantage of + /// the old behavior. However, as mentioned in the Python issue, we expect code relying on this + /// to be very rare and not worth the additional complexity to detect. + /// + /// [#111123]: https://github.com/python/cpython/issues/111123 + LoadBeforeGlobalDeclaration { name: String, start: TextSize }, + + /// Represents the use of a `nonlocal` variable before its `nonlocal` declaration. + /// + /// ## Examples + /// + /// ```python + /// def f(): + /// counter = 0 + /// def increment(): + /// print(f"Adding 1 to {counter}") + /// nonlocal counter # SyntaxError: name 'counter' is used prior to nonlocal declaration + /// counter += 1 + /// ``` + /// + /// ## Known Issues + /// + /// See [`LoadBeforeGlobalDeclaration`][Self::LoadBeforeGlobalDeclaration]. + LoadBeforeNonlocalDeclaration { name: String, start: TextSize }, + + /// Represents the use of a starred expression in an invalid location, such as a `return` or + /// `yield` statement. + /// + /// ## Examples + /// + /// ```python + /// def f(): return *x + /// def f(): yield *x + /// for _ in *x: ... + /// for *x in xs: ... + /// ``` + InvalidStarExpression, + + /// Represents the use of an asynchronous comprehension inside of a synchronous comprehension + /// before Python 3.11. + /// + /// ## Examples + /// + /// Before Python 3.11, code like this produces a syntax error because of the implicit function + /// scope introduced by the outer comprehension: + /// + /// ```python + /// async def elements(n): yield n + /// + /// async def test(): return { n: [x async for x in elements(n)] for n in range(3)} + /// ``` + /// + /// This was discussed in [BPO 33346] and fixed in Python 3.11. + /// + /// [BPO 33346]: https://github.com/python/cpython/issues/77527 + AsyncComprehensionInSyncComprehension(PythonVersion), + + /// Represents the use of `yield`, `yield from`, or `await` outside of a function scope. + /// + /// + /// ## Examples + /// + /// `yield` and `yield from` are only allowed if the immediately-enclosing scope is a function + /// or lambda and not allowed otherwise: + /// + /// ```python + /// yield 1 # error + /// + /// def f(): + /// [(yield 1) for x in y] # error + /// ``` + /// + /// `await` is additionally allowed in comprehensions, if the comprehension itself is in a + /// function scope: + /// + /// ```python + /// await 1 # error + /// + /// async def f(): + /// await 1 # okay + /// [await 1 for x in y] # also okay + /// ``` + /// + /// This last case _is_ an error, but it has to do with the lambda not being an async function. + /// For the sake of this error kind, this is okay. + /// + /// ## References + /// + /// See [PEP 255] for details on `yield`, [PEP 380] for the extension to `yield from`, [PEP 492] + /// for async-await syntax, and [PEP 530] for async comprehensions. + /// + /// [PEP 255]: https://peps.python.org/pep-0255/ + /// [PEP 380]: https://peps.python.org/pep-0380/ + /// [PEP 492]: https://peps.python.org/pep-0492/ + /// [PEP 530]: https://peps.python.org/pep-0530/ + YieldOutsideFunction(YieldOutsideFunctionKind), + + /// Represents the use of `return` outside of a function scope. + ReturnOutsideFunction, + + /// Represents the use of `await`, `async for`, or `async with` outside of an asynchronous + /// function. + /// + /// ## Examples + /// + /// ```python + /// def f(): + /// await 1 # error + /// async for x in y: ... # error + /// async with x: ... # error + /// ``` + AwaitOutsideAsyncFunction(AwaitOutsideAsyncFunctionKind), + + /// Represents a duplicate parameter name in a function or lambda expression. + /// + /// ## Examples + /// + /// ```python + /// def f(x, x): ... + /// lambda x, x: ... + /// ``` + DuplicateParameter(String), + + /// Represents duplicated keyword arguments in a function call or class definition. + /// + /// ## Examples + /// + /// ```python + /// def f(x): ... + /// f(x=1, x=2) + /// class C(metaclass=type, metaclass=type): ... + /// ``` + DuplicateKeywordArgument(String), + + /// Represents a nonlocal declaration at module level + NonlocalDeclarationAtModuleLevel, + + /// Represents the same variable declared as both nonlocal and global + NonlocalAndGlobal(String), + + /// Represents a type annotation on a variable that's been declared global + AnnotatedGlobal(String), + + /// Represents a type annotation on a variable that's been declared nonlocal + AnnotatedNonlocal(String), + + /// Represents the use of `yield from` inside an asynchronous function. + YieldFromInAsyncFunction, + + /// Represents the use of `from import *` outside module scope. + NonModuleImportStar(String), + + /// Represents the use of more than one starred expression in an assignment. + /// + /// Python only allows a single starred target when unpacking values on the + /// left-hand side of an assignment. Using multiple starred expressions makes + /// the statement invalid and results in a `SyntaxError`. + MultipleStarredExpressions, + + /// Represents the use of a `__future__` feature that is not defined. + FutureFeatureNotDefined(String), + + /// Represents the use of a `break` statement outside of a loop. + BreakOutsideLoop, + + /// Represents the use of a `continue` statement outside of a loop. + ContinueOutsideLoop, + + /// Represents a function parameter that is also declared as `global`. + /// + /// Declaring a parameter as `global` is invalid, since parameters are already + /// bound in the local scope of the function. Using `global` on them introduces + /// ambiguity and will result in a `SyntaxError`. + GlobalParameter(String), + + /// Represents a function parameter that is also declared as `nonlocal`. + /// + /// Declaring a parameter as `nonlocal` is invalid, since parameters are already + /// bound in a local scope of the function. using `nonlocal` on them introduces + /// ambiguity and will result in a `SyntaxError`. + NonlocalParameter(String), + + /// Represents the use of alternative patterns in a `match` statement that bind different names. + /// + /// Python requires all alternatives in an OR pattern (`|`) to bind the same set of names. + /// Using different names results in a `SyntaxError`. + /// + /// ## Example: + /// + /// ```python + /// match 5: + /// case [x] | [y]: # error + /// ... + /// ``` + DifferentMatchPatternBindings, + + /// Represents a nonlocal statement for a name that has no binding in an enclosing scope. + NonlocalWithoutBinding(String), + + /// Represents a default type parameter followed by a non-default type parameter. + TypeParameterDefaultOrder(String), + + /// Represents a `return` statement with a value in an asynchronous generator. + ReturnInGenerator, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum AwaitOutsideAsyncFunctionKind { + Await, + AsyncFor, + AsyncWith, + AsyncComprehension, +} + +impl Display for AwaitOutsideAsyncFunctionKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + AwaitOutsideAsyncFunctionKind::Await => "`await`", + AwaitOutsideAsyncFunctionKind::AsyncFor => "`async for`", + AwaitOutsideAsyncFunctionKind::AsyncWith => "`async with`", + AwaitOutsideAsyncFunctionKind::AsyncComprehension => "asynchronous comprehension", + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum YieldOutsideFunctionKind { + Yield, + YieldFrom, + Await, +} + +impl YieldOutsideFunctionKind { + pub fn is_await(&self) -> bool { + matches!(self, Self::Await) + } +} + +impl Display for YieldOutsideFunctionKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + YieldOutsideFunctionKind::Yield => "yield", + YieldOutsideFunctionKind::YieldFrom => "yield from", + YieldOutsideFunctionKind::Await => "await", + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum InvalidExpressionPosition { + TypeVarBound, + TypeVarDefault, + TypeVarTupleDefault, + ParamSpecDefault, + TypeAnnotation, + GenericDefinition, + TypeAlias, +} + +impl Display for InvalidExpressionPosition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + InvalidExpressionPosition::TypeVarBound => "TypeVar bound", + InvalidExpressionPosition::TypeVarDefault => "TypeVar default", + InvalidExpressionPosition::TypeVarTupleDefault => "TypeVarTuple default", + InvalidExpressionPosition::ParamSpecDefault => "ParamSpec default", + InvalidExpressionPosition::TypeAnnotation => "type annotation", + InvalidExpressionPosition::GenericDefinition => "generic definition", + InvalidExpressionPosition::TypeAlias => "type alias", + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum InvalidExpressionKind { + Yield, + NamedExpr, + Await, +} + +impl Display for InvalidExpressionKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + InvalidExpressionKind::Yield => "yield expression", + InvalidExpressionKind::NamedExpr => "named expression", + InvalidExpressionKind::Await => "await expression", + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum WriteToDebugKind { + Store, + Delete(PythonVersion), +} + +fn comprehension_target_names( + comprehensions: &[ast::Comprehension], +) -> FxHashSet<&ast::name::Name> { + let mut visitor = helpers::StoredNameFinder::default(); + for comprehension in comprehensions { + visitor.visit_expr(&comprehension.target); + } + visitor.names.into_values().map(|name| &name.id).collect() +} + +#[derive(Default)] +struct ComprehensionIterableNamedExpressionVisitor { + ranges: Vec, +} + +impl Visitor<'_> for ComprehensionIterableNamedExpressionVisitor { + fn visit_expr(&mut self, expr: &Expr) { + if let Expr::Named(ast::ExprNamed { target, range, .. }) = expr + && target.is_name_expr() + { + self.ranges.push(*range); + } + walk_expr(self, expr); + } +} + +#[derive(Default)] +struct ClassBodyNamedExpressionVisitor { + ranges: Vec, +} + +impl Visitor<'_> for ClassBodyNamedExpressionVisitor { + fn visit_expr(&mut self, expr: &Expr) { + match expr { + Expr::Lambda(ast::ExprLambda { parameters, .. }) => { + // Defaults execute in the enclosing comprehension; lambda bodies do not. + if let Some(parameters) = parameters { + self.visit_parameters(parameters); + } + return; + } + Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::Generator(_) => { + // Nested comprehensions are checked when normal traversal reaches them. + return; + } + Expr::Named(ast::ExprNamed { target, range, .. }) if target.is_name_expr() => { + self.ranges.push(*range); + } + _ => {} + } + walk_expr(self, expr); + } +} + +/// Searches for named expressions (`x := y`) rebinding a comprehension or generator expression's +/// iteration variables. +struct ReboundComprehensionVisitor<'a> { + /// Targets that apply inside nested comprehensions. + targets: FxHashSet<&'a ast::name::Name>, + /// Targets from later filter clauses, which do not apply inside nested comprehensions. + direct_targets: FxHashSet<&'a ast::name::Name>, + ranges: Vec, +} + +impl Visitor<'_> for ReboundComprehensionVisitor<'_> { + fn visit_expr(&mut self, expr: &Expr) { + match expr { + Expr::Lambda(ast::ExprLambda { parameters, .. }) => { + if let Some(parameters) = parameters { + self.visit_parameters(parameters); + } + return; + } + Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::Generator(_) + if !self.direct_targets.is_empty() => + { + let direct_targets = std::mem::take(&mut self.direct_targets); + walk_expr(self, expr); + self.direct_targets = direct_targets; + return; + } + Expr::Named(ast::ExprNamed { target, .. }) => { + if let Expr::Name(ast::ExprName { id, range, .. }) = &**target + && (self.targets.contains(id) || self.direct_targets.contains(id)) + { + self.ranges.push(*range); + } + } + _ => {} + } + walk_expr(self, expr); + } +} + +#[derive(Default)] +struct ReturnVisitor { + return_range: Option, + has_yield: bool, +} + +impl Visitor<'_> for ReturnVisitor { + fn visit_stmt(&mut self, stmt: &Stmt) { + match stmt { + // Do not recurse into nested functions; they're evaluated separately. + Stmt::FunctionDef(_) | Stmt::ClassDef(_) => {} + Stmt::Return(ast::StmtReturn { + value: Some(_), + range, + .. + }) => { + self.return_range = Some(*range); + walk_stmt(self, stmt); + } + _ => walk_stmt(self, stmt), + } + } + + fn visit_expr(&mut self, expr: &Expr) { + match expr { + Expr::Lambda(_) => {} + Expr::Yield(_) | Expr::YieldFrom(_) => { + self.has_yield = true; + } + _ => walk_expr(self, expr), + } + } +} + +struct MatchPatternVisitor<'a, Ctx> { + names: FxHashSet<&'a ast::name::Name>, + ctx: &'a Ctx, +} + +impl<'a, Ctx: SemanticSyntaxContext> MatchPatternVisitor<'a, Ctx> { + fn visit_pattern(&mut self, pattern: &'a Pattern) { + // test_ok class_keyword_in_case_pattern + // match 2: + // case Class(x=x): ... + + // test_err multiple_assignment_in_case_pattern + // match 2: + // case [y, z, y]: ... # MatchSequence + // case [y, z, *y]: ... # MatchSequence + // case [y, y, y]: ... # MatchSequence multiple + // case {1: x, 2: x}: ... # MatchMapping duplicate pattern + // case {1: x, **x}: ... # MatchMapping duplicate in **rest + // case Class(x, x): ... # MatchClass positional + // case Class(y=x, z=x): ... # MatchClass keyword + // case [x] | {1: x} | Class(y=x, z=x): ... # MatchOr + // case x as x: ... # MatchAs + + // test_err multiple_starred_names_in_sequence_pattern + // match subject: + // case *first, *second, *third: ... + match pattern { + Pattern::MatchValue(_) | Pattern::MatchSingleton(_) => {} + Pattern::MatchStar(ast::PatternMatchStar { name, .. }) => { + if let Some(name) = name { + self.insert(name); + } + } + Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) => { + let mut seen_star_pattern = false; + for pattern in patterns { + if pattern.is_match_star() { + if seen_star_pattern { + SemanticSyntaxChecker::add_error( + self.ctx, + SemanticSyntaxErrorKind::MultipleStarredNamesInSequencePattern, + pattern.range(), + ); + } + seen_star_pattern = true; + } + self.visit_pattern(pattern); + } + } + Pattern::MatchMapping(ast::PatternMatchMapping { + keys, + patterns, + rest, + .. + }) => { + for pattern in patterns { + self.visit_pattern(pattern); + } + if let Some(rest) = rest { + self.insert(rest); + } + + let mut seen = FxHashSet::default(); + for key in keys + .iter() + // Signed and complex numbers are allowed as keys but are represented as unary + // or binary expressions rather than literals. + .filter(|key| { + key.is_literal_expr() || key.is_unary_op_expr() || key.is_bin_op_expr() + }) + { + if !seen.insert(HashableExpr::from(key)) { + let key_range = key.range(); + let duplicate_key = self.ctx.source()[key_range].to_string(); + // test_ok duplicate_match_key_attr + // match x: + // case {x.a: 1, x.a: 2}: ... + + // test_err duplicate_match_key + // match x: + // case {"x": 1, "x": 2}: ... + // case {b"x": 1, b"x": 2}: ... + // case {0: 1, 0: 2}: ... + // case {1.0: 1, 1.0: 2}: ... + // case {1.0 + 2j: 1, 1.0 + 2j: 2}: ... + // case {True: 1, True: 2}: ... + // case {None: 1, None: 2}: ... + // case {0: 1, False: 2}: ... + // case {1.0: 1, True: 2}: ... + // case {-0: 1, False: 2}: ... + // case {1 + 0j: 1, True: 2}: ... + // case { + // """x + // y + // z + // """: 1, + // """x + // y + // z + // """: 2}: ... + // case {"x": 1, "x": 2, "x": 3}: ... + // case {0: 1, "x": 1, 0: 2, "x": 2}: ... + // case [{"x": 1, "x": 2}]: ... + // case Foo(x=1, y={"x": 1, "x": 2}): ... + // case [Foo(x=1), Foo(x=1, y={"x": 1, "x": 2})]: ... + // case {2: 1, 2.0: 2}: ... + // case {9007199254740993: 1, 9007199254740993 + 0j: 2}: ... + SemanticSyntaxChecker::add_error( + self.ctx, + SemanticSyntaxErrorKind::DuplicateMatchKey(duplicate_key), + key_range, + ); + } + } + } + Pattern::MatchClass(ast::PatternMatchClass { arguments, .. }) => { + for pattern in &arguments.patterns { + self.visit_pattern(pattern); + } + let mut seen = FxHashSet::default(); + for keyword in &arguments.keywords { + if !seen.insert(&keyword.attr.id) { + // test_err duplicate_match_class_attr + // match x: + // case Class(x=1, x=2): ... + // case [Class(x=1, x=2)]: ... + // case {"x": x, "y": Foo(x=1, x=2)}: ... + // case [{}, {"x": x, "y": Foo(x=1, x=2)}]: ... + // case Class(x=1, d={"x": 1, "x": 2}, other=Class(x=1, x=2)): ... + SemanticSyntaxChecker::add_error( + self.ctx, + SemanticSyntaxErrorKind::DuplicateMatchClassAttribute( + keyword.attr.id.clone(), + ), + keyword.attr.range, + ); + } + self.visit_pattern(&keyword.pattern); + } + } + Pattern::MatchAs(ast::PatternMatchAs { pattern, name, .. }) => { + if let Some(pattern) = pattern { + self.visit_pattern(pattern); + } + if let Some(name) = name { + self.insert(name); + } + } + Pattern::MatchOr(ast::PatternMatchOr { + patterns, range, .. + }) => { + // each of these patterns should be visited separately because patterns can only be + // duplicated within a single arm of the or pattern. For example, the case below is + // a valid pattern. + + // test_ok multiple_assignment_in_case_pattern + // match 2: + // case Class(x) | [x] | x: ... + + let mut previous_names: Option> = None; + for pattern in patterns { + let mut visitor = Self { + names: FxHashSet::default(), + ctx: self.ctx, + }; + visitor.visit_pattern(pattern); + let Some(prev) = &previous_names else { + previous_names = Some(visitor.names); + continue; + }; + if prev.symmetric_difference(&visitor.names).next().is_some() { + // test_err different_match_pattern_bindings + // match x: + // case [a] | [b]: ... + // case [a] | []: ... + // case (x, y) | (x,): ... + // case [a, _] | [a, b]: ... + // case (x, (y | z)): ... + // case [a] | [b] | [c]: ... + // case [] | [a]: ... + // case [a] | [C(x)]: ... + // case [[a] | [b]]: ... + // case [C(a)] | [C(b)]: ... + // case [C(D(a))] | [C(D(b))]: ... + // case [(a, b)] | [(c, d)]: ... + + // test_ok different_match_pattern_bindings + // match x: + // case [a] | [a]: ... + // case (x, y) | (x, y): ... + // case (x, (y | y)): ... + // case [a, _] | [a, _]: ... + // case [a] | [C(a)]: ... + + // test_ok nested_alternative_patterns + // match ruff: + // case {"lint": {"select": x} | {"extend-select": x}} | {"select": x}: + // ... + // match 42: + // case [[x] | [x]] | x: ... + // match 42: + // case [[x | x] | [x]] | x: ... + // match 42: + // case ast.Subscript(n, ast.Constant() | ast.Slice()) | ast.Attribute(n): ... + SemanticSyntaxChecker::add_error( + self.ctx, + SemanticSyntaxErrorKind::DifferentMatchPatternBindings, + *range, + ); + break; + } + self.names.extend(visitor.names); + } + } + } + } + + /// Add an identifier to the set of visited names in `self` and emit a [`SemanticSyntaxError`] + /// if `ident` has already been seen. + fn insert(&mut self, ident: &'a ast::Identifier) { + if !self.names.insert(&ident.id) { + SemanticSyntaxChecker::add_error( + self.ctx, + SemanticSyntaxErrorKind::MultipleCaseAssignment(ident.id.clone()), + ident.range(), + ); + } + // test_err debug_shadow_match + // match x: + // case __debug__: ... + SemanticSyntaxChecker::check_identifier(ident, self.ctx); + } +} + +struct InvalidExpressionVisitor<'a, Ctx> { + /// Context used for emitting errors. + ctx: &'a Ctx, + + position: InvalidExpressionPosition, +} + +impl Visitor<'_> for InvalidExpressionVisitor<'_, Ctx> +where + Ctx: SemanticSyntaxContext, +{ + fn visit_expr(&mut self, expr: &Expr) { + match expr { + Expr::Named(ast::ExprNamed { range, .. }) => { + SemanticSyntaxChecker::add_error( + self.ctx, + SemanticSyntaxErrorKind::InvalidExpression( + InvalidExpressionKind::NamedExpr, + self.position, + ), + *range, + ); + } + Expr::Yield(ast::ExprYield { range, .. }) + | Expr::YieldFrom(ast::ExprYieldFrom { range, .. }) => { + SemanticSyntaxChecker::add_error( + self.ctx, + SemanticSyntaxErrorKind::InvalidExpression( + InvalidExpressionKind::Yield, + self.position, + ), + *range, + ); + } + Expr::Await(ast::ExprAwait { range, .. }) => { + SemanticSyntaxChecker::add_error( + self.ctx, + SemanticSyntaxErrorKind::InvalidExpression( + InvalidExpressionKind::Await, + self.position, + ), + *range, + ); + } + _ => {} + } + ast::visitor::walk_expr(self, expr); + } + + fn visit_type_param(&mut self, type_param: &ast::TypeParam) { + match type_param { + ast::TypeParam::TypeVar(ast::TypeParamTypeVar { bound, default, .. }) => { + if let Some(expr) = bound { + self.position = InvalidExpressionPosition::TypeVarBound; + self.visit_expr(expr); + } + if let Some(expr) = default { + self.position = InvalidExpressionPosition::TypeVarDefault; + self.visit_expr(expr); + } + } + ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { default, .. }) => { + if let Some(expr) = default { + self.position = InvalidExpressionPosition::TypeVarTupleDefault; + self.visit_expr(expr); + } + } + ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { default, .. }) => { + if let Some(expr) = default { + self.position = InvalidExpressionPosition::ParamSpecDefault; + self.visit_expr(expr); + } + } + } + } +} + +/// Information needed from a parent visitor to emit semantic syntax errors. +/// +/// Note that the `in_*_scope` methods should refer to the immediately-enclosing scope. For example, +/// `in_function_scope` should return true for this case: +/// +/// ```python +/// def f(): +/// x # here +/// ``` +/// +/// but not for this case: +/// +/// ```python +/// def f(): +/// class C: +/// x # here +/// ``` +/// +/// In contrast, the `in_*_context` methods should traverse parent scopes. For example, +/// `in_function_context` should return true for this case: +/// +/// ```python +/// def f(): +/// [x # here +/// for x in range(3)] +/// ``` +/// +/// but not here: +/// +/// ```python +/// def f(): +/// class C: +/// x # here, classes break function scopes +/// ``` +pub trait SemanticSyntaxContext { + /// Returns `true` if `__future__`-style type annotations are enabled. + fn future_annotations_or_stub(&self) -> bool; + + /// Returns the nearest invalid context for a `lazy` import statement, if any. + /// + /// This should return the innermost relevant restriction in order of precedence: + /// function, class, then `try`/`except`. + fn lazy_import_context(&self) -> Option; + + /// The target Python version for detecting backwards-incompatible syntax changes. + fn python_version(&self) -> PythonVersion; + + /// Returns the source text under analysis. + fn source(&self) -> &str; + + /// Return the [`TextRange`] at which a name is declared as `global` in the current scope. + fn global(&self, name: &str) -> Option; + + /// Returns `true` if `name` has a binding in an enclosing scope. + fn has_nonlocal_binding(&self, name: &str) -> bool; + + /// Returns `true` if the visitor is currently in an async context, i.e. an async function. + fn in_async_context(&self) -> bool; + + /// Returns `true` if the visitor is currently in a context where the `await` keyword is + /// allowed. + /// + /// Note that this is method is primarily used to report `YieldOutsideFunction` errors for + /// `await` outside function scopes, irrespective of their async status. As such, this differs + /// from `in_async_context` in two ways: + /// + /// 1. `await` is allowed in a lambda, despite it not being async + /// 2. `await` is allowed in any function, regardless of its async status + /// + /// In short, only nested class definitions should cause this method to return `false`, for + /// example: + /// + /// ```python + /// def f(): + /// await 1 # okay, in a function + /// class C: + /// await 1 # error + /// ``` + /// + /// See the trait-level documentation for more details. + fn in_await_allowed_context(&self) -> bool; + + /// Returns `true` if the visitor is currently in a context where `yield` and `yield from` + /// expressions are allowed. + /// + /// Yield expressions are allowed only in: + /// 1. Function definitions + /// 2. Lambda expressions + /// + /// Unlike `await`, yield is not allowed in: + /// - Comprehensions (list, set, dict) + /// - Generator expressions + /// - Class definitions + /// + /// This method should traverse parent scopes to check if the closest relevant scope + /// is a function or lambda, and that no disallowed context (class, comprehension, generator) + /// intervenes. For example: + /// + /// ```python + /// def f(): + /// yield 1 # okay, in a function + /// lambda: (yield 1) # okay, in a lambda + /// + /// [(yield 1) for x in range(3)] # error, in a comprehension + /// ((yield 1) for x in range(3)) # error, in a generator expression + /// class C: + /// yield 1 # error, in a class within a function + /// ``` + /// + fn in_yield_allowed_context(&self) -> bool; + + /// Returns `true` if the visitor is currently inside of a synchronous comprehension. + /// + /// This method is necessary because `in_async_context` only checks for the nearest, enclosing + /// function to determine the (a)sync context. Instead, this method will search all enclosing + /// scopes until it finds a sync comprehension. As a result, the two methods will typically be + /// used together. + fn in_sync_comprehension(&self) -> bool; + + /// Returns `true` if a comprehension introduced at the current position is nested directly + /// or indirectly in a class body, without crossing a function or lambda boundary. + fn in_class_body_comprehension(&self) -> bool; + + /// Returns `true` if the visitor is at the top-level module scope. + fn in_module_scope(&self) -> bool; + + /// Returns `true` if the visitor is in a function scope. + fn in_function_scope(&self) -> bool; + + /// Returns `true` if the visitor is within a generator scope. + /// + /// Note that this refers to an `Expr::Generator` precisely, not to comprehensions more + /// generally. + fn in_generator_context(&self) -> bool; + + /// Returns `true` if the source file is a Jupyter notebook. + fn in_notebook(&self) -> bool; + + fn report_semantic_error(&self, error: SemanticSyntaxError); + + /// Returns `true` if the visitor is inside a `for` or `while` loop. + fn in_loop_context(&self) -> bool; + + /// Returns `true` if `name` is a bound parameter in the current function or lambda scope. + fn is_bound_parameter(&self, name: &str) -> bool; +} + +/// Modified version of [`std::str::EscapeDefault`] that does not escape single or double quotes. +struct EscapeDefault<'a>(&'a str); + +impl Display for EscapeDefault<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::fmt::Write; + + for c in self.0.chars() { + match c { + '\'' | '\"' => f.write_char(c)?, + _ => write!(f, "{}", c.escape_default())?, + } + } + Ok(()) + } +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__assignment.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__assignment.snap new file mode 100644 index 00000000000..fd236f4c7d8 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__assignment.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + Identifier 0..10, + Equal 11..12, + Int 13..15, + Plus 16..17, + Int 18..19, + Minus 19..20, + Int 20..21, + Newline 21..21, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_at_root.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_at_root.snap new file mode 100644 index 00000000000..1721a9ec5b3 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_at_root.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(&source) +--- +## Tokens +``` +[ + If 0..2, + True 3..7, + Colon 7..8, + Newline 8..9, + Indent 9..15, + Int 15..16, + Newline 16..17, + If 21..23, + True 24..28, + Colon 28..29, + Newline 29..30, + Indent 30..40, + Int 40..41, + Newline 41..42, + Dedent 42..42, + Dedent 42..42, + Else 42..46, + Colon 46..47, + Int 53..54, + Newline 54..55, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_indentation.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_indentation.snap new file mode 100644 index 00000000000..dfeb61ba2a2 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_indentation.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(&source) +--- +## Tokens +``` +[ + If 0..2, + True 3..7, + Colon 7..8, + Newline 8..9, + Indent 9..13, + Int 13..14, + Newline 14..15, + Int 29..30, + Newline 30..31, + Int 37..38, + Newline 38..39, + Dedent 39..39, + Else 39..43, + Colon 43..44, + Newline 44..45, + Indent 45..49, + Pass 49..53, + Newline 53..54, + Dedent 54..54, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_mismatch_indentation.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_mismatch_indentation.snap new file mode 100644 index 00000000000..3d9d62651a8 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_mismatch_indentation.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_invalid(&source, Mode::Module)" +--- +## Tokens +``` +[ + If 0..2, + True 3..7, + Colon 7..8, + Newline 8..9, + Indent 9..13, + Int 13..14, + Newline 14..15, + Unknown 15..23, + Int 23..24, + Newline 24..25, +] +``` +## Errors +``` +[ + LexicalError { + error: IndentationError, + location: 24..24, + }, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__bom.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__bom.snap new file mode 100644 index 00000000000..51178c96245 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__bom.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + Identifier 3..4, + Equal 5..6, + Int 7..8, + Newline 8..8, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__bom_with_offset.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__bom_with_offset.snap new file mode 100644 index 00000000000..3703cb3567c --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__bom_with_offset.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_source_with_offset(source, TextSize::new(7))" +--- +## Tokens +``` +[ + Identifier 7..8, + Plus 9..10, + Identifier 11..12, + Newline 12..12, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__bom_with_offset_edge.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__bom_with_offset_edge.snap new file mode 100644 index 00000000000..b32e84f8433 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__bom_with_offset_edge.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_source_with_offset(source, TextSize::new(11))" +--- +## Tokens +``` +[ + Identifier 11..12, + Newline 12..12, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__comment_until_mac_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__comment_until_mac_eol.snap new file mode 100644 index 00000000000..4947a0472ad --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__comment_until_mac_eol.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: comment_until_eol(MAC_EOL) +--- +## Tokens +``` +[ + Int 0..3, + Comment 5..10, + Newline 10..11, + Int 11..14, + Newline 14..14, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__comment_until_unix_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__comment_until_unix_eol.snap new file mode 100644 index 00000000000..5028411b20a --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__comment_until_unix_eol.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: comment_until_eol(UNIX_EOL) +--- +## Tokens +``` +[ + Int 0..3, + Comment 5..10, + Newline 10..11, + Int 11..14, + Newline 14..14, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__comment_until_windows_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__comment_until_windows_eol.snap new file mode 100644 index 00000000000..ccf21644f82 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__comment_until_windows_eol.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: comment_until_eol(WINDOWS_EOL) +--- +## Tokens +``` +[ + Int 0..3, + Comment 5..10, + Newline 10..12, + Int 12..15, + Newline 15..15, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__dedent_after_whitespace.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__dedent_after_whitespace.snap new file mode 100644 index 00000000000..e0072326a41 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__dedent_after_whitespace.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + If 0..2, + Identifier 3..8, + Colon 8..9, + Newline 9..10, + Indent 10..14, + If 14..16, + Identifier 17..23, + Colon 23..24, + Newline 24..25, + Indent 25..33, + Pass 33..37, + Newline 37..38, + Dedent 42..42, + Identifier 42..45, + Newline 45..46, + Dedent 46..46, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_mac_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_mac_eol.snap new file mode 100644 index 00000000000..1fd6b1e0dd1 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_mac_eol.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: double_dedent_with_eol(MAC_EOL) +--- +## Tokens +``` +[ + Def 0..3, + Identifier 4..7, + Lpar 7..8, + Rpar 8..9, + Colon 9..10, + Newline 10..11, + Indent 11..12, + If 12..14, + Identifier 15..16, + Colon 16..17, + Newline 17..18, + NonLogicalNewline 18..19, + Indent 19..21, + Return 21..27, + Int 28..30, + Newline 30..31, + NonLogicalNewline 31..32, + Dedent 32..32, + Dedent 32..32, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_mac_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_mac_eol.snap new file mode 100644 index 00000000000..289631111e4 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_mac_eol.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: double_dedent_with_tabs_eol(MAC_EOL) +--- +## Tokens +``` +[ + Def 0..3, + Identifier 4..7, + Lpar 7..8, + Rpar 8..9, + Colon 9..10, + Newline 10..11, + Indent 11..12, + If 12..14, + Identifier 15..16, + Colon 16..17, + Newline 17..18, + NonLogicalNewline 18..19, + Indent 19..22, + Return 22..28, + Int 29..31, + Newline 31..32, + NonLogicalNewline 32..33, + Dedent 33..33, + Dedent 33..33, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_unix_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_unix_eol.snap new file mode 100644 index 00000000000..d05e35f5d75 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_unix_eol.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: double_dedent_with_tabs_eol(UNIX_EOL) +--- +## Tokens +``` +[ + Def 0..3, + Identifier 4..7, + Lpar 7..8, + Rpar 8..9, + Colon 9..10, + Newline 10..11, + Indent 11..12, + If 12..14, + Identifier 15..16, + Colon 16..17, + Newline 17..18, + NonLogicalNewline 18..19, + Indent 19..22, + Return 22..28, + Int 29..31, + Newline 31..32, + NonLogicalNewline 32..33, + Dedent 33..33, + Dedent 33..33, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_windows_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_windows_eol.snap new file mode 100644 index 00000000000..7c7b879dc29 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_tabs_windows_eol.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: double_dedent_with_tabs_eol(WINDOWS_EOL) +--- +## Tokens +``` +[ + Def 0..3, + Identifier 4..7, + Lpar 7..8, + Rpar 8..9, + Colon 9..10, + Newline 10..12, + Indent 12..13, + If 13..15, + Identifier 16..17, + Colon 17..18, + Newline 18..20, + NonLogicalNewline 20..22, + Indent 22..25, + Return 25..31, + Int 32..34, + Newline 34..36, + NonLogicalNewline 36..38, + Dedent 38..38, + Dedent 38..38, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_unix_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_unix_eol.snap new file mode 100644 index 00000000000..0fde645188b --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_unix_eol.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: double_dedent_with_eol(UNIX_EOL) +--- +## Tokens +``` +[ + Def 0..3, + Identifier 4..7, + Lpar 7..8, + Rpar 8..9, + Colon 9..10, + Newline 10..11, + Indent 11..12, + If 12..14, + Identifier 15..16, + Colon 16..17, + Newline 17..18, + NonLogicalNewline 18..19, + Indent 19..21, + Return 21..27, + Int 28..30, + Newline 30..31, + NonLogicalNewline 31..32, + Dedent 32..32, + Dedent 32..32, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_windows_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_windows_eol.snap new file mode 100644 index 00000000000..73f41e0f12f --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__double_dedent_with_windows_eol.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: double_dedent_with_eol(WINDOWS_EOL) +--- +## Tokens +``` +[ + Def 0..3, + Identifier 4..7, + Lpar 7..8, + Rpar 8..9, + Colon 9..10, + Newline 10..12, + Indent 12..13, + If 13..15, + Identifier 16..17, + Colon 17..18, + Newline 18..20, + NonLogicalNewline 20..22, + Indent 22..24, + Return 24..30, + Int 31..33, + Newline 33..35, + NonLogicalNewline 35..37, + Dedent 37..37, + Dedent 37..37, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__emoji_identifier.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__emoji_identifier.snap new file mode 100644 index 00000000000..5dbb6139202 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__emoji_identifier.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_invalid(source, Mode::Module)" +--- +## Tokens +``` +[ + Unknown 0..4, +] +``` +## Errors +``` +[ + LexicalError { + error: UnrecognizedToken { + tok: '🐦', + }, + location: 0..4, + }, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_fstrings.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_fstrings.snap new file mode 100644 index 00000000000..6697afbedf7 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_fstrings.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 2..3 (flags = DOUBLE_QUOTES | F_STRING), + String 4..6 (flags = DOUBLE_QUOTES), + FStringStart 7..9 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 9..10 (flags = DOUBLE_QUOTES | F_STRING), + FStringStart 11..13 (flags = F_STRING), + FStringEnd 13..14 (flags = F_STRING), + String 15..17, + FStringStart 18..22 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | F_STRING), + FStringEnd 22..25 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | F_STRING), + FStringStart 26..30 (flags = TRIPLE_QUOTED_STRING | F_STRING), + FStringEnd 30..33 (flags = TRIPLE_QUOTED_STRING | F_STRING), + Newline 33..33, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_ipython_escape_command.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_ipython_escape_command.snap new file mode 100644 index 00000000000..28f833eb2d2 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_ipython_escape_command.snap @@ -0,0 +1,27 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_jupyter_source(source) +--- +## Tokens +``` +[ + IpyEscapeCommand 0..1, + Newline 1..2, + IpyEscapeCommand 2..4, + Newline 4..5, + IpyEscapeCommand 5..6, + Newline 6..7, + IpyEscapeCommand 7..9, + Newline 9..10, + IpyEscapeCommand 10..11, + Newline 11..12, + IpyEscapeCommand 12..14, + Newline 14..15, + IpyEscapeCommand 15..16, + Newline 16..17, + IpyEscapeCommand 17..18, + Newline 18..19, + IpyEscapeCommand 19..20, + Newline 20..20, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap new file mode 100644 index 00000000000..92385065003 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 2..3 (flags = DOUBLE_QUOTES | T_STRING), + String 4..6 (flags = DOUBLE_QUOTES), + TStringStart 7..9 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 9..10 (flags = DOUBLE_QUOTES | T_STRING), + TStringStart 11..13 (flags = T_STRING), + TStringEnd 13..14 (flags = T_STRING), + String 15..17, + TStringStart 18..22 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING), + TStringEnd 22..25 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING), + TStringStart 26..30 (flags = TRIPLE_QUOTED_STRING | T_STRING), + TStringEnd 30..33 (flags = TRIPLE_QUOTED_STRING | T_STRING), + Newline 33..33, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__escape_unicode_name.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__escape_unicode_name.snap new file mode 100644 index 00000000000..03b470bf1fd --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__escape_unicode_name.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + String 0..14 (flags = DOUBLE_QUOTES), + Newline 14..14, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap new file mode 100644 index 00000000000..3a9a8059844 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 2..9 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 9..10, + Identifier 10..13, + Rbrace 13..14, + FStringMiddle 14..27 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 27..28, + Identifier 28..31, + Rbrace 31..32, + FStringMiddle 32..35 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 35..36, + Identifier 36..41, + Rbrace 41..42, + FStringMiddle 42..44 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 44..45 (flags = DOUBLE_QUOTES | F_STRING), + Newline 45..45, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap new file mode 100644 index 00000000000..4e1c1d5a07d --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..4 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | F_STRING), + FStringMiddle 4..21 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | F_STRING), + Lbrace 21..22, + Comment 23..34, + NonLogicalNewline 34..35, + Identifier 39..40, + NonLogicalNewline 40..41, + Rbrace 41..42, + FStringMiddle 42..59 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | F_STRING), + FStringEnd 59..62 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | F_STRING), + Newline 62..62, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap new file mode 100644 index 00000000000..33305d581d3 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 2..3, + Identifier 3..4, + Exclamation 4..5, + Identifier 5..6, + Rbrace 6..7, + FStringMiddle 7..8 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 8..9, + Identifier 9..10, + Equal 10..11, + Exclamation 11..12, + Identifier 12..13, + Rbrace 13..14, + FStringMiddle 14..15 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 15..16, + Identifier 16..17, + Colon 17..18, + FStringMiddle 18..23 (flags = DOUBLE_QUOTES | F_STRING), + Rbrace 23..24, + FStringMiddle 24..32 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 32..33 (flags = DOUBLE_QUOTES | F_STRING), + Newline 33..33, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap new file mode 100644 index 00000000000..ff4eb211256 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 2..3 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 3..4, + Identifier 4..5, + Colon 5..6, + FStringMiddle 6..9 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 9..10, + Identifier 10..11, + Rbrace 11..12, + Rbrace 12..13, + FStringMiddle 13..24 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 24..25 (flags = DOUBLE_QUOTES | F_STRING), + Newline 25..25, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap new file mode 100644 index 00000000000..d20a60137c8 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = F_STRING), + FStringMiddle 2..3 (flags = F_STRING), + Lbrace 3..4, + Identifier 4..7, + Rbrace 7..8, + FStringEnd 8..9 (flags = F_STRING), + FStringStart 10..12 (flags = F_STRING), + FStringMiddle 12..14 (flags = F_STRING), + Lbrace 14..15, + Identifier 15..18, + Rbrace 18..19, + FStringEnd 19..20 (flags = F_STRING), + FStringStart 21..23 (flags = F_STRING), + FStringMiddle 23..31 (flags = F_STRING), + FStringEnd 31..32 (flags = F_STRING), + FStringStart 33..35 (flags = F_STRING), + FStringMiddle 35..44 (flags = F_STRING), + FStringEnd 44..45 (flags = F_STRING), + Newline 45..45, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap new file mode 100644 index 00000000000..83a9f56098d --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..3 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + FStringMiddle 3..4 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + Lbrace 4..5, + Identifier 5..6, + Colon 6..7, + FStringMiddle 7..10 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + Lbrace 10..11, + Identifier 11..12, + Rbrace 12..13, + Rbrace 13..14, + FStringMiddle 14..25 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + FStringEnd 25..26 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + Newline 26..26, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap new file mode 100644 index 00000000000..60f9ac80e75 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 2..8 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 8..9, + NonLogicalNewline 9..10, + Identifier 14..15, + NonLogicalNewline 15..16, + Star 24..25, + NonLogicalNewline 25..26, + Identifier 38..39, + NonLogicalNewline 39..40, + Rbrace 40..41, + FStringMiddle 41..48 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 48..49 (flags = DOUBLE_QUOTES | F_STRING), + Newline 49..49, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap new file mode 100644 index 00000000000..d783bca7944 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..4 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | F_STRING), + FStringMiddle 4..21 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | F_STRING), + FStringEnd 21..24 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | F_STRING), + FStringStart 25..29 (flags = TRIPLE_QUOTED_STRING | F_STRING), + FStringMiddle 29..46 (flags = TRIPLE_QUOTED_STRING | F_STRING), + FStringEnd 46..49 (flags = TRIPLE_QUOTED_STRING | F_STRING), + FStringStart 50..52 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 52..57 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 57..58, + FStringStart 58..62 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | F_STRING), + FStringMiddle 62..80 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | F_STRING), + Lbrace 80..81, + Identifier 81..82, + Rbrace 82..83, + FStringEnd 83..86 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | F_STRING), + Rbrace 86..87, + FStringMiddle 87..94 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 94..95 (flags = DOUBLE_QUOTES | F_STRING), + Newline 95..95, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap new file mode 100644 index 00000000000..b1f6c163fea --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 2..28 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 28..29 (flags = DOUBLE_QUOTES | F_STRING), + Newline 29..29, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap new file mode 100644 index 00000000000..c9b06bdd0b9 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..3 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + FStringMiddle 3..5 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + Lbrace 5..6, + Identifier 6..12, + Rbrace 12..13, + FStringMiddle 13..20 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + FStringEnd 20..21 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + Newline 21..21, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap new file mode 100644 index 00000000000..95b0a28ebdd --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 2..6 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 6..7, + FStringStart 7..9 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 9..13 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 13..14, + Identifier 14..15, + Plus 16..17, + FStringStart 18..20 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 20..21, + Identifier 21..24, + Rbrace 24..25, + FStringEnd 25..26 (flags = DOUBLE_QUOTES | F_STRING), + Rbrace 26..27, + FStringEnd 27..28 (flags = DOUBLE_QUOTES | F_STRING), + Rbrace 28..29, + FStringMiddle 29..33 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 33..34 (flags = DOUBLE_QUOTES | F_STRING), + FStringStart 35..37 (flags = F_STRING), + FStringMiddle 37..41 (flags = F_STRING), + Lbrace 41..42, + FStringStart 42..44 (flags = F_STRING), + FStringMiddle 44..47 (flags = F_STRING), + FStringEnd 47..48 (flags = F_STRING), + Rbrace 48..49, + FStringMiddle 49..55 (flags = F_STRING), + Lbrace 55..56, + FStringStart 56..58 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 58..65 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 65..66 (flags = DOUBLE_QUOTES | F_STRING), + Rbrace 66..67, + FStringEnd 67..68 (flags = F_STRING), + Newline 68..68, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_newline_format_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_newline_format_spec.snap new file mode 100644 index 00000000000..cce66157c42 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_newline_format_spec.snap @@ -0,0 +1,83 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_invalid(source, Mode::Module)" +--- +## Tokens +``` +[ + NonLogicalNewline 0..1, + FStringStart 1..3 (flags = F_STRING), + FStringMiddle 3..5 (flags = F_STRING), + Lbrace 5..6, + NonLogicalNewline 6..7, + Identifier 11..12, + Colon 12..13, + FStringMiddle 13..14 (flags = F_STRING), + Newline 14..15, + Rbrace 15..16, + Identifier 16..18, + String 18..19 (flags = UNCLOSED_STRING), + Newline 19..20, + FStringStart 20..22 (flags = F_STRING), + FStringMiddle 22..24 (flags = F_STRING), + Lbrace 24..25, + NonLogicalNewline 25..26, + Identifier 30..31, + Colon 31..32, + FStringMiddle 32..33 (flags = F_STRING), + Newline 33..34, + Indent 34..42, + Identifier 42..43, + Newline 43..44, + Dedent 44..44, + Rbrace 44..45, + Identifier 45..47, + String 47..48 (flags = UNCLOSED_STRING), + Newline 48..49, +] +``` +## Errors +``` +[ + LexicalError { + error: FStringError( + NewlineInFormatSpec, + ), + location: 13..14, + }, + LexicalError { + error: UnmatchedBracket { + closing: '}', + }, + location: 15..15, + }, + LexicalError { + error: UnclosedStringError { + triple_quoted: false, + escaped_end_quote: false, + detected_line: 4, + }, + location: 18..18, + }, + LexicalError { + error: FStringError( + NewlineInFormatSpec, + ), + location: 32..33, + }, + LexicalError { + error: UnmatchedBracket { + closing: '}', + }, + location: 44..44, + }, + LexicalError { + error: UnclosedStringError { + triple_quoted: false, + escaped_end_quote: false, + detected_line: 8, + }, + location: 47..47, + }, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap new file mode 100644 index 00000000000..5a2ba337aa7 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 2..3, + Rbrace 3..4, + FStringEnd 4..5 (flags = DOUBLE_QUOTES | F_STRING), + FStringStart 6..8 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 8..12 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 12..13 (flags = DOUBLE_QUOTES | F_STRING), + FStringStart 14..16 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 16..17 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 17..18, + Rbrace 18..19, + FStringEnd 19..20 (flags = DOUBLE_QUOTES | F_STRING), + FStringStart 21..23 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 23..25 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 25..26, + Rbrace 26..27, + FStringMiddle 27..29 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 29..30 (flags = DOUBLE_QUOTES | F_STRING), + FStringStart 31..33 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 33..41 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 41..42 (flags = DOUBLE_QUOTES | F_STRING), + FStringStart 43..45 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 45..46 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 46..47, + Rbrace 47..48, + FStringMiddle 48..56 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 56..57, + Rbrace 57..58, + FStringMiddle 58..71 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 71..72 (flags = DOUBLE_QUOTES | F_STRING), + Newline 72..72, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_prefix.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_prefix.snap new file mode 100644 index 00000000000..29a3c10713f --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_prefix.snap @@ -0,0 +1,30 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 2..3 (flags = DOUBLE_QUOTES | F_STRING), + FStringStart 4..6 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 6..7 (flags = DOUBLE_QUOTES | F_STRING), + FStringStart 8..11 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + FStringEnd 11..12 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + FStringStart 13..16 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + FStringEnd 16..17 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + FStringStart 18..21 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_UPPERCASE), + FStringEnd 21..22 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_UPPERCASE), + FStringStart 23..26 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_UPPERCASE), + FStringEnd 26..27 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_UPPERCASE), + FStringStart 28..31 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + FStringEnd 31..32 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + FStringStart 33..36 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + FStringEnd 36..37 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_LOWERCASE), + FStringStart 38..41 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_UPPERCASE), + FStringEnd 41..42 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_UPPERCASE), + FStringStart 43..46 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_UPPERCASE), + FStringEnd 46..47 (flags = DOUBLE_QUOTES | F_STRING | RAW_STRING_UPPERCASE), + Newline 47..47, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap new file mode 100644 index 00000000000..22be1adb227 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: fstring_single_quote_escape_eol(MAC_EOL) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = F_STRING), + FStringMiddle 2..19 (flags = F_STRING), + FStringEnd 19..20 (flags = F_STRING), + Newline 20..20, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap new file mode 100644 index 00000000000..645159de9d8 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: fstring_single_quote_escape_eol(UNIX_EOL) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = F_STRING), + FStringMiddle 2..19 (flags = F_STRING), + FStringEnd 19..20 (flags = F_STRING), + Newline 20..20, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap new file mode 100644 index 00000000000..ea57f6dc217 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: fstring_single_quote_escape_eol(WINDOWS_EOL) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = F_STRING), + FStringMiddle 2..20 (flags = F_STRING), + FStringEnd 20..21 (flags = F_STRING), + Newline 21..21, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap new file mode 100644 index 00000000000..b9ed53a182b --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 2..3, + Identifier 3..6, + Colon 6..7, + Rbrace 7..8, + FStringMiddle 8..9 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 9..10, + Identifier 10..11, + Equal 11..12, + Exclamation 12..13, + Identifier 13..14, + Colon 14..15, + FStringMiddle 15..18 (flags = DOUBLE_QUOTES | F_STRING), + Rbrace 18..19, + FStringMiddle 19..20 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 20..21, + Identifier 21..22, + Colon 22..23, + FStringMiddle 23..24 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 24..25, + Identifier 25..26, + Rbrace 26..27, + FStringMiddle 27..28 (flags = DOUBLE_QUOTES | F_STRING), + Rbrace 28..29, + FStringMiddle 29..30 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 30..31, + String 31..33, + Colon 33..34, + FStringMiddle 34..36 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 36..37, + Int 37..38, + Colon 38..39, + Lbrace 39..40, + Int 40..41, + Rbrace 41..42, + Rbrace 42..43, + Rbrace 43..44, + FStringMiddle 44..45 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 45..46, + Identifier 46..47, + Colon 47..48, + Lbrace 48..49, + Lbrace 49..50, + Int 50..51, + Rbrace 51..52, + Dot 52..53, + Identifier 53..56, + Lpar 56..57, + Rpar 57..58, + Rbrace 58..59, + Rbrace 59..60, + FStringEnd 60..61 (flags = DOUBLE_QUOTES | F_STRING), + Newline 61..61, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap new file mode 100644 index 00000000000..1e0ce49eb2b --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 2..6 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 6..7, + Exclamation 7..8, + Identifier 8..11, + Rbrace 11..12, + FStringMiddle 12..16 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 16..17 (flags = DOUBLE_QUOTES | F_STRING), + Newline 17..17, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_lambda_expression.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_lambda_expression.snap new file mode 100644 index 00000000000..418227fd050 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_lambda_expression.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 2..3, + Lambda 3..9, + Identifier 10..11, + Colon 11..12, + Lbrace 12..13, + Identifier 13..14, + Rbrace 14..15, + Rbrace 15..16, + FStringEnd 16..17 (flags = DOUBLE_QUOTES | F_STRING), + Newline 17..18, + FStringStart 18..20 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 20..21, + Lpar 21..22, + Lambda 22..28, + Identifier 29..30, + Colon 30..31, + Lbrace 31..32, + Identifier 32..33, + Rbrace 33..34, + Rpar 34..35, + Rbrace 35..36, + FStringEnd 36..37 (flags = DOUBLE_QUOTES | F_STRING), + Newline 37..37, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap new file mode 100644 index 00000000000..4e73bf70982 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..4 (flags = TRIPLE_QUOTED_STRING | F_STRING), + FStringMiddle 4..6 (flags = TRIPLE_QUOTED_STRING | F_STRING), + Lbrace 6..7, + NonLogicalNewline 7..8, + Identifier 12..13, + Colon 13..14, + FStringMiddle 14..16 (flags = TRIPLE_QUOTED_STRING | F_STRING), + Rbrace 16..17, + FStringMiddle 17..19 (flags = TRIPLE_QUOTED_STRING | F_STRING), + FStringEnd 19..22 (flags = TRIPLE_QUOTED_STRING | F_STRING), + Newline 22..23, + FStringStart 23..27 (flags = TRIPLE_QUOTED_STRING | F_STRING), + FStringMiddle 27..29 (flags = TRIPLE_QUOTED_STRING | F_STRING), + Lbrace 29..30, + NonLogicalNewline 30..31, + Identifier 35..36, + Colon 36..37, + FStringMiddle 37..61 (flags = TRIPLE_QUOTED_STRING | F_STRING), + Rbrace 61..62, + FStringMiddle 62..64 (flags = TRIPLE_QUOTED_STRING | F_STRING), + FStringEnd 64..67 (flags = TRIPLE_QUOTED_STRING | F_STRING), + Newline 67..68, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap new file mode 100644 index 00000000000..8682d194310 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap @@ -0,0 +1,43 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 2..3, + Identifier 3..4, + Colon 4..5, + FStringMiddle 5..8 (flags = DOUBLE_QUOTES | F_STRING), + Rbrace 8..9, + FStringMiddle 9..10 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 10..11, + Lpar 11..12, + Identifier 12..13, + ColonEqual 13..15, + Int 15..17, + Rpar 17..18, + Rbrace 18..19, + FStringMiddle 19..20 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 20..21, + Identifier 21..22, + Comma 22..23, + Lbrace 23..24, + Identifier 24..25, + ColonEqual 25..27, + Int 27..29, + Rbrace 29..30, + Rbrace 30..31, + FStringMiddle 31..32 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 32..33, + Lsqb 33..34, + Identifier 34..35, + ColonEqual 35..37, + Int 37..39, + Rsqb 39..40, + Rbrace 40..41, + FStringEnd 41..42 (flags = DOUBLE_QUOTES | F_STRING), + Newline 42..42, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap new file mode 100644 index 00000000000..2e2e17afab5 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + FStringStart 0..2 (flags = F_STRING), + FStringMiddle 2..4 (flags = F_STRING), + FStringEnd 4..5 (flags = F_STRING), + Newline 5..5, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__indentation_with_mac_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__indentation_with_mac_eol.snap new file mode 100644 index 00000000000..1ae5dbc017d --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__indentation_with_mac_eol.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: indentation_with_eol(MAC_EOL) +--- +## Tokens +``` +[ + Def 0..3, + Identifier 4..7, + Lpar 7..8, + Rpar 8..9, + Colon 9..10, + Newline 10..11, + Indent 11..15, + Return 15..21, + Int 22..24, + Newline 24..25, + NonLogicalNewline 25..26, + Dedent 26..26, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__indentation_with_unix_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__indentation_with_unix_eol.snap new file mode 100644 index 00000000000..f8b10df194b --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__indentation_with_unix_eol.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: indentation_with_eol(UNIX_EOL) +--- +## Tokens +``` +[ + Def 0..3, + Identifier 4..7, + Lpar 7..8, + Rpar 8..9, + Colon 9..10, + Newline 10..11, + Indent 11..15, + Return 15..21, + Int 22..24, + Newline 24..25, + NonLogicalNewline 25..26, + Dedent 26..26, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__indentation_with_windows_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__indentation_with_windows_eol.snap new file mode 100644 index 00000000000..e1627fd256b --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__indentation_with_windows_eol.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: indentation_with_eol(WINDOWS_EOL) +--- +## Tokens +``` +[ + Def 0..3, + Identifier 4..7, + Lpar 7..8, + Rpar 8..9, + Colon 9..10, + Newline 10..12, + Indent 12..16, + Return 16..22, + Int 23..25, + Newline 25..27, + NonLogicalNewline 27..29, + Dedent 29..29, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__invalid_leading_zero_big.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__invalid_leading_zero_big.snap new file mode 100644 index 00000000000..1cc2d92de23 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__invalid_leading_zero_big.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_invalid(source, Mode::Module)" +--- +## Tokens +``` +[ + Unknown 0..85, + Newline 85..85, +] +``` +## Errors +``` +[ + LexicalError { + error: LeadingZerosInDecimalInteger, + location: 0..1, + }, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__invalid_leading_zero_small.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__invalid_leading_zero_small.snap new file mode 100644 index 00000000000..f0cde0f8a63 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__invalid_leading_zero_small.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_invalid(source, Mode::Module)" +--- +## Tokens +``` +[ + Unknown 0..3, + Newline 3..3, +] +``` +## Errors +``` +[ + LexicalError { + error: LeadingZerosInDecimalInteger, + location: 0..1, + }, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command.snap new file mode 100644 index 00000000000..9ddd784c6f2 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_jupyter_source(source) +--- +## Tokens +``` +[ + IpyEscapeCommand 0..4, + Newline 4..5, + IpyEscapeCommand 5..10, + Newline 10..11, + IpyEscapeCommand 11..24, + Newline 24..25, + IpyEscapeCommand 25..38, + Newline 38..39, + IpyEscapeCommand 39..65, + Newline 65..66, + IpyEscapeCommand 66..103, + Newline 103..104, + IpyEscapeCommand 104..149, + Newline 149..150, + IpyEscapeCommand 150..158, + Newline 158..159, + IpyEscapeCommand 159..167, + Newline 167..168, + IpyEscapeCommand 168..176, + Newline 176..177, + IpyEscapeCommand 177..180, + Newline 180..180, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_assignment.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_assignment.snap new file mode 100644 index 00000000000..667af635cbd --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_assignment.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_jupyter_source(source) +--- +## Tokens +``` +[ + Identifier 0..3, + Equal 4..5, + IpyEscapeCommand 6..10, + Newline 10..11, + Identifier 11..14, + Equal 15..16, + IpyEscapeCommand 17..30, + Newline 30..31, + Identifier 31..34, + Equal 35..36, + IpyEscapeCommand 37..50, + Newline 50..51, + Identifier 51..54, + Equal 55..56, + IpyEscapeCommand 57..85, + Newline 85..86, + Identifier 86..89, + Equal 90..91, + IpyEscapeCommand 92..97, + Newline 97..98, + Identifier 98..102, + Equal 103..104, + IpyEscapeCommand 105..110, + Newline 110..110, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_indentation.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_indentation.snap new file mode 100644 index 00000000000..8203edac7c9 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_indentation.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_jupyter_source(source) +--- +## Tokens +``` +[ + If 0..2, + True 3..7, + Colon 7..8, + Newline 8..9, + Indent 9..13, + IpyEscapeCommand 13..43, + Newline 43..43, + Dedent 43..43, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_mac_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_mac_eol.snap new file mode 100644 index 00000000000..da4b09f2ace --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_mac_eol.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: ipython_escape_command_line_continuation_eol(MAC_EOL) +--- +## Tokens +``` +[ + IpyEscapeCommand 0..24, + Newline 24..24, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_unix_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_unix_eol.snap new file mode 100644 index 00000000000..2eefe52c061 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_unix_eol.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: ipython_escape_command_line_continuation_eol(UNIX_EOL) +--- +## Tokens +``` +[ + IpyEscapeCommand 0..24, + Newline 24..24, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_windows_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_windows_eol.snap new file mode 100644 index 00000000000..7cc905f393c --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_windows_eol.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: ipython_escape_command_line_continuation_eol(WINDOWS_EOL) +--- +## Tokens +``` +[ + IpyEscapeCommand 0..25, + Newline 25..25, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_mac_eol_and_eof.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_mac_eol_and_eof.snap new file mode 100644 index 00000000000..399d055ae89 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_mac_eol_and_eof.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: ipython_escape_command_line_continuation_with_eol_and_eof(MAC_EOL) +--- +## Tokens +``` +[ + IpyEscapeCommand 0..14, + Newline 14..14, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_unix_eol_and_eof.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_unix_eol_and_eof.snap new file mode 100644 index 00000000000..25e1804d672 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_unix_eol_and_eof.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: ipython_escape_command_line_continuation_with_eol_and_eof(UNIX_EOL) +--- +## Tokens +``` +[ + IpyEscapeCommand 0..14, + Newline 14..14, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_windows_eol_and_eof.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_windows_eol_and_eof.snap new file mode 100644 index 00000000000..15b519d2a82 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_escape_command_line_continuation_with_windows_eol_and_eof.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: ipython_escape_command_line_continuation_with_eol_and_eof(WINDOWS_EOL) +--- +## Tokens +``` +[ + IpyEscapeCommand 0..15, + Newline 15..15, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_help_end_escape_command.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_help_end_escape_command.snap new file mode 100644 index 00000000000..ad12c890d39 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__ipython_help_end_escape_command.snap @@ -0,0 +1,41 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_jupyter_source(source) +--- +## Tokens +``` +[ + IpyEscapeCommand 0..5, + Newline 5..6, + IpyEscapeCommand 6..15, + Newline 15..16, + IpyEscapeCommand 16..27, + Newline 27..28, + IpyEscapeCommand 28..34, + Newline 34..35, + IpyEscapeCommand 35..42, + Newline 42..43, + IpyEscapeCommand 43..50, + Newline 50..51, + IpyEscapeCommand 51..59, + Newline 59..60, + IpyEscapeCommand 60..68, + Newline 68..69, + IpyEscapeCommand 69..78, + Newline 78..79, + IpyEscapeCommand 79..92, + Newline 92..93, + IpyEscapeCommand 93..99, + Newline 99..100, + IpyEscapeCommand 100..104, + Newline 104..105, + IpyEscapeCommand 105..110, + Newline 110..111, + IpyEscapeCommand 111..117, + Newline 117..118, + IpyEscapeCommand 118..126, + Newline 126..127, + IpyEscapeCommand 127..132, + Newline 132..132, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_empty.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_empty.snap new file mode 100644 index 00000000000..e7ea7a4186e --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_empty.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(&source) +--- +## Tokens +``` +[ + Int 0..5, + Comment 7..8, + Newline 8..8, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_long.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_long.snap new file mode 100644 index 00000000000..25e05ea4975 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_long.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(&source) +--- +## Tokens +``` +[ + Int 0..5, + Comment 7..12, + Newline 12..12, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_single_whitespace.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_single_whitespace.snap new file mode 100644 index 00000000000..8baa13a2928 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_single_whitespace.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(&source) +--- +## Tokens +``` +[ + Int 0..5, + Comment 7..9, + Newline 9..9, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_whitespace.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_whitespace.snap new file mode 100644 index 00000000000..b495fad3b58 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_comment_whitespace.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(&source) +--- +## Tokens +``` +[ + Int 0..5, + Comment 7..10, + Newline 10..10, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_mac_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_mac_eol.snap new file mode 100644 index 00000000000..7120f7d2cb7 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_mac_eol.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: line_continuation_at_eof_after_newline(MAC_EOL) +--- +## Tokens +``` +[ + Unknown 0..2, +] +``` +## Errors +``` +[ + LexicalError { + error: Eof, + location: 0..2, + }, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_unix_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_unix_eol.snap new file mode 100644 index 00000000000..e49b8d7103b --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_unix_eol.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: line_continuation_at_eof_after_newline(UNIX_EOL) +--- +## Tokens +``` +[ + Unknown 0..2, +] +``` +## Errors +``` +[ + LexicalError { + error: Eof, + location: 0..2, + }, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_windows_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_windows_eol.snap new file mode 100644 index 00000000000..3f3b7930db9 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_after_newline_windows_eol.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: line_continuation_at_eof_after_newline(WINDOWS_EOL) +--- +## Tokens +``` +[ + Unknown 0..3, +] +``` +## Errors +``` +[ + LexicalError { + error: Eof, + location: 0..3, + }, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_mac_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_mac_eol.snap new file mode 100644 index 00000000000..e717f8ce2b3 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_mac_eol.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: line_continuation_at_eof(MAC_EOL) +--- +## Tokens +``` +[ + Int 0..1, + Comma 1..2, + Unknown 2..5, + Newline 5..5, +] +``` +## Errors +``` +[ + LexicalError { + error: Eof, + location: 2..5, + }, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_unix_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_unix_eol.snap new file mode 100644 index 00000000000..0c355080f1a --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_unix_eol.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: line_continuation_at_eof(UNIX_EOL) +--- +## Tokens +``` +[ + Int 0..1, + Comma 1..2, + Unknown 2..5, + Newline 5..5, +] +``` +## Errors +``` +[ + LexicalError { + error: Eof, + location: 2..5, + }, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_windows_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_windows_eol.snap new file mode 100644 index 00000000000..09585b3191f --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__line_continuation_at_eof_windows_eol.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: line_continuation_at_eof(WINDOWS_EOL) +--- +## Tokens +``` +[ + Int 0..1, + Comma 1..2, + Unknown 2..6, + Newline 6..6, +] +``` +## Errors +``` +[ + LexicalError { + error: Eof, + location: 2..6, + }, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__logical_newline_line_comment.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__logical_newline_line_comment.snap new file mode 100644 index 00000000000..1c72a0708b7 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__logical_newline_line_comment.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + Comment 0..6, + NonLogicalNewline 6..7, + Comment 7..13, + NonLogicalNewline 13..14, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__match_softkeyword_in_notebook.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__match_softkeyword_in_notebook.snap new file mode 100644 index 00000000000..a1a7afc6333 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__match_softkeyword_in_notebook.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_jupyter_source(source) +--- +## Tokens +``` +[ + Match 0..5, + Identifier 6..9, + Colon 9..10, + Newline 10..11, + Indent 11..15, + Case 15..19, + Identifier 20..23, + Colon 23..24, + Newline 24..25, + Indent 25..33, + Pass 33..37, + Newline 37..37, + Dedent 37..37, + Dedent 37..37, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__multiple_backslash_continuation.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__multiple_backslash_continuation.snap new file mode 100644 index 00000000000..6e75126db99 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__multiple_backslash_continuation.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(&source) +--- +## Tokens +``` +[ + If 0..2, + True 3..7, + Colon 7..8, + Newline 8..9, + Indent 9..13, + Int 13..14, + Newline 14..15, + Int 55..56, + Newline 56..57, + Dedent 57..57, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap new file mode 100644 index 00000000000..1d15d06932b --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap @@ -0,0 +1,44 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 2..6 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 6..7, + FStringStart 7..9 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 9..13 (flags = DOUBLE_QUOTES | F_STRING), + Lbrace 13..14, + Identifier 14..15, + Plus 16..17, + TStringStart 18..20 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 20..21, + Identifier 21..24, + Rbrace 24..25, + TStringEnd 25..26 (flags = DOUBLE_QUOTES | T_STRING), + Rbrace 26..27, + FStringEnd 27..28 (flags = DOUBLE_QUOTES | F_STRING), + Rbrace 28..29, + TStringMiddle 29..33 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 33..34 (flags = DOUBLE_QUOTES | T_STRING), + FStringStart 35..37 (flags = F_STRING), + FStringMiddle 37..41 (flags = F_STRING), + Lbrace 41..42, + TStringStart 42..44 (flags = T_STRING), + TStringMiddle 44..47 (flags = T_STRING), + TStringEnd 47..48 (flags = T_STRING), + Exclamation 48..49, + Identifier 49..50, + Rbrace 50..51, + FStringMiddle 51..57 (flags = F_STRING), + Lbrace 57..58, + FStringStart 58..60 (flags = DOUBLE_QUOTES | F_STRING), + FStringMiddle 60..67 (flags = DOUBLE_QUOTES | F_STRING), + FStringEnd 67..68 (flags = DOUBLE_QUOTES | F_STRING), + Rbrace 68..69, + FStringEnd 69..70 (flags = F_STRING), + Newline 70..70, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_mac_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_mac_eol.snap new file mode 100644 index 00000000000..febd77e18b2 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_mac_eol.snap @@ -0,0 +1,39 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: newline_in_brackets_eol(MAC_EOL) +--- +## Tokens +``` +[ + Identifier 0..1, + Equal 2..3, + Lsqb 4..5, + NonLogicalNewline 5..6, + NonLogicalNewline 6..7, + Int 11..12, + Comma 12..13, + Int 13..14, + NonLogicalNewline 14..15, + Comma 15..16, + Lpar 16..17, + Int 17..18, + Comma 18..19, + NonLogicalNewline 19..20, + Int 20..21, + Comma 21..22, + NonLogicalNewline 22..23, + Rpar 23..24, + Comma 24..25, + Lbrace 26..27, + NonLogicalNewline 27..28, + Int 28..29, + Comma 29..30, + NonLogicalNewline 30..31, + Int 31..32, + Comma 32..33, + Int 35..36, + Rbrace 36..37, + Rsqb 37..38, + Newline 38..39, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_unix_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_unix_eol.snap new file mode 100644 index 00000000000..8a023a2a363 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_unix_eol.snap @@ -0,0 +1,39 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: newline_in_brackets_eol(UNIX_EOL) +--- +## Tokens +``` +[ + Identifier 0..1, + Equal 2..3, + Lsqb 4..5, + NonLogicalNewline 5..6, + NonLogicalNewline 6..7, + Int 11..12, + Comma 12..13, + Int 13..14, + NonLogicalNewline 14..15, + Comma 15..16, + Lpar 16..17, + Int 17..18, + Comma 18..19, + NonLogicalNewline 19..20, + Int 20..21, + Comma 21..22, + NonLogicalNewline 22..23, + Rpar 23..24, + Comma 24..25, + Lbrace 26..27, + NonLogicalNewline 27..28, + Int 28..29, + Comma 29..30, + NonLogicalNewline 30..31, + Int 31..32, + Comma 32..33, + Int 35..36, + Rbrace 36..37, + Rsqb 37..38, + Newline 38..39, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_windows_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_windows_eol.snap new file mode 100644 index 00000000000..fab0cae4213 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__newline_in_brackets_windows_eol.snap @@ -0,0 +1,39 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: newline_in_brackets_eol(WINDOWS_EOL) +--- +## Tokens +``` +[ + Identifier 0..1, + Equal 2..3, + Lsqb 4..5, + NonLogicalNewline 5..7, + NonLogicalNewline 7..9, + Int 13..14, + Comma 14..15, + Int 15..16, + NonLogicalNewline 16..18, + Comma 18..19, + Lpar 19..20, + Int 20..21, + Comma 21..22, + NonLogicalNewline 22..24, + Int 24..25, + Comma 25..26, + NonLogicalNewline 26..28, + Rpar 28..29, + Comma 29..30, + Lbrace 31..32, + NonLogicalNewline 32..34, + Int 34..35, + Comma 35..36, + NonLogicalNewline 36..38, + Int 38..39, + Comma 39..40, + Int 43..44, + Rbrace 44..45, + Rsqb 45..46, + Newline 46..48, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__non_logical_newline_in_string_continuation.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__non_logical_newline_in_string_continuation.snap new file mode 100644 index 00000000000..c25615ff0f6 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__non_logical_newline_in_string_continuation.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + Lpar 0..1, + NonLogicalNewline 1..2, + String 6..9, + NonLogicalNewline 9..10, + String 14..17, + NonLogicalNewline 17..18, + NonLogicalNewline 18..19, + String 23..26, + String 33..36, + NonLogicalNewline 36..37, + Rpar 37..38, + Newline 38..38, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__numbers.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__numbers.snap new file mode 100644 index 00000000000..00811efdb21 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__numbers.snap @@ -0,0 +1,24 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + Int 0..4, + Int 5..9, + Int 10..16, + Int 17..18, + Int 19..22, + Int 23..36, + Float 37..40, + Float 41..45, + Float 46..51, + Complex 52..54, + Complex 55..59, + Int 60..63, + Int 64..82, + Int 83..117, + Newline 117..117, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__operators.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__operators.snap new file mode 100644 index 00000000000..0a98896c6d4 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__operators.snap @@ -0,0 +1,15 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + DoubleSlash 0..2, + DoubleSlash 2..4, + DoubleSlashEqual 4..7, + Slash 7..8, + Slash 9..10, + Newline 10..10, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string.snap new file mode 100644 index 00000000000..1213cb13581 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + String 0..8 (flags = DOUBLE_QUOTES), + String 9..17, + String 18..26, + String 27..33 (flags = DOUBLE_QUOTES), + String 34..42, + String 43..47, + String 48..56 (flags = RAW_STRING_LOWERCASE), + String 57..63, + String 64..73, + Newline 73..73, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_mac_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_mac_eol.snap new file mode 100644 index 00000000000..6af635cfb10 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_mac_eol.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: string_continuation_with_eol(MAC_EOL) +--- +## Tokens +``` +[ + String 0..10 (flags = DOUBLE_QUOTES), + Newline 10..10, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_unix_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_unix_eol.snap new file mode 100644 index 00000000000..b50028a14ee --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_unix_eol.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: string_continuation_with_eol(UNIX_EOL) +--- +## Tokens +``` +[ + String 0..10 (flags = DOUBLE_QUOTES), + Newline 10..10, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_windows_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_windows_eol.snap new file mode 100644 index 00000000000..c39191378fb --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__string_continuation_with_windows_eol.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: string_continuation_with_eol(WINDOWS_EOL) +--- +## Tokens +``` +[ + String 0..11 (flags = DOUBLE_QUOTES), + Newline 11..11, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tet_too_low_dedent.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tet_too_low_dedent.snap new file mode 100644 index 00000000000..15d725022db --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tet_too_low_dedent.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_invalid(source, Mode::Module)" +--- +## Tokens +``` +[ + If 0..2, + True 3..7, + Colon 7..8, + Newline 8..9, + Indent 9..13, + Pass 13..17, + Newline 17..18, + Unknown 18..20, + Pass 20..24, + Newline 24..24, +] +``` +## Errors +``` +[ + LexicalError { + error: IndentationError, + location: 24..24, + }, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_mac_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_mac_eol.snap new file mode 100644 index 00000000000..85716826a72 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_mac_eol.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: triple_quoted_eol(MAC_EOL) +--- +## Tokens +``` +[ + String 0..21 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING), + Newline 21..21, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_unix_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_unix_eol.snap new file mode 100644 index 00000000000..a2d2bc497da --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_unix_eol.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: triple_quoted_eol(UNIX_EOL) +--- +## Tokens +``` +[ + String 0..21 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING), + Newline 21..21, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_windows_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_windows_eol.snap new file mode 100644 index 00000000000..3909f94408a --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__triple_quoted_windows_eol.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: triple_quoted_eol(WINDOWS_EOL) +--- +## Tokens +``` +[ + String 0..23 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING), + Newline 23..23, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap new file mode 100644 index 00000000000..6ef933153da --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 2..9 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 9..10, + Identifier 10..13, + Rbrace 13..14, + TStringMiddle 14..27 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 27..28, + Identifier 28..31, + Rbrace 31..32, + TStringMiddle 32..35 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 35..36, + Identifier 36..41, + Rbrace 41..42, + TStringMiddle 42..44 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 44..45 (flags = DOUBLE_QUOTES | T_STRING), + Newline 45..45, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap new file mode 100644 index 00000000000..ebc873db90e --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..4 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING), + TStringMiddle 4..21 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING), + Lbrace 21..22, + Comment 23..34, + NonLogicalNewline 34..35, + Identifier 39..40, + NonLogicalNewline 40..41, + Rbrace 41..42, + TStringMiddle 42..59 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING), + TStringEnd 59..62 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING), + Newline 62..62, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap new file mode 100644 index 00000000000..f9c32bbebd7 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 2..3, + Identifier 3..4, + Exclamation 4..5, + Identifier 5..6, + Rbrace 6..7, + TStringMiddle 7..8 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 8..9, + Identifier 9..10, + Equal 10..11, + Exclamation 11..12, + Identifier 12..13, + Rbrace 13..14, + TStringMiddle 14..15 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 15..16, + Identifier 16..17, + Colon 17..18, + TStringMiddle 18..23 (flags = DOUBLE_QUOTES | T_STRING), + Rbrace 23..24, + TStringMiddle 24..32 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 32..33 (flags = DOUBLE_QUOTES | T_STRING), + Newline 33..33, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap new file mode 100644 index 00000000000..1ad248d025b --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 2..3 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 3..4, + Identifier 4..5, + Colon 5..6, + TStringMiddle 6..9 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 9..10, + Identifier 10..11, + Rbrace 11..12, + Rbrace 12..13, + TStringMiddle 13..24 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 24..25 (flags = DOUBLE_QUOTES | T_STRING), + Newline 25..25, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap new file mode 100644 index 00000000000..7fcb5f56c2c --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = T_STRING), + TStringMiddle 2..3 (flags = T_STRING), + Lbrace 3..4, + Identifier 4..7, + Rbrace 7..8, + TStringEnd 8..9 (flags = T_STRING), + TStringStart 10..12 (flags = T_STRING), + TStringMiddle 12..14 (flags = T_STRING), + Lbrace 14..15, + Identifier 15..18, + Rbrace 18..19, + TStringEnd 19..20 (flags = T_STRING), + TStringStart 21..23 (flags = T_STRING), + TStringMiddle 23..31 (flags = T_STRING), + TStringEnd 31..32 (flags = T_STRING), + TStringStart 33..35 (flags = T_STRING), + TStringMiddle 35..44 (flags = T_STRING), + TStringEnd 44..45 (flags = T_STRING), + Newline 45..45, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap new file mode 100644 index 00000000000..d9489f30f70 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..3 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + TStringMiddle 3..4 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + Lbrace 4..5, + Identifier 5..6, + Colon 6..7, + TStringMiddle 7..10 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + Lbrace 10..11, + Identifier 11..12, + Rbrace 12..13, + Rbrace 13..14, + TStringMiddle 14..25 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + TStringEnd 25..26 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + Newline 26..26, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap new file mode 100644 index 00000000000..75c5eb7a98e --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 2..8 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 8..9, + NonLogicalNewline 9..10, + Identifier 14..15, + NonLogicalNewline 15..16, + Star 24..25, + NonLogicalNewline 25..26, + Identifier 38..39, + NonLogicalNewline 39..40, + Rbrace 40..41, + TStringMiddle 41..48 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 48..49 (flags = DOUBLE_QUOTES | T_STRING), + Newline 49..49, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap new file mode 100644 index 00000000000..d09a41a8f20 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..4 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING), + TStringMiddle 4..21 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING), + TStringEnd 21..24 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING), + TStringStart 25..29 (flags = TRIPLE_QUOTED_STRING | T_STRING), + TStringMiddle 29..46 (flags = TRIPLE_QUOTED_STRING | T_STRING), + TStringEnd 46..49 (flags = TRIPLE_QUOTED_STRING | T_STRING), + TStringStart 50..52 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 52..57 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 57..58, + TStringStart 58..62 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING), + TStringMiddle 62..80 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING), + Lbrace 80..81, + Identifier 81..82, + Rbrace 82..83, + TStringEnd 83..86 (flags = DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING), + Rbrace 86..87, + TStringMiddle 87..94 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 94..95 (flags = DOUBLE_QUOTES | T_STRING), + Newline 95..95, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap new file mode 100644 index 00000000000..3530dbc2c40 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 2..28 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 28..29 (flags = DOUBLE_QUOTES | T_STRING), + Newline 29..29, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap new file mode 100644 index 00000000000..9fdf4051bee --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..3 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + TStringMiddle 3..5 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + Lbrace 5..6, + Identifier 6..12, + Rbrace 12..13, + TStringMiddle 13..20 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + TStringEnd 20..21 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + Newline 21..21, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap new file mode 100644 index 00000000000..90da7c0f3f6 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 2..6 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 6..7, + TStringStart 7..9 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 9..13 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 13..14, + Identifier 14..15, + Plus 16..17, + TStringStart 18..20 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 20..21, + Identifier 21..24, + Rbrace 24..25, + TStringEnd 25..26 (flags = DOUBLE_QUOTES | T_STRING), + Rbrace 26..27, + TStringEnd 27..28 (flags = DOUBLE_QUOTES | T_STRING), + Rbrace 28..29, + TStringMiddle 29..33 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 33..34 (flags = DOUBLE_QUOTES | T_STRING), + TStringStart 35..37 (flags = T_STRING), + TStringMiddle 37..41 (flags = T_STRING), + Lbrace 41..42, + TStringStart 42..44 (flags = T_STRING), + TStringMiddle 44..47 (flags = T_STRING), + TStringEnd 47..48 (flags = T_STRING), + Rbrace 48..49, + TStringMiddle 49..55 (flags = T_STRING), + Lbrace 55..56, + TStringStart 56..58 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 58..65 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 65..66 (flags = DOUBLE_QUOTES | T_STRING), + Rbrace 66..67, + TStringEnd 67..68 (flags = T_STRING), + Newline 68..68, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_newline_format_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_newline_format_spec.snap new file mode 100644 index 00000000000..1e293ceef45 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_newline_format_spec.snap @@ -0,0 +1,83 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_invalid(source, Mode::Module)" +--- +## Tokens +``` +[ + NonLogicalNewline 0..1, + TStringStart 1..3 (flags = T_STRING), + TStringMiddle 3..5 (flags = T_STRING), + Lbrace 5..6, + NonLogicalNewline 6..7, + Identifier 11..12, + Colon 12..13, + TStringMiddle 13..14 (flags = T_STRING), + Newline 14..15, + Rbrace 15..16, + Identifier 16..18, + String 18..19 (flags = UNCLOSED_STRING), + Newline 19..20, + TStringStart 20..22 (flags = T_STRING), + TStringMiddle 22..24 (flags = T_STRING), + Lbrace 24..25, + NonLogicalNewline 25..26, + Identifier 30..31, + Colon 31..32, + TStringMiddle 32..33 (flags = T_STRING), + Newline 33..34, + Indent 34..42, + Identifier 42..43, + Newline 43..44, + Dedent 44..44, + Rbrace 44..45, + Identifier 45..47, + String 47..48 (flags = UNCLOSED_STRING), + Newline 48..49, +] +``` +## Errors +``` +[ + LexicalError { + error: TStringError( + NewlineInFormatSpec, + ), + location: 13..14, + }, + LexicalError { + error: UnmatchedBracket { + closing: '}', + }, + location: 15..15, + }, + LexicalError { + error: UnclosedStringError { + triple_quoted: false, + escaped_end_quote: false, + detected_line: 4, + }, + location: 18..18, + }, + LexicalError { + error: TStringError( + NewlineInFormatSpec, + ), + location: 32..33, + }, + LexicalError { + error: UnmatchedBracket { + closing: '}', + }, + location: 44..44, + }, + LexicalError { + error: UnclosedStringError { + triple_quoted: false, + escaped_end_quote: false, + detected_line: 8, + }, + location: 47..47, + }, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap new file mode 100644 index 00000000000..5a49cf07f7b --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 2..3, + Rbrace 3..4, + TStringEnd 4..5 (flags = DOUBLE_QUOTES | T_STRING), + TStringStart 6..8 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 8..12 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 12..13 (flags = DOUBLE_QUOTES | T_STRING), + TStringStart 14..16 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 16..17 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 17..18, + Rbrace 18..19, + TStringEnd 19..20 (flags = DOUBLE_QUOTES | T_STRING), + TStringStart 21..23 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 23..25 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 25..26, + Rbrace 26..27, + TStringMiddle 27..29 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 29..30 (flags = DOUBLE_QUOTES | T_STRING), + TStringStart 31..33 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 33..41 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 41..42 (flags = DOUBLE_QUOTES | T_STRING), + TStringStart 43..45 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 45..46 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 46..47, + Rbrace 47..48, + TStringMiddle 48..56 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 56..57, + Rbrace 57..58, + TStringMiddle 58..71 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 71..72 (flags = DOUBLE_QUOTES | T_STRING), + Newline 72..72, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap new file mode 100644 index 00000000000..60399e03141 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap @@ -0,0 +1,30 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 2..3 (flags = DOUBLE_QUOTES | T_STRING), + TStringStart 4..6 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 6..7 (flags = DOUBLE_QUOTES | T_STRING), + TStringStart 8..11 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + TStringEnd 11..12 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + TStringStart 13..16 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + TStringEnd 16..17 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + TStringStart 18..21 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE), + TStringEnd 21..22 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE), + TStringStart 23..26 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE), + TStringEnd 26..27 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE), + TStringStart 28..31 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + TStringEnd 31..32 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + TStringStart 33..36 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + TStringEnd 36..37 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE), + TStringStart 38..41 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE), + TStringEnd 41..42 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE), + TStringStart 43..46 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE), + TStringEnd 46..47 (flags = DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE), + Newline 47..47, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap new file mode 100644 index 00000000000..a4c6e93e6cf --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: tstring_single_quote_escape_eol(MAC_EOL) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = T_STRING), + TStringMiddle 2..19 (flags = T_STRING), + TStringEnd 19..20 (flags = T_STRING), + Newline 20..20, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap new file mode 100644 index 00000000000..da5d6350ee7 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: tstring_single_quote_escape_eol(UNIX_EOL) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = T_STRING), + TStringMiddle 2..19 (flags = T_STRING), + TStringEnd 19..20 (flags = T_STRING), + Newline 20..20, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap new file mode 100644 index 00000000000..e6b8d3c3543 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: tstring_single_quote_escape_eol(WINDOWS_EOL) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = T_STRING), + TStringMiddle 2..20 (flags = T_STRING), + TStringEnd 20..21 (flags = T_STRING), + Newline 21..21, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap new file mode 100644 index 00000000000..47e20e51706 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 2..3, + Identifier 3..6, + Colon 6..7, + Rbrace 7..8, + TStringMiddle 8..9 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 9..10, + Identifier 10..11, + Equal 11..12, + Exclamation 12..13, + Identifier 13..14, + Colon 14..15, + TStringMiddle 15..18 (flags = DOUBLE_QUOTES | T_STRING), + Rbrace 18..19, + TStringMiddle 19..20 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 20..21, + Identifier 21..22, + Colon 22..23, + TStringMiddle 23..24 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 24..25, + Identifier 25..26, + Rbrace 26..27, + TStringMiddle 27..28 (flags = DOUBLE_QUOTES | T_STRING), + Rbrace 28..29, + TStringMiddle 29..30 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 30..31, + String 31..33, + Colon 33..34, + TStringMiddle 34..36 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 36..37, + Int 37..38, + Colon 38..39, + Lbrace 39..40, + Int 40..41, + Rbrace 41..42, + Rbrace 42..43, + Rbrace 43..44, + TStringMiddle 44..45 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 45..46, + Identifier 46..47, + Colon 47..48, + Lbrace 48..49, + Lbrace 49..50, + Int 50..51, + Rbrace 51..52, + Dot 52..53, + Identifier 53..56, + Lpar 56..57, + Rpar 57..58, + Rbrace 58..59, + Rbrace 59..60, + TStringEnd 60..61 (flags = DOUBLE_QUOTES | T_STRING), + Newline 61..61, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap new file mode 100644 index 00000000000..4eec0912a40 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + TStringMiddle 2..6 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 6..7, + Exclamation 7..8, + Identifier 8..11, + Rbrace 11..12, + TStringMiddle 12..16 (flags = DOUBLE_QUOTES | T_STRING), + TStringEnd 16..17 (flags = DOUBLE_QUOTES | T_STRING), + Newline 17..17, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap new file mode 100644 index 00000000000..ff7d048ddee --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 2..3, + Lambda 3..9, + Identifier 10..11, + Colon 11..12, + Lbrace 12..13, + Identifier 13..14, + Rbrace 14..15, + Rbrace 15..16, + TStringEnd 16..17 (flags = DOUBLE_QUOTES | T_STRING), + Newline 17..18, + TStringStart 18..20 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 20..21, + Lpar 21..22, + Lambda 22..28, + Identifier 29..30, + Colon 30..31, + Lbrace 31..32, + Identifier 32..33, + Rbrace 33..34, + Rpar 34..35, + Rbrace 35..36, + TStringEnd 36..37 (flags = DOUBLE_QUOTES | T_STRING), + Newline 37..37, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap new file mode 100644 index 00000000000..d5e9df36ec8 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..4 (flags = TRIPLE_QUOTED_STRING | T_STRING), + TStringMiddle 4..6 (flags = TRIPLE_QUOTED_STRING | T_STRING), + Lbrace 6..7, + NonLogicalNewline 7..8, + Identifier 12..13, + Colon 13..14, + TStringMiddle 14..16 (flags = TRIPLE_QUOTED_STRING | T_STRING), + Rbrace 16..17, + TStringMiddle 17..19 (flags = TRIPLE_QUOTED_STRING | T_STRING), + TStringEnd 19..22 (flags = TRIPLE_QUOTED_STRING | T_STRING), + Newline 22..23, + TStringStart 23..27 (flags = TRIPLE_QUOTED_STRING | T_STRING), + TStringMiddle 27..29 (flags = TRIPLE_QUOTED_STRING | T_STRING), + Lbrace 29..30, + NonLogicalNewline 30..31, + Identifier 35..36, + Colon 36..37, + TStringMiddle 37..61 (flags = TRIPLE_QUOTED_STRING | T_STRING), + Rbrace 61..62, + TStringMiddle 62..64 (flags = TRIPLE_QUOTED_STRING | T_STRING), + TStringEnd 64..67 (flags = TRIPLE_QUOTED_STRING | T_STRING), + Newline 67..68, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap new file mode 100644 index 00000000000..26ab2f87360 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap @@ -0,0 +1,43 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 2..3, + Identifier 3..4, + Colon 4..5, + TStringMiddle 5..8 (flags = DOUBLE_QUOTES | T_STRING), + Rbrace 8..9, + TStringMiddle 9..10 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 10..11, + Lpar 11..12, + Identifier 12..13, + ColonEqual 13..15, + Int 15..17, + Rpar 17..18, + Rbrace 18..19, + TStringMiddle 19..20 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 20..21, + Identifier 21..22, + Comma 22..23, + Lbrace 23..24, + Identifier 24..25, + ColonEqual 25..27, + Int 27..29, + Rbrace 29..30, + Rbrace 30..31, + TStringMiddle 31..32 (flags = DOUBLE_QUOTES | T_STRING), + Lbrace 32..33, + Lsqb 33..34, + Identifier 34..35, + ColonEqual 35..37, + Int 37..39, + Rsqb 39..40, + Rbrace 40..41, + TStringEnd 41..42 (flags = DOUBLE_QUOTES | T_STRING), + Newline 42..42, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap new file mode 100644 index 00000000000..19f2bc03977 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + TStringStart 0..2 (flags = T_STRING), + TStringMiddle 2..4 (flags = T_STRING), + TStringEnd 4..5 (flags = T_STRING), + Newline 5..5, +] +``` diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__backspace_alias.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__backspace_alias.snap new file mode 100644 index 00000000000..c6c8e9cf859 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__backspace_alias.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..15, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..15, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 0..15, + node_index: NodeIndex(None), + value: "\u{8}", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__bell_alias.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__bell_alias.snap new file mode 100644 index 00000000000..5c6fd12129f --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__bell_alias.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..9, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..9, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 0..9, + node_index: NodeIndex(None), + value: "\u{7}", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__carriage_return_alias.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__carriage_return_alias.snap new file mode 100644 index 00000000000..56bda12a56f --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__carriage_return_alias.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..21, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..21, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 0..21, + node_index: NodeIndex(None), + value: "\r", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__character_tabulation_with_justification_alias.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__character_tabulation_with_justification_alias.snap new file mode 100644 index 00000000000..783e8c736e4 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__character_tabulation_with_justification_alias.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..45, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..45, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 0..45, + node_index: NodeIndex(None), + value: "\u{89}", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__delete_alias.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__delete_alias.snap new file mode 100644 index 00000000000..68c017dfc79 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__delete_alias.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..12, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..12, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 0..12, + node_index: NodeIndex(None), + value: "\u{7f}", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__dont_panic_on_8_in_octal_escape.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__dont_panic_on_8_in_octal_escape.snap new file mode 100644 index 00000000000..3e4f0b217a6 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__dont_panic_on_8_in_octal_escape.snap @@ -0,0 +1,46 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 0..16, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 0..4, + id: Name("bold"), + ctx: Store, + }, + ), + ], + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 7..16, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 7..16, + node_index: NodeIndex(None), + value: "\u{3}8[1m", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__double_quoted_byte.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__double_quoted_byte.snap new file mode 100644 index 00000000000..0b61a513786 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__double_quoted_byte.snap @@ -0,0 +1,290 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..738, + value: BytesLiteral( + ExprBytesLiteral { + node_index: NodeIndex(None), + range: 0..738, + value: BytesLiteralValue { + inner: Single( + BytesLiteral { + range: 0..738, + node_index: NodeIndex(None), + value: [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 157, + 158, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 209, + 210, + 211, + 212, + 213, + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225, + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245, + 246, + 247, + 248, + 249, + 250, + 251, + 252, + 253, + 254, + 255, + ], + flags: BytesLiteralFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_alias.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_alias.snap new file mode 100644 index 00000000000..e271519126f --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_alias.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..12, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..12, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 0..12, + node_index: NodeIndex(None), + value: "\u{1b}", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_char_in_byte_literal.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_char_in_byte_literal.snap new file mode 100644 index 00000000000..36ed21596ef --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_char_in_byte_literal.snap @@ -0,0 +1,44 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..13, + value: BytesLiteral( + ExprBytesLiteral { + node_index: NodeIndex(None), + range: 0..13, + value: BytesLiteralValue { + inner: Single( + BytesLiteral { + range: 0..13, + node_index: NodeIndex(None), + value: [ + 111, + 109, + 107, + 109, + 111, + 107, + 92, + 88, + 97, + 97, + ], + flags: BytesLiteralFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_octet.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_octet.snap new file mode 100644 index 00000000000..f4f90563abc --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_octet.snap @@ -0,0 +1,39 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..14, + value: BytesLiteral( + ExprBytesLiteral { + node_index: NodeIndex(None), + range: 0..14, + value: BytesLiteralValue { + inner: Single( + BytesLiteral { + range: 0..14, + node_index: NodeIndex(None), + value: [ + 35, + 97, + 4, + 83, + 52, + ], + flags: BytesLiteralFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__form_feed_alias.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__form_feed_alias.snap new file mode 100644 index 00000000000..a2121c46f80 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__form_feed_alias.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..15, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..15, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 0..15, + node_index: NodeIndex(None), + value: "\u{c}", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap new file mode 100644 index 00000000000..a0e76437cdc --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap @@ -0,0 +1,97 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..22, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..22, + value: FStringValue { + inner: Single( + FString { + range: 0..22, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..5, + node_index: NodeIndex(None), + value: "aaa", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..10, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 6..9, + id: Name("bbb"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 10..13, + node_index: NodeIndex(None), + value: "ccc", + }, + ), + Interpolation( + InterpolatedElement { + range: 13..18, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 14..17, + id: Name("ddd"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 18..21, + node_index: NodeIndex(None), + value: "eee", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap new file mode 100644 index 00000000000..922049ab6e1 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..8, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..8, + value: FStringValue { + inner: Single( + FString { + range: 0..8, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..4, + node_index: NodeIndex(None), + value: "\\", + }, + ), + Interpolation( + InterpolatedElement { + range: 4..7, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 5..6, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap new file mode 100644 index 00000000000..e71015793a4 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..8, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..8, + value: FStringValue { + inner: Single( + FString { + range: 0..8, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..4, + node_index: NodeIndex(None), + value: "\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 4..7, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 5..6, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap new file mode 100644 index 00000000000..f1721389a50 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap @@ -0,0 +1,65 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..9, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..9, + value: FStringValue { + inner: Single( + FString { + range: 0..9, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 3..5, + node_index: NodeIndex(None), + value: "\\\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..8, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap new file mode 100644 index 00000000000..8b2dd962f2f --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..10, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..10, + value: FStringValue { + inner: Single( + FString { + range: 0..10, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..7, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + expression: "user", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap new file mode 100644 index 00000000000..9d9d41c30ec --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap @@ -0,0 +1,102 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..38, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..38, + value: FStringValue { + inner: Single( + FString { + range: 0..38, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..6, + node_index: NodeIndex(None), + value: "mix ", + }, + ), + Interpolation( + InterpolatedElement { + range: 6..13, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 7..11, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + expression: "user", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 13..28, + node_index: NodeIndex(None), + value: " with text and ", + }, + ), + Interpolation( + InterpolatedElement { + range: 28..37, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 29..35, + id: Name("second"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + expression: "second", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap new file mode 100644 index 00000000000..1bf215a9cb7 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap @@ -0,0 +1,76 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..14, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..14, + value: FStringValue { + inner: Single( + FString { + range: 0..14, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..13, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..7, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + expression: "user", + trailing: "=", + }, + ), + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 9..12, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 9..12, + node_index: NodeIndex(None), + value: ">10", + }, + ), + ], + }, + ), + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap new file mode 100644 index 00000000000..0978f293175 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..11, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..11, + value: FStringValue { + inner: Single( + FString { + range: 0..11, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 4..5, + node_index: NodeIndex(None), + value: "\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..8, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__hts_alias.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__hts_alias.snap new file mode 100644 index 00000000000..392be7a93c1 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__hts_alias.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..9, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..9, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 0..9, + node_index: NodeIndex(None), + value: "\u{88}", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__invalid_byte_literal_error.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__invalid_byte_literal_error.snap new file mode 100644 index 00000000000..5cac49de923 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__invalid_byte_literal_error.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: error +--- +ParseError { + error: Lexical( + InvalidByteLiteral, + ), + location: 0..12, +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__invalid_unicode_literal.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__invalid_unicode_literal.snap new file mode 100644 index 00000000000..24deb403ce9 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__invalid_unicode_literal.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: error +--- +ParseError { + error: Lexical( + UnicodeEscapeError { + kind: TruncatedHexByte, + start: 0, + end: 2, + }, + ), + location: 0..9, +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__invalid_unicode_name_error.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__invalid_unicode_name_error.snap new file mode 100644 index 00000000000..92cfae586e9 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__invalid_unicode_name_error.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: error +--- +ParseError { + error: Lexical( + UnicodeEscapeError { + kind: UnknownName, + start: 0, + end: 10, + }, + ), + location: 0..13, +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__missing_unicode_lbrace_error.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__missing_unicode_lbrace_error.snap new file mode 100644 index 00000000000..1dd2c53b0e0 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__missing_unicode_lbrace_error.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: error +--- +ParseError { + error: Lexical( + UnicodeEscapeError { + kind: MalformedName, + start: 0, + end: 1, + }, + ), + location: 0..5, +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__missing_unicode_rbrace_error.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__missing_unicode_rbrace_error.snap new file mode 100644 index 00000000000..4987a7c98b5 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__missing_unicode_rbrace_error.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: error +--- +ParseError { + error: Lexical( + UnicodeEscapeError { + kind: MalformedName, + start: 0, + end: 7, + }, + ), + location: 0..10, +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap new file mode 100644 index 00000000000..6dd2bec2a0e --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap @@ -0,0 +1,35 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..3, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..3, + value: FStringValue { + inner: Single( + FString { + range: 0..3, + node_index: NodeIndex(None), + elements: [], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap new file mode 100644 index 00000000000..a7917162060 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap @@ -0,0 +1,35 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..3, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..3, + value: TStringValue { + inner: Single( + TString { + range: 0..3, + node_index: NodeIndex(None), + elements: [], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap new file mode 100644 index 00000000000..96f03262afa --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap @@ -0,0 +1,60 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..17, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..17, + value: FStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + node_index: NodeIndex(None), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + FString( + FString { + range: 9..17, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + node_index: NodeIndex(None), + value: "world", + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + ], + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap new file mode 100644 index 00000000000..96f03262afa --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap @@ -0,0 +1,60 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..17, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..17, + value: FStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + node_index: NodeIndex(None), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + FString( + FString { + range: 9..17, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + node_index: NodeIndex(None), + value: "world", + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + ], + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap new file mode 100644 index 00000000000..f0c4625a0d4 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap @@ -0,0 +1,93 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..22, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..22, + value: FStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + node_index: NodeIndex(None), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + FString( + FString { + range: 9..22, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + node_index: NodeIndex(None), + value: "world", + }, + ), + Interpolation( + InterpolatedElement { + range: 16..21, + node_index: NodeIndex(None), + expression: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 17..20, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 17..20, + node_index: NodeIndex(None), + value: "!", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + ], + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap new file mode 100644 index 00000000000..2b1936fb46b --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap @@ -0,0 +1,106 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..31, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..31, + value: FStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + node_index: NodeIndex(None), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + FString( + FString { + range: 9..22, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + node_index: NodeIndex(None), + value: "world", + }, + ), + Interpolation( + InterpolatedElement { + range: 16..21, + node_index: NodeIndex(None), + expression: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 17..20, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 17..20, + node_index: NodeIndex(None), + value: "!", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + Literal( + StringLiteral { + range: 23..31, + node_index: NodeIndex(None), + value: "again!", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + ], + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1_error.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1_error.snap new file mode 100644 index 00000000000..819956a4275 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1_error.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +ParseError { + error: OtherError( + "cannot mix t-string literals with string or bytes literals", + ), + location: 0..18, +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2_error.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2_error.snap new file mode 100644 index 00000000000..3b4b4164ec2 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2_error.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +ParseError { + error: OtherError( + "cannot mix t-string literals with string or bytes literals", + ), + location: 0..22, +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap new file mode 100644 index 00000000000..51c5bd1ec1e --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap @@ -0,0 +1,83 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..18, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..18, + value: FStringValue { + inner: Single( + FString { + range: 0..18, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..5, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..4, + id: Name("a"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + Interpolation( + InterpolatedElement { + range: 5..10, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 7..8, + id: Name("b"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 10..17, + node_index: NodeIndex(None), + value: "{foo}", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap new file mode 100644 index 00000000000..9f509cfcb1e --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap @@ -0,0 +1,78 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..13, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..13, + value: FStringValue { + inner: Single( + FString { + range: 0..13, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + node_index: NodeIndex(None), + expression: Compare( + ExprCompare { + node_index: NodeIndex(None), + range: 3..11, + ops: [ + Eq, + ], + operands: [ + NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 3..5, + value: Int( + 42, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 9..11, + value: Int( + 42, + ), + }, + ), + ], + runtime_comparators: None, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap new file mode 100644 index 00000000000..64bcd97bc8f --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap @@ -0,0 +1,112 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..16, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..16, + value: FStringValue { + inner: Single( + FString { + range: 0..16, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..15, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..14, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 7..14, + node_index: NodeIndex(None), + expression: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 8..13, + value: StringLiteralValue { + inner: Concatenated( + ConcatenatedStringLiteral { + strings: [ + StringLiteral { + range: 8..10, + node_index: NodeIndex(None), + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + StringLiteral { + range: 11..13, + node_index: NodeIndex(None), + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ], + value: "", + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + }, + ), + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap new file mode 100644 index 00000000000..04d9059dddc --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap @@ -0,0 +1,83 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..15, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..15, + value: FStringValue { + inner: Single( + FString { + range: 0..15, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..14, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..13, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 7..13, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 8..12, + id: Name("spec"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + }, + ), + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap new file mode 100644 index 00000000000..204e506619e --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap @@ -0,0 +1,96 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..13, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..13, + value: FStringValue { + inner: Single( + FString { + range: 0..13, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..11, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 7..11, + node_index: NodeIndex(None), + expression: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 8..10, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 8..10, + node_index: NodeIndex(None), + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + }, + ), + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap new file mode 100644 index 00000000000..ac2bf6c186e --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap @@ -0,0 +1,78 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..11, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..11, + value: FStringValue { + inner: Single( + FString { + range: 0..11, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..10, + node_index: NodeIndex(None), + expression: Compare( + ExprCompare { + node_index: NodeIndex(None), + range: 3..9, + ops: [ + NotEq, + ], + operands: [ + NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 3..4, + value: Int( + 1, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 8..9, + value: Int( + 2, + ), + }, + ), + ], + runtime_comparators: None, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap new file mode 100644 index 00000000000..52c5ead4363 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap @@ -0,0 +1,70 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..13, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..13, + value: FStringValue { + inner: Single( + FString { + range: 0..13, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..11, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 7..11, + node_index: NodeIndex(None), + value: "spec", + }, + ), + ], + }, + ), + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap new file mode 100644 index 00000000000..764c7e01530 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..10, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..10, + value: FStringValue { + inner: Single( + FString { + range: 0..10, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..4, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + expression: "x", + trailing: " =", + }, + ), + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap new file mode 100644 index 00000000000..fb50e7a19a4 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..10, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..10, + value: FStringValue { + inner: Single( + FString { + range: 0..10, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..4, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + expression: "x", + trailing: "= ", + }, + ), + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap new file mode 100644 index 00000000000..820aecec556 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..10, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..10, + value: FStringValue { + inner: Single( + FString { + range: 0..10, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + node_index: NodeIndex(None), + expression: Yield( + ExprYield { + node_index: NodeIndex(None), + range: 3..8, + value: None, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_concat.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_concat.snap new file mode 100644 index 00000000000..8b7e266d6df --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_concat.snap @@ -0,0 +1,49 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..16, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..16, + value: StringLiteralValue { + inner: Concatenated( + ConcatenatedStringLiteral { + strings: [ + StringLiteral { + range: 0..8, + node_index: NodeIndex(None), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + StringLiteral { + range: 9..16, + node_index: NodeIndex(None), + value: "world", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ], + value: "Hello world", + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_triple_quotes_with_kind.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_triple_quotes_with_kind.snap new file mode 100644 index 00000000000..bbacfc3e4ac --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_triple_quotes_with_kind.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..20, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..20, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 0..20, + node_index: NodeIndex(None), + value: "Hello, world!", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Unicode, + triple_quoted: true, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1_error.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1_error.snap new file mode 100644 index 00000000000..c9da8b3ca73 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1_error.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +ParseError { + error: OtherError( + "cannot mix t-string literals with string or bytes literals", + ), + location: 0..17, +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2_error.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2_error.snap new file mode 100644 index 00000000000..c9da8b3ca73 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2_error.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +ParseError { + error: OtherError( + "cannot mix t-string literals with string or bytes literals", + ), + location: 0..17, +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3_error.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3_error.snap new file mode 100644 index 00000000000..3b4b4164ec2 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3_error.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +ParseError { + error: OtherError( + "cannot mix t-string literals with string or bytes literals", + ), + location: 0..22, +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4_error.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4_error.snap new file mode 100644 index 00000000000..9ae7f8f91d1 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4_error.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +ParseError { + error: OtherError( + "cannot mix t-string literals with string or bytes literals", + ), + location: 0..31, +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap new file mode 100644 index 00000000000..37afbe0a605 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap @@ -0,0 +1,83 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..18, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..18, + value: TStringValue { + inner: Single( + TString { + range: 0..18, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..5, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..4, + id: Name("a"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + Interpolation( + InterpolatedElement { + range: 5..10, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 7..8, + id: Name("b"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 10..17, + node_index: NodeIndex(None), + value: "{foo}", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap new file mode 100644 index 00000000000..1124d57cc08 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap @@ -0,0 +1,78 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..13, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..13, + value: TStringValue { + inner: Single( + TString { + range: 0..13, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + node_index: NodeIndex(None), + expression: Compare( + ExprCompare { + node_index: NodeIndex(None), + range: 3..11, + ops: [ + Eq, + ], + operands: [ + NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 3..5, + value: Int( + 42, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 9..11, + value: Int( + 42, + ), + }, + ), + ], + runtime_comparators: None, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap new file mode 100644 index 00000000000..50203d0862f --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap @@ -0,0 +1,112 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..16, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..16, + value: TStringValue { + inner: Single( + TString { + range: 0..16, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..15, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..14, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 7..14, + node_index: NodeIndex(None), + expression: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 8..13, + value: StringLiteralValue { + inner: Concatenated( + ConcatenatedStringLiteral { + strings: [ + StringLiteral { + range: 8..10, + node_index: NodeIndex(None), + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + StringLiteral { + range: 11..13, + node_index: NodeIndex(None), + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ], + value: "", + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + }, + ), + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap new file mode 100644 index 00000000000..4744dd172bb --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap @@ -0,0 +1,83 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..15, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..15, + value: TStringValue { + inner: Single( + TString { + range: 0..15, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..14, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..13, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 7..13, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 8..12, + id: Name("spec"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + }, + ), + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap new file mode 100644 index 00000000000..0925628ab1c --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap @@ -0,0 +1,96 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..13, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..13, + value: TStringValue { + inner: Single( + TString { + range: 0..13, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..11, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 7..11, + node_index: NodeIndex(None), + expression: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 8..10, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 8..10, + node_index: NodeIndex(None), + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + }, + ), + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap new file mode 100644 index 00000000000..06deb34408e --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap @@ -0,0 +1,78 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..11, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..11, + value: TStringValue { + inner: Single( + TString { + range: 0..11, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..10, + node_index: NodeIndex(None), + expression: Compare( + ExprCompare { + node_index: NodeIndex(None), + range: 3..9, + ops: [ + NotEq, + ], + operands: [ + NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 3..4, + value: Int( + 1, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 8..9, + value: Int( + 2, + ), + }, + ), + ], + runtime_comparators: None, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap new file mode 100644 index 00000000000..b8eecfca360 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap @@ -0,0 +1,70 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..13, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..13, + value: TStringValue { + inner: Single( + TString { + range: 0..13, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..11, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 7..11, + node_index: NodeIndex(None), + value: "spec", + }, + ), + ], + }, + ), + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap new file mode 100644 index 00000000000..15b2b4222a0 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..10, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..10, + value: TStringValue { + inner: Single( + TString { + range: 0..10, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..4, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + expression: "x", + trailing: " =", + }, + ), + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap new file mode 100644 index 00000000000..b1591af5fd3 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..10, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..10, + value: TStringValue { + inner: Single( + TString { + range: 0..10, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..4, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + expression: "x", + trailing: "= ", + }, + ), + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap new file mode 100644 index 00000000000..8196e9d5318 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..10, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..10, + value: TStringValue { + inner: Single( + TString { + range: 0..10, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + node_index: NodeIndex(None), + expression: Yield( + ExprYield { + node_index: NodeIndex(None), + range: 3..8, + value: None, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap new file mode 100644 index 00000000000..a4c3363a0c4 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap @@ -0,0 +1,60 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..18, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..18, + value: FStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..9, + node_index: NodeIndex(None), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Unicode, + triple_quoted: false, + unclosed: false, + }, + }, + ), + FString( + FString { + range: 10..18, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + node_index: NodeIndex(None), + value: "world", + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + ], + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap new file mode 100644 index 00000000000..6492750da07 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap @@ -0,0 +1,73 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..22, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..22, + value: FStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..9, + node_index: NodeIndex(None), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Unicode, + triple_quoted: false, + unclosed: false, + }, + }, + ), + FString( + FString { + range: 10..18, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + node_index: NodeIndex(None), + value: "world", + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + Literal( + StringLiteral { + range: 19..22, + node_index: NodeIndex(None), + value: "!", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + ], + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_1.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_1.snap new file mode 100644 index 00000000000..6c751657101 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_1.snap @@ -0,0 +1,49 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..17, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..17, + value: StringLiteralValue { + inner: Concatenated( + ConcatenatedStringLiteral { + strings: [ + StringLiteral { + range: 0..8, + node_index: NodeIndex(None), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + StringLiteral { + range: 9..17, + node_index: NodeIndex(None), + value: "world", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Unicode, + triple_quoted: false, + unclosed: false, + }, + }, + ], + value: "Hello world", + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_2.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_2.snap new file mode 100644 index 00000000000..aca2fefc216 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_2.snap @@ -0,0 +1,49 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..17, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..17, + value: StringLiteralValue { + inner: Concatenated( + ConcatenatedStringLiteral { + strings: [ + StringLiteral { + range: 0..9, + node_index: NodeIndex(None), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Unicode, + triple_quoted: false, + unclosed: false, + }, + }, + StringLiteral { + range: 10..17, + node_index: NodeIndex(None), + value: "world", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ], + value: "Hello world", + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1_error.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1_error.snap new file mode 100644 index 00000000000..819956a4275 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1_error.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +ParseError { + error: OtherError( + "cannot mix t-string literals with string or bytes literals", + ), + location: 0..18, +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2_error.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2_error.snap new file mode 100644 index 00000000000..3b4b4164ec2 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2_error.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +ParseError { + error: OtherError( + "cannot mix t-string literals with string or bytes literals", + ), + location: 0..22, +} diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_1.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_1.snap new file mode 100644 index 00000000000..68e5a30fee1 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_1.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..8, + value: BytesLiteral( + ExprBytesLiteral { + node_index: NodeIndex(None), + range: 0..8, + value: BytesLiteralValue { + inner: Single( + BytesLiteral { + range: 0..8, + node_index: NodeIndex(None), + value: [ + 92, + 120, + 49, + 122, + ], + flags: BytesLiteralFlags { + quote_style: Single, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_2.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_2.snap new file mode 100644 index 00000000000..76ddf8520d9 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_2.snap @@ -0,0 +1,38 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..6, + value: BytesLiteral( + ExprBytesLiteral { + node_index: NodeIndex(None), + range: 0..6, + value: BytesLiteralValue { + inner: Single( + BytesLiteral { + range: 0..6, + node_index: NodeIndex(None), + value: [ + 92, + 92, + ], + flags: BytesLiteralFlags { + quote_style: Single, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap new file mode 100644 index 00000000000..b83e2b7ef52 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap @@ -0,0 +1,58 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..7, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..7, + value: FStringValue { + inner: Single( + FString { + range: 0..7, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 3..6, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 4..5, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap new file mode 100644 index 00000000000..fedb90aea31 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap @@ -0,0 +1,58 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..7, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..7, + value: TStringValue { + inner: Single( + TString { + range: 0..7, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 3..6, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 4..5, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__single_quoted_byte.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__single_quoted_byte.snap new file mode 100644 index 00000000000..b919928dc5f --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__single_quoted_byte.snap @@ -0,0 +1,290 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..738, + value: BytesLiteral( + ExprBytesLiteral { + node_index: NodeIndex(None), + range: 0..738, + value: BytesLiteralValue { + inner: Single( + BytesLiteral { + range: 0..738, + node_index: NodeIndex(None), + value: [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 157, + 158, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 209, + 210, + 211, + 212, + 213, + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225, + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245, + 246, + 247, + 248, + 249, + 250, + 251, + 252, + 253, + 254, + 255, + ], + flags: BytesLiteralFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_mac_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_mac_eol.snap new file mode 100644 index 00000000000..4352fb34d44 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_mac_eol.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..18, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..18, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 0..18, + node_index: NodeIndex(None), + value: "text more text", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_unix_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_unix_eol.snap new file mode 100644 index 00000000000..4352fb34d44 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_unix_eol.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..18, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..18, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 0..18, + node_index: NodeIndex(None), + value: "text more text", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_windows_eol.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_windows_eol.snap new file mode 100644 index 00000000000..58ff7730d8e --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_windows_eol.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..19, + value: StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 0..19, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 0..19, + node_index: NodeIndex(None), + value: "text more text", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap new file mode 100644 index 00000000000..4e607c7a65d --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap @@ -0,0 +1,58 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..11, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 0..11, + value: FStringValue { + inner: Single( + FString { + range: 0..11, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 5..8, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: true, + unclosed: false, + }, + }, + ), + }, + runtime_joined_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap new file mode 100644 index 00000000000..07931097aac --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap @@ -0,0 +1,58 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..11, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..11, + value: TStringValue { + inner: Single( + TString { + range: 0..11, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 5..8, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: true, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap new file mode 100644 index 00000000000..416126ca54d --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap @@ -0,0 +1,97 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..22, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..22, + value: TStringValue { + inner: Single( + TString { + range: 0..22, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..5, + node_index: NodeIndex(None), + value: "aaa", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..10, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 6..9, + id: Name("bbb"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 10..13, + node_index: NodeIndex(None), + value: "ccc", + }, + ), + Interpolation( + InterpolatedElement { + range: 13..18, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 14..17, + id: Name("ddd"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 18..21, + node_index: NodeIndex(None), + value: "eee", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap new file mode 100644 index 00000000000..665defa674a --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..8, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..8, + value: TStringValue { + inner: Single( + TString { + range: 0..8, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..4, + node_index: NodeIndex(None), + value: "\\", + }, + ), + Interpolation( + InterpolatedElement { + range: 4..7, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 5..6, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap new file mode 100644 index 00000000000..50db1e54f38 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..8, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..8, + value: TStringValue { + inner: Single( + TString { + range: 0..8, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..4, + node_index: NodeIndex(None), + value: "\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 4..7, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 5..6, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap new file mode 100644 index 00000000000..61e3bd112b3 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap @@ -0,0 +1,65 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..9, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..9, + value: TStringValue { + inner: Single( + TString { + range: 0..9, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 3..5, + node_index: NodeIndex(None), + value: "\\\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..8, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap new file mode 100644 index 00000000000..8983d383432 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..10, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..10, + value: TStringValue { + inner: Single( + TString { + range: 0..10, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..7, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + expression: "user", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap new file mode 100644 index 00000000000..8c0dd4e135c --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap @@ -0,0 +1,102 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..38, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..38, + value: TStringValue { + inner: Single( + TString { + range: 0..38, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..6, + node_index: NodeIndex(None), + value: "mix ", + }, + ), + Interpolation( + InterpolatedElement { + range: 6..13, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 7..11, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + expression: "user", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 13..28, + node_index: NodeIndex(None), + value: " with text and ", + }, + ), + Interpolation( + InterpolatedElement { + range: 28..37, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 29..35, + id: Name("second"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + expression: "second", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap new file mode 100644 index 00000000000..d906b82413d --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap @@ -0,0 +1,76 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..14, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..14, + value: TStringValue { + inner: Single( + TString { + range: 0..14, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..13, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 3..7, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + expression: "user", + trailing: "=", + }, + ), + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 9..12, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 9..12, + node_index: NodeIndex(None), + value: ">10", + }, + ), + ], + }, + ), + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap new file mode 100644 index 00000000000..988c2f8ec66 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 0..11, + value: TString( + ExprTString { + node_index: NodeIndex(None), + range: 0..11, + value: TStringValue { + inner: Single( + TString { + range: 0..11, + node_index: NodeIndex(None), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 4..5, + node_index: NodeIndex(None), + value: "\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..8, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + unclosed: false, + }, + }, + ), + }, + runtime_template_str: None, + runtime_values: None, + }, + ), + }, + ), +] diff --git a/vendor/rustpython-ruff_python_parser/src/string.rs b/vendor/rustpython-ruff_python_parser/src/string.rs new file mode 100644 index 00000000000..a9083b2b8f3 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/string.rs @@ -0,0 +1,1238 @@ +//! Parsing of string literals, bytes literals, and implicit string concatenation. + +use bstr::ByteSlice; +use std::fmt; + +use ruff_python_ast::token::TokenKind; +use ruff_python_ast::{self as ast, AnyStringFlags, AtomicNodeIndex, Expr, StringFlags}; +use ruff_text_size::{Ranged, TextRange, TextSize}; + +use crate::error::{LexicalError, LexicalErrorType, UnicodeEscapeErrorKind}; + +#[derive(Debug)] +pub(crate) enum StringType { + Str(ast::StringLiteral), + Bytes(ast::BytesLiteral), + FString(ast::FString), + TString(ast::TString), +} + +impl Ranged for StringType { + fn range(&self) -> TextRange { + match self { + Self::Str(node) => node.range(), + Self::Bytes(node) => node.range(), + Self::FString(node) => node.range(), + Self::TString(node) => node.range(), + } + } +} + +impl From for Expr { + fn from(string: StringType) -> Self { + match string { + StringType::Str(node) => Expr::from(node), + StringType::Bytes(node) => Expr::from(node), + StringType::FString(node) => Expr::from(node), + StringType::TString(node) => Expr::from(node), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum InterpolatedStringKind { + FString, + TString, +} + +impl InterpolatedStringKind { + #[inline] + pub(crate) const fn start_token(self) -> TokenKind { + match self { + InterpolatedStringKind::FString => TokenKind::FStringStart, + InterpolatedStringKind::TString => TokenKind::TStringStart, + } + } + + #[inline] + pub(crate) const fn middle_token(self) -> TokenKind { + match self { + InterpolatedStringKind::FString => TokenKind::FStringMiddle, + InterpolatedStringKind::TString => TokenKind::TStringMiddle, + } + } + + #[inline] + pub(crate) const fn end_token(self) -> TokenKind { + match self { + InterpolatedStringKind::FString => TokenKind::FStringEnd, + InterpolatedStringKind::TString => TokenKind::TStringEnd, + } + } +} + +impl fmt::Display for InterpolatedStringKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InterpolatedStringKind::FString => f.write_str("f-string"), + InterpolatedStringKind::TString => f.write_str("t-string"), + } + } +} + +enum EscapedChar { + Literal(char), + Escape(char), +} + +struct StringParser<'src> { + /// The raw content of the string e.g., the `foo` part in `"foo"`. + source: &'src str, + /// Current position of the parser in the source. + cursor: usize, + /// Flags that can be used to query information about the string. + flags: AnyStringFlags, + /// The location of the first character in the source from the start of the file. + offset: TextSize, + /// The range of the string literal. + range: TextRange, +} + +impl<'src> StringParser<'src> { + fn new(source: &'src str, flags: AnyStringFlags, offset: TextSize, range: TextRange) -> Self { + Self { + source, + cursor: 0, + flags, + offset, + range, + } + } + + #[inline] + fn skip_bytes(&mut self, bytes: usize) -> &str { + let skipped_str = &self.source[self.cursor..self.cursor + bytes]; + self.cursor += bytes; + skipped_str + } + + /// Returns the current position of the parser considering the offset. + #[inline] + fn position(&self) -> TextSize { + self.compute_position(self.cursor) + } + + /// Computes the position of the cursor considering the offset. + #[inline] + fn compute_position(&self, cursor: usize) -> TextSize { + self.offset + TextSize::try_from(cursor).unwrap() + } + + /// Returns the next byte in the string, if there is one. + /// + /// # Panics + /// + /// When the next byte is a part of a multi-byte character. + #[inline] + fn next_byte(&mut self) -> Option { + self.source.as_bytes()[self.cursor..].first().map(|&byte| { + self.cursor += 1; + byte + }) + } + + #[inline] + fn next_char(&mut self) -> Option { + self.source[self.cursor..].chars().next().inspect(|c| { + self.cursor += c.len_utf8(); + }) + } + + #[inline] + fn peek_byte(&self) -> Option { + self.source.as_bytes()[self.cursor..].first().copied() + } + + #[inline] + fn peek_char(&self) -> Option { + self.source[self.cursor..].chars().next() + } + + /// Returns the position of the byte at `index` as the unicode escape decoder sees the + /// content: every non-ASCII character is replaced by its `\UXXXXXXXX` escape, and a + /// backslash that starts a pair before a non-ASCII character or at the end gains `u005c`. + fn escape_position(&self, index: usize) -> u32 { + let width = |c: char| if c.is_ascii() { 1 } else { 10 }; + let mut position = 0; + let mut chars = self.source.char_indices().peekable(); + while let Some((offset, c)) = chars.next() { + if offset >= index { + break; + } + if c != '\\' { + position += width(c); + continue; + } + position += 1; + let Some(&(next_offset, next)) = chars.peek() else { + position += 5; + break; + }; + if !next.is_ascii() { + position += 5; + } + if next_offset >= index { + break; + } + chars.next(); + position += width(next); + } + position + } + + /// Returns the error for the escape sequence starting at the backslash at `start`, where the + /// decoder stopped before the byte at `end`. + fn escape_error(&self, kind: UnicodeEscapeErrorKind, start: usize, end: usize) -> LexicalError { + let error = if self.flags.is_byte_string() { + LexicalErrorType::BytesEscapeError { + position: u32::try_from(start).unwrap(), + } + } else { + LexicalErrorType::UnicodeEscapeError { + kind, + start: self.escape_position(start), + end: self.escape_position(end) - 1, + } + }; + LexicalError::new(error, self.range) + } + + fn parse_unicode_literal( + &mut self, + literal_number: usize, + escape_start: usize, + ) -> Result { + let kind = match literal_number { + 2 => UnicodeEscapeErrorKind::TruncatedHexByte, + 4 => UnicodeEscapeErrorKind::TruncatedShortUnicode, + _ => UnicodeEscapeErrorKind::TruncatedLongUnicode, + }; + let mut p: u32 = 0u32; + for i in 1..=literal_number { + let Some(d) = self.peek_char().and_then(|c| c.to_digit(16)) else { + return Err(self.escape_error(kind, escape_start, self.cursor)); + }; + self.cursor += 1; + p += d << ((literal_number - i) * 4); + } + match p { + 0xD800..=0xDFFF => Ok(std::char::REPLACEMENT_CHARACTER), + _ => std::char::from_u32(p).ok_or_else(|| { + self.escape_error( + UnicodeEscapeErrorKind::IllegalCharacter, + escape_start, + self.cursor, + ) + }), + } + } + + fn parse_octet(&mut self, o: u8) -> char { + let mut radix_bytes = [o, 0, 0]; + let mut len = 1; + + while len < 3 { + let Some(b'0'..=b'7') = self.peek_byte() else { + break; + }; + + radix_bytes[len] = self.next_byte().unwrap(); + len += 1; + } + + // OK because radix_bytes is always going to be in the ASCII range. + let radix_str = std::str::from_utf8(&radix_bytes[..len]).expect("ASCII bytes"); + let value = u32::from_str_radix(radix_str, 8).unwrap(); + char::from_u32(value).unwrap() + } + + fn parse_unicode_name(&mut self, escape_start: usize) -> Result { + if self.peek_char() != Some('{') { + return Err(self.escape_error( + UnicodeEscapeErrorKind::MalformedName, + escape_start, + self.cursor, + )); + } + self.cursor += 1; + let Some(close_idx) = self.source[self.cursor..].find('}') else { + return Err(self.escape_error( + UnicodeEscapeErrorKind::MalformedName, + escape_start, + self.source.len(), + )); + }; + if close_idx == 0 { + return Err(self.escape_error( + UnicodeEscapeErrorKind::MalformedName, + escape_start, + self.cursor, + )); + } + + let name_and_ending = self.skip_bytes(close_idx + 1); + let name = &name_and_ending[..name_and_ending.len() - 1]; + + unicode_names2::character(name).ok_or_else(|| { + self.escape_error( + UnicodeEscapeErrorKind::UnknownName, + escape_start, + self.cursor, + ) + }) + } + + /// Parse an escaped character, returning the new character. + fn parse_escaped_char(&mut self) -> Result, LexicalError> { + let escape_start = self.cursor - 1; + let Some(first_char) = self.next_char() else { + // TODO: check when this error case happens + return Err(LexicalError::new( + LexicalErrorType::StringError, + TextRange::empty(self.position()), + )); + }; + + let new_char = match first_char { + '\\' => '\\', + '\'' => '\'', + '\"' => '"', + 'a' => '\x07', + 'b' => '\x08', + 'f' => '\x0c', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + 'v' => '\x0b', + o @ '0'..='7' => self.parse_octet(o as u8), + 'x' => self.parse_unicode_literal(2, escape_start)?, + 'u' if !self.flags.is_byte_string() => self.parse_unicode_literal(4, escape_start)?, + 'U' if !self.flags.is_byte_string() => self.parse_unicode_literal(8, escape_start)?, + 'N' if !self.flags.is_byte_string() => self.parse_unicode_name(escape_start)?, + // Special cases where the escape sequence is not a single character + '\n' => return Ok(None), + '\r' => { + if self.peek_byte() == Some(b'\n') { + self.next_byte(); + } + + return Ok(None); + } + _ => return Ok(Some(EscapedChar::Escape(first_char))), + }; + + Ok(Some(EscapedChar::Literal(new_char))) + } + + fn parse_interpolated_string_middle( + mut self, + ) -> Result { + // Fast-path: if the f-string or t-string doesn't contain any escape sequences, return the literal. + let Some(mut index) = memchr::memchr3(b'{', b'}', b'\\', self.source.as_bytes()) else { + return Ok(ast::InterpolatedStringLiteralElement { + value: self.source.into(), + range: self.range, + node_index: AtomicNodeIndex::NONE, + }); + }; + + let mut value = String::with_capacity(self.source.len()); + loop { + // Add the characters before the escape sequence (or curly brace) to the string. + let before_with_slash_or_brace = self.skip_bytes(index + 1); + let before = &before_with_slash_or_brace[..before_with_slash_or_brace.len() - 1]; + value.push_str(before); + + // Add the escaped character to the string. + match self.source.as_bytes()[self.cursor - 1] { + // If there are any curly braces inside a `F/TStringMiddle` token, + // then they were escaped (i.e. `{{` or `}}`). This means that + // the raw source contains a doubled brace, but the literal value only + // contains one brace. + brace @ (b'{' | b'}') => { + if self.peek_byte() == Some(brace) { + self.next_byte(); + } + value.push(char::from(brace)); + } + // We can encounter a `\` as the last character in a `F/TStringMiddle` + // token which is valid in this context. For example, + // + // ```python + // f"\{foo} \{bar:\}" + // # ^ ^^ ^ + // ``` + // + // Here, the `F/TStringMiddle` token content will be "\" and " \" + // which is invalid if we look at the content in isolation: + // + // ```python + // "\" + // ``` + // + // However, the content is syntactically valid in the context of + // the f/t-string because it's a substring of the entire f/t-string. + // This is still an invalid escape sequence, but we don't want to + // raise a syntax error as is done by the CPython parser. It might + // be supported in the future, refer to point 3: https://peps.python.org/pep-0701/#rejected-ideas + b'\\' => { + if !self.flags.is_raw_string() && self.peek_byte().is_some() { + if let Some(brace @ (b'{' | b'}')) = self.peek_byte() + && self.source.as_bytes().get(self.cursor + 1).copied() == Some(brace) + { + // Leave the doubled brace for the next iteration to collapse. + value.push('\\'); + } else { + match self.parse_escaped_char()? { + None => {} + Some(EscapedChar::Literal(c)) => value.push(c), + Some(EscapedChar::Escape(c)) => { + value.push('\\'); + value.push(c); + } + } + } + } else { + value.push('\\'); + } + } + ch => { + unreachable!("Expected '{{', '}}', or '\\' but got {:?}", ch); + } + } + + let Some(next_index) = + memchr::memchr3(b'{', b'}', b'\\', &self.source.as_bytes()[self.cursor..]) + else { + // Add the rest of the string to the value. + let rest = &self.source[self.cursor..]; + value.push_str(rest); + break; + }; + + index = next_index; + } + + Ok(ast::InterpolatedStringLiteralElement { + value: value.into_boxed_str(), + range: self.range, + node_index: AtomicNodeIndex::NONE, + }) + } + + fn parse_bytes(mut self) -> Result { + if self.source.as_bytes().find_non_ascii_byte().is_some() { + return Err(LexicalError::new( + LexicalErrorType::InvalidByteLiteral, + self.range, + )); + } + + if self.flags.is_raw_string() { + // For raw strings, no escaping is necessary. + return Ok(StringType::Bytes(ast::BytesLiteral { + value: self.source.as_bytes().into(), + range: self.range, + flags: self.flags.into(), + node_index: AtomicNodeIndex::NONE, + })); + } + + let Some(mut escape) = memchr::memchr(b'\\', self.source.as_bytes()) else { + // If the string doesn't contain any escape sequences, return the owned string. + return Ok(StringType::Bytes(ast::BytesLiteral { + value: self.source.as_bytes().into(), + range: self.range, + flags: self.flags.into(), + node_index: AtomicNodeIndex::NONE, + })); + }; + + // If the string contains escape sequences, we need to parse them. + let mut value = Vec::with_capacity(self.source.len()); + loop { + // Add the characters before the escape sequence to the string. + let before_with_slash = self.skip_bytes(escape + 1); + let before = &before_with_slash[..before_with_slash.len() - 1]; + value.extend_from_slice(before.as_bytes()); + + // Add the escaped character to the string. + match self.parse_escaped_char()? { + None => {} + Some(EscapedChar::Literal(c)) => value.push(c as u8), + Some(EscapedChar::Escape(c)) => { + value.push(b'\\'); + value.push(c as u8); + } + } + + let Some(next_escape) = memchr::memchr(b'\\', &self.source.as_bytes()[self.cursor..]) + else { + // Add the rest of the string to the value. + let rest = &self.source[self.cursor..]; + value.extend_from_slice(rest.as_bytes()); + break; + }; + + // Update the position of the next escape sequence. + escape = next_escape; + } + + Ok(StringType::Bytes(ast::BytesLiteral { + value: value.into_boxed_slice(), + range: self.range, + flags: self.flags.into(), + node_index: AtomicNodeIndex::NONE, + })) + } + + fn parse_string(mut self) -> Result { + if self.flags.is_raw_string() { + // For raw strings, no escaping is necessary. + return Ok(StringType::Str(ast::StringLiteral { + value: self.source.into(), + range: self.range, + flags: self.flags.into(), + node_index: AtomicNodeIndex::NONE, + })); + } + + let Some(mut escape) = memchr::memchr(b'\\', self.source.as_bytes()) else { + // If the string doesn't contain any escape sequences, return the owned string. + return Ok(StringType::Str(ast::StringLiteral { + value: self.source.into(), + range: self.range, + flags: self.flags.into(), + node_index: AtomicNodeIndex::NONE, + })); + }; + + // If the string contains escape sequences, we need to parse them. + let mut value = String::with_capacity(self.source.len()); + + loop { + // Add the characters before the escape sequence to the string. + let before_with_slash = self.skip_bytes(escape + 1); + let before = &before_with_slash[..before_with_slash.len() - 1]; + value.push_str(before); + + // Add the escaped character to the string. + match self.parse_escaped_char()? { + None => {} + Some(EscapedChar::Literal(c)) => value.push(c), + Some(EscapedChar::Escape(c)) => { + value.push('\\'); + value.push(c); + } + } + + let Some(next_escape) = self.source[self.cursor..].find('\\') else { + // Add the rest of the string to the value. + let rest = &self.source[self.cursor..]; + value.push_str(rest); + break; + }; + + // Update the position of the next escape sequence. + escape = next_escape; + } + + Ok(StringType::Str(ast::StringLiteral { + value: value.into_boxed_str(), + range: self.range, + flags: self.flags.into(), + node_index: AtomicNodeIndex::NONE, + })) + } + + fn parse(self) -> Result { + if self.flags.is_byte_string() { + self.parse_bytes() + } else { + self.parse_string() + } + } +} + +pub(crate) fn parse_string_literal( + source: &str, + flags: AnyStringFlags, + range: TextRange, +) -> Result { + StringParser::new(source, flags, range.start() + flags.opener_len(), range).parse() +} + +pub(crate) fn parse_interpolated_string_literal_element( + source: &str, + flags: AnyStringFlags, + range: TextRange, +) -> Result { + StringParser::new(source, flags, range.start(), range).parse_interpolated_string_middle() +} + +#[cfg(test)] +mod tests { + use ruff_python_ast::Suite; + + use crate::error::LexicalErrorType; + use crate::{InterpolatedStringErrorType, ParseError, ParseErrorType, Parsed, parse_module}; + + const WINDOWS_EOL: &str = "\r\n"; + const MAC_EOL: &str = "\r"; + const UNIX_EOL: &str = "\n"; + + fn parse_suite(source: &str) -> Result { + parse_module(source).map(Parsed::into_suite) + } + + fn nested_format_spec(prefix: char, depth: usize) -> String { + let mut replacement_field = String::from("{spec}"); + for _ in 0..depth { + replacement_field = format!("{{foo:{replacement_field}}}"); + } + format!(r#"{prefix}"{replacement_field}""#) + } + + fn string_parser_escaped_eol(eol: &str) -> Suite { + let source = format!(r"'text \{eol}more text'"); + parse_suite(&source).unwrap() + } + + #[test] + fn test_string_parser_escaped_unix_eol() { + let suite = string_parser_escaped_eol(UNIX_EOL); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_string_parser_escaped_mac_eol() { + let suite = string_parser_escaped_eol(MAC_EOL); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_string_parser_escaped_windows_eol() { + let suite = string_parser_escaped_eol(WINDOWS_EOL); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_fstring() { + let source = r#"f"{a}{ b }{{foo}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_fstring_nested_spec() { + let source = r#"f"{foo:{spec}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn parse_fstring_nested_spec_grows_stack() { + let error = parse_suite(&nested_format_spec('f', 200)).unwrap_err(); + assert_eq!( + error.error, + ParseErrorType::Lexical(LexicalErrorType::TooDeeplyNestedBrackets) + ); + } + + #[test] + fn test_parse_fstring_not_nested_spec() { + let source = r#"f"{foo:spec}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_empty_fstring() { + let source = r#"f"""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_fstring_parse_self_documenting_base() { + let source = r#"f"{user=}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_fstring_parse_self_documenting_base_more() { + let source = r#"f"mix {user=} with text and {second=}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_fstring_parse_self_documenting_format() { + let source = r#"f"{user=:>10}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + fn parse_fstring_error(source: &str) -> InterpolatedStringErrorType { + parse_suite(source) + .map_err(|e| match e.error { + ParseErrorType::Lexical(LexicalErrorType::FStringError(e)) => e, + ParseErrorType::FStringError(e) => e, + e => unreachable!("Expected FStringError: {:?}", e), + }) + .expect_err("Expected error") + } + + #[test] + fn test_parse_invalid_fstring() { + use InterpolatedStringErrorType::{InvalidConversionFlag, LambdaWithoutParentheses}; + + assert_eq!(parse_fstring_error(r#"f"{5!1}""#), InvalidConversionFlag); + assert_eq!( + parse_fstring_error("f'{lambda x:{x}}'"), + LambdaWithoutParentheses + ); + // NOTE: The parser produces the `LambdaWithoutParentheses` for this case, but + // since the parser only return the first error to maintain compatibility with + // the rest of the codebase, this test case fails. The `LambdaWithoutParentheses` + // error appears after the unexpected `FStringMiddle` token, which is between the + // `:` and the `{`. + // assert_eq!(parse_fstring_error("f'{lambda x: {x}}'"), LambdaWithoutParentheses); + assert!(parse_suite(r#"f"{class}""#).is_err()); + } + + #[test] + fn test_parse_fstring_not_equals() { + let source = r#"f"{1 != 2}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_fstring_equals() { + let source = r#"f"{42 == 42}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_fstring_self_doc_prec_space() { + let source = r#"f"{x =}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_fstring_self_doc_trailing_space() { + let source = r#"f"{x= }""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_fstring_yield_expr() { + let source = r#"f"{yield}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring() { + let source = r#"t"{a}{ b }{{foo}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_nested_spec() { + let source = r#"t"{foo:{spec}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn parse_tstring_nested_spec_grows_stack() { + let error = parse_suite(&nested_format_spec('t', 200)).unwrap_err(); + assert_eq!( + error.error, + ParseErrorType::Lexical(LexicalErrorType::TooDeeplyNestedBrackets) + ); + } + + #[test] + fn test_parse_tstring_not_nested_spec() { + let source = r#"t"{foo:spec}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_empty_tstring() { + let source = r#"t"""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_parse_self_documenting_base() { + let source = r#"t"{user=}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_parse_self_documenting_base_more() { + let source = r#"t"mix {user=} with text and {second=}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_parse_self_documenting_format() { + let source = r#"t"{user=:>10}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + fn parse_tstring_error(source: &str) -> InterpolatedStringErrorType { + parse_suite(source) + .map_err(|e| match e.error { + ParseErrorType::Lexical(LexicalErrorType::TStringError(e)) => e, + ParseErrorType::TStringError(e) => e, + e => unreachable!("Expected TStringError: {:?}", e), + }) + .expect_err("Expected error") + } + + #[test] + fn test_parse_invalid_tstring() { + use InterpolatedStringErrorType::{InvalidConversionFlag, LambdaWithoutParentheses}; + + assert_eq!(parse_tstring_error(r#"t"{5!1}""#), InvalidConversionFlag); + assert_eq!( + parse_tstring_error("t'{lambda x:{x}}'"), + LambdaWithoutParentheses + ); + // NOTE: The parser produces the `LambdaWithoutParentheses` for this case, but + // since the parser only return the first error to maintain compatibility with + // the rest of the codebase, this test case fails. The `LambdaWithoutParentheses` + // error appears after the unexpected `tStringMiddle` token, which is between the + // `:` and the `{`. + // assert_eq!(parse_tstring_error("f'{lambda x: {x}}'"), LambdaWithoutParentheses); + assert!(parse_suite(r#"t"{class}""#).is_err()); + } + + #[test] + fn test_parse_tstring_not_equals() { + let source = r#"t"{1 != 2}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_equals() { + let source = r#"t"{42 == 42}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_self_doc_prec_space() { + let source = r#"t"{x =}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_self_doc_trailing_space() { + let source = r#"t"{x= }""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_yield_expr() { + let source = r#"t"{yield}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_string_concat() { + let source = "'Hello ' 'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_u_string_concat_1() { + let source = "'Hello ' u'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_u_string_concat_2() { + let source = "u'Hello ' 'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_f_string_concat_1() { + let source = "'Hello ' f'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_f_string_concat_2() { + let source = "'Hello ' f'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_f_string_concat_3() { + let source = "'Hello ' f'world{\"!\"}'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_f_string_concat_4() { + let source = "'Hello ' f'world{\"!\"}' 'again!'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_u_f_string_concat_1() { + let source = "u'Hello ' f'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_u_f_string_concat_2() { + let source = "u'Hello ' f'world' '!'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_t_string_concat_1_error() { + let source = "'Hello ' t'world'"; + let suite = parse_suite(source).unwrap_err(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_t_string_concat_2_error() { + let source = "'Hello ' t'world'"; + let suite = parse_suite(source).unwrap_err(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_t_string_concat_3_error() { + let source = "'Hello ' t'world{\"!\"}'"; + let suite = parse_suite(source).unwrap_err(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_t_string_concat_4_error() { + let source = "'Hello ' t'world{\"!\"}' 'again!'"; + let suite = parse_suite(source).unwrap_err(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_u_t_string_concat_1_error() { + let source = "u'Hello ' t'world'"; + let suite = parse_suite(source).unwrap_err(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_u_t_string_concat_2_error() { + let source = "u'Hello ' t'world' '!'"; + let suite = parse_suite(source).unwrap_err(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_f_t_string_concat_1_error() { + let source = "f'Hello ' t'world'"; + let suite = parse_suite(source).unwrap_err(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_f_t_string_concat_2_error() { + let source = "f'Hello ' t'world' '!'"; + let suite = parse_suite(source).unwrap_err(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_string_triple_quotes_with_kind() { + let source = "u'''Hello, world!'''"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_single_quoted_byte() { + // single quote + let source = r##"b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'"##; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_double_quoted_byte() { + // double quote + let source = r##"b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff""##; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_escape_char_in_byte_literal() { + // backslash does not escape + let source = r#"b"omkmok\Xaa""#; // spell-checker:ignore omkmok + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_raw_byte_literal_1() { + let source = r"rb'\x1z'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_raw_byte_literal_2() { + let source = r"rb'\\'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_escape_octet() { + let source = r"b'\43a\4\1234'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_fstring_escaped_newline() { + let source = r#"f"\n{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_fstring_constant_range() { + let source = r#"f"aaa{bbb}ccc{ddd}eee""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_fstring_unescaped_newline() { + let source = r#"f""" +{x}""""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_fstring_escaped_character() { + let source = r#"f"\\{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_raw_fstring() { + let source = r#"rf"{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_triple_quoted_raw_fstring() { + let source = r#"rf"""{x}""""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_fstring_line_continuation() { + let source = r#"rf"\ +{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_fstring_nested_string_spec() { + let source = r#"f"{foo:{''}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_fstring_nested_concatenation_string_spec() { + let source = r#"f"{foo:{'' ''}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_escaped_newline() { + let source = r#"t"\n{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_constant_range() { + let source = r#"t"aaa{bbb}ccc{ddd}eee""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_unescaped_newline() { + let source = r#"t""" +{x}""""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_escaped_character() { + let source = r#"t"\\{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_raw_tstring() { + let source = r#"rt"{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_triple_quoted_raw_tstring() { + let source = r#"rt"""{x}""""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_line_continuation() { + let source = r#"rt"\ +{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_nested_string_spec() { + let source = r#"t"{foo:{''}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_nested_concatenation_string_spec() { + let source = r#"t"{foo:{'' ''}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + /// + #[test] + fn test_dont_panic_on_8_in_octal_escape() { + let source = r"bold = '\038[1m'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_invalid_unicode_literal() { + let source = r"'\x1ó34'"; + let error = parse_suite(source).unwrap_err(); + insta::assert_debug_snapshot!(error); + } + + #[test] + fn test_missing_unicode_lbrace_error() { + let source = r"'\N '"; + let error = parse_suite(source).unwrap_err(); + insta::assert_debug_snapshot!(error); + } + + #[test] + fn test_missing_unicode_rbrace_error() { + let source = r"'\N{SPACE'"; + let error = parse_suite(source).unwrap_err(); + insta::assert_debug_snapshot!(error); + } + + #[test] + fn test_invalid_unicode_name_error() { + let source = r"'\N{INVALID}'"; + let error = parse_suite(source).unwrap_err(); + insta::assert_debug_snapshot!(error); + } + + #[test] + fn test_invalid_byte_literal_error() { + let source = r"b'123a𝐁c'"; + let error = parse_suite(source).unwrap_err(); + insta::assert_debug_snapshot!(error); + } + + macro_rules! test_aliases_parse { + ($($name:ident: $alias:expr,)*) => { + $( + #[test] + fn $name() { + let source = format!(r#""\N{{{0}}}""#, $alias); + let suite = parse_suite(&source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + )* + } + } + + test_aliases_parse! { + test_backspace_alias: "BACKSPACE", + test_bell_alias: "BEL", + test_carriage_return_alias: "CARRIAGE RETURN", + test_delete_alias: "DELETE", + test_escape_alias: "ESCAPE", + test_form_feed_alias: "FORM FEED", + test_hts_alias: "HTS", + test_character_tabulation_with_justification_alias: "CHARACTER TABULATION WITH JUSTIFICATION", + } +} diff --git a/vendor/rustpython-ruff_python_parser/src/token_set.rs b/vendor/rustpython-ruff_python_parser/src/token_set.rs new file mode 100644 index 00000000000..8edcc61118b --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/token_set.rs @@ -0,0 +1,52 @@ +use ruff_python_ast::token::TokenKind; + +/// A bit-set of `TokenKind`s +#[derive(Clone, Copy)] +pub(crate) struct TokenSet(u128); + +impl TokenSet { + pub(crate) const fn new(kinds: [TokenKind; N]) -> TokenSet { + let mut res = 0u128; + let mut i = 0usize; + + while i < N { + let kind = kinds[i]; + res |= mask(kind); + i += 1; + } + TokenSet(res) + } + + pub(crate) const fn union(self, other: TokenSet) -> TokenSet { + TokenSet(self.0 | other.0) + } + + pub(crate) const fn remove(self, kind: TokenKind) -> TokenSet { + TokenSet(self.0 & !mask(kind)) + } + + pub(crate) const fn contains(&self, kind: TokenKind) -> bool { + self.0 & mask(kind) != 0 + } +} + +const fn mask(kind: TokenKind) -> u128 { + 1u128 << (kind as usize) +} + +impl From<[TokenKind; N]> for TokenSet { + fn from(kinds: [TokenKind; N]) -> Self { + TokenSet::new(kinds) + } +} + +#[test] +fn token_set_works_for_tokens() { + use ruff_python_ast::token::TokenKind::*; + let mut ts = TokenSet::new([EndOfFile, Identifier]); + assert!(ts.contains(EndOfFile)); + assert!(ts.contains(Identifier)); + assert!(!ts.contains(Plus)); + ts = ts.remove(Identifier); + assert!(!ts.contains(Identifier)); +} diff --git a/vendor/rustpython-ruff_python_parser/src/token_source.rs b/vendor/rustpython-ruff_python_parser/src/token_source.rs new file mode 100644 index 00000000000..c4dabb52414 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/token_source.rs @@ -0,0 +1,276 @@ +use ruff_python_ast::token::{Token, TokenFlags, TokenKind}; +use ruff_text_size::{Ranged, TextRange, TextSize}; + +use crate::Mode; +use crate::error::LexicalError; +use crate::lexer::{Lexer, LexerCheckpoint}; +use crate::string::InterpolatedStringKind; + +/// Token source for the parser that skips over any trivia tokens. +#[derive(Debug)] +pub(crate) struct TokenSource<'src> { + /// The underlying source for the tokens. + lexer: Lexer<'src>, + + /// A vector containing all the tokens emitted by the lexer. This is returned when the parser + /// is finished consuming all the tokens. Note that unlike the emitted tokens, this vector + /// holds both the trivia and non-trivia tokens. + tokens: Vec, +} + +impl<'src> TokenSource<'src> { + /// Create a new token source for the given lexer. + fn new(lexer: Lexer<'src>, source: &str, start_offset: TextSize) -> Self { + TokenSource { + lexer, + tokens: allocate_tokens_vec(&source[start_offset.to_usize()..]), + } + } + + /// Create a new token source from the given source code which starts at the given offset. + pub(crate) fn from_source(source: &'src str, mode: Mode, start_offset: TextSize) -> Self { + let lexer = Lexer::new(source, mode, start_offset); + let mut source = TokenSource::new(lexer, source, start_offset); + + // Initialize the token source so that the current token is set correctly. + source.do_bump(); + source + } + + /// Returns the kind of the current token. + pub(crate) const fn current_kind(&self) -> TokenKind { + self.lexer.current_kind() + } + + /// Returns the range of the current token. + pub(crate) const fn current_range(&self) -> TextRange { + self.lexer.current_range() + } + + /// Returns the flags for the current token. + pub(crate) const fn current_flags(&self) -> TokenFlags { + self.lexer.current_flags() + } + + /// Returns `true` if the lexer found an error that stops tokenization in `range`. + pub(crate) fn has_tokenizer_error_in(&self, range: TextRange) -> bool { + self.lexer.errors().iter().any(|error| { + range.contains_inclusive(error.location().start()) && error.error().is_tokenizer_error() + }) + } + + /// Calls the underlying [`re_lex_logical_token`] method on the lexer with the new lexer + /// position and updates the token vector accordingly. + /// + /// [`re_lex_logical_token`]: Lexer::re_lex_logical_token + pub(crate) fn re_lex_logical_token(&mut self) { + let mut non_logical_newline = None; + + #[cfg(debug_assertions)] + let last_non_trivia_end_before = { + self.tokens + .iter() + .rev() + .find(|tok| !tok.kind().is_trivia()) + .map(ruff_text_size::Ranged::end) + }; + + for (index, token) in self.tokens.iter().enumerate().rev() { + match token.kind() { + TokenKind::NonLogicalNewline => { + non_logical_newline = Some((index, token.start())); + } + TokenKind::Comment => continue, + _ => break, + } + } + + if !self + .lexer + .re_lex_logical_token(non_logical_newline.map(|(_, start)| start)) + { + return; + } + + let non_logical_line_index = non_logical_newline + .expect( + "`re_lex_logical_token` should only return `true` if `non_logical_line` is `Some`", + ) + .0; + + // Trim the already bumped logical line token (and comments coming after it) as it might now have become a logical line token + self.tokens.truncate(non_logical_line_index); + + #[cfg(debug_assertions)] + { + let last_non_trivia_end_now = { + self.tokens + .iter() + .rev() + .find(|tok| !tok.kind().is_trivia()) + .map(ruff_text_size::Ranged::end) + }; + + assert_eq!(last_non_trivia_end_before, last_non_trivia_end_now); + } + + // Ensure `current` is positioned at a non-trivia token. + if self.current_kind().is_trivia() { + self.bump(self.current_kind()); + } + } + + pub(crate) fn re_lex_string_token_in_interpolation_element( + &mut self, + kind: InterpolatedStringKind, + ) { + self.lexer + .re_lex_string_token_in_interpolation_element(kind); + } + + pub(crate) fn re_lex_raw_string_in_format_spec(&mut self) { + self.lexer.re_lex_raw_string_in_format_spec(); + } + + /// Returns the next non-trivia token without consuming it. + /// + /// Use [`peek2`] to get the next two tokens. + /// + /// [`peek2`]: TokenSource::peek2 + pub(crate) fn peek(&mut self) -> TokenKind { + let checkpoint = self.lexer.checkpoint(); + let next = self.next_non_trivia_token(); + self.lexer.rewind(checkpoint); + next + } + + /// Returns the next two non-trivia tokens without consuming it. + /// + /// Use [`peek`] to only get the next token. + /// + /// [`peek`]: TokenSource::peek + pub(crate) fn peek2(&mut self) -> (TokenKind, TokenKind) { + let checkpoint = self.lexer.checkpoint(); + let first = self.next_non_trivia_token(); + let second = self.next_non_trivia_token(); + self.lexer.rewind(checkpoint); + (first, second) + } + + /// Bumps the token source to the next non-trivia token. + /// + /// It pushes the given kind to the token vector with the current token range. + pub(crate) fn bump(&mut self, kind: TokenKind) { + self.tokens + .push(Token::new(kind, self.current_range(), self.current_flags())); + self.do_bump(); + } + + /// Bumps the token source to the next non-trivia token without adding the current token to the + /// token vector. It does add the trivia tokens to the token vector. + fn do_bump(&mut self) { + loop { + let kind = self.lexer.next_token(); + if kind.is_trivia() { + self.tokens + .push(Token::new(kind, self.current_range(), self.current_flags())); + continue; + } + break; + } + } + + /// Returns the next non-trivia token without adding it to the token vector. + fn next_non_trivia_token(&mut self) -> TokenKind { + loop { + let kind = self.lexer.next_token(); + if kind.is_trivia() { + continue; + } + break kind; + } + } + + /// Creates a checkpoint to which the token source can later return to using [`Self::rewind`]. + pub(crate) fn checkpoint(&self) -> TokenSourceCheckpoint { + TokenSourceCheckpoint { + lexer_checkpoint: self.lexer.checkpoint(), + tokens_position: self.tokens.len(), + } + } + + /// Restore the token source to the given checkpoint. + pub(crate) fn rewind(&mut self, checkpoint: TokenSourceCheckpoint) { + let TokenSourceCheckpoint { + lexer_checkpoint, + tokens_position, + } = checkpoint; + + self.lexer.rewind(lexer_checkpoint); + self.tokens.truncate(tokens_position); + } + + /// Returns the tokens bumped so far, including the trivia tokens. + pub(crate) fn bumped(&self) -> &[Token] { + &self.tokens + } + + /// Returns a slice of [`Token`] that are within the given `range`. + pub(crate) fn in_range(&self, range: TextRange) -> &[Token] { + let start = self + .tokens + .iter() + .rposition(|tok| tok.start() == range.start()); + let end = self.tokens.iter().rposition(|tok| tok.end() == range.end()); + + let (Some(start), Some(end)) = (start, end) else { + return &self.tokens; + }; + + &self.tokens[start..=end] + } + + /// Consumes the token source, returning the collected tokens, comment ranges, and any errors + /// encountered during lexing. The token collection includes both the trivia and non-trivia + /// tokens. + pub(crate) fn finish(mut self) -> (Vec, Vec) { + assert_eq!( + self.current_kind(), + TokenKind::EndOfFile, + "TokenSource was not fully consumed" + ); + + // The `EndOfFile` token shouldn't be included in the token stream, it's mainly to signal + // the parser to stop. This isn't in `do_bump` because it only needs to be done once. + if let Some(last) = self.tokens.pop() { + assert_eq!(last.kind(), TokenKind::EndOfFile); + } + + self.tokens.shrink_to_fit(); + + (self.tokens, self.lexer.finish()) + } +} + +pub(crate) struct TokenSourceCheckpoint { + lexer_checkpoint: LexerCheckpoint, + tokens_position: usize, +} + +/// Allocates a token buffer with a capacity intended to skip early grows. +fn allocate_tokens_vec(contents: &str) -> Vec { + // In sampled ruff-ecosystem projects, about three quarters of Python files contain at least + // one token per eight source bytes. Intentionally underestimate the final token count to avoid + // over-reserving for token-sparse files. + const BYTES_PER_PREALLOCATED_TOKEN: usize = 8; + const MIN_INITIAL_CAPACITY: usize = 128; + + let capacity_hint = contents.len() / BYTES_PER_PREALLOCATED_TOKEN; + if capacity_hint < MIN_INITIAL_CAPACITY { + return Vec::new(); + } + + // Stay on a power-of-two bucket so that later geometric growth does not preserve an + // arbitrary capacity offset. + Vec::with_capacity(1 << capacity_hint.ilog2()) +} diff --git a/vendor/rustpython-ruff_python_parser/src/typing.rs b/vendor/rustpython-ruff_python_parser/src/typing.rs new file mode 100644 index 00000000000..e0c690c3dd7 --- /dev/null +++ b/vendor/rustpython-ruff_python_parser/src/typing.rs @@ -0,0 +1,92 @@ +//! This module takes care of parsing a type annotation. + +use ruff_python_ast::relocate::relocate_expr; +use ruff_python_ast::{Expr, ExprStringLiteral, ModExpression, StringLiteral}; +use ruff_text_size::Ranged; + +use crate::{ParseError, Parsed, parse_expression, parse_string_annotation}; + +type AnnotationParseResult = Result; + +#[derive(Debug)] +pub struct ParsedAnnotation { + parsed: Parsed, + kind: AnnotationKind, +} + +impl ParsedAnnotation { + pub fn parsed(&self) -> &Parsed { + &self.parsed + } + + pub fn expression(&self) -> &Expr { + self.parsed.expr() + } + + pub fn kind(&self) -> AnnotationKind { + self.kind + } +} + +#[derive(Copy, Clone, Debug)] +pub enum AnnotationKind { + /// The annotation is defined as part a simple string literal, + /// e.g. `x: "List[int]" = []`. Annotations within simple literals + /// can be accurately located. For example, we can underline specific + /// expressions within the annotation and apply automatic fixes, which is + /// not possible for complex string literals. + Simple, + + /// The annotation is defined as part of a complex string literal, such as + /// a literal containing an implicit concatenation or escaped characters, + /// e.g. `x: "List" "[int]" = []`. These are comparatively rare, but valid. + Complex, +} + +impl AnnotationKind { + /// Returns `true` if the annotation kind is simple. + pub const fn is_simple(self) -> bool { + matches!(self, AnnotationKind::Simple) + } +} + +/// Parses the given string expression node as a type annotation. The given `source` is the entire +/// source code. +pub fn parse_type_annotation( + string_expr: &ExprStringLiteral, + source: &str, +) -> AnnotationParseResult { + if let Some(string_literal) = string_expr.as_single_part_string() { + // Compare the raw contents (without quotes) of the expression with the parsed contents + // contained in the string literal. + if &source[string_literal.content_range()] == string_literal.as_str() { + parse_simple_type_annotation(string_literal, source) + } else { + // The raw contents of the string doesn't match the parsed content. This could be the + // case for annotations that contain escaped quotes. + parse_complex_type_annotation(string_expr) + } + } else { + // String is implicitly concatenated. + parse_complex_type_annotation(string_expr) + } +} + +fn parse_simple_type_annotation( + string_literal: &StringLiteral, + source: &str, +) -> AnnotationParseResult { + Ok(ParsedAnnotation { + parsed: parse_string_annotation(source, string_literal)?, + kind: AnnotationKind::Simple, + }) +} + +fn parse_complex_type_annotation(string_expr: &ExprStringLiteral) -> AnnotationParseResult { + let mut parsed = parse_expression(string_expr.value.to_str())?; + relocate_expr(parsed.expr_mut(), string_expr.range()); + Ok(ParsedAnnotation { + parsed, + kind: AnnotationKind::Complex, + }) +} diff --git a/vendor/xml/.cargo_vcs_info.json b/vendor/xml/.cargo_vcs_info.json new file mode 100644 index 00000000000..37cfb8fff9e --- /dev/null +++ b/vendor/xml/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "73c24eac0639a1e085269cf6c7b81b41cf1b9ad3" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/vendor/xml/.gitignore b/vendor/xml/.gitignore new file mode 100644 index 00000000000..042776aad72 --- /dev/null +++ b/vendor/xml/.gitignore @@ -0,0 +1,2 @@ +/Cargo.lock +/target/ diff --git a/vendor/xml/Cargo.toml b/vendor/xml/Cargo.toml new file mode 100644 index 00000000000..066a469e172 --- /dev/null +++ b/vendor/xml/Cargo.toml @@ -0,0 +1,63 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.73" +name = "xml" +version = "1.3.0" +authors = [ + "Vladimir Matveev ", + "Kornel (https://github.com/kornelski)", +] +build = false +include = [ + "src/**", + "LICENSE", + "README.md", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "An XML library in pure Rust" +homepage = "https://lib.rs/crates/xml" +documentation = "https://docs.rs/xml/" +readme = "README.md" +keywords = [ + "xml", + "parser", + "sax", + "event-reader", + "writer", +] +categories = ["parser-implementations"] +license = "MIT" +repository = "https://github.com/kornelski/xml-rs" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +rustdoc-args = ["--generate-link-to-definition"] + +[package.metadata.release] +tag-name = "{{version}}" +tag-message = "" + +[badges.maintenance] +status = "actively-developed" + +[lib] +name = "xml" +path = "src/lib.rs" + +# Keep the vendored dependency independently checkable. +[workspace] diff --git a/vendor/xml/Cargo.toml.orig b/vendor/xml/Cargo.toml.orig new file mode 100644 index 00000000000..0fc5cab60f4 --- /dev/null +++ b/vendor/xml/Cargo.toml.orig @@ -0,0 +1,26 @@ +[package] +name = "xml" +version = "1.3.0" +authors = ["Vladimir Matveev ", "Kornel (https://github.com/kornelski)"] +license = "MIT" +description = "An XML library in pure Rust" +repository = "https://github.com/kornelski/xml-rs" +homepage = "https://lib.rs/crates/xml" +documentation = "https://docs.rs/xml/" +readme = "README.md" +keywords = ["xml", "parser", "sax", "event-reader", "writer"] +categories = ["parser-implementations"] +edition = "2021" +rust-version = "1.73" # bump CI too +include = ["src/**", "LICENSE", "README.md"] + +[badges] +maintenance = { status = "actively-developed" } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +rustdoc-args = ["--generate-link-to-definition"] + +[package.metadata.release] +tag-name = "{{version}}" +tag-message = "" diff --git a/vendor/xml/LICENSE b/vendor/xml/LICENSE new file mode 100644 index 00000000000..6caa1d3e782 --- /dev/null +++ b/vendor/xml/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Vladimir Matveev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/xml/README.md b/vendor/xml/README.md new file mode 100644 index 00000000000..1abe6991855 --- /dev/null +++ b/vendor/xml/README.md @@ -0,0 +1,225 @@ +xml-rs, an XML library for Rust +=============================== + +[![CI](https://github.com/kornelski/xml-rs/actions/workflows/main.yml/badge.svg)](https://github.com/kornelski/xml-rs/actions/workflows/main.yml) +[![crates.io][crates-io-img]](https://lib.rs/crates/xml) +[![docs][docs-img]](https://docs.rs/xml/) + +[Documentation](https://docs.rs/xml/) + + [crates-io-img]: https://img.shields.io/crates/v/xml.svg + [docs-img]: https://img.shields.io/badge/docs-latest%20release-6495ed.svg + +xml-rs is an XML library for the [Rust](https://www.rust-lang.org/) programming language. +It supports reading and writing of XML documents in a streaming fashion (without DOM). + +### Features + +* XML spec conformance better than other pure-Rust libraries. + +* Easy to use API based on `Iterator`s and regular `String`s without tricky lifetimes. + +* Support for UTF-16, UTF-8, ISO-8859-1, and ASCII encodings. + +* Written entirely in the safe Rust subset. Designed to safely handle untrusted input. + + +The API is heavily inspired by Java Streaming API for XML ([StAX][stax]). It contains a pull parser much like StAX event reader. It provides an iterator API, so you can leverage Rust's existing iterators library features. + + [stax]: https://en.wikipedia.org/wiki/StAX + +It also provides a streaming document writer much like StAX event writer. +This writer consumes its own set of events, but reader events can be converted to +writer events easily, and so it is possible to write XML transformation chains in a pretty +clean manner. + +This parser is mostly full-featured, however, there are limitations: +* Legacy code pages and non-Unicode encodings are not supported; +* DTD validation is not supported (but entities defined in the internal subset are supported); +* attribute value normalization is not performed, and end-of-line characters are not normalized either. + +Other than that the parser tries to be mostly XML-1.1-compliant. + +Writer is also mostly full-featured with the following limitations: +* no support for encodings other than UTF-8, +* no support for emitting `` declarations; +* more validations of input are needed, for example, checking that namespace prefixes are bounded + or comments are well-formed. + +Building and using +------------------ + +xml-rs uses [Cargo](https://crates.io), so add it with `cargo add xml` or modify `Cargo.toml`: + +```toml +[dependencies] +xml = "1.0" +``` + +The package exposes a single crate called `xml`. + +Reading XML documents +--------------------- + +[`xml::reader::EventReader`][EventReader] requires a [`Read`][stdread] instance to read from. It can be a `File` wrapped in `BufReader`, or a `Vec`, or a `&[u8]` slice. + +[EventReader]: https://docs.rs/xml/latest/xml/reader/struct.EventReader.html +[stdread]: https://doc.rust-lang.org/stable/std/io/trait.Read.html + +`EventReader` implements `IntoIterator` trait, so you can use it in a `for` loop directly: + +```rust,no_run +use std::fs::File; +use std::io::BufReader; + +use xml::reader::{EventReader, XmlEvent}; + +fn main() -> std::io::Result<()> { + let file = File::open("file.xml")?; + let file = BufReader::new(file); // Buffering is important for performance + + let parser = EventReader::new(file); + let mut depth = 0; + for e in parser { + match e { + Ok(XmlEvent::StartElement { name, .. }) => { + println!("{:spaces$}+{name}", "", spaces = depth * 2); + depth += 1; + } + Ok(XmlEvent::EndElement { name }) => { + depth -= 1; + println!("{:spaces$}-{name}", "", spaces = depth * 2); + } + Err(e) => { + eprintln!("Error: {e}"); + break; + } + // There's more: https://docs.rs/xml/latest/xml/reader/enum.XmlEvent.html + _ => {} + } + } + + Ok(()) +} +``` + +Document parsing can end normally or with an error. Regardless of exact cause, the parsing +process will be stopped, and the iterator will terminate normally. + +You can also have finer control over when to pull the next event from the parser using its own +`next()` method: + +```rust,ignore +match parser.next() { + ... +} +``` + +Upon the end of the document or an error, the parser will remember the last event and will always +return it in the result of `next()` call afterwards. If iterator is used, then it will yield +error or end-of-document event once and will produce `None` afterwards. + +It is also possible to tweak parsing process a little using [`xml::reader::ParserConfig`][ParserConfig] structure. +See its documentation for more information and examples. + +[ParserConfig]: https://docs.rs/xml/latest/xml/reader/struct.ParserConfig.html + +You can find a more extensive example of using `EventReader` in `src/analyze.rs`, which is a +small program (BTW, it is built with `cargo build` and can be run after that) which shows various +statistics about specified XML document. It can also be used to check for well-formedness of +XML documents - if a document is not well-formed, this program will exit with an error. + + +## Parsing untrusted inputs + +The parser is written in safe Rust subset, so by Rust's guarantees the worst that it can do is to cause a panic. +You can use `ParserConfig` to set limits on maximum lenghts of names, attributes, text, entities, etc. +You should also set a maximum document size via `io::Read`'s [`take(max)`](https://doc.rust-lang.org/stable/std/io/trait.Read.html#method.take) method. + +Writing XML documents +--------------------- + +xml-rs also provides a streaming writer much like StAX event writer. With it you can write an +XML document to any `Write` implementor. + +```rust,no_run +use std::io; +use xml::writer::{EmitterConfig, XmlEvent}; + +/// A simple demo syntax where "+foo" makes ``, "-foo" makes `` +fn make_event_from_line(line: &str) -> XmlEvent { + let line = line.trim(); + if let Some(name) = line.strip_prefix("+") { + XmlEvent::start_element(name).into() + } else if line.starts_with("-") { + XmlEvent::end_element().into() + } else { + XmlEvent::characters(line).into() + } +} + +fn main() -> io::Result<()> { + let input = io::stdin(); + let output = io::stdout(); + let mut writer = EmitterConfig::new() + .perform_indent(true) + .create_writer(output); + + let mut line = String::new(); + loop { + line.clear(); + let bytes_read = input.read_line(&mut line)?; + if bytes_read == 0 { + break; // EOF + } + + let event = make_event_from_line(&line); + if let Err(e) = writer.write(event) { + panic!("Write error: {e}") + } + } + Ok(()) +} +``` + +The code example above also demonstrates how to create a writer out of its configuration. +Similar thing also works with `EventReader`. + +The library provides an XML event building DSL which helps to construct complex events, +e.g. ones having namespace definitions. Some examples: + +```rust,ignore +// +XmlEvent::start_element("a:hello").attr("a:param", "value").ns("a", "urn:some:document") + +// +XmlEvent::start_element("hello").attr("b:config", "value").default_ns("urn:defaul:uri") + +// +XmlEvent::cdata("some unescaped text") +``` + +Of course, one can create `XmlEvent` enum variants directly instead of using the builder DSL. +There are more examples in [`xml::writer::XmlEvent`][XmlEvent] documentation. + +[XmlEvent]: https://docs.rs/xml/latest/xml/reader/enum.XmlEvent.html + +The writer has multiple configuration options; see `EmitterConfig` documentation for more +information. + +[EmitterConfig]: https://docs.rs/xml/latest/xml/writer/struct.EmitterConfig.html + +Bug reports +------------ + +Please report issues at: . + +Before reporting issues with XML conformance, please find the relevant section in the XML spec first. + +## [Upgrading from 0.8 to 1.0](https://github.com/kornelski/xml-rs/blob/main/Changelog.md) + +It should be pretty painless: + +* Change `xml-rs = "0.8"` to `xml = "1.0"` in `Cargo.toml` +* Add `_ => {}` to `match` statements where the compiler complains. A new `Doctype` event has been added, and error enums are non-exhaustive. +* If you were creating `ParserConfig` using a struct literal, please use `ParserConfig::new()` and the setters. diff --git a/vendor/xml/RUSTPYTHON-PROVENANCE.md b/vendor/xml/RUSTPYTHON-PROVENANCE.md new file mode 100644 index 00000000000..43cd4ad9fae --- /dev/null +++ b/vendor/xml/RUSTPYTHON-PROVENANCE.md @@ -0,0 +1,77 @@ +# xml 1.3.0 local accounting changes + +This directory contains the official `xml` 1.3.0 crate from crates.io, with its +MIT license preserved in `LICENSE`. Upstream repository: +https://github.com/kornelski/xml-rs + +- Upstream commit recorded by the package: `73c24eac0639a1e085269cf6c7b81b41cf1b9ad3` +- Published archive: https://static.crates.io/crates/xml/xml-1.3.0.crate +- Archive SHA-256: `636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89` +- Included upstream files: `src/**`, `Cargo.toml`, `Cargo.toml.orig`, `README.md`, + `LICENSE`, and `.cargo_vcs_info.json` +- Original-file hashes: `UPSTREAM-SHA256SUMS` +- Exact source/manifest changes: `patches/amplification.patch`, applicable with + `patch -p1` from a fresh unpacking of the published archive + +The root workspace dependency uses this directory with a version requirement, +so the accounting changes also apply when RustPython is a path or Git dependency +of a separate workspace. Its lockfile keeps version 1.3.0 without a registry +source/checksum. The local Cargo manifest has an empty workspace table so this +dependency can be checked separately, without building RustPython. + +Cargo replaces path dependencies with their version requirements when publishing +a package to a registry. Before publishing RustPython crates, use a published +dependency that includes these accounting APIs and semantics; the declared `1.3` +registry requirement alone does not provide these local changes. + +## Supported accounting + +`ParserConfig::amplification_limits` installs immutable cumulative byte limits. +Direct bytes are measured at the decoder, including UTF-16 widths and the BOM. +Reparsed entity characters, parameter-entity substitutions, attribute entities, +and configured extra entities contribute indirect bytes. Numeric character +references do not contribute an additional replacement byte; the five named +predefined entities do, with the deferred checks used by Expat. Arithmetic +overflow is fatal even when the factor is infinity. + +Attribute expansion and namespace installation wait for the complete opening +tag, then charge replacements before appending them. Attribute whitespace is +normalized with the distinction between physical newlines and character +references preserved. Existing depth and queue limits remain independent. Queue +boundaries now count actual nesting, rather than queued siblings, and the queue +cap is checked before reserving replacement storage. + +RustPython enables a factor of 100 and activation threshold of 8,388,608 bytes +by default. Its two configuration methods take effect before the first Parse or +ParseFile call. Otherwise-valid later changes raise NotImplementedError, +including changes from handlers and after successful completion. Expat permits +live changes; supporting them requires callback synchronization and a replay +frontier for RustPython's synchronous fallback. External child parser Parse is +still explicitly unsupported, so shared accounting across external entities is +not claimed. + +This is genuine cumulative expansion protection, not complete Expat tokenizer +parity. xml-rs may group character data and incomplete feeds differently, and +error positions remain xml-rs positions. In particular, exact timing at every +possible text-token/feed boundary is not promised. The fixed limits can reject +documents Expat accepts. No allocation-tracker API is supplied by these changes. + +## Local checks + +Run `cargo test --manifest-path vendor/xml/Cargo.toml --offline` from the repository +root. This runs the original crate tests and small accounting regressions, with +no network, external entities, or large generated documents. + +For the current Rust toolchain, unchanged upstream code has three clippy lints: +`new_without_default`, `unbuffered_bytes`, and `enum_variant_names`. The local +changes pass with those existing lints permitted on the command line: + +``` +cargo clippy --manifest-path vendor/xml/Cargo.toml --offline --all-targets -- \ + -D warnings -A clippy::new_without_default -A clippy::unbuffered_bytes \ + -A clippy::enum_variant_names +``` + +Canonical Python tests are unchanged by this patch. The integration owner must +build RustPython and run the pinned pyexpat and XML consumer tests before removing +any stale expected-failure markers or claiming interpreter validation. diff --git a/vendor/xml/UPSTREAM-SHA256SUMS b/vendor/xml/UPSTREAM-SHA256SUMS new file mode 100644 index 00000000000..0fb55448e54 --- /dev/null +++ b/vendor/xml/UPSTREAM-SHA256SUMS @@ -0,0 +1,32 @@ +1c411c711834944f9a47c554489baebe13133a222dc09d1348c625afd1444612 src/attribute.rs +c5ca8f613723fc70d96ce1de0a8a7319bc3c7b20cc2496a1ad31410b9c75a7fd src/common.rs +1d782f3d5a28ea0f8350e650d0eae91f34b94e6cd32ab4dc9801af6944c850e6 src/escape.rs +9f68b90c472b7bc35c74d8b22425669b434f12bdd86bf39442cf8b2624907b9d src/lib.rs +f4415e64c41310335a79cba31db6aa3efc65f08a766b07b031e342c7b0b5b454 src/macros.rs +172b5a683cc73993d990607cf9006f60c3115d7d66552d81cdebf8232f339856 src/name.rs +ec97fbaab916da287fd2cba7bf8163cebff1e03bcc032987f1c4f1cfd4e633ea src/namespace.rs +b164e74492491e3daa00911a05acb5d953c04a34db2a38502d1793c28f82446d src/reader/config.rs +608c4a248af3e12c5855c4ae494f7e034db5483a0f7c9ac148a3f45862690a5c src/reader/error.rs +50527390297f3c509d833d1870630988585f13ba0e46ac28ab5621daad20c800 src/reader/events.rs +c28535189c020ff0cd5a536e631fd69c73c816de51f017ee260a3f5bd6cdb027 src/reader/lexer.rs +a22a4f64458fcdd2cd4cf08dea917c909e097db5d854b9a67c32dbfa05a06d3d src/reader/parser/inside_cdata.rs +66b817f6aa22a9b2b75d3bd12abc3c11796b9d1b97a573ca9ca501121aaf5d45 src/reader/parser/inside_closing_tag_name.rs +330d34a371f49232a9a70ea7b14fb4deade4cb195fcd1f00922279aae51149f3 src/reader/parser/inside_comment.rs +a4492563959de42039a3b068f5cef53578f47bb2f349474bd3fe8800561f6f07 src/reader/parser/inside_declaration.rs +acee6c6e6dcb13f64c429f1d6b3e3c316eef865359abaf2b5dc660a989553458 src/reader/parser/inside_doctype.rs +cd6f24c8fc7f955acffbf01c2ad248a92b14fe7679d8d8963ff2ee155e3a53d1 src/reader/parser/inside_opening_tag.rs +93b8ec0b7bd6ecb465739ed1a98ef83b72b55f1f1ad98a5d1a0777971889d1e5 src/reader/parser/inside_processing_instruction.rs +3fbbef6ea3b42b6e51128b6780f962c60e71db64e07f5d69c2104c20e4b2baae src/reader/parser/inside_reference.rs +712274357fe8d8c87aea69f18f977d62efad3582cdce35838637b3e21403de68 src/reader/parser/outside_tag.rs +9910cc1c25e3f9e09d0dee288f75f00ec007cd396c4e27e05b48f9ea37f5451d src/reader/parser.rs +15368cad7d99ceaaf6891b925a93124d4afa0ec56c66464f3bf46b0dd354eaec src/reader.rs +a5038dc218140904fa17f00a8b35fe72ce0c8291fa2c26063f9d438951687935 src/util.rs +edcd7a68c33454bb7dfcd176bbc860bbb03b5125a45db0188d6d6d4db1c8f6bd src/writer/config.rs +38ea6aea4dde553d4c6e38ee22bbfc81ce0a9a891eb31cec61220f1567ce4ef1 src/writer/emitter.rs +3d5d90718c6d053bcf4361089484e4f0d7b95015ffc5dd680980c4fc3dd10838 src/writer/events.rs +109d21b0293cf5774cb4ede18fcb4ebaaa8c8bda4bc55d68925d3ac8048dd138 src/writer.rs +ef69a33efd4f78740cf73b71dd0477625009f37e82cf443d4daa49ef9811bd4f Cargo.toml +1f9d2ff97d08fc3cd3857abd6ff2bf46a25dce35b8fd439eefd52c5312523ec0 Cargo.toml.orig +0dc18d924dc0a5f41172a393012843a5eaaef338e795b3645da9cc3b6068220b LICENSE +c6e0827d94b4e9811ccdc300d1fd0163876813e9861d9e720637a13a889668e0 README.md +c915e6fc7e866c0a1b39be56d28a0f5377687ddc4e9562f89281e359c8a1b52a .cargo_vcs_info.json diff --git a/vendor/xml/patches/amplification.patch b/vendor/xml/patches/amplification.patch new file mode 100644 index 00000000000..6f228e97f30 --- /dev/null +++ b/vendor/xml/patches/amplification.patch @@ -0,0 +1,1071 @@ +diff --git a/Cargo.toml b/Cargo.toml +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -58,3 +58,6 @@ + [lib] + name = "xml" + path = "src/lib.rs" ++ ++# Keep the vendored dependency independently checkable. ++[workspace] +diff --git a/src/reader/amplification.rs b/src/reader/amplification.rs +new file mode 100644 +--- /dev/null ++++ b/src/reader/amplification.rs +@@ -0,0 +1,106 @@ ++//! Cumulative entity accounting, independent of the fixed depth and queue limits. ++ ++/// Immutable limits for cumulative entity expansion. The factor uses the same ++/// single-precision ratio as Expat: `(direct + indirect) / direct`. ++#[derive(Debug, Clone, Copy, PartialEq, Eq)] ++pub struct AmplificationLimits { ++ maximum_factor_bits: u32, ++ activation_threshold: u64, ++} ++ ++impl AmplificationLimits { ++ /// Rejects NaN and factors below one. Positive infinity is permitted. ++ #[must_use] ++ pub fn new(maximum_factor: f32, activation_threshold: u64) -> Option { ++ if maximum_factor.is_nan() || maximum_factor < 1.0 { ++ return None; ++ } ++ Some(Self { ++ maximum_factor_bits: maximum_factor.to_bits(), ++ activation_threshold, ++ }) ++ } ++} ++ ++#[derive(Clone)] ++pub(crate) struct Accounting { ++ limits: Option, ++ pub(crate) direct: u64, ++ indirect: u64, ++ overflow: bool, ++} ++ ++impl Accounting { ++ pub(crate) fn new(limits: Option) -> Self { ++ Self { ++ limits, ++ direct: 0, ++ indirect: 0, ++ overflow: false, ++ } ++ } ++ ++ pub(crate) fn add_direct(&mut self, bytes: usize) { ++ self.direct = self.direct.checked_add(bytes as u64).unwrap_or_else(|| { ++ self.overflow = true; ++ u64::MAX ++ }); ++ } ++ ++ pub(crate) fn add_indirect(&mut self, bytes: usize) { ++ self.indirect = self.indirect.checked_add(bytes as u64).unwrap_or_else(|| { ++ self.overflow = true; ++ u64::MAX ++ }); ++ } ++ ++ pub(crate) fn tolerated(&self) -> bool { ++ self.tolerated_at_direct(self.direct) ++ } ++ ++ pub(crate) fn tolerated_at_direct(&self, direct: u64) -> bool { ++ let Some(limits) = self.limits else { ++ return true; ++ }; ++ let Some(total) = direct.checked_add(self.indirect) else { ++ return false; ++ }; ++ if self.overflow { ++ return false; ++ } ++ if total < limits.activation_threshold { ++ return true; ++ } ++ let factor = if direct != 0 { ++ total as f32 / direct as f32 ++ } else { ++ // Expat uses the shortest possible external entity declaration. ++ (22.0 + self.indirect as f32) / 22.0 ++ }; ++ factor <= f32::from_bits(limits.maximum_factor_bits) ++ } ++} ++ ++#[cfg(test)] ++mod tests { ++ use super::*; ++ ++ #[test] ++ fn threshold_factor_and_overflow() { ++ assert!(AmplificationLimits::new(f32::NAN, 0).is_none()); ++ assert!(AmplificationLimits::new(0.99, 0).is_none()); ++ let mut a = Accounting::new(AmplificationLimits::new(1.0, 10)); ++ a.add_direct(5); ++ a.add_indirect(4); ++ assert!(a.tolerated()); ++ a.add_indirect(1); ++ assert!(!a.tolerated()); ++ a.limits = AmplificationLimits::new(f32::INFINITY, 0); ++ assert!(a.tolerated()); ++ a.direct = u64::MAX; ++ assert!(!a.tolerated()); ++ a.indirect = 0; ++ a.add_direct(1); ++ assert!(!a.tolerated()); ++ } ++} +diff --git a/src/reader/amplification_tests.rs b/src/reader/amplification_tests.rs +new file mode 100644 +--- /dev/null ++++ b/src/reader/amplification_tests.rs +@@ -0,0 +1,129 @@ ++use super::{AmplificationLimits, ErrorKind, ParserConfig, XmlEvent}; ++ ++fn parse(source: &[u8], threshold: u64, factor: f32) -> (Option, String, Vec) { ++ let mut config = ParserConfig::new().coalesce_characters(false); ++ config.amplification_limits = AmplificationLimits::new(factor, threshold); ++ let mut reader = config.create_reader(source); ++ let mut text = String::new(); ++ let mut attributes = Vec::new(); ++ loop { ++ match reader.next() { ++ Ok(XmlEvent::Characters(s) | XmlEvent::Whitespace(s)) => text.push_str(&s), ++ Ok(XmlEvent::StartElement { ++ attributes: attrs, .. ++ }) => { ++ attributes.extend(attrs.into_iter().map(|a| a.value)); ++ } ++ Ok(XmlEvent::EndDocument) => return (None, text, attributes), ++ Ok(_) => {} ++ Err(error) => { ++ assert_eq!(reader.next().unwrap_err(), error); ++ return (Some(error.kind().clone()), text, attributes); ++ } ++ } ++ } ++} ++ ++#[test] ++fn attribute_frontier_and_predefined_deferral() { ++ let input = br#"]>"#; ++ assert_eq!(input.len(), 44); ++ let (error, _, attributes) = parse(input, 0, 1.07); ++ assert!(error.is_none()); ++ assert_eq!(attributes, ["xyz"]); ++ assert!(matches!( ++ parse(input, 0, 1.0).0, ++ Some(ErrorKind::AmplificationLimit) ++ )); ++ assert!(parse(br#""#, 0, 1.0).0.is_none()); ++ let (error, text, _) = parse(b"&\nxxxxxxxxxxxxxxxxxxxx", 0, 1.1); ++ assert!(matches!(error, Some(ErrorKind::AmplificationLimit))); ++ assert_eq!(text, "&"); ++} ++ ++#[test] ++fn numeric_references_and_attribute_normalization() { ++ let (error, text, _) = parse(b"A", 0, 1.0); ++ assert!(error.is_none()); ++ assert_eq!(text, "A"); ++ let (_, _, attrs) = parse( ++ br#"]>"#, ++ 0, ++ 100.0, ++ ); ++ assert_eq!(attrs, [" "]); ++ let (_, _, attrs) = parse( ++ br#"]>"#, ++ 0, ++ 100.0, ++ ); ++ assert_eq!(attrs, [" "]); ++ let (_, _, attrs) = parse( ++ b"]>", ++ 0, ++ 100.0, ++ ); ++ assert_eq!(attrs, [" "]); ++ for input in [ ++ br#"]>"#.as_slice(), ++ br#"]>"#.as_slice(), ++ ] { ++ assert!(matches!( ++ parse(input, 0, 100.0).0, ++ Some(ErrorKind::Syntax(_)) ++ )); ++ } ++} ++ ++#[test] ++fn cdata_start_checks_deferred_predefined_bytes() { ++ let input = b"&"; ++ let (error, text, _) = parse(input, 0, 1.05); ++ assert!(matches!(error, Some(ErrorKind::AmplificationLimit))); ++ assert_eq!(text, "&"); ++ let mut config = ParserConfig::new().cdata_to_characters(true); ++ config.amplification_limits = AmplificationLimits::new(1.05, 0); ++ let result = config ++ .create_reader(input.as_slice()) ++ .into_iter() ++ .collect::, _>>(); ++ assert!(matches!( ++ result.unwrap_err().kind(), ++ ErrorKind::AmplificationLimit ++ )); ++} ++ ++#[test] ++fn encoded_input_uses_raw_bytes() { ++ let source = "]>&b;"; ++ assert!(matches!( ++ parse(source.as_bytes(), 0, 1.1).0, ++ Some(ErrorKind::AmplificationLimit) ++ )); ++ let utf16: Vec = [0xff, 0xfe] ++ .into_iter() ++ .chain(source.encode_utf16().flat_map(u16::to_le_bytes)) ++ .collect(); ++ let (error, text, _) = parse(&utf16, 0, 1.1); ++ assert!(error.is_none()); ++ assert_eq!(text, "xyz"); ++} ++ ++#[test] ++fn fixed_caps_remain_independent_and_measure_nesting() { ++ let sibling = "]>".to_owned() + &"&a;".repeat(20) + ""; ++ assert_eq!(parse(sibling.as_bytes(), 10000, 1.0).1, "x".repeat(20)); ++ for depth in [4, 11, 20] { ++ let mut source = "".to_owned(); ++ for n in 1..=depth { ++ source += &format!("", n - 1); ++ } ++ source += &format!("]>&a{depth};"); ++ let (error, _, _) = parse(source.as_bytes(), 10000, f32::INFINITY); ++ if depth == 4 { ++ assert!(error.is_none()); ++ } else { ++ assert!(matches!(error, Some(ErrorKind::Syntax(_)))); ++ } ++ } ++} +diff --git a/src/reader/config.rs b/src/reader/config.rs +--- a/src/reader/config.rs ++++ b/src/reader/config.rs +@@ -110,6 +110,9 @@ + + /// Entities can expand into other entities this many times (be careful about exponential cost!) + pub max_entity_expansion_depth: u8, ++ ++ /// Optional cumulative entity byte limits, independent of the fixed caps. ++ pub amplification_limits: Option, + + /// Maximum length of tag name or attribute name + pub max_name_length: usize, +@@ -156,6 +159,7 @@ + allow_multiple_root_elements: true, + max_entity_expansion_length: DEFAULT_MAX_ENTITY_EXPANSION_LENGTH, + max_entity_expansion_depth: DEFAULT_MAX_ENTITY_EXPANSION_DEPTH, ++ amplification_limits: None, + max_attributes: 1 << 16, + max_attribute_length: 1 << 30, + max_data_length: 1 << 30, +diff --git a/src/reader/error.rs b/src/reader/error.rs +--- a/src/reader/error.rs ++++ b/src/reader/error.rs +@@ -21,6 +21,8 @@ + Utf8(str::Utf8Error), + /// The document ended while they were elements/comments/etc. still open + UnexpectedEof, ++ /// Cumulative entity expansion exceeded its configured byte ratio. ++ AmplificationLimit, + /// [Writer error](crate::writer::Error) for convenience of using a single [`Error`] type + EmitterError(Box), + } +@@ -150,7 +152,7 @@ + + impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +- use self::ErrorKind::{EmitterError, Io, Syntax, UnexpectedEof, Utf8}; ++ use self::ErrorKind::{AmplificationLimit, EmitterError, Io, Syntax, UnexpectedEof, Utf8}; + + write!(f, "{} ", self.pos)?; + match &self.kind { +@@ -158,6 +160,7 @@ + Utf8(reason) => reason.fmt(f), + Syntax(msg) => f.write_str(msg), + UnexpectedEof => f.write_str("Unexpected EOF"), ++ AmplificationLimit => f.write_str("limit on input amplification factor (from DTD and entities) breached"), + EmitterError(e) => e.fmt(f), + } + } +@@ -283,9 +286,10 @@ + impl Clone for ErrorKind { + #[cold] + fn clone(&self) -> Self { +- use self::ErrorKind::{EmitterError, Io, Syntax, UnexpectedEof, Utf8}; ++ use self::ErrorKind::{AmplificationLimit, EmitterError, Io, Syntax, UnexpectedEof, Utf8}; + match self { + UnexpectedEof => UnexpectedEof, ++ AmplificationLimit => AmplificationLimit, + Utf8(reason) => Utf8(*reason), + Io(io_error) => Io(io::Error::new(io_error.kind(), io_error.to_string())), + Syntax(msg) => Syntax(msg.clone()), +@@ -300,6 +304,7 @@ + use self::ErrorKind::{Io, Syntax, UnexpectedEof, Utf8}; + match (self, other) { + (UnexpectedEof, UnexpectedEof) => true, ++ (Self::AmplificationLimit, Self::AmplificationLimit) => true, + (Utf8(left), Utf8(right)) => left == right, + (Io(left), Io(right)) => + left.kind() == right.kind() && +diff --git a/src/reader/lexer.rs b/src/reader/lexer.rs +--- a/src/reader/lexer.rs ++++ b/src/reader/lexer.rs +@@ -224,12 +224,16 @@ + reader: CharReader, + pos: TextPosition, + head_pos: TextPosition, +- char_queue: VecDeque, ++ char_queue: VecDeque<(Option, bool, usize)>, ++ accounting: super::amplification::Accounting, ++ token_direct_start: u64, ++ unread_direct_bytes: u64, ++ current_is_direct: bool, + /// Default state to go back to after a tag end (may be `InsideDoctype`) + normal_state: State, + inside_token: bool, + eof_handled: bool, +- reparse_depth: u8, ++ reparse_depth: u16, + #[cfg(test)] + skip_errors: bool, + +@@ -250,6 +254,10 @@ + reader: CharReader::new(Encoding::Unknown), + pos: TextPosition::new(), + head_pos: TextPosition::new(), ++ accounting: super::amplification::Accounting::new(config.amplification_limits), ++ token_direct_start: 0, ++ unread_direct_bytes: 0, ++ current_is_direct: true, + char_queue: VecDeque::with_capacity(4), // TODO: check size + st: State::Normal, + normal_state: State::Normal, +@@ -262,6 +270,22 @@ + max_entity_expansion_depth: config.max_entity_expansion_depth, + max_entity_expansion_length: config.max_entity_expansion_length, + } ++ } ++ ++ pub(crate) fn account_indirect(&mut self, bytes: usize, enforce: bool) -> Result<()> { ++ self.accounting.add_indirect(bytes); ++ if enforce { self.check_amplification()?; } ++ Ok(()) ++ } ++ ++ pub(crate) fn check_amplification_before_token(&self) -> Result<()> { ++ if self.accounting.tolerated_at_direct(self.token_direct_start) { return Ok(()); } ++ Err(Error { pos: self.position(), kind: super::error::ErrorKind::AmplificationLimit }) ++ } ++ ++ pub(crate) fn check_amplification(&self) -> Result<()> { ++ if self.accounting.tolerated() { return Ok(()); } ++ Err(Error { pos: self.position(), kind: super::error::ErrorKind::AmplificationLimit }) + } + + pub(crate) fn encoding(&self) -> Encoding { +@@ -298,12 +322,20 @@ + } + + if !self.inside_token { ++ self.token_direct_start = self.accounting.direct.saturating_sub(self.unread_direct_bytes); + self.pos = self.head_pos; + self.inside_token = true; + } + + // Check if we have saved a char or two for ourselves +- while let Some(c) = self.char_queue.pop_front() { ++ while let Some((c, expanded, direct_bytes)) = self.char_queue.pop_front() { ++ let Some(c) = c else { ++ self.reparse_depth -= 1; ++ continue; ++ }; ++ self.unread_direct_bytes -= direct_bytes as u64; ++ self.current_is_direct = direct_bytes != 0; ++ if expanded { self.account_indirect(c.len_utf8(), true)?; } + if let Some(t) = self.dispatch_char(c)? { + self.inside_token = false; + return Ok(t); +@@ -311,7 +343,12 @@ + } + // if char_queue is empty, all circular reparsing is done + self.reparse_depth = 0; +- while let Some(c) = self.reader.next_char_from(b)? { ++ loop { ++ let mut counted = CountingRead { source: b, bytes: 0 }; ++ let next = self.reader.next_char_from(&mut counted); ++ self.accounting.add_direct(counted.bytes); ++ let Some(c) = next? else { break; }; ++ self.current_is_direct = true; + if c == '\n' { + self.head_pos.new_line(); + } else { +@@ -396,7 +433,15 @@ + + fn move_to_with_unread(&mut self, st: State, cs: &[char], token: Token) -> Token { + for c in cs.iter().rev().copied() { +- self.char_queue.push_front(c); ++ let bytes = if self.current_is_direct { ++ match self.reader.encoding { ++ Encoding::Utf16Be | Encoding::Utf16Le | Encoding::Utf16 => c.len_utf16() * 2, ++ Encoding::Latin1 | Encoding::Ascii => 1, ++ _ => c.len_utf8(), ++ } ++ } else { 0 }; ++ self.unread_direct_bytes += bytes as u64; ++ self.char_queue.push_front((Some(c), false, bytes)); + } + self.move_to_with(st, token) + } +@@ -407,14 +452,23 @@ + } + + self.reparse_depth += 1; +- if self.reparse_depth > self.max_entity_expansion_depth || self.char_queue.len() > self.max_entity_expansion_length { ++ if self.reparse_depth > u16::from(self.max_entity_expansion_depth) || self.char_queue.len() > self.max_entity_expansion_length { + return Err(self.error(SyntaxError::EntityTooBig)); + } + ++ // Check the pending queue before allocating it. This is a separate ++ // fixed cap, not a cumulative amplification failure. ++ let characters = markup.chars().count(); ++ if characters > self.max_entity_expansion_length.saturating_sub(self.char_queue.len()) { ++ return Err(self.error(SyntaxError::EntityTooBig)); ++ } + self.reset_eof_handled(); +- self.char_queue.reserve(markup.len()); ++ self.char_queue.reserve(characters + 1); ++ // A boundary records when an expansion actually finishes; queued ++ // sibling references do not increase the nesting depth. ++ self.char_queue.push_front((None, false, 0)); + for c in markup.chars().rev() { +- self.char_queue.push_front(c); ++ self.char_queue.push_front((Some(c), true, 0)); + } + + Ok(()) +@@ -428,8 +482,8 @@ + if self.skip_errors { + let mut chars = chunk.chars(); + let first = chars.next().unwrap_or('\0'); +- self.char_queue.extend(chars); +- self.char_queue.push_back(c); ++ self.char_queue.extend(chars.map(|c| (Some(c), false, 0))); ++ self.char_queue.push_back((Some(c), false, 0)); + return Ok(Some(self.move_to_with(State::Normal, Token::Character(first)))); + } + Err(self.error(SyntaxError::UnexpectedTokenBefore(chunk, c))) +@@ -633,6 +687,16 @@ + } + } + ++// Count at the decoder boundary, not at a buffered source that can read ahead. ++struct CountingRead<'a, R> { source: &'a mut R, bytes: usize } ++impl Read for CountingRead<'_, R> { ++ fn read(&mut self, buffer: &mut [u8]) -> std::io::Result { ++ let n = self.source.read(buffer)?; ++ self.bytes += n; ++ Ok(n) ++ } ++} ++ + #[cfg(test)] + mod tests { + use crate::common::Position; +@@ -1145,3 +1209,4 @@ + assert_none!(for lex and buf); + } + } ++ +diff --git a/src/reader/parser/attribute_expansion.rs b/src/reader/parser/attribute_expansion.rs +new file mode 100644 +--- /dev/null ++++ b/src/reader/parser/attribute_expansion.rs +@@ -0,0 +1,158 @@ ++use super::PullParser; ++use crate::common::Position; ++use crate::reader::error::SyntaxError; ++use crate::reader::{Error, Result}; ++ ++impl PullParser { ++ pub(super) fn expand_attribute(&mut self, raw: &str) -> Result { ++ let mut value = String::new(); ++ Self::expand_attribute_part( ++ raw, ++ 0, ++ &self.entities, ++ &self.config, ++ self.data.version, ++ &mut self.lexer, ++ &mut value, ++ )?; ++ Ok(value) ++ } ++ ++ fn expand_attribute_part( ++ raw: &str, ++ depth: usize, ++ entities: &std::collections::HashMap, ++ config: &crate::reader::ParserConfig, ++ version: Option, ++ lexer: &mut crate::reader::lexer::Lexer, ++ value: &mut String, ++ ) -> Result<()> { ++ let error = |e: SyntaxError| Error::syntax(e.to_cow(), lexer.position()); ++ if depth > usize::from(config.max_entity_expansion_depth) { ++ return Err(error(SyntaxError::EntityTooBig)); ++ } ++ let mut rest = raw; ++ while !rest.is_empty() { ++ let split = rest.split_once('&'); ++ let (literal, tail) = split.unwrap_or((rest, "")); ++ if literal.contains('<') { ++ return Err(Error::syntax( ++ SyntaxError::UnexpectedOpeningTag.to_cow(), ++ lexer.position(), ++ )); ++ } ++ if value.len().saturating_add(literal.len()) > config.max_attribute_length { ++ return Err(Error::syntax( ++ SyntaxError::ExceededConfiguredLimit.to_cow(), ++ lexer.position(), ++ )); ++ } ++ // XML attribute whitespace normalization also applies to the ++ // replacement text of general entities, but not to numeric ++ // references written directly in the attribute. ++ let mut chars = literal.chars().peekable(); ++ while let Some(c) = chars.next() { ++ value.push(if matches!(c, '\t' | '\n' | '\r') { ++ ' ' ++ } else { ++ c ++ }); ++ if depth == 0 && c == '\r' && chars.peek() == Some(&'\n') { ++ chars.next(); ++ } ++ } ++ if split.is_none() { ++ break; ++ } ++ let Some((name, after)) = tail.split_once(';') else { ++ return Err(Error::syntax( ++ SyntaxError::EmptyEntity.to_cow(), ++ lexer.position(), ++ )); ++ }; ++ rest = after; ++ let predefined = match name { ++ "lt" => Some('<'), ++ "gt" => Some('>'), ++ "amp" => Some('&'), ++ "apos" => Some('\''), ++ "quot" => Some('"'), ++ _ => None, ++ }; ++ if let Some(c) = predefined { ++ lexer.account_indirect(c.len_utf8(), false)?; ++ value.push(c); ++ } else if let Some(number) = name.strip_prefix('#') { ++ let n = if let Some(hex) = number.strip_prefix('x') { ++ u32::from_str_radix(hex, 16) ++ } else { ++ number.parse::() ++ }; ++ let n = n.map_err(|_| { ++ Error::syntax( ++ SyntaxError::InvalidNumericEntity(number.into()).to_cow(), ++ lexer.position(), ++ ) ++ })?; ++ let valid = char::from_u32(n).filter(|&c| { ++ if version == Some(crate::common::XmlVersion::Version11) { ++ crate::common::is_xml11_char(c) ++ } else { ++ crate::common::is_xml10_char(c) ++ } ++ }); ++ let c = if let Some(c) = valid { ++ c ++ } else if config.replace_unknown_entity_references { ++ '\u{fffd}' ++ } else { ++ return Err(Error::syntax( ++ SyntaxError::InvalidCharacterEntity(n).to_cow(), ++ lexer.position(), ++ )); ++ }; ++ value.push(c); ++ } else if let Some(entity) = config.extra_entities.get(name) { ++ lexer.account_indirect(entity.len(), true)?; ++ if value.len().saturating_add(entity.len()) > config.max_attribute_length { ++ return Err(Error::syntax( ++ SyntaxError::ExceededConfiguredLimit.to_cow(), ++ lexer.position(), ++ )); ++ } ++ value.push_str(entity); ++ } else if let Some(entity) = entities.get(name) { ++ // Charge each replacement before recursively materializing it. ++ // The spelling of nested references is counted again when used. ++ lexer.account_indirect(entity.len(), true)?; ++ if entity.len() > config.max_entity_expansion_length { ++ return Err(Error::syntax( ++ SyntaxError::EntityTooBig.to_cow(), ++ lexer.position(), ++ )); ++ } ++ Self::expand_attribute_part( ++ entity, ++ depth + 1, ++ entities, ++ config, ++ version, ++ lexer, ++ value, ++ )?; ++ } else { ++ return Err(Error::syntax( ++ SyntaxError::UnexpectedEntity(name.into()).to_cow(), ++ lexer.position(), ++ )); ++ } ++ if value.len() > config.max_attribute_length { ++ return Err(Error::syntax( ++ SyntaxError::ExceededConfiguredLimit.to_cow(), ++ lexer.position(), ++ )); ++ } ++ } ++ Ok(()) ++ } ++} +diff --git a/src/reader/parser/inside_comment.rs b/src/reader/parser/inside_comment.rs +--- a/src/reader/parser/inside_comment.rs ++++ b/src/reader/parser/inside_comment.rs +@@ -6,6 +6,9 @@ + + impl PullParser { + pub fn inside_comment(&mut self, t: Token) -> Option { ++ if t == Token::CommentEnd { ++ if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } ++ } + match t { + Token::CommentEnd if self.config.ignore_comments => { + self.into_state_continue(State::OutsideTag) +diff --git a/src/reader/parser/inside_doctype.rs b/src/reader/parser/inside_doctype.rs +--- a/src/reader/parser/inside_doctype.rs ++++ b/src/reader/parser/inside_doctype.rs +@@ -16,6 +16,14 @@ + } + } + ++ if self.config.amplification_limits.is_some() { ++ // Normalize physical newlines before storing entity text. Numeric ++ // character references retain their CR/LF identity for later ++ // attribute normalization. ++ let skip_lf = self.entity_value_after_cr && substate == DoctypeSubstate::EntityValue && t == Token::Character('\n'); ++ self.entity_value_after_cr = substate == DoctypeSubstate::EntityValue && t == Token::Character('\r'); ++ if skip_lf { return None; } ++ } + match substate { + DoctypeSubstate::BeforeDoctypeName => match t { + Token::Character(c) if is_whitespace_char(c) => None, +@@ -312,7 +320,7 @@ + Some(self.error(SyntaxError::InvalidCharacterEntity(c as u32))) + } + Token::Character(c) => { +- self.buf.push(c); ++ self.buf.push(if self.config.amplification_limits.is_some() && c == '\r' { '\n' } else { c }); + None + } + _ => Some(self.error(SyntaxError::UnexpectedTokenInEntity(t))), +@@ -370,6 +378,7 @@ + let name = self.data.take_ref_data(); + match self.entities.get(&name) { + Some(ent) => { ++ if let Err(e) = self.lexer.account_indirect(ent.len(), true) { return Some(Err(e)); } + self.buf.push_str(ent); + self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::EntityValue, +diff --git a/src/reader/parser/inside_opening_tag.rs b/src/reader/parser/inside_opening_tag.rs +--- a/src/reader/parser/inside_opening_tag.rs ++++ b/src/reader/parser/inside_opening_tag.rs +@@ -9,7 +9,6 @@ + + impl PullParser { + pub fn inside_opening_tag(&mut self, t: Token, s: OpeningTagSubstate) -> Option { +- let max_attrs = self.config.max_attributes; + match s { + OpeningTagSubstate::InsideName => self.read_qualified_name(t, QualifiedNameTarget::OpeningTag, |this, token, name| { + match name.prefix_ref() { +@@ -66,46 +65,15 @@ + OpeningTagSubstate::InsideAttributeValue => self.read_attribute_value(t, |this, value| { + let name = this.data.take_attr_name()?; // will always succeed here + let pos = this.data.take_attr_pos(); +- match name.prefix_ref() { +- // declaring a new prefix; it is sufficient to check prefix only +- // because "xmlns" prefix is reserved +- Some(namespace::NS_XMLNS_PREFIX) => { +- let ln = &*name.local_name; +- if ln == namespace::NS_XMLNS_PREFIX { +- Some(this.error(SyntaxError::CannotRedefineXmlnsPrefix)) +- } else if ln == namespace::NS_XML_PREFIX && &*value != namespace::NS_XML_URI { +- Some(this.error(SyntaxError::CannotRedefineXmlPrefix)) +- } else if value.is_empty() { +- Some(this.error(SyntaxError::CannotUndefinePrefix(ln.into()))) +- } else { +- this.nst.put(name.local_name.clone(), value); +- this.into_state_continue(State::InsideOpeningTag(OpeningTagSubstate::AfterAttributeValue)) +- } +- }, +- +- // declaring default namespace +- None if &*name.local_name == namespace::NS_XMLNS_PREFIX => +- match &*value { +- namespace::NS_XMLNS_PREFIX | namespace::NS_XML_PREFIX | namespace::NS_XML_URI | namespace::NS_XMLNS_URI => +- Some(this.error(SyntaxError::InvalidDefaultNamespace(value.into()))), +- _ => { +- this.nst.put(namespace::NS_NO_PREFIX, value.clone()); +- this.into_state_continue(State::InsideOpeningTag(OpeningTagSubstate::AfterAttributeValue)) +- } +- }, +- +- // regular attribute +- _ => { +- if this.data.attributes.len() >= max_attrs { +- return Some(this.error(SyntaxError::ExceededConfiguredLimit)); +- } +- if let Some(pos) = pos { +- this.data.attribute_positions.push(pos); +- } +- this.data.attributes.push(OwnedAttribute { name, value }); +- this.into_state_continue(State::InsideOpeningTag(OpeningTagSubstate::AfterAttributeValue)) +- }, ++ if this.config.amplification_limits.is_some() { ++ if this.pending_attributes.len() >= this.config.max_attributes { ++ return Some(this.error(SyntaxError::ExceededConfiguredLimit)); ++ } ++ this.pending_attributes.push((name, value, pos)); ++ } else if let Err(e) = this.finish_attribute(name, value, pos) { ++ return Some(Err(e)); + } ++ this.into_state_continue(State::InsideOpeningTag(OpeningTagSubstate::AfterAttributeValue)) + }), + + OpeningTagSubstate::AfterAttributeValue => match t { +@@ -118,4 +86,39 @@ + }, + } + } ++ ++ pub(super) fn finish_attribute(&mut self, name: crate::name::OwnedName, value: String, ++ pos: Option) -> crate::reader::Result<()> { ++ match name.prefix_ref() { ++ Some(namespace::NS_XMLNS_PREFIX) => { ++ let ln = &*name.local_name; ++ let error = if ln == namespace::NS_XMLNS_PREFIX { ++ Some(SyntaxError::CannotRedefineXmlnsPrefix) ++ } else if ln == namespace::NS_XML_PREFIX && &*value != namespace::NS_XML_URI { ++ Some(SyntaxError::CannotRedefineXmlPrefix) ++ } else if value.is_empty() { ++ Some(SyntaxError::CannotUndefinePrefix(ln.into())) ++ } else { None }; ++ if let Some(e) = error { ++ return Err(crate::reader::Error::syntax(e.to_cow(), self.lexer.position())); ++ } ++ self.nst.put(name.local_name, value); ++ } ++ None if &*name.local_name == namespace::NS_XMLNS_PREFIX => { ++ match &*value { ++ namespace::NS_XMLNS_PREFIX | namespace::NS_XML_PREFIX | namespace::NS_XML_URI | namespace::NS_XMLNS_URI => ++ return Err(crate::reader::Error::syntax(SyntaxError::InvalidDefaultNamespace(value.into()).to_cow(), self.lexer.position())), ++ _ => { self.nst.put(namespace::NS_NO_PREFIX, value); } ++ } ++ } ++ _ => { ++ if self.data.attributes.len() >= self.config.max_attributes { ++ return Err(crate::reader::Error::syntax(SyntaxError::ExceededConfiguredLimit.to_cow(), self.lexer.position())); ++ } ++ if let Some(pos) = pos { self.data.attribute_positions.push(pos); } ++ self.data.attributes.push(OwnedAttribute { name, value }); ++ } ++ } ++ Ok(()) ++ } + } +diff --git a/src/reader/parser/inside_processing_instruction.rs b/src/reader/parser/inside_processing_instruction.rs +--- a/src/reader/parser/inside_processing_instruction.rs ++++ b/src/reader/parser/inside_processing_instruction.rs +@@ -8,6 +8,9 @@ + + impl PullParser { + pub fn inside_processing_instruction(&mut self, t: Token, s: ProcessingInstructionSubstate) -> Option { ++ if t == Token::ProcessingInstructionEnd { ++ if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } ++ } + match s { + ProcessingInstructionSubstate::PIInsideName => match t { + Token::Character(c) if self.buf.is_empty() && is_name_start_char(c) || +diff --git a/src/reader/parser/inside_reference.rs b/src/reader/parser/inside_reference.rs +--- a/src/reader/parser/inside_reference.rs ++++ b/src/reader/parser/inside_reference.rs +@@ -19,6 +19,16 @@ + return Some(self.error(SyntaxError::EmptyEntity)); + } + ++ // Opening-tag attributes are expanded after the entire raw ++ // tag has been counted, including namespace declarations. ++ if self.config.amplification_limits.is_some() ++ && matches!(self.state_after_reference, State::InsideOpeningTag(_)) { ++ self.buf.push('&'); ++ self.buf.push_str(&name); ++ self.buf.push(';'); ++ return self.into_state_continue(self.state_after_reference); ++ } ++ if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } + let c = match &*name { + "lt" => Some('<'), + "gt" => Some('>'), +@@ -32,8 +42,14 @@ + _ => None, + }; + if let Some(c) = c { ++ // Numeric references are part of the input token; the five ++ // named predefined replacements count as indirect bytes. ++ if !name.starts_with('#') { ++ if let Err(e) = self.lexer.account_indirect(c.len_utf8(), false) { return Some(Err(e)); } ++ } + self.buf.push(c); + } else if let Some(v) = self.config.extra_entities.get(&name) { ++ if let Err(e) = self.lexer.account_indirect(v.len(), true) { return Some(Err(e)); } + self.buf.push_str(v); + } else if let Some(v) = self.entities.get(&name) { + if self.state_after_reference == State::OutsideTag { +@@ -44,6 +60,7 @@ + } else { + // however, inside attributes it's not allowed to affect attribute quoting, + // so it can't be fed to the lexer ++ if let Err(e) = self.lexer.account_indirect(v.len(), true) { return Some(Err(e)); } + self.buf.push_str(v); + } + } else { +@@ -52,6 +69,12 @@ + let prev_st = self.state_after_reference; + if prev_st == State::OutsideTag && !is_whitespace_char(self.buf.chars().last().unwrap_or('\0')) { + self.inside_whitespace = false; ++ } ++ if prev_st == State::OutsideTag && c.is_some() ++ && self.config.amplification_limits.is_some() && !self.config.coalesce_characters { ++ let value = self.take_buf(); ++ self.push_pos(); ++ return self.into_state_emit(prev_st, Ok(crate::reader::XmlEvent::Characters(value))); + } + self.into_state_continue(prev_st) + }, +diff --git a/src/reader/parser/outside_tag.rs b/src/reader/parser/outside_tag.rs +--- a/src/reader/parser/outside_tag.rs ++++ b/src/reader/parser/outside_tag.rs +@@ -12,6 +12,20 @@ + pub fn outside_tag(&mut self, t: Token) -> Option { + match t { + Token::Character(c) => { ++ if self.config.amplification_limits.is_some() && matches!(c, '\n' | '\r') { ++ if self.literal_text_pending { ++ if let Err(e) = self.lexer.check_amplification_before_token() { return Some(Err(e)); } ++ } ++ if let Err(e) = self.lexer.check_amplification() { ++ if !self.buf.is_empty() && !self.config.coalesce_characters { ++ let text = self.take_buf(); ++ self.next_event = Some(Err(e)); ++ return Some(Ok(XmlEvent::Characters(text))); ++ } ++ return Some(Err(e)); ++ } ++ } ++ self.literal_text_pending = true; + if is_whitespace_char(c) { + // skip whitespace outside of the root element + if (self.config.trim_whitespace && self.buf.is_empty()) || +@@ -45,6 +59,7 @@ + return Some(self.error(SyntaxError::UnexpectedTokenOutsideRoot(t))); + } + self.inside_whitespace = false; ++ self.literal_text_pending = true; + + if let Some(s) = t.as_static_str() { + if self.buf.is_empty() { +@@ -59,11 +74,20 @@ + }, + + Token::ReferenceStart if self.depth() > 0 => { ++ if self.config.amplification_limits.is_some() && self.literal_text_pending { ++ if let Err(e) = self.lexer.check_amplification_before_token() { return Some(Err(e)); } ++ } ++ self.literal_text_pending = false; + self.state_after_reference = State::OutsideTag; ++ if self.config.amplification_limits.is_some() && !self.config.coalesce_characters && !self.buf.is_empty() { ++ let text = self.take_buf(); ++ return self.into_state_emit(State::InsideReference, Ok(XmlEvent::Characters(text))); ++ } + self.into_state_continue(State::InsideReference) + }, + + Token::ReferenceEnd if self.depth() > 0 => { // Semi-colon in a text outside an entity ++ self.literal_text_pending = true; + self.inside_whitespace = false; + if self.buf.len() > self.config.max_data_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); +@@ -79,6 +103,7 @@ + }, + + Token::CDataStart if self.depth() > 0 && self.config.coalesce_characters && self.config.cdata_to_characters => { ++ if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } + if self.buf.is_empty() { + self.push_pos(); // CDataEnd will pop pos if the buffer remains empty + } +@@ -87,6 +112,10 @@ + }, + + _ => { ++ if self.config.amplification_limits.is_some() && self.literal_text_pending { ++ if let Err(e) = self.lexer.check_amplification_before_token() { return Some(Err(e)); } ++ } ++ self.literal_text_pending = false; + // Encountered some markup event, flush the buffer as characters + // or a whitespace + let mut next_event = if self.buf_has_data() { +@@ -148,6 +177,7 @@ + ), + + Token::CDataStart if self.depth() > 0 => { ++ if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } + self.into_state(State::InsideCData, next_event) + }, + +diff --git a/src/reader/parser.rs b/src/reader/parser.rs +--- a/src/reader/parser.rs ++++ b/src/reader/parser.rs +@@ -53,6 +53,7 @@ + mod inside_opening_tag; + mod inside_processing_instruction; + mod inside_reference; ++mod attribute_expansion; + mod outside_tag; + + static DEFAULT_VERSION: XmlVersion = XmlVersion::Version10; +@@ -77,6 +78,7 @@ + + /// From DTD internal subset + entities: HashMap, ++ pending_attributes: Vec<(OwnedName, String, Option)>, + + nst: NamespaceStack, + +@@ -88,6 +90,8 @@ + + encountered: Encountered, + inside_whitespace: bool, ++ literal_text_pending: bool, ++ entity_value_after_cr: bool, + seen_prefix_separator: bool, + pop_namespace: bool, + } +@@ -128,6 +132,7 @@ + buf: String::with_capacity(STRING_RESERVE_CAPACITY), + qualified_name_buf: String::with_capacity(STRING_RESERVE_CAPACITY), + entities: HashMap::new(), ++ pending_attributes: Vec::new(), + nst: NamespaceStack::default(), + + data: MarkupData { +@@ -154,6 +159,8 @@ + + encountered: Encountered::None, + inside_whitespace: true, ++ literal_text_pending: false, ++ entity_value_after_cr: false, + seen_prefix_separator: false, + pop_namespace: false, + } +@@ -383,7 +390,7 @@ + } + + if let Some(ev) = self.next_event.take() { +- return ev; ++ return if ev.is_err() { self.set_final_result(ev) } else { ev }; + } + + if self.pop_namespace { +@@ -429,6 +436,9 @@ + /// Handle end of stream + #[cold] + fn handle_eof(&mut self) -> std::result::Result { ++ if self.literal_text_pending { ++ if let Err(e) = self.lexer.check_amplification() { return self.set_final_result(Err(e)); } ++ } + let ev = if self.depth() == 0 { + if self.encountered == Encountered::Element && self.st == State::OutsideTag { // all is ok + Ok(XmlEvent::EndDocument) +@@ -671,6 +681,17 @@ + } + + fn emit_start_element(&mut self, emit_end_element: bool) -> Option { ++ // Expat accounts for a complete start tag before expanding attributes. ++ // Keep references as source spellings until here, so neither expanded ++ // strings nor namespace values can be allocated before their check. ++ if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } ++ for (name, raw, pos) in std::mem::take(&mut self.pending_attributes) { ++ let value = match self.expand_attribute(&raw) { ++ Ok(value) => value, ++ Err(e) => return Some(Err(e)), ++ }; ++ if let Err(e) = self.finish_attribute(name, value, pos) { return Some(Err(e)); } ++ } + let mut name = self.data.take_element_name()?; + let mut attributes = self.data.take_attributes(); + let attribute_positions = self.data.take_attribute_positions(); +@@ -717,6 +738,7 @@ + } + + fn emit_end_element(&mut self) -> Option { ++ if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } + let mut name = self.data.take_element_name()?; + + // check whether the name prefix is bound and fix its namespace +diff --git a/src/reader.rs b/src/reader.rs +--- a/src/reader.rs ++++ b/src/reader.rs +@@ -9,6 +9,7 @@ + + use crate::common::{Position, TextPosition}; + ++pub use amplification::AmplificationLimits; + pub use config::ParserConfig; + pub use error::{Error, ErrorKind, ImmutableEntitiesError}; + pub use events::{DoctypeRef, XmlEvent}; +@@ -20,6 +21,9 @@ + + use self::parser::PullParser; + ++mod amplification; ++#[cfg(test)] ++mod amplification_tests; + mod config; + mod error; + mod events; diff --git a/vendor/xml/src/attribute.rs b/vendor/xml/src/attribute.rs new file mode 100644 index 00000000000..081c531120c --- /dev/null +++ b/vendor/xml/src/attribute.rs @@ -0,0 +1,99 @@ +//! Contains XML attributes manipulation types and functions. + +use std::fmt; + +use crate::escape::{AttributeEscapes, Escaped}; +use crate::name::{Name, OwnedName}; + +/// A borrowed version of an XML attribute. +/// +/// Consists of a borrowed qualified name and a borrowed string value. +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +pub struct Attribute<'a> { + /// Attribute name. + pub name: Name<'a>, + + /// Attribute value. + pub value: &'a str, +} + +impl fmt::Display for Attribute<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}=\"{}\"", self.name, Escaped::::new(self.value)) + } +} + +impl<'a> Attribute<'a> { + /// Creates an owned attribute out of this borrowed one. + #[inline] + #[must_use] + pub fn to_owned(&self) -> OwnedAttribute { + OwnedAttribute { + name: self.name.into(), + value: self.value.into(), + } + } + + /// Creates a borrowed attribute using the provided borrowed name and a borrowed string value. + #[inline] + #[must_use] + pub const fn new(name: Name<'a>, value: &'a str) -> Self { + Attribute { name, value } + } +} + +/// An owned version of an XML attribute. +/// +/// Consists of an owned qualified name and an owned string value. +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct OwnedAttribute { + /// Attribute name. + pub name: OwnedName, + + /// Attribute value. + pub value: String, +} + +impl OwnedAttribute { + /// Returns a borrowed `Attribute` out of this owned one. + #[must_use] + #[inline] + pub fn borrow(&self) -> Attribute<'_> { + Attribute { + name: self.name.borrow(), + value: &self.value, + } + } + + /// Creates a new owned attribute using the provided owned name and an owned string value. + #[inline] + pub fn new>(name: OwnedName, value: S) -> Self { + Self { name, value: value.into() } + } +} + +impl fmt::Display for OwnedAttribute { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}=\"{}\"", self.name, Escaped::::new(&self.value)) + } +} + +#[cfg(test)] +mod tests { + use super::Attribute; + + use crate::name::Name; + + #[test] + fn attribute_display() { + let attr = Attribute::new( + Name::qualified("attribute", "urn:namespace", Some("n")), + "its value with > & \" ' < weird symbols", + ); + + assert_eq!( + &*attr.to_string(), + "{urn:namespace}n:attribute=\"its value with > & " ' < weird symbols\"" + ); + } +} diff --git a/vendor/xml/src/common.rs b/vendor/xml/src/common.rs new file mode 100644 index 00000000000..9e514c64914 --- /dev/null +++ b/vendor/xml/src/common.rs @@ -0,0 +1,199 @@ +//! Contains common types and functions used throughout the library. + +use std::fmt; + +/// Represents a position inside some textual document. +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct TextPosition { + #[doc(hidden)] + pub row: u64, + + #[doc(hidden)] + pub column: u64, +} + +impl TextPosition { + /// Creates a new position initialized to the beginning of the document + #[inline] + #[must_use] + pub const fn new() -> Self { + Self { row: 0, column: 0 } + } + + /// Advances the position in a line + #[inline] + pub fn advance(&mut self, count: u8) { + self.column += u64::from(count); + } + + #[doc(hidden)] + #[deprecated] + pub fn advance_to_tab(&mut self, width: u8) { + let width = u64::from(width); + self.column += width - self.column % width; + } + + /// Advances the position to the beginning of the next line + #[inline] + pub fn new_line(&mut self) { + self.column = 0; + self.row += 1; + } + + /// Row, counting from 0. Add 1 to display as users expect! + #[must_use] + pub fn row(&self) -> u64 { + self.row + } + + /// Column, counting from 0. Add 1 to display as users expect! + #[must_use] + pub fn column(&self) -> u64 { + self.column + } +} + +impl fmt::Debug for TextPosition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl fmt::Display for TextPosition { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.row + 1, self.column + 1) + } +} + +/// Get the position in the document corresponding to the object +/// +/// This trait is implemented by parsers, lexers and errors. +pub trait Position { + /// Returns the current position or a position corresponding to the object. + fn position(&self) -> TextPosition; +} + +impl Position for TextPosition { + #[inline] + fn position(&self) -> TextPosition { + *self + } +} + +/// XML version enumeration. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum XmlVersion { + /// XML version 1.0, or any 1.x version other than 1.1 + /// + /// All future versions are disallowed since XML 1.1, so any version beyond 1.1 is an error tolerated only in XML 1.0. + /// + Version10, + + /// XML version 1.1. + Version11, +} + +impl XmlVersion { + /// Convenience helper which returns a string representation of the given version. + /// + /// ``` + /// # use xml::common::XmlVersion; + /// assert_eq!(XmlVersion::Version10.as_str(), "1.0"); + /// assert_eq!(XmlVersion::Version11.as_str(), "1.1"); + /// ``` + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::Version10 => "1.0", + Self::Version11 => "1.1", + } + } +} + +impl fmt::Display for XmlVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_str().fmt(f) + } +} + +impl fmt::Debug for XmlVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +/// Checks whether the given character is a white space character (`S`) +/// as is defined by XML 1.1 specification, [section 2.3][1]. +/// +/// [1]: http://www.w3.org/TR/2006/REC-xml11-20060816/#sec-common-syn +#[must_use] +#[inline] +pub const fn is_whitespace_char(c: char) -> bool { + matches!(c, '\x20' | '\x0a' | '\x09' | '\x0d') +} + +/// Matches the `PubIdChar` production. +pub(crate) fn is_pubid_char(c: char) -> bool { + matches!(c, '\x20' | '\x0D' | '\x0A' | 'a'..='z' | 'A'..='Z' | '0'..='9' | + '-' | '\'' | '(' | ')' | '+' | ',' | '.' | '/' | ':' | '=' | '?' | ';' | + '!' | '*' | '#' | '@' | '$' | '_' | '%') +} + +/// Checks whether the given string is compound only by white space +/// characters (`S`) using the previous `is_whitespace_char` to check +/// all characters of this string +pub fn is_whitespace_str(s: &str) -> bool { + s.chars().all(is_whitespace_char) +} + +/// Is it a valid character in XML 1.0 +#[must_use] +pub const fn is_xml10_char(c: char) -> bool { + matches!(c, '\u{09}' | '\u{0A}' | '\u{0D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..) +} + +/// Is it a valid character in XML 1.1 +#[must_use] +pub const fn is_xml11_char(c: char) -> bool { + matches!(c, '\u{01}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..) +} + +/// Is it a valid character in XML 1.1 but not part of the restricted character set +#[must_use] +pub const fn is_xml11_char_not_restricted(c: char) -> bool { + is_xml11_char(c) && + !matches!(c, '\u{01}'..='\u{08}' | '\u{0B}'..='\u{0C}' | '\u{0E}'..='\u{1F}' | '\u{7F}'..='\u{84}' | '\u{86}'..='\u{9F}') +} + +/// Checks whether the given character is a name start character (`NameStartChar`) +/// as is defined by XML 1.1 specification, [section 2.3][1]. +/// +/// [1]: http://www.w3.org/TR/2006/REC-xml11-20060816/#sec-common-syn +#[must_use] +pub const fn is_name_start_char(c: char) -> bool { + matches!(c, + ':' | 'A'..='Z' | '_' | 'a'..='z' | + '\u{C0}'..='\u{D6}' | '\u{D8}'..='\u{F6}' | '\u{F8}'..='\u{2FF}' | + '\u{370}'..='\u{37D}' | '\u{37F}'..='\u{1FFF}' | + '\u{200C}'..='\u{200D}' | '\u{2070}'..='\u{218F}' | + '\u{2C00}'..='\u{2FEF}' | '\u{3001}'..='\u{D7FF}' | + '\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}' | + '\u{10000}'..='\u{EFFFF}' + ) +} + +/// Checks whether the given character is a name character (`NameChar`) +/// as is defined by XML 1.1 specification, [section 2.3][1]. +/// +/// [1]: http://www.w3.org/TR/2006/REC-xml11-20060816/#sec-common-syn +#[must_use] +pub const fn is_name_char(c: char) -> bool { + if is_name_start_char(c) { + return true; + } + matches!(c, + '-' | '.' | '0'..='9' | '\u{B7}' | + '\u{300}'..='\u{36F}' | '\u{203F}'..='\u{2040}' + ) +} diff --git a/vendor/xml/src/escape.rs b/vendor/xml/src/escape.rs new file mode 100644 index 00000000000..c1e4dd16b49 --- /dev/null +++ b/vendor/xml/src/escape.rs @@ -0,0 +1,162 @@ +//! Contains functions for performing XML special characters escaping. + +use std::borrow::Cow; +use std::fmt::{Display, Formatter, Result}; +use std::marker::PhantomData; + +pub(crate) trait Escapes { + fn escape(c: u8) -> Option<&'static str>; + + fn byte_needs_escaping(c: u8) -> bool { + Self::escape(c).is_some() + } + + fn str_needs_escaping(s: &str) -> bool { + s.bytes().any(|c| Self::escape(c).is_some()) + } +} + +pub(crate) struct Escaped<'a, E: Escapes> { + _escape_phantom: PhantomData, + to_escape: &'a str, +} + +impl<'a, E: Escapes> Escaped<'a, E> { + pub const fn new(s: &'a str) -> Self { + Escaped { + _escape_phantom: PhantomData, + to_escape: s, + } + } +} + +impl Display for Escaped<'_, E> { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let mut total_remaining = self.to_escape; + + // find the next occurence + while let Some(n) = total_remaining.bytes().position(E::byte_needs_escaping) { + let (start, remaining) = total_remaining.split_at(n); + + f.write_str(start)?; + + // unwrap is safe because we checked is_some for position n earlier + let next_byte = remaining.bytes().next().unwrap(); + let replacement = E::escape(next_byte).unwrap_or("unexpected token"); + f.write_str(replacement)?; + + total_remaining = &remaining[1..]; + } + + f.write_str(total_remaining) + } +} + +fn escape_str(s: &str) -> Cow<'_, str> { + if E::str_needs_escaping(s) { + Cow::Owned(Escaped::::new(s).to_string()) + } else { + Cow::Borrowed(s) + } +} + +macro_rules! escapes { + { + $name: ident, + $($k: expr => $v: expr),* $(,)? + } => { + pub(crate) struct $name; + + impl Escapes for $name { + fn escape(c: u8) -> Option<&'static str> { + match c { + $( $k => Some($v),)* + _ => None + } + } + } + }; +} + +escapes!( + AttributeEscapes, + b'<' => "<", + b'>' => ">", + b'"' => """, + b'\'' => "'", + b'&' => "&", + b'\n' => " ", + b'\r' => " ", +); + +escapes!( + PcDataEscapes, + b'<' => "<", + b'>' => ">", + b'&' => "&", +); + +/// Performs escaping of common XML characters inside an attribute value. +/// +/// This function replaces several important markup characters with their +/// entity equivalents: +/// +/// * `<` → `<` +/// * `>` → `>` +/// * `"` → `"` +/// * `'` → `'` +/// * `&` → `&` +/// +/// The following characters are escaped so that attributes are printed on +/// a single line: +/// * `\n` → ` ` +/// * `\r` → ` ` +/// +/// The resulting string is safe to use inside XML attribute values or in PCDATA sections. +/// +/// Does not perform allocations if the given string does not contain escapable characters. +#[inline] +#[must_use] +pub fn escape_str_attribute(s: &str) -> Cow<'_, str> { + escape_str::(s) +} + +/// Performs escaping of common XML characters inside PCDATA. +/// +/// This function replaces several important markup characters with their +/// entity equivalents: +/// +/// * `<` → `<` +/// * `&` → `&` +/// +/// The resulting string is safe to use inside PCDATA sections but NOT inside attribute values. +/// +/// Does not perform allocations if the given string does not contain escapable characters. +#[inline] +#[must_use] +pub fn escape_str_pcdata(s: &str) -> Cow<'_, str> { + escape_str::(s) +} + +#[cfg(test)] +mod tests { + use super::{escape_str_attribute, escape_str_pcdata}; + + #[test] + fn test_escape_str_attribute() { + assert_eq!(escape_str_attribute("<>'\"&\n\r"), "<>'"& "); + assert_eq!(escape_str_attribute("no_escapes"), "no_escapes"); + } + + #[test] + fn test_escape_str_pcdata() { + assert_eq!(escape_str_pcdata("<>&"), "<>&"); + assert_eq!(escape_str_pcdata("no_escapes"), "no_escapes"); + } + + #[test] + fn test_escape_multibyte_code_points() { + assert_eq!(escape_str_attribute("☃<"), "☃<"); + assert_eq!(escape_str_pcdata("☃<"), "☃<"); + } +} diff --git a/vendor/xml/src/lib.rs b/vendor/xml/src/lib.rs new file mode 100644 index 00000000000..4a0dfed56d6 --- /dev/null +++ b/vendor/xml/src/lib.rs @@ -0,0 +1,27 @@ +#![warn(missing_docs)] +#![forbid(non_camel_case_types)] +#![forbid(unsafe_code)] +#![allow(clippy::redundant_closure_for_method_calls)] +#![allow(clippy::module_name_repetitions)] + +//! This crate currently provides an almost XML 1.0/1.1-compliant pull parser. +//! +//! Please note that functions of this parser may panic. +//! If a panic could cause a Denial Of Service in your codebase, *you're* responsible for wrapping access to this library in `catch_unwind`. + +#![cfg_attr(doctest, doc = include_str!("../README.md"))] + +pub use crate::reader::{EventReader, ParserConfig}; +pub use crate::util::Encoding; +pub use crate::writer::{EmitterConfig, EventWriter}; + +pub mod attribute; +pub mod common; +pub mod escape; +#[doc(hidden)] // FIXME: not supposed to be public +pub mod macros; +pub mod name; +pub mod namespace; +pub mod reader; +mod util; +pub mod writer; diff --git a/vendor/xml/src/macros.rs b/vendor/xml/src/macros.rs new file mode 100644 index 00000000000..c558327d326 --- /dev/null +++ b/vendor/xml/src/macros.rs @@ -0,0 +1,38 @@ +#![macro_use] + +//! Contains several macros used in this crate. + +macro_rules! gen_setter { + ($(#[$comments:meta])* $field:ident : into $t:ty) => { + + $(#[$comments])* + /// + /// See [`ParserConfig`][crate::ParserConfig] fields docs for details + #[inline] + #[must_use] + pub fn $field>(mut self, value: T) -> Self { + self.$field = value.into(); + self + } + }; + ($(#[$comments:meta])* $field:ident : val $t:ty) => { + $(#[$comments])* + /// + /// See [`ParserConfig`][crate::ParserConfig] fields docs for details + #[inline] + #[must_use] + pub const fn $field(mut self, value: $t) -> Self { + self.$field = value; + self + } + }; +} + +macro_rules! gen_setters { + ($target:ident, $($(#[$comments:meta])* $field:ident : $k:tt $tpe:ty),+) => ( + impl $target {$( + + gen_setter! { $(#[$comments])* $field : $k $tpe } + )+ + }) +} diff --git a/vendor/xml/src/name.rs b/vendor/xml/src/name.rs new file mode 100644 index 00000000000..f142f4df362 --- /dev/null +++ b/vendor/xml/src/name.rs @@ -0,0 +1,334 @@ +//! Contains XML qualified names manipulation types and functions. + +use std::fmt; +use std::str::FromStr; + +use crate::namespace::NS_NO_PREFIX; + +/// Represents a qualified XML name. +/// +/// A qualified name always consists at least of a local name. It can optionally contain +/// a prefix; when reading an XML document, if it contains a prefix, it must also contain a +/// namespace URI, but this is not enforced statically; see below. The name can contain a +/// namespace without a prefix; in that case a default, empty prefix is assumed. +/// +/// When writing XML documents, it is possible to omit the namespace URI, leaving only +/// the prefix. In this case the writer will check that the specifed prefix is bound to some +/// URI in the current namespace context. If both prefix and namespace URI are specified, +/// it is checked that the current namespace context contains this exact correspondence +/// between prefix and namespace URI. +/// +/// # Prefixes and URIs +/// +/// A qualified name with a prefix must always contain a proper namespace URI --- names with +/// a prefix but without a namespace associated with that prefix are meaningless. However, +/// it is impossible to obtain proper namespace URI by a prefix without a context, and such +/// context is only available when parsing a document (or it can be constructed manually +/// when writing a document). Tying a name to a context statically seems impractical. This +/// may change in future, though. +/// +/// # Conversions +/// +/// `Name` implements some `From` instances for conversion from strings and tuples. For example: +/// +/// ```rust +/// # use xml::name::Name; +/// let n1: Name = "p:some-name".into(); +/// let n2: Name = ("p", "some-name").into(); +/// +/// assert_eq!(n1, n2); +/// assert_eq!(n1.local_name, "some-name"); +/// assert_eq!(n1.prefix, Some("p")); +/// assert!(n1.namespace.is_none()); +/// ``` +/// +/// This is added to support easy specification of XML elements when writing XML documents. +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct Name<'a> { + /// A local name, e.g. `string` in `xsi:string`. + pub local_name: &'a str, + + /// A namespace URI, e.g. `http://www.w3.org/2000/xmlns/`. + pub namespace: Option<&'a str>, + + /// A name prefix, e.g. `xsi` in `xsi:string`. + pub prefix: Option<&'a str>, +} + +impl<'a> From<&'a str> for Name<'a> { + fn from(s: &'a str) -> Self { + if let Some((prefix, name)) = s.split_once(':') { + Name::prefixed(name, prefix) + } else { + Name::local(s) + } + } +} + +impl<'a> From<(&'a str, &'a str)> for Name<'a> { + fn from((prefix, name): (&'a str, &'a str)) -> Self { + Name::prefixed(name, prefix) + } +} + +impl fmt::Display for Name<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(namespace) = self.namespace { + write!(f, "{{{namespace}}}")?; + } + + if let Some(prefix) = self.prefix { + write!(f, "{prefix}:")?; + } + + f.write_str(self.local_name) + } +} + +impl<'a> Name<'a> { + /// Returns an owned variant of the qualified name. + #[must_use] + pub fn to_owned(&self) -> OwnedName { + OwnedName { + local_name: self.local_name.into(), + namespace: self.namespace.map(Into::into), + prefix: self.prefix.map(Into::into), + } + } + + /// Returns a new `Name` instance representing plain local name. + #[inline] + #[must_use] + pub const fn local(local_name: &str) -> Name<'_> { + Name { + local_name, + prefix: None, + namespace: None, + } + } + + /// Returns a new `Name` instance with the given local name and prefix. + #[inline] + #[must_use] + pub const fn prefixed(local_name: &'a str, prefix: &'a str) -> Self { + Name { + local_name, + namespace: None, + prefix: Some(prefix), + } + } + + /// Returns a new `Name` instance representing a qualified name with or without a prefix and + /// with a namespace URI. + #[inline] + #[must_use] + pub const fn qualified(local_name: &'a str, namespace: &'a str, prefix: Option<&'a str>) -> Self { + Name { + local_name, + namespace: Some(namespace), + prefix, + } + } + + /// Returns a correct XML representation of this local name and prefix. + /// + /// This method is different from the autoimplemented `to_string()` because it does not + /// include namespace URI in the result. + #[must_use] + pub fn to_repr(&self) -> String { + self.repr_display().to_string() + } + + /// Returns a structure which can be displayed with `std::fmt` machinery to obtain this + /// local name and prefix. + /// + /// This method is needed for efficiency purposes in order not to create unnecessary + /// allocations. + #[inline] + #[must_use] + pub const fn repr_display(&self) -> ReprDisplay<'_, '_> { + ReprDisplay(self) + } + + /// Returns either a prefix of this name or `namespace::NS_NO_PREFIX` constant. + #[inline] + #[must_use] + pub fn prefix_repr(&self) -> &str { + self.prefix.unwrap_or(NS_NO_PREFIX) + } +} + +/// A wrapper around `Name` whose `Display` implementation prints the wrapped name as it is +/// displayed in an XML document. +pub struct ReprDisplay<'a, 'b>(&'a Name<'b>); + +impl<'a, 'b: 'a> fmt::Display for ReprDisplay<'a, 'b> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0.prefix { + Some(prefix) => write!(f, "{}:{}", prefix, self.0.local_name), + None => self.0.local_name.fmt(f), + } + } +} + +/// An owned variant of `Name`. +/// +/// Everything about `Name` applies to this structure as well. +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct OwnedName { + /// A local name, e.g. `string` in `xsi:string`. + /// + /// Local name is ambiguous, because multiple namespaces can share the same name. + /// Always check `namespace` too. + pub local_name: String, + + /// A namespace URI, e.g. `http://www.w3.org/2000/xmlns/`. + /// + /// `None` for default namespace. + /// + /// Note that in XML attributes don't inherit element's namespace + /// and are in the default namespace unless they have a prefix. + pub namespace: Option, + + /// Semantically meaningless name prefix, e.g. `xsi` in `xsi:string`. + /// + /// Prefixes are just a syntactic detail used for decoration or to avoid repetition. + /// Only `namespace` matters for identity of the element. + pub prefix: Option, +} + +impl PartialEq<(&str, &str)> for OwnedName { + /// Compare `(namespaceURI, localName)`. Default namespace is `""`. + fn eq(&self, other: &(&str, &str)) -> bool { + other.1.eq(&self.local_name) && other.0.eq(self.namespace.as_deref().unwrap_or_default()) + } +} +impl PartialEq for (&str, &str) { + /// Compare `(namespaceURI, localName)`. Default namespace is `""`. + fn eq(&self, other: &OwnedName) -> bool { + other.eq(self) + } +} + +impl fmt::Display for OwnedName { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.borrow(), f) + } +} + +impl OwnedName { + /// Constructs a borrowed `Name` based on this owned name. + #[must_use] + #[inline] + pub fn borrow(&self) -> Name<'_> { + Name { + local_name: &self.local_name, + namespace: self.namespace.as_deref(), + prefix: self.prefix.as_deref(), + } + } + + /// Returns a new `OwnedName` instance representing a plain local name. + #[inline] + pub fn local(local_name: S) -> Self where S: Into { + Self { + local_name: local_name.into(), + namespace: None, + prefix: None, + } + } + + /// Returns a new `OwnedName` instance representing a qualified name with or without + /// a prefix and with a namespace URI. + #[inline] + pub fn qualified(local_name: S1, namespace: S2, prefix: Option) -> Self + where S1: Into, S2: Into, S3: Into + { + Self { + local_name: local_name.into(), + namespace: Some(namespace.into()), + prefix: prefix.map(Into::into), + } + } + + /// Returns an optional prefix by reference, equivalent to `self.borrow().prefix` + /// but avoids extra work. + #[inline] + #[must_use] + pub fn prefix_ref(&self) -> Option<&str> { + self.prefix.as_deref() + } + + /// Returns an optional namespace by reference, equivalen to `self.borrow().namespace` + /// but avoids extra work. + #[inline] + #[must_use] + pub fn namespace_ref(&self) -> Option<&str> { + self.namespace.as_deref() + } +} + +impl<'a> From> for OwnedName { + #[inline] + fn from(n: Name<'a>) -> Self { + n.to_owned() + } +} + +impl FromStr for OwnedName { + type Err = (); + + /// Parses the given string slice into a qualified name. + /// + /// This function, when finishes sucessfully, always return a qualified + /// name without a namespace (`name.namespace == None`). It should be filled later + /// using proper `NamespaceStack`. + /// + /// It is supposed that all characters in the argument string are correct + /// as defined by the XML specification. No additional checks except a check + /// for emptiness are done. + fn from_str(s: &str) -> Result { + let mut it = s.split(':'); + + let r = match (it.next(), it.next(), it.next()) { + (Some(prefix), Some(local_name), None) if !prefix.is_empty() && + !local_name.is_empty() => + Some((local_name.into(), Some(prefix.into()))), + (Some(local_name), None, None) if !local_name.is_empty() => + Some((local_name.into(), None)), + (_, _, _) => None + }; + r.map(|(local_name, prefix)| Self { + local_name, + namespace: None, + prefix + }).ok_or(()) + } +} + +#[cfg(test)] +mod tests { + use super::OwnedName; + + #[test] + fn test_owned_name_from_str() { + assert_eq!("prefix:name".parse(), Ok(OwnedName { + local_name: "name".into(), + namespace: None, + prefix: Some("prefix".into()) + })); + + assert_eq!("name".parse(), Ok(OwnedName { + local_name: "name".into(), + namespace: None, + prefix: None + })); + + assert_eq!("".parse(), Err::(())); + assert_eq!(":".parse(), Err::(())); + assert_eq!(":a".parse(), Err::(())); + assert_eq!("a:".parse(), Err::(())); + assert_eq!("a:b:c".parse(), Err::(())); + } +} diff --git a/vendor/xml/src/namespace.rs b/vendor/xml/src/namespace.rs new file mode 100644 index 00000000000..f0950e38440 --- /dev/null +++ b/vendor/xml/src/namespace.rs @@ -0,0 +1,515 @@ +//! Contains namespace manipulation types and functions. + +use std::borrow::Cow; +use std::collections::btree_map::{BTreeMap, Entry, Iter as Entries}; +use std::collections::HashSet; +use std::iter::{Map, Rev}; +use std::slice::Iter; + +/// Designates prefix for namespace definitions. +/// +/// See [Namespaces in XML][namespace] spec for more information. +/// +/// [namespace]: http://www.w3.org/TR/xml-names/#ns-decl +pub const NS_XMLNS_PREFIX: &str = "xmlns"; + +/// Designates the standard URI for `xmlns` prefix. +/// +/// See [A Namespace Name for xmlns Attributes][namespace] for more information. +/// +/// [namespace]: http://www.w3.org/2000/xmlns/ +pub const NS_XMLNS_URI: &str = "http://www.w3.org/2000/xmlns/"; + +/// Designates prefix for a namespace containing several special predefined attributes. +/// +/// See [2.10 White Space handling][1], [2.1 Language Identification][2], +/// [XML Base specification][3] and [xml:id specification][4] for more information. +/// +/// [1]: http://www.w3.org/TR/REC-xml/#sec-white-space +/// [2]: http://www.w3.org/TR/REC-xml/#sec-lang-tag +/// [3]: http://www.w3.org/TR/xmlbase/ +/// [4]: http://www.w3.org/TR/xml-id/ +pub const NS_XML_PREFIX: &str = "xml"; + +/// Designates the standard URI for `xml` prefix. +/// +/// See `NS_XML_PREFIX` documentation for more information. +pub const NS_XML_URI: &str = "http://www.w3.org/XML/1998/namespace"; + +/// Designates the absence of prefix in a qualified name. +/// +/// This constant should be used to define or query default namespace which should be used +/// for element or attribute names without prefix. For example, if a namespace mapping +/// at a particular point in the document contains correspondence like +/// +/// ```none +/// NS_NO_PREFIX --> urn:some:namespace +/// ``` +/// +/// then all names declared without an explicit prefix `urn:some:namespace` is assumed as +/// a namespace URI. +/// +/// By default empty prefix corresponds to absence of namespace, but this can change either +/// when writing an XML document (manually) or when reading an XML document (based on namespace +/// declarations). +pub const NS_NO_PREFIX: &str = ""; + +/// Designates an empty namespace URI, which is equivalent to absence of namespace. +/// +/// This constant should not usually be used directly; it is used to designate that +/// empty prefix corresponds to absent namespace in `NamespaceStack` instances created with +/// `NamespaceStack::default()`. Therefore, it can be used to restore `NS_NO_PREFIX` mapping +/// in a namespace back to its default value. +pub const NS_EMPTY_URI: &str = ""; + +/// Namespace is a map from prefixes to namespace URIs. +/// +/// No prefix (i.e. default namespace) is designated by `NS_NO_PREFIX` constant. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct Namespace(pub BTreeMap); + +impl Namespace { + /// Returns an empty namespace. + #[inline] + #[must_use] + pub fn empty() -> Self { + Self(BTreeMap::new()) + } + + /// Checks whether this namespace is empty. + #[inline] + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Checks whether this namespace is essentially empty, that is, it does not contain + /// anything but default mappings. + #[must_use] + pub fn is_essentially_empty(&self) -> bool { + // a shortcut for a namespace which is definitely not empty + if self.0.len() > 3 { return false; } + + self.0.iter().all(|(k, v)| matches!((&**k, &**v), + (NS_NO_PREFIX, NS_EMPTY_URI) | + (NS_XMLNS_PREFIX, NS_XMLNS_URI) | + (NS_XML_PREFIX, NS_XML_URI)) + ) + } + + /// Checks whether this namespace mapping contains the given prefix. + /// + /// # Parameters + /// * `prefix` --- namespace prefix. + /// + /// # Return value + /// `true` if this namespace contains the given prefix, `false` otherwise. + #[inline] + pub fn contains>(&self, prefix: &P) -> bool { + self.0.contains_key(prefix.as_ref()) + } + + /// Puts a mapping into this namespace. + /// + /// This method does not override any already existing mappings. + /// + /// Returns a boolean flag indicating whether the map already contained + /// the given prefix. + /// + /// # Parameters + /// * `prefix` --- namespace prefix; + /// * `uri` --- namespace URI. + /// + /// # Return value + /// `true` if `prefix` has been inserted successfully; `false` if the `prefix` + /// was already present in the namespace. + pub fn put(&mut self, prefix: P, uri: U) -> bool + where P: Into, U: Into + { + match self.0.entry(prefix.into()) { + Entry::Occupied(_) => false, + Entry::Vacant(ve) => { + ve.insert(uri.into()); + true + }, + } + } + + /// Puts a mapping into this namespace forcefully. + /// + /// This method, unlike `put()`, does replace an already existing mapping. + /// + /// Returns previous URI which was assigned to the given prefix, if it is present. + /// + /// # Parameters + /// * `prefix` --- namespace prefix; + /// * `uri` --- namespace URI. + /// + /// # Return value + /// `Some(uri)` with `uri` being a previous URI assigned to the `prefix`, or + /// `None` if such prefix was not present in the namespace before. + pub fn force_put(&mut self, prefix: P, uri: U) -> Option + where P: Into, U: Into + { + self.0.insert(prefix.into(), uri.into()) + } + + /// Queries the namespace for the given prefix. + /// + /// # Parameters + /// * `prefix` --- namespace prefix. + /// + /// # Return value + /// Namespace URI corresponding to the given prefix, if it is present. + pub fn get<'a, P: ?Sized + AsRef>(&'a self, prefix: &P) -> Option<&'a str> { + self.0.get(prefix.as_ref()).map(|s| &**s) + } + + /// Borrowed namespace for the writer + #[must_use] + pub const fn borrow(&self) -> Cow<'_, Self> { + Cow::Borrowed(self) + } + + /// Namespace mappings contained in a namespace. + pub fn iter(&self) -> NamespaceMappings<'_> { + self.into_iter() + } +} + +/// An alias for iterator type for namespace mappings contained in a namespace. +pub type NamespaceMappings<'a> = Map< + Entries<'a, String, String>, + for<'b> fn((&'b String, &'b String)) -> UriMapping<'b> +>; + +impl<'a> IntoIterator for &'a Namespace { + type IntoIter = NamespaceMappings<'a>; + type Item = UriMapping<'a>; + + fn into_iter(self) -> Self::IntoIter { + fn mapper<'a>((prefix, uri): (&'a String, &'a String)) -> UriMapping<'a> { + (prefix, uri) + } + self.0.iter().map(mapper) + } +} + +/// Namespace stack is a sequence of namespaces. +/// +/// Namespace stack is used to represent cumulative namespace consisting of +/// combined namespaces from nested elements. +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct NamespaceStack(pub Vec); + +impl NamespaceStack { + /// Returns an empty namespace stack. + #[inline] + #[must_use] + pub fn empty() -> Self { + Self(Vec::with_capacity(2)) + } + + /// Returns a namespace stack with default items in it. + /// + /// Default items are the following: + /// + /// * `xml` → `http://www.w3.org/XML/1998/namespace`; + /// * `xmlns` → `http://www.w3.org/2000/xmlns/`. + #[inline] + #[must_use] + #[allow(clippy::should_implement_trait)] + pub fn default() -> Self { + let mut nst = Self::empty(); + nst.push_empty(); + // xml namespace + nst.put(NS_XML_PREFIX, NS_XML_URI); + // xmlns namespace + nst.put(NS_XMLNS_PREFIX, NS_XMLNS_URI); + // empty namespace + nst.put(NS_NO_PREFIX, NS_EMPTY_URI); + nst + } + + /// Adds an empty namespace to the top of this stack. + #[inline] + pub fn push_empty(&mut self) -> &mut Self { + self.0.push(Namespace::empty()); + self + } + + /// Removes the topmost namespace in this stack. + /// + /// Panics if the stack is empty. + #[inline] + #[track_caller] + pub fn pop(&mut self) -> Namespace { + self.0.pop().unwrap() + } + + /// Removes the topmost namespace in this stack. + /// + /// Returns `Some(namespace)` if this stack is not empty and `None` otherwise. + #[inline] + pub fn try_pop(&mut self) -> Option { + self.0.pop() + } + + /// Borrows the topmost namespace mutably, leaving the stack intact. + /// + /// Panics if the stack is empty. + #[inline] + #[track_caller] + pub fn peek_mut(&mut self) -> &mut Namespace { + self.0.last_mut().unwrap() + } + + /// Borrows the topmost namespace immutably, leaving the stack intact. + /// + /// Panics if the stack is empty. + #[inline] + #[must_use] + #[track_caller] + pub fn peek(&self) -> &Namespace { + self.0.last().unwrap() + } + + /// Puts a mapping into the topmost namespace if this stack does not already contain one. + /// + /// Returns a boolean flag indicating whether the insertion has completed successfully. + /// Note that both key and value are matched and the mapping is inserted if either + /// namespace prefix is not already mapped, or if it is mapped, but to a different URI. + /// + /// # Parameters + /// * `prefix` --- namespace prefix; + /// * `uri` --- namespace URI. + /// + /// # Return value + /// `true` if `prefix` has been inserted successfully; `false` if the `prefix` + /// was already present in the namespace stack. + pub fn put_checked(&mut self, prefix: P, uri: U) -> bool + where P: Into + AsRef, + U: Into + AsRef + { + if self.0.iter().any(|ns| ns.get(&prefix) == Some(uri.as_ref())) { + false + } else { + self.put(prefix, uri); + true + } + } + + /// Puts a mapping into the topmost namespace in this stack. + /// + /// This method does not override a mapping in the topmost namespace if it is + /// already present, however, it does not depend on other namespaces in the stack, + /// so it is possible to put a mapping which is present in lower namespaces. + /// + /// Returns a boolean flag indicating whether the insertion has completed successfully. + /// + /// # Parameters + /// * `prefix` --- namespace prefix; + /// * `uri` --- namespace URI. + /// + /// # Return value + /// `true` if `prefix` has been inserted successfully; `false` if the `prefix` + /// was already present in the namespace. + #[inline] + pub fn put(&mut self, prefix: P, uri: U) -> bool + where P: Into, U: Into + { + if let Some(ns) = self.0.last_mut() { + ns.put(prefix, uri) + } else { + false + } + } + + /// Performs a search for the given prefix in the whole stack. + /// + /// This method walks the stack from top to bottom, querying each namespace + /// in order for the given prefix. If none of the namespaces contains the prefix, + /// `None` is returned. + /// + /// # Parameters + /// * `prefix` --- namespace prefix. + #[inline] + pub fn get<'a, P: ?Sized + AsRef>(&'a self, prefix: &P) -> Option<&'a str> { + let prefix = prefix.as_ref(); + for ns in self.0.iter().rev() { + match ns.get(prefix) { + None => {}, + r => return r, + } + } + None + } + + /// Combines this stack of namespaces into a single namespace. + /// + /// Namespaces are combined in left-to-right order, that is, rightmost namespace + /// elements take priority over leftmost ones. + #[must_use] + pub fn squash(&self) -> Namespace { + let mut result = BTreeMap::new(); + for ns in &self.0 { + result.extend(ns.0.iter().map(|(k, v)| (k.clone(), v.clone()))); + } + Namespace(result) + } + + /// Returns an object which implements `Extend` using `put_checked()` instead of `put()`. + /// + /// See `CheckedTarget` for more information. + #[inline] + pub fn checked_target(&mut self) -> CheckedTarget<'_> { + CheckedTarget(self) + } + + /// Returns an iterator over all mappings in this namespace stack. + #[inline] + #[must_use] + pub fn iter(&self) -> NamespaceStackMappings<'_> { + self.into_iter() + } +} + +/// An iterator over mappings from prefixes to URIs in a namespace stack. +/// +/// # Example +/// ``` +/// # use xml::namespace::NamespaceStack; +/// let mut nst = NamespaceStack::empty(); +/// nst.push_empty(); +/// nst.put("a", "urn:A"); +/// nst.put("b", "urn:B"); +/// nst.push_empty(); +/// nst.put("c", "urn:C"); +/// +/// assert_eq!(vec![("c", "urn:C"), ("a", "urn:A"), ("b", "urn:B")], nst.iter().collect::>()); +/// ``` +pub struct NamespaceStackMappings<'a> { + namespaces: Rev>, + current_namespace: Option>, + used_keys: HashSet<&'a str>, +} + +impl NamespaceStackMappings<'_> { + fn go_to_next_namespace(&mut self) -> bool { + self.current_namespace = self.namespaces.next().map(|ns| ns.into_iter()); + self.current_namespace.is_some() + } +} + +impl<'a> Iterator for NamespaceStackMappings<'a> { + type Item = UriMapping<'a>; + + fn next(&mut self) -> Option> { + // If there is no current namespace and no next namespace, we're finished + if self.current_namespace.is_none() && !self.go_to_next_namespace() { + return None; + } + let next_item = self.current_namespace.as_mut()?.next(); + + match next_item { + // There is an element in the current namespace + Some((k, v)) => if self.used_keys.contains(&k) { + // If the current key is used, go to the next one + self.next() + } else { + // Otherwise insert the current key to the set of used keys and + // return the mapping + self.used_keys.insert(k); + Some((k, v)) + }, + // Current namespace is exhausted + None => if self.go_to_next_namespace() { + // If there is next namespace, continue from it + self.next() + } else { + // No next namespace, exiting + None + } + } + } +} + +impl<'a> IntoIterator for &'a NamespaceStack { + type IntoIter = NamespaceStackMappings<'a>; + type Item = UriMapping<'a>; + + fn into_iter(self) -> Self::IntoIter { + NamespaceStackMappings { + namespaces: self.0.iter().rev(), + current_namespace: None, + used_keys: HashSet::new(), + } + } +} + +/// A type alias for a pair of `(prefix, uri)` values returned by namespace iterators. +pub type UriMapping<'a> = (&'a str, &'a str); + +impl<'a> Extend> for Namespace { + fn extend(&mut self, iterable: T) where T: IntoIterator> { + for (prefix, uri) in iterable { + self.put(prefix, uri); + } + } +} + +impl<'a> Extend> for NamespaceStack { + fn extend(&mut self, iterable: T) where T: IntoIterator> { + for (prefix, uri) in iterable { + self.put(prefix, uri); + } + } +} + +/// A wrapper around `NamespaceStack` which implements `Extend` using `put_checked()`. +/// +/// # Example +/// +/// ``` +/// # use xml::namespace::NamespaceStack; +/// +/// let mut nst = NamespaceStack::empty(); +/// nst.push_empty(); +/// nst.put("a", "urn:A"); +/// nst.put("b", "urn:B"); +/// nst.push_empty(); +/// nst.put("c", "urn:C"); +/// +/// nst.checked_target().extend(vec![("a", "urn:Z"), ("b", "urn:B"), ("c", "urn:Y"), ("d", "urn:D")]); +/// assert_eq!( +/// vec![("a", "urn:Z"), ("c", "urn:C"), ("d", "urn:D"), ("b", "urn:B")], +/// nst.iter().collect::>() +/// ); +/// ``` +/// +/// Compare: +/// +/// ``` +/// # use xml::namespace::NamespaceStack; +/// # let mut nst = NamespaceStack::empty(); +/// # nst.push_empty(); +/// # nst.put("a", "urn:A"); +/// # nst.put("b", "urn:B"); +/// # nst.push_empty(); +/// # nst.put("c", "urn:C"); +/// +/// nst.extend(vec![("a", "urn:Z"), ("b", "urn:B"), ("c", "urn:Y"), ("d", "urn:D")]); +/// assert_eq!( +/// vec![("a", "urn:Z"), ("b", "urn:B"), ("c", "urn:C"), ("d", "urn:D")], +/// nst.iter().collect::>() +/// ); +/// ``` +pub struct CheckedTarget<'a>(&'a mut NamespaceStack); + +impl<'b> Extend> for CheckedTarget<'_> { + fn extend(&mut self, iterable: T) where T: IntoIterator> { + for (prefix, uri) in iterable { + self.0.put_checked(prefix, uri); + } + } +} diff --git a/vendor/xml/src/reader.rs b/vendor/xml/src/reader.rs new file mode 100644 index 00000000000..9e61ba3ec66 --- /dev/null +++ b/vendor/xml/src/reader.rs @@ -0,0 +1,239 @@ +//! Contains high-level interface for a pull-based XML parser. +//! +//! The most important type in this module is `EventReader`, which provides an iterator +//! view for events in XML document. + +use std::io::Read; +use std::iter::FusedIterator; +use std::result; + +use crate::common::{Position, TextPosition}; + +pub use amplification::AmplificationLimits; +pub use config::ParserConfig; +pub use error::{Error, ErrorKind, ImmutableEntitiesError}; +pub use events::{DoctypeRef, XmlEvent}; + +// back compat +#[doc(hidden)] +#[deprecated(note = "Merged into ParserConfig")] +pub type ParserConfig2 = ParserConfig; + +use self::parser::PullParser; + +mod amplification; +#[cfg(test)] +mod amplification_tests; +mod config; +mod error; +mod events; +mod lexer; +mod parser; + +/// A result type yielded by `XmlReader`. +pub type Result = result::Result; + +/// A wrapper around an `std::io::Read` instance which provides pull-based XML parsing. +/// +/// The reader should be wrapped in a `BufReader`, otherwise parsing may be very slow. +pub struct EventReader { + source: R, + parser: PullParser, +} + +impl EventReader { + /// Creates a new reader, consuming the given stream. The reader should be wrapped in a `BufReader`, otherwise parsing may be very slow. + #[inline] + pub fn new(source: R) -> Self { + Self::new_with_config(source, ParserConfig::new()) + } + + /// Creates a new reader with the provded configuration, consuming the given stream. The reader should be wrapped in a `BufReader`, otherwise parsing may be very slow. + #[inline] + pub fn new_with_config(source: R, config: impl Into) -> Self { + Self { + source, + parser: PullParser::new(config), + } + } + + /// Pulls and returns next XML event from the stream. + /// + /// If this returns [Err] or [`XmlEvent::EndDocument`] then further calls to + /// this method will return this event again. + #[inline] + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> Result { + self.parser.next(&mut self.source) + } + + /// Skips all XML events until the next end tag at the current level. + /// + /// Convenience function that is useful for the case where you have + /// encountered a start tag that is of no interest and want to + /// skip the entire XML subtree until the corresponding end tag. + #[inline] + pub fn skip(&mut self) -> Result<()> { + let mut depth = 1; + + while depth > 0 { + match self.next()? { + XmlEvent::StartElement { .. } => depth += 1, + XmlEvent::EndElement { .. } => depth -= 1, + XmlEvent::EndDocument => return Err(Error { + kind: ErrorKind::UnexpectedEof, + pos: self.parser.position(), + }), + _ => {}, + } + } + + Ok(()) + } + + /// Access underlying reader + /// + /// Using it directly while the event reader is parsing is not recommended + pub fn source(&self) -> &R { &self.source } + + /// Access underlying reader + /// + /// Using it directly while the event reader is parsing is not recommended + pub fn source_mut(&mut self) -> &mut R { &mut self.source } + + /// Unwraps this `EventReader`, returning the underlying reader. + /// + /// Note that this operation is destructive; unwrapping the reader and wrapping it + /// again with `EventReader::new()` will create a fresh reader which will attempt + /// to parse an XML document from the beginning. + pub fn into_inner(self) -> R { + self.source + } + + /// Returns the DOCTYPE of the document if it has already been seen + /// + /// Available only after the `Doctype` event + #[inline] + #[deprecated(note = "there is `XmlEvent::Doctype` now")] + #[allow(deprecated)] + pub fn doctype(&self) -> Option<&str> { + self.parser.doctype() + } + + /// Returns PUBLIC/SYSTEM DOCTYPE IDs if it has already been seen + /// + /// Available only after the `Doctype` event + #[inline] + pub fn doctype_ids(&self) -> Option> { + self.parser.doctype_ids() + } + + /// Add new entity definitions **before any XML elements have been parsed**. + /// + /// ## Errors + /// + /// It's valid to call this after DOCTYPE, but not later. It won't be possible to add entities to a document without either XML decl or DOCTYPE. + /// + /// It will fail if the document is declared as _standalone_. + #[inline] + pub fn add_entities, T: Into>(&mut self, entities: impl IntoIterator) -> result::Result<(), ImmutableEntitiesError> { + self.parser.add_entities(entities) + } +} + +impl Position for EventReader { + /// Returns the position of the last event produced by the reader. + #[inline] + fn position(&self) -> TextPosition { + self.parser.position() + } +} + +impl IntoIterator for EventReader { + type IntoIter = Events; + type Item = Result; + + fn into_iter(self) -> Events { + Events { reader: self, finished: false } + } +} + +impl Clone for EventReader { + fn clone(&self) -> Self { + Self { + source: self.source.clone(), + parser: self.parser.clone() + } + } +} + +/// An iterator over XML events created from some type implementing `Read`. +/// +/// When the next event is `xml::event::Error` or `xml::event::EndDocument`, then +/// it will be returned by the iterator once, and then it will stop producing events. +pub struct Events { + reader: EventReader, + finished: bool, +} + +impl Events { + /// Unwraps the iterator, returning the internal `EventReader`. + #[inline] + pub fn into_inner(self) -> EventReader { + self.reader + } + + /// Access the underlying reader + /// + /// It's not recommended to use it while the events are still being parsed + pub fn source(&self) -> &R { &self.reader.source } + + /// Access the underlying reader + /// + /// It's not recommended to use it while the events are still being parsed + pub fn source_mut(&mut self) -> &mut R { &mut self.reader.source } +} + +impl std::ops::Deref for Events { + type Target = EventReader; + + fn deref(&self) -> &Self::Target { + &self.reader + } +} + +impl std::ops::DerefMut for Events { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.reader + } +} + +impl FusedIterator for Events { +} + +impl Iterator for Events { + type Item = Result; + + #[inline] + fn next(&mut self) -> Option> { + if self.finished && !self.reader.parser.is_ignoring_end_of_stream() { + None + } else { + let ev = self.reader.next(); + if let Ok(XmlEvent::EndDocument) | Err(_) = ev { + self.finished = true; + } + Some(ev) + } + } +} + +impl<'r> EventReader<&'r [u8]> { + /// A convenience method to create an `XmlReader` from a string slice. + #[inline] + #[must_use] + #[allow(clippy::should_implement_trait)] + pub fn from_str(source: &'r str) -> Self { + EventReader::new(source.as_bytes()) + } +} diff --git a/vendor/xml/src/reader/amplification.rs b/vendor/xml/src/reader/amplification.rs new file mode 100644 index 00000000000..d07b1f98d87 --- /dev/null +++ b/vendor/xml/src/reader/amplification.rs @@ -0,0 +1,106 @@ +//! Cumulative entity accounting, independent of the fixed depth and queue limits. + +/// Immutable limits for cumulative entity expansion. The factor uses the same +/// single-precision ratio as Expat: `(direct + indirect) / direct`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AmplificationLimits { + maximum_factor_bits: u32, + activation_threshold: u64, +} + +impl AmplificationLimits { + /// Rejects NaN and factors below one. Positive infinity is permitted. + #[must_use] + pub fn new(maximum_factor: f32, activation_threshold: u64) -> Option { + if maximum_factor.is_nan() || maximum_factor < 1.0 { + return None; + } + Some(Self { + maximum_factor_bits: maximum_factor.to_bits(), + activation_threshold, + }) + } +} + +#[derive(Clone)] +pub(crate) struct Accounting { + limits: Option, + pub(crate) direct: u64, + indirect: u64, + overflow: bool, +} + +impl Accounting { + pub(crate) fn new(limits: Option) -> Self { + Self { + limits, + direct: 0, + indirect: 0, + overflow: false, + } + } + + pub(crate) fn add_direct(&mut self, bytes: usize) { + self.direct = self.direct.checked_add(bytes as u64).unwrap_or_else(|| { + self.overflow = true; + u64::MAX + }); + } + + pub(crate) fn add_indirect(&mut self, bytes: usize) { + self.indirect = self.indirect.checked_add(bytes as u64).unwrap_or_else(|| { + self.overflow = true; + u64::MAX + }); + } + + pub(crate) fn tolerated(&self) -> bool { + self.tolerated_at_direct(self.direct) + } + + pub(crate) fn tolerated_at_direct(&self, direct: u64) -> bool { + let Some(limits) = self.limits else { + return true; + }; + let Some(total) = direct.checked_add(self.indirect) else { + return false; + }; + if self.overflow { + return false; + } + if total < limits.activation_threshold { + return true; + } + let factor = if direct != 0 { + total as f32 / direct as f32 + } else { + // Expat uses the shortest possible external entity declaration. + (22.0 + self.indirect as f32) / 22.0 + }; + factor <= f32::from_bits(limits.maximum_factor_bits) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn threshold_factor_and_overflow() { + assert!(AmplificationLimits::new(f32::NAN, 0).is_none()); + assert!(AmplificationLimits::new(0.99, 0).is_none()); + let mut a = Accounting::new(AmplificationLimits::new(1.0, 10)); + a.add_direct(5); + a.add_indirect(4); + assert!(a.tolerated()); + a.add_indirect(1); + assert!(!a.tolerated()); + a.limits = AmplificationLimits::new(f32::INFINITY, 0); + assert!(a.tolerated()); + a.direct = u64::MAX; + assert!(!a.tolerated()); + a.indirect = 0; + a.add_direct(1); + assert!(!a.tolerated()); + } +} diff --git a/vendor/xml/src/reader/amplification_tests.rs b/vendor/xml/src/reader/amplification_tests.rs new file mode 100644 index 00000000000..d7c5554651b --- /dev/null +++ b/vendor/xml/src/reader/amplification_tests.rs @@ -0,0 +1,129 @@ +use super::{AmplificationLimits, ErrorKind, ParserConfig, XmlEvent}; + +fn parse(source: &[u8], threshold: u64, factor: f32) -> (Option, String, Vec) { + let mut config = ParserConfig::new().coalesce_characters(false); + config.amplification_limits = AmplificationLimits::new(factor, threshold); + let mut reader = config.create_reader(source); + let mut text = String::new(); + let mut attributes = Vec::new(); + loop { + match reader.next() { + Ok(XmlEvent::Characters(s) | XmlEvent::Whitespace(s)) => text.push_str(&s), + Ok(XmlEvent::StartElement { + attributes: attrs, .. + }) => { + attributes.extend(attrs.into_iter().map(|a| a.value)); + } + Ok(XmlEvent::EndDocument) => return (None, text, attributes), + Ok(_) => {} + Err(error) => { + assert_eq!(reader.next().unwrap_err(), error); + return (Some(error.kind().clone()), text, attributes); + } + } + } +} + +#[test] +fn attribute_frontier_and_predefined_deferral() { + let input = br#"]>"#; + assert_eq!(input.len(), 44); + let (error, _, attributes) = parse(input, 0, 1.07); + assert!(error.is_none()); + assert_eq!(attributes, ["xyz"]); + assert!(matches!( + parse(input, 0, 1.0).0, + Some(ErrorKind::AmplificationLimit) + )); + assert!(parse(br#""#, 0, 1.0).0.is_none()); + let (error, text, _) = parse(b"&\nxxxxxxxxxxxxxxxxxxxx", 0, 1.1); + assert!(matches!(error, Some(ErrorKind::AmplificationLimit))); + assert_eq!(text, "&"); +} + +#[test] +fn numeric_references_and_attribute_normalization() { + let (error, text, _) = parse(b"A", 0, 1.0); + assert!(error.is_none()); + assert_eq!(text, "A"); + let (_, _, attrs) = parse( + br#"]>"#, + 0, + 100.0, + ); + assert_eq!(attrs, [" "]); + let (_, _, attrs) = parse( + br#"]>"#, + 0, + 100.0, + ); + assert_eq!(attrs, [" "]); + let (_, _, attrs) = parse( + b"]>", + 0, + 100.0, + ); + assert_eq!(attrs, [" "]); + for input in [ + br#"]>"#.as_slice(), + br#"]>"#.as_slice(), + ] { + assert!(matches!( + parse(input, 0, 100.0).0, + Some(ErrorKind::Syntax(_)) + )); + } +} + +#[test] +fn cdata_start_checks_deferred_predefined_bytes() { + let input = b"&"; + let (error, text, _) = parse(input, 0, 1.05); + assert!(matches!(error, Some(ErrorKind::AmplificationLimit))); + assert_eq!(text, "&"); + let mut config = ParserConfig::new().cdata_to_characters(true); + config.amplification_limits = AmplificationLimits::new(1.05, 0); + let result = config + .create_reader(input.as_slice()) + .into_iter() + .collect::, _>>(); + assert!(matches!( + result.unwrap_err().kind(), + ErrorKind::AmplificationLimit + )); +} + +#[test] +fn encoded_input_uses_raw_bytes() { + let source = "]>&b;"; + assert!(matches!( + parse(source.as_bytes(), 0, 1.1).0, + Some(ErrorKind::AmplificationLimit) + )); + let utf16: Vec = [0xff, 0xfe] + .into_iter() + .chain(source.encode_utf16().flat_map(u16::to_le_bytes)) + .collect(); + let (error, text, _) = parse(&utf16, 0, 1.1); + assert!(error.is_none()); + assert_eq!(text, "xyz"); +} + +#[test] +fn fixed_caps_remain_independent_and_measure_nesting() { + let sibling = "]>".to_owned() + &"&a;".repeat(20) + ""; + assert_eq!(parse(sibling.as_bytes(), 10000, 1.0).1, "x".repeat(20)); + for depth in [4, 11, 20] { + let mut source = "".to_owned(); + for n in 1..=depth { + source += &format!("", n - 1); + } + source += &format!("]>&a{depth};"); + let (error, _, _) = parse(source.as_bytes(), 10000, f32::INFINITY); + if depth == 4 { + assert!(error.is_none()); + } else { + assert!(matches!(error, Some(ErrorKind::Syntax(_)))); + } + } +} diff --git a/vendor/xml/src/reader/config.rs b/vendor/xml/src/reader/config.rs new file mode 100644 index 00000000000..1a8a314567d --- /dev/null +++ b/vendor/xml/src/reader/config.rs @@ -0,0 +1,312 @@ +//! Contains parser configuration structure. +use std::collections::HashMap; +use std::io::Read; + +use crate::reader::EventReader; +use crate::util::Encoding; + +/// Limits to defend from billion laughs attack +const DEFAULT_MAX_ENTITY_EXPANSION_LENGTH: usize = 1_000_000; +const DEFAULT_MAX_ENTITY_EXPANSION_DEPTH: u8 = 10; + +/// Parser configuration structure. **There are more config methods than public fileds — see methods below**. +/// +/// This structure contains various configuration options which affect +/// behavior of the parser. +#[derive(Clone, PartialEq, Eq, Debug)] +#[non_exhaustive] +pub struct ParserConfig { + /// Whether or not should whitespace in textual events be removed. Default is false. + /// + /// When true, all standalone whitespace will be removed (this means no + /// `Whitespace` events will be emitted), and leading and trailing whitespace + /// from `Character` events will be deleted. If after trimming `Characters` + /// event will be empty, it will also be omitted from output stream. This is + /// possible, however, only if `whitespace_to_characters` or + /// `cdata_to_characters` options are set. + /// + /// This option does not affect CDATA events, unless `cdata_to_characters` + /// option is also set. In that case CDATA content will also be trimmed. + pub trim_whitespace: bool, + + /// Whether or not should whitespace be converted to characters. + /// Default is false. + /// + /// If true, instead of `Whitespace` events `Characters` events with the + /// same content will be emitted. If `trim_whitespace` is also true, these + /// events will be trimmed to nothing and, consequently, not emitted. + pub whitespace_to_characters: bool, + + /// Whether or not should CDATA be converted to characters. + /// Default is false. + /// + /// If true, instead of `CData` events `Characters` events with the same + /// content will be emitted. If `trim_whitespace` is also true, these events + /// will be trimmed. If corresponding CDATA contained nothing but whitespace, + /// this event will be omitted from the stream. + pub cdata_to_characters: bool, + + /// Whether or not should comments be omitted. Default is true. + /// + /// If true, `Comment` events will not be emitted at all. + pub ignore_comments: bool, + + /// Whether or not should sequential `Characters` events be merged. + /// Default is true. + /// + /// If true, multiple sequential `Characters` events will be merged into + /// a single event, that is, their data will be concatenated. + /// + /// Multiple sequential `Characters` events are only possible if either + /// `cdata_to_characters` or `ignore_comments` are set. Otherwise character + /// events will always be separated by other events. + pub coalesce_characters: bool, + + /// A map of extra entities recognized by the parser. Default is an empty map. + /// + /// By default the XML parser recognizes the entities defined in the XML spec. Sometimes, + /// however, it is convenient to make the parser recognize additional entities which + /// are also not available through the DTD definitions (especially given that at the moment + /// DTD parsing is not supported). + pub extra_entities: HashMap, + + /// Whether or not the parser should ignore the end of stream. Default is false. + /// + /// By default the parser will either error out when it encounters a premature end of + /// stream or complete normally if the end of stream was expected. If you want to continue + /// reading from a stream whose input is supplied progressively, you can set this option to true. + /// In this case the parser will allow you to invoke the `next()` method even if a supposed end + /// of stream has happened. + /// + /// Note that support for this functionality is incomplete; for example, the parser will fail if + /// the premature end of stream happens inside PCDATA. Therefore, use this option at your own risk. + pub ignore_end_of_stream: bool, + + /// Whether or not non-unicode entity references get replaced with the replacement character + /// + /// When true, any decimal or hexadecimal character reference that cannot be converted from a + /// u32 to a char using [std::char::from_u32](https://doc.rust-lang.org/std/char/fn.from_u32.html) + /// will be converted into the unicode REPLACEMENT CHARACTER (U+FFFD). + pub replace_unknown_entity_references: bool, + + /// Whether or not whitespace at the root level of the document is ignored. Default is true. + /// + /// By default any whitespace that is not enclosed within at least one level of elements will be + /// ignored. Setting this value to false will cause root level whitespace events to be emitted. + pub ignore_root_level_whitespace: bool, + + /// Use this encoding as the default. Necessary for UTF-16 files without BOM. + pub override_encoding: Option, + + /// Allow `` to contain unsupported encoding names, + /// and interpret them as Latin1 instead. This will mangle non-ASCII characters, but usually it won't fail parsing. + pub ignore_invalid_encoding_declarations: bool, + + /// Documents with multiple root elements are ill-formed + pub allow_multiple_root_elements: bool, + + /// Abort if custom entities create a string longer than this + pub max_entity_expansion_length: usize, + + /// Entities can expand into other entities this many times (be careful about exponential cost!) + pub max_entity_expansion_depth: u8, + + /// Optional cumulative entity byte limits, independent of the fixed caps. + pub amplification_limits: Option, + + /// Maximum length of tag name or attribute name + pub max_name_length: usize, + + /// Max number of attributes per element + pub max_attributes: usize, + + /// Max number of bytes in each attribute + pub max_attribute_length: usize, + + /// Maximum length of strings reprsenting characters, comments, and processing instructions + pub max_data_length: usize, +} + +impl ParserConfig { + /// Returns a new config with default values. + /// + /// You can tweak default values using builder-like pattern: + /// + /// ```rust + /// use xml::reader::ParserConfig; + /// + /// let config = ParserConfig::new() + /// .trim_whitespace(true) + /// .ignore_comments(true) + /// .coalesce_characters(false); + /// ``` + #[must_use] + #[inline] + pub fn new() -> Self { + Self { + trim_whitespace: false, + whitespace_to_characters: false, + cdata_to_characters: false, + ignore_comments: true, + coalesce_characters: true, + extra_entities: HashMap::new(), + ignore_end_of_stream: false, + replace_unknown_entity_references: false, + ignore_root_level_whitespace: true, + + override_encoding: None, + ignore_invalid_encoding_declarations: false, + allow_multiple_root_elements: true, + max_entity_expansion_length: DEFAULT_MAX_ENTITY_EXPANSION_LENGTH, + max_entity_expansion_depth: DEFAULT_MAX_ENTITY_EXPANSION_DEPTH, + amplification_limits: None, + max_attributes: 1 << 16, + max_attribute_length: 1 << 30, + max_data_length: 1 << 30, + max_name_length: 1 << 18, + } + } + + /// Creates an XML reader with this configuration. The reader should be wrapped in a `BufReader`, otherwise parsing may be very slow. + /// + /// This is a convenience method for configuring and creating a reader at the same time: + /// + /// ```rust + /// use xml::reader::ParserConfig; + /// + /// let mut source: &[u8] = b"..."; + /// + /// let reader = ParserConfig::new() + /// .trim_whitespace(true) + /// .ignore_comments(true) + /// .coalesce_characters(false) + /// .create_reader(&mut source); + /// ``` + /// + /// This method is exactly equivalent to calling `EventReader::new_with_config()` with + /// this configuration object. + #[inline] + pub fn create_reader(self, source: R) -> EventReader { + EventReader::new_with_config(source, self) + } + + /// Adds a new entity mapping and returns an updated config object. + /// + /// This is a convenience method for adding external entities mappings to the XML parser. + /// An example: + /// + /// ```rust + /// use xml::reader::ParserConfig; + /// + /// let mut source: &[u8] = b"..."; + /// + /// let reader = ParserConfig::new() + /// .add_entity("nbsp", " ") + /// .add_entity("copy", "©") + /// .add_entity("reg", "®") + /// .create_reader(&mut source); + /// ``` + #[must_use] + #[inline] + pub fn add_entity, T: Into>(mut self, entity: S, value: T) -> Self { + self.extra_entities.insert(entity.into(), value.into()); + self + } + + /// Adds entities and returns an updated config object. + /// + /// This is a convenience method for adding external entities mappings to the XML parser. + /// An example: + /// + /// ```rust + /// use xml::reader::ParserConfig; + /// + /// let mut source: &[u8] = b"..."; + /// + /// let reader = ParserConfig::new() + /// .add_entities([ + /// ("nbsp", " "), + /// ("copy", "©"), + /// ("reg", "®"), + /// ]) + /// .create_reader(&mut source); + /// ``` + #[must_use] + #[inline] + pub fn add_entities, T: Into>(mut self, entities: impl IntoIterator) -> Self { + self.extra_entities.extend(entities.into_iter().map(|(k, v)| (k.into(), v.into()))); + self + } +} + +gen_setters! { ParserConfig, + trim_whitespace: val bool, + whitespace_to_characters: val bool, + cdata_to_characters: val bool, + ignore_comments: val bool, + coalesce_characters: val bool, + ignore_end_of_stream: val bool, + replace_unknown_entity_references: val bool, + /// Whether or not whitespace at the root level of the document is ignored. Default is true. + ignore_root_level_whitespace: val bool +} + +impl Default for ParserConfig { + fn default() -> Self { + Self::new() + } +} + +impl ParserConfig { + /// Read character encoding from `Content-Type` header. + /// Set this when parsing XML documents fetched over HTTP. + /// + /// `text/*` MIME types do *not* imply latin1. UTF-8 is always the default fallback. + #[must_use] + pub fn content_type(mut self, mime_type: &str) -> Self { + let charset = mime_type.split_once(';') + .and_then(|(_, args)| args.split_once("charset")) + .and_then(|(_, args)| args.split_once('=')); + if let Some((_, charset)) = charset { + let name = charset.trim().trim_matches('"'); + if let Ok(enc) = name.parse() { + self.override_encoding = Some(enc); + } + } + self + } +} + +gen_setters! { ParserConfig, + /// Set if you got one in the HTTP header + override_encoding: val Option, + /// Allows invalid documents. There should be only a single root element in XML. + allow_multiple_root_elements: val bool, + /// Abort if custom entities create a string longer than this + max_entity_expansion_length: val usize, + /// Entities can expand into other entities this many times (be careful about exponential cost!) + max_entity_expansion_depth: val u8, + /// Max number of attributes per element + max_attributes: val usize, + /// Maximum length of tag name or attribute name + max_name_length: val usize, + /// Max number of bytes in each attribute + max_attribute_length: val usize, + /// Maximum length of strings reprsenting characters, comments, and processing instructions + max_data_length: val usize, + /// Allow `` + ignore_invalid_encoding_declarations: val bool +} + +#[test] +fn mime_parse() { + let c = ParserConfig::new() + .content_type("text/xml;charset=Us-AScii") + .max_entity_expansion_length(1000); + assert_eq!(c.override_encoding, Some(Encoding::Ascii)); + + let c = ParserConfig::new() + .max_entity_expansion_depth(3) + .content_type("text/xml;charset = \"UTF-16\""); + assert_eq!(c.override_encoding, Some(Encoding::Utf16)); +} diff --git a/vendor/xml/src/reader/error.rs b/vendor/xml/src/reader/error.rs new file mode 100644 index 00000000000..978f6858b88 --- /dev/null +++ b/vendor/xml/src/reader/error.rs @@ -0,0 +1,323 @@ +use crate::reader::lexer::Token; +use crate::writer::Error as EmitterError; +use crate::Encoding; + +use std::borrow::Cow; +use std::error::Error as _; +use std::{error, fmt, io, str}; + +use crate::common::{Position, TextPosition}; +use crate::util; + +/// Failure reason +#[derive(Debug)] +#[non_exhaustive] +pub enum ErrorKind { + /// This is an ill-formed XML document + Syntax(Cow<'static, str>), + /// Reader/writer reported an error + Io(io::Error), + /// The document contains bytes that are not allowed in UTF-8 strings + Utf8(str::Utf8Error), + /// The document ended while they were elements/comments/etc. still open + UnexpectedEof, + /// Cumulative entity expansion exceeded its configured byte ratio. + AmplificationLimit, + /// [Writer error](crate::writer::Error) for convenience of using a single [`Error`] type + EmitterError(Box), +} + +/// Returned by `add_entities()` +#[derive(Clone, PartialEq)] +#[non_exhaustive] +pub enum ImmutableEntitiesError { + /// Too late to modify + ElementEncountered, + /// `` can't have entities + StandaloneDocument, +} + +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub(crate) enum SyntaxError { + CannotRedefineXmlnsPrefix, + CannotRedefineXmlPrefix, + /// Recursive custom entity expanded to too many chars, it could be DoS + EntityTooBig, + EmptyEntity, + NoRootElement, + ProcessingInstructionWithoutName, + UnbalancedRootElement, + UnexpectedEof, + UnexpectedOpeningTag, + /// Missing `]]>` + UnclosedCdata, + UnexpectedQualifiedName(Token), + UnexpectedTokenOutsideRoot(Token), + UnexpectedToken(Token), + UnexpectedTokenInEntity(Token), + UnexpectedTokenInClosingTag(Token), + UnexpectedTokenInOpeningTag(Token), + InvalidQualifiedName(Box), + UnboundAttribute(Box), + UnboundElementPrefix(Box), + UnexpectedClosingTag(Box), + UnexpectedName(Box), + /// Found , Token), + CannotUndefinePrefix(Box), + InvalidCharacterEntity(u32), + InvalidDefaultNamespace(Box), + InvalidNamePrefix(Box), + InvalidNumericEntity(Box), + InvalidStandaloneDeclaration(Box), + InvalidXmlProcessingInstruction(Box), + RedefinedAttribute(Box), + UndefinedEntity(Box), + UnexpectedEntity(Box), + UnexpectedNameInsideXml(Box), + UnsupportedEncoding(Box), + /// In DTD + UnknownMarkupDeclaration(Box), + UnexpectedXmlVersion(Box), + ConflictingEncoding(Encoding, Encoding), + UnexpectedTokenBefore(&'static str, char), + /// Document has more stuff than `ParserConfig` allows + ExceededConfiguredLimit, +} + +impl fmt::Display for SyntaxError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.to_cow().fmt(f) + } +} + +impl SyntaxError { + #[inline(never)] + #[cold] + pub(crate) fn to_cow(&self) -> Cow<'static, str> { + match *self { + Self::CannotRedefineXmlnsPrefix => "Cannot redefine XMLNS prefix".into(), + Self::CannotRedefineXmlPrefix => "Default XMLNS prefix cannot be rebound to another value".into(), + Self::EmptyEntity => "Encountered empty entity".into(), + Self::EntityTooBig => "Entity too big".into(), + Self::NoRootElement => "Unexpected end of stream: no root element found".into(), + Self::ProcessingInstructionWithoutName => "Encountered processing instruction without a name".into(), + Self::UnbalancedRootElement => "Unexpected end of stream: still inside the root element".into(), + Self::UnclosedCdata => "Unclosed "Unexpected end of stream".into(), + Self::UnexpectedOpeningTag => "'<' is not allowed in attributes".into(), + Self::CannotUndefinePrefix(ref ln) => format!("Cannot undefine prefix '{ln}'").into(), + Self::ConflictingEncoding(a, b) => format!("Declared encoding {a}, but uses {b}").into(), + Self::InvalidCharacterEntity(num) => format!("Invalid character U+{num:04X}").into(), + Self::InvalidDefaultNamespace(ref name) => format!("Namespace '{name}' cannot be default").into(), + Self::InvalidNamePrefix(ref prefix) => format!("'{prefix}' cannot be an element name prefix").into(), + Self::InvalidNumericEntity(ref v) => format!("Invalid numeric entity: {v}").into(), + Self::InvalidQualifiedName(ref e) => format!("Qualified name is invalid: {e}").into(), + Self::InvalidStandaloneDeclaration(ref value) => format!("Invalid standalone declaration value: {value}").into(), + Self::InvalidXmlProcessingInstruction(ref name) => format!("Invalid processing instruction: format!("Attribute '{name}' is redefined").into(), + Self::UnboundAttribute(ref name) => format!("Attribute {name} prefix is unbound").into(), + Self::UnboundElementPrefix(ref name) => format!("Element {name} prefix is unbound").into(), + Self::UndefinedEntity(ref v) => format!("Undefined entity: {v}").into(), + Self::UnexpectedClosingTag(ref expected_got) => format!("Unexpected closing tag: {expected_got}").into(), + Self::UnexpectedEntity(ref name) => format!("Unexpected entity: {name}").into(), + Self::UnexpectedName(ref name) => format!("Unexpected name: {name}").into(), + Self::UnexpectedNameInsideXml(ref name) => format!("Unexpected name inside XML declaration: {name}").into(), + Self::UnexpectedProcessingInstruction(ref buf, token) => format!("Unexpected token inside processing instruction: format!("Unexpected token inside qualified name: {e}").into(), + Self::UnexpectedToken(token) => format!("Unexpected token: {token}").into(), + Self::UnexpectedTokenBefore(before, c) => format!("Unexpected token '{before}' before '{c}'").into(), + Self::UnexpectedTokenInClosingTag(token) => format!("Unexpected token inside closing tag: {token}").into(), + Self::UnexpectedTokenInEntity(token) => format!("Unexpected token inside entity: {token}").into(), + Self::UnexpectedTokenInOpeningTag(token) => format!("Unexpected token inside opening tag: {token}").into(), + Self::UnexpectedTokenOutsideRoot(token) => format!("Unexpected characters outside the root element: {token}").into(), + Self::UnexpectedXmlVersion(ref version) => format!("Invalid XML version: {version}").into(), + Self::UnknownMarkupDeclaration(ref v) => format!("Unknown markup declaration: {v}").into(), + Self::UnsupportedEncoding(ref v) => format!("Unsupported encoding: {v}").into(), + Self::ExceededConfiguredLimit => "This document is larger/more complex than allowed by the parser's configuration".into(), + } + } +} + +/// An XML parsing error. +/// +/// Consists of a 2D position in a document and a textual message describing the error. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Error { + pub(crate) pos: TextPosition, + pub(crate) kind: ErrorKind, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use self::ErrorKind::{AmplificationLimit, EmitterError, Io, Syntax, UnexpectedEof, Utf8}; + + write!(f, "{} ", self.pos)?; + match &self.kind { + Io(io_error) => io_error.fmt(f), + Utf8(reason) => reason.fmt(f), + Syntax(msg) => f.write_str(msg), + UnexpectedEof => f.write_str("Unexpected EOF"), + AmplificationLimit => f.write_str("limit on input amplification factor (from DTD and entities) breached"), + EmitterError(e) => e.fmt(f), + } + } +} + +impl fmt::Display for ImmutableEntitiesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::ElementEncountered => "Element encountered", + Self::StandaloneDocument => "Standalone XML", + }) + } +} + +impl fmt::Debug for ImmutableEntitiesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl error::Error for ImmutableEntitiesError { +} + +impl Position for Error { + #[inline] + fn position(&self) -> TextPosition { self.pos } +} + +impl Error { + #[doc(hidden)] + #[must_use] + pub fn msg(&self) -> String { + self.to_string() + } + + /// Failure reason + #[must_use] + #[inline] + pub fn kind(&self) -> &ErrorKind { + &self.kind + } + + pub(crate) fn syntax(syntax_msg: Cow<'static, str>, pos: TextPosition) -> Self { + Self { + kind: ErrorKind::Syntax(syntax_msg), + pos + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match &self.kind { + ErrorKind::Io(e) => e.source(), + ErrorKind::Utf8(e) => Some(e), + ErrorKind::EmitterError(e) => Some(e), + _ => None, + } + } +} + +impl<'a, P, M> From<(&'a P, M)> for Error where P: Position, M: Into> { + #[cold] + fn from(orig: (&'a P, M)) -> Self { + Self { + pos: orig.0.position(), + kind: ErrorKind::Syntax(orig.1.into()), + } + } +} + +impl From for Error { + #[cold] + fn from(e: util::CharReadError) -> Self { + use crate::util::CharReadError::{Io, UnexpectedEof, Utf8}; + Self { + pos: TextPosition::new(), + kind: match e { + UnexpectedEof => ErrorKind::UnexpectedEof, + Utf8(reason) => ErrorKind::Utf8(reason), + Io(io_error) => ErrorKind::Io(io_error), + }, + } + } +} + +impl From for Error { + #[cold] + fn from(e: io::Error) -> Self { + Self { + pos: TextPosition::new(), + kind: ErrorKind::Io(e), + } + } +} + +impl From for Error { + #[cold] + fn from(e: EmitterError) -> Self { + Self { + pos: TextPosition::new(), + kind: ErrorKind::EmitterError(Box::new(e)), + } + } +} + +impl From for Error { + #[cold] + fn from(e: ImmutableEntitiesError) -> Self { + Self { + pos: TextPosition::new(), + kind: ErrorKind::Io(io::Error::new(io::ErrorKind::Other, e)), + } + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Self { + Self { kind, pos: TextPosition::new() } + } +} + +impl Clone for ErrorKind { + #[cold] + fn clone(&self) -> Self { + use self::ErrorKind::{AmplificationLimit, EmitterError, Io, Syntax, UnexpectedEof, Utf8}; + match self { + UnexpectedEof => UnexpectedEof, + AmplificationLimit => AmplificationLimit, + Utf8(reason) => Utf8(*reason), + Io(io_error) => Io(io::Error::new(io_error.kind(), io_error.to_string())), + Syntax(msg) => Syntax(msg.clone()), + EmitterError(e) => EmitterError(e.clone()), + } + } +} + +impl PartialEq for ErrorKind { + #[allow(deprecated)] + fn eq(&self, other: &Self) -> bool { + use self::ErrorKind::{Io, Syntax, UnexpectedEof, Utf8}; + match (self, other) { + (UnexpectedEof, UnexpectedEof) => true, + (Self::AmplificationLimit, Self::AmplificationLimit) => true, + (Utf8(left), Utf8(right)) => left == right, + (Io(left), Io(right)) => + left.kind() == right.kind() && + left.description() == right.description(), + (Syntax(left), Syntax(right)) => + left == right, + (_, _) => false, + } + } +} +impl Eq for ErrorKind {} + +#[test] +fn err_size() { + assert!(std::mem::size_of::() <= 24); +} diff --git a/vendor/xml/src/reader/events.rs b/vendor/xml/src/reader/events.rs new file mode 100644 index 00000000000..abe68230394 --- /dev/null +++ b/vendor/xml/src/reader/events.rs @@ -0,0 +1,270 @@ +//! Contains `XmlEvent` datatype, instances of which are emitted by the parser. + +use crate::attribute::OwnedAttribute; +use crate::common::XmlVersion; +use crate::name::OwnedName; +use crate::namespace::Namespace; +use std::fmt; + +/// An element of an XML input stream. +/// +/// Items of this enum are emitted by `reader::EventReader`. They correspond to different +/// elements of an XML document. +#[derive(PartialEq, Clone)] +pub enum XmlEvent { + /// Corresponds to XML document declaration. + /// + /// This event is always emitted before any other event. It is emitted + /// even if the actual declaration is not present in the document. + StartDocument { + /// XML version. + /// + /// If XML declaration is not present, defaults to `Version10`. + version: XmlVersion, + + /// XML document encoding. + /// + /// If XML declaration is not present or does not contain `encoding` attribute, + /// defaults to `"UTF-8"`. This field is currently used for no other purpose than + /// informational. + encoding: String, + + /// XML standalone declaration. + /// + /// If XML document is not present or does not contain `standalone` attribute, + /// defaults to `None`. This field is currently used for no other purpose than + /// informational. + standalone: Option, + }, + + /// Denotes to the end of the document stream. + /// + /// This event is always emitted after any other event (except `Error`). After it + /// is emitted for the first time, it will always be emitted on next event pull attempts. + EndDocument, + + /// Denotes an XML processing instruction. + /// + /// This event contains a processing instruction target (`name`) and opaque `data`. It + /// is up to the application to process them. + ProcessingInstruction { + /// Processing instruction target. + name: String, + + /// Processing instruction content. + data: Option, + }, + + /// Denotes a beginning of an XML element. + /// + /// This event is emitted after parsing opening tags or after parsing bodiless tags. In the + /// latter case `EndElement` event immediately follows. + StartElement { + /// Qualified name of the element. + name: OwnedName, + + /// A list of attributes associated with the element. + /// + /// Currently attributes are not checked for duplicates (TODO) + attributes: Vec, + + /// Contents of the namespace mapping at this point of the document. + namespace: Namespace, + }, + + /// Denotes an end of an XML element. + /// + /// This event is emitted after parsing closing tags or after parsing bodiless tags. In the + /// latter case it is emitted immediately after corresponding `StartElement` event. + EndElement { + /// Qualified name of the element. + name: OwnedName, + }, + + /// Denotes CDATA content. + /// + /// This event contains unparsed data. No unescaping will be performed. + /// + /// It is possible to configure a parser to emit `Characters` event instead of `CData`. See + /// `pull::ParserConfiguration` structure for more information. + CData(String), + + /// Denotes a comment. + /// + /// It is possible to configure a parser to ignore comments, so this event will never be emitted. + /// See `pull::ParserConfiguration` structure for more information. + Comment(String), + + /// Denotes character data outside of tags. + /// + /// Contents of this event will always be unescaped, so no entities like `<` or `&` or `{` + /// will appear in it. + /// + /// It is possible to configure a parser to trim leading and trailing whitespace for this event. + /// See `pull::ParserConfiguration` structure for more information. + Characters(String), + + /// Denotes a chunk of whitespace outside of tags. + /// + /// It is possible to configure a parser to emit `Characters` event instead of `Whitespace`. + /// See `pull::ParserConfiguration` structure for more information. When combined with whitespace + /// trimming, it will eliminate standalone whitespace from the event stream completely. + Whitespace(String), + /// The whole DOCTYPE markup + Doctype { + /// Everything including `<` and `>` + syntax: String, + }, +} + +/// Supplement to the Doctype event (use the event if you want the full syntax) +pub struct DoctypeRef<'tmp> { + pub(crate) syntax: &'tmp str, + /// Doctype name, following `, + /// [System id](https://www.w3.org/TR/xml/#NT-ExternalID) of Doctype, if available + pub(crate) system_id: Option<&'tmp str>, +} + +impl DoctypeRef<'_> { + /// Doctype name, following &str { + self.name + } + + /// [Public id](https://www.w3.org/TR/xml/#NT-ExternalID) of Doctype, if available. + #[must_use] + pub fn public_id(&self) -> Option<&str> { + self.public_id + } + + /// [System id](https://www.w3.org/TR/xml/#NT-ExternalID) of Doctype, if available + #[must_use] + pub fn system_id(&self) -> Option<&str> { + self.system_id + } +} + +impl std::ops::Deref for DoctypeRef<'_> { + type Target = str; + + /// Don't use it. It's for back-compat with v0.8 + fn deref(&self) -> &Self::Target { + self.syntax + } +} + +impl fmt::Debug for XmlEvent { + #[cold] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::StartDocument { version, encoding, standalone } => + write!(f, "StartDocument({}, {}, {:?})", version, *encoding, standalone), + Self::EndDocument => + write!(f, "EndDocument"), + Self::ProcessingInstruction { name, data } => + write!(f, "ProcessingInstruction({}{})", *name, match data { + Some(data) => format!(", {data}"), + None => String::new() + }), + Self::StartElement { name, attributes, namespace: Namespace(namespace) } => + write!(f, "StartElement({}, {:?}{})", name, namespace, if attributes.is_empty() { + String::new() + } else { + let attributes: Vec = attributes.iter().map( + |a| format!("{} -> {}", a.name, a.value) + ).collect(); + format!(", [{}]", attributes.join(", ")) + }), + Self::EndElement { name } => + write!(f, "EndElement({name})"), + Self::Comment(data) => + write!(f, "Comment({data})"), + Self::CData(data) => + write!(f, "CData({data})"), + Self::Characters(data) => + write!(f, "Characters({data})"), + Self::Whitespace(data) => + write!(f, "Whitespace({data})"), + Self::Doctype { syntax } => + write!(f, "Doctype({syntax})"), + } + } +} + +impl XmlEvent { + /// Obtains a writer event from this reader event. + /// + /// This method is useful for streaming processing of XML documents where the output + /// is also an XML document. With this method it is possible to process some events + /// while passing other events through to the writer unchanged: + /// + /// ```rust + /// use std::str; + /// + /// use xml::reader::XmlEvent as ReaderEvent; + /// use xml::writer::XmlEvent as WriterEvent; + /// use xml::{EventReader, EventWriter}; + /// + /// let mut input: &[u8] = b"world"; + /// let mut output: Vec = Vec::new(); + /// + /// { + /// let mut reader = EventReader::new(&mut input); + /// let mut writer = EventWriter::new(&mut output); + /// + /// for e in reader { + /// match e.unwrap() { + /// ReaderEvent::Characters(s) => { + /// writer.write(WriterEvent::characters(&s.to_uppercase())).unwrap() + /// }, + /// e => { + /// if let Some(e) = e.as_writer_event() { + /// writer.write(e).unwrap() + /// } + /// }, + /// } + /// } + /// } + /// + /// assert_eq!( + /// str::from_utf8(&output).unwrap(), + /// r#"WORLD"# + /// ); + /// ``` + /// + /// Note that this API may change or get additions in future to improve its ergonomics. + #[must_use] + pub fn as_writer_event(&self) -> Option> { + match self { + Self::StartDocument { version, encoding, standalone } => + Some(crate::writer::events::XmlEvent::StartDocument { + version: *version, + encoding: Some(encoding), + standalone: *standalone + }), + Self::ProcessingInstruction { name, data } => + Some(crate::writer::events::XmlEvent::ProcessingInstruction { + name, + data: data.as_ref().map(|s| &**s) + }), + Self::StartElement { name, attributes, namespace } => + Some(crate::writer::events::XmlEvent::StartElement { + name: name.borrow(), + attributes: attributes.iter().map(|a| a.borrow()).collect(), + namespace: namespace.borrow(), + }), + Self::EndElement { name } => + Some(crate::writer::events::XmlEvent::EndElement { name: Some(name.borrow()) }), + Self::Comment(data) => Some(crate::writer::events::XmlEvent::Comment(data)), + Self::CData(data) => Some(crate::writer::events::XmlEvent::CData(data)), + Self::Characters(data) | + Self::Whitespace(data) => Some(crate::writer::events::XmlEvent::Characters(data)), + Self::Doctype { syntax, .. } => Some(crate::writer::events::XmlEvent::Doctype(syntax)), + Self::EndDocument => None, + } + } +} diff --git a/vendor/xml/src/reader/lexer.rs b/vendor/xml/src/reader/lexer.rs new file mode 100644 index 00000000000..94821b27d2b --- /dev/null +++ b/vendor/xml/src/reader/lexer.rs @@ -0,0 +1,1212 @@ +//! Contains simple lexer for XML documents. +//! +//! This module is for internal use. Use `xml::pull` module to do parsing. + +use crate::common::{is_name_char, is_whitespace_char, is_xml10_char, is_xml11_char, Position, TextPosition}; +use crate::reader::error::SyntaxError; +use crate::reader::Error; +use crate::util::{CharReader, Encoding}; +use std::collections::VecDeque; +use std::io::Read; +use std::{fmt, result}; + +use super::ParserConfig; + +/// `Token` represents a single lexeme of an XML document. These lexemes +/// are used to perform actual parsing. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub(crate) enum Token { + /// `` + ProcessingInstructionEnd, + /// `` + TagEnd, + /// `/>` + EmptyTagEnd, + /// `` + CommentEnd, + /// Any non-special character except whitespace. + Character(char), + /// `=` + EqualsSign, + /// `'` + SingleQuote, + /// `"` + DoubleQuote, + /// `` + CDataEnd, + /// `&` + ReferenceStart, + /// `;` + ReferenceEnd, + /// `) -> fmt::Result { + match *self { + Token::Character(c) => c.fmt(f), + other => match other { + Token::OpeningTagStart => "<", + Token::ProcessingInstructionStart => " " " "", + Token::CDataEnd => "]]>", + Token::ReferenceStart => "&", + Token::ReferenceEnd => ";", + Token::EqualsSign => "=", + Token::SingleQuote => "'", + Token::DoubleQuote => "\"", + Token::MarkupDeclarationStart => " { + debug_assert!(false); + "" + }, + }.fmt(f), + } + } +} + +impl Token { + pub const fn as_static_str(self) -> Option<&'static str> { + match self { + Self::OpeningTagStart => Some("<"), + Self::ProcessingInstructionStart => Some(" Some(" Some(" Some(""), + Self::CDataEnd => Some("]]>"), + Self::ReferenceStart => Some("&"), + Self::ReferenceEnd => Some(";"), + Self::EqualsSign => Some("="), + Self::SingleQuote => Some("'"), + Self::DoubleQuote => Some("\""), + _ => None + } + } + + // using String.push_str(token.to_string()) is simply way too slow + pub fn push_to_string(self, target: &mut String) { + match self { + Self::Character(c) => { + debug_assert!(is_xml10_char(c) || is_xml11_char(c)); + target.push(c); + }, + _ => if let Some(s) = self.as_static_str() { + target.push_str(s); + } + } + } +} + +#[derive(Copy, Clone)] +enum State { + /// Default state + Normal, + /// Triggered on '<' + TagStarted, + /// Triggered on '` + InsideMarkupDeclarationQuotedString(QuoteStyle), +} + +#[derive(Copy, Clone, Eq, PartialEq)] +enum QuoteStyle { + Single, Double +} + +#[derive(Copy, Clone)] +enum ClosingSubstate { + First, Second +} + +#[derive(Copy, Clone)] +#[allow(clippy::upper_case_acronyms)] +enum DoctypeStartedSubstate { + D, DO, DOC, DOCT, DOCTY, DOCTYP +} + +#[derive(Copy, Clone)] +#[allow(clippy::upper_case_acronyms)] +enum CDataStartedSubstate { + E, C, CD, CDA, CDAT, CDATA +} + +/// `Result` represents lexing result. It is either a token or an error message. +pub(crate) type Result, E = Error> = result::Result; + +/// Helps to set up a dispatch table for lexing large unambigous tokens like +/// ` ( + match $s { + $( + $st => match $c { + $stc => Ok($_self.move_to($is($next_st))), + _ => $_self.handle_error($chunk, $c) + }, + )+ + $end_st => match $c { + $end_c => $e, + _ => $_self.handle_error($end_chunk, $c) + } + } + ) +); + +/// `Lexer` is a lexer for XML documents, which implements pull API. +/// +/// Main method is `next_token` which accepts an `std::io::Read` instance and +/// tries to read the next lexeme from it. +/// +/// When `skip_errors` flag is set, invalid lexemes will be returned as `Chunk`s. +/// When it is not set, errors will be reported as `Err` objects with a string message. +/// By default this flag is not set. Use `enable_errors` and `disable_errors` methods +/// to toggle the behavior. +#[derive(Clone)] +pub(crate) struct Lexer { + st: State, + reader: CharReader, + pos: TextPosition, + head_pos: TextPosition, + char_queue: VecDeque<(Option, bool, usize)>, + accounting: super::amplification::Accounting, + token_direct_start: u64, + unread_direct_bytes: u64, + current_is_direct: bool, + /// Default state to go back to after a tag end (may be `InsideDoctype`) + normal_state: State, + inside_token: bool, + eof_handled: bool, + reparse_depth: u16, + #[cfg(test)] + skip_errors: bool, + + max_entity_expansion_depth: u8, + max_entity_expansion_length: usize, +} + +impl Position for Lexer { + #[inline] + /// Returns the position of the last token produced by the lexer + fn position(&self) -> TextPosition { self.pos } +} + +impl Lexer { + /// Returns a new lexer with default state. + pub(crate) fn new(config: &ParserConfig) -> Self { + Self { + reader: CharReader::new(Encoding::Unknown), + pos: TextPosition::new(), + head_pos: TextPosition::new(), + accounting: super::amplification::Accounting::new(config.amplification_limits), + token_direct_start: 0, + unread_direct_bytes: 0, + current_is_direct: true, + char_queue: VecDeque::with_capacity(4), // TODO: check size + st: State::Normal, + normal_state: State::Normal, + inside_token: false, + eof_handled: false, + reparse_depth: 0, + #[cfg(test)] + skip_errors: false, + + max_entity_expansion_depth: config.max_entity_expansion_depth, + max_entity_expansion_length: config.max_entity_expansion_length, + } + } + + pub(crate) fn account_indirect(&mut self, bytes: usize, enforce: bool) -> Result<()> { + self.accounting.add_indirect(bytes); + if enforce { self.check_amplification()?; } + Ok(()) + } + + pub(crate) fn check_amplification_before_token(&self) -> Result<()> { + if self.accounting.tolerated_at_direct(self.token_direct_start) { return Ok(()); } + Err(Error { pos: self.position(), kind: super::error::ErrorKind::AmplificationLimit }) + } + + pub(crate) fn check_amplification(&self) -> Result<()> { + if self.accounting.tolerated() { return Ok(()); } + Err(Error { pos: self.position(), kind: super::error::ErrorKind::AmplificationLimit }) + } + + pub(crate) fn encoding(&self) -> Encoding { + self.reader.encoding + } + + pub(crate) fn set_encoding(&mut self, encoding: Encoding) { + self.reader.encoding = encoding; + } + + /// Disables error handling so `next_token` will return `Some(Chunk(..))` + /// upon invalid lexeme with this lexeme content. + #[cfg(test)] fn disable_errors(&mut self) { self.skip_errors = true; } + + /// Reset the eof handled flag of the lexer. + #[inline] + pub fn reset_eof_handled(&mut self) { + self.eof_handled = false; + } + + /// Tries to read the next token from the buffer. + /// + /// It is possible to pass different instaces of `BufReader` each time + /// this method is called, but the resulting behavior is undefined in this case. + /// + /// Return value: + /// * `Err(reason) where reason: reader::Error` - when an error occurs; + /// * `Ok(Token::Eof)` - upon end of stream is reached; + /// * `Ok(token) where token: Token` - in case a complete-token has been read from the stream. + pub fn next_token(&mut self, b: &mut B) -> Result { + // Already reached end of buffer + if self.eof_handled { + return Ok(Token::Eof); + } + + if !self.inside_token { + self.token_direct_start = self.accounting.direct.saturating_sub(self.unread_direct_bytes); + self.pos = self.head_pos; + self.inside_token = true; + } + + // Check if we have saved a char or two for ourselves + while let Some((c, expanded, direct_bytes)) = self.char_queue.pop_front() { + let Some(c) = c else { + self.reparse_depth -= 1; + continue; + }; + self.unread_direct_bytes -= direct_bytes as u64; + self.current_is_direct = direct_bytes != 0; + if expanded { self.account_indirect(c.len_utf8(), true)?; } + if let Some(t) = self.dispatch_char(c)? { + self.inside_token = false; + return Ok(t); + } + } + // if char_queue is empty, all circular reparsing is done + self.reparse_depth = 0; + loop { + let mut counted = CountingRead { source: b, bytes: 0 }; + let next = self.reader.next_char_from(&mut counted); + self.accounting.add_direct(counted.bytes); + let Some(c) = next? else { break; }; + self.current_is_direct = true; + if c == '\n' { + self.head_pos.new_line(); + } else { + self.head_pos.advance(1); + } + + if let Some(t) = self.dispatch_char(c)? { + self.inside_token = false; + return Ok(t); + } + } + + self.end_of_stream() + } + + #[inline(never)] + fn end_of_stream(&mut self) -> Result { + // Handle end of stream + self.eof_handled = true; + self.pos = self.head_pos; + match self.st { + State::InsideCdata | State::CDataClosing(_) => Err(self.error(SyntaxError::UnclosedCdata)), + State::TagStarted | State::CommentOrCDataOrDoctypeStarted | + State::CommentStarted | State::CDataStarted(_)| State::DoctypeStarted(_) | + State::CommentClosing(_) | + State::InsideComment | State::InsideMarkupDeclaration | + State::InsideProcessingInstruction | State::ProcessingInstructionClosing | + State::InsideDoctype | State::InsideMarkupDeclarationQuotedString(_) | + State::EmptyTagClosing | State::InvalidCDataClosing(_) => + Err(self.error(SyntaxError::UnexpectedEof)), + State::Normal => Ok(Token::Eof), + } + } + + #[cold] + #[allow(clippy::needless_pass_by_value)] + fn error(&self, e: SyntaxError) -> Error { + Error::syntax(e.to_cow(), self.position()) + } + + #[inline(never)] + fn dispatch_char(&mut self, c: char) -> Result { + match self.st { + State::Normal => Ok(self.normal(c)), + State::TagStarted => self.tag_opened(c), + State::EmptyTagClosing => Ok(Some(self.empty_element_closing(c))), + State::CommentOrCDataOrDoctypeStarted => self.comment_or_cdata_or_doctype_started(c), + State::InsideCdata => Ok(self.inside_cdata(c)), + State::CDataStarted(s) => self.cdata_started(c, s), + State::InsideComment => Ok(self.inside_comment_state(c)), + State::CommentStarted => self.comment_started(c), + State::InsideProcessingInstruction => Ok(self.inside_processing_instruction(c)), + State::ProcessingInstructionClosing => Ok(Some(self.processing_instruction_closing(c))), + State::CommentClosing(s) => self.comment_closing(c, s), + State::CDataClosing(s) => Ok(self.cdata_closing(c, s)), + State::InsideDoctype => Ok(self.inside_doctype(c)), + State::DoctypeStarted(s) => self.doctype_started(c, s), + State::InvalidCDataClosing(s) => Ok(self.invalid_cdata_closing(c, s)), + State::InsideMarkupDeclaration => self.markup_declaration(c), + State::InsideMarkupDeclarationQuotedString(q) => Ok(Some(self.markup_declaration_string(c, q))), + } + } + + #[inline] + fn move_to(&mut self, st: State) -> Option { + self.st = st; + None + } + + #[inline] + fn move_to_with(&mut self, st: State, token: Token) -> Token { + self.st = st; + token + } + + #[inline] + fn move_to_and_reset_normal(&mut self, st: State, token: Token) -> Token { + self.normal_state = st; + self.st = st; + token + } + + fn move_to_with_unread(&mut self, st: State, cs: &[char], token: Token) -> Token { + for c in cs.iter().rev().copied() { + let bytes = if self.current_is_direct { + match self.reader.encoding { + Encoding::Utf16Be | Encoding::Utf16Le | Encoding::Utf16 => c.len_utf16() * 2, + Encoding::Latin1 | Encoding::Ascii => 1, + _ => c.len_utf8(), + } + } else { 0 }; + self.unread_direct_bytes += bytes as u64; + self.char_queue.push_front((Some(c), false, bytes)); + } + self.move_to_with(st, token) + } + + pub(crate) fn reparse(&mut self, markup: &str) -> Result<()> { + if markup.is_empty() { + return Ok(()); + } + + self.reparse_depth += 1; + if self.reparse_depth > u16::from(self.max_entity_expansion_depth) || self.char_queue.len() > self.max_entity_expansion_length { + return Err(self.error(SyntaxError::EntityTooBig)); + } + + // Check the pending queue before allocating it. This is a separate + // fixed cap, not a cumulative amplification failure. + let characters = markup.chars().count(); + if characters > self.max_entity_expansion_length.saturating_sub(self.char_queue.len()) { + return Err(self.error(SyntaxError::EntityTooBig)); + } + self.reset_eof_handled(); + self.char_queue.reserve(characters + 1); + // A boundary records when an expansion actually finishes; queued + // sibling references do not increase the nesting depth. + self.char_queue.push_front((None, false, 0)); + for c in markup.chars().rev() { + self.char_queue.push_front((Some(c), true, 0)); + } + + Ok(()) + } + + #[allow(clippy::needless_pass_by_ref_mut)] + fn handle_error(&mut self, chunk: &'static str, c: char) -> Result { + debug_assert!(!chunk.is_empty()); + + #[cfg(test)] + if self.skip_errors { + let mut chars = chunk.chars(); + let first = chars.next().unwrap_or('\0'); + self.char_queue.extend(chars.map(|c| (Some(c), false, 0))); + self.char_queue.push_back((Some(c), false, 0)); + return Ok(Some(self.move_to_with(State::Normal, Token::Character(first)))); + } + Err(self.error(SyntaxError::UnexpectedTokenBefore(chunk, c))) + } + + /// Encountered a char + fn normal(&mut self, c: char) -> Option { + match c { + '<' => self.move_to(State::TagStarted), + '>' => Some(Token::TagEnd), + '/' => self.move_to(State::EmptyTagClosing), + '=' => Some(Token::EqualsSign), + '"' => Some(Token::DoubleQuote), + '\'' => Some(Token::SingleQuote), + ']' => self.move_to(State::InvalidCDataClosing(ClosingSubstate::First)), + '&' => Some(Token::ReferenceStart), + ';' => Some(Token::ReferenceEnd), + _ => Some(Token::Character(c)) + } + } + + fn inside_cdata(&mut self, c: char) -> Option { + match c { + ']' => self.move_to(State::CDataClosing(ClosingSubstate::First)), + _ => Some(Token::Character(c)), + } + } + + fn inside_processing_instruction(&mut self, c: char) -> Option { + // These tokens are used by `` parser + match c { + '?' => self.move_to(State::ProcessingInstructionClosing), + '<' => Some(Token::OpeningTagStart), + '>' => Some(Token::TagEnd), + '=' => Some(Token::EqualsSign), + '"' => Some(Token::DoubleQuote), + '\'' => Some(Token::SingleQuote), + '&' => Some(Token::ReferenceStart), + ';' => Some(Token::ReferenceEnd), + _ => Some(Token::Character(c)) + } + } + + fn inside_comment_state(&mut self, c: char) -> Option { + match c { + '-' => self.move_to(State::CommentClosing(ClosingSubstate::First)), + _ => Some(Token::Character(c)), + } + } + + /// Encountered '<' + fn tag_opened(&mut self, c: char) -> Result { + match c { + '?' => Ok(Some(self.move_to_with(State::InsideProcessingInstruction, Token::ProcessingInstructionStart))), + '/' => Ok(Some(self.move_to_with(self.normal_state, Token::ClosingTagStart))), + '!' => Ok(self.move_to(State::CommentOrCDataOrDoctypeStarted)), + _ if is_whitespace_char(c) => Ok(Some(self.move_to_with_unread(self.normal_state, &[c], Token::OpeningTagStart))), + _ if is_name_char(c) => Ok(Some(self.move_to_with_unread(self.normal_state, &[c], Token::OpeningTagStart))), + _ => self.handle_error("<", c) + } + } + + /// Encountered ' Result { + match c { + '-' => Ok(self.move_to(State::CommentStarted)), + '[' => Ok(self.move_to(State::CDataStarted(CDataStartedSubstate::E))), + 'D' => Ok(self.move_to(State::DoctypeStarted(DoctypeStartedSubstate::D))), + 'E' | 'A' | 'N' if matches!(self.normal_state, State::InsideDoctype) => { + Ok(Some(self.move_to_with_unread(State::InsideMarkupDeclaration, &[c], Token::MarkupDeclarationStart))) + }, + _ => self.handle_error(" Result { + match c { + '-' => Ok(Some(self.move_to_with(State::InsideComment, Token::CommentStart))), + _ => self.handle_error(" Result { + use self::CDataStartedSubstate::{C, CD, CDA, CDAT, CDATA, E}; + dispatch_on_enum_state!(self, s, c, State::CDataStarted, + E ; 'C' ; C ; " Result { + match c { + '<' => self.handle_error("' => Ok(Some(self.move_to_with(self.normal_state, Token::TagEnd))), + '&' => Ok(Some(Token::ReferenceStart)), + ';' => Ok(Some(Token::ReferenceEnd)), + '"' => Ok(Some(self.move_to_with(State::InsideMarkupDeclarationQuotedString(QuoteStyle::Double), Token::DoubleQuote))), + '\'' => Ok(Some(self.move_to_with(State::InsideMarkupDeclarationQuotedString(QuoteStyle::Single), Token::SingleQuote))), + _ => Ok(Some(Token::Character(c))), + } + } + + fn markup_declaration_string(&mut self, c: char, q: QuoteStyle) -> Token { + match c { + '"' if q == QuoteStyle::Double => self.move_to_with(State::InsideMarkupDeclaration, Token::DoubleQuote), + '\'' if q == QuoteStyle::Single => self.move_to_with(State::InsideMarkupDeclaration, Token::SingleQuote), + _ => Token::Character(c), + } + } + + /// Encountered ' Result { + use self::DoctypeStartedSubstate::{D, DO, DOC, DOCT, DOCTY, DOCTYP}; + dispatch_on_enum_state!(self, s, c, State::DoctypeStarted, + D ; 'O' ; DO ; " Option { + match c { + '>' => Some(self.move_to_and_reset_normal(State::Normal, Token::TagEnd)), + '<' => self.move_to(State::TagStarted), + '&' => Some(Token::ReferenceStart), + ';' => Some(Token::ReferenceEnd), + '"' => Some(Token::DoubleQuote), + '\'' => Some(Token::SingleQuote), + _ => Some(Token::Character(c)), + } + } + + /// Encountered '?' + fn processing_instruction_closing(&mut self, c: char) -> Token { + match c { + '>' => self.move_to_with(self.normal_state, Token::ProcessingInstructionEnd), + _ => self.move_to_with_unread(State::InsideProcessingInstruction, &[c], Token::Character('?')), + } + } + + /// Encountered '/' + fn empty_element_closing(&mut self, c: char) -> Token { + match c { + '>' => self.move_to_with(self.normal_state, Token::EmptyTagEnd), + _ => self.move_to_with_unread(self.normal_state, &[c], Token::Character('/')), + } + } + + /// Encountered '-' + fn comment_closing(&mut self, c: char, s: ClosingSubstate) -> Result { + match s { + ClosingSubstate::First => match c { + '-' => Ok(self.move_to(State::CommentClosing(ClosingSubstate::Second))), + _ => Ok(Some(self.move_to_with_unread(State::InsideComment, &[c], Token::Character('-')))), + }, + ClosingSubstate::Second => match c { + '>' => Ok(Some(self.move_to_with(self.normal_state, Token::CommentEnd))), + // double dash not followed by a greater-than is a hard error inside comment + _ => self.handle_error("--", c), + }, + } + } + + /// Encountered ']' + fn cdata_closing(&mut self, c: char, s: ClosingSubstate) -> Option { + match s { + ClosingSubstate::First => match c { + ']' => self.move_to(State::CDataClosing(ClosingSubstate::Second)), + _ => Some(self.move_to_with_unread(State::InsideCdata, &[c], Token::Character(']'))), + }, + ClosingSubstate::Second => match c { + '>' => Some(self.move_to_with(State::Normal, Token::CDataEnd)), + _ => Some(self.move_to_with_unread(State::InsideCdata, &[']', c], Token::Character(']'))), + }, + } + } + + /// Encountered ']' + fn invalid_cdata_closing(&mut self, c: char, s: ClosingSubstate) -> Option { + match s { + ClosingSubstate::First => match c { + ']' => self.move_to(State::InvalidCDataClosing(ClosingSubstate::Second)), + _ => Some(self.move_to_with_unread(State::Normal, &[c], Token::Character(']'))), + }, + ClosingSubstate::Second => match c { + '>' => Some(self.move_to_with(self.normal_state, Token::CDataEnd)), + _ => Some(self.move_to_with_unread(State::Normal, &[']', c], Token::Character(']'))), + }, + } + } +} + +// Count at the decoder boundary, not at a buffered source that can read ahead. +struct CountingRead<'a, R> { source: &'a mut R, bytes: usize } +impl Read for CountingRead<'_, R> { + fn read(&mut self, buffer: &mut [u8]) -> std::io::Result { + let n = self.source.read(buffer)?; + self.bytes += n; + Ok(n) + } +} + +#[cfg(test)] +mod tests { + use crate::common::Position; + use crate::reader::ParserConfig; + use std::io::{BufReader, Cursor}; + + use super::{Lexer, Token}; + + macro_rules! assert_oks( + (for $lex:ident and $buf:ident ; $($e:expr)+) => ({ + $( + let tmp = $e; + assert_eq!(Ok(tmp), $lex.next_token(&mut $buf), "expected '{}'", tmp); + )+ + }) + ); + + macro_rules! assert_err( + (for $lex:ident and $buf:ident expect row $r:expr ; $c:expr, $s:expr) => ({ + let err = $lex.next_token(&mut $buf); + assert!(err.is_err()); + let err = err.unwrap_err(); + assert_eq!($r, err.position().row); + assert_eq!($c, err.position().column); + }) + ); + + macro_rules! assert_none( + (for $lex:ident and $buf:ident) => ( + assert_eq!(Ok(Token::Eof), $lex.next_token(&mut $buf)) + ) + ); + + fn make_lex_and_buf(s: &str) -> (Lexer, BufReader>>) { + (Lexer::new(&ParserConfig::default()), BufReader::new(Cursor::new(s.to_owned().into_bytes()))) + } + + #[test] + fn tricky_pi() { + let (mut lex, mut buf) = make_lex_and_buf(r""); + + assert_oks!(for lex and buf ; + Token::ProcessingInstructionStart + Token::Character('x') + Token::OpeningTagStart // processing of relies on the extra tokens + Token::Character('!') + Token::Character('-') + Token::Character('-') + Token::Character(' ') + Token::ReferenceStart + Token::Character('?') + Token::ProcessingInstructionEnd + Token::OpeningTagStart + Token::Character('x') + Token::TagEnd + ); + assert_none!(for lex and buf); + } + + #[test] + fn reparser() { + let (mut lex, mut buf) = make_lex_and_buf(r"&a;"); + + assert_oks!(for lex and buf ; + Token::ReferenceStart + Token::Character('a') + Token::ReferenceEnd + ); + lex.reparse("").unwrap(); + assert_oks!(for lex and buf ; + Token::OpeningTagStart + Token::Character('h') + Token::Character('i') + Token::EmptyTagEnd + ); + assert_none!(for lex and buf); + } + + #[test] + fn simple_lexer_test() { + let (mut lex, mut buf) = make_lex_and_buf( + r#" xd

     "# + ); + + assert_oks!(for lex and buf ; + Token::OpeningTagStart + Token::Character('a') + Token::Character(' ') + Token::Character('p') + Token::EqualsSign + Token::SingleQuote + Token::Character('q') + Token::SingleQuote + Token::TagEnd + Token::Character(' ') + Token::Character('x') + Token::OpeningTagStart + Token::Character('b') + Token::Character(' ') + Token::Character('z') + Token::EqualsSign + Token::DoubleQuote + Token::Character('y') + Token::DoubleQuote + Token::TagEnd + Token::Character('d') + Token::Character('\t') + Token::ClosingTagStart + Token::Character('b') + Token::TagEnd + Token::ClosingTagStart + Token::Character('a') + Token::TagEnd + Token::OpeningTagStart + Token::Character('p') + Token::EmptyTagEnd + Token::Character(' ') + Token::ProcessingInstructionStart + Token::Character('n') + Token::Character('m') + Token::Character(' ') + Token::ProcessingInstructionEnd + Token::Character(' ') + Token::CommentStart + Token::Character(' ') + Token::Character('a') + Token::Character(' ') + Token::Character('c') + Token::Character(' ') + Token::CommentEnd + Token::Character(' ') + Token::ReferenceStart + Token::Character('n') + Token::Character('b') + Token::Character('s') + Token::Character('p') + Token::ReferenceEnd + ); + assert_none!(for lex and buf); + } + + #[test] + fn special_chars_test() { + let (mut lex, mut buf) = make_lex_and_buf( + r"?x!+ // -| ]z]] " + ); + + assert_oks!(for lex and buf ; + Token::Character('?') + Token::Character('x') + Token::Character('!') + Token::Character('+') + Token::Character(' ') + Token::Character('/') + Token::Character('/') + Token::Character(' ') + Token::Character('-') + Token::Character('|') + Token::Character(' ') + Token::Character(']') + Token::Character('z') + Token::Character(']') + Token::Character(']') + Token::Character(' ') + ); + assert_none!(for lex and buf); + } + + #[test] + fn cdata_test() { + let (mut lex, mut buf) = make_lex_and_buf( + r" " + ); + + assert_oks!(for lex and buf ; + Token::OpeningTagStart + Token::Character('a') + Token::TagEnd + Token::CDataStart + Token::Character('x') + Token::Character(' ') + Token::Character('y') + Token::Character(' ') + Token::Character('?') + Token::CDataEnd + Token::Character(' ') + Token::ClosingTagStart + Token::Character('a') + Token::TagEnd + ); + assert_none!(for lex and buf); + } + + #[test] + fn cdata_closers_test() { + let (mut lex, mut buf) = make_lex_and_buf( + r" ]> ]]>]]" + ); + + assert_oks!(for lex and buf ; + Token::CDataStart + Token::Character(']') + Token::Character(' ') + Token::Character('>') + Token::Character(' ') + Token::Character(']') + Token::Character('>') + Token::Character(' ') + Token::CDataEnd + Token::CommentStart + Token::CommentEnd + Token::Character(']') + Token::Character(']') + Token::OpeningTagStart + Token::Character('a') + Token::TagEnd + ); + assert_none!(for lex and buf); + } + + #[test] + fn doctype_test() { + let (mut lex, mut buf) = make_lex_and_buf( + r" " + ); + assert_oks!(for lex and buf ; + Token::OpeningTagStart + Token::Character('a') + Token::TagEnd + Token::DoctypeStart + Token::Character(' ') + Token::Character('a') + Token::Character('b') + Token::Character(' ') + Token::Character('x') + Token::Character('x') + Token::Character(' ') + Token::Character('z') + Token::TagEnd + Token::Character(' ') + ); + assert_none!(for lex and buf); + } + + #[test] + fn tricky_comments() { + let (mut lex, mut buf) = make_lex_and_buf( + r"" + ); + assert_oks!(for lex and buf ; + Token::OpeningTagStart + Token::Character('a') + Token::TagEnd + Token::CommentStart + Token::Character(' ') + Token::Character('C') + Token::Character(' ') + Token::Character('-') + Token::Character('>') + Token::CommentEnd + Token::ClosingTagStart + Token::Character('a') + Token::TagEnd + ); + assert_none!(for lex and buf); + } + + #[test] + fn doctype_with_internal_subset_test() { + let (mut lex, mut buf) = make_lex_and_buf( + r#">>"> ]> "# + ); + assert_oks!(for lex and buf ; + Token::OpeningTagStart + Token::Character('a') + Token::TagEnd + Token::DoctypeStart + Token::Character(' ') + Token::Character('a') + Token::Character('b') + Token::Character('[') + Token::MarkupDeclarationStart + Token::Character('E') + Token::Character('L') + Token::Character('E') + Token::Character('M') + Token::Character('E') + Token::Character('N') + Token::Character('T') + Token::Character(' ') + Token::Character('b') + Token::Character('a') + Token::Character(' ') + Token::DoubleQuote + Token::Character('>') + Token::Character('>') + Token::Character('>') + Token::DoubleQuote + Token::TagEnd + Token::Character(' ') + Token::Character(']') + Token::TagEnd + Token::Character(' ') + ); + assert_none!(for lex and buf); + } + + #[test] + fn doctype_internal_pi_comment() { + let (mut lex, mut buf) = make_lex_and_buf( + " ?> \n]>" + ); + assert_oks!(for lex and buf ; + Token::DoctypeStart + Token::Character(' ') + Token::Character('a') + Token::Character(' ') + Token::Character('[') + Token::Character('\n') + Token::MarkupDeclarationStart + Token::Character('E') + Token::Character('L') + Token::Character('E') + Token::Character('M') + Token::Character('E') + Token::Character('N') + Token::Character('T') + Token::Character(' ') + Token::Character('l') + Token::Character(' ') + Token::Character('A') + Token::Character('N') + Token::Character('Y') + Token::TagEnd + Token::Character(' ') + Token::CommentStart + Token::Character(' ') + Token::Character('<') + Token::Character('?') + Token::Character('n') + Token::Character('o') + Token::Character('n') + Token::Character('?') + Token::Character('>') + Token::CommentEnd + Token::Character(' ') + Token::ProcessingInstructionStart + Token::Character('p') + Token::Character('i') + Token::Character(' ') + Token::TagEnd // not really + Token::Character(' ') + Token::ProcessingInstructionEnd + Token::Character(' ') + Token::Character('\n') + Token::Character(']') + Token::TagEnd // DTD + ); + assert_none!(for lex and buf); + } + + #[test] + fn end_of_stream_handling_ok() { + macro_rules! eof_check( + ($data:expr ; $token:expr) => ({ + let (mut lex, mut buf) = make_lex_and_buf($data); + assert_oks!(for lex and buf ; $token); + assert_none!(for lex and buf); + }) + ); + eof_check!("?" ; Token::Character('?')); + eof_check!("-" ; Token::Character('-')); + } + + #[test] + fn end_of_stream_handling_error() { + macro_rules! eof_check( + ($data:expr; $r:expr, $c:expr) => ({ + let (mut lex, mut buf) = make_lex_and_buf($data); + assert_err!(for lex and buf expect row $r ; $c, "Unexpected end of stream"); + assert_none!(for lex and buf); + }) + ); + eof_check!("<" ; 0, 1); + eof_check!(" ({ + let (mut lex, mut buf) = make_lex_and_buf($data); + assert_err!(for lex and buf expect row $r ; $c, $s); + + let (mut lex, mut buf) = make_lex_and_buf($data); + lex.disable_errors(); + for c in $chunk.chars() { + assert_eq!(Ok(Token::Character(c)), lex.next_token(&mut buf)); + } + assert_oks!(for lex and buf ; + Token::Character($app) + ); + assert_none!(for lex and buf); + }) + ); + + #[test] + fn token_size() { + assert_eq!(4, std::mem::size_of::()); + assert_eq!(2, std::mem::size_of::()); + } + + #[test] + fn error_in_cdata_started() { + check_case!("" + ); + + assert_oks!(for lex and buf ; + Token::CDataStart + Token::Character('F') + Token::Character('o') + Token::Character('o') + Token::Character(' ') + Token::Character('[') + Token::Character('B') + Token::Character('a') + Token::Character('r') + Token::Character(']') + Token::CDataEnd + ); + assert_none!(for lex and buf); + } +} + diff --git a/vendor/xml/src/reader/parser.rs b/vendor/xml/src/reader/parser.rs new file mode 100644 index 00000000000..1da29288ea4 --- /dev/null +++ b/vendor/xml/src/reader/parser.rs @@ -0,0 +1,948 @@ +//! Contains an implementation of pull-based XML parser. + +use crate::common::{is_xml10_char, is_xml11_char, is_xml11_char_not_restricted, is_name_char, is_name_start_char, is_whitespace_char}; +use crate::common::{Position, TextPosition, XmlVersion}; +use crate::attribute::OwnedAttribute; +use crate::name::OwnedName; +use crate::namespace::NamespaceStack; +use crate::reader::config::ParserConfig; +use crate::reader::error::{Error, ImmutableEntitiesError, SyntaxError}; +use crate::reader::events::XmlEvent; +use crate::reader::lexer::{Lexer, Token}; +use crate::reader::DoctypeRef; + +use std::collections::{HashMap, HashSet}; +use std::io::Read; + +static STRING_RESERVE_CAPACITY: usize = 20; + +macro_rules! gen_takes( + ($($field:ident -> $method:ident, $t:ty, $def:expr);+) => ( + $( + impl MarkupData { + #[inline] + #[allow(clippy::mem_replace_option_with_none)] + #[allow(clippy::mem_replace_with_default)] + fn $method(&mut self) -> $t { + std::mem::replace(&mut self.$field, $def) + } + } + )+ + ) +); + +gen_takes!( + name -> take_name, String, String::with_capacity(STRING_RESERVE_CAPACITY); + ref_data -> take_ref_data, String, String::with_capacity(STRING_RESERVE_CAPACITY); + + encoding -> take_encoding, Option, None; + + element_name -> take_element_name, Option, None; + + attr_name -> take_attr_name, Option, None; + attr_pos -> take_attr_pos, Option, None; + attributes -> take_attributes, Vec, Vec::with_capacity(8); + attribute_positions -> take_attribute_positions, Vec, Vec::with_capacity(8) +); + +mod inside_cdata; +mod inside_closing_tag_name; +mod inside_comment; +mod inside_declaration; +mod inside_doctype; +mod inside_opening_tag; +mod inside_processing_instruction; +mod inside_reference; +mod attribute_expansion; +mod outside_tag; + +static DEFAULT_VERSION: XmlVersion = XmlVersion::Version10; +static DEFAULT_STANDALONE: Option = None; + +type ElementStack = Vec; + +/// Newtype for `XmlEvent` only. If you import this, use `std::result::Result` for other results. +pub type Result = super::Result; + +/// Pull-based XML parser. +#[derive(Clone)] +pub(crate) struct PullParser { + config: ParserConfig, + lexer: Lexer, + st: State, + state_after_reference: State, + buf: String, + // Separate scratch space as an optimization avoiding reallocations + // for parsing qualified names. + qualified_name_buf: String, + + /// From DTD internal subset + entities: HashMap, + pending_attributes: Vec<(OwnedName, String, Option)>, + + nst: NamespaceStack, + + data: MarkupData, + final_result: Option, + next_event: Option, + est: ElementStack, + pos: Vec, + + encountered: Encountered, + inside_whitespace: bool, + literal_text_pending: bool, + entity_value_after_cr: bool, + seen_prefix_separator: bool, + pop_namespace: bool, +} + +// Keeps track when XML declaration can happen +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +enum Encountered { + None = 0, + AnyChars, // whitespace before ) -> Self { + Self::new_with_config(config.into()) + } + + #[inline] + fn new_with_config(config: ParserConfig) -> Self { + let mut lexer = Lexer::new(&config); + if let Some(enc) = config.override_encoding { + lexer.set_encoding(enc); + } + + let mut pos = Vec::with_capacity(16); + pos.push(TextPosition::new()); + + Self { + config, + lexer, + st: State::DocumentStart, + state_after_reference: State::OutsideTag, + buf: String::with_capacity(STRING_RESERVE_CAPACITY), + qualified_name_buf: String::with_capacity(STRING_RESERVE_CAPACITY), + entities: HashMap::new(), + pending_attributes: Vec::new(), + nst: NamespaceStack::default(), + + data: MarkupData { + name: String::new(), + doctype: None, + doctype_name: None, + doctype_public_id: None, + doctype_system_id: None, + version: None, + encoding: None, + standalone: None, + ref_data: String::new(), + element_name: None, + quote: None, + attr_name: None, + attr_pos: None, + attributes: Vec::with_capacity(8), + attribute_positions: Vec::with_capacity(8), + }, + final_result: None, + next_event: None, + est: Vec::new(), + pos, + + encountered: Encountered::None, + inside_whitespace: true, + literal_text_pending: false, + entity_value_after_cr: false, + seen_prefix_separator: false, + pop_namespace: false, + } + } + + /// Checks if this parser ignores the end of stream errors. + pub fn is_ignoring_end_of_stream(&self) -> bool { self.config.ignore_end_of_stream } + + /// Retrieves the Doctype from the document if any + #[inline] + #[deprecated(note = "there is `XmlEvent::Doctype` now")] + pub fn doctype(&self) -> Option<&str> { + self.data.doctype.as_deref() + } + + pub fn doctype_ids(&self) -> Option> { + Some(DoctypeRef { + syntax: self.data.doctype.as_deref()?, + name: self.data.doctype_name.as_deref()?, + public_id: self.data.doctype_public_id.as_deref(), + system_id: self.data.doctype_system_id.as_deref(), + }) + } + + #[inline(never)] + fn set_encountered(&mut self, new_encounter: Encountered) -> Option { + if new_encounter <= self.encountered { + return None; + } + let prev_enc = self.encountered; + self.encountered = new_encounter; + + // If declaration was not parsed and we have encountered an element, + // emit this declaration as the next event. + if prev_enc == Encountered::None { + self.push_pos(); + Some(Ok(XmlEvent::StartDocument { + version: DEFAULT_VERSION, + encoding: self.lexer.encoding().to_string(), + standalone: DEFAULT_STANDALONE, + })) + } else { + None + } + } + + #[inline] + pub fn add_entities, T: Into>(&mut self, entities: impl IntoIterator) -> std::result::Result<(), ImmutableEntitiesError> { + if self.data.standalone == Some(true) { + return Err(ImmutableEntitiesError::StandaloneDocument); + } + if self.encountered == Encountered::Element { + return Err(ImmutableEntitiesError::ElementEncountered); + } + self.config.extra_entities.extend(entities.into_iter().map(|(k, v)| (k.into(), v.into()))); + Ok(()) + } +} + +impl Position for PullParser { + /// Returns the position of the last event produced by the parser + #[inline] + fn position(&self) -> TextPosition { + self.pos.first().copied().unwrap_or_else(TextPosition::new) + } +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub(crate) enum State { + OutsideTag, + InsideOpeningTag(OpeningTagSubstate), + InsideClosingTag(ClosingTagSubstate), + InsideProcessingInstruction(ProcessingInstructionSubstate), + InsideComment, + InsideCData, + InsideDeclaration(DeclarationSubstate), + InsideDoctype(DoctypeSubstate), + InsideReference, + DocumentStart, +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub(crate) enum DoctypeSubstate { + BeforeDoctypeName, + DoctypeName, + Outside, + // PUBLIC ... SYSTEM... public and system literal parts. + ExternalIdKeyword, + BeforeSystemLiteral, + SystemLiteral, + BeforePubId, + PubId, + // Internal Subset related bits, parts inside [...]. + InternalSubset, + String, + InsideName, + BeforeEntityName, + EntityName, + BeforeEntityValue, + EntityValue, + NumericReferenceStart, + NumericReference, + /// expansion + PEReferenceInValue, + PEReferenceInDtd, + /// name definition + PEReferenceDefinitionStart, + PEReferenceDefinition, + IgnorePI, + SkipDeclaration, + Comment, +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub(crate) enum OpeningTagSubstate { + InsideName, + + InsideTag, + + InsideAttributeName, + AfterAttributeName, + + InsideAttributeValue, + AfterAttributeValue, +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub(crate) enum ClosingTagSubstate { + CTInsideName, + CTAfterName, +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub(crate) enum ProcessingInstructionSubstate { + PIInsideName, + PIInsideData, +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub(crate) enum DeclarationSubstate { + BeforeVersion, + InsideVersion, + AfterVersion, + + InsideVersionValue, + AfterVersionValue, + + BeforeEncoding, + InsideEncoding, + AfterEncoding, + + InsideEncodingValue, + AfterEncodingValue, + + BeforeStandaloneDecl, + InsideStandaloneDecl, + AfterStandaloneDecl, + + InsideStandaloneDeclValue, + AfterStandaloneDeclValue, +} + +#[derive(Copy, Clone, PartialEq, Debug)] +enum QualifiedNameTarget { + Attribute, + OpeningTag, + ClosingTag, +} + +#[derive(Copy, Clone, PartialEq, Eq)] +enum QuoteToken { + SingleQuoteToken, + DoubleQuoteToken, +} + +impl QuoteToken { + #[inline] + fn from_token(t: Token) -> Option { + match t { + Token::SingleQuote => Some(Self::SingleQuoteToken), + Token::DoubleQuote => Some(Self::DoubleQuoteToken), + _ => { + debug_assert!(false); + None + }, + } + } + + const fn as_token(self) -> Token { + match self { + Self::SingleQuoteToken => Token::SingleQuote, + Self::DoubleQuoteToken => Token::DoubleQuote, + } + } +} + +#[derive(Clone)] +struct MarkupData { + name: String, // used for processing instruction name + ref_data: String, // used for reference content + + doctype: Option, // keeps a copy of the original doctype + doctype_name: Option>, + doctype_public_id: Option>, + doctype_system_id: Option>, + version: Option, // used for XML declaration version + encoding: Option, // used for XML declaration encoding + standalone: Option, // used for XML declaration standalone parameter + + element_name: Option, // used for element name + + quote: Option, // used to hold opening quote for attribute value + attr_name: Option, // used to hold attribute name + attr_pos: Option, // used to hold attribute name start position + attributes: Vec, // used to hold all accumulated attributes + attribute_positions: Vec, // used to hold all accumulated attributes start positions +} + +impl PullParser { + /// Returns next event read from the given buffer. + /// + /// This method should be always called with the same buffer. If you call it + /// providing different buffers each time, the result will be undefined. + pub fn next(&mut self, r: &mut R) -> Result { + if let Some(ref ev) = self.final_result { + return ev.clone(); + } + + if let Some(ev) = self.next_event.take() { + return if ev.is_err() { self.set_final_result(ev) } else { ev }; + } + + if self.pop_namespace { + self.pop_namespace = false; + self.nst.pop(); + } + + loop { + debug_assert!(self.next_event.is_none()); + debug_assert!(!self.pop_namespace); + + // While lexer gives us Ok(maybe_token) -- we loop. + // Upon having a complete XML-event -- we return from the whole function. + match self.lexer.next_token(r) { + Ok(Token::Eof) => { + // Forward pos to the lexer head + self.next_pos(); + return self.handle_eof(); + }, + Ok(token) => match self.dispatch_token(token) { + None => continue, + Some(Ok(xml_event)) => { + self.next_pos(); + return Ok(xml_event); + }, + Some(Err(xml_error)) => { + self.next_pos(); + return self.set_final_result(Err(xml_error)); + }, + }, + Err(lexer_error) => { + self.next_pos(); + if self.config.ignore_end_of_stream { + self.lexer.reset_eof_handled(); + return Err(lexer_error); + } + return self.set_final_result(Err(lexer_error)); + }, + } + } + } + + /// Handle end of stream + #[cold] + fn handle_eof(&mut self) -> std::result::Result { + if self.literal_text_pending { + if let Err(e) = self.lexer.check_amplification() { return self.set_final_result(Err(e)); } + } + let ev = if self.depth() == 0 { + if self.encountered == Encountered::Element && self.st == State::OutsideTag { // all is ok + Ok(XmlEvent::EndDocument) + } else if self.config.ignore_end_of_stream { + self.final_result = None; + self.lexer.reset_eof_handled(); + return if self.encountered < Encountered::Element { + self.error(SyntaxError::NoRootElement) + } else { + self.error(SyntaxError::UnexpectedEof) + }; + } else if self.encountered < Encountered::Element { + self.error(SyntaxError::NoRootElement) + } else { // self.st != State::OutsideTag + self.error(SyntaxError::UnexpectedEof) // TODO: add expected hint? + } + } else if self.config.ignore_end_of_stream { + self.final_result = None; + self.lexer.reset_eof_handled(); + return self.error(SyntaxError::UnbalancedRootElement); + } else { + self.error(SyntaxError::UnbalancedRootElement) + }; + self.set_final_result(ev) + } + + // This function is to be called when a terminal event is reached. + // The function sets up the `self.final_result` into `Some(result)` and return `result`. + #[inline] + fn set_final_result(&mut self, result: Result) -> Result { + self.final_result = Some(result.clone()); + result + } + + #[cold] + #[allow(clippy::needless_pass_by_value)] + fn error(&self, e: SyntaxError) -> Result { + Err(Error::syntax(e.to_cow(), self.lexer.position())) + } + + #[cold] + #[allow(clippy::needless_pass_by_value)] + fn error_at(e: SyntaxError, pos: TextPosition) -> Result { + Err(Error::syntax(e.to_cow(), pos)) + } + + #[inline] + fn next_pos(&mut self) { + // unfortunately calls to next_pos will never be perfectly balanced with push_pos, + // at very least because parse errors and EOF can happen unexpectedly without a prior push. + if !self.pos.is_empty() { + if self.pos.len() > 1 { + self.pos.remove(0); + } else { + self.pos[0] = self.lexer.position(); + } + } + } + + #[inline] + #[track_caller] + fn push_pos(&mut self) { + debug_assert!(self.pos.len() != self.pos.capacity(), "You've found a bug in xml-rs, caused by calls to push_pos() in states that don't end up emitting events. + This case is ignored in release mode, and merely causes document positions to be out of sync. + Please file a bug and include the XML document that triggers this assert."); + + // it has capacity preallocated for more than it ever needs, so this reduces code size + if self.pos.len() != self.pos.capacity() { + self.pos.push(self.lexer.position()); + } else if self.pos.len() > 1 { + self.pos.remove(0); // this mitigates the excessive push_pos() call + } + } + + #[inline(never)] + fn dispatch_token(&mut self, t: Token) -> Option { + match self.st { + State::OutsideTag => self.outside_tag(t), + State::InsideOpeningTag(s) => self.inside_opening_tag(t, s), + State::InsideClosingTag(s) => self.inside_closing_tag_name(t, s), + State::InsideReference => self.inside_reference(t), + State::InsideComment => self.inside_comment(t), + State::InsideCData => self.inside_cdata(t), + State::InsideProcessingInstruction(s) => self.inside_processing_instruction(t, s), + State::InsideDoctype(s) => self.inside_doctype(t, s), + State::InsideDeclaration(s) => self.inside_declaration(t, s), + State::DocumentStart => self.document_start(t), + } + } + + #[inline] + fn depth(&self) -> usize { + self.est.len() + } + + #[inline] + fn buf_has_data(&self) -> bool { + !self.buf.is_empty() + } + + #[inline] + fn take_buf(&mut self) -> String { + std::mem::replace(&mut self.buf, String::with_capacity(STRING_RESERVE_CAPACITY)) + } + + #[inline] + fn take_buf_boxed(&mut self) -> Box { + let res = self.buf.as_str().into(); + self.buf.clear(); + res + } + + #[inline] + #[allow(clippy::wrong_self_convention)] + fn into_state(&mut self, st: State, ev: Option) -> Option { + self.st = st; + ev + } + + #[inline] + #[allow(clippy::wrong_self_convention)] + fn into_state_continue(&mut self, st: State) -> Option { + self.into_state(st, None) + } + + #[inline] + #[allow(clippy::wrong_self_convention)] + fn into_state_emit(&mut self, st: State, ev: Result) -> Option { + self.into_state(st, Some(ev)) + } + + /// Dispatches tokens in order to process qualified name. If qualified name cannot be parsed, + /// an error is returned. + /// + /// # Parameters + /// * `t` --- next token; + /// * `on_name` --- a callback which is executed when whitespace is encountered. + fn read_qualified_name(&mut self, t: Token, target: QualifiedNameTarget, on_name: F) -> Option + where F: Fn(&mut Self, Token, OwnedName) -> Option { + + let try_consume_name = move |this: &mut Self, t| { + + this.seen_prefix_separator = false; + let result = match this.qualified_name_buf.parse() { + Ok(name) => on_name(this, t, name), + Err(()) => Some(this.error(SyntaxError::InvalidQualifiedName(this.qualified_name_buf.clone().into()))), + }; + this.qualified_name_buf.clear(); + result + }; + + match t { + // There can be only one colon, and not as the first character + Token::Character(':') if !self.qualified_name_buf.is_empty() && !self.seen_prefix_separator => { + self.qualified_name_buf.push(':'); + self.seen_prefix_separator = true; + None + }, + + Token::Character(c) if c != ':' && (self.qualified_name_buf.is_empty() && is_name_start_char(c) || + !self.qualified_name_buf.is_empty() && is_name_char(c)) => { + if self.qualified_name_buf.len() > self.config.max_name_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + self.qualified_name_buf.push(c); + None + }, + + Token::EqualsSign if target == QualifiedNameTarget::Attribute => try_consume_name(self, t), + + Token::EmptyTagEnd if target == QualifiedNameTarget::OpeningTag => try_consume_name(self, t), + + Token::TagEnd if target == QualifiedNameTarget::OpeningTag || + target == QualifiedNameTarget::ClosingTag => try_consume_name(self, t), + + Token::Character(c) if is_whitespace_char(c) => try_consume_name(self, t), + + _ => Some(self.error(SyntaxError::UnexpectedQualifiedName(t))), + } + } + + /// Dispatches tokens in order to process attribute value. + /// + /// # Parameters + /// * `t` --- next token; + /// * `on_value` --- a callback which is called when terminating quote is encountered. + fn read_attribute_value(&mut self, t: Token, on_value: F) -> Option + where F: Fn(&mut Self, String) -> Option { + match t { + Token::Character(c) if self.data.quote.is_none() && is_whitespace_char(c) => None, // skip leading whitespace + + Token::DoubleQuote | Token::SingleQuote => match self.data.quote { + None => { // Entered attribute value + self.data.quote = QuoteToken::from_token(t); + None + }, + Some(q) if q.as_token() == t => { + self.data.quote = None; + let value = self.take_buf(); + on_value(self, value) + }, + _ => { + if let Token::Character(c) = t { + if !self.is_valid_xml_char_not_restricted(c) { + return Some(self.error(SyntaxError::InvalidCharacterEntity(c as u32))); + } + } + if self.buf.len() > self.config.max_attribute_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + t.push_to_string(&mut self.buf); + None + }, + }, + + Token::ReferenceStart if self.data.quote.is_some() => { + self.state_after_reference = self.st; + self.into_state_continue(State::InsideReference) + }, + + Token::OpeningTagStart | Token::ProcessingInstructionStart => { + Some(self.error(SyntaxError::UnexpectedOpeningTag)) + }, + + Token::Character(c) if !self.is_valid_xml_char_not_restricted(c) => { + Some(self.error(SyntaxError::InvalidCharacterEntity(c as u32))) + }, + + // Every character except " and ' and < is okay + _ if self.data.quote.is_some() => { + if self.buf.len() > self.config.max_attribute_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + t.push_to_string(&mut self.buf); + None + }, + + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + } + } + + fn emit_start_element(&mut self, emit_end_element: bool) -> Option { + // Expat accounts for a complete start tag before expanding attributes. + // Keep references as source spellings until here, so neither expanded + // strings nor namespace values can be allocated before their check. + if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } + for (name, raw, pos) in std::mem::take(&mut self.pending_attributes) { + let value = match self.expand_attribute(&raw) { + Ok(value) => value, + Err(e) => return Some(Err(e)), + }; + if let Err(e) = self.finish_attribute(name, value, pos) { return Some(Err(e)); } + } + let mut name = self.data.take_element_name()?; + let mut attributes = self.data.take_attributes(); + let attribute_positions = self.data.take_attribute_positions(); + + // check whether the name prefix is bound and fix its namespace + match self.nst.get(name.borrow().prefix_repr()) { + Some("") => name.namespace = None, // default namespace + Some(ns) => name.namespace = Some(ns.into()), + None => return Some(self.error(SyntaxError::UnboundElementPrefix(name.to_string().into()))), + } + + // Fix attribute namespaces + for i in 0..attributes.len() { + if let Some(ref pfx) = attributes[i].name.prefix { + attributes[i].name.namespace = match self.nst.get(pfx) { + Some("") => None, // default namespace + Some(ns) => Some(ns.into()), + None => return Some(Self::error_at(SyntaxError::UnboundAttribute(attributes[i].name.to_string().into()), attribute_positions[i])), + }; + } + } + + // check for duplicates (namespace-aware) + if let Some(idx) = Self::find_duplicate_attribute(&attributes) { + let attr = &attributes[idx]; + let pos = attribute_positions[idx]; + return Some(Self::error_at(SyntaxError::RedefinedAttribute(attr.name.to_string().into()), pos)); + } + + if emit_end_element { + self.pop_namespace = true; + self.next_event = Some(Ok(XmlEvent::EndElement { + name: name.clone() + })); + } else { + self.est.push(name.clone()); + } + let namespace = self.nst.squash(); + self.into_state_emit(State::OutsideTag, Ok(XmlEvent::StartElement { + name, + attributes, + namespace + })) + } + + fn emit_end_element(&mut self) -> Option { + if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } + let mut name = self.data.take_element_name()?; + + // check whether the name prefix is bound and fix its namespace + match self.nst.get(name.borrow().prefix_repr()) { + Some("") => name.namespace = None, // default namespace + Some(ns) => name.namespace = Some(ns.into()), + None => return Some(self.error(SyntaxError::UnboundElementPrefix(name.to_string().into()))), + } + + let op_name = self.est.pop()?; + + if name == op_name { + self.pop_namespace = true; + self.into_state_emit(State::OutsideTag, Ok(XmlEvent::EndElement { name })) + } else { + Some(self.error(SyntaxError::UnexpectedClosingTag(format!("{name} != {op_name}").into()))) + } + } + + fn find_duplicate_attribute(attributes: &[OwnedAttribute]) -> Option { + if attributes.len() >= 8 { + let mut seen = HashSet::with_capacity(attributes.len()); + attributes.iter().enumerate().find(|(_, attr)| { + !seen.insert((attr.name.namespace.as_deref(), &attr.name.local_name)) + }).map(|(i, _)| i) + } else { + // Linear search for duplicates for small number of attributes. + attributes.iter().enumerate().find(|&(i, attr)| { + let identity = (attr.name.namespace.as_deref(), &attr.name.local_name); + attributes[..i].iter().any(|prev| { + identity == (prev.name.namespace.as_deref(), &prev.name.local_name) + }) + }).map(|(i, _)| i) + } + } + + #[inline] + fn is_valid_xml_char(&self, c: char) -> bool { + if Some(XmlVersion::Version11) == self.data.version { + is_xml11_char(c) + } else { + is_xml10_char(c) + } + } + + #[inline] + fn is_valid_xml_char_not_restricted(&self, c: char) -> bool { + if Some(XmlVersion::Version11) == self.data.version { + is_xml11_char_not_restricted(c) + } else { + is_xml10_char(c) + } + } +} + +#[cfg(test)] +mod tests { + use crate::attribute::OwnedAttribute; + use crate::common::TextPosition; + use crate::name::OwnedName; + use crate::reader::events::XmlEvent; + use crate::reader::parser::PullParser; + use crate::reader::ParserConfig; + use std::io::BufReader; + + fn new_parser() -> PullParser { + PullParser::new(ParserConfig::new()) + } + + macro_rules! expect_event( + ($r:expr, $p:expr, $t:pat) => ( + match $p.next(&mut $r) { + $t => {} + e => panic!("Unexpected event: {e:?}\nExpected: {}", stringify!($t)) + } + ); + ($r:expr, $p:expr, $t:pat => $c:expr ) => ( + match $p.next(&mut $r) { + $t if $c => {} + e => panic!("Unexpected event: {e:?}\nExpected: {} if {}", stringify!($t), stringify!($c)) + } + ) + ); + + macro_rules! test_data( + ($d:expr) => ({ + static DATA: &'static str = $d; + let r = BufReader::new(DATA.as_bytes()); + let p = new_parser(); + (r, p) + }) + ); + + #[test] + fn issue_3_semicolon_in_attribute_value() { + let (mut r, mut p) = test_data!(r#" + + "#); + + expect_event!(r, p, Ok(XmlEvent::StartDocument { .. })); + expect_event!(r, p, Ok(XmlEvent::StartElement { ref name, ref attributes, ref namespace }) => + *name == OwnedName::local("a") && + attributes.len() == 1 && + attributes[0] == OwnedAttribute::new(OwnedName::local("attr"), "zzz;zzz") && + namespace.is_essentially_empty() + ); + expect_event!(r, p, Ok(XmlEvent::EndElement { ref name }) => *name == OwnedName::local("a")); + expect_event!(r, p, Ok(XmlEvent::EndDocument)); + } + + #[test] + fn issue_140_entity_reference_inside_tag() { + let (mut r, mut p) = test_data!(r" + ♫ + "); + + expect_event!(r, p, Ok(XmlEvent::StartDocument { .. })); + expect_event!(r, p, Ok(XmlEvent::StartElement { ref name, .. }) => *name == OwnedName::local("bla")); + expect_event!(r, p, Ok(XmlEvent::Characters(ref s)) => s == "\u{266b}"); + expect_event!(r, p, Ok(XmlEvent::EndElement { ref name, .. }) => *name == OwnedName::local("bla")); + expect_event!(r, p, Ok(XmlEvent::EndDocument)); + } + + #[test] + fn issue_220_comment() { + let (mut r, mut p) = test_data!(r""); + expect_event!(r, p, Ok(XmlEvent::StartDocument { .. })); + expect_event!(r, p, Ok(XmlEvent::StartElement { .. })); + expect_event!(r, p, Ok(XmlEvent::EndElement { .. })); + expect_event!(r, p, Ok(XmlEvent::EndDocument)); + + let (mut r, mut p) = test_data!(r""); + expect_event!(r, p, Ok(XmlEvent::StartDocument { .. })); + expect_event!(r, p, Ok(XmlEvent::StartElement { .. })); + expect_event!(r, p, Err(_)); // ---> is forbidden in comments + + let (mut r, mut p) = test_data!(r""); + p.config.ignore_comments = false; + expect_event!(r, p, Ok(XmlEvent::StartDocument { .. })); + expect_event!(r, p, Ok(XmlEvent::StartElement { .. })); + expect_event!(r, p, Ok(XmlEvent::Comment(s)) => s == " "#); + expect_event!(r, p, Err(_)); + + let (mut r, mut p) = test_data!(r#""#); + expect_event!(r, p, Err(_)); + + let (mut r, mut p) = test_data!(r#""#); + expect_event!(r, p, Err(_)); + + let (mut r, mut p) = test_data!(r#""#); + expect_event!(r, p, Err(_)); + + let (mut r, mut p) = test_data!(r#""#); + expect_event!(r, p, Err(_)); + } + + #[test] + fn opening_tag_in_attribute_value() { + use crate::reader::error::{SyntaxError, Error}; + + let (mut r, mut p) = test_data!(r#" + + "#); + + expect_event!(r, p, Ok(XmlEvent::StartDocument { .. })); + expect_event!(r, p, Err(ref e) => + *e == Error::syntax(SyntaxError::UnexpectedOpeningTag.to_cow(), TextPosition { row: 1, column: 24 })); + } + + #[test] + fn processing_instruction_in_attribute_value() { + use crate::reader::error::{Error, SyntaxError}; + + let (mut r, mut p) = test_data!(r#" + + "#); + + expect_event!(r, p, Ok(XmlEvent::StartDocument { .. })); + expect_event!(r, p, Err(ref e) => + *e == Error::syntax(SyntaxError::UnexpectedOpeningTag.to_cow(), + TextPosition { row: 1, column: 18 })); + } + + #[test] + fn reference_err() { + let (mut r, mut p) = test_data!(r" + && + "); + + expect_event!(r, p, Ok(XmlEvent::StartDocument { .. })); + expect_event!(r, p, Ok(XmlEvent::StartElement { .. })); + expect_event!(r, p, Err(_)); + } + + #[test] + fn state_size() { + assert_eq!(2, std::mem::size_of::()); + assert_eq!(1, std::mem::size_of::()); + } +} diff --git a/vendor/xml/src/reader/parser/attribute_expansion.rs b/vendor/xml/src/reader/parser/attribute_expansion.rs new file mode 100644 index 00000000000..31bc10b000a --- /dev/null +++ b/vendor/xml/src/reader/parser/attribute_expansion.rs @@ -0,0 +1,158 @@ +use super::PullParser; +use crate::common::Position; +use crate::reader::error::SyntaxError; +use crate::reader::{Error, Result}; + +impl PullParser { + pub(super) fn expand_attribute(&mut self, raw: &str) -> Result { + let mut value = String::new(); + Self::expand_attribute_part( + raw, + 0, + &self.entities, + &self.config, + self.data.version, + &mut self.lexer, + &mut value, + )?; + Ok(value) + } + + fn expand_attribute_part( + raw: &str, + depth: usize, + entities: &std::collections::HashMap, + config: &crate::reader::ParserConfig, + version: Option, + lexer: &mut crate::reader::lexer::Lexer, + value: &mut String, + ) -> Result<()> { + let error = |e: SyntaxError| Error::syntax(e.to_cow(), lexer.position()); + if depth > usize::from(config.max_entity_expansion_depth) { + return Err(error(SyntaxError::EntityTooBig)); + } + let mut rest = raw; + while !rest.is_empty() { + let split = rest.split_once('&'); + let (literal, tail) = split.unwrap_or((rest, "")); + if literal.contains('<') { + return Err(Error::syntax( + SyntaxError::UnexpectedOpeningTag.to_cow(), + lexer.position(), + )); + } + if value.len().saturating_add(literal.len()) > config.max_attribute_length { + return Err(Error::syntax( + SyntaxError::ExceededConfiguredLimit.to_cow(), + lexer.position(), + )); + } + // XML attribute whitespace normalization also applies to the + // replacement text of general entities, but not to numeric + // references written directly in the attribute. + let mut chars = literal.chars().peekable(); + while let Some(c) = chars.next() { + value.push(if matches!(c, '\t' | '\n' | '\r') { + ' ' + } else { + c + }); + if depth == 0 && c == '\r' && chars.peek() == Some(&'\n') { + chars.next(); + } + } + if split.is_none() { + break; + } + let Some((name, after)) = tail.split_once(';') else { + return Err(Error::syntax( + SyntaxError::EmptyEntity.to_cow(), + lexer.position(), + )); + }; + rest = after; + let predefined = match name { + "lt" => Some('<'), + "gt" => Some('>'), + "amp" => Some('&'), + "apos" => Some('\''), + "quot" => Some('"'), + _ => None, + }; + if let Some(c) = predefined { + lexer.account_indirect(c.len_utf8(), false)?; + value.push(c); + } else if let Some(number) = name.strip_prefix('#') { + let n = if let Some(hex) = number.strip_prefix('x') { + u32::from_str_radix(hex, 16) + } else { + number.parse::() + }; + let n = n.map_err(|_| { + Error::syntax( + SyntaxError::InvalidNumericEntity(number.into()).to_cow(), + lexer.position(), + ) + })?; + let valid = char::from_u32(n).filter(|&c| { + if version == Some(crate::common::XmlVersion::Version11) { + crate::common::is_xml11_char(c) + } else { + crate::common::is_xml10_char(c) + } + }); + let c = if let Some(c) = valid { + c + } else if config.replace_unknown_entity_references { + '\u{fffd}' + } else { + return Err(Error::syntax( + SyntaxError::InvalidCharacterEntity(n).to_cow(), + lexer.position(), + )); + }; + value.push(c); + } else if let Some(entity) = config.extra_entities.get(name) { + lexer.account_indirect(entity.len(), true)?; + if value.len().saturating_add(entity.len()) > config.max_attribute_length { + return Err(Error::syntax( + SyntaxError::ExceededConfiguredLimit.to_cow(), + lexer.position(), + )); + } + value.push_str(entity); + } else if let Some(entity) = entities.get(name) { + // Charge each replacement before recursively materializing it. + // The spelling of nested references is counted again when used. + lexer.account_indirect(entity.len(), true)?; + if entity.len() > config.max_entity_expansion_length { + return Err(Error::syntax( + SyntaxError::EntityTooBig.to_cow(), + lexer.position(), + )); + } + Self::expand_attribute_part( + entity, + depth + 1, + entities, + config, + version, + lexer, + value, + )?; + } else { + return Err(Error::syntax( + SyntaxError::UnexpectedEntity(name.into()).to_cow(), + lexer.position(), + )); + } + if value.len() > config.max_attribute_length { + return Err(Error::syntax( + SyntaxError::ExceededConfiguredLimit.to_cow(), + lexer.position(), + )); + } + } + Ok(()) + } +} diff --git a/vendor/xml/src/reader/parser/inside_cdata.rs b/vendor/xml/src/reader/parser/inside_cdata.rs new file mode 100644 index 00000000000..294e8d677b1 --- /dev/null +++ b/vendor/xml/src/reader/parser/inside_cdata.rs @@ -0,0 +1,41 @@ +use crate::common::is_whitespace_char; +use crate::reader::error::SyntaxError; +use crate::reader::events::XmlEvent; +use crate::reader::lexer::Token; + +use super::{PullParser, Result, State}; + +impl PullParser { + pub fn inside_cdata(&mut self, t: Token) -> Option { + match t { + Token::CDataEnd => { + let event = if self.config.cdata_to_characters { + // start called push_pos, but there will be no event to pop it + if self.buf.is_empty() { + self.next_pos(); + } + None + } else { + let data = self.take_buf(); + Some(Ok(XmlEvent::CData(data))) + }; + self.into_state(State::OutsideTag, event) + }, + + Token::Character(c) if !self.is_valid_xml_char(c) => { + Some(self.error(SyntaxError::InvalidCharacterEntity(c as u32))) + }, + Token::Character(c) => { + if !is_whitespace_char(c) { + self.inside_whitespace = false; + } + self.buf.push(c); + None + }, + _ => { + debug_assert!(false, "unreachable"); + None + }, + } + } +} diff --git a/vendor/xml/src/reader/parser/inside_closing_tag_name.rs b/vendor/xml/src/reader/parser/inside_closing_tag_name.rs new file mode 100644 index 00000000000..63d02318a78 --- /dev/null +++ b/vendor/xml/src/reader/parser/inside_closing_tag_name.rs @@ -0,0 +1,32 @@ +use super::{ClosingTagSubstate, PullParser, QualifiedNameTarget, Result, State}; +use crate::common::is_whitespace_char; +use crate::namespace; +use crate::reader::error::SyntaxError; +use crate::reader::lexer::Token; + +impl PullParser { + pub fn inside_closing_tag_name(&mut self, t: Token, s: ClosingTagSubstate) -> Option { + match s { + ClosingTagSubstate::CTInsideName => self.read_qualified_name(t, QualifiedNameTarget::ClosingTag, |this, token, name| { + match name.prefix_ref() { + Some(prefix) if prefix == namespace::NS_XML_PREFIX || + prefix == namespace::NS_XMLNS_PREFIX => + Some(this.error(SyntaxError::InvalidNamePrefix(prefix.into()))), + _ => { + this.data.element_name = Some(name.clone()); + match token { + Token::TagEnd => this.emit_end_element(), + Token::Character(c) if is_whitespace_char(c) => this.into_state_continue(State::InsideClosingTag(ClosingTagSubstate::CTAfterName)), + _ => Some(this.error(SyntaxError::UnexpectedTokenInClosingTag(token))), + } + } + } + }), + ClosingTagSubstate::CTAfterName => match t { + Token::TagEnd => self.emit_end_element(), + Token::Character(c) if is_whitespace_char(c) => None, // Skip whitespace + _ => Some(self.error(SyntaxError::UnexpectedTokenInClosingTag(t))), + }, + } + } +} diff --git a/vendor/xml/src/reader/parser/inside_comment.rs b/vendor/xml/src/reader/parser/inside_comment.rs new file mode 100644 index 00000000000..7ebefd924d8 --- /dev/null +++ b/vendor/xml/src/reader/parser/inside_comment.rs @@ -0,0 +1,37 @@ +use crate::reader::error::SyntaxError; +use crate::reader::events::XmlEvent; +use crate::reader::lexer::Token; + +use super::{PullParser, Result, State}; + +impl PullParser { + pub fn inside_comment(&mut self, t: Token) -> Option { + if t == Token::CommentEnd { + if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } + } + match t { + Token::CommentEnd if self.config.ignore_comments => { + self.into_state_continue(State::OutsideTag) + }, + + Token::CommentEnd => { + let data = self.take_buf(); + self.into_state_emit(State::OutsideTag, Ok(XmlEvent::Comment(data))) + }, + + Token::Character(c) if !self.is_valid_xml_char(c) => { + Some(self.error(SyntaxError::InvalidCharacterEntity(c as u32))) + }, + + _ if self.config.ignore_comments => None, // Do not modify buffer if ignoring the comment + + _ => { + if self.buf.len() > self.config.max_data_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + t.push_to_string(&mut self.buf); + None + }, + } + } +} diff --git a/vendor/xml/src/reader/parser/inside_declaration.rs b/vendor/xml/src/reader/parser/inside_declaration.rs new file mode 100644 index 00000000000..c9a507ae20c --- /dev/null +++ b/vendor/xml/src/reader/parser/inside_declaration.rs @@ -0,0 +1,204 @@ +use crate::common::{is_whitespace_char, XmlVersion}; +use crate::reader::error::SyntaxError; +use crate::reader::events::XmlEvent; +use crate::reader::lexer::Token; +use crate::util::Encoding; + +use super::{ + DeclarationSubstate, Encountered, PullParser, QualifiedNameTarget, Result, State, + DEFAULT_VERSION, +}; + +impl PullParser { + #[inline(never)] + fn emit_start_document(&mut self) -> Option { + debug_assert!(self.encountered == Encountered::None); + self.encountered = Encountered::Declaration; + + let version = self.data.version; + let encoding = self.data.take_encoding(); + let standalone = self.data.standalone; + + if let Some(new_encoding) = encoding.as_deref() { + let new_encoding = match new_encoding.parse() { + Ok(e) => e, + Err(_) if self.config.ignore_invalid_encoding_declarations => Encoding::Latin1, + Err(_) => return Some(self.error(SyntaxError::UnsupportedEncoding(new_encoding.into()))), + }; + let current_encoding = self.lexer.encoding(); + if current_encoding != new_encoding { + let set = match (current_encoding, new_encoding) { + (Encoding::Unknown | Encoding::Default, new) if new != Encoding::Utf16 => new, + (Encoding::Utf16Be | Encoding::Utf16Le, Encoding::Utf16) => current_encoding, + _ if self.config.ignore_invalid_encoding_declarations => current_encoding, + _ => return Some(self.error(SyntaxError::ConflictingEncoding(new_encoding, current_encoding))), + }; + self.lexer.set_encoding(set); + } + } + + let current_encoding = self.lexer.encoding(); + self.into_state_emit(State::OutsideTag, Ok(XmlEvent::StartDocument { + version: version.unwrap_or(DEFAULT_VERSION), + encoding: encoding.unwrap_or_else(move || current_encoding.to_string()), + standalone + })) + } + + // TODO: remove redundancy via macros or extra methods + pub fn inside_declaration(&mut self, t: Token, s: DeclarationSubstate) -> Option { + match s { + DeclarationSubstate::BeforeVersion => match t { + Token::Character('v') => { + self.into_state_continue(State::InsideDeclaration(DeclarationSubstate::InsideVersion)) + }, + Token::Character(c) if is_whitespace_char(c) => None, // continue + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + + DeclarationSubstate::InsideVersion => self.read_qualified_name(t, QualifiedNameTarget::Attribute, |this, token, name| { + match &*name.local_name { + "ersion" if name.namespace.is_none() => + this.into_state_continue(State::InsideDeclaration( + if token == Token::EqualsSign { + DeclarationSubstate::InsideVersionValue + } else { + DeclarationSubstate::AfterVersion + } + )), + _ => Some(this.error(SyntaxError::UnexpectedNameInsideXml(name.to_string().into()))), + } + }), + + DeclarationSubstate::AfterVersion => match t { + Token::EqualsSign => { + self.into_state_continue(State::InsideDeclaration(DeclarationSubstate::InsideVersionValue)) + }, + Token::Character(c) if is_whitespace_char(c) => None, + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + + DeclarationSubstate::InsideVersionValue => self.read_attribute_value(t, |this, value| { + this.data.version = match &*value { + "1.0" => Some(XmlVersion::Version10), + "1.1" => Some(XmlVersion::Version11), + // https://www.w3.org/TR/REC-xml/#sec-prolog-dtd + // VersionNum ::= '1.' [0-9]+ + // "[...] This means that an XML 1.0 processor will accept 1.x + // documents provided they do not use any non-1.0 features." + v if v.starts_with("1.") + && v.len() >= 3 + && v.chars().skip(2).all(|c| c.is_ascii_digit()) => + { + // XML 1.1 forbids any other versions + Some(XmlVersion::Version10) + }, + _ => None, + }; + if this.data.version.is_some() { + this.into_state_continue(State::InsideDeclaration(DeclarationSubstate::AfterVersionValue)) + } else { + Some(this.error(SyntaxError::UnexpectedXmlVersion(value.into()))) + } + }), + + DeclarationSubstate::AfterVersionValue => match t { + Token::Character(c) if is_whitespace_char(c) => { + self.into_state_continue(State::InsideDeclaration(DeclarationSubstate::BeforeEncoding)) + }, + Token::ProcessingInstructionEnd => self.emit_start_document(), + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + + DeclarationSubstate::BeforeEncoding => match t { + Token::Character('e') => self.into_state_continue(State::InsideDeclaration(DeclarationSubstate::InsideEncoding)), + Token::Character('s') => self.into_state_continue(State::InsideDeclaration(DeclarationSubstate::InsideStandaloneDecl)), + Token::ProcessingInstructionEnd => self.emit_start_document(), + Token::Character(c) if is_whitespace_char(c) => None, // skip whitespace + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + + DeclarationSubstate::InsideEncoding => self.read_qualified_name(t, QualifiedNameTarget::Attribute, |this, token, name| { + match &*name.local_name { + "ncoding" if name.namespace.is_none() => + this.into_state_continue(State::InsideDeclaration( + if token == Token::EqualsSign { DeclarationSubstate::InsideEncodingValue } else { DeclarationSubstate::AfterEncoding } + )), + _ => Some(this.error(SyntaxError::UnexpectedName(name.to_string().into()))) + } + }), + + DeclarationSubstate::AfterEncoding => match t { + Token::EqualsSign => { + self.into_state_continue(State::InsideDeclaration(DeclarationSubstate::InsideEncodingValue)) + }, + Token::Character(c) if is_whitespace_char(c) => None, + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + + DeclarationSubstate::InsideEncodingValue => self.read_attribute_value(t, |this, value| { + this.data.encoding = Some(value); + this.into_state_continue(State::InsideDeclaration(DeclarationSubstate::AfterEncodingValue)) + }), + + DeclarationSubstate::AfterEncodingValue => match t { + Token::Character(c) if is_whitespace_char(c) => { + self.into_state_continue(State::InsideDeclaration(DeclarationSubstate::BeforeStandaloneDecl)) + }, + Token::ProcessingInstructionEnd => self.emit_start_document(), + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + + DeclarationSubstate::BeforeStandaloneDecl => match t { + Token::Character('s') => { + self.into_state_continue(State::InsideDeclaration(DeclarationSubstate::InsideStandaloneDecl)) + }, + Token::ProcessingInstructionEnd => self.emit_start_document(), + Token::Character(c) if is_whitespace_char(c) => None, // skip whitespace + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + + DeclarationSubstate::InsideStandaloneDecl => self.read_qualified_name(t, QualifiedNameTarget::Attribute, |this, token, name| { + match &*name.local_name { + "tandalone" if name.namespace.is_none() => + this.into_state_continue(State::InsideDeclaration( + if token == Token::EqualsSign { + DeclarationSubstate::InsideStandaloneDeclValue + } else { + DeclarationSubstate::AfterStandaloneDecl + } + )), + _ => Some(this.error(SyntaxError::UnexpectedName(name.to_string().into()))), + } + }), + + DeclarationSubstate::AfterStandaloneDecl => match t { + Token::EqualsSign => { + self.into_state_continue(State::InsideDeclaration(DeclarationSubstate::InsideStandaloneDeclValue)) + }, + Token::Character(c) if is_whitespace_char(c) => None, + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + + DeclarationSubstate::InsideStandaloneDeclValue => self.read_attribute_value(t, |this, value| { + let standalone = match &*value { + "yes" => Some(true), + "no" => Some(false), + _ => None + }; + if standalone.is_some() { + this.data.standalone = standalone; + this.into_state_continue(State::InsideDeclaration(DeclarationSubstate::AfterStandaloneDeclValue)) + } else { + Some(this.error(SyntaxError::InvalidStandaloneDeclaration(value.into()))) + } + }), + + DeclarationSubstate::AfterStandaloneDeclValue => match t { + Token::ProcessingInstructionEnd => self.emit_start_document(), + Token::Character(c) if is_whitespace_char(c) => None, // skip whitespace + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + } + } +} diff --git a/vendor/xml/src/reader/parser/inside_doctype.rs b/vendor/xml/src/reader/parser/inside_doctype.rs new file mode 100644 index 00000000000..b1d370764d2 --- /dev/null +++ b/vendor/xml/src/reader/parser/inside_doctype.rs @@ -0,0 +1,437 @@ +use std::fmt::Write; + +use crate::common::{is_name_char, is_name_start_char, is_pubid_char, is_whitespace_char}; +use crate::reader::error::SyntaxError; +use crate::reader::lexer::Token; +use crate::reader::XmlEvent; + +use super::{DoctypeSubstate, PullParser, QuoteToken, Result, State}; + +impl PullParser { + pub fn inside_doctype(&mut self, t: Token, substate: DoctypeSubstate) -> Option { + if let Some(ref mut doctype) = self.data.doctype { + write!(doctype, "{t}").ok()?; + if doctype.len() > self.config.max_data_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + } + + if self.config.amplification_limits.is_some() { + // Normalize physical newlines before storing entity text. Numeric + // character references retain their CR/LF identity for later + // attribute normalization. + let skip_lf = self.entity_value_after_cr && substate == DoctypeSubstate::EntityValue && t == Token::Character('\n'); + self.entity_value_after_cr = substate == DoctypeSubstate::EntityValue && t == Token::Character('\r'); + if skip_lf { return None; } + } + match substate { + DoctypeSubstate::BeforeDoctypeName => match t { + Token::Character(c) if is_whitespace_char(c) => None, + Token::Character(c) if is_name_start_char(c) => { + self.buf.push(c); + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::DoctypeName)) + }, + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + DoctypeSubstate::DoctypeName => match t { + Token::TagEnd => { + self.data.doctype_name = Some(self.take_buf_boxed()); + let event = XmlEvent::Doctype { + syntax: self.data.doctype.clone().unwrap_or_default(), + }; + self.into_state_emit(State::OutsideTag, Ok(event)) + }, + Token::Character(c) if is_whitespace_char(c) => { + self.data.doctype_name = Some(self.take_buf_boxed()); + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::Outside)) + }, + Token::Character(c) if is_name_char(c) => { + self.buf.push(c); + if self.buf.len() > self.config.max_name_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + None + }, + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + DoctypeSubstate::ExternalIdKeyword => match t { + Token::Character(c @ 'A'..='Z') => { + self.buf.push(c); + if self.buf == "SYSTEM" { + self.buf.clear(); + return self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::BeforeSystemLiteral, + )); + } + if self.buf == "PUBLIC" { + self.buf.clear(); + return self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::BeforePubId, + )); + } + if "PUBLIC".starts_with(&self.buf) || "SYSTEM".starts_with(&self.buf) { + return None; + } + Some(self.error(SyntaxError::UnexpectedToken(t))) + }, + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + DoctypeSubstate::BeforeSystemLiteral => match t { + Token::Character(c) if is_whitespace_char(c) => None, + Token::SingleQuote | Token::DoubleQuote => { + self.data.quote = QuoteToken::from_token(t); + self.buf.clear(); + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::SystemLiteral)) + }, + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + DoctypeSubstate::SystemLiteral => match t { + Token::SingleQuote if self.data.quote != Some(QuoteToken::SingleQuoteToken) => { + self.buf.push('\''); + None + }, + Token::DoubleQuote if self.data.quote != Some(QuoteToken::DoubleQuoteToken) => { + self.buf.push('"'); + None + }, + Token::SingleQuote | Token::DoubleQuote => { + self.data.quote = None; + self.data.doctype_system_id = Some(self.take_buf_boxed()); + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::Outside)) + }, + Token::Character(c) => { + self.buf.push(c); + None + }, + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + DoctypeSubstate::BeforePubId => match t { + Token::Character(c) if is_whitespace_char(c) => None, + Token::SingleQuote | Token::DoubleQuote => { + self.data.quote = QuoteToken::from_token(t); + self.buf.clear(); + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::PubId)) + }, + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + DoctypeSubstate::PubId => match t { + Token::SingleQuote if self.data.quote != Some(QuoteToken::SingleQuoteToken) => { + self.buf.push('\''); + None + }, + Token::DoubleQuote if self.data.quote != Some(QuoteToken::DoubleQuoteToken) => { + self.buf.push('"'); + None + }, + Token::SingleQuote | Token::DoubleQuote => { + self.data.quote = None; + self.data.doctype_public_id = Some(self.take_buf_boxed()); + self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::BeforeSystemLiteral, + )) + } + Token::Character(c) if is_pubid_char(c) => { + self.buf.push(c); + None + }, + Token::ReferenceEnd => { + self.buf.push(';'); + None + }, + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + DoctypeSubstate::Outside => match t { + Token::TagEnd => { + let event = XmlEvent::Doctype { + syntax: self.data.doctype.clone().unwrap_or_default(), + }; + self.into_state_emit(State::OutsideTag, Ok(event)) + } + Token::CDataEnd | Token::CDataStart => { + Some(self.error(SyntaxError::UnexpectedToken(t))) + } + Token::Character('[') => { + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::InternalSubset)) + } + Token::Character(c @ ('S' | 'P')) => { + self.buf.push(c); + self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::ExternalIdKeyword, + )) + } + Token::Character(c) if is_whitespace_char(c) => None, + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + DoctypeSubstate::InternalSubset => match t { + Token::Character(']') => { + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::Outside)) + } + + Token::SingleQuote | Token::DoubleQuote => { + // just discard string literals + self.data.quote = QuoteToken::from_token(t); + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::String)) + } + Token::CommentStart => { + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::Comment)) + } + Token::Character('%') => { + self.data.ref_data.clear(); + self.data.ref_data.push('%'); + self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::PEReferenceInDtd, + )) + } + Token::MarkupDeclarationStart => { + self.buf.clear(); + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::InsideName)) + } + Token::Character(c) if is_whitespace_char(c) => None, + Token::ProcessingInstructionStart => { + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::IgnorePI)) + } + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + DoctypeSubstate::IgnorePI => match t { + Token::ProcessingInstructionEnd => { + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::InternalSubset)) + } + _ => None, + }, + DoctypeSubstate::String => match t { + Token::SingleQuote if self.data.quote != Some(QuoteToken::SingleQuoteToken) => None, + Token::DoubleQuote if self.data.quote != Some(QuoteToken::DoubleQuoteToken) => None, + Token::SingleQuote | Token::DoubleQuote => { + self.data.quote = None; + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::InternalSubset)) + } + _ => None, + }, + DoctypeSubstate::Comment => match t { + Token::CommentEnd => { + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::InternalSubset)) + } + _ => None, + }, + DoctypeSubstate::InsideName => match t { + Token::Character(c @ 'A'..='Z') => { + self.buf.push(c); + None + }, + Token::Character(c) if is_whitespace_char(c) => { + let state = match self.buf.as_str() { + "ENTITY" => self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::BeforeEntityName, + )), + "NOTATION" | "ELEMENT" | "ATTLIST" => self.into_state_continue( + State::InsideDoctype(DoctypeSubstate::SkipDeclaration), + ), + _ => Some(self.error(SyntaxError::UnknownMarkupDeclaration(self.buf.as_str().into()))), + }; + self.buf.clear(); + state + }, + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + }, + DoctypeSubstate::BeforeEntityName => { + self.data.name.clear(); + match t { + Token::Character(c) if is_whitespace_char(c) => None, + Token::Character('%') => { + // % is for PEDecl + self.data.name.push('%'); + self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::PEReferenceDefinitionStart, + )) + } + Token::Character(c) if is_name_start_char(c) => { + if self.data.name.len() > self.config.max_name_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + self.data.name.push(c); + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::EntityName)) + }, + _ => Some(self.error(SyntaxError::UnexpectedTokenInEntity(t))), + } + }, + DoctypeSubstate::EntityName => match t { + Token::Character(c) if is_whitespace_char(c) => self + .into_state_continue(State::InsideDoctype(DoctypeSubstate::BeforeEntityValue)), + Token::Character(c) if is_name_char(c) => { + if self.data.name.len() > self.config.max_name_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + self.data.name.push(c); + None + }, + _ => Some(self.error(SyntaxError::UnexpectedTokenInEntity(t))), + }, + DoctypeSubstate::BeforeEntityValue => { + self.buf.clear(); + match t { + Token::Character(c) if is_whitespace_char(c) => None, + // SYSTEM/PUBLIC not supported + Token::Character('S' | 'P') => { + let name = self.data.take_name(); + self.entities.entry(name).or_default(); // Dummy value, but at least the name is recognized + + self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::SkipDeclaration, + )) + } + Token::SingleQuote | Token::DoubleQuote => { + self.data.quote = QuoteToken::from_token(t); + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::EntityValue)) + } + _ => Some(self.error(SyntaxError::UnexpectedTokenInEntity(t))), + } + } + DoctypeSubstate::EntityValue => match t { + Token::SingleQuote if self.data.quote != Some(QuoteToken::SingleQuoteToken) => { + self.buf.push('\''); + None + } + Token::DoubleQuote if self.data.quote != Some(QuoteToken::DoubleQuoteToken) => { + self.buf.push('"'); + None + } + Token::SingleQuote | Token::DoubleQuote => { + self.data.quote = None; + let name = self.data.take_name(); + let val = self.take_buf(); + self.entities.entry(name).or_insert(val); // First wins + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::SkipDeclaration)) + // FIXME + } + Token::ReferenceStart | Token::Character('&') => { + self.data.ref_data.clear(); + self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::NumericReferenceStart, + )) + } + Token::Character('%') => { + self.data.ref_data.clear(); + self.data.ref_data.push('%'); // include literal % in the name to distinguish from regular entities + self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::PEReferenceInValue, + )) + } + Token::Character(c) if !self.is_valid_xml_char(c) => { + Some(self.error(SyntaxError::InvalidCharacterEntity(c as u32))) + } + Token::Character(c) => { + self.buf.push(if self.config.amplification_limits.is_some() && c == '\r' { '\n' } else { c }); + None + } + _ => Some(self.error(SyntaxError::UnexpectedTokenInEntity(t))), + }, + DoctypeSubstate::PEReferenceDefinitionStart => match t { + Token::Character(c) if is_whitespace_char(c) => None, + Token::Character(c) if is_name_start_char(c) => { + debug_assert_eq!(self.data.name, "%"); + self.data.name.push(c); + self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::PEReferenceDefinition, + )) + } + _ => Some(self.error(SyntaxError::UnexpectedTokenInEntity(t))), + }, + DoctypeSubstate::PEReferenceDefinition => match t { + Token::Character(c) if is_name_char(c) => { + if self.data.name.len() > self.config.max_name_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + self.data.name.push(c); + None + } + Token::Character(c) if is_whitespace_char(c) => self + .into_state_continue(State::InsideDoctype(DoctypeSubstate::BeforeEntityValue)), + _ => Some(self.error(SyntaxError::UnexpectedTokenInEntity(t))), + }, + DoctypeSubstate::PEReferenceInDtd => match t { + Token::Character(c) if is_name_char(c) => { + self.data.ref_data.push(c); + None + } + Token::ReferenceEnd | Token::Character(';') => { + let name = self.data.take_ref_data(); + match self.entities.get(&name) { + Some(ent) => { + if let Err(e) = self.lexer.reparse(ent) { + return Some(Err(e)); + } + self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::InternalSubset, + )) + } + None => Some(self.error(SyntaxError::UndefinedEntity(name.into()))), + } + } + _ => Some(self.error(SyntaxError::UnexpectedTokenInEntity(t))), + }, + DoctypeSubstate::PEReferenceInValue => match t { + Token::Character(c) if is_name_char(c) => { + self.data.ref_data.push(c); + None + } + Token::ReferenceEnd | Token::Character(';') => { + let name = self.data.take_ref_data(); + match self.entities.get(&name) { + Some(ent) => { + if let Err(e) = self.lexer.account_indirect(ent.len(), true) { return Some(Err(e)); } + self.buf.push_str(ent); + self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::EntityValue, + )) + } + None => Some(self.error(SyntaxError::UndefinedEntity(name.into()))), + } + } + _ => Some(self.error(SyntaxError::UnexpectedTokenInEntity(t))), + }, + DoctypeSubstate::NumericReferenceStart => match t { + Token::Character('#') => self + .into_state_continue(State::InsideDoctype(DoctypeSubstate::NumericReference)), + Token::Character(c) if !self.is_valid_xml_char(c) => { + Some(self.error(SyntaxError::InvalidCharacterEntity(c as u32))) + } + Token::Character(c) => { + self.buf.push('&'); + self.buf.push(c); + // named entities are not expanded inside doctype + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::EntityValue)) + } + _ => Some(self.error(SyntaxError::UnexpectedTokenInEntity(t))), + }, + DoctypeSubstate::NumericReference => match t { + Token::ReferenceEnd | Token::Character(';') => { + let r = self.data.take_ref_data(); + // https://www.w3.org/TR/xml/#sec-entexpand + match self.numeric_reference_from_str(&r) { + Ok(c) => { + self.buf.push(c); + self.into_state_continue(State::InsideDoctype( + DoctypeSubstate::EntityValue, + )) + } + Err(e) => Some(self.error(e)), + } + } + Token::Character(c) if !self.is_valid_xml_char(c) => { + Some(self.error(SyntaxError::InvalidCharacterEntity(c as u32))) + } + Token::Character(c) => { + self.data.ref_data.push(c); + None + } + _ => Some(self.error(SyntaxError::UnexpectedTokenInEntity(t))), + }, + DoctypeSubstate::SkipDeclaration => match t { + Token::TagEnd => { + self.into_state_continue(State::InsideDoctype(DoctypeSubstate::InternalSubset)) + } + _ => None, + }, + } + } +} diff --git a/vendor/xml/src/reader/parser/inside_opening_tag.rs b/vendor/xml/src/reader/parser/inside_opening_tag.rs new file mode 100644 index 00000000000..a1efb609acc --- /dev/null +++ b/vendor/xml/src/reader/parser/inside_opening_tag.rs @@ -0,0 +1,124 @@ +use crate::attribute::OwnedAttribute; +use crate::common::{is_name_start_char, is_whitespace_char, Position}; +use crate::namespace; +use crate::reader::error::SyntaxError; + +use crate::reader::lexer::Token; + +use super::{OpeningTagSubstate, PullParser, QualifiedNameTarget, Result, State}; + +impl PullParser { + pub fn inside_opening_tag(&mut self, t: Token, s: OpeningTagSubstate) -> Option { + match s { + OpeningTagSubstate::InsideName => self.read_qualified_name(t, QualifiedNameTarget::OpeningTag, |this, token, name| { + match name.prefix_ref() { + Some(prefix) if prefix == namespace::NS_XML_PREFIX || + prefix == namespace::NS_XMLNS_PREFIX => + Some(this.error(SyntaxError::InvalidNamePrefix(prefix.into()))), + _ => { + this.data.element_name = Some(name.clone()); + match token { + Token::TagEnd => this.emit_start_element(false), + Token::EmptyTagEnd => this.emit_start_element(true), + Token::Character(c) if is_whitespace_char(c) => this.into_state_continue(State::InsideOpeningTag(OpeningTagSubstate::InsideTag)), + _ => { + debug_assert!(false, "unreachable"); + None + }, + } + } + } + }), + + OpeningTagSubstate::InsideTag => match t { + Token::TagEnd => self.emit_start_element(false), + Token::EmptyTagEnd => self.emit_start_element(true), + Token::Character(c) if is_whitespace_char(c) => None, // skip whitespace + Token::Character(c) if is_name_start_char(c) => { + if self.qualified_name_buf.len() > self.config.max_name_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + self.data.attr_pos = Some(self.lexer.position()); + self.qualified_name_buf.push(c); + self.into_state_continue(State::InsideOpeningTag(OpeningTagSubstate::InsideAttributeName)) + }, + _ => Some(self.error(SyntaxError::UnexpectedTokenInOpeningTag(t))), + }, + + OpeningTagSubstate::InsideAttributeName => self.read_qualified_name(t, QualifiedNameTarget::Attribute, |this, token, name| { + this.data.attr_name = Some(name); + match token { + Token::EqualsSign => this.into_state_continue(State::InsideOpeningTag(OpeningTagSubstate::InsideAttributeValue)), + Token::Character(c) if is_whitespace_char(c) => this.into_state_continue(State::InsideOpeningTag(OpeningTagSubstate::AfterAttributeName)), + _ => Some(this.error(SyntaxError::UnexpectedTokenInOpeningTag(t))) // likely unreachable + } + }), + + OpeningTagSubstate::AfterAttributeName => match t { + Token::EqualsSign => { + self.into_state_continue(State::InsideOpeningTag(OpeningTagSubstate::InsideAttributeValue)) + }, + Token::Character(c) if is_whitespace_char(c) => None, + _ => Some(self.error(SyntaxError::UnexpectedTokenInOpeningTag(t))), + }, + + OpeningTagSubstate::InsideAttributeValue => self.read_attribute_value(t, |this, value| { + let name = this.data.take_attr_name()?; // will always succeed here + let pos = this.data.take_attr_pos(); + if this.config.amplification_limits.is_some() { + if this.pending_attributes.len() >= this.config.max_attributes { + return Some(this.error(SyntaxError::ExceededConfiguredLimit)); + } + this.pending_attributes.push((name, value, pos)); + } else if let Err(e) = this.finish_attribute(name, value, pos) { + return Some(Err(e)); + } + this.into_state_continue(State::InsideOpeningTag(OpeningTagSubstate::AfterAttributeValue)) + }), + + OpeningTagSubstate::AfterAttributeValue => match t { + Token::Character(c) if is_whitespace_char(c) => { + self.into_state_continue(State::InsideOpeningTag(OpeningTagSubstate::InsideTag)) + }, + Token::TagEnd => self.emit_start_element(false), + Token::EmptyTagEnd => self.emit_start_element(true), + _ => Some(self.error(SyntaxError::UnexpectedTokenInOpeningTag(t))), + }, + } + } + + pub(super) fn finish_attribute(&mut self, name: crate::name::OwnedName, value: String, + pos: Option) -> crate::reader::Result<()> { + match name.prefix_ref() { + Some(namespace::NS_XMLNS_PREFIX) => { + let ln = &*name.local_name; + let error = if ln == namespace::NS_XMLNS_PREFIX { + Some(SyntaxError::CannotRedefineXmlnsPrefix) + } else if ln == namespace::NS_XML_PREFIX && &*value != namespace::NS_XML_URI { + Some(SyntaxError::CannotRedefineXmlPrefix) + } else if value.is_empty() { + Some(SyntaxError::CannotUndefinePrefix(ln.into())) + } else { None }; + if let Some(e) = error { + return Err(crate::reader::Error::syntax(e.to_cow(), self.lexer.position())); + } + self.nst.put(name.local_name, value); + } + None if &*name.local_name == namespace::NS_XMLNS_PREFIX => { + match &*value { + namespace::NS_XMLNS_PREFIX | namespace::NS_XML_PREFIX | namespace::NS_XML_URI | namespace::NS_XMLNS_URI => + return Err(crate::reader::Error::syntax(SyntaxError::InvalidDefaultNamespace(value.into()).to_cow(), self.lexer.position())), + _ => { self.nst.put(namespace::NS_NO_PREFIX, value); } + } + } + _ => { + if self.data.attributes.len() >= self.config.max_attributes { + return Err(crate::reader::Error::syntax(SyntaxError::ExceededConfiguredLimit.to_cow(), self.lexer.position())); + } + if let Some(pos) = pos { self.data.attribute_positions.push(pos); } + self.data.attributes.push(OwnedAttribute { name, value }); + } + } + Ok(()) + } +} diff --git a/vendor/xml/src/reader/parser/inside_processing_instruction.rs b/vendor/xml/src/reader/parser/inside_processing_instruction.rs new file mode 100644 index 00000000000..642f63b6600 --- /dev/null +++ b/vendor/xml/src/reader/parser/inside_processing_instruction.rs @@ -0,0 +1,116 @@ +use crate::common::{is_name_char, is_name_start_char, is_whitespace_char}; +use crate::reader::error::SyntaxError; + +use crate::reader::events::XmlEvent; +use crate::reader::lexer::Token; + +use super::{DeclarationSubstate, Encountered, ProcessingInstructionSubstate, PullParser, Result, State}; + +impl PullParser { + pub fn inside_processing_instruction(&mut self, t: Token, s: ProcessingInstructionSubstate) -> Option { + if t == Token::ProcessingInstructionEnd { + if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } + } + match s { + ProcessingInstructionSubstate::PIInsideName => match t { + Token::Character(c) if self.buf.is_empty() && is_name_start_char(c) || + self.buf_has_data() && is_name_char(c) => { + if self.buf.len() > self.config.max_name_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + self.buf.push(c); + None + }, + + Token::ProcessingInstructionEnd => { + // self.buf contains PI name + let name = self.take_buf(); + + // Don't need to check for declaration because it has mandatory attributes + // but there is none + match &*name { + // Name is empty, it is an error + "" => Some(self.error(SyntaxError::ProcessingInstructionWithoutName)), + + // Found { + Some(self.error(SyntaxError::InvalidXmlProcessingInstruction(name.into()))) + }, + + // All is ok, emitting event + _ => { + debug_assert!(self.next_event.is_none(), "{:?}", self.next_event); + // can't have a PI before ` { + // self.buf contains PI name + let name = self.take_buf(); + + match &*name { + // We have not ever encountered an element and have not parsed XML declaration + "xml" if self.encountered == Encountered::None => { + self.into_state_continue(State::InsideDeclaration(DeclarationSubstate::BeforeVersion)) + }, + + // Found { + Some(self.error(SyntaxError::InvalidXmlProcessingInstruction(name.into()))) + }, + + // All is ok, starting parsing PI data + _ => { + self.data.name = name; + // can't have a PI before ` { + let buf = self.take_buf_boxed(); + Some(self.error(SyntaxError::UnexpectedProcessingInstruction(buf, t))) + }, + }, + + ProcessingInstructionSubstate::PIInsideData => match t { + Token::ProcessingInstructionEnd => { + let name = self.data.take_name(); + let data = self.take_buf(); + self.into_state_emit( + State::OutsideTag, + Ok(XmlEvent::ProcessingInstruction { name, data: Some(data) }), + ) + }, + + Token::Character(c) if !self.is_valid_xml_char(c) => { + Some(self.error(SyntaxError::InvalidCharacterEntity(c as u32))) + }, + + // Any other token should be treated as plain characters + _ => { + if self.buf.len() > self.config.max_data_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + t.push_to_string(&mut self.buf); + None + }, + }, + } + } +} diff --git a/vendor/xml/src/reader/parser/inside_reference.rs b/vendor/xml/src/reader/parser/inside_reference.rs new file mode 100644 index 00000000000..37e7385bc21 --- /dev/null +++ b/vendor/xml/src/reader/parser/inside_reference.rs @@ -0,0 +1,99 @@ +use super::{PullParser, Result, State}; +use crate::common::{is_name_char, is_name_start_char, is_whitespace_char}; +use crate::reader::error::SyntaxError; +use crate::reader::lexer::Token; +use std::char; + +impl PullParser { + pub fn inside_reference(&mut self, t: Token) -> Option { + match t { + Token::Character(c) if !self.data.ref_data.is_empty() && is_name_char(c) || + self.data.ref_data.is_empty() && (is_name_start_char(c) || c == '#') => { + self.data.ref_data.push(c); + None + }, + + Token::ReferenceEnd => { + let name = self.data.take_ref_data(); + if name.is_empty() { + return Some(self.error(SyntaxError::EmptyEntity)); + } + + // Opening-tag attributes are expanded after the entire raw + // tag has been counted, including namespace declarations. + if self.config.amplification_limits.is_some() + && matches!(self.state_after_reference, State::InsideOpeningTag(_)) { + self.buf.push('&'); + self.buf.push_str(&name); + self.buf.push(';'); + return self.into_state_continue(self.state_after_reference); + } + if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } + let c = match &*name { + "lt" => Some('<'), + "gt" => Some('>'), + "amp" => Some('&'), + "apos" => Some('\''), + "quot" => Some('"'), + _ if name.starts_with('#') => match self.numeric_reference_from_str(&name[1..]) { + Ok(c) => Some(c), + Err(e) => return Some(self.error(e)), + }, + _ => None, + }; + if let Some(c) = c { + // Numeric references are part of the input token; the five + // named predefined replacements count as indirect bytes. + if !name.starts_with('#') { + if let Err(e) = self.lexer.account_indirect(c.len_utf8(), false) { return Some(Err(e)); } + } + self.buf.push(c); + } else if let Some(v) = self.config.extra_entities.get(&name) { + if let Err(e) = self.lexer.account_indirect(v.len(), true) { return Some(Err(e)); } + self.buf.push_str(v); + } else if let Some(v) = self.entities.get(&name) { + if self.state_after_reference == State::OutsideTag { + // an entity can expand to *elements*, so outside of a tag it needs a full reparse + if let Err(e) = self.lexer.reparse(v) { + return Some(Err(e)); + } + } else { + // however, inside attributes it's not allowed to affect attribute quoting, + // so it can't be fed to the lexer + if let Err(e) = self.lexer.account_indirect(v.len(), true) { return Some(Err(e)); } + self.buf.push_str(v); + } + } else { + return Some(self.error(SyntaxError::UnexpectedEntity(name.into()))); + } + let prev_st = self.state_after_reference; + if prev_st == State::OutsideTag && !is_whitespace_char(self.buf.chars().last().unwrap_or('\0')) { + self.inside_whitespace = false; + } + if prev_st == State::OutsideTag && c.is_some() + && self.config.amplification_limits.is_some() && !self.config.coalesce_characters { + let value = self.take_buf(); + self.push_pos(); + return self.into_state_emit(prev_st, Ok(crate::reader::XmlEvent::Characters(value))); + } + self.into_state_continue(prev_st) + }, + + _ => Some(self.error(SyntaxError::UnexpectedTokenInEntity(t))), + } + } + + pub(crate) fn numeric_reference_from_str(&self, num_str: &str) -> std::result::Result { + let val = if let Some(hex) = num_str.strip_prefix('x') { + u32::from_str_radix(hex, 16).map_err(move |_| SyntaxError::InvalidNumericEntity(num_str.into()))? + } else { + num_str.parse::().map_err(move |_| SyntaxError::InvalidNumericEntity(num_str.into()))? + }; + match char::from_u32(val) { + Some(c) if self.is_valid_xml_char(c) => Ok(c), + Some(_) if self.config.replace_unknown_entity_references => Ok('\u{fffd}'), + None if self.config.replace_unknown_entity_references => Ok('\u{fffd}'), + _ => Err(SyntaxError::InvalidCharacterEntity(val)), + } + } +} diff --git a/vendor/xml/src/reader/parser/outside_tag.rs b/vendor/xml/src/reader/parser/outside_tag.rs new file mode 100644 index 00000000000..63d8a0cbcaa --- /dev/null +++ b/vendor/xml/src/reader/parser/outside_tag.rs @@ -0,0 +1,242 @@ +use crate::common::is_whitespace_char; +use crate::reader::error::SyntaxError; +use crate::reader::events::XmlEvent; +use crate::reader::lexer::Token; + +use super::{ + ClosingTagSubstate, DoctypeSubstate, Encountered, OpeningTagSubstate, + ProcessingInstructionSubstate, PullParser, Result, State, +}; + +impl PullParser { + pub fn outside_tag(&mut self, t: Token) -> Option { + match t { + Token::Character(c) => { + if self.config.amplification_limits.is_some() && matches!(c, '\n' | '\r') { + if self.literal_text_pending { + if let Err(e) = self.lexer.check_amplification_before_token() { return Some(Err(e)); } + } + if let Err(e) = self.lexer.check_amplification() { + if !self.buf.is_empty() && !self.config.coalesce_characters { + let text = self.take_buf(); + self.next_event = Some(Err(e)); + return Some(Ok(XmlEvent::Characters(text))); + } + return Some(Err(e)); + } + } + self.literal_text_pending = true; + if is_whitespace_char(c) { + // skip whitespace outside of the root element + if (self.config.trim_whitespace && self.buf.is_empty()) || + (self.depth() == 0 && self.config.ignore_root_level_whitespace) { + return None; + } + } else { + self.inside_whitespace = false; + if self.depth() == 0 { + return Some(self.error(SyntaxError::UnexpectedTokenOutsideRoot(t))); + } + } + + if !self.is_valid_xml_char_not_restricted(c) { + return Some(self.error(SyntaxError::InvalidCharacterEntity(c as u32))); + } + + if self.buf.is_empty() { + self.push_pos(); + } else if self.buf.len() > self.config.max_data_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + self.buf.push(c); + None + }, + + Token::CommentEnd | Token::TagEnd | Token::EqualsSign | + Token::DoubleQuote | Token::SingleQuote | + Token::ProcessingInstructionEnd | Token::EmptyTagEnd => { + if self.depth() == 0 { + return Some(self.error(SyntaxError::UnexpectedTokenOutsideRoot(t))); + } + self.inside_whitespace = false; + self.literal_text_pending = true; + + if let Some(s) = t.as_static_str() { + if self.buf.is_empty() { + self.push_pos(); + } else if self.buf.len() > self.config.max_data_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + + self.buf.push_str(s); + } + None + }, + + Token::ReferenceStart if self.depth() > 0 => { + if self.config.amplification_limits.is_some() && self.literal_text_pending { + if let Err(e) = self.lexer.check_amplification_before_token() { return Some(Err(e)); } + } + self.literal_text_pending = false; + self.state_after_reference = State::OutsideTag; + if self.config.amplification_limits.is_some() && !self.config.coalesce_characters && !self.buf.is_empty() { + let text = self.take_buf(); + return self.into_state_emit(State::InsideReference, Ok(XmlEvent::Characters(text))); + } + self.into_state_continue(State::InsideReference) + }, + + Token::ReferenceEnd if self.depth() > 0 => { // Semi-colon in a text outside an entity + self.literal_text_pending = true; + self.inside_whitespace = false; + if self.buf.len() > self.config.max_data_length { + return Some(self.error(SyntaxError::ExceededConfiguredLimit)); + } + Token::ReferenceEnd.push_to_string(&mut self.buf); + None + }, + + Token::CommentStart if self.config.coalesce_characters && self.config.ignore_comments => { + let next_event = self.set_encountered(Encountered::Comment); + // We need to switch the lexer into a comment mode inside comments + self.into_state(State::InsideComment, next_event) + }, + + Token::CDataStart if self.depth() > 0 && self.config.coalesce_characters && self.config.cdata_to_characters => { + if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } + if self.buf.is_empty() { + self.push_pos(); // CDataEnd will pop pos if the buffer remains empty + } + // if coalescing chars, continue without event + self.into_state_continue(State::InsideCData) + }, + + _ => { + if self.config.amplification_limits.is_some() && self.literal_text_pending { + if let Err(e) = self.lexer.check_amplification_before_token() { return Some(Err(e)); } + } + self.literal_text_pending = false; + // Encountered some markup event, flush the buffer as characters + // or a whitespace + let mut next_event = if self.buf_has_data() { + let buf = self.take_buf(); + if self.inside_whitespace && self.config.trim_whitespace { + // there will be no event emitted for this, but start of buffering has pushed a pos + self.next_pos(); + None + } else if self.inside_whitespace && !self.config.whitespace_to_characters { + debug_assert!(buf.chars().all(|ch| ch.is_whitespace()), "ws={buf:?}"); + Some(Ok(XmlEvent::Whitespace(buf))) + } else if self.config.trim_whitespace { + Some(Ok(XmlEvent::Characters(buf.trim_matches(is_whitespace_char).into()))) + } else { + Some(Ok(XmlEvent::Characters(buf))) + } + } else { None }; + self.inside_whitespace = true; // Reset inside_whitespace flag + + // pos is popped whenever an event is emitted, so pushes must happen only if there will be an event to balance it + // and ignored comments don't pop + if t != Token::CommentStart || !self.config.ignore_comments { + self.push_pos(); + } + match t { + Token::OpeningTagStart if self.depth() > 0 || self.encountered < Encountered::Element || self.config.allow_multiple_root_elements => { + if let Some(e) = self.set_encountered(Encountered::Element) { + next_event = Some(e); + } + self.nst.push_empty(); + self.into_state(State::InsideOpeningTag(OpeningTagSubstate::InsideName), next_event) + }, + + Token::ClosingTagStart if self.depth() > 0 => { + self.into_state(State::InsideClosingTag(ClosingTagSubstate::CTInsideName), next_event) + }, + + Token::CommentStart => { + if let Some(e) = self.set_encountered(Encountered::Comment) { + next_event = Some(e); + } + // We need to switch the lexer into a comment mode inside comments + self.into_state(State::InsideComment, next_event) + }, + + Token::DoctypeStart if self.encountered < Encountered::Doctype => { + if let Some(e) = self.set_encountered(Encountered::Doctype) { + next_event = Some(e); + } + self.data.doctype = Some(Token::DoctypeStart.to_string()); + + self.push_pos(); + self.into_state(State::InsideDoctype(DoctypeSubstate::BeforeDoctypeName), next_event) + }, + + Token::ProcessingInstructionStart => self.into_state( + State::InsideProcessingInstruction(ProcessingInstructionSubstate::PIInsideName), + next_event, + ), + + Token::CDataStart if self.depth() > 0 => { + if let Err(e) = self.lexer.check_amplification() { return Some(Err(e)); } + self.into_state(State::InsideCData, next_event) + }, + + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + } + }, + } + } + + pub fn document_start(&mut self, t: Token) -> Option { + debug_assert!(self.encountered < Encountered::Declaration); + + match t { + Token::Character(c) => { + let next_event = self.set_encountered(Encountered::AnyChars); + + if !is_whitespace_char(c) { + return Some(self.error(SyntaxError::UnexpectedTokenOutsideRoot(t))); + } + self.inside_whitespace = true; + + // skip whitespace outside of the root element + if (self.config.trim_whitespace && self.buf.is_empty()) || + (self.depth() == 0 && self.config.ignore_root_level_whitespace) { + return self.into_state(State::OutsideTag, next_event); + } + + self.push_pos(); + self.buf.push(c); + self.into_state(State::OutsideTag, next_event) + }, + + Token::CommentStart => { + let next_event = self.set_encountered(Encountered::Comment); + self.into_state(State::InsideComment, next_event) + }, + + Token::OpeningTagStart => { + let next_event = self.set_encountered(Encountered::Element); + self.nst.push_empty(); + self.into_state(State::InsideOpeningTag(OpeningTagSubstate::InsideName), next_event) + }, + + Token::DoctypeStart => { + let next_event = self.set_encountered(Encountered::Doctype); + self.data.doctype = Some(Token::DoctypeStart.to_string()); + + self.push_pos(); + self.into_state(State::InsideDoctype(DoctypeSubstate::BeforeDoctypeName), next_event) + }, + + Token::ProcessingInstructionStart => { + self.push_pos(); + self.into_state_continue(State::InsideProcessingInstruction( + ProcessingInstructionSubstate::PIInsideName, + )) + }, + + _ => Some(self.error(SyntaxError::UnexpectedToken(t))), + } + } +} diff --git a/vendor/xml/src/util.rs b/vendor/xml/src/util.rs new file mode 100644 index 00000000000..5bd429b5599 --- /dev/null +++ b/vendor/xml/src/util.rs @@ -0,0 +1,356 @@ +use std::fmt; +use std::io::{self, Read}; +use std::str::{self, FromStr}; + +#[derive(Debug)] +pub(crate) enum CharReadError { + UnexpectedEof, + Utf8(str::Utf8Error), + Io(io::Error), +} + +impl From for CharReadError { + #[cold] + fn from(e: str::Utf8Error) -> Self { + Self::Utf8(e) + } +} + +impl From for CharReadError { + #[cold] + fn from(e: io::Error) -> Self { + Self::Io(e) + } +} + +impl fmt::Display for CharReadError { + #[cold] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use self::CharReadError::{Io, UnexpectedEof, Utf8}; + match *self { + UnexpectedEof => write!(f, "unexpected end of stream"), + Utf8(ref e) => write!(f, "UTF-8 decoding error: {e}"), + Io(ref e) => write!(f, "I/O error: {e}"), + } + } +} + +/// Character encoding used for parsing +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub enum Encoding { + /// Explicitly UTF-8 only + Utf8, + /// UTF-8 fallback, but can be any 8-bit encoding + Default, + /// ISO-8859-1 + Latin1, + /// US-ASCII + Ascii, + /// Big-Endian + Utf16Be, + /// Little-Endian + Utf16Le, + /// Unknown endianness yet, will be sniffed + Utf16, + /// Not determined yet, may be sniffed to be anything + Unknown, +} + +// Rustc inlines eq_ignore_ascii_case and creates kilobytes of code! +#[inline(never)] +fn icmp(lower: &str, varcase: &str) -> bool { + lower.bytes().zip(varcase.bytes()).all(|(l, v)| l == v.to_ascii_lowercase()) +} + +impl FromStr for Encoding { + type Err = &'static str; + + fn from_str(val: &str) -> Result { + if ["utf-8", "utf8"].into_iter().any(move |label| icmp(label, val)) { + Ok(Self::Utf8) + } else if ["iso-8859-1", "latin1"].into_iter().any(move |label| icmp(label, val)) { + Ok(Self::Latin1) + } else if ["utf-16", "utf16"].into_iter().any(move |label| icmp(label, val)) { + Ok(Self::Utf16) + } else if ["ascii", "us-ascii"].into_iter().any(move |label| icmp(label, val)) { + Ok(Self::Ascii) + } else { + Err("unknown encoding name") + } + } +} + +impl fmt::Display for Encoding { + #[cold] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Utf8 | + Self::Default => "UTF-8", + Self::Latin1 => "ISO-8859-1", + Self::Ascii => "US-ASCII", + Self::Utf16Be | + Self::Utf16Le | + Self::Utf16 => "UTF-16", + Self::Unknown => "(unknown)", + }) + } +} + +#[derive(Clone)] +pub(crate) struct CharReader { + pub encoding: Encoding, +} + +impl CharReader { + pub fn new(encoding: Encoding) -> Self { + Self { + encoding, + } + } + + #[inline] + pub fn next_char_from(&mut self, source: &mut R) -> Result, CharReadError> { + let mut bytes = source.bytes(); + const MAX_CODEPOINT_LEN: usize = 4; + + let mut buf = [0u8; MAX_CODEPOINT_LEN]; + let mut pos = 0; + while pos < MAX_CODEPOINT_LEN { + let next = match bytes.next() { + Some(Ok(b)) => b, + Some(Err(e)) => return Err(e.into()), + None if pos == 0 => return Ok(None), + None => return Err(CharReadError::UnexpectedEof), + }; + + match self.encoding { + Encoding::Utf8 | Encoding::Default => { + // fast path for ASCII subset + if pos == 0 && next.is_ascii() { + return Ok(Some(next.into())); + } + + buf[pos] = next; + pos += 1; + + match str::from_utf8(&buf[..pos]) { + Ok(s) => return Ok(s.chars().next()), // always Some(..) + Err(_) if pos < MAX_CODEPOINT_LEN => continue, + Err(e) => return Err(e.into()), + } + }, + Encoding::Latin1 => { + return Ok(Some(next.into())); + }, + Encoding::Ascii => { + return if next.is_ascii() { + Ok(Some(next.into())) + } else { + Err(CharReadError::Io(io::Error::new(io::ErrorKind::InvalidData, "char is not ASCII"))) + }; + }, + Encoding::Unknown | Encoding::Utf16 => { + buf[pos] = next; + pos += 1; + if let Some(value) = self.sniff_bom(&buf[..pos], &mut pos) { + return value; + } + }, + Encoding::Utf16Be => { + buf[pos] = next; + pos += 1; + if pos == 2 { + if let Some(Ok(c)) = char::decode_utf16([u16::from_be_bytes(buf[..2].try_into().unwrap())]).next() { + return Ok(Some(c)); + } + } else if pos == 4 { + return Self::surrogate([u16::from_be_bytes(buf[..2].try_into().unwrap()), u16::from_be_bytes(buf[2..4].try_into().unwrap())]); + } + }, + Encoding::Utf16Le => { + buf[pos] = next; + pos += 1; + if pos == 2 { + if let Some(Ok(c)) = char::decode_utf16([u16::from_le_bytes(buf[..2].try_into().unwrap())]).next() { + return Ok(Some(c)); + } + } else if pos == 4 { + return Self::surrogate([u16::from_le_bytes(buf[..2].try_into().unwrap()), u16::from_le_bytes(buf[2..4].try_into().unwrap())]); + } + }, + } + } + Err(CharReadError::Io(io::ErrorKind::InvalidData.into())) + } + + #[cold] + fn sniff_bom(&mut self, buf: &[u8], pos: &mut usize) -> Option, CharReadError>> { + // sniff BOM + if buf.len() <= 3 && [0xEF, 0xBB, 0xBF].starts_with(buf) { + if buf.len() == 3 && self.encoding != Encoding::Utf16 { + *pos = 0; + self.encoding = Encoding::Utf8; + } + } else if buf.len() <= 2 && [0xFE, 0xFF].starts_with(buf) { + if buf.len() == 2 { + *pos = 0; + self.encoding = Encoding::Utf16Be; + } + } else if buf.len() <= 2 && [0xFF, 0xFE].starts_with(buf) { + if buf.len() == 2 { + *pos = 0; + self.encoding = Encoding::Utf16Le; + } + } else if buf.len() == 1 && self.encoding == Encoding::Utf16 { + // sniff ASCII char in UTF-16 + self.encoding = if buf[0] == 0 { Encoding::Utf16Be } else { Encoding::Utf16Le }; + } else { + // UTF-8 is the default, but XML decl can change it to other 8-bit encoding + self.encoding = Encoding::Default; + if buf.len() == 1 && buf[0].is_ascii() { + return Some(Ok(Some(buf[0].into()))); + } + } + None + } + + fn surrogate(buf: [u16; 2]) -> Result, CharReadError> { + char::decode_utf16(buf).next().transpose() + .map_err(|e| CharReadError::Io(io::Error::new(io::ErrorKind::InvalidData, e))) + } +} + +#[cfg(test)] +mod tests { + use super::{CharReadError, CharReader, Encoding}; + + #[test] + fn test_next_char_from() { + use std::io; + + let mut bytes: &[u8] = b"correct"; // correct ASCII + assert_eq!(CharReader::new(Encoding::Unknown).next_char_from(&mut bytes).unwrap(), Some('c')); + + let mut bytes: &[u8] = b"\xEF\xBB\xBF\xE2\x80\xA2!"; // BOM + assert_eq!(CharReader::new(Encoding::Unknown).next_char_from(&mut bytes).unwrap(), Some('•')); + + let mut bytes: &[u8] = b"\xEF\xBB\xBFx123"; // BOM + assert_eq!(CharReader::new(Encoding::Unknown).next_char_from(&mut bytes).unwrap(), Some('x')); + + let mut bytes: &[u8] = b"\xEF\xBB\xBF"; // Nothing after BOM + assert_eq!(CharReader::new(Encoding::Unknown).next_char_from(&mut bytes).unwrap(), None); + + let mut bytes: &[u8] = b"\xEF\xBB"; // Nothing after BO + assert!(matches!(CharReader::new(Encoding::Unknown).next_char_from(&mut bytes), Err(CharReadError::UnexpectedEof))); + + let mut bytes: &[u8] = b"\xEF\xBB\x42"; // Nothing after BO + assert!(CharReader::new(Encoding::Unknown).next_char_from(&mut bytes).is_err()); + + let mut bytes: &[u8] = b"\xFE\xFF\x00\x42"; // UTF-16 + assert_eq!(CharReader::new(Encoding::Unknown).next_char_from(&mut bytes).unwrap(), Some('B')); + + let mut bytes: &[u8] = b"\xFF\xFE\x42\x00"; // UTF-16 + assert_eq!(CharReader::new(Encoding::Unknown).next_char_from(&mut bytes).unwrap(), Some('B')); + + let mut bytes: &[u8] = b"\xFF\xFE"; // UTF-16 + assert_eq!(CharReader::new(Encoding::Unknown).next_char_from(&mut bytes).unwrap(), None); + + let mut bytes: &[u8] = b"\xFF\xFE\x00"; // UTF-16 + assert!(matches!(CharReader::new(Encoding::Unknown).next_char_from(&mut bytes), Err(CharReadError::UnexpectedEof))); + + let mut bytes: &[u8] = "правильно".as_bytes(); // correct BMP + assert_eq!(CharReader::new(Encoding::Unknown).next_char_from(&mut bytes).unwrap(), Some('п')); + + let mut bytes: &[u8] = "правильно".as_bytes(); + assert_eq!(CharReader::new(Encoding::Utf16Be).next_char_from(&mut bytes).unwrap(), Some('킿')); + + let mut bytes: &[u8] = "правильно".as_bytes(); + assert_eq!(CharReader::new(Encoding::Utf16Le).next_char_from(&mut bytes).unwrap(), Some('뿐')); + + let mut bytes: &[u8] = b"\xD8\xD8\x80"; + assert!(CharReader::new(Encoding::Utf16).next_char_from(&mut bytes).is_err()); + + let mut bytes: &[u8] = b"\x00\x42"; + assert_eq!(CharReader::new(Encoding::Utf16).next_char_from(&mut bytes).unwrap(), Some('B')); + + let mut bytes: &[u8] = b"\x42\x00"; + assert_eq!(CharReader::new(Encoding::Utf16).next_char_from(&mut bytes).unwrap(), Some('B')); + + let mut bytes: &[u8] = &[0xEF, 0xBB, 0xBF, 0xFF, 0xFF]; + assert!(CharReader::new(Encoding::Utf16).next_char_from(&mut bytes).is_err()); + + let mut bytes: &[u8] = b"\x00"; + assert!(CharReader::new(Encoding::Utf16Be).next_char_from(&mut bytes).is_err()); + + let mut bytes: &[u8] = "😊".as_bytes(); // correct non-BMP + assert_eq!(CharReader::new(Encoding::Unknown).next_char_from(&mut bytes).unwrap(), Some('😊')); + + let mut bytes: &[u8] = b""; // empty + assert_eq!(CharReader::new(Encoding::Unknown).next_char_from(&mut bytes).unwrap(), None); + + let mut bytes: &[u8] = b"\xf0\x9f\x98"; // incomplete code point + match CharReader::new(Encoding::Unknown).next_char_from(&mut bytes).unwrap_err() { + CharReadError::UnexpectedEof => {}, + e => panic!("Unexpected result: {e:?}") + } + + let mut bytes: &[u8] = b"\xff\x9f\x98\x32"; // invalid code point + match CharReader::new(Encoding::Unknown).next_char_from(&mut bytes).unwrap_err() { + CharReadError::Utf8(_) => {}, + e => panic!("Unexpected result: {e:?}"), + } + + // error during read + struct ErrorReader; + impl io::Read for ErrorReader { + fn read(&mut self, _: &mut [u8]) -> io::Result { + Err(io::Error::new(io::ErrorKind::Other, "test error")) + } + } + + let mut r = ErrorReader; + match CharReader::new(Encoding::Unknown).next_char_from(&mut r).unwrap_err() { + CharReadError::Io(ref e) if e.kind() == io::ErrorKind::Other && + e.to_string().contains("test error") => {}, + e => panic!("Unexpected result: {e:?}") + } + } + + #[test] + fn test_latin1_transcoding() { + let mut bytes: &[u8] = &[0xE9, 0x20, 0xFC]; // é, space, ü + let mut ch = CharReader::new(Encoding::Latin1); + assert_eq!(ch.next_char_from(&mut bytes).unwrap(), Some('é')); + assert_eq!(ch.next_char_from(&mut bytes).unwrap(), Some(' ')); + assert_eq!(ch.next_char_from(&mut bytes).unwrap(), Some('ü')); + assert_eq!(ch.next_char_from(&mut bytes).unwrap(), None); + } + + #[test] + fn test_ascii_encoding() { + let mut bytes: &[u8] = b"ok"; + let mut ch = CharReader::new(Encoding::Ascii); + assert_eq!(ch.next_char_from(&mut bytes).unwrap(), Some('o')); + assert_eq!(ch.next_char_from(&mut bytes).unwrap(), Some('k')); + assert_eq!(ch.next_char_from(&mut bytes).unwrap(), None); + + let mut bytes: &[u8] = &[0x80]; + let mut ch = CharReader::new(Encoding::Ascii); + assert!(ch.next_char_from(&mut bytes).is_err()); + } + + #[test] + fn test_encoding_switch() { + let data: &[u8] = &[b'<', b'?', 0xE9]; // { + sink: W, + emitter: Emitter, +} + +impl EventWriter { + /// Creates a new `EventWriter` out of an `std::io::Write` instance using the default + /// configuration. + #[inline] + pub fn new(sink: W) -> Self { + Self::new_with_config(sink, EmitterConfig::new()) + } + + /// Creates a new `EventWriter` out of an `std::io::Write` instance using the provided + /// configuration. + #[inline] + pub fn new_with_config(sink: W, config: EmitterConfig) -> Self { + Self { + sink, + emitter: Emitter::new(config), + } + } + + /// Writes the next piece of XML document according to the provided event. + /// + /// Note that output data may not exactly correspond to the written event because + /// of various configuration options. For example, `XmlEvent::EndElement` may + /// correspond to a separate closing element or it may cause writing an empty element. + /// Another example is that `XmlEvent::CData` may be represented as characters in + /// the output stream. + pub fn write<'a, E>(&mut self, event: E) -> Result<()> where E: Into> { + match event.into() { + XmlEvent::StartDocument { version, encoding, standalone } => + self.emitter.emit_start_document(&mut self.sink, version, encoding.unwrap_or("UTF-8"), standalone), + XmlEvent::ProcessingInstruction { name, data } => + self.emitter.emit_processing_instruction(&mut self.sink, name, data), + XmlEvent::StartElement { name, attributes, namespace } => { + self.emitter.namespace_stack_mut().push_empty().checked_target().extend(namespace.as_ref()); + self.emitter.emit_start_element(&mut self.sink, name, &attributes) + }, + XmlEvent::EndElement { name } => { + let r = self.emitter.emit_end_element(&mut self.sink, name); + self.emitter.namespace_stack_mut().try_pop(); + r + }, + XmlEvent::Comment(content) => self.emitter.emit_comment(&mut self.sink, content), + XmlEvent::CData(content) => self.emitter.emit_cdata(&mut self.sink, content), + XmlEvent::Characters(content) => self.emitter.emit_characters(&mut self.sink, content), + XmlEvent::RawCharacters(content) => self.emitter.emit_raw_characters(&mut self.sink, content), + XmlEvent::Doctype(content) => self.emitter.emit_raw_characters(&mut self.sink, content), + } + } + + /// Returns a mutable reference to the underlying `Writer`. + /// + /// Note that having a reference to the underlying sink makes it very easy to emit invalid XML + /// documents. Use this method with care. Valid use cases for this method include accessing + /// methods like `Write::flush`, which do not emit new data but rather change the state + /// of the stream itself. + pub fn inner_mut(&mut self) -> &mut W { + &mut self.sink + } + + /// Returns an immutable reference to the underlying `Writer`. + pub fn inner_ref(&self) -> &W { + &self.sink + } + + /// Unwraps this `EventWriter`, returning the underlying writer. + /// + /// Note that this is a destructive operation: unwrapping a writer and then wrapping + /// it again with `EventWriter::new()` will create a fresh writer whose state will be + /// blank; for example, accumulated namespaces will be reset. + pub fn into_inner(self) -> W { + self.sink + } +} diff --git a/vendor/xml/src/writer/config.rs b/vendor/xml/src/writer/config.rs new file mode 100644 index 00000000000..b2ec762391c --- /dev/null +++ b/vendor/xml/src/writer/config.rs @@ -0,0 +1,157 @@ +//! Contains emitter configuration structure. + +use crate::writer::EventWriter; +use std::borrow::Cow; +use std::io::Write; + +/// Emitter configuration structure. +/// +/// This structure contains various options which control XML document emitter behavior. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct EmitterConfig { + /// Line separator used to separate lines in formatted output. Default is `"\n"`. + pub line_separator: Cow<'static, str>, + + /// A string which will be used for a single level of indentation. Default is `" "` + /// (two spaces). + pub indent_string: Cow<'static, str>, + + /// Whether or not the emitted document should be indented. Default is false. + /// + /// The emitter is capable to perform automatic indentation of the emitted XML document. + /// It is done in stream-like fashion and does not require the knowledge of the whole + /// document in advance. + /// + /// Sometimes, however, automatic indentation is undesirable, e.g. when you want to keep + /// existing layout when processing an existing XML document. Also the indentiation algorithm + /// is not thoroughly tested. Hence by default it is disabled. + pub perform_indent: bool, + + /// Whether or not characters in output events will be escaped. Default is true. + /// + /// The emitter can automatically escape characters which can't appear in PCDATA sections + /// or element attributes of an XML document, like `<` or `"` (in attributes). This may + /// introduce some overhead because then every corresponding piece of character data + /// should be scanned for invalid characters. + /// + /// If this option is disabled, the XML writer may produce non-well-formed documents, so + /// use `false` value for this option with care. + pub perform_escaping: bool, + + /// Whether or not to write XML document declaration at the beginning of a document. + /// Default is true. + /// + /// This option controls whether the document declaration should be emitted automatically + /// before a root element is written if it was not emitted explicitly by the user. + pub write_document_declaration: bool, + + /// Whether or not to convert elements with empty content to empty elements. Default is true. + /// + /// This option allows turning elements like `` (an element with empty content) + /// into `` (an empty element). + pub normalize_empty_elements: bool, + + /// Whether or not to emit CDATA events as plain characters. Default is false. + /// + /// This option forces the emitter to convert CDATA events into regular character events, + /// performing all the necessary escaping beforehand. This may be occasionally useful + /// for feeding the document into incorrect parsers which do not support CDATA. + pub cdata_to_characters: bool, + + /// Whether or not to keep element names to support `EndElement` events without explicit names. + /// Default is true. + /// + /// This option makes the emitter to keep names of written elements in order to allow + /// omitting names when writing closing element tags. This could incur some memory overhead. + pub keep_element_names_stack: bool, + + /// Whether or not to automatically insert leading and trailing spaces in emitted comments, + /// if necessary. Default is true. + /// + /// This is a convenience option in order for the user not to append spaces before and after + /// comments text in order to get more pretty comments: `` instead of + /// ``. + pub autopad_comments: bool, + + /// Whether or not to automatically insert spaces before the trailing `/>` in self-closing + /// elements. Default is true. + /// + /// This option is only meaningful if `normalize_empty_elements` is true. For example, the + /// element `` would be unaffected. When `normalize_empty_elements` is true, then when + /// this option is also true, the same element would appear ``. If this option is false, + /// then the same element would appear ``. + pub pad_self_closing: bool, +} + +impl EmitterConfig { + /// Creates an emitter configuration with default values. + /// + /// You can tweak default options with builder-like pattern: + /// + /// ```rust + /// use xml::writer::EmitterConfig; + /// + /// let config = EmitterConfig::new() + /// .line_separator("\r\n") + /// .perform_indent(true) + /// .normalize_empty_elements(false); + /// ``` + #[inline] + #[must_use] + pub fn new() -> Self { + Self { + line_separator: "\n".into(), + indent_string: " ".into(), // two spaces + perform_indent: false, + perform_escaping: true, + write_document_declaration: true, + normalize_empty_elements: true, + cdata_to_characters: false, + keep_element_names_stack: true, + autopad_comments: true, + pad_self_closing: true, + } + } + + /// Creates an XML writer with this configuration. + /// + /// This is a convenience method for configuring and creating a writer at the same time: + /// + /// ```rust + /// use xml::writer::EmitterConfig; + /// + /// let mut target: Vec = Vec::new(); + /// + /// let writer = EmitterConfig::new() + /// .line_separator("\r\n") + /// .perform_indent(true) + /// .normalize_empty_elements(false) + /// .create_writer(&mut target); + /// ``` + /// + /// This method is exactly equivalent to calling `EventWriter::new_with_config()` with + /// this configuration object. + #[inline] + pub fn create_writer(self, sink: W) -> EventWriter { + EventWriter::new_with_config(sink, self) + } +} + +impl Default for EmitterConfig { + #[inline] + fn default() -> Self { + Self::new() + } +} + +gen_setters!(EmitterConfig, + line_separator: into Cow<'static, str>, + indent_string: into Cow<'static, str>, + perform_indent: val bool, + write_document_declaration: val bool, + normalize_empty_elements: val bool, + cdata_to_characters: val bool, + keep_element_names_stack: val bool, + autopad_comments: val bool, + pad_self_closing: val bool +); diff --git a/vendor/xml/src/writer/emitter.rs b/vendor/xml/src/writer/emitter.rs new file mode 100644 index 00000000000..608c9ecac59 --- /dev/null +++ b/vendor/xml/src/writer/emitter.rs @@ -0,0 +1,483 @@ +use std::error::Error; +use std::io::prelude::*; +use std::{fmt, io, result}; + +use crate::attribute::Attribute; +use crate::common::XmlVersion; +use crate::escape::{AttributeEscapes, Escaped, PcDataEscapes}; +use crate::name::{Name, OwnedName}; +use crate::namespace::{NamespaceStack, NS_EMPTY_URI, NS_NO_PREFIX, NS_XMLNS_PREFIX, NS_XML_PREFIX}; + +use crate::writer::config::EmitterConfig; + +/// An error which may be returned by `XmlWriter` when writing XML events. +#[derive(Debug)] +#[non_exhaustive] +pub enum EmitterError { + /// An I/O error occured in the underlying `Write` instance. + Io(io::Error), + + /// Document declaration has already been written to the output stream. + DocumentStartAlreadyEmitted, + + /// The name of the last opening element is not available. + LastElementNameNotAvailable, + + /// The name of the last opening element is not equal to the name of the provided + /// closing element. + EndElementNameIsNotEqualToLastStartElementName, + + /// End element name is not specified when it is needed, for example, when automatic + /// closing is not enabled in configuration. + EndElementNameIsNotSpecified, +} + +impl Clone for EmitterError { + #[cold] + fn clone(&self) -> Self { + match self { + Self::Io(io_error) => Self::Io(io::Error::new(io_error.kind(), io_error.to_string())), + Self::DocumentStartAlreadyEmitted => Self::DocumentStartAlreadyEmitted, + Self::LastElementNameNotAvailable => Self::LastElementNameNotAvailable, + Self::EndElementNameIsNotEqualToLastStartElementName => Self::EndElementNameIsNotEqualToLastStartElementName, + Self::EndElementNameIsNotSpecified => Self::EndElementNameIsNotSpecified, + } + } +} + +impl From for EmitterError { + #[cold] + fn from(err: io::Error) -> Self { + Self::Io(err) + } +} + +impl fmt::Display for EmitterError { + #[cold] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("emitter error: ")?; + match self { + Self::Io(e) => write!(f, "I/O error: {e}"), + Self::DocumentStartAlreadyEmitted => f.write_str("document start event has already been emitted"), + Self::LastElementNameNotAvailable => f.write_str("last element name is not available"), + Self::EndElementNameIsNotEqualToLastStartElementName => f.write_str("end element name is not equal to last start element name"), + Self::EndElementNameIsNotSpecified => f.write_str("end element name is not specified and can't be inferred"), + } + } +} + +impl Error for EmitterError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Io(e) => e.source(), + _ => None, + } + } +} + +/// A result type yielded by `XmlWriter`. +pub type Result = result::Result; + +// TODO: split into a low-level fast writer without any checks and formatting logic and a +// high-level indenting validating writer +pub struct Emitter { + config: EmitterConfig, + + nst: NamespaceStack, + + indent_level: usize, + indent_stack: Vec, + + element_names: Vec, + + start_document_emitted: bool, + just_wrote_start_element: bool, +} + +impl Emitter { + pub fn new(config: EmitterConfig) -> Self { + let mut indent_stack = Vec::with_capacity(16); + indent_stack.push(IndentFlags::WroteNothing); + + Self { + config, + + nst: NamespaceStack::empty(), + + indent_level: 0, + indent_stack, + + element_names: Vec::new(), + + start_document_emitted: false, + just_wrote_start_element: false, + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +enum IndentFlags { + WroteNothing, + WroteMarkup, + WroteText, +} + +impl Emitter { + /// Returns the current state of namespaces. + #[inline] + pub fn namespace_stack_mut(&mut self) -> &mut NamespaceStack { + &mut self.nst + } + + #[inline] + fn wrote_text(&self) -> bool { + self.indent_stack.last().is_some_and(|&e| e == IndentFlags::WroteText) + } + + #[inline] + fn wrote_markup(&self) -> bool { + self.indent_stack.last().is_some_and(|&e| e == IndentFlags::WroteMarkup) + } + + #[inline] + fn set_wrote_text(&mut self) { + if let Some(e) = self.indent_stack.last_mut() { + *e = IndentFlags::WroteText; + } + } + + #[inline] + fn set_wrote_markup(&mut self) { + if let Some(e) = self.indent_stack.last_mut() { + *e = IndentFlags::WroteMarkup; + } + } + + fn write_newline(&self, target: &mut W, level: usize) -> Result<()> { + target.write_all(self.config.line_separator.as_bytes())?; + for _ in 0..level { + target.write_all(self.config.indent_string.as_bytes())?; + } + Ok(()) + } + + fn before_markup(&mut self, target: &mut W) -> Result<()> { + if self.config.perform_indent && !self.wrote_text() && + (self.indent_level > 0 || self.wrote_markup()) { + let indent_level = self.indent_level; + self.write_newline(target, indent_level)?; + if self.indent_level > 0 && !self.config.indent_string.is_empty() { + self.after_markup(); + } + } + Ok(()) + } + + fn after_markup(&mut self) { + self.set_wrote_markup(); + } + + fn before_start_element(&mut self, target: &mut W) -> Result<()> { + self.before_markup(target)?; + self.indent_stack.push(IndentFlags::WroteNothing); + Ok(()) + } + + fn after_start_element(&mut self) { + self.after_markup(); + self.indent_level += 1; + } + + fn before_end_element(&self, target: &mut W) -> Result<()> { + if self.config.perform_indent && self.indent_level > 0 && self.wrote_markup() && + !self.wrote_text() { + let indent_level = self.indent_level; + self.write_newline(target, indent_level - 1) + } else { + Ok(()) + } + } + + fn after_end_element(&mut self) { + if self.indent_level > 0 { + self.indent_level -= 1; + self.indent_stack.pop(); + } + self.set_wrote_markup(); + } + + fn after_text(&mut self) { + self.set_wrote_text(); + } + + pub fn emit_start_document(&mut self, target: &mut W, + version: XmlVersion, + encoding: &str, + standalone: Option) -> Result<()> { + if self.start_document_emitted { + return Err(EmitterError::DocumentStartAlreadyEmitted); + } + self.start_document_emitted = true; + + self.before_markup(target)?; + let result = { + let mut write = move || { + write!(target, "")?; + + Ok(()) + }; + write() + }; + self.after_markup(); + + result + } + + fn check_document_started(&mut self, target: &mut W) -> Result<()> { + if !self.start_document_emitted && self.config.write_document_declaration { + self.emit_start_document(target, XmlVersion::Version10, "UTF-8", None) + } else { + Ok(()) + } + } + + fn fix_non_empty_element(&mut self, target: &mut W) -> Result<()> { + if self.config.normalize_empty_elements && self.just_wrote_start_element { + self.just_wrote_start_element = false; + target.write_all(b">").map_err(From::from) + } else { + Ok(()) + } + } + + pub fn emit_processing_instruction(&mut self, + target: &mut W, + name: &str, + data: Option<&str>) -> Result<()> { + self.check_document_started(target)?; + self.fix_non_empty_element(target)?; + + self.before_markup(target)?; + + let result = { + let mut write = move || { + write!(target, "")?; + + Ok(()) + }; + write() + }; + + self.after_markup(); + + result + } + + #[track_caller] + fn emit_start_element_initial(&mut self, target: &mut W, + name: Name<'_>, + attributes: &[Attribute<'_>]) -> Result<()> + where W: Write + { + self.check_document_started(target)?; + self.fix_non_empty_element(target)?; + self.before_start_element(target)?; + write!(target, "<{}", name.repr_display())?; + self.emit_current_namespace_attributes(target)?; + self.emit_attributes(target, attributes)?; + self.after_start_element(); + Ok(()) + } + + #[track_caller] + pub fn emit_start_element(&mut self, target: &mut W, + name: Name<'_>, + attributes: &[Attribute<'_>]) -> Result<()> + where W: Write + { + if self.config.keep_element_names_stack { + self.element_names.push(name.to_owned()); + } + + self.emit_start_element_initial(target, name, attributes)?; + self.just_wrote_start_element = true; + + if !self.config.normalize_empty_elements { + write!(target, ">")?; + } + + Ok(()) + } + + #[track_caller] + pub fn emit_current_namespace_attributes(&self, target: &mut W) -> Result<()> + where W: Write + { + for (prefix, uri) in self.nst.peek() { + match prefix { + // internal namespaces are not emitted + NS_XMLNS_PREFIX | NS_XML_PREFIX => Ok(()), + //// there is already a namespace binding with this prefix in scope + //prefix if self.nst.get(prefix) == Some(uri) => Ok(()), + // emit xmlns only if it is overridden + NS_NO_PREFIX => if uri == NS_EMPTY_URI { + Ok(()) + } else { + write!(target, " xmlns=\"{uri}\"") + }, + // everything else + prefix => write!(target, " xmlns:{prefix}=\"{uri}\""), + }?; + } + Ok(()) + } + + pub fn emit_attributes(&self, target: &mut W, attributes: &[Attribute<'_>]) -> Result<()> { + for attr in attributes { + write!(target, " {}=\"", attr.name.repr_display())?; + if self.config.perform_escaping { + write!(target, "{}", Escaped::::new(attr.value))?; + } else { + write!(target, "{}", attr.value)?; + } + write!(target, "\"")?; + } + Ok(()) + } + + pub fn emit_end_element(&mut self, target: &mut W, name: Option>) -> Result<()> { + let owned_name = if self.config.keep_element_names_stack { + Some(self.element_names.pop().ok_or(EmitterError::LastElementNameNotAvailable)?) + } else { + None + }; + + // Check that last started element name equals to the provided name, if there are both + if let Some(ref last_name) = owned_name { + if let Some(ref name) = name { + if last_name.borrow() != *name { + return Err(EmitterError::EndElementNameIsNotEqualToLastStartElementName); + } + } + } + + if let Some(name) = owned_name.as_ref().map(|n| n.borrow()).or(name) { + if self.config.normalize_empty_elements && self.just_wrote_start_element { + self.just_wrote_start_element = false; + let termination = if self.config.pad_self_closing { " />" } else { "/>" }; + let result = target.write_all(termination.as_bytes()).map_err(From::from); + self.after_end_element(); + result + } else { + self.just_wrote_start_element = false; + + self.before_end_element(target)?; + let result = write!(target, "", name.repr_display()).map_err(From::from); + self.after_end_element(); + + result + } + } else { + Err(EmitterError::EndElementNameIsNotSpecified) + } + } + + pub fn emit_cdata(&mut self, target: &mut W, content: &str) -> Result<()> { + self.fix_non_empty_element(target)?; + if self.config.cdata_to_characters { + self.emit_characters(target, content) + } else { + target.write_all(b"") { + let chunk_safe = chunk.strip_suffix("]]>"); + let emit_escaped = chunk_safe.is_some(); + + target.write_all(chunk_safe.unwrap_or(chunk).as_bytes())?; + if emit_escaped { + target.write_all(b"]]]]>")?; + } + } + + target.write_all(b"]]>")?; + self.after_text(); + + Ok(()) + } + } + + pub fn emit_characters(&mut self, target: &mut W, content: &str) -> Result<()> { + self.check_document_started(target)?; + self.fix_non_empty_element(target)?; + + if self.config.perform_escaping { + write!(target, "{}", Escaped::::new(content))?; + } else { + target.write_all(content.as_bytes())?; + } + + self.after_text(); + Ok(()) + } + + pub fn emit_raw_characters(&mut self, target: &mut W, content: &str) -> Result<()> { + self.check_document_started(target)?; + self.fix_non_empty_element(target)?; + + target.write_all(content.as_bytes())?; + + self.after_text(); + Ok(()) + } + + pub fn emit_comment(&mut self, target: &mut W, content: &str) -> Result<()> { + self.fix_non_empty_element(target)?; + + // TODO: add escaping dashes at the end of the comment + + let autopad_comments = self.config.autopad_comments; + let write = move |target: &mut W| -> Result<()> { + target.write_all(b"")?; + + Ok(()) + }; + + self.before_markup(target)?; + let result = write(target); + self.after_markup(); + + result + } +} diff --git a/vendor/xml/src/writer/events.rs b/vendor/xml/src/writer/events.rs new file mode 100644 index 00000000000..4ce578c6f43 --- /dev/null +++ b/vendor/xml/src/writer/events.rs @@ -0,0 +1,289 @@ +//! Contains `XmlEvent` datatype, instances of which are consumed by the writer. + +use std::borrow::Cow; + +use crate::attribute::Attribute; +use crate::common::XmlVersion; +use crate::name::Name; +use crate::namespace::{Namespace, NS_NO_PREFIX}; +use crate::reader::ErrorKind; + +/// A part of an XML output stream. +/// +/// Objects of this enum are consumed by `EventWriter`. They correspond to different parts of +/// an XML document. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum XmlEvent<'a> { + /// Corresponds to XML document declaration. + /// + /// This event should always be written before any other event. If it is not written + /// at all, a default XML declaration will be outputted if the corresponding option + /// is set in the configuration. Otherwise an error will be returned. + StartDocument { + /// XML version. + /// + /// Defaults to `XmlVersion::Version10`. + version: XmlVersion, + + /// XML document encoding. + /// + /// Defaults to `Some("UTF-8")`. + encoding: Option<&'a str>, + + /// XML standalone declaration. + /// + /// Defaults to `None`. + standalone: Option, + }, + + /// Denotes an XML processing instruction. + ProcessingInstruction { + /// Processing instruction target. + name: &'a str, + + /// Processing instruction content. + data: Option<&'a str>, + }, + + /// Denotes a beginning of an XML element. + StartElement { + /// Qualified name of the element. + name: Name<'a>, + + /// A list of attributes associated with the element. + /// + /// Currently attributes are not checked for duplicates (TODO). Attribute values + /// will be escaped, and all characters invalid for attribute values like `"` or `<` + /// will be changed into character entities. + attributes: Cow<'a, [Attribute<'a>]>, + + /// Contents of the namespace mapping at this point of the document. + /// + /// This mapping will be inspected for "new" entries, and if at this point of the document + /// a particular pair of prefix and namespace URI is already defined, no namespace + /// attributes will be emitted. + namespace: Cow<'a, Namespace>, + }, + + /// Denotes an end of an XML element. + EndElement { + /// Optional qualified name of the element. + /// + /// If `None`, then it is assumed that the element name should be the last valid one. + /// If `Some` and element names tracking is enabled, then the writer will check it for + /// correctness. + name: Option>, + }, + + /// Denotes CDATA content. + /// + /// This event contains unparsed data, and no escaping will be performed when writing it + /// to the output stream. + CData(&'a str), + + /// Denotes a comment. + /// + /// The string will be checked for invalid sequences and error will be returned by the + /// write operation + Comment(&'a str), + + /// Denotes character data outside of tags. + /// + /// Contents of this event will be escaped if `perform_escaping` option is enabled, + /// that is, every character invalid for PCDATA will appear as a character entity. + Characters(&'a str), + + /// Emits raw characters which will never be escaped. + /// + /// This event is only used for writing to an output stream, there is no equivalent + /// reader event. Care must be taken when using this event, as it can easily result + /// non-well-formed documents. + RawCharacters(&'a str), + + /// Syntax of the `DOCTYPE`, everyhing including `<` and `>` + Doctype(&'a str), +} + +impl<'a> XmlEvent<'a> { + /// Returns an writer event for a processing instruction. + #[inline] + #[must_use] + pub const fn processing_instruction(name: &'a str, data: Option<&'a str>) -> Self { + XmlEvent::ProcessingInstruction { name, data } + } + + /// Returns a builder for a starting element. + /// + /// This builder can then be used to tweak attributes and namespace starting at + /// this element. + #[inline] + pub fn start_element(name: S) -> StartElementBuilder<'a> where S: Into> { + StartElementBuilder { + name: name.into(), + attributes: Vec::new(), + namespace: Namespace::empty(), + } + } + + /// Returns a builder for an closing element. + /// + /// This method, unlike `start_element()`, does not accept a name because by default + /// the writer is able to determine it automatically. However, when this functionality + /// is disabled, it is possible to specify the name with `name()` method on the builder. + #[inline] + #[must_use] + pub const fn end_element() -> EndElementBuilder<'a> { + EndElementBuilder { name: None } + } + + /// Returns a CDATA event. + /// + /// Naturally, the provided string won't be escaped, except for closing CDATA token `]]>` + /// (depending on the configuration). + #[inline] + #[must_use] + pub const fn cdata(data: &'a str) -> Self { + XmlEvent::CData(data) + } + + /// Returns a regular characters (PCDATA) event. + /// + /// All offending symbols, in particular, `&` and `<`, will be escaped by the writer. + #[inline] + #[must_use] + pub const fn characters(data: &'a str) -> Self { + XmlEvent::Characters(data) + } + + /// Returns a raw characters event. + /// + /// No escaping takes place. + /// This event is only used for writing to an output stream, there is no equivalent + /// reader event. Care must be taken when using this event, as it can easily result + /// non-well-formed documents. + #[inline] + #[must_use] + pub const fn raw_characters(data: &'a str) -> Self { + XmlEvent::RawCharacters(data) + } + + /// Returns a comment event. + #[inline] + #[must_use] + pub const fn comment(data: &'a str) -> Self { + XmlEvent::Comment(data) + } +} + +impl<'a> From<&'a str> for XmlEvent<'a> { + #[inline] + fn from(s: &'a str) -> Self { + XmlEvent::Characters(s) + } +} + +/// A builder for a closing element event. +pub struct EndElementBuilder<'a> { + name: Option>, +} + +/// A builder for a closing element event. +impl<'a> EndElementBuilder<'a> { + /// Sets the name of this closing element. + /// + /// Usually the writer is able to determine closing element names automatically. If + /// this functionality is enabled (by default it is), then this name is checked for correctness. + /// It is possible, however, to disable such behavior; then the user must ensure that + /// closing element name is correct manually. + #[inline] + #[must_use] + pub fn name(mut self, name: N) -> Self where N: Into> { + self.name = Some(name.into()); + self + } +} + +impl<'a> From> for XmlEvent<'a> { + fn from(b: EndElementBuilder<'a>) -> Self { + XmlEvent::EndElement { name: b.name } + } +} + +/// A builder for a starting element event. +pub struct StartElementBuilder<'a> { + name: Name<'a>, + attributes: Vec>, + namespace: Namespace, +} + +impl<'a> StartElementBuilder<'a> { + /// Sets an attribute value of this element to the given string. + /// + /// This method can be used to add attributes to the starting element. Name is a qualified + /// name; its namespace is ignored, but its prefix is checked for correctness, that is, + /// it is checked that the prefix is bound to some namespace in the current context. + /// + /// Currently attributes are not checked for duplicates. Note that duplicate attributes + /// are a violation of XML document well-formedness. + /// + /// The writer checks that you don't specify reserved prefix names, for example `xmlns`. + #[inline] + #[must_use] + pub fn attr(mut self, name: N, value: &'a str) -> Self + where N: Into> { + self.attributes.push(Attribute::new(name.into(), value)); + self + } + + /// Adds a namespace to the current namespace context. + /// + /// If no namespace URI was bound to the provided prefix at this point of the document, + /// then the mapping from the prefix to the provided namespace URI will be written as + /// a part of this element attribute set. + /// + /// If the same namespace URI was bound to the provided prefix at this point of the document, + /// then no namespace attributes will be emitted. + /// + /// If some other namespace URI was bound to the provided prefix at this point of the document, + /// then another binding will be added as a part of this element attribute set, shadowing + /// the outer binding. + #[inline] + #[must_use] + pub fn ns(mut self, prefix: S1, uri: S2) -> Self + where S1: Into, S2: Into + { + self.namespace.put(prefix, uri); + self + } + + /// Adds a default namespace mapping to the current namespace context. + /// + /// Same rules as for `ns()` are also valid for the default namespace mapping. + #[inline] + #[must_use] + pub fn default_ns(mut self, uri: S) -> Self + where S: Into { + self.namespace.put(NS_NO_PREFIX, uri); + self + } +} + +impl<'a> From> for XmlEvent<'a> { + #[inline] + fn from(b: StartElementBuilder<'a>) -> Self { + XmlEvent::StartElement { + name: b.name, + attributes: Cow::Owned(b.attributes), + namespace: Cow::Owned(b.namespace), + } + } +} + +impl<'a> TryFrom<&'a crate::reader::XmlEvent> for XmlEvent<'a> { + type Error = crate::reader::Error; + + fn try_from(event: &crate::reader::XmlEvent) -> Result, Self::Error> { + Ok(event.as_writer_event().ok_or(ErrorKind::UnexpectedEof)?) + } +} From 0f1bf86d968efafd3f12649e623119d142d0d41b Mon Sep 17 00:00:00 2001 From: youknowdot Date: Thu, 15 Oct 2026 13:08:25 +0900 Subject: [PATCH 02/12] Defer non-Limited C API additions and correct native config observations Keep the mega C export set equal to main under the abi3t-only scope. Defer new monitoring exports and the unneeded legacy PyCFunction_Call entry, remove the new non-Stable Windows export exception, and regenerate existing stable buffer exports. Preserve main tests and Python monitoring fixtures. Read live bytecode configuration and return integer context flags as in CPython final. Assisted-by: OpenAI Codex:model version unavailable --- capi-exports.def | 9 +- crates/capi/src/methodobject.rs | 51 +---- crates/capi/src/monitoring.rs | 202 -------------------- crates/vm/src/stdlib/_testcapi.rs | 19 +- crates/vm/src/stdlib/sys/monitoring/capi.rs | 30 +-- scripts/generate_capi_exports.py | 33 +--- 6 files changed, 29 insertions(+), 315 deletions(-) delete mode 100644 crates/capi/src/monitoring.rs diff --git a/capi-exports.def b/capi-exports.def index b07cda3bfd1..f0d3e0198fd 100644 --- a/capi-exports.def +++ b/capi-exports.def @@ -2,11 +2,15 @@ ; CPython v3.15.0: d220409a13b77de7a6c30abe16ea90db01528edc ; Misc/stable_abi.toml SHA-256: eab726f2482c71e67a23ea04aaa9c5a11d565bee4472320f813262f273c1e966 ; Implemented Stable ABI functions/data, including ABI-only names. -; Non-Stable ABI compatibility export: PyFrame_GetLocals. EXPORTS PyABIInfo_Check PyAIter_Check PyBool_FromLong + PyBuffer_FromContiguous + PyBuffer_GetPointer + PyBuffer_IsContiguous + PyBuffer_Release + PyBuffer_ToContiguous PyByteArray_AsString PyByteArray_FromObject PyByteArray_FromStringAndSize @@ -20,7 +24,6 @@ EXPORTS PyBytes_FromString PyBytes_FromStringAndSize PyBytes_Size - PyCFunction_Call PyCFunction_New PyCFunction_NewEx PyCMethod_New @@ -193,7 +196,6 @@ EXPORTS PyFloat_GetMin PyFrame_GetCode PyFrame_GetLineNumber - PyFrame_GetLocals PyFrozenSet_New PyGC_Collect PyGC_Disable @@ -358,6 +360,7 @@ EXPORTS PyObject_GetAIter PyObject_GetAttr PyObject_GetAttrString + PyObject_GetBuffer PyObject_GetItem PyObject_GetIter PyObject_GetOptionalAttr diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index ef28462a8b4..1f2a3baa731 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -11,16 +11,6 @@ use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; define_py_check!(fn PyCFunction_Check, types.builtin_function_or_method_type); define_py_check!(exact fn PyCFunction_CheckExact, types.builtin_function_or_method_type); -// Removed from the public API in 3.13, but retained in the stable ABI. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn PyCFunction_Call( - callable: *mut PyObject, - args: *mut PyObject, - kwargs: *mut PyObject, -) -> *mut PyObject { - unsafe { crate::abstract_::PyObject_Call(callable, args, kwargs) } -} - #[repr(C)] #[derive(Debug)] pub struct PyMethodDef { @@ -327,46 +317,7 @@ mod tests { use pyo3::exceptions::PyException; use pyo3::ffi::{PyLong_FromLong, PyObject}; use pyo3::prelude::*; - use pyo3::types::{PyCFunction, PyDict, PyInt, PyString, PyTuple}; - - #[test] - fn legacy_call_python_function() { - Python::attach(|py| { - let callable = py - .eval(c"lambda *args, **kwargs: (args, kwargs)", None, None) - .unwrap(); - let args = PyTuple::new(py, [1, 2]).unwrap(); - let kwargs = PyDict::new(py); - kwargs.set_item("num", 5).unwrap(); - // PyO3 no longer declares this ABI-only compatibility symbol. - let result = unsafe { - Bound::::from_owned_ptr_or_err( - py, - super::PyCFunction_Call( - callable.as_ptr().cast(), - args.as_ptr().cast(), - kwargs.as_ptr().cast(), - ) - .cast(), - ) - } - .unwrap(); - assert_eq!( - result.get_item(0).unwrap().extract::<(i32, i32)>().unwrap(), - (1, 2) - ); - assert_eq!( - result - .get_item(1) - .unwrap() - .get_item("num") - .unwrap() - .extract::() - .unwrap(), - 5 - ); - }); - } + use pyo3::types::{PyCFunction, PyInt, PyString}; #[test] fn closure_function() { diff --git a/crates/capi/src/monitoring.rs b/crates/capi/src/monitoring.rs deleted file mode 100644 index 2253619eca9..00000000000 --- a/crates/capi/src/monitoring.rs +++ /dev/null @@ -1,202 +0,0 @@ -use crate::{PyObject, pystate::with_vm, util::FfiPtrExt}; -use core::ffi::c_int; -use rustpython_vm::stdlib::sys::monitoring_capi; - -pub use monitoring_capi::EventState as PyMonitoringState; - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn PyMonitoring_EnterScope( - state_array: *mut PyMonitoringState, - version: *mut u64, - event_types: *const u8, - length: isize, -) -> c_int { - with_vm(|vm| { - let length = usize::try_from(length) - .map_err(|_| vm.new_value_error("invalid monitoring scope length"))?; - let (states, events) = if length == 0 { - (&mut [][..], &[][..]) - } else { - unsafe { - ( - core::slice::from_raw_parts_mut(state_array, length), - core::slice::from_raw_parts(event_types, length), - ) - } - }; - monitoring_capi::enter_scope(vm, states, unsafe { &mut *version }, events) - }) -} - -#[unsafe(no_mangle)] -pub extern "C" fn PyMonitoring_ExitScope() -> c_int { - monitoring_capi::exit_scope(); - 0 -} - -macro_rules! fire_event { - ($name:ident, $event:expr $(, $arg:ident)*) => { - #[unsafe(no_mangle)] - pub unsafe extern "C" fn $name( - state: *mut PyMonitoringState, - codelike: *mut PyObject, - offset: i32, - $($arg: *mut PyObject,)* - ) -> c_int { - with_vm(|vm| { - // Exception setup precedes reading the codelike or arguments, - // matching CPython's exception_event_setup error ordering. - if matches!($event, 11..=15 | 17) - && unsafe { (*state).active } != 0 - && vm.raised_exception().is_none() - { - return Err(vm.new_value_error(format!( - "Firing event {} with no exception set", $event - ))); - } - unsafe { monitoring_capi::fire_event( - vm, - state, - codelike.assume_borrowed(), - offset, - $event, - &[$($arg.assume_borrowed().to_owned(),)*], - ) } - }) - } - }; -} - -fire_event!(_PyMonitoring_FirePyStartEvent, 0); -fire_event!(_PyMonitoring_FirePyResumeEvent, 1); -fire_event!(_PyMonitoring_FirePyReturnEvent, 2, retval); -fire_event!(_PyMonitoring_FirePyYieldEvent, 3, retval); -fire_event!(_PyMonitoring_FireCallEvent, 4, callable, arg0); -fire_event!(_PyMonitoring_FireJumpEvent, 7, target_offset); -fire_event!(_PyMonitoring_FireBranchLeftEvent, 8, target_offset); -fire_event!(_PyMonitoring_FireBranchRightEvent, 9, target_offset); -// The deprecated BRANCH entry point fires BRANCH_RIGHT. -fire_event!(_PyMonitoring_FireBranchEvent, 9, target_offset); -fire_event!(_PyMonitoring_FireRaiseEvent, 11); -fire_event!(_PyMonitoring_FireExceptionHandledEvent, 12); -fire_event!(_PyMonitoring_FirePyUnwindEvent, 13); -fire_event!(_PyMonitoring_FirePyThrowEvent, 14); -fire_event!(_PyMonitoring_FireReraiseEvent, 15); -fire_event!(_PyMonitoring_FireCReturnEvent, 16, retval); -fire_event!(_PyMonitoring_FireCRaiseEvent, 17); - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn _PyMonitoring_FireLineEvent( - state: *mut PyMonitoringState, - codelike: *mut PyObject, - offset: i32, - lineno: c_int, -) -> c_int { - with_vm(|vm| unsafe { - monitoring_capi::fire_event( - vm, - state, - codelike.assume_borrowed(), - offset, - 5, - &[vm.ctx.new_int(lineno).into()], - ) - }) -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn _PyMonitoring_FireStopIterationEvent( - state: *mut PyMonitoringState, - codelike: *mut PyObject, - offset: i32, - value: *mut PyObject, -) -> c_int { - with_vm(|vm| unsafe { - let value = value.assume_borrowed_or_opt().map(PyObject::to_owned); - monitoring_capi::fire_event( - vm, - state, - codelike.assume_borrowed(), - offset, - 10, - value.as_slice(), - ) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use pyo3::{prelude::*, types::PyModule}; - - #[test] - fn native_scope_and_disable() { - // Monitoring callbacks are interpreter-wide. Other C-API unit tests - // execute Python concurrently in the shared main interpreter. - const CHILD_MARKER: &str = "RUSTPYTHON_CAPI_MONITORING_TEST_CHILD"; - if std::env::var_os(CHILD_MARKER).is_none() { - let status = std::process::Command::new(std::env::current_exe().unwrap()) - .args([ - "--exact", - "monitoring::tests::native_scope_and_disable", - "--nocapture", - ]) - .env(CHILD_MARKER, "1") - .status() - .unwrap(); - assert!( - status.success(), - "isolated monitoring test failed: {status}" - ); - return; - } - Python::attach(|py| { - let fixture = PyModule::from_code( - py, - c"import sys\nseen = []\ndef callback(code, offset):\n seen.append((code, offset))\n return sys.monitoring.DISABLE\n", - c"monitoring_fixture.py", - c"monitoring_fixture", - ) - .unwrap(); - let monitoring = py.import("sys").unwrap().getattr("monitoring").unwrap(); - monitoring - .call_method1("use_tool_id", (5, "capi scope")) - .unwrap(); - monitoring - .call_method1( - "register_callback", - (5, 1, fixture.getattr("callback").unwrap()), - ) - .unwrap(); - monitoring.call_method1("set_events", (5, 1)).unwrap(); - let mut state = PyMonitoringState::default(); - let mut version = 0; - assert_eq!( - unsafe { PyMonitoring_EnterScope(&mut state, &mut version, &0, 1) }, - 0 - ); - assert_eq!(state.active, 1 << 5); - let codelike = py.None(); - assert_eq!( - unsafe { _PyMonitoring_FirePyStartEvent(&mut state, codelike.as_ptr().cast(), 42) }, - 0 - ); - assert_eq!(state.active, 0); - let seen: Vec<(Option, i32)> = fixture.getattr("seen").unwrap().extract().unwrap(); - assert_eq!(seen, [(None, 42)]); - assert_eq!( - unsafe { PyMonitoring_EnterScope(&mut state, &mut version, &0, 1) }, - 0 - ); - assert_eq!(state.active, 0); - monitoring.call_method0("restart_events").unwrap(); - assert_eq!( - unsafe { PyMonitoring_EnterScope(&mut state, &mut version, &0, 1) }, - 0 - ); - assert_eq!(state.active, 1 << 5); - assert_eq!(PyMonitoring_ExitScope(), 0); - monitoring.call_method1("free_tool_id", (5,)).unwrap(); - }); - } -} diff --git a/crates/vm/src/stdlib/_testcapi.rs b/crates/vm/src/stdlib/_testcapi.rs index c6820f4e1f6..75cd1d280a7 100644 --- a/crates/vm/src/stdlib/_testcapi.rs +++ b/crates/vm/src/stdlib/_testcapi.rs @@ -131,10 +131,23 @@ mod _testcapi { "site_import" => vm.ctx.new_bool(settings.import_site).into(), "inspect" => vm.ctx.new_bool(settings.inspect).into(), "interactive" => vm.ctx.new_bool(settings.interactive).into(), - "write_bytecode" => vm.ctx.new_bool(settings.write_bytecode).into(), + "write_bytecode" => vm + .ctx + .new_bool( + !vm.sys_module + .get_attr("dont_write_bytecode", vm)? + .try_to_bool(vm)?, + ) + .into(), "warn_default_encoding" => vm.ctx.new_bool(settings.warn_default_encoding).into(), - "thread_inherit_context" => vm.ctx.new_bool(settings.thread_inherit_context).into(), - "context_aware_warnings" => vm.ctx.new_bool(settings.context_aware_warnings).into(), + "thread_inherit_context" => vm + .ctx + .new_int(i32::from(settings.thread_inherit_context)) + .into(), + "context_aware_warnings" => vm + .ctx + .new_int(i32::from(settings.context_aware_warnings)) + .into(), "optimization_level" => vm.ctx.new_int(settings.optimize).into(), "verbose" => vm.ctx.new_int(settings.verbose).into(), "bytes_warning" => vm.ctx.new_int(settings.bytes_warning).into(), diff --git a/crates/vm/src/stdlib/sys/monitoring/capi.rs b/crates/vm/src/stdlib/sys/monitoring/capi.rs index 2a0d0e9e862..a7f868bdf25 100644 --- a/crates/vm/src/stdlib/sys/monitoring/capi.rs +++ b/crates/vm/src/stdlib/sys/monitoring/capi.rs @@ -50,40 +50,12 @@ pub fn enter_scope( /// The current API does not keep a stack of scopes. pub fn exit_scope() {} -/// Fire a native event using the interpreter's monitoring callbacks. -/// Exception events consume the raised exception during callbacks and restore it -/// on success. A callback error replaces it, as for the C exception indicator. -/// -/// # Safety -/// `state` must remain valid throughout the call. It is accessed through a raw -/// pointer so a Python callback may refresh the same state with EnterScope. -pub unsafe fn fire_event( - vm: &VirtualMachine, - state: *mut EventState, - codelike: &PyObject, - offset: i32, - event: u8, - extra: &[PyObjectRef], -) -> PyResult<()> { - struct NativeState(*mut EventState); - impl EventStateAccess for NativeState { - fn active(&self) -> u8 { - unsafe { (*self.0).active } - } - - fn disable(&self, tool: usize) { - unsafe { (*self.0).active &= !(1 << tool) }; - } - } - fire_event_with_state(vm, &NativeState(state), codelike, offset, event, extra) -} - pub(crate) trait EventStateAccess { fn active(&self) -> u8; fn disable(&self, tool: usize); } -// The native ABI and thread-safe test fixtures update their actual state after +// Thread-safe test fixtures update their actual state after // each callback, so subsequent callbacks can observe an earlier DISABLE. pub(crate) fn fire_event_with_state( vm: &VirtualMachine, diff --git a/scripts/generate_capi_exports.py b/scripts/generate_capi_exports.py index fd983cbda5f..9aa0465de8c 100644 --- a/scripts/generate_capi_exports.py +++ b/scripts/generate_capi_exports.py @@ -1,7 +1,7 @@ """Generate Windows C API exports from implemented definitions. -Exports are Stable ABI definitions plus the explicitly listed existing -non-Stable ABI compatibility function. +Exports are implemented Stable ABI definitions without non-Stable ABI +compatibility exceptions. Maintenance dependencies: Python 3.11+, Git, the pinned CPython checkout, and RustPython source. Compiled verification additionally needs GNU nm and a matching @@ -21,9 +21,6 @@ MANIFEST_SHA256 = "eab726f2482c71e67a23ea04aaa9c5a11d565bee4472320f813262f273c1e966" EXPORT_ATTR = "#[unsafe(no_mangle)]" ITEM_PREFIX = r"#\[unsafe\(no_mangle\)\](?:\s*#\[[^\n]*\])*\s*pub\s+" -# Already implemented and exported on Unix; needed for Windows frame access. -# This function is not part of CPython's Stable ABI. -NON_STABLE_COMPATIBILITY_EXPORTS = {"PyFrame_GetLocals": "function"} def fail(message): @@ -32,7 +29,7 @@ def fail(message): def source_symbols(repo): symbols = {} - macro_templates = {"object.rs": 2, "pyerrors.rs": 1, "monitoring.rs": 1} + macro_templates = {"object.rs": 2, "pyerrors.rs": 1} for path in sorted((repo / "crates/capi/src").rglob("*.rs")): source, _, tests = path.read_text().partition("#[cfg(test)]") if EXPORT_ATTR in tests: @@ -51,7 +48,6 @@ def source_symbols(repo): if source.count(EXPORT_ATTR) != accounted: fail(f"Unrecognized no_mangle item or macro template: {path}") functions += re.findall(r"define_py_check!\(\s*(?:exact\s+)?fn\s+(\w+)", source) - functions += re.findall(r"^fire_event!\((\w+)", source, re.MULTILINE) data += re.findall(r"^\s*(PyExc_\w+)\s*=>", source, re.MULTILINE) # Current production cfg only selects private FfiResult implementations. # New conditional modules/exports require an explicit target mapping. @@ -102,26 +98,18 @@ def generate(args): manifest = tomllib.loads(manifest_bytes.decode()) source = source_symbols(args.repo) if args.source_only: - if args.allow_unbuilt: - fail("--allow-unbuilt requires an actual --library comparison") print("SOURCE-ONLY: ELF and PE verification are pending", file=sys.stderr) else: compiled = compiled_symbols(args.library, args.nm) missing = source.keys() - compiled.keys() unexpected = compiled.keys() - source.keys() - allowed = set(args.allow_unbuilt) - if missing != allowed or unexpected: + if missing or unexpected: fail( f"Source/library mismatch: unbuilt={sorted(missing)}, stale={sorted(unexpected)}" ) for name in source.keys() & compiled.keys(): if source[name] != compiled[name]: fail(f"Source/library kind mismatch: {name}") - if allowed: - print( - f"UNVERIFIED compiled definitions: {', '.join(sorted(allowed))}", - file=sys.stderr, - ) print(f"{len(compiled)} compiled exports compared") exports = {} for name, kind in source.items(): @@ -131,19 +119,11 @@ def generate(args): if "ifdef" in entry: fail(f"Review target condition for {name}: {entry['ifdef']}") exports[name] = kind - stable_count = len(exports) - for name, kind in NON_STABLE_COMPATIBILITY_EXPORTS.items(): - if source.get(name) != kind: - fail(f"Missing or mismatched compatibility definition: {name}") - if name in manifest.get(kind, {}): - fail(f"Remove the now-stable compatibility exception for {name}") - exports[name] = kind contents = ( "; Generated by scripts/generate_capi_exports.py; do not edit by hand.\n" f"; CPython v3.15.0: {CPYTHON_REVISION}\n" f"; Misc/stable_abi.toml SHA-256: {MANIFEST_SHA256}\n" "; Implemented Stable ABI functions/data, including ABI-only names.\n" - "; Non-Stable ABI compatibility export: PyFrame_GetLocals.\n" "EXPORTS\n" ) + "".join( f" {name}{' DATA' if kind == 'data' else ''}\n" @@ -156,7 +136,7 @@ def generate(args): args.output.write_text(contents) print( f"{len(source)} source exports; {len(exports)} Windows exports " - f"({stable_count} Stable ABI, {len(NON_STABLE_COMPATIBILITY_EXPORTS)} non-Stable ABI); " + "(Stable ABI only); " f"({sum(kind == 'data' for kind in exports.values())} data)" ) @@ -195,9 +175,6 @@ def main(): generation.add_argument("--nm", default="nm") generation.add_argument("--output", type=Path, required=True) generation.add_argument("--check", action="store_true") - generation.add_argument( - "--allow-unbuilt", action="append", default=[], choices=["PyCFunction_Call"] - ) pe = modes.add_parser("verify-pe") pe.add_argument("--definition", type=Path, required=True) pe.add_argument("--dumpbin", type=Path, required=True) From e5cad352f821156244a27931efd406c340b3c0e7 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Sun, 18 Oct 2026 16:26:36 +0900 Subject: [PATCH 03/12] Match final compiler expectations and WASI chmod semantics Preserve bound-method reduce error ordering and restrict subprocess integration to supported platforms. Assisted-by: OpenAI Codex:model version unavailable --- .github/workflows/ci.yaml | 1 + crates/vm/src/builtins/function.rs | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1380fcf7ce1..b6137883a8f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1110,6 +1110,7 @@ jobs: builtin_str builtin_thread cli_int_max_str_digits + import_lazy_native protocol_index_argument recursion stdlib_ast diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index ee822bd0e89..902f0358765 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -1482,9 +1482,28 @@ impl Py { &self, vm: &VirtualMachine, ) -> PyResult<(PyObjectRef, (PyObjectRef, PyObjectRef))> { - let func_name = self.function.get_attr("__name__", vm)?; - let builtins_getattr = vm.eval_get_builtin(vm.ctx.intern_str("getattr"))?; let func_self = self.object.clone(); + let mut func_name = self.function.get_attr("__name__", vm)?; + if let Some(name) = func_name.downcast_ref::() + && name.as_bytes().starts_with(b"__") + && !name.as_bytes().ends_with(b"__") + && !name.as_bytes().contains(&b'.') + { + let class = func_self + .downcast_ref::() + .unwrap_or_else(|| func_self.class()); + let class_name = class.__name__(vm); + let class_name = class_name + .as_wtf8() + .trim_start_matches(|ch| ch.to_u32() == u32::from(b'_')); + if !class_name.is_empty() { + func_name = vm + .ctx + .new_str(wtf8_concat!("_", class_name, name.as_wtf8())) + .into(); + } + } + let builtins_getattr = vm.eval_get_builtin(vm.ctx.intern_str("getattr"))?; Ok((builtins_getattr, (func_self, func_name))) } From c6a37ca021b8b373055a43602b92cac9d8d4d600 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Thu, 15 Oct 2026 13:54:49 +0900 Subject: [PATCH 04/12] Match final dictionary union and venv executable lookup Accept frozendict operands in defaultdict union. Follow final-component links once and otherwise use the CPython home/name fallback, ignoring creator metadata. Align existing venv assertions with exact-final reference results while preserving nested environments after parent removal. Assisted-by: OpenAI Codex:model version unavailable --- crates/vm/src/stdlib/_collections.rs | 6 +-- extra_tests/snippets/stdlib_venv_cycles.py | 6 +-- extra_tests/snippets/stdlib_venv_nested.py | 44 +++++++++++----------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/crates/vm/src/stdlib/_collections.rs b/crates/vm/src/stdlib/_collections.rs index e120e73cbc5..5a11f409d8b 100644 --- a/crates/vm/src/stdlib/_collections.rs +++ b/crates/vm/src/stdlib/_collections.rs @@ -11,8 +11,8 @@ mod _collections { VirtualMachine, atomic_func, builtins::{ IterStatus::{Active, Exhausted}, - PositionIterInternal, PyDict, PyGenericAlias, PyInt, PyList, PyStr, PyTuple, PyType, - PyTypeRef, locked_step, + PositionIterInternal, PyAnyDict, PyDict, PyGenericAlias, PyInt, PyList, PyStr, PyTuple, + PyType, PyTypeRef, locked_step, }, common::lock::{PyMutex, PyRwLock, PyRwLockReadGuard, PyRwLockWriteGuard}, convert::ToPyObject, @@ -1072,7 +1072,7 @@ mod _collections { rhs.class().name() ))); }; - if !other.fast_isinstance(vm.ctx.types.dict_type) { + if PyAnyDict::from_object(other).is_none() { return not_implemented(); } diff --git a/extra_tests/snippets/stdlib_venv_cycles.py b/extra_tests/snippets/stdlib_venv_cycles.py index 1cb537219be..abfb430b69e 100644 --- a/extra_tests/snippets/stdlib_venv_cycles.py +++ b/extra_tests/snippets/stdlib_venv_cycles.py @@ -1,4 +1,4 @@ -"""Malformed venv metadata must not select another installed interpreter.""" +"""Creator metadata does not override the venv home-based executable lookup.""" import os import sys @@ -45,5 +45,5 @@ timeout=10, check=True, ) - assert json.loads(result.stdout) == str(executable), result.stdout - print("PASS: cyclic venv metadata preserves interpreter identity") + assert json.loads(result.stdout) == str(decoy), result.stdout + print("PASS: cyclic creator metadata does not override venv home") diff --git a/extra_tests/snippets/stdlib_venv_nested.py b/extra_tests/snippets/stdlib_venv_nested.py index 80f0c5f007e..c74e8a5be52 100644 --- a/extra_tests/snippets/stdlib_venv_nested.py +++ b/extra_tests/snippets/stdlib_venv_nested.py @@ -1,4 +1,4 @@ -"""Regress RustPython venv aliases selecting the wrong base interpreter. +"""Check venv copies and symlinks against CPython's base-executable lookup. Standalone snippet: no third-party packages, network, or persistent files. The runtime must provide venv and its usual subprocess/filesystem support. @@ -9,8 +9,8 @@ def main(): - # This tests RustPython's native executable identity and its persisted base - # metadata. Windows uses a separate launcher and Scripts/python.exe layout. + # This tests RustPython's native executable identity. Windows uses a + # separate launcher and Scripts/python.exe layout. if sys.implementation.name != "rustpython": print("SKIP: RustPython-specific venv base-executable regression") return @@ -53,8 +53,8 @@ def main(): unrelated_cwd = work / "unrelated-cwd" unrelated_cwd.mkdir() - # A nonstandard primary name makes guessed home/rustpython or - # home/python3 names insufficient; venv must preserve its actual base. + # A nonstandard primary name distinguishes home/ from + # a symlink's actual target. base = base_home / "rustpython-base" source = os.path.realpath(sys.executable) try: @@ -62,8 +62,8 @@ def main(): except OSError: shutil.copy2(source, base) - # This path exists and is executable, but is deliberately not our - # interpreter. Missing-file-only fallback logic must not select it. + # CPython selects this existing home/python for a copied python alias, + # but follows the real target for a symlinked alias. decoy = base_home / "python" decoy.write_text( "#!/bin/sh\n" @@ -96,11 +96,13 @@ def run(command, *, expected_returncode=0): ) return result.stdout - def check_identity(executable, prefix=None): + def check_identity(executable, prefix=None, expected_base=base): result = json.loads(run([executable, "-I", "-c", identity_code])) assert result["implementation"] == "rustpython", result assert result["base_exists"], result - assert os.path.realpath(result["base"]) == os.path.realpath(base), result + assert os.path.realpath(result["base"]) == os.path.realpath( + expected_base + ), result if prefix is not None: assert os.path.realpath(result["prefix"]) == os.path.realpath(prefix), ( result @@ -114,16 +116,16 @@ def check_identity(executable, prefix=None): run([base, "-I", "-m", "venv", "--without-pip", f"--{outer_mode}", outer]) check_identity(outer / "bin" / base.name, outer) alias = outer / "bin" / "python" - check_identity(alias, outer) + check_identity(alias, outer, decoy if outer_mode == "copies" else base) nested_environments = [] for nested_mode in ("copies", "symlinks"): nested = work / f"nested-{outer_mode}-{nested_mode}" - # The common python alias is the trigger. Both modes must use - # the original base even though unrelated home/python exists. + # A copied alias selects home/python, so create through the + # primary name. A symlinked alias resolves the original base. run( [ - alias, + outer / "bin" / base.name if outer_mode == "copies" else alias, "-I", "-m", "venv", @@ -133,22 +135,22 @@ def check_identity(executable, prefix=None): ] ) check_identity(nested / "bin" / base.name, nested) - check_identity(nested / "bin" / "python", nested) - nested_environments.append(nested) + expected_base = decoy if nested_mode == "copies" else base + check_identity(nested / "bin" / "python", nested, expected_base) + nested_environments.append((nested, expected_base)) - # A copied nested environment's creator metadata names its parent - # venv. Stable base metadata must remain usable after that parent - # and its copied interpreter are gone. + # Creator metadata names the parent venv, but home and final-link + # lookup remain usable after that parent and its interpreter go. shutil.rmtree(outer) - for nested in nested_environments: + for nested, expected_base in nested_environments: check_identity(nested / "bin" / base.name, nested) - check_identity(nested / "bin" / "python", nested) + check_identity(nested / "bin" / "python", nested, expected_base) shutil.rmtree(nested) print( f"PASS: outer {outer_mode}; nested copies/symlinks survive parent deletion" ) - print("PASS: nested venv aliases preserve the RustPython base interpreter") + print("PASS: nested venv aliases follow CPython base-executable lookup") if __name__ == "__main__": From 600023f7d511e765fb23e2174ca397ad85587354 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Thu, 15 Oct 2026 14:21:09 +0900 Subject: [PATCH 05/12] Limit WASI integration snippets to configured capabilities Keep native thread, TLS and subprocess checks enabled on supported platforms. Assisted-by: OpenAI Codex:model version unavailable --- .github/workflows/ci.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b6137883a8f..06f34a8f13b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1115,6 +1115,8 @@ jobs: recursion stdlib_ast stdlib_asyncio + stdlib_asyncio_reinitialization_315 + stdlib_bytearray_take_bytes_threads_315 stdlib_collections_deque stdlib_contextvars stdlib_ctypes @@ -1126,6 +1128,7 @@ jobs: stdlib_io_blocking_buffer stdlib_itertools stdlib_json + stdlib_lock_precedence_315 stdlib_mmap stdlib_multiprocessing_condition stdlib_multiprocessing_fork_sharer @@ -1133,10 +1136,12 @@ jobs: stdlib_os stdlib_pwd stdlib_queue_signal + stdlib_rlock_constructor_315 stdlib_select stdlib_socket stdlib_sqlite stdlib_ssl + stdlib_ssl_introspection stdlib_ssl_short_recv stdlib_struct stdlib_subinterpreters @@ -1153,6 +1158,8 @@ jobs: stdlib_types stdlib_typing stdlib_urllib_https_misaligned_recv + stdlib_venv_cycles + stdlib_weakref_gc_threads_315 stdlib_zoneinfo_cache syntax_async syntax_async_comprehension From 6eeb03a882dc0b497a60b6e5e81ff867ae912f39 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Thu, 15 Oct 2026 14:24:05 +0900 Subject: [PATCH 06/12] Preserve Element.remove repr error semantics Match final CPython error formatting and propagate representation failures without losing surrogate text. Assisted-by: OpenAI Codex:model version unavailable --- crates/stdlib/src/elementtree.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/stdlib/src/elementtree.rs b/crates/stdlib/src/elementtree.rs index 73e46d0d6d0..d7a65c02d03 100644 --- a/crates/stdlib/src/elementtree.rs +++ b/crates/stdlib/src/elementtree.rs @@ -630,7 +630,10 @@ pub(crate) mod _elementtree { i += 1; } let Some(i) = found else { - return Err(vm.new_value_error("Element.remove(x): element not found")); + let mut message = subelement.repr(vm)?.as_wtf8().to_owned(); + message.push_str(" not in "); + message.push_wtf8(zelf.as_object().repr(vm)?.as_wtf8()); + return Err(vm.new_value_error(message)); }; let _recycle = { let mut inner = zelf.inner.write(); From bc92520e1e903df214e639e4c0e738845e54da7d Mon Sep 17 00:00:00 2001 From: youknowdot Date: Sun, 18 Oct 2026 18:22:19 +0900 Subject: [PATCH 07/12] Retire passing pyexpat expected-failure markers Regenerate the canonical CPython 3.15 test module through update_lib, removing only 18 unconditional expected-failure markers whose tests unexpectedly succeeded on Linux, macOS, and Windows at 0d9119bd44. Preserve all test bodies, guards, assertions, and remaining markers; six super-only marker wrappers return to canonical inheritance. Assisted-by: OpenAI Codex:model version unavailable --- Lib/test/test_pyexpat.py | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index a9400831e3a..40dc6a80d4b 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -864,7 +864,6 @@ def test_large_character_data_does_not_crash(self, size): self.assertEqual(parser.Parse(xml_data, True), 1) class ElementDeclHandlerTest(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: TypeError not raised by Parse def test_trigger_leak(self): # Unfixed, this test would leak the memory of the so-called # "content model" in function ``my_ElementDeclHandler`` of pyexpat. @@ -944,7 +943,6 @@ class ForeignDTDTests(unittest.TestCase): """ Tests for the UseForeignDTD method of expat parser objects. """ - @unittest.expectedFailure # TODO: RUSTPYTHON def test_use_foreign_dtd(self): """ If UseForeignDTD is passed True and a document without an external @@ -973,7 +971,6 @@ def resolve_entity(context, base, system_id, public_id): parser.Parse(b"") self.assertEqual(handler_call_args, [(None, None)]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_ignore_use_foreign_dtd(self): """ If UseForeignDTD is passed True and a document with an external @@ -1002,7 +999,6 @@ class ParentParserLifetimeTest(unittest.TestCase): See https://github.com/python/cpython/issues/139400. """ - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'ExternalEntityParserCreate' def test_parent_parser_outlives_its_subparsers__single(self): parser = expat.ParserCreate() subparser = parser.ExternalEntityParserCreate(None) @@ -1011,7 +1007,6 @@ def test_parent_parser_outlives_its_subparsers__single(self): # while it's still being referenced by a related subparser. del parser - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'ExternalEntityParserCreate' def test_parent_parser_outlives_its_subparsers__multiple(self): parser = expat.ParserCreate() subparser_one = parser.ExternalEntityParserCreate(None) @@ -1021,7 +1016,6 @@ def test_parent_parser_outlives_its_subparsers__multiple(self): # while it's still being referenced by a related subparser. del parser - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'ExternalEntityParserCreate' def test_parent_parser_outlives_its_subparsers__chain(self): parser = expat.ParserCreate() subparser = parser.ExternalEntityParserCreate(None) @@ -1082,7 +1076,6 @@ def test_error_path_no_crash(self): class ReparseDeferralTest(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'GetReparseDeferralEnabled' def test_getter_setter_round_trip(self): parser = expat.ParserCreate() enabled = (expat.version_info >= (2, 6, 0)) @@ -1118,7 +1111,6 @@ def start_element(name, _): self.assertEqual(started, ['doc']) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetReparseDeferralEnabled' def test_reparse_deferral_disabled(self): started = [] @@ -1288,7 +1280,6 @@ def set_activation_threshold(self, parser, threshold): def set_maximum_amplification(self, parser, max_factor): return parser.SetBillionLaughsAttackProtectionMaximumAmplification(max_factor) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetBillionLaughsAttackProtectionMaximumAmplification' def test_set_activation_threshold__threshold_reached(self): parser = expat.ParserCreate() # Choose a threshold expected to be always reached. @@ -1299,7 +1290,6 @@ def test_set_activation_threshold__threshold_reached(self): payload = self.exponential_expansion_payload(ncols=10, nrows=4) self.assert_rejected(parser.Parse, payload, True) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetBillionLaughsAttackProtectionMaximumAmplification' def test_set_activation_threshold__threshold_not_reached(self): parser = expat.ParserCreate() # Choose a threshold expected to be never reached. @@ -1310,7 +1300,6 @@ def test_set_activation_threshold__threshold_not_reached(self): payload = self.exponential_expansion_payload(ncols=10, nrows=4) self.assertIsNotNone(parser.Parse(payload, True)) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetBillionLaughsAttackProtectionMaximumAmplification' def test_set_maximum_amplification__amplification_exceeded(self): parser = expat.ParserCreate() # Unconditionally enable maximum activation factor. @@ -1321,7 +1310,6 @@ def test_set_maximum_amplification__amplification_exceeded(self): payload = self.exponential_expansion_payload(ncols=1, nrows=2) self.assert_rejected(parser.Parse, payload, True) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetBillionLaughsAttackProtectionMaximumAmplification' def test_set_maximum_amplification__amplification_not_exceeded(self): parser = expat.ParserCreate() # Unconditionally enable maximum activation factor. @@ -1332,30 +1320,6 @@ def test_set_maximum_amplification__amplification_not_exceeded(self): payload = self.exponential_expansion_payload(ncols=1, nrows=2) self.assertIsNotNone(parser.Parse(payload, True)) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetBillionLaughsAttackProtectionMaximumAmplification' - def test_set_activation_threshold__fail_for_subparser(self): - return super().test_set_activation_threshold__fail_for_subparser() - - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetBillionLaughsAttackProtectionMaximumAmplification' - def test_set_activation_threshold__invalid_threshold_type(self): - return super().test_set_activation_threshold__invalid_threshold_type() - - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetBillionLaughsAttackProtectionMaximumAmplification' - def test_set_maximum_amplification__fail_for_subparser(self): - return super().test_set_maximum_amplification__fail_for_subparser() - - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetBillionLaughsAttackProtectionMaximumAmplification' - def test_set_maximum_amplification__infinity(self): - return super().test_set_maximum_amplification__infinity() - - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetBillionLaughsAttackProtectionMaximumAmplification' - def test_set_maximum_amplification__invalid_max_factor_range(self): - return super().test_set_maximum_amplification__invalid_max_factor_range() - - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetBillionLaughsAttackProtectionMaximumAmplification' - def test_set_maximum_amplification__invalid_max_factor_type(self): - return super().test_set_maximum_amplification__invalid_max_factor_type() - @unittest.skipIf(not hasattr(expat.XMLParserType, "SetAllocTrackerMaximumAmplification"), From 1470e0fe6fec124eb70ce44d83c0ee77e4daf525 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Sun, 18 Oct 2026 18:28:03 +0900 Subject: [PATCH 08/12] Preserve compiler module conversion after REPL rebase Keep the AsObject trait available in the file execution submodule after main split its imports from the interactive execution scope. Assisted-by: OpenAI Codex:model version unavailable --- crates/vm/src/vm/python_run.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vm/src/vm/python_run.rs b/crates/vm/src/vm/python_run.rs index e40bca944cc..789c5e7d151 100644 --- a/crates/vm/src/vm/python_run.rs +++ b/crates/vm/src/vm/python_run.rs @@ -61,7 +61,7 @@ impl VirtualMachine { #[cfg(feature = "host_env")] mod file_run { use crate::{ - Py, PyResult, VirtualMachine, + AsObject, Py, PyResult, VirtualMachine, builtins::{PyCode, PyDict}, compiler::{self}, scope::Scope, From a33c6184a274a622d834152e15a7f41da90b712e Mon Sep 17 00:00:00 2001 From: youknowdot Date: Sun, 18 Oct 2026 21:02:33 +0900 Subject: [PATCH 09/12] Sync canonical doctest and pickletools coverage for Python 3.15 Refresh canonical test expectations and color control to match the already updated stdlib. Retire two command-line expected failures confirmed as unexpected successes on Linux, macOS, and Windows. Assisted-by: OpenAI Codex:model version unavailable --- Lib/test/test_cmd_line.py | 2 - Lib/test/test_doctest/test_doctest.py | 459 ++++++++++++++++---------- Lib/test/test_pickletools.py | 179 +++++++++- 3 files changed, 450 insertions(+), 190 deletions(-) diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index 248d6047f45..c7716fb7191 100644 --- a/Lib/test/test_cmd_line.py +++ b/Lib/test/test_cmd_line.py @@ -1285,7 +1285,6 @@ def test_import_time(self): assert_python_failure('-X', 'importtime=-1', '-c', code) assert_python_failure('-X', 'importtime=3', '-c', code) - @unittest.expectedFailure # TODO: RUSTPYTHON; -X importtime=2 timing output and surrogate module import exception semantics are not implemented correctly def test_import_time_unencodable_module_name(self): code = textwrap.dedent(""" import sys, types @@ -1371,7 +1370,6 @@ def test_presite(self): proc = assert_python_ok("-X", f"presite={entrypoint}", "-c", "pass") self.assertEqual(proc.out.rstrip(), b"presite func") - @unittest.expectedFailure # TODO: RUSTPYTHON; explicit PYTHONHOME startup path configuration and fatal encodings failure reporting are not implemented def test_dump_path_config(self): # gh-151253: At the first import (import encodings) during Python # startup, if the import fails, dump the Python path configuration. diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index b9f67f07ebc..b125693ab08 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -833,6 +833,118 @@ def test_empty_namespace_package(self): self.assertEqual(len(include_empty_finder.find(mod)), 1) self.assertEqual(len(exclude_empty_finder.find(mod)), 0) + def test_lineno_of_test_dict_strings(self): + """Test line numbers are found for __test__ dict strings.""" + module_content = '''\ +"""Module docstring.""" + +def dummy_function(): + """Dummy function docstring.""" + pass + +__test__ = { + 'test_string': """ + This is a test string. + >>> 1 + 1 + 2 + """, +} +''' + with tempfile.TemporaryDirectory() as tmpdir: + module_path = os.path.join(tmpdir, 'test_module_lineno.py') + with open(module_path, 'w') as f: + f.write(module_content) + + sys.path.insert(0, tmpdir) + try: + import test_module_lineno + finder = doctest.DocTestFinder() + tests = finder.find(test_module_lineno) + + test_dict_test = None + for test in tests: + if '__test__' in test.name: + test_dict_test = test + break + + self.assertIsNotNone( + test_dict_test, + "__test__ dict test not found" + ) + # gh-69113: line number should not be None for __test__ strings + self.assertIsNotNone( + test_dict_test.lineno, + "Line number should not be None for __test__ dict strings" + ) + self.assertGreater( + test_dict_test.lineno, + 0, + "Line number should be positive" + ) + finally: + if 'test_module_lineno' in sys.modules: + del sys.modules['test_module_lineno'] + sys.path.pop(0) + + def test_lineno_multiline_matching(self): + """Test multi-line matching when no unique line exists.""" + # gh-69113: test that line numbers are found even when lines + # appear multiple times (e.g., ">>> x = 1" in both test entries) + module_content = '''\ +"""Module docstring.""" + +__test__ = { + 'test_one': """ + >>> x = 1 + >>> x + 1 + """, + 'test_two': """ + >>> x = 1 + >>> x + 2 + """, +} +''' + with tempfile.TemporaryDirectory() as tmpdir: + module_path = os.path.join(tmpdir, 'test_module_multiline.py') + with open(module_path, 'w') as f: + f.write(module_content) + + sys.path.insert(0, tmpdir) + try: + import test_module_multiline + finder = doctest.DocTestFinder() + tests = finder.find(test_module_multiline) + + test_one = None + test_two = None + for test in tests: + if 'test_one' in test.name: + test_one = test + elif 'test_two' in test.name: + test_two = test + + self.assertIsNotNone(test_one, "test_one not found") + self.assertIsNotNone(test_two, "test_two not found") + self.assertIsNotNone( + test_one.lineno, + "Line number should not be None for test_one" + ) + self.assertIsNotNone( + test_two.lineno, + "Line number should not be None for test_two" + ) + self.assertNotEqual( + test_one.lineno, + test_two.lineno, + "test_one and test_two should have different line numbers" + ) + finally: + if 'test_module_multiline' in sys.modules: + del sys.modules['test_module_multiline'] + sys.path.pop(0) + def test_DocTestParser(): r""" Unit tests for the `DocTestParser` class. @@ -2273,20 +2385,22 @@ def test_DocTestSuite(): >>> suite = doctest.DocTestSuite(test.test_doctest.sample_doctest) >>> result = suite.run(unittest.TestResult()) >>> result - + >>> for tst, _ in result.failures: ... print(tst) - bad (test.test_doctest.sample_doctest.__test__) - foo (test.test_doctest.sample_doctest) - test_silly_setup (test.test_doctest.sample_doctest) - y_is_one (test.test_doctest.sample_doctest) + bad (test.test_doctest.sample_doctest.__test__) [0] + foo (test.test_doctest.sample_doctest) [0] + >>> for tst, _ in result.errors: + ... print(tst) + test_silly_setup (test.test_doctest.sample_doctest) [1] + y_is_one (test.test_doctest.sample_doctest) [0] We can also supply the module by name: >>> suite = doctest.DocTestSuite('test.test_doctest.sample_doctest') >>> result = suite.run(unittest.TestResult()) >>> result - + The module need not contain any doctest examples: @@ -2308,21 +2422,26 @@ def test_DocTestSuite(): >>> result >>> len(result.skipped) - 2 + 7 >>> for tst, _ in result.skipped: ... print(tst) + double_skip (test.test_doctest.sample_doctest_skip) [0] + double_skip (test.test_doctest.sample_doctest_skip) [1] double_skip (test.test_doctest.sample_doctest_skip) + partial_skip_fail (test.test_doctest.sample_doctest_skip) [0] + partial_skip_pass (test.test_doctest.sample_doctest_skip) [0] + single_skip (test.test_doctest.sample_doctest_skip) [0] single_skip (test.test_doctest.sample_doctest_skip) >>> for tst, _ in result.failures: ... print(tst) - no_skip_fail (test.test_doctest.sample_doctest_skip) - partial_skip_fail (test.test_doctest.sample_doctest_skip) + no_skip_fail (test.test_doctest.sample_doctest_skip) [0] + partial_skip_fail (test.test_doctest.sample_doctest_skip) [1] We can use the current module: >>> suite = test.test_doctest.sample_doctest.test_suite() >>> suite.run(unittest.TestResult()) - + We can also provide a DocTestFinder: @@ -2330,7 +2449,7 @@ def test_DocTestSuite(): >>> suite = doctest.DocTestSuite('test.test_doctest.sample_doctest', ... test_finder=finder) >>> suite.run(unittest.TestResult()) - + The DocTestFinder need not return any tests: @@ -2346,7 +2465,7 @@ def test_DocTestSuite(): >>> suite = doctest.DocTestSuite('test.test_doctest.sample_doctest', globs={}) >>> suite.run(unittest.TestResult()) - + Alternatively, we can provide extra globals. Here we'll make an error go away by providing an extra global variable: @@ -2354,7 +2473,7 @@ def test_DocTestSuite(): >>> suite = doctest.DocTestSuite('test.test_doctest.sample_doctest', ... extraglobs={'y': 1}) >>> suite.run(unittest.TestResult()) - + You can pass option flags. Here we'll cause an extra error by disabling the blank-line feature: @@ -2362,7 +2481,7 @@ def test_DocTestSuite(): >>> suite = doctest.DocTestSuite('test.test_doctest.sample_doctest', ... optionflags=doctest.DONT_ACCEPT_BLANKLINE) >>> suite.run(unittest.TestResult()) - + You can supply setUp and tearDown functions: @@ -2379,7 +2498,7 @@ def test_DocTestSuite(): >>> suite = doctest.DocTestSuite('test.test_doctest.sample_doctest', ... setUp=setUp, tearDown=tearDown) >>> suite.run(unittest.TestResult()) - + But the tearDown restores sanity: @@ -2397,7 +2516,7 @@ def test_DocTestSuite(): >>> suite = doctest.DocTestSuite('test.test_doctest.sample_doctest', setUp=setUp) >>> suite.run(unittest.TestResult()) - + Here, we didn't need to use a tearDown function because we modified the test globals, which are a copy of the @@ -2413,115 +2532,99 @@ def test_DocTestSuite_errors(): >>> suite = doctest.DocTestSuite(mod) >>> result = suite.run(unittest.TestResult()) >>> result - + >>> print(result.failures[0][1]) # doctest: +ELLIPSIS - AssertionError: Failed doctest test for test.test_doctest.sample_doctest_errors - File "...sample_doctest_errors.py", line 0, in sample_doctest_errors - - ---------------------------------------------------------------------- - File "...sample_doctest_errors.py", line 5, in test.test_doctest.sample_doctest_errors - Failed example: + Traceback (most recent call last): + File "...sample_doctest_errors.py", line 5, in test.test_doctest.sample_doctest_errors + >...>> 2 + 2 + AssertionError: Failed example: 2 + 2 Expected: 5 Got: 4 - ---------------------------------------------------------------------- - File "...sample_doctest_errors.py", line 7, in test.test_doctest.sample_doctest_errors - Failed example: - 1/0 - Exception raised: - Traceback (most recent call last): - File "", line 1, in - 1/0 - ~^~ - ZeroDivisionError: division by zero >>> print(result.failures[1][1]) # doctest: +ELLIPSIS - AssertionError: Failed doctest test for test.test_doctest.sample_doctest_errors.__test__.bad - File "...sample_doctest_errors.py", line unknown line number, in bad - - ---------------------------------------------------------------------- - File "...sample_doctest_errors.py", line ?, in test.test_doctest.sample_doctest_errors.__test__.bad - Failed example: + Traceback (most recent call last): + File "...sample_doctest_errors.py", line 37, in test.test_doctest.sample_doctest_errors.__test__.bad + >...>> 2 + 2 + AssertionError: Failed example: 2 + 2 Expected: 5 Got: 4 - ---------------------------------------------------------------------- - File "...sample_doctest_errors.py", line ?, in test.test_doctest.sample_doctest_errors.__test__.bad - Failed example: - 1/0 - Exception raised: - Traceback (most recent call last): - File "", line 1, in - 1/0 - ~^~ - ZeroDivisionError: division by zero >>> print(result.failures[2][1]) # doctest: +ELLIPSIS - AssertionError: Failed doctest test for test.test_doctest.sample_doctest_errors.errors - File "...sample_doctest_errors.py", line 14, in errors - - ---------------------------------------------------------------------- - File "...sample_doctest_errors.py", line 16, in test.test_doctest.sample_doctest_errors.errors - Failed example: + Traceback (most recent call last): + File "...sample_doctest_errors.py", line 16, in test.test_doctest.sample_doctest_errors.errors + >...>> 2 + 2 + AssertionError: Failed example: 2 + 2 Expected: 5 Got: 4 - ---------------------------------------------------------------------- - File "...sample_doctest_errors.py", line 18, in test.test_doctest.sample_doctest_errors.errors - Failed example: + + >>> print(result.errors[0][1]) # doctest: +ELLIPSIS + Traceback (most recent call last): + File "...sample_doctest_errors.py", line 7, in test.test_doctest.sample_doctest_errors + >...>> 1/0 + File "", line 1, in 1/0 - Exception raised: - Traceback (most recent call last): - File "", line 1, in - 1/0 - ~^~ - ZeroDivisionError: division by zero - ---------------------------------------------------------------------- - File "...sample_doctest_errors.py", line 23, in test.test_doctest.sample_doctest_errors.errors - Failed example: + ~^~ + ZeroDivisionError: division by zero + + >>> print(result.errors[1][1]) # doctest: +ELLIPSIS + Traceback (most recent call last): + File "...sample_doctest_errors.py", line 39, in test.test_doctest.sample_doctest_errors.__test__.bad + >...>> 1/0 + File "", line 1, in + 1/0 + ~^~ + ZeroDivisionError: division by zero + + >>> print(result.errors[2][1]) # doctest: +ELLIPSIS + Traceback (most recent call last): + File "...sample_doctest_errors.py", line 18, in test.test_doctest.sample_doctest_errors.errors + >...>> 1/0 + File "", line 1, in + 1/0 + ~^~ + ZeroDivisionError: division by zero + + >>> print(result.errors[3][1]) # doctest: +ELLIPSIS + Traceback (most recent call last): + File "...sample_doctest_errors.py", line 23, in test.test_doctest.sample_doctest_errors.errors + >...>> f() + File "", line 1, in f() - Exception raised: - Traceback (most recent call last): - File "", line 1, in - f() - ~^^ - File "", line 2, in f - 2 + '2' - ~~^~~~~ - TypeError: ... - ---------------------------------------------------------------------- - File "...sample_doctest_errors.py", line 25, in test.test_doctest.sample_doctest_errors.errors - Failed example: - g() - Exception raised: - Traceback (most recent call last): - File "", line 1, in - g() - ~^^ - File "...sample_doctest_errors.py", line 12, in g - [][0] # line 12 - ~~^^^ - IndexError: list index out of range + ~^^ + File "", line 2, in f + 2 + '2' + ~~^~~~~ + TypeError: ... - >>> print(result.failures[3][1]) # doctest: +ELLIPSIS - AssertionError: Failed doctest test for test.test_doctest.sample_doctest_errors.syntax_error - File "...sample_doctest_errors.py", line 29, in syntax_error + >>> print(result.errors[4][1]) # doctest: +ELLIPSIS + Traceback (most recent call last): + File "...sample_doctest_errors.py", line 25, in test.test_doctest.sample_doctest_errors.errors + >...>> g() + File "", line 1, in + g() + ~^^ + File "...sample_doctest_errors.py", line 12, in g + [][0] # line 12 + ~~^^^ + IndexError: list index out of range - ---------------------------------------------------------------------- - File "...sample_doctest_errors.py", line 31, in test.test_doctest.sample_doctest_errors.syntax_error - Failed example: + >>> print(result.errors[5][1]) # doctest: +ELLIPSIS + Traceback (most recent call last): + File "...sample_doctest_errors.py", line 31, in test.test_doctest.sample_doctest_errors.syntax_error + >...>> 2+*3 + File "", line 1 2+*3 - Exception raised: - File "", line 1 - 2+*3 - ^ - SyntaxError: invalid syntax + ^ + SyntaxError: invalid syntax """ @@ -2536,7 +2639,7 @@ def test_DocFileSuite(): ... 'test_doctest2.txt', ... 'test_doctest4.txt') >>> suite.run(unittest.TestResult()) - + The test files are looked for in the directory containing the calling module. A package keyword argument can be provided to @@ -2548,14 +2651,14 @@ def test_DocFileSuite(): ... 'test_doctest4.txt', ... package='test.test_doctest') >>> suite.run(unittest.TestResult()) - + '/' should be used as a path separator. It will be converted to a native separator at run time: >>> suite = doctest.DocFileSuite('../test_doctest/test_doctest.txt') >>> suite.run(unittest.TestResult()) - + If DocFileSuite is used from an interactive session, then files are resolved relative to the directory of sys.argv[0]: @@ -2581,7 +2684,7 @@ def test_DocFileSuite(): >>> suite = doctest.DocFileSuite(test_file, module_relative=False) >>> suite.run(unittest.TestResult()) - + It is an error to specify `package` when `module_relative=False`: @@ -2599,12 +2702,15 @@ def test_DocFileSuite(): ... 'test_doctest_skip2.txt') >>> result = suite.run(unittest.TestResult()) >>> result - + >>> len(result.skipped) - 1 + 4 >>> for tst, _ in result.skipped: # doctest: +ELLIPSIS ... print('=', tst) + = ...test_doctest_skip.txt [0] + = ...test_doctest_skip.txt [1] = ...test_doctest_skip.txt + = ...test_doctest_skip2.txt [0] You can specify initial global variables: @@ -2613,7 +2719,7 @@ def test_DocFileSuite(): ... 'test_doctest4.txt', ... globs={'favorite_color': 'blue'}) >>> suite.run(unittest.TestResult()) - + In this case, we supplied a missing favorite color. You can provide doctest options: @@ -2624,7 +2730,7 @@ def test_DocFileSuite(): ... optionflags=doctest.DONT_ACCEPT_BLANKLINE, ... globs={'favorite_color': 'blue'}) >>> suite.run(unittest.TestResult()) - + And, you can provide setUp and tearDown functions: @@ -2643,7 +2749,7 @@ def test_DocFileSuite(): ... 'test_doctest4.txt', ... setUp=setUp, tearDown=tearDown) >>> suite.run(unittest.TestResult()) - + But the tearDown restores sanity: @@ -2685,7 +2791,7 @@ def test_DocFileSuite(): ... 'test_doctest4.txt', ... encoding='utf-8') >>> suite.run(unittest.TestResult()) - + """ def test_DocFileSuite_errors(): @@ -2695,52 +2801,49 @@ def test_DocFileSuite_errors(): >>> suite = doctest.DocFileSuite('test_doctest_errors.txt') >>> result = suite.run(unittest.TestResult()) >>> result - + >>> print(result.failures[0][1]) # doctest: +ELLIPSIS - AssertionError: Failed doctest test for test_doctest_errors.txt - File "...test_doctest_errors.txt", line 0 - - ---------------------------------------------------------------------- - File "...test_doctest_errors.txt", line 4, in test_doctest_errors.txt - Failed example: + Traceback (most recent call last): + File "...test_doctest_errors.txt", line 4, in test_doctest_errors.txt + >...>> 2 + 2 + AssertionError: Failed example: 2 + 2 Expected: 5 Got: 4 - ---------------------------------------------------------------------- - File "...test_doctest_errors.txt", line 6, in test_doctest_errors.txt - Failed example: + + >>> print(result.errors[0][1]) # doctest: +ELLIPSIS + Traceback (most recent call last): + File "...test_doctest_errors.txt", line 6, in test_doctest_errors.txt + >...>> 1/0 + File "", line 1, in 1/0 - Exception raised: - Traceback (most recent call last): - File "", line 1, in - 1/0 - ~^~ - ZeroDivisionError: division by zero - ---------------------------------------------------------------------- - File "...test_doctest_errors.txt", line 11, in test_doctest_errors.txt - Failed example: + ~^~ + ZeroDivisionError: division by zero + + >>> print(result.errors[1][1]) # doctest: +ELLIPSIS + Traceback (most recent call last): + File "...test_doctest_errors.txt", line 11, in test_doctest_errors.txt + >...>> f() + File "", line 1, in f() - Exception raised: - Traceback (most recent call last): - File "", line 1, in - f() - ~^^ - File "", line 2, in f - 2 + '2' - ~~^~~~~ - TypeError: ... - ---------------------------------------------------------------------- - File "...test_doctest_errors.txt", line 13, in test_doctest_errors.txt - Failed example: + ~^^ + File "", line 2, in f + 2 + '2' + ~~^~~~~ + TypeError: ... + + >>> print(result.errors[2][1]) # doctest: +ELLIPSIS + Traceback (most recent call last): + File "...test_doctest_errors.txt", line 13, in test_doctest_errors.txt + >...>> 2+*3 + File "", line 1 2+*3 - Exception raised: - File "", line 1 - 2+*3 - ^ - SyntaxError: invalid syntax + ^ + SyntaxError: invalid syntax + """ def test_trailing_space_in_test(): @@ -2811,16 +2914,25 @@ def test_unittest_reportflags(): >>> import unittest >>> result = suite.run(unittest.TestResult()) >>> result - + >>> print(result.failures[0][1]) # doctest: +ELLIPSIS - AssertionError: Failed doctest test for test_doctest.txt - ... - Failed example: - favorite_color - ... - Failed example: + Traceback (most recent call last): + File ... + >...>> if 1: + AssertionError: Failed example: if 1: - ... + print('a') + print() + print('b') + Expected: + a + + b + Got: + a + + b + Note that we see both failures displayed. @@ -2829,18 +2941,8 @@ def test_unittest_reportflags(): Now, when we run the test: - >>> result = suite.run(unittest.TestResult()) - >>> result - - >>> print(result.failures[0][1]) # doctest: +ELLIPSIS - AssertionError: Failed doctest test for test_doctest.txt - ... - Failed example: - favorite_color - Exception raised: - ... - NameError: name 'favorite_color' is not defined - + >>> suite.run(unittest.TestResult()) + We get only the first failure. @@ -2850,22 +2952,20 @@ def test_unittest_reportflags(): >>> suite = doctest.DocFileSuite('test_doctest.txt', ... optionflags=doctest.DONT_ACCEPT_BLANKLINE | doctest.REPORT_NDIFF) - Then the default eporting options are ignored: + Then the default reporting options are ignored: >>> result = suite.run(unittest.TestResult()) >>> result - + *NOTE*: These doctest are intentionally not placed in raw string to depict the trailing whitespace using `\x20` in the diff below. >>> print(result.failures[0][1]) # doctest: +ELLIPSIS - AssertionError: Failed doctest test for test_doctest.txt - ... - Failed example: - favorite_color - ... - Failed example: + Traceback ... + File ... + >...>> if 1: + AssertionError: Failed example: if 1: print('a') print() @@ -3270,7 +3370,7 @@ def test_testmod_errors(): r""" ~^~ ZeroDivisionError: division by zero ********************************************************************** - File "...sample_doctest_errors.py", line ?, in test.test_doctest.sample_doctest_errors.__test__.bad + File "...sample_doctest_errors.py", line 37, in test.test_doctest.sample_doctest_errors.__test__.bad Failed example: 2 + 2 Expected: @@ -3278,7 +3378,7 @@ def test_testmod_errors(): r""" Got: 4 ********************************************************************** - File "...sample_doctest_errors.py", line ?, in test.test_doctest.sample_doctest_errors.__test__.bad + File "...sample_doctest_errors.py", line 39, in test.test_doctest.sample_doctest_errors.__test__.bad Failed example: 1/0 Exception raised: @@ -3673,9 +3773,9 @@ def test_run_doctestsuite_multiple_times(): >>> import test.test_doctest.sample_doctest >>> suite = doctest.DocTestSuite(test.test_doctest.sample_doctest) >>> suite.run(unittest.TestResult()) - + >>> suite.run(unittest.TestResult()) - + """ @@ -3855,8 +3955,7 @@ def test_syntax_error_with_incorrect_expected_note(): def load_tests(loader, tests, pattern): tests.addTest(doctest.DocTestSuite(doctest)) - from test.support.rustpython import DocTestChecker # TODO: RUSTPYTHON - tests.addTest(doctest.DocTestSuite(checker=DocTestChecker())) # TODO: RUSTPYTHON + tests.addTest(doctest.DocTestSuite()) return tests diff --git a/Lib/test/test_pickletools.py b/Lib/test/test_pickletools.py index a0c478e50bd..eb854aaa81a 100644 --- a/Lib/test/test_pickletools.py +++ b/Lib/test/test_pickletools.py @@ -1,7 +1,11 @@ import io +import itertools import pickle import pickletools +import tempfile +import textwrap from test import support +from test.support import os_helper from test.pickletester import AbstractPickleTests import doctest import unittest @@ -62,17 +66,11 @@ def test_optimize_binput_and_memoize(self): self.assertIs(unpickled2[1], unpickled2[2]) self.assertNotIn(pickle.BINPUT, pickled2) - - - @unittest.expectedFailure # TODO: RUSTPYTHON def test_c_methods(self): return super().test_c_methods() - - - class SimpleReader: def __init__(self, data): self.data = data @@ -166,6 +164,7 @@ def test_unknown_opcode_without_pos(self): next(it) +@support.force_not_colorized_test_class class DisTests(unittest.TestCase): maxDiff = None @@ -524,9 +523,173 @@ def test__all__(self): support.check__all__(self, pickletools, not_exported=not_exported) +@support.force_not_colorized_test_class +class CommandLineTest(unittest.TestCase): + def setUp(self): + self.filename = tempfile.mktemp() + self.addCleanup(os_helper.unlink, self.filename) + + @staticmethod + def text_normalize(string): + return textwrap.dedent(string).strip() + + def set_pickle_data(self, data): + with open(self.filename, 'wb') as f: + pickle.dump(data, f) + + def invoke_pickletools(self, *flags): + with ( + support.captured_stdout() as stdout, + support.captured_stderr() as stderr, + ): + pickletools._main(args=[*flags, self.filename]) + self.assertEqual(stderr.getvalue(), '') + return self.text_normalize(stdout.getvalue()) + + def check_output(self, data, expect, *flags): + with self.subTest(data=data, flags=flags): + self.set_pickle_data(data) + res = self.invoke_pickletools(*flags) + expect = self.text_normalize(expect) + self.assertListEqual(res.splitlines(), expect.splitlines()) + + def test_invocation(self): + # test various combinations of parameters + output_file = tempfile.mktemp() + self.addCleanup(os_helper.unlink, output_file) + base_flags = [ + (f'-o={output_file}', f'--output={output_file}'), + ('-m', '--memo'), + ('-l=2', '--indentlevel=2'), + ('-a', '--annotate'), + ('-p="Another:"', '--preamble="Another:"'), + ] + data = { 'a', 'b', 'c' } + + self.set_pickle_data(data) + + for r in range(1, len(base_flags) + 1): + for choices in itertools.combinations(base_flags, r=r): + for args in itertools.product(*choices): + with self.subTest(args=args[1:]): + self.invoke_pickletools(*args) + + def test_unknown_flag(self): + with self.assertRaises(SystemExit): + with support.captured_stderr() as stderr: + pickletools._main(args=['--unknown']) + self.assertStartsWith(stderr.getvalue(), 'usage: ') + + def test_output_flag(self): + # test 'python -m pickletools -o/--output' + output_file = tempfile.mktemp() + self.addCleanup(os_helper.unlink, output_file) + data = ('fake_data',) + expect = r''' + 0: \x80 PROTO 5 + 2: \x95 FRAME 15 + 11: \x8c SHORT_BINUNICODE 'fake_data' + 22: \x94 MEMOIZE (as 0) + 23: \x85 TUPLE1 + 24: \x94 MEMOIZE (as 1) + 25: . STOP + highest protocol among opcodes = 4 + ''' + for flag in [f'-o={output_file}', f'--output={output_file}']: + with self.subTest(data=data, flags=flag): + self.set_pickle_data(data) + res = self.invoke_pickletools(flag) + with open(output_file, 'r') as f: + res_from_file = self.text_normalize(f.read()) + expect = self.text_normalize(expect) + + self.assertListEqual(res.splitlines(), []) + self.assertListEqual(res_from_file.splitlines(), + expect.splitlines()) + + def test_memo_flag(self): + # test 'python -m pickletools -m/--memo' + data = ('fake_data',) + expect = r''' + 0: \x80 PROTO 5 + 2: \x95 FRAME 15 + 11: \x8c SHORT_BINUNICODE 'fake_data' + 22: \x94 MEMOIZE (as 0) + 23: \x85 TUPLE1 + 24: \x94 MEMOIZE (as 1) + 25: . STOP + highest protocol among opcodes = 4 + ''' + for flag in ['-m', '--memo']: + self.check_output(data, expect, flag) + + def test_indentlevel_flag(self): + # test 'python -m pickletools -l/--indentlevel' + data = ('fake_data',) + expect = r''' + 0: \x80 PROTO 5 + 2: \x95 FRAME 15 + 11: \x8c SHORT_BINUNICODE 'fake_data' + 22: \x94 MEMOIZE (as 0) + 23: \x85 TUPLE1 + 24: \x94 MEMOIZE (as 1) + 25: . STOP + highest protocol among opcodes = 4 + ''' + for flag in ['-l=2', '--indentlevel=2']: + self.check_output(data, expect, flag) + + def test_annotate_flag(self): + # test 'python -m pickletools -a/--annotate' + data = ('fake_data',) + expect = r''' + 0: \x80 PROTO 5 Protocol version indicator. + 2: \x95 FRAME 15 Indicate the beginning of a new frame. + 11: \x8c SHORT_BINUNICODE 'fake_data' Push a Python Unicode string object. + 22: \x94 MEMOIZE (as 0) Store the stack top into the memo. The stack is not popped. + 23: \x85 TUPLE1 Build a one-tuple out of the topmost item on the stack. + 24: \x94 MEMOIZE (as 1) Store the stack top into the memo. The stack is not popped. + 25: . STOP Stop the unpickling machine. + highest protocol among opcodes = 4 + ''' + for flag in ['-a', '--annotate']: + self.check_output(data, expect, flag) + + def test_preamble_flag(self): + # test 'python -m pickletools -p/--preamble' + data = ('fake_data',) + expect = r''' + Another: + 0: \x80 PROTO 5 + 2: \x95 FRAME 15 + 11: \x8c SHORT_BINUNICODE 'fake_data' + 22: \x94 MEMOIZE (as 0) + 23: \x85 TUPLE1 + 24: \x94 MEMOIZE (as 1) + 25: . STOP + highest protocol among opcodes = 4 + Another: + 0: \x80 PROTO 5 + 2: \x95 FRAME 15 + 11: \x8c SHORT_BINUNICODE 'fake_data' + 22: \x94 MEMOIZE (as 0) + 23: \x85 TUPLE1 + 24: \x94 MEMOIZE (as 1) + 25: . STOP + highest protocol among opcodes = 4 + ''' + for flag in ['-p=Another:', '--preamble=Another:']: + with self.subTest(data=data, flags=flag): + self.set_pickle_data(data) + with support.captured_stdout() as stdout: + pickletools._main(args=[flag, self.filename, self.filename]) + res = self.text_normalize(stdout.getvalue()) + expect = self.text_normalize(expect) + self.assertListEqual(res.splitlines(), expect.splitlines()) + + def load_tests(loader, tests, pattern): - from test.support.rustpython import DocTestChecker # TODO: RUSTPYTHON; Remove this - tests.addTest(doctest.DocTestSuite(pickletools, checker=DocTestChecker())) # XXX: RUSTPYTHON + tests.addTest(doctest.DocTestSuite(pickletools)) return tests From e2f58c865e24b49e2537c25dbb9a02cea6e889cc Mon Sep 17 00:00:00 2001 From: youknowdot Date: Sun, 18 Oct 2026 21:05:49 +0900 Subject: [PATCH 10/12] Sync pydoc and IMAP tests with canonical Python 3.15 behavior Use the pinned canonical keyword coverage and deprecated file-property contract while preserving existing RustPython test markers. Assisted-by: OpenAI Codex:model version unavailable --- Lib/test/test_imaplib.py | 52 ++++++++++++++----- Lib/test/test_pydoc/test_pydoc.py | 84 +++++++++++++------------------ 2 files changed, 73 insertions(+), 63 deletions(-) diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index 62d4d6e6e9f..ceabd085190 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -13,8 +13,7 @@ import socket from test.support import verbose, run_with_tz, run_with_locale, cpython_only -from test.support import hashlib_helper -from test.support import threading_helper +from test.support import hashlib_helper, threading_helper import unittest from unittest import mock from datetime import datetime, timezone, timedelta @@ -684,17 +683,12 @@ def test_login_cram_md5_plain_text(self): ret, _ = client.login_cram_md5("tim", "tanstaaftanstaaf") self.assertEqual(ret, "OK") + @hashlib_helper.block_algorithm("md5") def test_login_cram_md5_blocked(self): - def side_effect(*a, **kw): - raise ValueError - client, _ = self._setup(AuthHandler_CRAM_MD5) self.assertIn('AUTH=CRAM-MD5', client.capabilities) msg = re.escape("CRAM-MD5 authentication is not supported") - with ( - mock.patch("hmac.HMAC", side_effect=side_effect), - self.assertRaisesRegex(imaplib.IMAP4.error, msg) - ): + with self.assertRaisesRegex(imaplib.IMAP4.error, msg): client.login_cram_md5("tim", b"tanstaaftanstaaf") def test_aborted_authentication(self): @@ -1831,11 +1825,33 @@ def test_control_characters(self): # property tests - def test_file_property_should_not_be_accessed(self): + def test_file_property_getter(self): + client, _ = self._setup(SimpleIMAPHandler) + with self.assertWarns(DeprecationWarning): + self.assertIsInstance(client.file.raw, socket.SocketIO) + + def test_file_property_setter(self): client, _ = self._setup(SimpleIMAPHandler) - # the 'file' property replaced a private attribute that is now unsafe - with self.assertWarns(RuntimeWarning): - client.file + with self.assertWarns(DeprecationWarning): + # ensure that the caller closes the existing file + client.file.close() + for new_file in [mock.Mock(), None]: + with self.assertWarns(DeprecationWarning): + client.file = new_file + with self.assertWarns(DeprecationWarning): + self.assertIs(client.file, new_file) + + def test_file_property_setter_should_not_close_previous_file(self): + client, _ = self._setup(SimpleIMAPHandler) + with mock.patch.object(client, "_imaplib_file", mock.Mock()) as f: + f.close.assert_not_called() + with self.assertWarns(DeprecationWarning): + self.assertIs(client.file, f) + with self.assertWarns(DeprecationWarning): + client.file = None + with self.assertWarns(DeprecationWarning): + self.assertIsNone(client.file) + f.close.assert_not_called() class NewIMAPTests(NewIMAPTestsMixin, unittest.TestCase): @@ -2288,5 +2304,15 @@ def test_ssl_verified(self): client.shutdown() +class TestModule(unittest.TestCase): + def test_deprecated__version__(self): + with self.assertWarnsRegex( + DeprecationWarning, + "'__version__' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(imaplib, "__version__") + self.assertEqual(cm.filename, __file__) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index bac96087a64..b0b219cfab4 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -8,10 +8,7 @@ import pydoc import py_compile import keyword -try: # TODO: RUSTPYTHON; Implement `_pickle` - import _pickle -except ImportError: - _pickle = None +import _pickle import pkgutil import re import tempfile @@ -581,7 +578,7 @@ class object # of the known subclasses of object. (doc.docclass() used to # fail if HeapType was imported before running this test, like # when running tests sequentially.) - from _testcapi import HeapType + from _testcapi import HeapType # noqa: F401 except ImportError: pass text = doc.docclass(object) @@ -1009,6 +1006,8 @@ def test_synopsis_sourceless(self): os = import_helper.import_fresh_module('os') expected = os.__doc__.splitlines()[0] filename = os.__spec__.cached + if not filename: + raise unittest.SkipTest('requires .pyc files') synopsis = pydoc.synopsis(filename) self.assertEqual(synopsis, expected) @@ -1016,10 +1015,10 @@ def test_synopsis_sourceless(self): def test_synopsis_sourceless_empty_doc(self): with os_helper.temp_cwd() as test_dir: init_path = os.path.join(test_dir, 'foomod42.py') - cached_path = importlib.util.cache_from_source(init_path) + cached_path = init_path + 'c' with open(init_path, 'w') as fobj: fobj.write("foo = 1") - py_compile.compile(init_path) + py_compile.compile(init_path, cached_path) synopsis = pydoc.synopsis(init_path, {}) self.assertIsNone(synopsis) synopsis_cached = pydoc.synopsis(cached_path, {}) @@ -1363,47 +1362,6 @@ def test_url_search_package_error(self): finally: sys.path[:] = saved_paths - @unittest.skip('causes undesirable side-effects (#20128)') - def test_modules(self): - # See Helper.listmodules(). - num_header_lines = 2 - num_module_lines_min = 5 # Playing it safe. - num_footer_lines = 3 - expected = num_header_lines + num_module_lines_min + num_footer_lines - - output = StringIO() - helper = pydoc.Helper(output=output) - helper('modules') - result = output.getvalue().strip() - num_lines = len(result.splitlines()) - - self.assertGreaterEqual(num_lines, expected) - - @unittest.skip('causes undesirable side-effects (#20128)') - def test_modules_search(self): - # See Helper.listmodules(). - expected = 'pydoc - ' - - output = StringIO() - helper = pydoc.Helper(output=output) - with captured_stdout() as help_io: - helper('modules pydoc') - result = help_io.getvalue() - - self.assertIn(expected, result) - - @unittest.skip('some buildbots are not cooperating (#20128)') - def test_modules_search_builtin(self): - expected = 'gc - ' - - output = StringIO() - helper = pydoc.Helper(output=output) - with captured_stdout() as help_io: - helper('modules garbage') - result = help_io.getvalue() - - self.assertStartsWith(result, expected) - def test_importfile(self): try: loaded_pydoc = pydoc.importfile(pydoc.__file__) @@ -1483,7 +1441,7 @@ def test_special_form(self): self.assertIn('NoReturn = typing.NoReturn', doc) self.assertIn(typing.NoReturn.__doc__.strip().splitlines()[0], doc) else: - self.assertIn('NoReturn = class _SpecialForm(_Final)', doc) + self.assertIn('NoReturn = class _SpecialForm(_Final, _NotIterable)', doc) def test_typing_pydoc(self): def foo(data: typing.List[typing.Any], @@ -2215,7 +2173,7 @@ def mock_getline(prompt): def test_keywords(self): self.assertEqual(sorted(pydoc.Helper.keywords), - sorted(keyword.kwlist + ['case', 'match'])) + sorted(keyword.kwlist + ['case', 'match', 'lazy'])) def test_interact_empty_line_continues(self): # gh-138568: test pressing Enter without input should continue in help session @@ -2414,6 +2372,32 @@ def test_sys_path_adjustment_when_curdir_already_included(self): trailing_argv0dir = trailing_curdir + [self.argv0dir] self.assertIsNone(self._get_revised_path(trailing_argv0dir)) + def test__get_version(self): + import json + import warnings + + class MyModule: + __name__ = 'my_module' + + @property + def __version__(self): + warnings._deprecated("__version__", remove=(3, 20)) + return "1.2.3" + + module = MyModule() + doc = pydoc.Doc() + with warnings.catch_warnings(record=True) as w: # TODO: remove in 3.20 + warnings.simplefilter("always") + version = doc._get_version(json) + self.assertEqual(version, "2.0.9") + self.assertEqual(len(w), 0) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + version = doc._get_version(module) + self.assertEqual(version, "1.2.3") + self.assertEqual(len(w), 1) + def setUpModule(): thread_info = threading_helper.threading_setup() From 7c0cc4e4d7fac9481547736cd981854db8df5ced Mon Sep 17 00:00:00 2001 From: youknowdot Date: Sun, 18 Oct 2026 21:47:31 +0900 Subject: [PATCH 11/12] Refresh remaining canonical test contracts and retire verified markers Align list helpers, specialization collection, and syntax expectations with pinned CPython 3.15. Preserve surviving doctest guards and retire only unconditional expected failures proven across all three CI platforms. Assisted-by: OpenAI Codex:model version unavailable --- Lib/test/list_tests.py | 4 +- Lib/test/test_builtin.py | 1 - Lib/test/test_cmd_line.py | 1 - Lib/test/test_exceptions.py | 1 - Lib/test/test_minidom.py | 2 - Lib/test/test_monitoring.py | 1 - Lib/test/test_opcache.py | 463 ++++++++++++++++++++++++++++---- Lib/test/test_pulldom.py | 3 - Lib/test/test_sax.py | 3 - Lib/test/test_str.py | 1 - Lib/test/test_syntax.py | 518 +++++++++++++++++++++++++++++++++--- Lib/test/test_traceback.py | 1 - Lib/test/test_xml_etree.py | 10 - 13 files changed, 897 insertions(+), 112 deletions(-) diff --git a/Lib/test/list_tests.py b/Lib/test/list_tests.py index ad9a9ea8303..ec2aa59f2cb 100644 --- a/Lib/test/list_tests.py +++ b/Lib/test/list_tests.py @@ -6,7 +6,7 @@ from functools import cmp_to_key from test import seq_tests -from test.support import ALWAYS_EQ, NEVER_EQ, run_with_limited_c_stack +from test.support import ALWAYS_EQ, NEVER_EQ, skip_if_huge_c_stack from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow @@ -60,7 +60,7 @@ def test_repr(self): self.assertEqual(str(a2), "[0, 1, 2, [...], 3]") self.assertEqual(repr(a2), "[0, 1, 2, [...], 3]") - @run_with_limited_c_stack(200_000) + @skip_if_huge_c_stack(200_000) @skip_wasi_stack_overflow() @skip_emscripten_stack_overflow() def test_repr_deep(self): diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 2a1a876660e..1ddf7910706 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -1171,7 +1171,6 @@ def four_freevars(): three_freevars.__globals__, closure=my_closure) - @unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: invalid decimal literal def test_exec_filter_syntax_warnings_by_module(self): filename = support.findfile('test_import/data/syntax_warnings.py') with open(filename, 'rb') as f: diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index c7716fb7191..4fe77bab3e6 100644 --- a/Lib/test/test_cmd_line.py +++ b/Lib/test/test_cmd_line.py @@ -1266,7 +1266,6 @@ def test_cpu_count_default(self): res = assert_python_ok('-c', code, PYTHON_CPU_COUNT='default') self.assertEqual(self.res2int(res), (os.cpu_count(), os.process_cpu_count())) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_import_time(self): # os is not imported at startup code = 'import os; import os' diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index bfa4653f9cb..17ef357c0e2 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2455,7 +2455,6 @@ class MySyntaxError(SyntaxError): ^^^^^ """, err.getvalue()) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_encodings(self): self.addCleanup(unlink, TESTFN) source = ( diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index 9bf83a8518c..47fa6c28d78 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -490,7 +490,6 @@ def testGetAttributeNode(self): self.assertIsNone(elem.getAttributeNode("b")) dom.unlink() - @unittest.expectedFailure # TODO: RUSTPYTHON def testGetElementsByTagNameNS(self): d=""" @@ -1516,7 +1515,6 @@ def testNodeListItem(self): "test NodeList.item()") doc.unlink() - @unittest.expectedFailure # TODO: RUSTPYTHON def testEncodings(self): doc = parseString('€') self.assertEqual(doc.toxml(), diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py index ad9b61a6576..97998e2d776 100644 --- a/Lib/test/test_monitoring.py +++ b/Lib/test/test_monitoring.py @@ -2333,7 +2333,6 @@ def test_attr(self): self.check_events(nonopt_func, recorders=self.RECORDERS, expected=nonopt_expected) self.check_events(opt_func, recorders=self.RECORDERS, expected=opt_expected) - @unittest.expectedFailure # TODO: RUSTPYTHON; line number differences in multi-line super() calls def test_vs_other_type_call(self): code_template = textwrap.dedent(""" class C: diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py index 2e50c1c62b3..7946550ec0d 100644 --- a/Lib/test/test_opcache.py +++ b/Lib/test/test_opcache.py @@ -1,3 +1,4 @@ +import collections import copy import pickle import dis @@ -5,7 +6,7 @@ import types import unittest from test.support import (threading_helper, check_impl_detail, - requires_specialization, requires_specialization_ft, + requires_specialization, cpython_only, requires_jit_disabled, reset_code) from test.support.import_helper import import_module @@ -524,7 +525,7 @@ def f(x, y): f() @requires_jit_disabled - @requires_specialization_ft + @requires_specialization def test_assign_init_code(self): class MyClass: def __init__(self): @@ -547,7 +548,7 @@ def count_args(self, *args): instantiate() @requires_jit_disabled - @requires_specialization_ft + @requires_specialization def test_push_init_frame_fails(self): def instantiate(): return InitTakesArg() @@ -575,6 +576,39 @@ def f(): for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): f() + @requires_jit_disabled + @requires_specialization + def test_specialize_call_function_ex_py(self): + def ex_py(*args, **kwargs): + return 1 + + def instantiate(): + args = (1, 2, 3) + kwargs = {} + return ex_py(*args, **kwargs) + + # Trigger specialization + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + instantiate() + self.assert_specialized(instantiate, "CALL_EX_PY") + + @requires_jit_disabled + @requires_specialization + def test_specialize_call_function_ex_py_fail(self): + def ex_py(*args, **kwargs): + return 1 + + def instantiate(): + args = (1, 2, 3) + kwargs = {} + return ex_py(*args, **kwargs) + + _testinternalcapi.set_vectorcall_nop(ex_py) + # Trigger specialization + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + instantiate() + self.assert_no_opcode(instantiate, "CALL_EX_PY") + self.assert_specialized(instantiate, "CALL_EX_NON_PY_GENERAL") def make_deferred_ref_count_obj(): """Create an object that uses deferred reference counting. @@ -590,7 +624,7 @@ def make_deferred_ref_count_obj(): class TestRacesDoNotCrash(TestBase): # Careful with these. Bigger numbers have a higher chance of catching bugs, # but you can also burn through a *ton* of type/dict/function versions: - ITEMS = 1000 + ITEMS = 1400 LOOPS = 4 WRITERS = 2 @@ -627,7 +661,7 @@ def assert_races_do_not_crash( for writer in writers: writer.join() - @requires_specialization_ft + @requires_specialization def test_binary_subscr_getitem(self): def get_items(): class C: @@ -657,7 +691,7 @@ def write(items): opname = "BINARY_OP_SUBSCR_GETITEM" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_binary_subscr_list_int(self): def get_items(): items = [] @@ -735,7 +769,7 @@ def write(items): opname = "FOR_ITER_LIST" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_load_attr_class(self): def get_items(): class C: @@ -765,7 +799,7 @@ def write(items): opname = "LOAD_ATTR_CLASS" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_load_attr_class_with_metaclass_check(self): def get_items(): class Meta(type): @@ -798,7 +832,7 @@ def write(items): opname = "LOAD_ATTR_CLASS_WITH_METACLASS_CHECK" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_load_attr_getattribute_overridden(self): def get_items(): class C: @@ -828,7 +862,7 @@ def write(items): opname = "LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_load_attr_instance_value(self): def get_items(): class C: @@ -852,7 +886,7 @@ def write(items): opname = "LOAD_ATTR_INSTANCE_VALUE" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_load_attr_method_lazy_dict(self): def get_items(): class C(Exception): @@ -882,7 +916,7 @@ def write(items): opname = "LOAD_ATTR_METHOD_LAZY_DICT" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_load_attr_method_no_dict(self): def get_items(): class C: @@ -913,7 +947,7 @@ def write(items): opname = "LOAD_ATTR_METHOD_NO_DICT" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_load_attr_method_with_values(self): def get_items(): class C: @@ -943,7 +977,7 @@ def write(items): opname = "LOAD_ATTR_METHOD_WITH_VALUES" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_load_attr_module(self): def get_items(): items = [] @@ -968,7 +1002,7 @@ def write(items): opname = "LOAD_ATTR_MODULE" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_load_attr_property(self): def get_items(): class C: @@ -998,7 +1032,7 @@ def write(items): opname = "LOAD_ATTR_PROPERTY" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_load_attr_slot(self): def get_items(): class C: @@ -1025,7 +1059,7 @@ def write(items): opname = "LOAD_ATTR_SLOT" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_load_attr_with_hint(self): def get_items(): class C: @@ -1052,7 +1086,7 @@ def write(items): opname = "LOAD_ATTR_WITH_HINT" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_load_global_module(self): if not have_dict_key_versions(): raise unittest.SkipTest("Low on dict key versions") @@ -1125,7 +1159,7 @@ def write(items): opname = "STORE_ATTR_WITH_HINT" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_store_subscr_list_int(self): def get_items(): items = [] @@ -1149,7 +1183,7 @@ def write(items): opname = "STORE_SUBSCR_LIST_INT" self.assert_races_do_not_crash(opname, get_items, read, write) - @requires_specialization_ft + @requires_specialization def test_unpack_sequence_list(self): def get_items(): items = [] @@ -1329,7 +1363,7 @@ def f(o, n): class TestSpecializer(TestBase): @cpython_only - @requires_specialization_ft + @requires_specialization def test_binary_op(self): def binary_op_add_int(): for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): @@ -1341,6 +1375,21 @@ def binary_op_add_int(): self.assert_specialized(binary_op_add_int, "BINARY_OP_ADD_INT") self.assert_no_opcode(binary_op_add_int, "BINARY_OP") + def binary_op_int_non_compact(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a, b = 10000000000, 1 + c = a + b + self.assertEqual(c, 10000000001) + c = a - b + self.assertEqual(c, 9999999999) + c = a * b + self.assertEqual(c, 10000000000) + + binary_op_int_non_compact() + self.assert_no_opcode(binary_op_int_non_compact, "BINARY_OP_ADD_INT") + self.assert_no_opcode(binary_op_int_non_compact, "BINARY_OP_SUBTRACT_INT") + self.assert_no_opcode(binary_op_int_non_compact, "BINARY_OP_MULTIPLY_INT") + def binary_op_add_unicode(): for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): a, b = "foo", "bar" @@ -1370,11 +1419,50 @@ def binary_op_add_extend(): self.assertEqual(c, 2.0) c = b / a self.assertEqual(c, 0.5) + c = a + c += b + self.assertEqual(c, 9.0) + c = b + c += a + self.assertEqual(c, 9.0) + c = a + c -= b + self.assertEqual(c, 3.0) + c = b + c -= a + self.assertEqual(c, -3.0) + c = a + c *= b + self.assertEqual(c, 18.0) + c = b + c *= a + self.assertEqual(c, 18.0) + c = a + c /= b + self.assertEqual(c, 2.0) + c = b + c /= a + self.assertEqual(c, 0.5) binary_op_add_extend() self.assert_specialized(binary_op_add_extend, "BINARY_OP_EXTEND") self.assert_no_opcode(binary_op_add_extend, "BINARY_OP") + def binary_op_add_extend_sequences(): + l1 = [1, 2] + l2 = [None] + t1 = (1, 2) + t2 = (None,) + for _ in range(100): + list_sum = l1 + l2 + self.assertEqual(list_sum, [1, 2, None]) + tuple_sum = t1 + t2 + self.assertEqual(tuple_sum, (1, 2, None)) + + binary_op_add_extend_sequences() + self.assert_specialized(binary_op_add_extend_sequences, "BINARY_OP_EXTEND") + self.assert_no_opcode(binary_op_add_extend_sequences, "BINARY_OP") + def binary_op_zero_division(): def compactlong_lhs(arg): 42 / arg @@ -1449,7 +1537,7 @@ def binary_op_bitwise_extend(): self.assert_no_opcode(binary_op_bitwise_extend, "BINARY_OP") @cpython_only - @requires_specialization_ft + @requires_specialization def test_load_super_attr(self): """Ensure that LOAD_SUPER_ATTR is specialized as expected.""" @@ -1488,7 +1576,7 @@ def init(self): self.assert_no_opcode(A.__init__, "LOAD_SUPER_ATTR_METHOD") @cpython_only - @requires_specialization_ft + @requires_specialization def test_contain_op(self): def contains_op_dict(): for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): @@ -1500,6 +1588,27 @@ def contains_op_dict(): self.assert_specialized(contains_op_dict, "CONTAINS_OP_DICT") self.assert_no_opcode(contains_op_dict, "CONTAINS_OP") + def contains_op_frozen_dict(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a, b = 1, frozendict({1: 2, 2: 5}) + self.assertTrue(a in b) + self.assertFalse(3 in b) + + contains_op_frozen_dict() + self.assert_specialized(contains_op_frozen_dict, "CONTAINS_OP_DICT") + self.assert_no_opcode(contains_op_frozen_dict, "CONTAINS_OP") + + def contains_op_frozen_dict_subclass(): + class MyFrozenDict(frozendict): + pass + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a, b = 1, MyFrozenDict({1: 2, 2: 5}) + self.assertTrue(a in b) + self.assertFalse(3 in b) + + contains_op_frozen_dict_subclass() + self.assert_no_opcode(contains_op_frozen_dict_subclass, "CONTAINS_OP_DICT") + def contains_op_set(): for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): a, b = 1, {1, 2} @@ -1511,7 +1620,7 @@ def contains_op_set(): self.assert_no_opcode(contains_op_set, "CONTAINS_OP") @cpython_only - @requires_specialization_ft + @requires_specialization def test_send_with(self): def run_async(coro): while True: @@ -1538,7 +1647,7 @@ async def send_with(): self.assert_specialized(send_with, "SEND_GEN") @cpython_only - @requires_specialization_ft + @requires_specialization def test_send_yield_from(self): def g(): yield None @@ -1553,7 +1662,38 @@ def send_yield_from(): self.assert_no_opcode(send_yield_from, "SEND") @cpython_only - @requires_specialization_ft + @requires_specialization + def test_send_yield_from_iter(self): + L = list(range(100)) + def send_yield_from(): + yield from L + + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + list(send_yield_from()) + + self.assert_specialized(send_yield_from, "SEND_VIRTUAL") + self.assert_no_opcode(send_yield_from, "SEND") + + @cpython_only + @requires_specialization + def test_send_async_for(self): + async def g(): + yield None + + async def send_for(): + async for _ in g(): + break + + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + try: + send_for().send(None) + except StopIteration: + pass + self.assert_specialized(send_for, "SEND_ASYNC_GEN") + self.assert_no_opcode(send_for, "SEND") + + @cpython_only + @requires_specialization def test_store_attr_slot(self): class C: __slots__ = ['x'] @@ -1574,7 +1714,7 @@ def set_slot(n): self.assert_no_opcode(set_slot, "STORE_ATTR_SLOT") @cpython_only - @requires_specialization_ft + @requires_specialization def test_store_attr_instance_value(self): class C: pass @@ -1596,7 +1736,7 @@ def set_value(n): self.assert_no_opcode(set_value, "STORE_ATTR_INSTANCE_VALUE") @cpython_only - @requires_specialization_ft + @requires_specialization def test_store_attr_with_hint(self): class C: pass @@ -1621,7 +1761,7 @@ def set_value(n): self.assert_no_opcode(set_value, "STORE_ATTR_WITH_HINT") @cpython_only - @requires_specialization_ft + @requires_specialization def test_to_bool(self): def to_bool_bool(): true_cnt, false_cnt = 0, 0 @@ -1689,7 +1829,7 @@ def to_bool_str(): self.assert_no_opcode(to_bool_str, "TO_BOOL") @cpython_only - @requires_specialization_ft + @requires_specialization def test_unpack_sequence(self): def unpack_sequence_two_tuple(): for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): @@ -1726,7 +1866,7 @@ def unpack_sequence_list(): self.assert_no_opcode(unpack_sequence_list, "UNPACK_SEQUENCE") @cpython_only - @requires_specialization_ft + @requires_specialization def test_binary_subscr(self): def binary_subscr_list_int(): for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): @@ -1760,6 +1900,63 @@ def binary_subscr_dict(): self.assert_specialized(binary_subscr_dict, "BINARY_OP_SUBSCR_DICT") self.assert_no_opcode(binary_subscr_dict, "BINARY_OP") + def binary_subscr_frozen_dict(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = frozendict({1: 2, 2: 3}) + self.assertEqual(a[1], 2) + self.assertEqual(a[2], 3) + + binary_subscr_frozen_dict() + self.assert_specialized(binary_subscr_frozen_dict, "BINARY_OP_SUBSCR_DICT") + self.assert_no_opcode(binary_subscr_frozen_dict, "BINARY_OP") + + def binary_subscr_frozen_dict_subclass(): + class MyFrozenDict(frozendict): + pass + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = MyFrozenDict({1: 2, 2: 3}) + self.assertEqual(a[1], 2) + self.assertEqual(a[2], 3) + + binary_subscr_frozen_dict_subclass() + self.assert_specialized(binary_subscr_frozen_dict_subclass, "BINARY_OP_SUBSCR_DICT") + self.assert_no_opcode(binary_subscr_frozen_dict_subclass, "BINARY_OP") + + def binary_subscr_defaultdict(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = collections.defaultdict(lambda: 42, {1: 2, 2: 3}) + self.assertEqual(a[1], 2) + self.assertEqual(a[2], 3) + self.assertEqual(a[7], 42) + + binary_subscr_defaultdict() + self.assert_specialized(binary_subscr_defaultdict, "BINARY_OP_SUBSCR_DICT") + self.assert_no_opcode(binary_subscr_defaultdict, "BINARY_OP") + + def binary_subscr_counter(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = collections.Counter('abcdeabcdabcaba') + self.assertEqual(a['a'], 5) + self.assertEqual(a['b'], 4) + self.assertEqual(a['m'], 0) + + binary_subscr_counter() + self.assert_specialized(binary_subscr_counter, "BINARY_OP_SUBSCR_DICT") + self.assert_no_opcode(binary_subscr_counter, "BINARY_OP") + + def binary_subscr_dict_subclass_override(): + class MyDict(dict): + def __getitem__(self, key): + return 42 + + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = MyDict() + self.assertEqual(a['a'], 42) + self.assertEqual(a['b'], 42) + + binary_subscr_dict_subclass_override() + self.assert_no_opcode(binary_subscr_dict_subclass_override, "BINARY_OP_SUBSCR_DICT") + def binary_subscr_str_int(): for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): a = "foobar" @@ -1770,6 +1967,16 @@ def binary_subscr_str_int(): self.assert_specialized(binary_subscr_str_int, "BINARY_OP_SUBSCR_STR_INT") self.assert_no_opcode(binary_subscr_str_int, "BINARY_OP") + def binary_subscr_str_int_non_compact(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = "바이트코드_특수화" + for idx, expected in enumerate(a): + self.assertEqual(a[idx], expected) + + binary_subscr_str_int_non_compact() + self.assert_specialized(binary_subscr_str_int_non_compact, "BINARY_OP_SUBSCR_USTR_INT") + self.assert_no_opcode(binary_subscr_str_int_non_compact, "BINARY_OP_SUBSCR_STR_INT") + def binary_subscr_getitems(): class C: def __init__(self, val): @@ -1786,7 +1993,54 @@ def __getitem__(self, item): self.assert_no_opcode(binary_subscr_getitems, "BINARY_OP") @cpython_only - @requires_specialization_ft + @requires_specialization + def test_store_subscr(self): + def store_subscr_dict(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = {1: 2, 2: 3} + a[1] = 4 + self.assertEqual(a[1], 4) + + store_subscr_dict() + self.assert_specialized(store_subscr_dict, "STORE_SUBSCR_DICT") + self.assert_no_opcode(store_subscr_dict, "STORE_SUBSCR") + + def store_subscr_frozen_dict(): + dicts = [{1: 2}, frozendict({1: 2})] + for i in range(_testinternalcapi.SPECIALIZATION_THRESHOLD + 1): + d = dicts[i == _testinternalcapi.SPECIALIZATION_THRESHOLD] + d[1] = 3 + + with self.assertRaises(TypeError): + store_subscr_frozen_dict() + self.assert_specialized(store_subscr_frozen_dict, "STORE_SUBSCR_DICT") + self.assert_no_opcode(store_subscr_frozen_dict, "STORE_SUBSCR") + + def store_subscr_defaultdict(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = collections.defaultdict(int) + a[1] = 4 + self.assertEqual(a[1], 4) + + store_subscr_defaultdict() + self.assert_specialized(store_subscr_defaultdict, "STORE_SUBSCR_DICT") + self.assert_no_opcode(store_subscr_defaultdict, "STORE_SUBSCR") + + def store_subscr_dict_subclass_override(): + class MyDict(dict): + def __setitem__(self, key, value): + super().__setitem__(key, value * 2) + + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = MyDict() + a['x'] = 5 + self.assertEqual(a['x'], 10) + + store_subscr_dict_subclass_override() + self.assert_no_opcode(store_subscr_dict_subclass_override, "STORE_SUBSCR_DICT") + + @cpython_only + @requires_specialization def test_compare_op(self): def compare_op_int(): for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): @@ -1818,23 +2072,9 @@ def compare_op_str(): self.assert_specialized(compare_op_str, "COMPARE_OP_STR") self.assert_no_opcode(compare_op_str, "COMPARE_OP") - @cpython_only - @requires_specialization_ft - def test_load_const(self): - def load_const(): - def unused(): pass - # Currently, the empty tuple is immortal, and the otherwise - # unused nested function's code object is mortal. This test will - # have to use different values if either of that changes. - return () - - load_const() - self.assert_specialized(load_const, "LOAD_CONST_IMMORTAL") - self.assert_specialized(load_const, "LOAD_CONST_MORTAL") - self.assert_no_opcode(load_const, "LOAD_CONST") @cpython_only - @requires_specialization_ft + @requires_specialization def test_for_iter(self): L = list(range(10)) def for_iter_list(): @@ -1854,6 +2094,15 @@ def for_iter_tuple(): self.assert_specialized(for_iter_tuple, "FOR_ITER_TUPLE") self.assert_no_opcode(for_iter_tuple, "FOR_ITER") + s = "abcdefghij" + def for_iter_str(): + for i in s: + self.assertIn(i, s) + + for_iter_str() + self.assert_specialized(for_iter_str, "FOR_ITER_VIRTUAL") + self.assert_no_opcode(for_iter_str, "FOR_ITER") + r = range(10) def for_iter_range(): for i in r: @@ -1871,6 +2120,126 @@ def for_iter_generator(): self.assert_specialized(for_iter_generator, "FOR_ITER_GEN") self.assert_no_opcode(for_iter_generator, "FOR_ITER") + @cpython_only + @requires_specialization + def test_get_iter(self): + L = list(range(10)) + def get_iter_list(): + n = 10 + while n: + n -= 1 + for i in L: + break + get_iter_list() + self.assert_specialized(get_iter_list, "GET_ITER_VIRTUAL") + self.assert_no_opcode(get_iter_list, "GET_ITER") + + def get_iter_gen(): + n = 10 + while n: + n -= 1 + for i in (i for i in range(10)): + break + get_iter_gen() + self.assert_specialized(get_iter_gen, "GET_ITER_SELF") + self.assert_no_opcode(get_iter_gen, "GET_ITER") + + @cpython_only + @requires_specialization + def test_call_list_append(self): + # gh-141367: only exact lists should use + # CALL_LIST_APPEND instruction after specialization. + + r = range(_testinternalcapi.SPECIALIZATION_THRESHOLD) + + def list_append(l): + for _ in r: + l.append(1) + + list_append([]) + self.assert_specialized(list_append, "CALL_LIST_APPEND") + self.assert_no_opcode(list_append, "CALL_METHOD_DESCRIPTOR_O") + self.assert_no_opcode(list_append, "CALL") + + def my_list_append(l): + for _ in r: + l.append(1) + + class MyList(list): pass + my_list_append(MyList()) + self.assert_specialized(my_list_append, "CALL_METHOD_DESCRIPTOR_O") + self.assert_no_opcode(my_list_append, "CALL_LIST_APPEND") + self.assert_no_opcode(my_list_append, "CALL") + + @cpython_only + @requires_specialization + def test_load_attr_module_with_getattr(self): + module = types.ModuleType("test_module_with_getattr") + module.__dict__["some_attr"] = "foo" + + def module_getattr(name): + if name == "missing_attr": + return 42 + raise AttributeError(f"module has no attribute {name}") + + module.__dict__["__getattr__"] = module_getattr + + import sys + sys.modules.pop("test_module_with_getattr", None) + sys.modules["test_module_with_getattr"] = module + try: + def load_module_attr_present(): + import test_module_with_getattr + return test_module_with_getattr.some_attr + + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + self.assertEqual(load_module_attr_present(), "foo") + + self.assert_specialized(load_module_attr_present, "LOAD_ATTR_MODULE") + self.assert_no_opcode(load_module_attr_present, "LOAD_ATTR") + + def load_module_attr_missing(): + import test_module_with_getattr + return test_module_with_getattr.missing_attr + + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + self.assertEqual(load_module_attr_missing(), 42) + + self.assert_no_opcode(load_module_attr_missing, "LOAD_ATTR_MODULE") + finally: + sys.modules.pop("test_module_with_getattr", None) + + @cpython_only + @requires_specialization + def test_specialized_iter_doesnt_skip_send_check(self): + def gen_func(seq): + yield from seq + gen = gen_func(list(range(10))) + for _ in range(3): + gen.send(None) + with self.assertRaises(AttributeError): + gen.send(1) + + + @cpython_only + @requires_specialization + def test_load_attr_enum(self): + import enum + + class Color(enum.IntEnum): + RED = 1 + GREEN = 2 + BLUE = 3 + + def load_enum_member(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + x = Color.RED + assert x == 1 + + load_enum_member() + self.assert_specialized(load_enum_member, + "LOAD_ATTR_CLASS_WITH_METACLASS_CHECK") + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_pulldom.py b/Lib/test/test_pulldom.py index 435b52d33eb..3c8ed251aca 100644 --- a/Lib/test/test_pulldom.py +++ b/Lib/test/test_pulldom.py @@ -40,7 +40,6 @@ def test_parse(self): with open(tstfile, "rb") as fin: list(pulldom.parse(fin)) - @unittest.expectedFailure # TODO: RUSTPYTHON; implement DOM semantic def test_parse_semantics(self): """Test DOMEventStream parsing semantics.""" @@ -103,7 +102,6 @@ def test_parse_semantics(self): #evt, node = next(items) #self.assertEqual(pulldom.END_DOCUMENT, evt) - @unittest.expectedFailure # TODO: RUSTPYTHON; pulldom.parseString(SMALL_SAMPLE) return iterator with tuple with 2 elements def test_expandItem(self): """Ensure expandItem works as expected.""" items = pulldom.parseString(SMALL_SAMPLE) @@ -300,7 +298,6 @@ class SAX2DOMTestCase(unittest.TestCase): def confirm(self, test, testname="Test"): self.assertTrue(test, testname) - @unittest.expectedFailure # TODO: RUSTPYTHON; read from stream io def test_basic(self): """Ensure SAX2DOM can parse from a stream.""" with io.StringIO(SMALL_SAMPLE) as fin: diff --git a/Lib/test/test_sax.py b/Lib/test/test_sax.py index 92190e7a538..19ed6f349aa 100644 --- a/Lib/test/test_sax.py +++ b/Lib/test/test_sax.py @@ -715,7 +715,6 @@ def test_1463026_3_empty(self): self.assertEqual(result.getvalue(), self.xml('')) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing' def test_5027_1(self): # The xml prefix (as in xml:lang below) is reserved and bound by # definition to http://www.w3.org/XML/1998/namespace. XMLGenerator had @@ -999,7 +998,6 @@ def test_expat_dtdhandler(self): [("GIF", "-//CompuServe//NOTATION Graphics Interchange Format 89a//EN", None)]) self.assertEqual(handler._entities, [("img", None, "expat.gif", "GIF")]) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing' def test_expat_external_dtd_enabled(self): # clear _opener global variable self.addCleanup(urllib.request.urlcleanup) @@ -1111,7 +1109,6 @@ def test_expat_nsattrs_empty(self): self.verify_empty_nsattrs(gather._attrs) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing' def test_expat_nsattrs_wattr(self): parser = create_parser(1) gather = self.AttrGatherer() diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py index 28f4e475fcc..837e0185321 100644 --- a/Lib/test/test_str.py +++ b/Lib/test/test_str.py @@ -1521,7 +1521,6 @@ def __format__(self, spec): self.assertEqual('{:{f}}{g}{}'.format(1, 3, g='g', f=2), ' 1g3') self.assertEqual('{f:{}}{}{g}'.format(2, 4, f=1, g='g'), ' 14g') - @unittest.expectedFailure # TODO: RUSTPYTHON; %c error wording uses bare class name; CPython uses fully qualified module.qualname (e.g. test.test_str.PseudoFloat) def test_formatting(self): string_tests.StringLikeTest.test_formatting(self) # Testing Unicode formatting strings... diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 51a4a8155f5..d90052720f7 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -370,18 +370,48 @@ Traceback (most recent call last): SyntaxError: invalid syntax ->>> match ...: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE -... case {**rest, "key": value}: -... ... -Traceback (most recent call last): -SyntaxError: invalid syntax - >>> match ...: ... case {**_}: ... ... Traceback (most recent call last): SyntaxError: invalid syntax +# Check incorrect "case" placement with specialized error messages + +>>> case "pattern": ... +Traceback (most recent call last): +SyntaxError: case statement must be inside match statement + +>>> case 1 | 2: ... +Traceback (most recent call last): +SyntaxError: case statement must be inside match statement + +>>> case klass(attr=1) | {}: ... +Traceback (most recent call last): +SyntaxError: case statement must be inside match statement + +>>> case [] if x > 1: ... +Traceback (most recent call last): +SyntaxError: case statement must be inside match statement + +>>> case match: ... +Traceback (most recent call last): +SyntaxError: case statement must be inside match statement + +>>> case case: ... +Traceback (most recent call last): +SyntaxError: case statement must be inside match statement + +>>> if some: +... case 1: ... +Traceback (most recent call last): +SyntaxError: case statement must be inside match statement + +>>> case some: +... case 1: ... +Traceback (most recent call last): +SyntaxError: case statement must be inside match statement + # But prefixes of soft keywords should # still raise specialized errors @@ -426,7 +456,7 @@ >>> def foo(/,a,b=,c): ... pass Traceback (most recent call last): -SyntaxError: at least one argument must precede / +SyntaxError: at least one parameter must precede / >>> def foo(a,/,/,b,c): ... pass @@ -461,67 +491,67 @@ >>> def foo(a,*b=3,c): ... pass Traceback (most recent call last): -SyntaxError: var-positional argument cannot have default value +SyntaxError: var-positional parameter cannot have default value >>> def foo(a,*b: int=,c): ... pass Traceback (most recent call last): -SyntaxError: var-positional argument cannot have default value +SyntaxError: var-positional parameter cannot have default value >>> def foo(a,**b=3): ... pass Traceback (most recent call last): -SyntaxError: var-keyword argument cannot have default value +SyntaxError: var-keyword parameter cannot have default value >>> def foo(a,**b: int=3): ... pass Traceback (most recent call last): -SyntaxError: var-keyword argument cannot have default value +SyntaxError: var-keyword parameter cannot have default value >>> def foo(a,*a, b, **c, d): ... pass Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> def foo(a,*a, b, **c, d=4): ... pass Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> def foo(a,*a, b, **c, *d): ... pass Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> def foo(a,*a, b, **c, **d): ... pass Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> def foo(a=1,/,**b,/,c): ... pass Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> def foo(*b,*d): ... pass Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> def foo(a,*b,c,*d,*e,c): ... pass Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> def foo(a,b,/,c,*b,c,*d,*e,c): ... pass Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> def foo(a,b,/,c,*b,c,*d,**e): ... pass Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> def foo(a=1,/*,b,c): ... pass @@ -545,7 +575,7 @@ >>> lambda /,a,b,c: None Traceback (most recent call last): -SyntaxError: at least one argument must precede / +SyntaxError: at least one parameter must precede / >>> lambda a,/,/,b,c: None Traceback (most recent call last): @@ -577,47 +607,47 @@ >>> lambda a,*b=3,c: None Traceback (most recent call last): -SyntaxError: var-positional argument cannot have default value +SyntaxError: var-positional parameter cannot have default value >>> lambda a,**b=3: None Traceback (most recent call last): -SyntaxError: var-keyword argument cannot have default value +SyntaxError: var-keyword parameter cannot have default value >>> lambda a, *a, b, **c, d: None Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> lambda a,*a, b, **c, d=4: None Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> lambda a,*a, b, **c, *d: None Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> lambda a,*a, b, **c, **d: None Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> lambda a=1,/,**b,/,c: None Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> lambda *b,*d: None Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> lambda a,*b,c,*d,*e,c: None Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> lambda a,b,/,c,*b,c,*d,*e,c: None Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> lambda a,b,/,c,*b,c,*d,**e: None Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> lambda a=1,d=,c: None Traceback (most recent call last): @@ -860,7 +890,7 @@ >>> __debug__ += 1 Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> f() += 1 # TODO: RUSTPYTHON; Raises an exception # doctest: +SKIP +>>> f() += 1 Traceback (most recent call last): SyntaxError: 'function call' is an illegal expression for augmented assignment @@ -1311,7 +1341,7 @@ Traceback (most recent call last): SyntaxError: expected '(' -Parenthesized arguments in function definitions +Parenthesized parameters in function definitions >>> def f(x, (y, z), w): ... pass @@ -1443,17 +1473,17 @@ >>> try: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE ... except TypeError as name: raise from None Traceback (most recent call last): - SyntaxError: invalid syntax + SyntaxError: did you forget an expression between 'raise' and 'from'? >>> try: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE ... except* TypeError as name: raise from None Traceback (most recent call last): - SyntaxError: invalid syntax + SyntaxError: did you forget an expression between 'raise' and 'from'? >>> match 1: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE ... case 1 | 2 as abc: raise from None Traceback (most recent call last): - SyntaxError: invalid syntax + SyntaxError: did you forget an expression between 'raise' and 'from'? Ensure that early = are not matched by the parser as invalid comparisons >>> f(2, 4, x=34); 1 $ 2 @@ -1702,6 +1732,28 @@ ... SyntaxError: invalid syntax +Better errors for `raise` statement: + + >>> raise ValueError from + Traceback (most recent call last): + SyntaxError: did you forget an expression after 'from'? + + >>> raise mod.ValueError() from + Traceback (most recent call last): + SyntaxError: did you forget an expression after 'from'? + + >>> raise from exc + Traceback (most recent call last): + SyntaxError: did you forget an expression between 'raise' and 'from'? + + >>> raise from None + Traceback (most recent call last): + SyntaxError: did you forget an expression between 'raise' and 'from'? + + >>> raise from + Traceback (most recent call last): + SyntaxError: did you forget an expression between 'raise' and 'from'? + Check that an multiple exception types with missing parentheses raise a custom exception only when using 'as' @@ -2009,6 +2061,30 @@ Traceback (most recent call last): SyntaxError: trailing comma not allowed without surrounding parentheses +>>> with item,: pass +Traceback (most recent call last): +SyntaxError: the last 'with' item has a trailing comma + +>>> with item as x,: pass +Traceback (most recent call last): +SyntaxError: the last 'with' item has a trailing comma + +>>> with item1, item2,: pass +Traceback (most recent call last): +SyntaxError: the last 'with' item has a trailing comma + +>>> with item1 as x, item2,: pass +Traceback (most recent call last): +SyntaxError: the last 'with' item has a trailing comma + +>>> with item1 as x, item2 as y,: pass +Traceback (most recent call last): +SyntaxError: the last 'with' item has a trailing comma + +>>> with item1, item2 as y,: pass +Traceback (most recent call last): +SyntaxError: the last 'with' item has a trailing comma + >>> import a from b Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? @@ -2162,6 +2238,16 @@ Traceback (most recent call last): SyntaxError: only single target (not list) can be annotated +>>> 1: int +Traceback (most recent call last): +SyntaxError: illegal target for annotation +>>> -x: int = 1 +Traceback (most recent call last): +SyntaxError: illegal target for annotation +>>> (x for x in y): int +Traceback (most recent call last): +SyntaxError: illegal target for annotation + # 'not' after operators: >>> 3 + not 3 @@ -2221,7 +2307,7 @@ >>> with (lambda *:0): pass Traceback (most recent call last): - SyntaxError: named arguments must follow bare * + SyntaxError: named parameters must follow bare * Corner-cases that used to crash: @@ -2237,7 +2323,7 @@ Traceback (most recent call last): SyntaxError: invalid character '£' (U+00A3) - Invalid pattern matching constructs: +Invalid pattern matching constructs: >>> match ...: ... case 42 as _: @@ -2293,12 +2379,36 @@ Traceback (most recent call last): SyntaxError: positional patterns follow keyword patterns + >>> match ...: + ... case Foo(y=1, x=2, y=3): + ... ... + Traceback (most recent call last): + SyntaxError: attribute name repeated in class pattern: y + >>> match ...: ... case C(a=b, c, d=e, f, g=h, i, j=k, ...): ... ... Traceback (most recent call last): SyntaxError: positional patterns follow keyword patterns + >>> match ...: + ... case {**double_star, "spam": "eggs"}: + ... ... + Traceback (most recent call last): + SyntaxError: double star pattern must be the last (right-most) subpattern in the mapping pattern + + >>> match ...: + ... case {"foo": 1, **double_star, "spam": "eggs"}: + ... ... + Traceback (most recent call last): + SyntaxError: double star pattern must be the last (right-most) subpattern in the mapping pattern + + >>> match ...: + ... case {"spam": "eggs", "b": {**d, "ham": "bacon"}}: + ... ... + Traceback (most recent call last): + SyntaxError: double star pattern must be the last (right-most) subpattern in the mapping pattern + Uses of the star operator which should fail: A[:*b] @@ -2683,6 +2793,76 @@ def f(x: *b) >>> f(x = 5, *:) Traceback (most recent call last): SyntaxError: Invalid star expression + +Asserts: + + >>> assert (a := 1) # ok + >>> assert 1, (a := 1) # ok + + >>> assert a := 1 + Traceback (most recent call last): + SyntaxError: cannot use named expression without parentheses here + + >>> assert 1, a := 1 + Traceback (most recent call last): + SyntaxError: cannot use named expression without parentheses here + + >>> assert 1 = 2 = 3 + Traceback (most recent call last): + SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='? + + >>> assert 1 = 2 + Traceback (most recent call last): + SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='? + + >>> assert (1 = 2) + Traceback (most recent call last): + SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='? + + >>> assert 'a' = a + Traceback (most recent call last): + SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='? + + >>> assert x[0] = 1 + Traceback (most recent call last): + SyntaxError: cannot assign to subscript here. Maybe you meant '==' instead of '='? + + >>> assert (yield a) = 2 + Traceback (most recent call last): + SyntaxError: cannot assign to yield expression here. Maybe you meant '==' instead of '='? + + >>> assert a = 2 + Traceback (most recent call last): + SyntaxError: cannot assign to name here. Maybe you meant '==' instead of '='? + + >>> assert (a = 2) + Traceback (most recent call last): + SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? + + >>> assert a = b + Traceback (most recent call last): + SyntaxError: cannot assign to name here. Maybe you meant '==' instead of '='? + + >>> assert 1, 1 = b + Traceback (most recent call last): + SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='? + + >>> assert 1, (1 = b) + Traceback (most recent call last): + SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='? + + >>> assert 1, a = 1 + Traceback (most recent call last): + SyntaxError: cannot assign to name here. Maybe you meant '==' instead of '='? + + >>> assert 1, (a = 1) + Traceback (most recent call last): + SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? + + >>> assert 1 = a, a = 1 + Traceback (most recent call last): + SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='? + """ import re @@ -2702,6 +2882,14 @@ def check_warning(self, code, errtext, filename="", mode="exec"): with self.assertWarnsRegex(SyntaxWarning, errtext): compile(code, filename, mode) + def check_no_warning(self, code, filename="", mode="exec"): + """Check that compiling code does not raise any warnings.""" + import warnings + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + compile(code, filename, mode) + self.assertEqual(caught, []) + def test_return_in_finally(self): source = textwrap.dedent(""" def f(): @@ -2772,6 +2960,75 @@ def test_break_and_continue_in_finally(self): """) self.check_warning(source, f"'{kw}' in a 'finally' block") + def test_from_lazy_imports(self): + # gh-150459 + self.check_warning( + "from . lazy import x", + "did you mean 'lazy from . import'?", + ) + self.check_warning( + "from . lazy import x as y", + "did you mean 'lazy from . import'?", + ) + self.check_warning( + "from . lazy import *", + "did you mean 'lazy from . import'?", + ) + self.check_warning( + "from .. lazy import x", + "did you mean 'lazy from .. import'?", + ) + self.check_warning( + "from ... lazy import x", + "did you mean 'lazy from ... import'?", + ) + self.check_warning( + "from .... lazy import x", + "did you mean 'lazy from .... import'?", + ) + self.check_warning( + "from . \\\n lazy import x", + "did you mean 'lazy from . import'?", + ) + self.check_warning( + "from .\\\nlazy import x", + "did you mean 'lazy from . import'?", + ) + self.check_warning( + "from .\tlazy import x", + "did you mean 'lazy from . import'?", + ) + + def test_not_from_lazy_imports(self): + self.check_no_warning("from .lazy import x") + self.check_no_warning("from .lazy import *") + self.check_no_warning("from ..lazy import x") + self.check_no_warning("from ...lazy import x") + self.check_no_warning("from .lazy.sub import x") + self.check_no_warning("from ..lazy.sub import x") + self.check_no_warning("from ...lazy.sub import x") + self.check_no_warning("from . lazier import x") + self.check_no_warning("from . lazy_module import x") + self.check_no_warning("from . lazy.sub import x") + self.check_no_warning("from . sub.lazy import x") + self.check_no_warning("from lazy import x") + self.check_no_warning("from lazy.sub import x") + self.check_no_warning("lazy from . lazy import x") + self.check_no_warning("from . import lazy") + + def test_from_lazy_imports_as_error(self): + import warnings + with warnings.catch_warnings(): + warnings.simplefilter("error", SyntaxWarning) + with self.assertRaisesRegex( + SyntaxError, + re.escape("did you mean 'lazy from . import'?"), + ) as cm: + compile("from . lazy import x", "", "exec") + self.assertEqual(cm.exception.lineno, 1) + self.assertEqual(cm.exception.offset, 8) + self.assertEqual(cm.exception.end_offset, 12) + class SyntaxErrorTestCase(unittest.TestCase): @@ -2869,6 +3126,13 @@ def error2(): """ self._check_error(source, "parameter and nonlocal", lineno=3) + def test_raise_from_error_message(self): + source = """if 1: + raise AssertionError() from None + print(1,,2) + """ + self._check_error(source, "invalid syntax", lineno=3) + def test_yield_outside_function(self): self._check_error("if 0: yield", "outside function") self._check_error("if 0: yield\nelse: x=1", "outside function") @@ -3189,6 +3453,20 @@ def test_multiline_compiler_error_points_to_the_end(self): lineno=3 ) + def test_multiline_string_concat_missing_comma_points_to_last_string(self): + # gh-142236: For multi-line string concatenations with a missing comma, + # the error should point to the last string, not the first. + self._check_error( + "print(\n" + ' "line1"\n' + ' "line2"\n' + ' "line3"\n' + " x=1\n" + ")", + "Perhaps you forgot a comma", + lineno=4, # Points to "line3", the last string + ) + @support.cpython_only def test_syntax_error_on_deeply_nested_blocks(self): # This raises a SyntaxError, it used to raise a SystemError. Context @@ -3314,6 +3592,168 @@ def test_ifexp_body_stmt_else_stmt(self): ]: self._check_error(f"x = {lhs_stmt} if 1 else {rhs_stmt}", msg) + def test_diamond_operator(self): + self._check_error( + "1<>2", + r"Maybe you meant '!=' instead of '<>'\?", + lineno=1, + end_lineno=1, + offset=2, + end_offset=4, + ) + + def test_diamond_operator_barry_as_flufl(self): + compile( + "from __future__ import barry_as_FLUFL\n1<>2", + "", "exec", + ) + self._check_error( + "from __future__ import barry_as_FLUFL\na != b", + "with Barry as BDFL, use '<>' instead of '!='", + lineno=2, + end_lineno=2, + offset=3, + end_offset=5, + ) + + +class LazyImportRestrictionTestCase(SyntaxErrorTestCase): + """Test syntax restrictions for lazy imports.""" + + def test_lazy_import_in_try_block(self): + """Test that lazy imports are not allowed inside try blocks.""" + self._check_error("""\ +try: + lazy import os +except: + pass +""", "lazy import not allowed inside try/except blocks") + + self._check_error("""\ +try: + lazy from sys import path +except ImportError: + pass +""", "lazy from ... import not allowed inside try/except blocks") + + def test_lazy_import_in_trystar_block(self): + """Test that lazy imports are not allowed inside try* blocks.""" + self._check_error("""\ +try: + lazy import json +except* Exception: + pass +""", "lazy import not allowed inside try/except blocks") + + self._check_error("""\ +try: + lazy from collections import defaultdict +except* ImportError: + pass +""", "lazy from ... import not allowed inside try/except blocks") + + def test_lazy_import_in_except_block(self): + """Test that lazy imports are not allowed inside except blocks.""" + self._check_error("""\ +try: + sys.modules # trigger the except block +except* Exception: + lazy import sys +""", "lazy import not allowed inside try/except blocks") + + def test_lazy_import_in_function(self): + """Test that lazy imports are not allowed inside functions.""" + self._check_error("""\ +def func(): + lazy import math +""", "lazy import not allowed inside functions") + + self._check_error("""\ +def func(): + lazy from datetime import datetime +""", "lazy from ... import not allowed inside functions") + + def test_lazy_import_in_async_function(self): + """Test that lazy imports are not allowed inside async functions.""" + self._check_error("""\ +async def async_func(): + lazy import asyncio +""", "lazy import not allowed inside functions") + + self._check_error("""\ +async def async_func(): + lazy from json import loads +""", "lazy from ... import not allowed inside functions") + + def test_lazy_import_in_class(self): + """Test that lazy imports are not allowed inside classes.""" + self._check_error("""\ +class MyClass: + lazy import typing +""", "lazy import not allowed inside classes") + + self._check_error("""\ +class MyClass: + lazy from abc import ABC +""", "lazy from ... import not allowed inside classes") + + def test_lazy_import_star_forbidden(self): + """Test that 'lazy from ... import *' is forbidden everywhere.""" + # At module level should also be forbidden + self._check_error("lazy from os import *", + "lazy from ... import \\* is not allowed") + + # Inside function should give lazy function error first + self._check_error("""\ +def func(): + lazy from sys import * +""", "lazy from ... import not allowed inside functions") + + def test_lazy_import_nested_scopes(self): + """Test lazy imports in nested scopes.""" + self._check_error("""\ +class Outer: + def method(self): + lazy import sys +""", "lazy import not allowed inside functions") + + self._check_error("""\ +def outer(): + class Inner: + lazy import json +""", "lazy import not allowed inside classes") + + self._check_error("""\ +def outer(): + def inner(): + lazy from collections import deque +""", "lazy from ... import not allowed inside functions") + + self._check_error("""\ +from os lazy import path +""", "use 'lazy from ... ' instead of 'from ... lazy import'") + self._check_error("""\ +from os.path lazy import join +""", "use 'lazy from ... ' instead of 'from ... lazy import'") + self._check_error("""\ +from .mod lazy import join +""", "use 'lazy from ... ' instead of 'from ... lazy import'") + self._check_error("""\ +from ..mod lazy import join +""", "use 'lazy from ... ' instead of 'from ... lazy import'") + self._check_error("""\ +from ...mod lazy import join +""", "use 'lazy from ... ' instead of 'from ... lazy import'") + + def test_lazy_import_valid_cases(self): + """Test that lazy imports work at module level.""" + # These should compile without errors + compile("lazy import os", "", "exec") + compile("lazy from sys import path", "", "exec") + compile("lazy import json as j", "", "exec") + compile("lazy from datetime import datetime as dt", "", "exec") + + def load_tests(loader, tests, pattern): from test.support.rustpython import DocTestChecker # TODO: RUSTPYTHON tests.addTest(doctest.DocTestSuite(checker=DocTestChecker())) # TODO: RUSTPYTHON diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index fa5f1e30355..e3e2bd29d92 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -5205,7 +5205,6 @@ def CHECK(a, b, expected): CHECK("AttributeError", "AttributeErrorTests", 10) CHECK("ABA", "AAB", 4) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: crates/pylib/Lib/test/levenshtein_examples.json is missing. Run `make regen-test-levenshtein` @support.requires_resource('cpu') def test_levenshtein_distance_short_circuit(self): if not LEVENSHTEIN_DATA_FILE.is_file(): diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 1baa0dcf634..b90ad943f0b 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -625,7 +625,6 @@ def test_writefile(self): elem[0] = ET.PI("key", "value") self.serialize_check(elem, 'textsubtext') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_custom_builder(self): # Test parser w. custom builder. @@ -1487,7 +1486,6 @@ def test_unknown_events(self): del cm gc_collect() - @unittest.expectedFailure # TODO: RUSTPYTHON def test_non_utf8(self): source = io.BytesIO( b"\n" @@ -1702,11 +1700,9 @@ def test_simple_xml(self, chunk_size=None, flush=False): self.assert_event_tags(parser, [('end', 'root')]) self.assertIsNone(parser.close()) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_simple_xml_chunk_1(self): self.test_simple_xml(chunk_size=1, flush=True) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_simple_xml_chunk_5(self): self.test_simple_xml(chunk_size=5, flush=True) @@ -1746,7 +1742,6 @@ def test_simple_xml_with_ns(self): self.assert_event_tags(parser, [('end', '{namespace}root')]) self.assertIsNone(parser.close()) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_ns_events(self): parser = ET.XMLPullParser(events=('start-ns', 'end-ns')) self._feed(parser, "\n") @@ -1762,7 +1757,6 @@ def test_ns_events(self): self.assertEqual(list(parser.read_events()), [('end-ns', None)]) self.assertIsNone(parser.close()) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_ns_events_start(self): parser = ET.XMLPullParser(events=('start-ns', 'start', 'end')) self._feed(parser, "\n") @@ -1786,7 +1780,6 @@ def test_ns_events_start(self): ('end', '{abc}tag'), ]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_ns_events_start_end(self): parser = ET.XMLPullParser(events=('start-ns', 'start', 'end', 'end-ns')) self._feed(parser, "\n") @@ -2615,7 +2608,6 @@ def test_expat224_utf8_bug(self): text = b'x' + b'\xc3\xa0' * 1024 self.check_expat224_utf8_bug(text) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_expat224_utf8_bug_file(self): with open(UTF8_BUG_XMLFILE, 'rb') as fp: raw = fp.read() @@ -4719,7 +4711,6 @@ class C14NTest(unittest.TestCase): # # simple roundtrip tests (from c14n.py) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_simple_roundtrip(self): # Basics self.assertEqual(c14n_roundtrip(""), '') @@ -4760,7 +4751,6 @@ def test_simple_roundtrip(self): xml = '' self.assertEqual(c14n_roundtrip(xml), xml) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_c14n_exclusion(self): xml = textwrap.dedent("""\ From 7735157610a221d5c2be9ed44c66a04a632cd44f Mon Sep 17 00:00:00 2001 From: youknowdot Date: Sun, 18 Oct 2026 22:00:08 +0900 Subject: [PATCH 12/12] Retire passing canonical syntax doctest guards Remove only the three stale raise-from diagnostic guards confirmed against the pinned CPython suite and the current mega runtime. Keep all six remaining doctest guards. Assisted-by: OpenAI Codex:model version unavailable --- Lib/test/test_syntax.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index d90052720f7..65d076ce6a0 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -1470,17 +1470,17 @@ Regression tests for gh-133999: - >>> try: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: pass ... except TypeError as name: raise from None Traceback (most recent call last): SyntaxError: did you forget an expression between 'raise' and 'from'? - >>> try: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: pass ... except* TypeError as name: raise from None Traceback (most recent call last): SyntaxError: did you forget an expression between 'raise' and 'from'? - >>> match 1: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match 1: ... case 1 | 2 as abc: raise from None Traceback (most recent call last): SyntaxError: did you forget an expression between 'raise' and 'from'?

    + +
    +
    + + Tachyon + + {{SUBTITLE}} +
    +
    + + +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    + + +
    + + Tachyon Profiler + + + Python Sampling Profiler + + + + + +
    +

    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%0D^s`}B83hHhfqv<6rq z-aLt77+_Zg!DPS}uA5i-zKLKFbgeAq0@m6>m~`a=`#2|H7O>sH%!p9+QK&1!NvMw& zte_nw61L+J8U|(8rk}b_O`Gpp^wnX2$CpY8krn>`|G%m*Hq&g08T5<^aji#WieDnI# zbo#Ilec8I@?fNU+6R%irPMeU7BrAj(+_K)38K zjGQ#oS@^QMmX|g@1_>RAda1NSvo}raARVSO7k-4%FXZJQ5K_IfDt|#-ZM^V=RBz#S z^8>X{yb}t+Zh}4&Bbk7HO&}g5GoAN(JCcVtl7(t8u8@oNGC1}u2+DxrM zvQiJ}Nj~k<>aNGij4stpH7X4;tB@L$EJ-9))fpNK#Wi>AE{D0Z#;P{_p9zIbT00yr z4vpz$Z&;z!3%+-aYZo>loR6(;$E|Ky3L<)Lg(iP3*_K-2dWZh>QQ6mv1DlIbXr`$b z>UU{BT@pOx5$fL?DHwa8j3|;OUCx8XabMj2j!Yzoc5V3kKuGIu(42#P;M~GvoRfau z{a~R=wfe}92^;H_uJ=!8c!2kVDbYuR=(S|1-j2+Op0*zW@&?*G+>LHq1kc=vELm!H5y$kp-M(-0u(fQFz^mVW2Ho$E)~1~0 zQp{a>xO49S(2u#W9VqClF%Kt3`PTHst|Gb3c~V2_1S)SG4cYW~b`z&&=mAL#@R~0> z)+l!+j-^_FT(SO9nwmbQsMq+G0b|%_sf$z163e;@)d3H-<4{jr^(~_LfN|Xlq@AhW z3iUe^i{QqWyAk^*kVr|;?a$%Za&B&?dyef$48 z2#g^I6Pc1Yaw6TMLQwR4C<4y611ROmcP`rB%14&wBgaMk6aj>QpPsS^jx>M+4q9_Z z)#plVoAn_dUn5db1R{12E&iB=^7i9&$dq3U%Rx*uA+mbEfMPx(yBpj4A5rmosOX%3 zxC}*QY(68B0C$KO_vO&(kJM4|2V^GV9Tx%jQ`zAi*T}(~Un4$IWiz+@S4J+IE_8|D zFIo!1;2blrxdgEDdW|&*VO)NWAF-jxx6hzx9E?BeB(63L z_6!8-1#0-)fbfPqsU!HFE`Q(LBr(6IYvoV!D%zMs&zf~?Q>k~IoJ01s%b2y4F}1^M zH)0x@E5quFcT5awH0X!-{e4N!iIlQ-+mB*Vrdx`6G^vxq&Eq4HWY_lQsD4u7ZjfQB|XRB1QF;Vf!MAjt2QOZ zLzzvcb|%cMoRjvEUa%#yasWvmuF%RlG!w_c=#i26q=8Oa14w88AJ|};G z9Tdl(71*rWnxBi&3wsOP8{|g};5)N0`@^Nq;fy&efhtHrZ>KiQ_Na~WtLe$|hnLz`A`bPABzKw(4W7%as>z0X zB>7O}S1eJ-e1b32p6idM0J#Ey!^hWwPT-~be`lVjdL#RsrMA9){@<8rk|w$W$3MO+ z9BgX7`v3NNv}WCdNr1usw;yE1SwY|i8w7;-pLYz52AH_cmau)SW`F9K?nok+P%Wi1 zm3*BLna#U=sOs7&~q!9HEE2~HC1;{deT9pFdjkK0< zcuy+dIAXh}DM!fMx##}O*G7G$d?W`fg2xPsRgirm2f`#JHF-nZ}L=7I?5VDj4#$_DyVAU8F4Evy zGKFwSgN>V;n|hFI{Gft*Jtt@UfF=M@3M9`98DMlA|JdQFG;ZBUPhUVvv9d~|`LUcw zl*+R6UV`y)y$B%~wf}N1chD8~Rt<6ptg0}2z#-76kyaFe=CWM+(9(dxsy4Srrj2F! z#BWkzOHLbTK>T5VRh=1AVm+>MgUl09(p)qg{`J!> zfDKB;(m0|@7HBihrKn;L%}|InqNEa>E*?A18WtLFwZ-Dg)jyEY9CQ6R2sP^fM9zJo z-8^(@!)SV z_hB`&aDQ+gK9@GCnio!OWztxdF%uEnz2muNy9<~)x<6)zdOHixLG)c|Mgwgj6pb~i z@teD0O;68NjFVDOVt}pF(~V38ro%I^K-uFqU=O3HoB8R|Ohpp)0%i4vjwW{YE_f1|p+?q@oS#kQS(hQT$PA3JQ*Z>=Z zd_=U2MFnT(<$3=13EQB$T)AV+d;pp0d~6#y{}!Ur!j(FWVTvIHpD9TnwF{}Wh9DT@WrI)^6@gKF`LE<7Kc#c zrid`?$Z2sO|Ga(vz)5$TB#t0=5m)M2Fo`7EZIScnDUYp5W+x8dEDxx&ML`*CIE{=U zi$_ktPY_ETt%=bVs(0|7_KYYgz`)Yu;n3c-+O#~O2r`}QD@nhJT0JbS-xV<->wMfw zn({{T9<3i>5kRvRO3j32j(!{4r+6Plas0UJwqSnK>r=B}ueL-gBBa>0O-tBrp4M=o zYE_S-bkr+ig^~dTYk}8{7nD^zpT2kU_KnGqn|}=K_O~nI`NYcumFlzxNjG@osSxpn zZPyW|0L+-TYzT4uGhMVP2)~q@+r&h;VrEh{m=kf-01o3N{7UcvKV^nIoQY%T33?lX zpz!HMS2KhJcpLnh;nSiq} zuk+qsytMMBXoGpvoyp(GO)yKk+Lv{viSc*~LY#~^9|XR5&?1m!#<&dM0L2AGZ8@c~ zA{b&%G|)rqmN?Y~P#(0=b)~Yr&VQW{rL<{eSsfE|6;%~Jj2uW!+M(6@@d5mOWPu9O zfaz9U7&TzkqITGut!OFFPfV3MUG{RLShc*&RXTCXKg&d+;b467YpbFQ4cdh4&>w+B zJ90MBqzZuscS7Dx=#@C?U%@GZUNb^dbo^b|1m~xxHoBK{hWv0sm09Yles(lM4R(g~ zgWSZG_dKPuxorz>zTP;|u4gM%$FGx2w>krJiL1bcU|PR2E@E>I6Vvk<1F<{==n?Co z#zXnEcQWm;7ETEzh;&g0a7jS7o!|&n61o?*l!k^8g(*vY1=9~>vWp;m;%8~~iv^09 za;_u=32OHjZ|DU5?aBZ?!L?>r&P2}Hxl7eu*BTt=wHf7Ng^fQ_yg6L z0^5vyRy-VJiUm8z6pQ3I4bgaYqu(6klMe_2ub~{)x9*c`;0n86T5Z{5cPmet=tP`N z$SiFXFP*#35WbDDj?(Xfx*9lSRhiYvjUJwqE{Y7|X1s1;p>+b*j@C32Tn4^EKMEcO zRR=PRg|_NA`lBGw8oYwC?rm-%Ll@GcePN5*;neTelX?7f$wu*-5&r z`~f(#+6ZPFR4p;inSxNXVf!dE{4v4CAFc}u&ka2g6e5*GRCUq3VIN|j^TVEo2iJ=y zCSA>U)|%$%p@{}8|Ee)c$o8fgsRL-Z)6;uFxl}V7Trn1WR)iPOAdTy;ZklYpiWEPK znEr$>YI=>UI`d3F4vr`%g(?p&xygZsIA|B_RAa$o71FUaYZ4*E$&5Rujbkj zUR97_ZZ$cRHknkzSa|>qUs^2H48BN_E{Hk~EB!3~dq4aaA*JYMNkwI0*Oa=*~WVgvoC$+BJm`f zfLM@g+QEEmm>{~Fs;(k1Y8gfjDMZmCL9IE~k5s?piw`_rg#U=<;0oz0l=QIAfemBw zwT{Idb@W7#Y5S>dBci`H4-+_YlYH5vYL3TBo04{d29Tu+nEe96emg)wT~I~=0)22R zq^az;;nT5Hagr_kSV}Y#2OOng;8w9f+GlC^|>2_Th2i&w-q zzCi?l4-!t1VBSXww-snVYD+r_7@50$>(j=uP+jH-GdbqjF+Y`PO9BfsQh!)G%84^r z)Dzghfu8hT`2^V$5d_wJDYZFOOcwrxlA)`maoj_WS=A7`ApASMIvg_Kz5_i0`lis+ z8UcQ4uLC6D%P_maBmHkc5M;^&~O#!q@f;IWa z4k%%j%C>pKaNP5R1#g6hTrmTh4Vb`?%HO0Y{I!g@rttlv;XtikomHbLJsGNLbm z5DjdNacUa#vav-ZI>y56!Y2g|BNe-jx9{nphR~H)rb8u*z$O+j-svsw! zx>1YMZXd8^8q&rHiz~dNxtFL36rf2oANmeFh<}zL31z^#}MviKD5*$!Jv$m?1BQetfV*O?83ome;n;-YITD;&M`(#C zO%5i$a6|qud{pUHgUXOM;fc*@P0=J&n5uiV7XdOg3(a;^n84O=*d@F~n^$@VOYGLdbC&y0Ux2nuC3g5%(Kd}NafWbK6khBPzm^p2%4%;3kt@5x+Rhn z7oL%PkwJOJ1)ub`v*>5zCd42+aNbF^=IY$=I2^C57U!MarXukO8Ls?-f|x$HrrBO$ zW&v{X^@fI%@-rv?dxKe7566njchbuiY;1c23diGs>w)PTg0A*NgxTWc?X=oDCLi*6 z6bOY}^&#f3fdiUwWFctI1Y$itL&y8NMC0>iHi7atV2~C1xCXrIm;^w8mbA@ zmeUpL*$t@Xn*{xr+}f>MSKRi<1ZfM!inU_p&9Kacc!n%I4??L>-O@Jec=Zm&$@k_rBp^jh zG3M%hF$!8Lld}{a_2J0dxo9f%%Sw<5bQMeoF`E2$pfS$smKcMsxPF*nFUeky5!b*X ztA-zsK`mCpx^9NsaXHRT=UgItB(mjSeM)m923TXZ7nU4blMITal# zn&~Bj8nLV}{~!>aD;Fd^4|$4-g&IRFT$Xk$H3=uVbdMy?k^p)1UJFA$Z|4a}yoOH>+}NJorf47YaG#NsI6XZluRP5V z;TZ6KDT76inpztb{T~u!8NktWt)qoaj^FBxSbt(yP=MLXGoBN}JYH0}#dSDND%+<6 zp6h7Ex84B*3kWNRP?p%PP*Xy#{X7HV8;%GfDI4W~Vy?jyeqnqmv+f(%>&Ug-HTpE% zsOS#f-&&^J5=_Hoe|Il$)r z5xbV^U+Tu>>Q=SqFi~qkCEcNlClvF`xV4nSa|G~||6Gzf=c@JIR7WKG}IPh##0R;oI?f<*w20wgb7c2*A?OYzK)yk02!I2}EI= zf=*=+2Bv<5kg;G8B&9z{o}?Y)Vg#(8FGL6hk2G)U*I<|iB~CwmM!BXhn8C;C9X%2U z`5TnP$9ksESApUn6ydRqE#3S4x7Hz{5H};gs7AXKg(YZyuXk?w>4Uhok{Z{BIUx+r znn*C{$?;A*oB>n*am*|wXJ?!=%*N;TRVu#x7vJS282`*|H-!9}s`oOlhZoDp6GDO% zo&^vIigDB_l{r^f7^LmE%JKaTjyuOB;OWk*BI4%hvF5taAE~er`3Dxo|G9q->8iIs z#HB#sQ*>F!*{j#8DYjUhebOpxfK(I(2v4e_@@Tk0s{uwJ<2t*MUkAmZfzFE+W`(x& zwlj#k@p>CzMxH#kP*f1n9}k+;UKeGds0bmV6_rn@>hdZn5EB?0ZqoJ5C~S;+3$=Nu z?0xW@#j&eYe^FueX7fPkTH#Qgtv6U%(nuv7eK;s(t(o?Mr;U&25}UW>lu(wPbycQK z>m3>IZ2>p>=HYWr%EY2J|^PtwRchz z34p+hQdqr(MS0RNkIw4TSb;_x#0kP?nrE^xKUeBywQFX(ldV@|uCRqm77p0{zzdDs z+leT2hlh(K?p&0m&2{}8!c}!tJCr+UZn_YXy9HueyPIQ1Uj*>km$vlw{aDaanbUcd zK-+lGSMz!Ikq+L3iyzM#@c*g%oZk>>X@+%0(+EOvn02`WCG5N0t=W1dM%>k&$vk(+ zjmNBP@@wbRCmqtFQ61LbVrB|t?y))l%7wB*L0Gjl@;SJ@o^pQg9HNYJb^W2PSIJw5z~BE#mFlsW9M$`?|3^NW`F9dRt(7s zZDL8x9pVZ1VyJOe21HKh*PQNHaQL2JVh*AcBXyrvdPJ(FFi{DP> zc2>Llq_#y6Ga6f#yBMq&W>x~2!B!L>aW`P0_oIL%cP-8U-yn;3Cy- z%3&zID?DWCC8Oe8-jjlkwZ%;k$$QaoTnuEeOq`UH>OGJOW@(i^TX)$Jdv^O2TiK^NS&21k6vc>nk}Do$5mdjgfqE`7R4$%A7k`p&1^ z(}8R)%_)8!NIsVGS&*Rd={zNr@25VI$-s_S{#c) zh37Tu7oikEN>6L5XPW2&9Vg})2Z$rX+c27tv)gr!m>5ja_cKB}V&hBf>ACuEzM4xc z|6C~RNpVKT9NLzq)mXS}zJB{`(JR9H!4!CI^Z~H}bL4+TDG-0sYJ7up}?9B+SLzwf0Mr?&!kNK2=qhn4@NrEqA_>hv+h$9QI^dFS_}#TQQcwI^A`!B}!m z&DU+XwetSO&Gj79=zRm(6tiP}caq*I=dZqtqr2bo-42)fouJqfkm~(bJ1L6ld6fEo z`y*Q3=CPaOzJdepko}$MUNXkSji~(1U}fYF5=+g{jBtOic&jkbXP>e5umMXhn%;U^ zyGDy2+@pGHq$s2)vu3q*q2d6U7~x*A3oAyq{xmFkHy1uz50No`G(Hq@-;i;acu#PX z*%rv98bPKmZ7OnaMIvY9kg~roYNwv3x5~oD_#t3cKmW|Ss8E&B^`QPN{+UpP_=vnO zh|y;36zS}7QSSkKant`j?|)+PhD>9=`0gw7v&?e4`JN7wD%`y*jIO}pu=&Bi)D08Z>LT) zH>Zn99G3-Y@$b6fKA+XF;StY+0|HUCEv3QsRJ59L{B=nn&O0a|kDVmuB^9D_#8foO z_$+uUxIbG+F+>-JD(dR)3NnEeX0i(qpj(xB^vQHhWB%d_2PK8wHS8m#Vhu`01GKTU z_WgW)uXPE`E9Obx>OsE8@gW$h_Y2vzrELQ} zs7>1yE$@$oP?v!GjDS>qdjG}MT>>~m367mH5(73M?nP`S7jIF>!dMhkyW0du8Yi#+ ztud8?o$qx&I#@obhJo&oppnP_gTV6L?ACzUu=;#G2TiQaK zUfNE#5KLd)S%NC@Og3lJ&1u2#{An2aH#CVc)rfn$;q=@ynv>{Qu56e^V0YJ@AMjbL z;2@GPhpyCaa`*pGtf_n`-~#`_uX-SNLx23sSqp;xPqI1>1soL^*Js<$02gxY@#B}^ znY*9Vew_sr-TGjRs%r33a99&U3*hoHwZ)QMk!?x|;Wcsl%Xe7@{>04w5?>oN4E>*6 z=6cNN-R|x~nM~vlel?kWCRLHgd8Bw@jNe9aG=;`m>VhkIL9R3zHcLR?npPRWfxn%_D#1e>j}>R}0y_AwUc5?&m?%wT1ya4@IQziPY>e z3`0CdyspxX+{0EU_&8X~h|qLCY^bUOFTvZ%3i50BHVXa{d;Yuyk`_61ZBZuK%NJJV zPt(1jzcTJ#3l8rsk-A7Dmy_n9tfj56Ihei^H~bm&|HMex|CKyX?M4L`|IbO8PC8-1 z{?i;kbyEB2z<;IIiQ}N8ilBiLL#+o0>i-FXNVP=+4+Qw6-l2h~fNw8fcBMw4gVO<1 zw&9Oat88>XB*1C0qxr*xQ}{u2+WENA4@dU4up)75o|`WggZt8@ZJ6(~eU9iRLd7T0r;&>>p0%yA?CjEm!?{|_ z(x+Lm5?~BY20v!D;R`w_O>FG?YIem_`nDjA< zQUOnQF8)LH$}!XC9S1+VZ(?9ySX@l+jt@IOJCnqukBz;GFOg42yHJxrWXtfhsX)s0 z=_=FA`X2O>hl>kDgi^ss++u4Cxt~VN*r)#FK2%&~N3*#=8{eRIcSlEidw>OzSrio} zM!}F{lpR}qP6JJc$V!+259=M6fgSH21R=NQ^U- zgRO)1_01lZ2a+siCM|c_4T(y(@*4|xb_9zOjQ&GCu$CIyBk&IXeKNQtpxp!n7LdU) zei5-{E3g`AR9Bz6SKHde2?uHEjG~VQ0y>MV5lO*FgZeYf=vHfa8~;4zOY3Pf=xDYc z&iKmF>EU(Ax_w^Qyc%c@HQ&&Ls(4MWSl)$&E0k`dY+Ycg1IYi&2XGY{fVg(N)9mC2 z$vmsLT4PBuFKG6?lpZxB>}%Glf*L|)q|{ku0F!r(>oh9PlI+&nuM>^>I> zCPNd~11=AaT@OU2UofOi+ZiI(m}R9yBN){RK4~>!1(~`HYnF^*66!&Zw2uFTOsMC< zC?%Z*Ue1Z#gZh^|a&Nehx*7dBUgkhIvD5Q&7U4EA{V|Cohw5VgPSTtQpnnUV;mxI> z9InYF|6zgB5H-Du+|l+qI5mSWruV2l*wPoFA4<8T?LQY}}6-61OO@SV)-)ep#5tPM+Ka`qU7FALR9SFH{_tOfw6U{hscW z7o$gjY408s#FlyS}{6Z7RzV-<;gDVuYD;AavJA_1*fg! z0h)Eln71Rz&m8mY>b<#99DTJMPXMh3%DpuWdRv%W79v1{ z-BY~P6gtD?O_Qoy;hD+CFp$^JE{#yt<&J9E)OAB2lw($b-FhoRP?^Krpy-rW17XeL&sYp#Ncj|4pkQCB*TR47F*DxqrNs88s zU!r9(McjcQDy0!o5Yf1r-JaK`(T|X(=>exMe0{B@WvaKjOWfrR6dRmj2b5XM-Th9W zBg3ttK7V!8m-}&HgvwF=@bxzrM*@@t24W(k{`0aO1a$BLGEebr5gp%Q6dEY9+kiZ< zP9O)=i#9EQ(o~Dvm6BgtB?N7X$i22&ln>VC(T49{rqa@1Rv$VvLbL`?EpTiRL-*TwPTsV-DFYG~Ipqz6kIzU`6|9m8HHgZSMpnh?FZqE7u$Vx5ea_~E zgtLV*@FpCzi9i~vgjei+pB;06D{v@1JGXrvT8XM$s!2PZjIvsaG_yNHgh3{jJb zt@p9vw+cdQUR@ESRdc|2f&?2iWDG>RHL(B%pSoR`))Z;7!)|c(DP7`Z>6^R6hs^pQ zWr}v_XqC6Y4ayMK2lJ~1NV#4Ent5beAbzRFb!5ympmb9NE0>wU2rQP37wCOHOGYhM z79^ckcSnR~7KNDOgmV0tTv(je9yES7IKnK%k4gt+&*neP&3Jf7unYqSJLzkWTP%*o zRuUjcMrd9?URq?toL#TLXwMdMpzgF4KZM9y#yz9cRYm2dH+tG%44qv6(q**o0z?oeoo%Fdzg*0_kU$cpwBXLssIeZ8+P$w8|D_B6CDeWhT$CSo_7dQF|F5 z#csrB2fHsKHck7*(nN}QMB?uB%bH@@?dMSLzUsfM7J=|N3QecMm+Bw{XC#Ibod!{P zAZbZm_FDbZ?TmBk8Ip90xjcQhRgX@p9KhxJtjU*kAp|%S4Mn&~05SHBQ|wAioz`UO zDa=7`m%$+fAQ3663sR7ec@<#CPbDSG^!z-R@*xC07uUc~au_&sGa2~MaZ|+X4(H!! z98xF$mqsE4LPeCGw2{7S%jP0PKDST8@ntMJE7e)UJm{bkAhkp?u?V6xOI^j`D;7bg z1ITC4*FPtA3n19y71apc7K^#M$+whbO#K&^%_0Oce{~@YVKUCtyyIU_CL(;?Xy57X zR3Gb;J<7ElLq<;h3Ny^J^=6NS+?C0xyaPrjmlVOlq9^3wG>y_?3h+uZTgQh;$O%7= zd`!dVqckwJj8EW*oKx>L_^W~YX{hP1C@D4)MmT!e$Onw)h{2scV{(p8DGe>P*D;}< zyE8(Zf9{S@+U(I?QLEwvjEk?+?p!FfId0OsslJ}}0LmwaG7~Rb8{~1fA@_Bf(3sm= z0dU#%m1!U<$G+#%IFn#%OM(D$f@W}5VO;JI!j_QHbU%~|;}11u`c3`O9FE-#q!&st zt9f0&9^ZE(7-=bPiH%r}2A#o{>l#8O9otS(fA4A3yy89J$+Pu2)ZIw|H&$b~s)7?> zDh||P;?Q@PnhiP-V(c`fzjo9{(?=1(owJCk&@2Y5-aurdFd73wJRZ)Jo)pRmHdTt> zcvW%NMj4|Ybr+?~u%Dy*BBP6A^*-msY8hLWRcL$y2(GL?V|{~q*wW;OGISc4^a%dy zf60PNwBTeiQ!YFJ7#(*%+XW_ShK$d`WXKb#)>f)DIK43Vf&!fy$)Z zNRj+XuNee}vege@mVqU$`w5FO4Nw-LJlbdF~~FL})R` z((!Z)@m>Yev`Ar;x}KFnSCy=DD&FT+nex&sjf1HFu z!Ce_o$CG&bD~d_90Ab7F1v6q%Lb=Gni&Zr;*i5@1u9J*L*47903HNYq1wncSei7`#m*e~oQw8j?0U$#ZFvxn$n`_8 zMT>uLF^ZuY83eO*cLcGVNVrdnZm{tZNz7vcVGU|re?jfe<8?DA`vw! zvDF>le3>iq;4ipM0eo9UtuG`pRww-qsXvk1eb=h7*AXO0V8N1#^;583Wcsu_tkqhN zqF|0*CF9Vk6nF&x1W7+f!4zZPI#ITFARp99Z9Htsj;4FWi^csTSxq1gzB}7TkTNYR z2lLnasfE~F_sOyONK*R+e>%9ew!PSnVo$57VK{DEqsRIr607PBxM?MJW8L7jY`Q?T z=AgiyFze-je65%9QrDf+)5(QK_7#=<1k&B(?I#ZQM+DG!k7AGY48IfaSSvdImnsvl z#vZejBUq}nkFCHa7rrRR7=p@G67=j?w^+Wh_i<1uDZr^l3T_Zle{t+!M|G)NH0z^O zv!V7RBgwY(>8)`NY0uMpjf*C(bbGrsbkujRYdqj{$^}@!PBFotY>EhLu}I?vrc{37 z%%(YOh(KnXv+yM)hbjHp?9@S?4j-K%zWv5Uwgx4?7HF<|(@9afhr^!Ssm z4uQHZx(*GJEh=2(e_Z^GeGs0E5#sv3mjjTtk!m}bNRI#R`ue_Zizc5=8e9-~%NV3I zIW}y%P7d$UMzjL#U(N=ShA%@`xr%{~{e-Lr-4M&z^COTPH zJQg7gNI@B3_M396_!fwd3)*8a1YAuh`tks=0=>z!{eg6-e@VvK@vc(WxYLarlaLbY z(HPfl?#THz&BF**n3ql_A`~OXJECCIRcUZx&v1_6t!d9uzArw~6}LqH07@e3I5+fD z@6XE}X13*k0QD6!M%=lIT-dh!hoY~s-lf|ov2XFB^^{dT6yBXjhE6`vH+K3TxtS71 z%HC0NSTCgtf12q7tney;DY_+#mF^)A<*U>NIRyZiuRNy>C}i7EbNmyoQKMjjyq z{RgNuL5&j&2Os~K9W)h>2e5Zu$7Q+*hvE-%QF4d`IXrgZE%+Am0;mJZT)X{k`cjk^ z+fvk3vPVu&GP30%WteshJj zc8eDF>NO?0&h1MRwdrGQ)*4!iCm^Y?MwWSq0TQ~PeCoR$$nP0HLLtgV7U)vP6ZPH+ z{wmIN(K~ZDtJ^>(ai;4hx|4}7$GbiQJ{%wm^BIV#q=(2(XBoGUmV~A5 zYq7lkf9HRR63=H97=M3O0rB}lt`8+BI6Eivz;rq8tHkkHG}Q!FbGO{WakJOdoPkqs zfC54sCDjEnzPywteQX=5{+We@o*~SGM@PMk|Ho zAcV7|d=C#~V1)Usme-a6vUkM98GA;YZvyzOyV0p#&=VBAG$F-IKhW&C#T1I@Q%eWU zf4B6R4jbl(Mn(mr$T(qoFFreuli2h5hMPNIhZ~= zB#?>AAlKqX-vqb`j!@A#V8qY25-@{&{$!*Dh5U3`oT{f>mgo9Kb54Ld@fS-d*Nsz> z%k=Vkvu1?vxeAHDP$`4;){)Lde@w!kBZUs%=Dt7?^Ck{JSW)AV_?z!@bbpK2wrP~0 zd5qh{A42jbul0wVl4z~c6IanwEQXnWfBx*x*0g*kA?WM#i50~yy`)bg;eE>72vN=h zmaBO6_zA|H8YfI@BlIrPJNXSRk7zFxHkeKYM2({E1w{_vWx)$nzwjxle`r|p+f?pP zZ*GoNvA&s4M*JaeU1Es%rwa^yCb!tJBeU#Et}ZPTXeW#xZO{wI;uvh30u>z(PVUI2 zgBXds?cOI=Um2bz2<`GhLR3-KEw8UK18KZlQg$+2NZ5{v#!3t6Q=wstUfTQO1PJgvzm&4h^H@P$8CdCk7H3*C zB%`TIhDeO-*35%Nqkngz4U2(OmN2O|l<1ELuRF#vPq9oxY`+AC)`^h&6;pLZid-j&jdT;k6V5Xdw(>163BzSiM+d6?HMya z478O-%h~*BBA5A0`v&#ItC4uUyr+;uN>zuB{`P}s4H4V3K9cdv1N8!S`|XB)i{irJ9|(h!mnD3nr~ zoWwI6=SEge8-HqT-QDY@Q zm}IX5aRHxqRXLbhudoW05*O?Q(^3dtr`}20u;FV=`TLhfU#le5c`2>}TxI5sS^NdHXbBPu-A(@`(OqnD3U+%xRpl z>?{=&f2;2~j%6)$dhdO6bDEtky@2L~%dhW*pxNCrJ>U!8yBd0+B1JMIT5|@FD>e7V+@j*TAe5% z8m7A$_EXF!x#WO0cA<<$i)v3-w;5Q@;|`RpQGek>pbta-l@N@*%7y}wq*5@GVM9Qt z@pKr@4(NB6?@3YgBCkN_UO--(OWYWxgWFwpE=L|%W4=*~xkVm!k<8teAAlC-$XGI! z?7IT;Y2$WS9^SkU+f~RsxiY#S43|J}(vK}$l%=C>k1t!E_y1b7-nt|)UBxBP`3u$v z(|`HO)g1c&v{*e3NH=wiSh}CN9(2$jrwS{i`-uz6lULd^DuYsJ`c3kO>k2B#sEaBZ z`v=80@ZBnnLpYq~CA5I!Zc_-nzuFFSBlISB9aCk-MWcWCMRpa8Ue2ENSq!4?nJ z9eYGze(ygg9zV2A{R>b_0|XQR000O81rUnATunIsh2;|f0I*R205O-ba|9=s9W?|I zf7?is-}NhcWDG1FG~Ah49B}ZR7Mq=Tc8r@#d>Q8!i@;!Lv1M^gks3B-N9*%{zk2ke znxteWaqh7=eMl^JcXf4jy{o8YQ?FH;F1PKr(P^rR^`>rGl~q;UW^GYdW;C+T%KG}c zsIJ{tvooV5u3pq-sTVXi$*vX-#96DGf9$H%JgMDnV67Xw`0M#;`omABKm6<2t3S`w zi)uGhZ!3T>Q;W>BBVMy9Hc5J`tGsTKO}4nnaQmduzio>~uXWWLzgKmel|}w03>|{7 zZHv+`eN{JWSo=4fzslAc_pj=zSkR-if=7oS-Ri~w)(+Nx@K=d`Q2`mMtW5NWe}&!< zk=*RbNWtgFtSEJUQJ*(@lQsH>tjdc#YxRsqf6C12v}x*wzn)d5&B~H4=9lf&rl}X& zm}mkK!p_tAIiYHS{hMwx0G*vx3qYeVEzqeuLd$2P>Ch3@c9uK9pE9Fg0+^<_+9F1S zYwBE=CMk>R#x469{+=m(Vp6;?f7mjNhs*_2 zB!yWiE>3k`w8);b;bA~e?;!(jg!Am0z=QO4m zWkS}R$%mJt(UE#_TNgPfsA^djprJqk-4*RhwJWX8cjsq{C0VQS`L12n)i@cwIsNyy zXKzk_Nzea!@zd*9>DkNEe^(c0Kc1bw0ZdUhO}YwW{4cziX=4d$zTt;|7{h@_xn8Qw zq<9DE!r?gaf1QLInW}I8pkf03C#a}OGx^8k@de=CY^%0dYvt5Wy^Pl+FpuWPJEn^= zGe#RY2@3^&_ELQ2r}StZh9au-<0%W&QWb_Iq*{oh&i0@*J@FtLe+{OnQhv%vJVZ)Z zdakn@oeqiZ4WAAFI$>Qy-&?)jltk>lpz;h$%@e1_E#-k)_#Y?(yqo@F#vD&ULfHD9 z60c&^*gXTHDED~m2<5Gaq4l&WoIU`84Gv%}Jo#5h}+@;UHWx7!w5s;=fr+Y1Agi2r(uD(*n8*oJyUys|ROF-t6f$2I-o<>_e_l+Ym4S|1V2#vlH=DB1 z`P}asJr1)M^;#PO8m5M4VP0Ws91{TD6A8;*xrF7Og?W;n=7U@o!CV&CsnIPmg^2*m zfk+$RRn<)aMjOJZAE0C3?e;WKPMb`?1CFXkKc=bnh@A%U5yxF(>NgNy#C5G{TiI>5fS{JQ9PD-xFz1#Z?gs>5mLWb8dYM(POlC9 zXpXLKi>9tP5<+LI(mFS)tzn5;>K0s=oSqlXQ%)56;)pP2MG(|5zKZZP8;rR@;gIjO zt^q%Af7@~AQbSl$H#h$b-wM_MOile^Q=_{M?p0H7nu7etila327u(u7XmttfZs~He z$(n3!tr}uTc65XGDR)cu5Vm)q1BbdRz!L;nAf$N@08WIWEpnlU5{a0px6MMQ9sCGN zV1u#s30iH&6j&vE-pjR{)r3u!w5Mq zf1PAnH-K_;9}Bcd0ikUQfv-DTl6JkqFd|Nd_9=Yp%yZd`xjRm0+}R3LZ1D&l5=JON z5|6kLZte~jkK+v^PBeGJnZC)i0|WK;i9*(k=pnBgxC3q*rxShPYcFmFvq<3BsPf;z z77AFXI-d$Gr8OWblN>y7bN2hegR`VQ#HtSbAq{P!v3nR%Lsju-k_;&JGcFG@Fd-| zr=XzS(jwf&SBU-}(HuaqvGa%vW)Jj6;f}ILz0*q>*#j9KU}#Ra9ze%(9-!O&f0Ru{ zqQN1Nv4+mVqQ8TxZ}bxLpKa}uRuOy9dSH(}YeXXHrL5x)4xgJdoWlo>4>}|i1Vhqg zA1Et!0h-l!15zMTK>JP_R7^N*SJ_0Tbb}rro8N{1QjH~p!^fONI2=+dYTTJRA#-|!i+>`w{?%|AuAUL7AlbeG=;f0;;@U~tn-29BJk6+H(yIK(i*b5ubN&{PPv)w!wk zO2YBGrdQA+11^RuY0to0CS#~`Dz{9=ui^Mjk)und%=U_;ek}K-WLN5nCaJHvEOu7I z4BPzy%LkJ20|F!0QNTgLazaIdfc~jS3KNeU6ssuTD2BdbN53OY9gRk`e;Fz&#jF!p z7)|P|H#SDlm_po|S1+l{)D-*#p7S&wX?51rauRXb-oJVzoq-FNmQ zMNA0%xY1P(EK}ITFqQelw2Rj-Ut{KkR0No`6LxafI;)Jj16ILeZmZd|kASrhzhvOu zw0Ffq+`$p9T4y_Tm$5HRe;zc1Aq`puUnP0zqDlO;;8{ioDKWo62gUtUx$&ek<}%$W z=TRopsWACmdmX3}W4iefuBIguahXJYJsr?4Pf;knGY+|iv6||g!xx1I&_7b=ObIay zFAc&H-IO~@CGJO8mR;tEC*#_7MCkO1SWqL z^G>QE_R=q9DerV$u&3OqC`Z+|WziVYu1E#=2a`&b0J5zxQCT-|vBBduWc4yCjZ+q* z1bI<;eOHxrmYd(P63Rm220yz(m(En$usNZmv^!$DA@ov#-Xu7xomrBmJP^6HeVR^@ z%OjGq=O<(ewNr!6m?wI4?R)@oO!t4Ef|`mrGi)|`F+p~1rZXXhLhWDcR;J{mQ}0Qw z9}dT&RBBXNpFEyewfaf=*UyqKdc1DVQusfa7>W`El3ZVHuP5WT1}z>f%xxZ#Dg3n=&m_Pkj`-bGP{&C;DawD>18tPmeX4**6;=d`CIiyOS4_|zrt%WnUg?W={(l>JW z!ENScWZ62X%q3d!#lvyu{XpJYllii1^6*0ra6c{4QNm&HIg(X&UE{+BXYbrV*X?l6 z-2^&2NJY9Z#`W zF#TW7(UE)HeYW|Du*44X*rs7eGT3;RMf zJZ{Qe5`oc{LEw()pj^HO_f3DuD6N5Z%}`?82zp#;N_@cv0-DjSidtR3wjUr)gvjfc zuTRu3*b;=9lniVywK?YsyTWT^m`1ziS#({to9iaaHI{RMu-D>L=BC)B7Q|eQZJ$a& zY~1C8zBG8g-K&4b#U|5_tSqOCmCPziOv@gO~$D2xCPj0cKs1Y=aI_6s0 zF=C&Cyi-Xh%Yk))&9X8o^x5V@ff}{zQF9~~T6`Uvnl9za8JYHW#nqo1wz*^S;W4UyFA{OlLoKSQBrnhlSTU*7LDR8OWS?oE%iNHqAy~k320j~pJ zGo@wXKTZwpy`4u&sYJs9hqWUT{C<7W+qKlc-}PEWv?A zbno0`ATX5l$L5{f#EuI(1!k;2m!)@+t_T8Lr^+OHbrlFHt{{l>FB(QQP}U~7c3k1c^bPQbd(-Im z#1MZ%$b8XL6i^4;b41ha?=}q*N4>JPoC*Zz{$e;cbGxgMYw`@O0BtuVaj_9>^w@F3 zVdOhl{_1h!aci~F0#TtUzyM}Njx$J6y)K@WX>!_?Nk2pab3*)Fb!ePcUe=x zUH5CU^fUxzj!KHQ}((^maa0=>64FU}fus1tt)-hq>g6w*_Y zNBskEHfi`^>Co%F?jw;JJmYgEgpQEeIgqrP3s|1BEy(0EQSDG!i75rKG=ydSXwpTg z$Kd|s+q3b{c?|s)hz%(+Q5+dcSGk5p?q_^*4USzBqW)8gaIB6uyXe=DAcTUzTED}$ zs8y0AmzJCZp&00~&f=`JP{5nsmU4ykxKAqXW?FOd=n>JNf-(Ck*Jf6^#>T z;DL!397ilrpdutJHwrGIbQwgFg{yx!w{=Pk0UoucFW{aoNJli3co4jG1PoQ8P15`B z8qvr&w6{N0P$W7bCS2j0(L6n|(CDq?ZBe%0z%nlVBoR*jnMGXZ=1QY-O00;mDioaasUL#D0BLD!Sj{pEJm$7pMCznA<1QmZ;_(pOxT-!)+IUSUo zv$5kv9A{x~;uwF&^iZVCre>^6_P?hd{eDQ=I0ANPZ(s-oHM`e7{UuItBoZ2C?CV?m-f{$2y!P@ z&8889>Hn2}@|8H+b3Zn%>GVZEY+=;DiZ#w~4#;k+Z(8yo(g0$VfNwq-hJMhGX^!fxSidG> zD5m(m80&RC)~`C?a0#Gc4Z}CoogePwY$Wn=!HQieLpw>iG z8~i%xIV$8nrH84`TZNYH@atL%mn8GzR*cnJZ0dh~J6585XokKEIMV{_*P@lMLfyIn zpu)0+rQ}Qf@(6&o>F#yk&*ASQO&^gkx-C&D)wtiGL?DT=8^o?2gk?Lk*Ix6RB$;5h zmbjwhum{OB1U%I6{3>Wwt%rVBfoL0QIgef<1|iuz|sdR=254AXfjMh@&xF!-2strnzk7a)mYye zDj^FQR?Y*stL2?Aoj#*)`PN1VxuXn8(HZmv9v=5~3(Ty$4*RYHNYx&^5}5jPdN!@-Fs>><^CdJ;3deNvax!~Hrye|9 z1A14;0l=Hr08S%L3X@oW+4nBN<<#mlY8vi>Es7Qnf-9%PHB-ZIJv5;a;j6nDxET;> zp#3P|6kNM4sfkAJ(;5Zpfrp241{8nDrfbHkni@h$(QfAE8y|2B|7f$Xm3$u-Dba45 zZR>kcIih)SMC93CblOy;fh-(-qxc(WQJt{uVD1&oUp6DOyy>-?NE7IXm4GqT=;7<; zw(AFj8&WR$Uagd@cEn&|tKFeex*0&)^owG|RlXuoRhH$oLK%3*Vm>P_e@TB|7ar+L zty3%&?fti!;P6`i8FY83292C=K|7v_6*$m1Bf6%+^kxC1aCQ0MMTHq)hA)G@-r7NP z?|VoXe<_hd{*$16dhMWF`o*)L`^~;-*VU$OS}?HWg0+(rO-N z2;P~I>@Cy@l!FkRP)R6;r(=IGRyt&l$24Ljv?#n z8W73RjZ#DEj$v_6-C9^z(sd0ITOy`jEBxS_(%5!% zmMrLD)Dl>jUVxlNTuNa3As9ki2Xfl>2(9nh!}8O*mBL5Q(hGETu{eJZ^|wr~|JecR}G&;=1S-oe`)2pJ&gwwzdE*A1w~p`|*7A!ZJ(N#urG zHFvW>27+m*rO+Mx@&H;RAXi31%0dBDN+XclvY z+}xqr(*BteIVUlLp;nO)Ra?`(2w22|qXQd7LXJ!D-6D2ZoXses*djZN6mK}(kl=%8 z-N-szo;u^M5)$~#^-}|6PsS;No|`EFPqI?IBnf#`>Yjrn42>9sLQhdRA!+>Po;*%V zf-Dp2DMi90;1qw9WI0Z;0{oxJ9r)ox)Yl;*TR`cl3+)W`8u&b>URjbRM4^>i4nchj zK1C8u2$xGb^7ktIOv_n%oePj->;{nmlK<9qKRwgai~nz3+|=S(+C4Ac{NXo$04*9_ zcSX$`yfE9-O3HZwf1xc>G$Ss|2#O}TN#U^rP3!5!rmcT(!O|I5ccrI(vTL zJ}KJO>MGHE&r@pxhx@7?*2pba zH|<^&26XfZw42+0bq`u_ITWU zXrv=(9C36Y%N71XW+nXrJD1{inMKzh*Z54I3>g9aS#G5jizhdoPc$}+h`sWQF+a%-uJJ-n)G(l$Gg{6OG`4G6%Gd@R zR$r)qx)&+iz+8?*nmW5mRkH>VXec+{=uVaaj#KrVW8*sXDT;{Zbw22%bGNRnvp;|G zvF3%A>tP3j>Q1EGeGMoK^cO9><}?W=RWbI(ZSz2uRqX(pXeDD(7N3a$7*!PC?IrN; zUEjmbLq6Oz13Ylp7J&_7hlj2c>U~~dbf_n@ZCg5zKkIj5vv1o2L3=&)--*>&P;Ltv zT0q`mz$avRZ5v4z9SS!ZoD%IUIE{ZV#*;Dv%$QM_dQwbm5WgVaL{BfJ6BS#W=nbIs z$?M|hAN*qCu77O7$HJ!%e8Ak#iw9hU9Fnlvw{n^bg(Q{&SpgUa*yaIwXnWWlSh#=Q2vqW5 zoDG=%>*9v_gmNV{I22awYz>^x?f@kR7*6=VFe+fY*%tjs*YGK>=F_O0rGJdA@M9!d z@gGYA&ZaqfZB?$2ZpdG!;{fngx{nxB5h>-&`kOmbf}q^DK0N5l67fg%^(l}`8i6Z$GYQz^a0OV zu9zl?R1csb08y{ufa|gI1h@{YlJx{T3_?YL?kKTJkbWn}RBalZadJkg2Au0M7)}nKbE1ZB^%X_6AQr?sn`Zs?9l}F1vsKwRzM2dt6 zfQDr5J2Vf(UgXhZZjfMQo4uqm2JoK>$3M0z_}Q@vTAgr=nKfIQztRo)U|!w@wuk=2 zZXrEyo<~{lFi+I?Z^~!Eo+NGJzdl+N-_#xQhn9o|C3;h`&AQlkt&ozf$0Wx+hjr(q zR8Yrvz{;?-^?`pneQ4_9V!6D)Fc|#1yqM|5>tJ02II?dAz)*>%thd$0!9&hpaZE*1 z@0a*xiexBtT8Vl?df1BCl*trGQrhma0@ORq7)jfOiG&&bLHKe{L=c z^jR*836NxxZj2u#qVQlk2jEW#;1PIu95&riU|O4klU9GChhaJ?-=Abo#gib9;jiA2 zuIaA_F9cd+@uJKk9(L1N6;L>S>!C%3Sj|SV!KvjBnhQh7N-meE2J0!aOG4*jMPusl zA6j6H?TjOPF(;;}n57_I=R(s$-|8xN^)NPdo0dR%9U$+h#y3lYXu8b_H2^;+Q~~`t zAscnTfbf6GMh#-gcW6Yb$Zga*q?zRu&1!!By-&vcdGF(pH*NfOk(bDS9jYz%ZRGvZ z($x{OR?+24{L_lQq?Tur76Y6RgWovAZ- zpDxN9V|y!yJ9%7-N!RPC;fTJYfd$a1d>yq^p1;Iy@^EiEmqoJPZbMp*n5xox_-r6UpDK)8+Rj85*xck=ep8rwBN^~HQKjR*^S ziJKCYL2Aiqe1$pCIzSMCLgESpmF(_3nc5MfO&|9s-zMse4kJaoKAu(VFwKBY#rA(` z2f6f1^o=}<9Gm3)E6Y=)vIIg=*={E@)bY(MEj1D@mGiO$@srk~jCyqZO6%0mGn~bM zNP&v!AFNTLqgJi_Bs}~?d=d~MnYD68AJ8xKPrzJHz(-TLDWp z{29T+oH+Hh4^N*t(MBjf_Ba;1n5u1z=rseqQK%}gUEnVFl7N#=8m5cD) zI^?hC1R>J;av{d&`i>+^XQf#bs5TE<4j39uz$Ihph}cNE<#4HYeaV-!D$Gmq(L{(B zoj=!}@K73+i@}v$?!aBg$F$08IA5s>b%vNr6H_hgHTyFoa2z)}29BtJejb0Y)F~9> zMRQ+mJlDA+!L>`=qz?lQHmr-Y(1<$V!lNCa4UPFiT0CiJb{xCJ|Le}Y)I#lddEcLk zx4eU`7Jj_FahpCC+37o-CB(b_oO7&Z(#*~=v@=`jIKo%g8Rt>;uA5AM&e|QO@zGuK z@p^rp`ieQ-cv-Jl_ncF}esX_MOF=bt0IHA%yXhbAn$?}nYVeIvx-q7l|kx#HKBjQ*0s7*=sJ#A zw2pVF7!BWb1nybg0XFEFnH6aT{>g&FZ|k)vfNfXzh|YoY+y-QC+IH+pn9d5$V{w>Kje|ZkOhLi;ASauw z{OF)kz(`oRP6-gGZ-ak>v%_2~PCD}UEaeN3+bY~1bEXD{e((Jq>;|(kyQy>hF0LLF zc(9y2-4j`H1~$ozlIROyFG8ebS6z)1mBk8Ge!kIn3@E7lyLKj%gj8}TR{VF)k3({%}x zoaH34mXyd@spXl5d^`(%h^80(;yr(P!TAY%3$Qr!`=M|ONhB_W zG&)f*^GdK@j4bze-~ z3)2?jwm#gjjtkR(RgCVV&=Cp*CyF47GfV~IT*x{BFjddAp!DuN27j_SpN5e#37j-w z^4@=(*%Q|pQJE`!mnd9fnfn~S^>a-qE4qxk)VmSm<3qCu)j?$?8*h5-RaC@sc5j+g zK32Ml0S4K3+A1Pct*bo+20F!XbQ6%^`TBxa*Yo1(E&dnWCYXPl3k9OvZ&5cFQ;Xv)tvEPwgaiwI0%tH^ z(jiN`UXja!+ySl&EMffhupPD-5j<8uHK9f*+!TOg`!&9#r#dA8W|YK?|1q0(<=)F+ zALhhHUyKYV0UTeo%ib!nk|BvYcBAm7VhLx_GTSuSl(YQx^^VvdmlQ*%pe>!~Q^|iu zCt92F!saq->Hck-%tXhJv(3-6ZzBpO6%Ypj&q`Q^xmx-YFHM%uVh5=HWckXuVKoG| z%pBmvCtNpCDF~$4n@dT=DavJG7cD3ndSZ$F)>j^ET<#{}sL_2r&p*%;{puup9Jkl& zecV{j%ww{&dVPBymfjj^$9+j(N!=Ek;d+qP|gd*AogS9SkQ zP1VfLp3{BKbjMcR$aTW?@>@sS_a@XId;y!4S>KluxxvLMt4`1 zc>G>1#l(x@z9t(ka{Ah3=!sDA{i1&|%B-tfjj7|9qB`#8EqKZbY0J=P50fA09vXx{ z8szPlpvj7fL+l6wf#bSG&>@U>nX}KEE~ln`2$RDq6{~jh)3d1#=0E|K%%vUWmi50$>+_k&>N2Vx;Hx@_ zj7hTmo>`PI_N@{l>U9EJ>-HG}aoIBrFu&3LM%@hIuU*wL_X&c|)$f1$p62f#Ma8m0 zI`M>H$>|Mpy*1(u#1rIK>0H$%w2v$j!~L=53tEgehcs?30?c7hw33UQtefs5|%rpB!E zxvl{|No8Mdsy12@`p>Xj$x~cWS?AAkPaMh|Hs|d_cMroe&q+CMZUbIn(Oy>4idyFW zhMh~iosWVF9zf;eS!=V;UBtBdbNcOo+T(-Sr+6!9AD4wdQ;$ZA{zmuEK!ffxJF3RkU=MZMFjE z!*ZvMEoUJjtgQNU88C&R{+YHV2s)3Sh-yt)jcQn>tm-s&p;lb5Qv>(cD0Q9+$1ZNN z)TA~}`(K~6wOqe&`gDp->E%2L3>$t7_R2ZbfbnH1I>2Y+nIjKK|Lr}YhT+1P+l}ZE zk~43%W8*EYFQu0?6QV1RhyqPLuE0D5`L%6r`8xh%ll?YS^azN3R;qGr_sqeGc~Sj) zxacjhQyzRu{?Fj9Qb5plf9oZ#yrD+Og&I3PG2m{d-@rDBUCX>zHpan*N#(ZG0n_i~ zj&03>(&ec<{(d-Bz`cn9Nax?Z?hAXbZBKo0XaS~Jwd{idRD%>9Ld_)Re;bxJPw=FJZt=wpk9(yG( zL*?)C+LoLLnmt^gc61}l-U`sdJ!`fS|RlZJUK zowv#UL%ka+7b?~+xKbk=G4dg)nx&a0-ZJR!B{{~Dur>QEyEJ@9X_L2+$NEGTB_`9d z(As%l(nE~zDgGua?kP1Ya{sK6TUFEuma{tJD8sq6vmvk4;14RfYliIKLxz?G3jmJL zeMy63)i5s(&GwI&E8Cg$`ahY}Fx~p^38^h;Uyq6NT9@K7U$psV#7tpXB(Hqu% zs8sNB7gLAO-N0L|^|=EF608@7z>OyLgJ7B|aukH`ehcGqoh4o=OH;&mR-6Qz3cY7J z`iqjE;0YRJ%EshFqb&@J+{11RuK*ISCj>qjVS*|CFa=4iE(7!wn_hGNpTERK5Z>o& zxRz|O+Ew!U<12KHVZ6Uw9V*>K`8(YMybH^-70Xz@J>tt?&0Mn!kCB1G*Cwm^hzT2^ zA`<{AN@us>{DR+6T7DmX?Iem^ePDw^_Hg#G`9qAsN49uwj(7V*ba-+fxByzsH$%78 zB?i~2YcbLz-BC-UEkLRME>_7NP(ZB8UA}4a=B??058P2BebS6 z|CY-n)ivV-?RH{#RwXoEo%R)L9&8bz4C9bnf;73%1XIUU0=DT5FLcBHA(ae1m8H_i z?|j|9&A}fnd0v4~QdavUB|vo*;$U9uEyT^#JG$ZMPsw@3?ak(`5V9>?1SqJT)g&r= zd%Ol1fqam|>Bc8IqAQnNJd?=1k2S2uD0ldU)=oC$< zo1vj#XYUxQxALz7=Xa}t1F}(5Y7j`=&%R^p?oNU|Qa|Q8QQ;|;Mr9ct0de9+krbk| z1>eHdVPsCCIxrMl3uv1(DtWv%WL7-kGXd%_ zb+$x-Y~Lai2pdG3@q#TMn^C39IZZz$SiPFh(@3@mHHN`hxA*;tmrHi(EP)P#20yvx zTHffqXc=V!8G#Y?MX|BH8G#*nrKf@ot)LZQp~i z<`Y^srt+|#d3bKTHfQ%GwLWaC#rw|W{E=gmY6H?5ofk>mRO=k2z3J~_bQ=+D?fVzLz+Xp zcqXUtrms9bn{RQe=J+N>V~N# zC@`jnwWpBn?EQBaebVi`U8m7t5l)R#k%d=x`kDd;jEi zwWhiq1Lbi8SJ7WixMv>U!<9MAsaKCnJ!@eividh%0gH&D2%AQMT)(vKeP;N{ga-YS z+SN%W9`bk}(SxX}ED4)R!>`hDU_a4k^X|HVazMl&^I5tBN2b%V=%wSNO-SxZ7K*To z&z3hosqd03EIBr#P)#4q*A+2H(XC1_u-vP_f-NpA$$+_#ke3+o{cS13cMakN2Cz;q zr$5u>wbC@smZ$5aPbs&2W0QCp*)YHTBKr55#!us#ew90i_(Ra{cDsY91>khR9oO46 z8xXfS9}p7-mVNBcQDkHEqNeA)k>p#m{M-3HHD<;ILTgrjr;PgZpWRt|<9@!Wq}4n+ z04_2^Y4;HtDON^0S8nduV($kKkw`jT)bg7*X@diaLX1)x?TO0F$q^#a%e`w3n0rDh z@sd{VlIFb(!eN(YoT)6pkxQ;!;1bj|1R!$LB5zB=^<8gcINHJ#LrvhXhw9|$yJM)z z3vSetJ$Ox1C|QyCTCMw`f>aq@ko*7#sp@fjUhuIB18Zo&>1iDl|kCJUD7=y5-`{j5Kw}XF>q0&^t^F!;;^un&e zw{_zVBI|b8Xx{qe4jdVcI-!X8^dfN@r0D0Q?a!QGKMQq)#~lPsZM6ZT1RhA;vjJlSbnwK)?= zcyBTRBqTY;>Q@zvbnalQ=fX88{AwyJ;~nOy0knTA5%mWL^;M&a>g;5 zZv7F;-EtP{%E@(YKw;2(0_p0OCA;lIZ@J-BwX*~4qFoMNUeV3RlkeOB*)NUnw>1OB zhN2G8YzEFq59iRv^{pXmnB(IW(q4<0b@3Adw1%ERsJ*6vPKQCPlpv(yn1W|1M9!J8 zaIL=FoR-+{yz7utHYqbW!kCs*D(q1%`5FzTFPM*%gbzQxzDqkNfIG8JZ~nd}TF#1V zD7BnUI*`o8F9r)#GX)9&J1ONsTYyKqfY}cr&L`daa(SMRI=F;FISex;8m6zv7;`6Pc9&#Iw_yyOk$Qw)wc=<%X?&Jmv%*-B@pc)om)rr1hO8VKy zIsEM5o!Y}|v*%?Si)uB8E_sA$TBtK{_axMp>|qQljy|6C+VDQWs|tBN9(I>me?Hn& z&pmBTm!$M)^BB2wLv&ML^J!!QyvV}ONZSDfXldIm`HH`3;DnrAbz5q%CCoubI;kH^ zVS*4F#7~I7nNPHjQ+!m7fwc5^IX!6==0PprQaAc|QPtiGg$9~z#`;!Vq-p7Eb$I`w z8~*;1nAPCxqtc}_g``=8njVS+FS#eMeA@DOqB*nyFSfAl-c!*>n^Lw5k&VYteETw5$Gge8fA`mv)m4Gp*Nzm|Y!MzvMGY zA}5^Y*1bND@~Y}F`&CEfOO>Y8MmK#Hh%{8mb5?L#{{SA%LQ}dT3l*b#nOxbu9KZY)0{B(=3~U0P6%)r`LjpIu%xNI~^{iRCmC|8C*5L0CDj5kq~r*O3%$-FX_ zeNZ#WiA?rgryCv2Xz@rKQ(?(Syge$Lk0)j0klpy^&;Hv>%Ns5&DSKo6X}k4!u~E^R!h?dW!XP_T=9NO>Pp=S)(V>X28E zbzzFBmWzhBPu_^pr%D!$d!<7!*|{`V;tr`z75VHkq=IuYxFdiF%n86P!p}VDiawm6 z2gOTWiq5SrjU}uUWIjb;rcb(q%610C)Z(5FPwJUqRJDimIRVs8em?j1&p$;x-wvKO zUT)?iS59uK8Md{v^w!9&>~+h1IDVqv7WkEILv!}uN~lw5*1&itTuSGas^)k84oj0S z3tJ@eH!XiVTk+}AN4S)^uH*m8h%3C{GkhSmvhCbkrTSi;mEyteA-kCTT`W+~ZJOrF zgX}>kClEPpoB-(d{JEZT!>p-Zn5c++QNvsjC|{C)((*gAgWHMc3MZ7$VBlVO06x(@ddbg_}gv2UJRDNJYGGkjZ%8j zGpve~U9SdOH;>{qdXV1p9jJy{pNBIpG0S=(myp>(>y`Gx)qHK$|5;@6z}wgdmi<)$nFiSHVWQZDjXkWygW6vUn z(i*^3&#ra^<_H0@VPkVBx4>aLC%R2Ca*PvFCDF6v8w;(ohwoVT=Yt-b9+cWmIDvu6_Hs%}T{QBz zxUY|6+ixsPCJO`vaZ3^ZeQPS*zgXlbGw|`I5zgU&cc))@8EA?Z8fqnajs@nPfk@5l z +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