diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 8c8a2d9ea51aef9..ce3d004374338ef 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -354,6 +354,7 @@ _PyStaticObjects_CheckAll(PyInterpreterState *interp) { _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(canonical), "canonical", 9); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(capath), "capath", 6); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(capitals), "capitals", 8); + _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(capture_features), "capture_features", 16); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(category), "category", 8); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(cb_type), "cb_type", 7); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(certfile), "certfile", 8); @@ -404,6 +405,7 @@ _PyStaticObjects_CheckAll(PyInterpreterState *interp) { _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(ctx), "ctx", 3); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(cwd), "cwd", 3); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(d_parameter_type), "d_parameter_type", 16); + _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(daemon), "daemon", 6); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(data), "data", 4); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(database), "database", 8); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(date), "date", 4); @@ -504,6 +506,7 @@ _PyStaticObjects_CheckAll(PyInterpreterState *interp) { _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(fset), "fset", 4); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(fullerror), "fullerror", 9); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(func), "func", 4); + _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(function), "function", 8); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(future), "future", 6); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(gc), "gc", 2); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(generation), "generation", 10); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 5b35c53e0aa03b1..a293f0528b91ae4 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -366,6 +366,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(canonical) STRUCT_FOR_ID(capath) STRUCT_FOR_ID(capitals) + STRUCT_FOR_ID(capture_features) STRUCT_FOR_ID(category) STRUCT_FOR_ID(cb_type) STRUCT_FOR_ID(certfile) @@ -416,6 +417,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(ctx) STRUCT_FOR_ID(cwd) STRUCT_FOR_ID(d_parameter_type) + STRUCT_FOR_ID(daemon) STRUCT_FOR_ID(data) STRUCT_FOR_ID(database) STRUCT_FOR_ID(date) @@ -516,6 +518,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(fset) STRUCT_FOR_ID(fullerror) STRUCT_FOR_ID(func) + STRUCT_FOR_ID(function) STRUCT_FOR_ID(future) STRUCT_FOR_ID(gc) STRUCT_FOR_ID(generation) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index c80925f020186ba..fa90d6b9582499c 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1641,6 +1641,7 @@ extern "C" { INIT_ID(canonical), \ INIT_ID(capath), \ INIT_ID(capitals), \ + INIT_ID(capture_features), \ INIT_ID(category), \ INIT_ID(cb_type), \ INIT_ID(certfile), \ @@ -1691,6 +1692,7 @@ extern "C" { INIT_ID(ctx), \ INIT_ID(cwd), \ INIT_ID(d_parameter_type), \ + INIT_ID(daemon), \ INIT_ID(data), \ INIT_ID(database), \ INIT_ID(date), \ @@ -1791,6 +1793,7 @@ extern "C" { INIT_ID(fset), \ INIT_ID(fullerror), \ INIT_ID(func), \ + INIT_ID(function), \ INIT_ID(future), \ INIT_ID(gc), \ INIT_ID(generation), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index a9db3b828d1ef80..aafe24e288e172c 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -321,6 +321,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { offsetof(struct _Py_global_strings, identifiers._py_canonical._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_capath._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_capitals._ascii.ob_base), + offsetof(struct _Py_global_strings, identifiers._py_capture_features._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_category._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_cb_type._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_certfile._ascii.ob_base), @@ -371,6 +372,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { offsetof(struct _Py_global_strings, identifiers._py_ctx._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_cwd._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_d_parameter_type._ascii.ob_base), + offsetof(struct _Py_global_strings, identifiers._py_daemon._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_data._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_database._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_date._ascii.ob_base), @@ -471,6 +473,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { offsetof(struct _Py_global_strings, identifiers._py_fset._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_fullerror._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_func._ascii.ob_base), + offsetof(struct _Py_global_strings, identifiers._py_function._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_future._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_gc._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_generation._ascii.ob_base), diff --git a/InternalDocs/profiling_binary_format.md b/InternalDocs/profiling_binary_format.md index 51b1944aa5c8a00..0947e946f41bd0c 100644 --- a/InternalDocs/profiling_binary_format.md +++ b/InternalDocs/profiling_binary_format.md @@ -84,15 +84,23 @@ with a single seek to `file_size - 32`, without first reading the header. | | | | reserved) | | 12 | 8 | uint64 | Start timestamp (microseconds) | | 20 | 8 | uint64 | Sample interval (microseconds) | -| 28 | 4 | uint32 | Total sample count | -| 32 | 4 | uint32 | Thread count | -| 36 | 8 | uint64 | String table offset | -| 44 | 8 | uint64 | Frame table offset | -| 52 | 4 | uint32 | Compression type (0=none, 1=zstd) | -| 56 | 8 | bytes | Reserved (zero-filled) | +| 28 | 8 | uint64 | Total sample count | +| 36 | 4 | uint32 | Thread count | +| 40 | 8 | uint64 | String table offset | +| 48 | 8 | uint64 | Frame table offset | +| 56 | 4 | uint32 | Compression type (0=none, 1=zstd) | +| 60 | 4 | uint32 | Profiling configuration bit field | +--------+------+---------+----------------------------------------+ ``` +The low three configuration bits contain the +`_remote_debugging.PROFILING_MODE_*` value plus one. Zero means that the mode +was not recorded, which is also the value in binaries written before this +field was defined. Bit 3 indicates that capture features are known; when set, +bits 4 through 8 respectively record `--all-threads`, `--native`, GC frames, +`--opcodes`, and `--blocking`. Remaining bits are reserved for future capture +features. + The magic number `0x54414348` ("TACH" for Tachyon) identifies the file format and also serves as an **endianness marker**. When read on a system with different byte order than the writer, it appears as `0x48434154`. The reader @@ -567,9 +575,10 @@ one write() call (or feeds through the compression stream). ## Future Considerations The optional profile-statistics block provides an extensible metadata area. -The 16-byte checksum field in the footer is currently unused. The version -field allows incompatible changes with graceful rejection. New compression -types could be added (compression_type > 1). +The Python-version field retains one reserved byte. The 16-byte checksum +field in the footer is currently unused. The version field allows +incompatible changes with graceful rejection. New compression types could +be added (compression_type > 1). Any changes that alter the meaning of existing fields or the parsing logic should increment the version number to prevent older readers from diff --git a/Lib/idlelib/idle_test/test_outwin.py b/Lib/idlelib/idle_test/test_outwin.py index 00a7432b9088732..667c1667465c3ad 100644 --- a/Lib/idlelib/idle_test/test_outwin.py +++ b/Lib/idlelib/idle_test/test_outwin.py @@ -46,7 +46,11 @@ def test_save_defaults_to_text(self): io = self.window.io self.assertEqual(io.defaultextension, '.txt') # Text files are offered before Python files. - self.assertEqual(io.filetypes[0][0], 'Text 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/iomenu.py b/Lib/idlelib/iomenu.py index 96bf6078f12c3ec..c2412e98fb9c4db 100644 --- a/Lib/idlelib/iomenu.py +++ b/Lib/idlelib/iomenu.py @@ -386,8 +386,10 @@ def print_window(self, event): ("All files", "*"), ) - # Output windows (Shell, Output) are not Python source, so they list - # text files first and default to ".txt" (gh-65339). + 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"), @@ -427,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/outwin.py b/Lib/idlelib/outwin.py index f7e92bb8ac0c776..b9dfeef173637c4 100644 --- a/Lib/idlelib/outwin.py +++ b/Lib/idlelib/outwin.py @@ -80,7 +80,7 @@ def __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.filetypes = self.io.text_filetypes + self.io.save_filetypes = self.io.text_filetypes self.io.defaultextension = self.io.text_defaultextension # Customize EditorWindow diff --git a/Lib/profiling/sampling/binary_collector.py b/Lib/profiling/sampling/binary_collector.py index ec6020078985caf..7a35044b1ee1d76 100644 --- a/Lib/profiling/sampling/binary_collector.py +++ b/Lib/profiling/sampling/binary_collector.py @@ -10,6 +10,23 @@ COMPRESSION_NONE = 0 COMPRESSION_ZSTD = 1 +CAPTURE_FEATURES = { + "all_threads": 1 << 0, + "native": 1 << 1, + "gc": 1 << 2, + "opcodes": 1 << 3, + "blocking": 1 << 4, +} + + +def encode_capture_config(capture_config): + if capture_config is None: + return -1 + return sum( + bit for name, bit in CAPTURE_FEATURES.items() + if capture_config.get(name, False) + ) + def _resolve_compression(compression): """Resolve compression type from string or int. @@ -49,7 +66,7 @@ class BinaryCollector(Collector): """ def __init__(self, filename, sample_interval_usec, *, skip_idle=False, - compression='auto'): + compression='auto', mode=None, capture_config=None): """Create a new binary collector. Args: @@ -57,6 +74,9 @@ def __init__(self, filename, sample_interval_usec, *, skip_idle=False, sample_interval_usec: Sampling interval in microseconds skip_idle: If True, skip idle threads (not used in binary format) compression: 'auto', 'zstd', 'none', or int (0=none, 1=zstd) + mode: Profiling mode, or None if unknown + capture_config: Mapping of capture feature names to booleans, or + None if the capture configuration is unknown """ self.filename = filename self.sample_interval_usec = sample_interval_usec @@ -65,7 +85,10 @@ def __init__(self, filename, sample_interval_usec, *, skip_idle=False, compression_type = _resolve_compression(compression) start_time_us = int(time.monotonic() * 1_000_000) self._writer = _remote_debugging.BinaryWriter( - filename, sample_interval_usec, start_time_us, compression=compression_type + filename, sample_interval_usec, start_time_us, + compression=compression_type, + mode=-1 if mode is None else mode, + capture_features=encode_capture_config(capture_config), ) def collect(self, stack_frames, timestamp_us=None): diff --git a/Lib/profiling/sampling/binary_reader.py b/Lib/profiling/sampling/binary_reader.py index e611dfd45523f1d..2087c0da7c43b6d 100644 --- a/Lib/profiling/sampling/binary_reader.py +++ b/Lib/profiling/sampling/binary_reader.py @@ -6,6 +6,7 @@ from .stack_collector import FlamegraphCollector, CollapsedStackCollector from .jsonl_collector import JsonlCollector from .pstats_collector import PstatsCollector +from .binary_collector import CAPTURE_FEATURES class BinaryReader: @@ -50,10 +51,21 @@ def get_info(self): - string_count: Number of unique strings - frame_count: Number of unique frames - compression: Compression type used + - mode: Profiling mode, or None if not recorded + - capture_config: Capture feature mapping, or None if not + recorded """ if self._reader is None: raise RuntimeError("Reader not open. Use as context manager.") - return self._reader.get_info() + info = self._reader.get_info() + capture_features = info.pop("capture_features") + info["capture_config"] = ( + None if capture_features is None else { + name: bool(capture_features & bit) + for name, bit in CAPTURE_FEATURES.items() + } + ) + return info def replay_samples(self, collector, progress_callback=None): """Replay samples from binary file through a collector. @@ -119,7 +131,7 @@ def convert_binary_to_format(input_file, output_file, output_format, elif output_format == 'gecko': collector = GeckoCollector(interval) elif output_format == "jsonl": - collector = JsonlCollector(interval) + collector = JsonlCollector(interval, mode=info.get("mode")) else: raise ValueError(f"Unknown output format: {output_format}") @@ -127,6 +139,8 @@ def convert_binary_to_format(input_file, output_file, output_format, 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")) # Export to target format collector.export(output_file) diff --git a/Lib/profiling/sampling/cli.py b/Lib/profiling/sampling/cli.py index 83af313e1efcbdb..c5fdcd7331096b1 100644 --- a/Lib/profiling/sampling/cli.py +++ b/Lib/profiling/sampling/cli.py @@ -629,9 +629,20 @@ def _sort_to_mode(sort_choice): } 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): + diff_baseline=None, capture_config=None): """Create the appropriate collector based on format type. Args: @@ -645,6 +656,7 @@ def _create_collector(format_type, sample_interval_usec, skip_idle, opcodes=Fals 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 @@ -661,7 +673,9 @@ def _create_collector(format_type, sample_interval_usec, skip_idle, opcodes=Fals return collector_class( sample_interval_usec, baseline_binary_path=diff_baseline, - skip_idle=skip_idle + skip_idle=skip_idle, + mode=mode, + capture_config=capture_config, ) # Binary format requires output file and compression @@ -669,7 +683,8 @@ def _create_collector(format_type, sample_interval_usec, skip_idle, opcodes=Fals 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) + 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 @@ -760,7 +775,9 @@ def _replay_with_reader(args, reader): collector = _create_collector( args.format, interval, skip_idle=False, - diff_baseline=args.diff_baseline + mode=info.get("mode"), + diff_baseline=args.diff_baseline, + capture_config=info.get("capture_config"), ) def progress_callback(current, total): @@ -778,6 +795,8 @@ def progress_callback(current, total): 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": @@ -791,7 +810,8 @@ def progress_callback(current, total): sort_mode = _sort_to_mode(sort_choice) collector.print_stats( sort_mode, limit, not args.no_summary, - PROFILING_MODE_WALL + info.get("mode") if info.get("mode") is not None + else PROFILING_MODE_WALL ) else: filename = ( @@ -1179,7 +1199,8 @@ def _handle_attach(args): 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 + diff_baseline=args.diff_baseline, + capture_config=_capture_config_from_args(args), ) with _get_child_monitor_context(args, args.pid): @@ -1286,7 +1307,8 @@ def _handle_run(args): 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 + diff_baseline=args.diff_baseline, + capture_config=_capture_config_from_args(args), ) with _get_child_monitor_context(args, process.pid): diff --git a/Lib/profiling/sampling/stack_collector.py b/Lib/profiling/sampling/stack_collector.py index 60ff5d7776b061d..7410d07c7e01624 100644 --- a/Lib/profiling/sampling/stack_collector.py +++ b/Lib/profiling/sampling/stack_collector.py @@ -162,6 +162,8 @@ def set_replay_stats(self, info): 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() @@ -566,13 +568,16 @@ def _create_flamegraph_html(self, data): 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): + 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.""" @@ -581,6 +586,32 @@ def _load_baseline(self): 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 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 index 253f419dae3c2e7..ff944b3163ec3e8 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py @@ -25,6 +25,7 @@ ) 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() @@ -151,19 +152,24 @@ def tearDown(self): if os.path.exists(f): os.unlink(f) - def create_binary_file(self, samples, interval=1000, compression="none"): + 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) + filename, _ = self.write_binary_file( + samples, interval, compression, mode, capture_config + ) return filename - def write_binary_file(self, samples, interval=1000, compression="none"): + 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 + filename, interval, compression=compression, mode=mode, + capture_config=capture_config, ) for sample in samples: collector.collect(sample) @@ -516,6 +522,37 @@ def test_sample_interval_preserved(self): 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 = [] @@ -1673,8 +1710,10 @@ def test_original_stats_extension_remains_readable(self): class TestBinaryReplayToJsonl(BinaryFormatTestBase): """Tests for binary -> JSONL replay via convert_binary_to_format.""" - def _replay_to_jsonl(self, samples, interval=1000): - bin_path = self.create_binary_file(samples, interval=interval) + 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) @@ -1704,6 +1743,16 @@ def test_binary_replay_to_jsonl_basic(self): 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") diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_cli.py b/Lib/test/test_profiling/test_sampling_profiler/test_cli.py index 3448258eca5d6c2..e5dcd72c27ef5c2 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_cli.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_cli.py @@ -23,11 +23,13 @@ requires_remote_subprocess_debugging, ) +from profiling.sampling.binary_reader import BinaryReader from profiling.sampling.cli import ( FORMAT_EXTENSIONS, _create_collector, _generate_output_filename, _handle_output, + _replay_with_reader, main, ) from profiling.sampling.constants import ( @@ -963,6 +965,41 @@ def test_cli_replay_reader_errors_exit_cleanly(self): "Error: Unsupported format version 2", ) + def test_cli_replay_propagates_recorded_mode(self): + reader = mock.MagicMock() + reader.get_info.return_value = { + "sample_interval_us": 1000, + "sample_count": 0, + "compression_type": 0, + "mode": PROFILING_MODE_CPU, + "capture_config": {"all_threads": True}, + } + reader.replay_samples.return_value = 0 + collector = mock.MagicMock() + collector.export.return_value = True + args = SimpleNamespace( + format="diff_flamegraph", + input_file="current.bin", + diff_baseline="baseline.bin", + outfile="diff.html", + browser=False, + ) + + with mock.patch( + "profiling.sampling.cli._create_collector", + return_value=collector, + ) as create_collector: + _replay_with_reader(args, reader) + + create_collector.assert_called_once_with( + "diff_flamegraph", + 1000, + skip_idle=False, + mode=PROFILING_MODE_CPU, + diff_baseline="baseline.bin", + capture_config={"all_threads": True}, + ) + def test_cli_jsonl_format_mutually_exclusive_with_pstats(self): """--jsonl and --pstats cannot be combined (mutually exclusive group).""" with ( @@ -1005,6 +1042,26 @@ def test_cli_jsonl_create_collector_propagates_mode(self): 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 = [ diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py index dcfca9a9bceb503..282f97e5259e096 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -1660,6 +1660,50 @@ def test_diff_flamegraph_changed_functions(self): 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 = [ diff --git a/Misc/NEWS.d/next/IDLE/2026-09-24-12-00-00.gh-issue-158067.opnPy1.rst b/Misc/NEWS.d/next/IDLE/2026-09-24-12-00-00.gh-issue-158067.opnPy1.rst new file mode 100644 index 000000000000000..2e34cf6a67b8274 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-09-24-12-00-00.gh-issue-158067.opnPy1.rst @@ -0,0 +1,2 @@ +Fix the IDLE File Open dialog listing text files before Python files when +opened from the Shell or an Output window. diff --git a/Misc/NEWS.d/next/Library/2026-07-19-14-00-00.gh-issue-154090.q2kH1b.rst b/Misc/NEWS.d/next/Library/2026-07-19-14-00-00.gh-issue-154090.q2kH1b.rst new file mode 100644 index 000000000000000..f2d2b370241db0b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-19-14-00-00.gh-issue-154090.q2kH1b.rst @@ -0,0 +1,2 @@ +Record the profiling mode in Tachyon binary profiles and reject differential +comparisons whose known modes do not match. diff --git a/Modules/_remote_debugging/binary_io.h b/Modules/_remote_debugging/binary_io.h index 615e3626e1cbc6f..c936d3372e5acda 100644 --- a/Modules/_remote_debugging/binary_io.h +++ b/Modules/_remote_debugging/binary_io.h @@ -59,7 +59,9 @@ extern "C" { #define HDR_SIZE_FRAME_TABLE 8 #define HDR_OFF_COMPRESSION (HDR_OFF_FRAME_TABLE + HDR_SIZE_FRAME_TABLE) #define HDR_SIZE_COMPRESSION 4 -#define FILE_HEADER_SIZE (HDR_OFF_COMPRESSION + HDR_SIZE_COMPRESSION) +#define HDR_OFF_CONFIG (HDR_OFF_COMPRESSION + HDR_SIZE_COMPRESSION) +#define HDR_SIZE_CONFIG 4 +#define FILE_HEADER_SIZE (HDR_OFF_CONFIG + HDR_SIZE_CONFIG) #define FILE_HEADER_PLACEHOLDER_SIZE 64 static_assert(FILE_HEADER_SIZE <= FILE_HEADER_PLACEHOLDER_SIZE, @@ -135,6 +137,19 @@ static_assert(PROFILE_STATS_SIZE == 56, #define COMPRESSION_NONE 0 #define COMPRESSION_ZSTD 1 +/* Profiling configuration. The mode occupies the low three bits and is + * stored plus one so zero remains compatible with files written before this + * field was defined. Capture features are valid when bit 3 is set. */ +#define PROFILING_CONFIG_MODE_MASK 0x7U +#define PROFILING_CONFIG_FEATURES_KNOWN (1U << 3) +#define PROFILING_CONFIG_FEATURES_SHIFT 4 +#define PROFILING_FEATURE_ALL_THREADS (1U << 0) +#define PROFILING_FEATURE_NATIVE (1U << 1) +#define PROFILING_FEATURE_GC (1U << 2) +#define PROFILING_FEATURE_OPCODES (1U << 3) +#define PROFILING_FEATURE_BLOCKING (1U << 4) +#define PROFILING_FEATURE_MASK 0x1FU + /* Stack encoding types for delta compression */ #define STACK_REPEAT 0x00 /* RLE: identical to previous, with count */ #define STACK_FULL 0x01 /* Full stack (first sample or no match) */ @@ -298,6 +313,8 @@ typedef struct { double missed_samples; uint32_t profile_stats_present; int has_profile_stats; + int profiling_mode; + int capture_features; /* String hash table: PyObject* -> uint32_t index */ _Py_hashtable_t *string_hash; @@ -373,6 +390,8 @@ typedef struct { int has_profile_stats; uint64_t string_table_offset; uint64_t frame_table_offset; + int profiling_mode; + int capture_features; /* Parsed string table: array of Python string objects */ PyObject **strings; @@ -559,6 +578,8 @@ grow_array_inplace(void **ptr_addr, size_t count, size_t *capacity, size_t elem_ * sample_interval_us: Sampling interval in microseconds * compression_type: COMPRESSION_NONE or COMPRESSION_ZSTD * start_time_us: Start timestamp in microseconds (from time.monotonic() * 1e6) + * profiling_mode: PROFILING_MODE_* value, or -1 if unknown + * capture_features: PROFILING_FEATURE_* bit mask, or -1 if unknown * * Returns: * New BinaryWriter* on success, NULL on failure (PyErr set) @@ -567,7 +588,9 @@ BinaryWriter *binary_writer_create( PyObject *path, uint64_t sample_interval_us, int compression_type, - uint64_t start_time_us + uint64_t start_time_us, + int profiling_mode, + int capture_features ); /* diff --git a/Modules/_remote_debugging/binary_io_reader.c b/Modules/_remote_debugging/binary_io_reader.c index 4e5ccaaabc72700..9625ee6f301f05f 100644 --- a/Modules/_remote_debugging/binary_io_reader.c +++ b/Modules/_remote_debugging/binary_io_reader.c @@ -85,7 +85,7 @@ reader_parse_header(BinaryReader *reader, const uint8_t *data, size_t file_size) /* Read header fields with byte-swapping if needed */ uint64_t start_time_us, sample_interval_us, string_table_offset, frame_table_offset; uint64_t sample_count; - uint32_t thread_count, compression_type; + uint32_t thread_count, compression_type, profiling_config; memcpy(&start_time_us, &data[HDR_OFF_START_TIME], HDR_SIZE_START_TIME); memcpy(&sample_interval_us, &data[HDR_OFF_INTERVAL], HDR_SIZE_INTERVAL); @@ -94,6 +94,7 @@ reader_parse_header(BinaryReader *reader, const uint8_t *data, size_t file_size) memcpy(&string_table_offset, &data[HDR_OFF_STR_TABLE], HDR_SIZE_STR_TABLE); memcpy(&frame_table_offset, &data[HDR_OFF_FRAME_TABLE], HDR_SIZE_FRAME_TABLE); memcpy(&compression_type, &data[HDR_OFF_COMPRESSION], HDR_SIZE_COMPRESSION); + memcpy(&profiling_config, &data[HDR_OFF_CONFIG], HDR_SIZE_CONFIG); reader->start_time_us = SWAP64_IF(reader->needs_swap, start_time_us); reader->sample_interval_us = SWAP64_IF(reader->needs_swap, sample_interval_us); @@ -102,6 +103,20 @@ reader_parse_header(BinaryReader *reader, const uint8_t *data, size_t file_size) reader->string_table_offset = SWAP64_IF(reader->needs_swap, string_table_offset); reader->frame_table_offset = SWAP64_IF(reader->needs_swap, frame_table_offset); reader->compression_type = (int)SWAP32_IF(reader->needs_swap, compression_type); + profiling_config = SWAP32_IF(reader->needs_swap, profiling_config); + uint32_t profiling_mode = + profiling_config & PROFILING_CONFIG_MODE_MASK; + if (profiling_mode > PROFILING_MODE_EXCEPTION + 1) { + PyErr_Format(PyExc_ValueError, + "Invalid profiling mode in header: %u", profiling_mode); + return -1; + } + reader->profiling_mode = (int)profiling_mode - 1; + reader->capture_features = + profiling_config & PROFILING_CONFIG_FEATURES_KNOWN + ? (int)((profiling_config >> PROFILING_CONFIG_FEATURES_SHIFT) & + PROFILING_FEATURE_MASK) + : -1; return 0; } @@ -1388,8 +1403,31 @@ binary_reader_get_info(BinaryReader *reader) Py_DECREF(error_rate); return NULL; } + PyObject *profiling_mode = reader->profiling_mode < 0 + ? Py_NewRef(Py_None) + : PyLong_FromLong(reader->profiling_mode); + if (profiling_mode == NULL) { + Py_DECREF(py_version); + Py_DECREF(duration); + Py_DECREF(sample_rate); + Py_DECREF(error_rate); + Py_DECREF(missed_samples); + return NULL; + } + PyObject *capture_features = reader->capture_features < 0 + ? Py_NewRef(Py_None) + : PyLong_FromLong(reader->capture_features); + if (capture_features == NULL) { + Py_DECREF(py_version); + Py_DECREF(profiling_mode); + Py_DECREF(duration); + Py_DECREF(sample_rate); + Py_DECREF(error_rate); + Py_DECREF(missed_samples); + return NULL; + } return Py_BuildValue( - "{s:I, s:N, s:K, s:K, s:K, s:I, s:I, s:I, s:i, s:N, s:N, s:N, s:N}", + "{s:I, s:N, s:K, s:K, s:K, s:I, s:I, s:I, s:i, s:N, s:N, s:N, s:N, s:N, s:N}", "version", BINARY_FORMAT_VERSION, "python_version", py_version, "start_time_us", reader->start_time_us, @@ -1402,7 +1440,9 @@ binary_reader_get_info(BinaryReader *reader) "duration_sec", duration, "sample_rate", sample_rate, "error_rate", error_rate, - "missed_samples", missed_samples + "missed_samples", missed_samples, + "mode", profiling_mode, + "capture_features", capture_features ); } diff --git a/Modules/_remote_debugging/binary_io_writer.c b/Modules/_remote_debugging/binary_io_writer.c index 753d0b0cc966d08..6af81515e7131d1 100644 --- a/Modules/_remote_debugging/binary_io_writer.c +++ b/Modules/_remote_debugging/binary_io_writer.c @@ -728,7 +728,8 @@ write_sample_with_encoding(BinaryWriter *writer, ThreadEntry *entry, BinaryWriter * binary_writer_create(PyObject *path, uint64_t sample_interval_us, int compression_type, - uint64_t start_time_us) + uint64_t start_time_us, int profiling_mode, + int capture_features) { BinaryWriter *writer = PyMem_Calloc(1, sizeof(BinaryWriter)); if (!writer) { @@ -739,6 +740,8 @@ binary_writer_create(PyObject *path, uint64_t sample_interval_us, int compressio writer->start_time_us = start_time_us; writer->sample_interval_us = sample_interval_us; writer->compression_type = compression_type; + writer->profiling_mode = profiling_mode; + writer->capture_features = capture_features; writer->write_buffer = PyMem_Malloc(WRITE_BUFFER_SIZE); if (!writer->write_buffer) { @@ -1202,6 +1205,12 @@ binary_writer_finalize(BinaryWriter *writer) uint64_t frame_table_offset_u64 = (uint64_t)frame_table_offset; uint32_t thread_count_u32 = (uint32_t)writer->thread_count; uint32_t compression_type_u32 = (uint32_t)writer->compression_type; + uint32_t profiling_config_u32 = (uint32_t)(writer->profiling_mode + 1); + if (writer->capture_features >= 0) { + profiling_config_u32 |= PROFILING_CONFIG_FEATURES_KNOWN; + profiling_config_u32 |= (uint32_t)writer->capture_features + << PROFILING_CONFIG_FEATURES_SHIFT; + } uint8_t header[FILE_HEADER_SIZE] = {0}; uint32_t magic = BINARY_FORMAT_MAGIC; @@ -1218,6 +1227,7 @@ binary_writer_finalize(BinaryWriter *writer) memcpy(header + HDR_OFF_STR_TABLE, &string_table_offset_u64, HDR_SIZE_STR_TABLE); memcpy(header + HDR_OFF_FRAME_TABLE, &frame_table_offset_u64, HDR_SIZE_FRAME_TABLE); memcpy(header + HDR_OFF_COMPRESSION, &compression_type_u32, HDR_SIZE_COMPRESSION); + memcpy(header + HDR_OFF_CONFIG, &profiling_config_u32, HDR_SIZE_CONFIG); if (fwrite_checked_allow_threads(header, FILE_HEADER_SIZE, writer->fp) < 0) { return -1; } diff --git a/Modules/_remote_debugging/clinic/module.c.h b/Modules/_remote_debugging/clinic/module.c.h index efbf529922b33df..4d63beb91515f84 100644 --- a/Modules/_remote_debugging/clinic/module.c.h +++ b/Modules/_remote_debugging/clinic/module.c.h @@ -717,7 +717,7 @@ _remote_debugging_GCMonitor_get_gc_stats(PyObject *self, PyObject *const *args, PyDoc_STRVAR(_remote_debugging_BinaryWriter___init____doc__, "BinaryWriter(filename, sample_interval_us, start_time_us, *,\n" -" compression=0)\n" +" compression=0, mode=-1, capture_features=-1)\n" "--\n" "\n" "High-performance binary writer for profiling data.\n" @@ -728,6 +728,9 @@ PyDoc_STRVAR(_remote_debugging_BinaryWriter___init____doc__, " start_time_us: Start timestamp in microseconds (from\n" " time.monotonic() * 1e6)\n" " compression: 0=none, 1=zstd (default: 0)\n" +" mode: Profiling mode, or -1 if unknown (default: -1)\n" +" capture_features: Capture feature bit mask, or -1 if unknown\n" +" (default: -1)\n" "\n" "Use as a context manager or call finalize() when done."); @@ -736,7 +739,8 @@ _remote_debugging_BinaryWriter___init___impl(BinaryWriterObject *self, PyObject *filename, unsigned long long sample_interval_us, unsigned long long start_time_us, - int compression); + int compression, int mode, + int capture_features); static int _remote_debugging_BinaryWriter___init__(PyObject *self, PyObject *args, PyObject *kwargs) @@ -744,7 +748,7 @@ _remote_debugging_BinaryWriter___init__(PyObject *self, PyObject *args, PyObject int return_value = -1; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 4 + #define NUM_KEYWORDS 6 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -753,7 +757,7 @@ _remote_debugging_BinaryWriter___init__(PyObject *self, PyObject *args, PyObject } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(filename), &_Py_ID(sample_interval_us), &_Py_ID(start_time_us), &_Py_ID(compression), }, + .ob_item = { &_Py_ID(filename), &_Py_ID(sample_interval_us), &_Py_ID(start_time_us), &_Py_ID(compression), &_Py_ID(mode), &_Py_ID(capture_features), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -762,14 +766,14 @@ _remote_debugging_BinaryWriter___init__(PyObject *self, PyObject *args, PyObject # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"filename", "sample_interval_us", "start_time_us", "compression", NULL}; + static const char * const _keywords[] = {"filename", "sample_interval_us", "start_time_us", "compression", "mode", "capture_features", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "BinaryWriter", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[4]; + PyObject *argsbuf[6]; PyObject * const *fastargs; Py_ssize_t nargs = PyTuple_GET_SIZE(args); Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 3; @@ -777,6 +781,8 @@ _remote_debugging_BinaryWriter___init__(PyObject *self, PyObject *args, PyObject unsigned long long sample_interval_us; unsigned long long start_time_us; int compression = 0; + int mode = -1; + int capture_features = -1; fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, /*minpos*/ 3, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf); @@ -793,12 +799,30 @@ _remote_debugging_BinaryWriter___init__(PyObject *self, PyObject *args, PyObject if (!noptargs) { goto skip_optional_kwonly; } - compression = PyLong_AsInt(fastargs[3]); - if (compression == -1 && PyErr_Occurred()) { + if (fastargs[3]) { + compression = PyLong_AsInt(fastargs[3]); + if (compression == -1 && PyErr_Occurred()) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (fastargs[4]) { + mode = PyLong_AsInt(fastargs[4]); + if (mode == -1 && PyErr_Occurred()) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + capture_features = PyLong_AsInt(fastargs[5]); + if (capture_features == -1 && PyErr_Occurred()) { goto exit; } skip_optional_kwonly: - return_value = _remote_debugging_BinaryWriter___init___impl((BinaryWriterObject *)self, filename, sample_interval_us, start_time_us, compression); + return_value = _remote_debugging_BinaryWriter___init___impl((BinaryWriterObject *)self, filename, sample_interval_us, start_time_us, compression, mode, capture_features); exit: return return_value; @@ -1685,4 +1709,4 @@ _remote_debugging_get_gc_stats(PyObject *module, PyObject *const *args, Py_ssize exit: return return_value; } -/*[clinic end generated code: output=5f20a08be0d0ed5a input=a9049054013a1b77]*/ +/*[clinic end generated code: output=8ad64b51aaffc3ad input=a9049054013a1b77]*/ diff --git a/Modules/_remote_debugging/module.c b/Modules/_remote_debugging/module.c index 708632bf603ddca..8513bf0e4e65a57 100644 --- a/Modules/_remote_debugging/module.c +++ b/Modules/_remote_debugging/module.c @@ -1717,6 +1717,8 @@ _remote_debugging.BinaryWriter.__init__ start_time_us: unsigned_long_long * compression: int = 0 + mode: int = -1 + capture_features: int = -1 High-performance binary writer for profiling data. @@ -1726,6 +1728,9 @@ High-performance binary writer for profiling data. start_time_us: Start timestamp in microseconds (from time.monotonic() * 1e6) compression: 0=none, 1=zstd (default: 0) + mode: Profiling mode, or -1 if unknown (default: -1) + capture_features: Capture feature bit mask, or -1 if unknown + (default: -1) Use as a context manager or call finalize() when done. [clinic start generated code]*/ @@ -1735,14 +1740,26 @@ _remote_debugging_BinaryWriter___init___impl(BinaryWriterObject *self, PyObject *filename, unsigned long long sample_interval_us, unsigned long long start_time_us, - int compression) -/*[clinic end generated code: output=00446656ea2e5986 input=2e3f298c69fc7666]*/ + int compression, int mode, + int capture_features) +/*[clinic end generated code: output=3c1c9576795658ce input=98add735b20403ad]*/ { + if (mode < -1 || mode > PROFILING_MODE_EXCEPTION) { + PyErr_SetString(PyExc_ValueError, "invalid profiling mode"); + return -1; + } + if (capture_features < -1 || + capture_features > (int)PROFILING_FEATURE_MASK) { + PyErr_SetString(PyExc_ValueError, "invalid capture features"); + return -1; + } if (self->writer) { binary_writer_destroy(self->writer); } - self->writer = binary_writer_create(filename, sample_interval_us, compression, start_time_us); + self->writer = binary_writer_create( + filename, sample_interval_us, compression, start_time_us, mode, + capture_features); if (!self->writer) { return -1; } diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index a79ea5728e72d64..058d16f14f290ea 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -85,8 +85,9 @@ static PF_SET_THREAD_DESCRIPTION pSetThreadDescription = NULL; module _thread class _thread.lock "lockobject *" "clinic_state()->lock_type" class _thread.RLock "rlockobject *" "clinic_state()->rlock_type" +class _thread._ThreadHandle "PyObject *" "clinic_state()->thread_handle_type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=c5a0f8c492a0c263]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=bd4eae07ae18bc2c]*/ #define clinic_state() get_thread_state_by_cls(type) #include "clinic/_threadmodule.c.h" @@ -678,43 +679,57 @@ PyThreadHandleObject_repr(PyObject *op) Py_TYPE(self)->tp_name, ident); } +/*[clinic input] +@getter +_thread._ThreadHandle.ident +[clinic start generated code]*/ + static PyObject * -PyThreadHandleObject_get_ident(PyObject *op, void *Py_UNUSED(closure)) +_thread__ThreadHandle_ident_get_impl(PyObject *self) +/*[clinic end generated code: output=66d40aa1a624ac23 input=2b3361e34a6ca79d]*/ { - PyThreadHandleObject *self = PyThreadHandleObject_CAST(op); - return PyLong_FromUnsignedLongLong(ThreadHandle_ident(self->handle)); + PyThreadHandleObject *handle = PyThreadHandleObject_CAST(self); + return PyLong_FromUnsignedLongLong(ThreadHandle_ident(handle->handle)); } +/*[clinic input] +_thread._ThreadHandle.join + timeout as timeout_obj: object = None + / +[clinic start generated code]*/ + static PyObject * -PyThreadHandleObject_join(PyObject *op, PyObject *args) +_thread__ThreadHandle_join_impl(PyObject *self, PyObject *timeout_obj) +/*[clinic end generated code: output=d3a4f0b20c21442e input=b66d17a0f77d6e9c]*/ { - PyThreadHandleObject *self = PyThreadHandleObject_CAST(op); - - PyObject *timeout_obj = NULL; - if (!PyArg_ParseTuple(args, "|O:join", &timeout_obj)) { - return NULL; - } + PyThreadHandleObject *handle = PyThreadHandleObject_CAST(self); PyTime_t timeout_ns = -1; - if (timeout_obj != NULL && timeout_obj != Py_None) { + if (timeout_obj != Py_None) { if (_PyTime_FromSecondsObject(&timeout_ns, timeout_obj, _PyTime_ROUND_TIMEOUT) < 0) { return NULL; } } - if (ThreadHandle_join(self->handle, timeout_ns) < 0) { + if (ThreadHandle_join(handle->handle, timeout_ns) < 0) { return NULL; } Py_RETURN_NONE; } +/*[clinic input] +_thread._ThreadHandle.is_done +[clinic start generated code]*/ + static PyObject * -PyThreadHandleObject_is_done(PyObject *op, PyObject *Py_UNUSED(dummy)) +_thread__ThreadHandle_is_done_impl(PyObject *self) +/*[clinic end generated code: output=f5c00923fb61967b input=dd905a1ac7fc2e7c]*/ { - PyThreadHandleObject *self = PyThreadHandleObject_CAST(op); - if (_PyEvent_IsSet(&self->handle->thread_is_exiting)) { - if (_PyOnceFlag_CallOnce(&self->handle->once, join_thread, self->handle) == -1) { + PyThreadHandleObject *handle = PyThreadHandleObject_CAST(self); + if (_PyEvent_IsSet(&handle->handle->thread_is_exiting)) { + if (_PyOnceFlag_CallOnce(&handle->handle->once, join_thread, + handle->handle) == -1) { return NULL; } Py_RETURN_TRUE; @@ -724,25 +739,30 @@ PyThreadHandleObject_is_done(PyObject *op, PyObject *Py_UNUSED(dummy)) } } +/*[clinic input] +_thread._ThreadHandle._set_done +[clinic start generated code]*/ + static PyObject * -PyThreadHandleObject_set_done(PyObject *op, PyObject *Py_UNUSED(dummy)) +_thread__ThreadHandle__set_done_impl(PyObject *self) +/*[clinic end generated code: output=709b4d7ca318bb6d input=9f0115db8d84806b]*/ { - PyThreadHandleObject *self = PyThreadHandleObject_CAST(op); - if (ThreadHandle_set_done(self->handle) < 0) { + PyThreadHandleObject *handle = PyThreadHandleObject_CAST(self); + if (ThreadHandle_set_done(handle->handle) < 0) { return NULL; } Py_RETURN_NONE; } static PyGetSetDef ThreadHandle_getsetlist[] = { - {"ident", PyThreadHandleObject_get_ident, NULL, NULL}, + _THREAD__THREADHANDLE_IDENT_GETSETDEF {0}, }; static PyMethodDef ThreadHandle_methods[] = { - {"join", PyThreadHandleObject_join, METH_VARARGS, NULL}, - {"_set_done", PyThreadHandleObject_set_done, METH_NOARGS, NULL}, - {"is_done", PyThreadHandleObject_is_done, METH_NOARGS, NULL}, + _THREAD__THREADHANDLE_JOIN_METHODDEF + _THREAD__THREADHANDLE__SET_DONE_METHODDEF + _THREAD__THREADHANDLE_IS_DONE_METHODDEF {0, 0} }; @@ -1875,8 +1895,17 @@ clear_locals(PyObject *locals_and_key, PyObject *dummyweakref) /* Module functions */ +/*[clinic input] +_thread.daemon_threads_allowed + +Return True if daemon threads are allowed in the current interpreter. + +Return False otherwise. +[clinic start generated code]*/ + static PyObject * -thread_daemon_threads_allowed(PyObject *module, PyObject *Py_UNUSED(ignored)) +_thread_daemon_threads_allowed_impl(PyObject *module) +/*[clinic end generated code: output=c82b3b9490c7e11e input=f94150f9e14b7d0c]*/ { PyInterpreterState *interp = _PyInterpreterState_GET(); if (interp->feature_flags & Py_RTFLAGS_DAEMON_THREADS) { @@ -1887,13 +1916,6 @@ thread_daemon_threads_allowed(PyObject *module, PyObject *Py_UNUSED(ignored)) } } -PyDoc_STRVAR(daemon_threads_allowed_doc, -"daemon_threads_allowed($module, /)\n\ ---\n\ -\n\ -Return True if daemon threads are allowed in the current interpreter,\n\ -and False otherwise.\n"); - static int do_start_new_thread(thread_module_state *state, PyObject *func, PyObject *args, PyObject *kwargs, ThreadHandle *handle, int daemon) @@ -1927,31 +1949,36 @@ do_start_new_thread(thread_module_state *state, PyObject *func, PyObject *args, return 0; } +/*[clinic input] +_thread.start_new_thread + + function as func: object + args: object(subclass_of='&PyTuple_Type') + kwargs: object(subclass_of='&PyDict_Type', c_default="NULL") = {} + / + +Start a new thread and return its identifier. + +The thread will call the function with positional arguments from the +tuple args and keyword arguments taken from the optional dictionary +kwargs. The thread exits when the function returns; the return value +is ignored. The thread will also exit when the function raises an +unhandled exception; a stack trace will be printed unless the exception +is SystemExit. +[clinic start generated code]*/ + static PyObject * -thread_PyThread_start_new_thread(PyObject *module, PyObject *fargs) +_thread_start_new_thread_impl(PyObject *module, PyObject *func, + PyObject *args, PyObject *kwargs) +/*[clinic end generated code: output=0c27eb61b7a7152b input=b85f6b3301d30719]*/ { - PyObject *func, *args, *kwargs = NULL; thread_module_state *state = get_thread_state(module); - if (!PyArg_UnpackTuple(fargs, "start_new_thread", 2, 3, - &func, &args, &kwargs)) - return NULL; if (!PyCallable_Check(func)) { PyErr_SetString(PyExc_TypeError, "first arg must be callable"); return NULL; } - if (!PyTuple_Check(args)) { - PyErr_SetString(PyExc_TypeError, - "2nd arg must be a tuple"); - return NULL; - } - if (kwargs != NULL && !PyDict_Check(kwargs)) { - PyErr_SetString(PyExc_TypeError, - "optional 3rd arg must be a dictionary"); - return NULL; - } - if (PySys_Audit("_thread.start_new_thread", "OOO", func, args, kwargs ? kwargs : Py_None) < 0) { return NULL; @@ -1973,39 +2000,36 @@ thread_PyThread_start_new_thread(PyObject *module, PyObject *fargs) return PyLong_FromUnsignedLongLong(ident); } -PyDoc_STRVAR(start_new_thread_doc, -"start_new_thread($module, function, args, kwargs={}, /)\n\ ---\n\ -\n\ -Start a new thread and return its identifier.\n\ -\n\ -The thread will call the function with positional arguments from the\n\ -tuple args and keyword arguments taken from the optional dictionary\n\ -kwargs. The thread exits when the function returns; the return value\n\ -is ignored. The thread will also exit when the function raises an\n\ -unhandled exception; a stack trace will be printed unless the exception\n\ -is SystemExit."); - PyDoc_STRVAR(start_new_doc, "start_new($module, function, args, kwargs={}, /)\n\ --\n\ \n\ An obsolete synonym of start_new_thread()."); +/*[clinic input] +_thread.start_joinable_thread + + function as func: object + handle as hobj: object = None + daemon: bool = True + +*For internal use only*: start a new thread. + +Like start_new_thread(), this starts a new thread calling the given +function. Unlike start_new_thread(), this returns a handle object with +methods to join or detach the given thread. +This function is not for third-party code, please use the `threading` +module instead. During finalization the runtime will not wait for the +thread to exit if daemon is True. If handle is provided it must be a +newly created thread._ThreadHandle instance. +[clinic start generated code]*/ + static PyObject * -thread_PyThread_start_joinable_thread(PyObject *module, PyObject *fargs, - PyObject *fkwargs) +_thread_start_joinable_thread_impl(PyObject *module, PyObject *func, + PyObject *hobj, int daemon) +/*[clinic end generated code: output=58691769e60620cf input=e17b3aebc087952a]*/ { - static char *keywords[] = {"function", "handle", "daemon", NULL}; - PyObject *func = NULL; - int daemon = 1; thread_module_state *state = get_thread_state(module); - PyObject *hobj = NULL; - if (!PyArg_ParseTupleAndKeywords(fargs, fkwargs, - "O|Op:start_joinable_thread", keywords, - &func, &hobj, &daemon)) { - return NULL; - } if (!PyCallable_Check(func)) { PyErr_SetString(PyExc_TypeError, @@ -2013,10 +2037,7 @@ thread_PyThread_start_joinable_thread(PyObject *module, PyObject *fargs, return NULL; } - if (hobj == NULL) { - hobj = Py_None; - } - else if (hobj != Py_None && !Py_IS_TYPE(hobj, state->thread_handle_type)) { + if (hobj != Py_None && !Py_IS_TYPE(hobj, state->thread_handle_type)) { PyErr_SetString(PyExc_TypeError, "'handle' must be a _ThreadHandle"); return NULL; } @@ -2050,48 +2071,49 @@ thread_PyThread_start_joinable_thread(PyObject *module, PyObject *fargs, return (PyObject *) hobj; } -PyDoc_STRVAR(start_joinable_doc, -"start_joinable_thread($module, /, function, handle=None, daemon=True)\n\ ---\n\ -\n\ -*For internal use only*: start a new thread.\n\ -\n\ -Like start_new_thread(), this starts a new thread calling the given function.\n\ -Unlike start_new_thread(), this returns a handle object with methods to join\n\ -or detach the given thread.\n\ -This function is not for third-party code, please use the\n\ -`threading` module instead. During finalization the runtime will not wait for\n\ -the thread to exit if daemon is True. If handle is provided it must be a\n\ -newly created thread._ThreadHandle instance."); +/*[clinic input] +_thread.exit + +Raise SystemExit. + +It will cause the current thread to exit silently unless the exception +is caught. +[clinic start generated code]*/ static PyObject * -thread_PyThread_exit_thread(PyObject *self, PyObject *Py_UNUSED(ignored)) +_thread_exit_impl(PyObject *module) +/*[clinic end generated code: output=b03924346e743f4c input=1f3dc5da8ad48c70]*/ { PyErr_SetNone(PyExc_SystemExit); return NULL; } -PyDoc_STRVAR(exit_doc, -"exit($module, /)\n\ ---\n\ -\n\ -This is synonymous to ``raise SystemExit''. It will cause the current\n\ -thread to exit silently unless the exception is caught."); - PyDoc_STRVAR(exit_thread_doc, "exit_thread($module, /)\n\ --\n\ \n\ An obsolete synonym of exit()."); +/*[clinic input] +_thread.interrupt_main + + signum: int(c_default="SIGINT") = signal.SIGINT + / + +Simulate the arrival of the given signal in the main thread. + +The corresponding signal handler will be executed. +If *signum* is omitted, SIGINT is assumed. +A subthread can use this function to interrupt the main thread. + +Note: the default signal handler for SIGINT raises +``KeyboardInterrupt``. +[clinic start generated code]*/ + static PyObject * -thread_PyThread_interrupt_main(PyObject *self, PyObject *args) +_thread_interrupt_main_impl(PyObject *module, int signum) +/*[clinic end generated code: output=fdafa7261ceb26e9 input=a8cf392f3a7f2a84]*/ { - int signum = SIGINT; - if (!PyArg_ParseTuple(args, "|i:signum", &signum)) { - return NULL; - } - if (PyErr_SetInterruptEx(signum)) { PyErr_SetString(PyExc_ValueError, "signal number out of range"); return NULL; @@ -2099,40 +2121,44 @@ thread_PyThread_interrupt_main(PyObject *self, PyObject *args) Py_RETURN_NONE; } -PyDoc_STRVAR(interrupt_doc, -"interrupt_main($module, signum=signal.SIGINT, /)\n\ ---\n\ -\n\ -Simulate the arrival of the given signal in the main thread,\n\ -where the corresponding signal handler will be executed.\n\ -If *signum* is omitted, SIGINT is assumed.\n\ -A subthread can use this function to interrupt the main thread.\n\ -\n\ -Note: the default signal handler for SIGINT raises ``KeyboardInterrupt``." -); +/*[clinic input] +_thread.allocate_lock + +Create a new lock object. + +See help(type(threading.Lock())) for information about locks. +[clinic start generated code]*/ static PyObject * -thread_PyThread_allocate_lock(PyObject *module, PyObject *Py_UNUSED(ignored)) +_thread_allocate_lock_impl(PyObject *module) +/*[clinic end generated code: output=884a4be6cb9ca731 input=49f5b1f966361bb6]*/ { thread_module_state *state = get_thread_state(module); return lock_new_impl(state->lock_type); } -PyDoc_STRVAR(allocate_lock_doc, -"allocate_lock($module, /)\n\ ---\n\ -\n\ -Create a new lock object. See help(type(threading.Lock())) for\n\ -information about locks."); - PyDoc_STRVAR(allocate_doc, "allocate($module, /)\n\ --\n\ \n\ An obsolete synonym of allocate_lock()."); +/*[clinic input] +_thread.get_ident + +Return a non-zero integer that uniquely identifies the current thread. + +It is unique amongst other threads that exist simultaneously. +This may be used to identify per-thread resources. +Even though on some platforms threads identities may appear to be +allocated consecutive numbers starting at 1, this behavior should not +be relied upon, and the number should be seen purely as a magic cookie. +A thread's identity may be reused for another thread after it exits. +[clinic start generated code]*/ + static PyObject * -thread_get_ident(PyObject *self, PyObject *Py_UNUSED(ignored)) +_thread_get_ident_impl(PyObject *module) +/*[clinic end generated code: output=8c3f9d8aa938b332 input=08faef37d1fd9e67]*/ { PyThread_ident_t ident = PyThread_get_thread_ident_ex(); if (ident == PYTHREAD_INVALID_THREAD_ID) { @@ -2142,64 +2168,79 @@ thread_get_ident(PyObject *self, PyObject *Py_UNUSED(ignored)) return PyLong_FromUnsignedLongLong(ident); } -PyDoc_STRVAR(get_ident_doc, -"get_ident($module, /)\n\ ---\n\ -\n\ -Return a non-zero integer that uniquely identifies the current thread\n\ -amongst other threads that exist simultaneously.\n\ -This may be used to identify per-thread resources.\n\ -Even though on some platforms threads identities may appear to be\n\ -allocated consecutive numbers starting at 1, this behavior should not\n\ -be relied upon, and the number should be seen purely as a magic cookie.\n\ -A thread's identity may be reused for another thread after it exits."); - #ifdef PY_HAVE_THREAD_NATIVE_ID +/*[clinic input] +_thread.get_native_id + +Return a non-negative integer identifying the thread. + +It is reported by the OS (kernel). This may be used to uniquely +identify a particular thread within a system. +[clinic start generated code]*/ + static PyObject * -thread_get_native_id(PyObject *self, PyObject *Py_UNUSED(ignored)) +_thread_get_native_id_impl(PyObject *module) +/*[clinic end generated code: output=d69767224b4ba922 input=454af91013e4f747]*/ { unsigned long native_id = PyThread_get_thread_native_id(); return PyLong_FromUnsignedLong(native_id); } -PyDoc_STRVAR(get_native_id_doc, -"get_native_id($module, /)\n\ ---\n\ -\n\ -Return a non-negative integer identifying the thread as reported\n\ -by the OS (kernel). This may be used to uniquely identify a\n\ -particular thread within a system."); #endif +/*[clinic input] +_thread._count + +Return the number of currently running Python threads. + +The main thread is excluded. The returned number comprises all threads +created through `start_new_thread()` as well as `threading.Thread`, and +not yet finished. + +This function is meant for internal and specialized purposes only. +In most applications `threading.enumerate()` should be used instead. +[clinic start generated code]*/ + static PyObject * -thread__count(PyObject *self, PyObject *Py_UNUSED(ignored)) +_thread__count_impl(PyObject *module) +/*[clinic end generated code: output=4cd03377c3e8521d input=bbd340043d174d48]*/ { PyInterpreterState *interp = _PyInterpreterState_GET(); return PyLong_FromSsize_t(_Py_atomic_load_ssize(&interp->threads.count)); } -PyDoc_STRVAR(_count_doc, -"_count($module, /)\n\ ---\n\ -\n\ -Return the number of currently running Python threads, excluding\n\ -the main thread. The returned number comprises all threads created\n\ -through `start_new_thread()` as well as `threading.Thread`, and not\n\ -yet finished.\n\ -\n\ -This function is meant for internal and specialized purposes only.\n\ -In most applications `threading.enumerate()` should be used instead."); +/*[clinic input] +_thread.stack_size + + size as new_size: Py_ssize_t = 0 + / + +Return the thread stack size used when creating new threads. + +The optional size argument specifies the stack size (in bytes) to be +used for subsequently created threads, and must be 0 (use platform or +configured default) or a positive integer value of at least 32,768 (32 +KiB). If changing the thread stack size is unsupported, a ThreadError +exception is raised. If the specified size is invalid, a ValueError +exception is raised, and the stack size is unmodified. 32 KiB +currently is the minimum supported stack size value to guarantee +sufficient stack space for the interpreter itself. + +Note that some systems may have particular restrictions on values for +the stack size, such as requiring a minimum stack size larger than 32 +KiB or requiring allocation in multiples of the system memory page size +- platform documentation should be referred to for more information +(4 KiB pages are common; using multiples of 4096 for the stack size is +the suggested approach in the absence of more specific information). +[clinic start generated code]*/ static PyObject * -thread_stack_size(PyObject *self, PyObject *args) +_thread_stack_size_impl(PyObject *module, Py_ssize_t new_size) +/*[clinic end generated code: output=cc7ba71bd35afc30 input=3249feb8ed2350f8]*/ { size_t old_size; - Py_ssize_t new_size = 0; int rc; - if (!PyArg_ParseTuple(args, "|n:stack_size", &new_size)) - return NULL; - Py_ssize_t min_size = _PyOS_MIN_STACK_SIZE + SYSTEM_PAGE_SIZE; if (new_size != 0 && new_size < min_size) { PyErr_Format(PyExc_ValueError, @@ -2225,27 +2266,6 @@ thread_stack_size(PyObject *self, PyObject *args) return PyLong_FromSsize_t((Py_ssize_t) old_size); } -PyDoc_STRVAR(stack_size_doc, -"stack_size($module, size=0, /)\n\ ---\n\ -\n\ -Return the thread stack size used when creating new threads. The\n\ -optional size argument specifies the stack size (in bytes) to be used\n\ -for subsequently created threads, and must be 0 (use platform or\n\ -configured default) or a positive integer value of at least 32,768 (32k).\n\ -If changing the thread stack size is unsupported, a ThreadError\n\ -exception is raised. If the specified size is invalid, a ValueError\n\ -exception is raised, and the stack size is unmodified. 32k bytes\n\ - currently the minimum supported stack size value to guarantee\n\ -sufficient stack space for the interpreter itself.\n\ -\n\ -Note that some platforms may have particular restrictions on values for\n\ -the stack size, such as requiring a minimum stack size larger than 32 KiB or\n\ -requiring allocation in multiples of the system memory page size\n\ -- platform documentation should be referred to for more information\n\ -(4 KiB pages are common; using multiples of 4096 for the stack size is\n\ -the suggested approach in the absence of more specific information)."); - static int thread_excepthook_file(PyObject *file, PyObject *exc_type, PyObject *exc_value, PyObject *exc_traceback, PyObject *thread) @@ -2324,8 +2344,18 @@ static PyStructSequence_Desc ExceptHookArgs_desc = { }; +/*[clinic input] +_thread._excepthook + + args: object + / + +Handle uncaught Thread.run() exception. +[clinic start generated code]*/ + static PyObject * -thread_excepthook(PyObject *module, PyObject *args) +_thread__excepthook(PyObject *module, PyObject *args) +/*[clinic end generated code: output=cadc54a3be0e6007 input=5ae2316c400a3497]*/ { thread_module_state *state = get_thread_state(module); @@ -2381,30 +2411,32 @@ thread_excepthook(PyObject *module, PyObject *args) Py_RETURN_NONE; } -PyDoc_STRVAR(excepthook_doc, -"_excepthook($module, args, /)\n\ ---\n\ -\n\ -Handle uncaught Thread.run() exception."); +/*[clinic input] +_thread._is_main_interpreter + +Return True if the current interpreter is the main Python interpreter. +[clinic start generated code]*/ static PyObject * -thread__is_main_interpreter(PyObject *module, PyObject *Py_UNUSED(ignored)) +_thread__is_main_interpreter_impl(PyObject *module) +/*[clinic end generated code: output=7dd82e1728339adc input=cc1eb00fd4598915]*/ { PyInterpreterState *interp = _PyInterpreterState_GET(); return PyBool_FromLong(_Py_IsMainInterpreter(interp)); } -PyDoc_STRVAR(thread__is_main_interpreter_doc, -"_is_main_interpreter($module, /)\n\ ---\n\ -\n\ -Return True if the current interpreter is the main Python interpreter."); +/*[clinic input] +_thread._shutdown + +Wait for all non-daemon threads (other than the calling thread) to stop. +[clinic start generated code]*/ static PyObject * -thread_shutdown(PyObject *self, PyObject *args) +_thread__shutdown_impl(PyObject *module) +/*[clinic end generated code: output=a19afe92bddc1824 input=bcaeaceb3a6fa590]*/ { PyThread_ident_t ident = PyThread_get_thread_ident_ex(); - thread_module_state *state = get_thread_state(self); + thread_module_state *state = get_thread_state(module); for (;;) { ThreadHandle *handle = NULL; @@ -2440,14 +2472,21 @@ thread_shutdown(PyObject *self, PyObject *args) Py_RETURN_NONE; } -PyDoc_STRVAR(shutdown_doc, -"_shutdown($module, /)\n\ ---\n\ -\n\ -Wait for all non-daemon threads (other than the calling thread) to stop."); +/*[clinic input] +_thread._make_thread_handle + + ident as identobj: object + / + +Internal only. + +Make a thread handle for threads not spawned by the _thread or +threading module. +[clinic start generated code]*/ static PyObject * -thread__make_thread_handle(PyObject *module, PyObject *identobj) +_thread__make_thread_handle(PyObject *module, PyObject *identobj) +/*[clinic end generated code: output=afa56c40a7e1c394 input=57bb20ebf5825dd1]*/ { thread_module_state *state = get_thread_state(module); if (!PyLong_Check(identobj)) { @@ -2470,26 +2509,22 @@ thread__make_thread_handle(PyObject *module, PyObject *identobj) return (PyObject*) hobj; } -PyDoc_STRVAR(thread__make_thread_handle_doc, -"_make_thread_handle($module, ident, /)\n\ ---\n\ -\n\ -Internal only. Make a thread handle for threads not spawned\n\ -by the _thread or threading module."); +/*[clinic input] +_thread._get_main_thread_ident + +Internal only. + +Return a non-zero integer that uniquely identifies the main thread of +the main interpreter. +[clinic start generated code]*/ static PyObject * -thread__get_main_thread_ident(PyObject *module, PyObject *Py_UNUSED(ignored)) +_thread__get_main_thread_ident_impl(PyObject *module) +/*[clinic end generated code: output=01aa07c5553458bf input=feea25c7039ba4b5]*/ { return PyLong_FromUnsignedLongLong(_PyRuntime.main_thread); } -PyDoc_STRVAR(thread__get_main_thread_ident_doc, -"_get_main_thread_ident($module, /)\n\ ---\n\ -\n\ -Internal only. Return a non-zero integer that uniquely identifies the main thread\n\ -of the main interpreter."); - #if defined(__OpenBSD__) /* pthread_*_np functions, especially pthread_{get,set}_name_np(). pthread_np.h exists on both OpenBSD and FreeBSD but the latter declares @@ -2649,44 +2684,29 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) static PyMethodDef thread_methods[] = { - {"start_new_thread", thread_PyThread_start_new_thread, - METH_VARARGS, start_new_thread_doc}, - {"start_new", thread_PyThread_start_new_thread, - METH_VARARGS, start_new_doc}, - {"start_joinable_thread", _PyCFunction_CAST(thread_PyThread_start_joinable_thread), - METH_VARARGS | METH_KEYWORDS, start_joinable_doc}, - {"daemon_threads_allowed", thread_daemon_threads_allowed, - METH_NOARGS, daemon_threads_allowed_doc}, - {"allocate_lock", thread_PyThread_allocate_lock, - METH_NOARGS, allocate_lock_doc}, - {"allocate", thread_PyThread_allocate_lock, + _THREAD_START_NEW_THREAD_METHODDEF + {"start_new", _PyCFunction_CAST(_thread_start_new_thread), + METH_FASTCALL, start_new_doc}, + _THREAD_START_JOINABLE_THREAD_METHODDEF + _THREAD_DAEMON_THREADS_ALLOWED_METHODDEF + _THREAD_ALLOCATE_LOCK_METHODDEF + {"allocate", _thread_allocate_lock, METH_NOARGS, allocate_doc}, - {"exit_thread", thread_PyThread_exit_thread, + {"exit_thread", _thread_exit, METH_NOARGS, exit_thread_doc}, - {"exit", thread_PyThread_exit_thread, - METH_NOARGS, exit_doc}, - {"interrupt_main", thread_PyThread_interrupt_main, - METH_VARARGS, interrupt_doc}, - {"get_ident", thread_get_ident, - METH_NOARGS, get_ident_doc}, + _THREAD_EXIT_METHODDEF + _THREAD_INTERRUPT_MAIN_METHODDEF + _THREAD_GET_IDENT_METHODDEF #ifdef PY_HAVE_THREAD_NATIVE_ID - {"get_native_id", thread_get_native_id, - METH_NOARGS, get_native_id_doc}, + _THREAD_GET_NATIVE_ID_METHODDEF #endif - {"_count", thread__count, - METH_NOARGS, _count_doc}, - {"stack_size", thread_stack_size, - METH_VARARGS, stack_size_doc}, - {"_excepthook", thread_excepthook, - METH_O, excepthook_doc}, - {"_is_main_interpreter", thread__is_main_interpreter, - METH_NOARGS, thread__is_main_interpreter_doc}, - {"_shutdown", thread_shutdown, - METH_NOARGS, shutdown_doc}, - {"_make_thread_handle", thread__make_thread_handle, - METH_O, thread__make_thread_handle_doc}, - {"_get_main_thread_ident", thread__get_main_thread_ident, - METH_NOARGS, thread__get_main_thread_ident_doc}, + _THREAD__COUNT_METHODDEF + _THREAD_STACK_SIZE_METHODDEF + _THREAD__EXCEPTHOOK_METHODDEF + _THREAD__IS_MAIN_INTERPRETER_METHODDEF + _THREAD__SHUTDOWN_METHODDEF + _THREAD__MAKE_THREAD_HANDLE_METHODDEF + _THREAD__GET_MAIN_THREAD_IDENT_METHODDEF _THREAD_SET_NAME_METHODDEF _THREAD__GET_NAME_METHODDEF {NULL, NULL} /* sentinel */ diff --git a/Modules/clinic/_threadmodule.c.h b/Modules/clinic/_threadmodule.c.h index 926bea8e1e419a4..090fdee8f6593e3 100644 --- a/Modules/clinic/_threadmodule.c.h +++ b/Modules/clinic/_threadmodule.c.h @@ -6,7 +6,82 @@ preserve # include "pycore_gc.h" // PyGC_Head # include "pycore_runtime.h" // _Py_ID() #endif -#include "pycore_modsupport.h" // _PyArg_UnpackKeywords() +#include "pycore_abstract.h" // _PyNumber_Index() +#include "pycore_modsupport.h" // _PyArg_CheckPositional() + +static PyObject * +_thread__ThreadHandle_ident_get_impl(PyObject *self); + +static PyObject * +_thread__ThreadHandle_ident_get(PyObject *self, void *Py_UNUSED(context)) +{ + return _thread__ThreadHandle_ident_get_impl(self); +} + +PyDoc_STRVAR(_thread__ThreadHandle_join__doc__, +"join($self, timeout=None, /)\n" +"--\n" +"\n"); + +#define _THREAD__THREADHANDLE_JOIN_METHODDEF \ + {"join", _PyCFunction_CAST(_thread__ThreadHandle_join), METH_FASTCALL, _thread__ThreadHandle_join__doc__}, + +static PyObject * +_thread__ThreadHandle_join_impl(PyObject *self, PyObject *timeout_obj); + +static PyObject * +_thread__ThreadHandle_join(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + PyObject *timeout_obj = Py_None; + + if (!_PyArg_CheckPositional("join", nargs, 0, 1)) { + goto exit; + } + if (nargs < 1) { + goto skip_optional; + } + timeout_obj = args[0]; +skip_optional: + return_value = _thread__ThreadHandle_join_impl(self, timeout_obj); + +exit: + return return_value; +} + +PyDoc_STRVAR(_thread__ThreadHandle_is_done__doc__, +"is_done($self, /)\n" +"--\n" +"\n"); + +#define _THREAD__THREADHANDLE_IS_DONE_METHODDEF \ + {"is_done", (PyCFunction)_thread__ThreadHandle_is_done, METH_NOARGS, _thread__ThreadHandle_is_done__doc__}, + +static PyObject * +_thread__ThreadHandle_is_done_impl(PyObject *self); + +static PyObject * +_thread__ThreadHandle_is_done(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return _thread__ThreadHandle_is_done_impl(self); +} + +PyDoc_STRVAR(_thread__ThreadHandle__set_done__doc__, +"_set_done($self, /)\n" +"--\n" +"\n"); + +#define _THREAD__THREADHANDLE__SET_DONE_METHODDEF \ + {"_set_done", (PyCFunction)_thread__ThreadHandle__set_done, METH_NOARGS, _thread__ThreadHandle__set_done__doc__}, + +static PyObject * +_thread__ThreadHandle__set_done_impl(PyObject *self); + +static PyObject * +_thread__ThreadHandle__set_done(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return _thread__ThreadHandle__set_done_impl(self); +} PyDoc_STRVAR(_thread_lock_acquire__doc__, "acquire($self, /, blocking=True, timeout=-1)\n" @@ -637,6 +712,457 @@ _thread_RLock__at_fork_reinit(PyObject *self, PyObject *Py_UNUSED(ignored)) #endif /* defined(HAVE_FORK) */ +PyDoc_STRVAR(_thread_daemon_threads_allowed__doc__, +"daemon_threads_allowed($module, /)\n" +"--\n" +"\n" +"Return True if daemon threads are allowed in the current interpreter.\n" +"\n" +"Return False otherwise."); + +#define _THREAD_DAEMON_THREADS_ALLOWED_METHODDEF \ + {"daemon_threads_allowed", (PyCFunction)_thread_daemon_threads_allowed, METH_NOARGS, _thread_daemon_threads_allowed__doc__}, + +static PyObject * +_thread_daemon_threads_allowed_impl(PyObject *module); + +static PyObject * +_thread_daemon_threads_allowed(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _thread_daemon_threads_allowed_impl(module); +} + +PyDoc_STRVAR(_thread_start_new_thread__doc__, +"start_new_thread($module, function, args, kwargs={}, /)\n" +"--\n" +"\n" +"Start a new thread and return its identifier.\n" +"\n" +"The thread will call the function with positional arguments from the\n" +"tuple args and keyword arguments taken from the optional dictionary\n" +"kwargs. The thread exits when the function returns; the return value\n" +"is ignored. The thread will also exit when the function raises an\n" +"unhandled exception; a stack trace will be printed unless the exception\n" +"is SystemExit."); + +#define _THREAD_START_NEW_THREAD_METHODDEF \ + {"start_new_thread", _PyCFunction_CAST(_thread_start_new_thread), METH_FASTCALL, _thread_start_new_thread__doc__}, + +static PyObject * +_thread_start_new_thread_impl(PyObject *module, PyObject *func, + PyObject *args, PyObject *kwargs); + +static PyObject * +_thread_start_new_thread(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + PyObject *func; + PyObject *__clinic_args; + PyObject *__clinic_kwargs = NULL; + + if (!_PyArg_CheckPositional("start_new_thread", nargs, 2, 3)) { + goto exit; + } + func = args[0]; + if (!PyTuple_Check(args[1])) { + _PyArg_BadArgument("start_new_thread", "argument 2", "tuple", args[1]); + goto exit; + } + __clinic_args = args[1]; + if (nargs < 3) { + goto skip_optional; + } + if (!PyDict_Check(args[2])) { + _PyArg_BadArgument("start_new_thread", "argument 3", "dict", args[2]); + goto exit; + } + __clinic_kwargs = args[2]; +skip_optional: + return_value = _thread_start_new_thread_impl(module, func, __clinic_args, __clinic_kwargs); + +exit: + return return_value; +} + +PyDoc_STRVAR(_thread_start_joinable_thread__doc__, +"start_joinable_thread($module, /, function, handle=None, daemon=True)\n" +"--\n" +"\n" +"*For internal use only*: start a new thread.\n" +"\n" +"Like start_new_thread(), this starts a new thread calling the given\n" +"function. Unlike start_new_thread(), this returns a handle object with\n" +"methods to join or detach the given thread.\n" +"This function is not for third-party code, please use the `threading`\n" +"module instead. During finalization the runtime will not wait for the\n" +"thread to exit if daemon is True. If handle is provided it must be a\n" +"newly created thread._ThreadHandle instance."); + +#define _THREAD_START_JOINABLE_THREAD_METHODDEF \ + {"start_joinable_thread", _PyCFunction_CAST(_thread_start_joinable_thread), METH_FASTCALL|METH_KEYWORDS, _thread_start_joinable_thread__doc__}, + +static PyObject * +_thread_start_joinable_thread_impl(PyObject *module, PyObject *func, + PyObject *hobj, int daemon); + +static PyObject * +_thread_start_joinable_thread(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 3 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(function), &_Py_ID(handle), &_Py_ID(daemon), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"function", "handle", "daemon", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "start_joinable_thread", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[3]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + PyObject *func; + PyObject *hobj = Py_None; + int daemon = 1; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, + /*minpos*/ 1, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + func = args[0]; + if (!noptargs) { + goto skip_optional_pos; + } + if (args[1]) { + hobj = args[1]; + if (!--noptargs) { + goto skip_optional_pos; + } + } + daemon = PyObject_IsTrue(args[2]); + if (daemon < 0) { + goto exit; + } +skip_optional_pos: + return_value = _thread_start_joinable_thread_impl(module, func, hobj, daemon); + +exit: + return return_value; +} + +PyDoc_STRVAR(_thread_exit__doc__, +"exit($module, /)\n" +"--\n" +"\n" +"Raise SystemExit.\n" +"\n" +"It will cause the current thread to exit silently unless the exception\n" +"is caught."); + +#define _THREAD_EXIT_METHODDEF \ + {"exit", (PyCFunction)_thread_exit, METH_NOARGS, _thread_exit__doc__}, + +static PyObject * +_thread_exit_impl(PyObject *module); + +static PyObject * +_thread_exit(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _thread_exit_impl(module); +} + +PyDoc_STRVAR(_thread_interrupt_main__doc__, +"interrupt_main($module, signum=signal.SIGINT, /)\n" +"--\n" +"\n" +"Simulate the arrival of the given signal in the main thread.\n" +"\n" +"The corresponding signal handler will be executed.\n" +"If *signum* is omitted, SIGINT is assumed.\n" +"A subthread can use this function to interrupt the main thread.\n" +"\n" +"Note: the default signal handler for SIGINT raises\n" +"``KeyboardInterrupt``."); + +#define _THREAD_INTERRUPT_MAIN_METHODDEF \ + {"interrupt_main", _PyCFunction_CAST(_thread_interrupt_main), METH_FASTCALL, _thread_interrupt_main__doc__}, + +static PyObject * +_thread_interrupt_main_impl(PyObject *module, int signum); + +static PyObject * +_thread_interrupt_main(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + int signum = SIGINT; + + if (!_PyArg_CheckPositional("interrupt_main", nargs, 0, 1)) { + goto exit; + } + if (nargs < 1) { + goto skip_optional; + } + signum = PyLong_AsInt(args[0]); + if (signum == -1 && PyErr_Occurred()) { + goto exit; + } +skip_optional: + return_value = _thread_interrupt_main_impl(module, signum); + +exit: + return return_value; +} + +PyDoc_STRVAR(_thread_allocate_lock__doc__, +"allocate_lock($module, /)\n" +"--\n" +"\n" +"Create a new lock object.\n" +"\n" +"See help(type(threading.Lock())) for information about locks."); + +#define _THREAD_ALLOCATE_LOCK_METHODDEF \ + {"allocate_lock", (PyCFunction)_thread_allocate_lock, METH_NOARGS, _thread_allocate_lock__doc__}, + +static PyObject * +_thread_allocate_lock_impl(PyObject *module); + +static PyObject * +_thread_allocate_lock(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _thread_allocate_lock_impl(module); +} + +PyDoc_STRVAR(_thread_get_ident__doc__, +"get_ident($module, /)\n" +"--\n" +"\n" +"Return a non-zero integer that uniquely identifies the current thread.\n" +"\n" +"It is unique amongst other threads that exist simultaneously.\n" +"This may be used to identify per-thread resources.\n" +"Even though on some platforms threads identities may appear to be\n" +"allocated consecutive numbers starting at 1, this behavior should not\n" +"be relied upon, and the number should be seen purely as a magic cookie.\n" +"A thread\'s identity may be reused for another thread after it exits."); + +#define _THREAD_GET_IDENT_METHODDEF \ + {"get_ident", (PyCFunction)_thread_get_ident, METH_NOARGS, _thread_get_ident__doc__}, + +static PyObject * +_thread_get_ident_impl(PyObject *module); + +static PyObject * +_thread_get_ident(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _thread_get_ident_impl(module); +} + +#if defined(PY_HAVE_THREAD_NATIVE_ID) + +PyDoc_STRVAR(_thread_get_native_id__doc__, +"get_native_id($module, /)\n" +"--\n" +"\n" +"Return a non-negative integer identifying the thread.\n" +"\n" +"It is reported by the OS (kernel). This may be used to uniquely\n" +"identify a particular thread within a system."); + +#define _THREAD_GET_NATIVE_ID_METHODDEF \ + {"get_native_id", (PyCFunction)_thread_get_native_id, METH_NOARGS, _thread_get_native_id__doc__}, + +static PyObject * +_thread_get_native_id_impl(PyObject *module); + +static PyObject * +_thread_get_native_id(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _thread_get_native_id_impl(module); +} + +#endif /* defined(PY_HAVE_THREAD_NATIVE_ID) */ + +PyDoc_STRVAR(_thread__count__doc__, +"_count($module, /)\n" +"--\n" +"\n" +"Return the number of currently running Python threads.\n" +"\n" +"The main thread is excluded. The returned number comprises all threads\n" +"created through `start_new_thread()` as well as `threading.Thread`, and\n" +"not yet finished.\n" +"\n" +"This function is meant for internal and specialized purposes only.\n" +"In most applications `threading.enumerate()` should be used instead."); + +#define _THREAD__COUNT_METHODDEF \ + {"_count", (PyCFunction)_thread__count, METH_NOARGS, _thread__count__doc__}, + +static PyObject * +_thread__count_impl(PyObject *module); + +static PyObject * +_thread__count(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _thread__count_impl(module); +} + +PyDoc_STRVAR(_thread_stack_size__doc__, +"stack_size($module, size=0, /)\n" +"--\n" +"\n" +"Return the thread stack size used when creating new threads.\n" +"\n" +"The optional size argument specifies the stack size (in bytes) to be\n" +"used for subsequently created threads, and must be 0 (use platform or\n" +"configured default) or a positive integer value of at least 32,768 (32\n" +"KiB). If changing the thread stack size is unsupported, a ThreadError\n" +"exception is raised. If the specified size is invalid, a ValueError\n" +"exception is raised, and the stack size is unmodified. 32 KiB\n" +"currently is the minimum supported stack size value to guarantee\n" +"sufficient stack space for the interpreter itself.\n" +"\n" +"Note that some systems may have particular restrictions on values for\n" +"the stack size, such as requiring a minimum stack size larger than 32\n" +"KiB or requiring allocation in multiples of the system memory page size\n" +"- platform documentation should be referred to for more information\n" +"(4 KiB pages are common; using multiples of 4096 for the stack size is\n" +"the suggested approach in the absence of more specific information)."); + +#define _THREAD_STACK_SIZE_METHODDEF \ + {"stack_size", _PyCFunction_CAST(_thread_stack_size), METH_FASTCALL, _thread_stack_size__doc__}, + +static PyObject * +_thread_stack_size_impl(PyObject *module, Py_ssize_t new_size); + +static PyObject * +_thread_stack_size(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + Py_ssize_t new_size = 0; + + if (!_PyArg_CheckPositional("stack_size", nargs, 0, 1)) { + goto exit; + } + if (nargs < 1) { + goto skip_optional; + } + { + Py_ssize_t ival = -1; + PyObject *iobj = _PyNumber_Index(args[0]); + if (iobj != NULL) { + ival = PyLong_AsSsize_t(iobj); + Py_DECREF(iobj); + } + if (ival == -1 && PyErr_Occurred()) { + goto exit; + } + new_size = ival; + } +skip_optional: + return_value = _thread_stack_size_impl(module, new_size); + +exit: + return return_value; +} + +PyDoc_STRVAR(_thread__excepthook__doc__, +"_excepthook($module, args, /)\n" +"--\n" +"\n" +"Handle uncaught Thread.run() exception."); + +#define _THREAD__EXCEPTHOOK_METHODDEF \ + {"_excepthook", (PyCFunction)_thread__excepthook, METH_O, _thread__excepthook__doc__}, + +PyDoc_STRVAR(_thread__is_main_interpreter__doc__, +"_is_main_interpreter($module, /)\n" +"--\n" +"\n" +"Return True if the current interpreter is the main Python interpreter."); + +#define _THREAD__IS_MAIN_INTERPRETER_METHODDEF \ + {"_is_main_interpreter", (PyCFunction)_thread__is_main_interpreter, METH_NOARGS, _thread__is_main_interpreter__doc__}, + +static PyObject * +_thread__is_main_interpreter_impl(PyObject *module); + +static PyObject * +_thread__is_main_interpreter(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _thread__is_main_interpreter_impl(module); +} + +PyDoc_STRVAR(_thread__shutdown__doc__, +"_shutdown($module, /)\n" +"--\n" +"\n" +"Wait for all non-daemon threads (other than the calling thread) to stop."); + +#define _THREAD__SHUTDOWN_METHODDEF \ + {"_shutdown", (PyCFunction)_thread__shutdown, METH_NOARGS, _thread__shutdown__doc__}, + +static PyObject * +_thread__shutdown_impl(PyObject *module); + +static PyObject * +_thread__shutdown(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _thread__shutdown_impl(module); +} + +PyDoc_STRVAR(_thread__make_thread_handle__doc__, +"_make_thread_handle($module, ident, /)\n" +"--\n" +"\n" +"Internal only.\n" +"\n" +"Make a thread handle for threads not spawned by the _thread or\n" +"threading module."); + +#define _THREAD__MAKE_THREAD_HANDLE_METHODDEF \ + {"_make_thread_handle", (PyCFunction)_thread__make_thread_handle, METH_O, _thread__make_thread_handle__doc__}, + +PyDoc_STRVAR(_thread__get_main_thread_ident__doc__, +"_get_main_thread_ident($module, /)\n" +"--\n" +"\n" +"Internal only.\n" +"\n" +"Return a non-zero integer that uniquely identifies the main thread of\n" +"the main interpreter."); + +#define _THREAD__GET_MAIN_THREAD_IDENT_METHODDEF \ + {"_get_main_thread_ident", (PyCFunction)_thread__get_main_thread_ident, METH_NOARGS, _thread__get_main_thread_ident__doc__}, + +static PyObject * +_thread__get_main_thread_ident_impl(PyObject *module); + +static PyObject * +_thread__get_main_thread_ident(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _thread__get_main_thread_ident_impl(module); +} + #if (defined(HAVE_PTHREAD_GETNAME_NP) || defined(HAVE_PTHREAD_GET_NAME_NP) || defined(MS_WINDOWS)) PyDoc_STRVAR(_thread__get_name__doc__, @@ -733,6 +1259,10 @@ _thread_set_name(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyOb #define _THREAD_RLOCK__AT_FORK_REINIT_METHODDEF #endif /* !defined(_THREAD_RLOCK__AT_FORK_REINIT_METHODDEF) */ +#ifndef _THREAD_GET_NATIVE_ID_METHODDEF + #define _THREAD_GET_NATIVE_ID_METHODDEF +#endif /* !defined(_THREAD_GET_NATIVE_ID_METHODDEF) */ + #ifndef _THREAD__GET_NAME_METHODDEF #define _THREAD__GET_NAME_METHODDEF #endif /* !defined(_THREAD__GET_NAME_METHODDEF) */ @@ -740,4 +1270,6 @@ _thread_set_name(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyOb #ifndef _THREAD_SET_NAME_METHODDEF #define _THREAD_SET_NAME_METHODDEF #endif /* !defined(_THREAD_SET_NAME_METHODDEF) */ -/*[clinic end generated code: output=0f1707cbafc0e8f2 input=a9049054013a1b77]*/ +#define _THREAD__THREADHANDLE_IDENT_GETSETDEF {"ident", (getter)_thread__ThreadHandle_ident_get, (setter)NULL, NULL}, + +/*[clinic end generated code: output=3bfc76a318c6579d input=a9049054013a1b77]*/