pydblclick 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pydblclick/__init__.py ADDED
File without changes
pydblclick/__main__.py ADDED
@@ -0,0 +1,208 @@
1
+ """pydblclick — parent supervisor process.
2
+
3
+ Entry point: python -m pydblclick <script.py> [args...]
4
+
5
+ The actual script execution happens in a child process (pydblclick/_child.py)
6
+ launched with the same interpreter. The child runs the script with plain-Python
7
+ semantics, shows tracebacks and displays the pause prompt/menu itself.
8
+
9
+ The parent's only job is to guarantee that the console window never flashes
10
+ away, even when the child cannot pause by itself:
11
+ - the script closed stdin with exit()/quit() (input() becomes impossible),
12
+ - the interpreter died hard (os._exit, native crash, MemoryError...),
13
+ - the script was Ctrl+C'd to death.
14
+
15
+ Child -> parent protocol: the child writes "handled" to the file pointed to
16
+ by the PYDBLCLICK_STATUS_FILE env var once it has fulfilled its pause-or-no-pause
17
+ duty. If the marker is missing after the child exits, the parent pauses.
18
+
19
+ Before launching, the parent inspects the script's source (pydblclick/_script_meta.py):
20
+ - `# pydblclick: off` -> run with plain Python, no wrapping at all;
21
+ - PEP 723 `# /// script` block -> run the child through `uv run` so the
22
+ declared dependencies are resolved in an ephemeral environment.
23
+ """
24
+ import os
25
+ import shutil
26
+ import signal
27
+ import subprocess
28
+ import sys
29
+ import tempfile
30
+
31
+ from pydblclick import _script_meta
32
+ from pydblclick._child import STATUS_HANDLED, User32, ensure_console, have_console, signed32
33
+
34
+ UV_INSTALL_URL = "https://docs.astral.sh/uv/getting-started/installation/"
35
+
36
+ # Exit code of a process killed because its console window was closed (or by a
37
+ # hard Ctrl+C/Ctrl+Break). Closing the window is a deliberate user action:
38
+ # the fallback pause must not fire for it.
39
+ STATUS_CONTROL_C_EXIT = 0xC000013A # 3221225786
40
+
41
+
42
+ def _console_python():
43
+ """The console interpreter (python.exe) even when running under pythonw.exe.
44
+
45
+ The parent of a windowless .pyw launch is pythonw.exe, but the child engine
46
+ needs a standard interpreter with working standard streams.
47
+ """
48
+ exe = sys.executable
49
+ if os.path.basename(exe).lower() == "pythonw.exe":
50
+ candidate = os.path.join(os.path.dirname(exe), "python.exe")
51
+ if os.path.exists(candidate):
52
+ return candidate
53
+ return exe
54
+
55
+
56
+ def _script_is_doubleclicked():
57
+ return (('PROMPT' not in os.environ)
58
+ or ('pydblclick_simulate_doubleclick' in os.environ)
59
+ or ('pyexewrap_simulate_doubleclick' in os.environ)) # legacy name
60
+
61
+
62
+ def _read_status(status_file):
63
+ try:
64
+ with open(status_file, encoding="UTF-8") as f:
65
+ return f.read().strip()
66
+ except OSError:
67
+ return ""
68
+
69
+
70
+ def _fallback_pause(returncode):
71
+ """Last-resort pause when the child could not display its own prompt."""
72
+ if sys.stdout is None or sys.stdin is None:
73
+ # Windowless parent (pythonw.exe): no usable stdio at all -- create a
74
+ # console on the spot so the failure is visible.
75
+ if not ensure_console(title="pydblclick"):
76
+ return
77
+ elif have_console():
78
+ # The console may still be hidden if a .pyw script crashed hard
79
+ User32.show_window(User32.Const.SW_SHOWDEFAULT)
80
+ if returncode != 0:
81
+ print("\nThe script ended (exit code " + str(returncode) + ") without pydblclick being able to pause.")
82
+ try:
83
+ input("Press <Enter> to Quit.\n")
84
+ except (EOFError, ValueError, KeyboardInterrupt):
85
+ pass # stdin unusable in the parent too: nothing more we can do
86
+
87
+
88
+ def _plain_python_for(script):
89
+ """The interpreter for unwrapped execution (pythonw for .pyw when available)."""
90
+ if os.path.splitext(script)[1].lower() == ".pyw":
91
+ pythonw = os.path.join(os.path.dirname(sys.executable), "pythonw.exe")
92
+ if os.path.exists(pythonw):
93
+ return pythonw
94
+ return sys.executable
95
+
96
+
97
+ def _find_uv():
98
+ """Locate the uv executable (PYDBLCLICK_UV overrides PATH, for tests)."""
99
+ return os.environ.get("PYDBLCLICK_UV") or shutil.which("uv")
100
+
101
+
102
+ def _build_child_command(script, script_args, env):
103
+ """Build the child command line, delegating to `uv run` for PEP 723 scripts."""
104
+ default_cmd = [_console_python(), "-m", "pydblclick._child", script] + script_args
105
+
106
+ meta = _script_meta.parse_pep723(_script_meta.read_script_text(script))
107
+ if meta is None:
108
+ return default_cmd
109
+
110
+ uv = _find_uv()
111
+ if not uv:
112
+ print("[pydblclick] This script declares PEP 723 dependencies, but 'uv' was not found on PATH.")
113
+ print(" Install uv to run it with its dependencies resolved automatically:")
114
+ print(" " + UV_INSTALL_URL)
115
+ print(" Running with plain Python instead...\n")
116
+ return default_cmd
117
+
118
+ cmd = [uv, "run", "--no-project"]
119
+ if meta["requires-python"]:
120
+ cmd += ["--python", meta["requires-python"]]
121
+ for dep in meta["dependencies"]:
122
+ cmd += ["--with", dep]
123
+ cmd += ["python", "-m", "pydblclick._child", script] + script_args
124
+
125
+ # pydblclick itself must be importable inside uv's ephemeral environment
126
+ package_parent = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
127
+ existing = env.get("PYTHONPATH")
128
+ env["PYTHONPATH"] = package_parent + (os.pathsep + existing if existing else "")
129
+ return cmd
130
+
131
+
132
+ def main():
133
+ if len(sys.argv) < 2:
134
+ print("Usage: pydblclick <script.py> [args...]")
135
+ print(" pydblclick register (set pydblclick as the .py/.pyw double-click handler)")
136
+ print(" pydblclick unregister (restore plain Python on double-click)")
137
+ print(" pydblclick diagnose (inspect the Windows file association chain)")
138
+ return 2
139
+
140
+ # Management subcommands (a real script file named e.g. 'register' still wins)
141
+ from pydblclick._cli import COMMANDS
142
+ if sys.argv[1] in COMMANDS and not os.path.exists(sys.argv[1]):
143
+ from pydblclick import _cli
144
+ return _cli.main(sys.argv[1:])
145
+
146
+ script, script_args = sys.argv[1], sys.argv[2:]
147
+
148
+ # Per-script opt-out: run with plain Python, no wrapping, no pause
149
+ if _script_meta.has_opt_out(_script_meta.read_script_text(script)):
150
+ result = subprocess.run([_plain_python_for(script), script] + script_args)
151
+ return signed32(result.returncode)
152
+
153
+ # The status file is how the child tells us "I already paused (or decided
154
+ # a pause was not needed)". It survives any way the child may die.
155
+ fd, status_file = tempfile.mkstemp(prefix="pydblclick_status_")
156
+ os.close(fd)
157
+ env = dict(os.environ)
158
+ env["PYDBLCLICK_STATUS_FILE"] = status_file
159
+
160
+ cmd = _build_child_command(script, script_args, env)
161
+
162
+ # Windowless mode: a double-clicked .pyw arrives here through pythonw.exe,
163
+ # so this parent has no console. The child runs fully detached (no console
164
+ # either), its output captured in a log file. Only if an exception occurs
165
+ # does the child create a console (AllocConsole) and replay the log there.
166
+ windowless = os.path.splitext(script)[1].lower() == ".pyw" and not have_console()
167
+ run_kwargs = {}
168
+ log_file = None
169
+ log_handle = None
170
+ if windowless:
171
+ fd, log_file = tempfile.mkstemp(prefix="pydblclick_pyw_", suffix=".log")
172
+ log_handle = os.fdopen(fd, "w", encoding="utf-8", errors="replace")
173
+ env["PYDBLCLICK_PYW_LOG"] = log_file
174
+ run_kwargs = {
175
+ "stdin": subprocess.DEVNULL,
176
+ "stdout": log_handle,
177
+ "stderr": subprocess.STDOUT,
178
+ "creationflags": subprocess.DETACHED_PROCESS,
179
+ }
180
+
181
+ # Ctrl+C is sent to every process attached to the console. The child is
182
+ # the one that must handle it (KeyboardInterrupt in the script, then its
183
+ # pause menu); the parent must survive to display the fallback pause.
184
+ previous_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
185
+ try:
186
+ result = subprocess.run(cmd, env=env, **run_kwargs)
187
+ finally:
188
+ signal.signal(signal.SIGINT, previous_handler)
189
+ if log_handle:
190
+ log_handle.close()
191
+
192
+ child_handled = _read_status(status_file) == STATUS_HANDLED
193
+ for temp_file in (status_file, log_file):
194
+ if temp_file:
195
+ try:
196
+ os.remove(temp_file)
197
+ except OSError:
198
+ pass
199
+
200
+ user_closed_console = result.returncode == STATUS_CONTROL_C_EXIT
201
+ if not child_handled and not user_closed_console and _script_is_doubleclicked():
202
+ _fallback_pause(result.returncode)
203
+
204
+ return signed32(result.returncode)
205
+
206
+
207
+ if __name__ == "__main__":
208
+ sys.exit(signed32(main()))
pydblclick/_child.py ADDED
@@ -0,0 +1,398 @@
1
+ """Child-side execution engine of pydblclick.
2
+
3
+ This module is launched by the parent supervisor (pydblclick/__main__.py) as:
4
+
5
+ python -m pydblclick._child <script.py> [args...]
6
+
7
+ It runs the target script with plain-Python semantics (via runpy.run_path),
8
+ shows the traceback on uncaught exceptions, and displays the pause prompt/menu.
9
+ The interactive console (<i> menu option) has access to the script's real
10
+ globals since everything happens in this process.
11
+
12
+ If the pause prompt cannot be displayed (the script closed stdin with
13
+ exit()/quit(), or the interpreter dies), the parent supervisor takes over
14
+ and displays a fallback pause — so the console window never flashes away.
15
+
16
+ Child -> parent protocol: the environment variable PYDBLCLICK_STATUS_FILE
17
+ points to a file where the child writes "handled" once it has fulfilled its
18
+ pause-or-no-pause duty. If the marker is missing, the parent pauses itself.
19
+ """
20
+ import os
21
+ import sys
22
+ import traceback
23
+ import code
24
+ import runpy
25
+
26
+ STATUS_HANDLED = "handled"
27
+
28
+ globalsParameter = {} # global variable that will store the script's namespace
29
+
30
+
31
+ class StdinUnavailable(Exception):
32
+ """Raised when the pause prompt cannot read stdin (closed by exit()/quit())."""
33
+
34
+
35
+ class User32:
36
+ class Const:
37
+ SW_HIDE = 0
38
+ SW_SHOWNORMAL = 1
39
+ SW_SHOWMINIMIZED = 2
40
+ SW_SHOWMAXIMIZED = 3
41
+ SW_SHOWNOACTIVATE = 4
42
+ SW_SHOW = 5
43
+ SW_MINIMIZE = 6
44
+ SW_SHOWMINNOACTIVE = 7
45
+ SW_SHOWNA = 8
46
+ SW_RESTORE = 9
47
+ SW_SHOWDEFAULT = 10
48
+ SW_FORCEMINIMIZE = 11
49
+
50
+ @staticmethod
51
+ def show_window(n_cmd_show):
52
+ """
53
+ Sets the current window's show state.
54
+ """
55
+ # https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
56
+ import ctypes
57
+ kernel32 = ctypes.WinDLL('kernel32')
58
+ user32 = ctypes.WinDLL('user32')
59
+ h_wnd = kernel32.GetConsoleWindow()
60
+ user32.ShowWindow(h_wnd, n_cmd_show)
61
+
62
+
63
+ def have_console():
64
+ """True if this process is attached to a console."""
65
+ import ctypes
66
+ return bool(ctypes.WinDLL('kernel32').GetConsoleWindow())
67
+
68
+
69
+ def ensure_console(title=None):
70
+ """Attach a brand-new console to this process and rewire the standard streams.
71
+
72
+ Used by windowless .pyw execution: the console only comes into existence
73
+ when there is something to show (an exception). Returns False if a console
74
+ could not be created.
75
+ """
76
+ import ctypes
77
+ kernel32 = ctypes.WinDLL('kernel32')
78
+ if not kernel32.GetConsoleWindow():
79
+ if not kernel32.AllocConsole():
80
+ return False
81
+ sys.stdin = open("CONIN$", "r", encoding="utf-8", errors="replace")
82
+ sys.stdout = open("CONOUT$", "w", buffering=1, encoding="utf-8", errors="replace")
83
+ sys.stderr = open("CONOUT$", "w", buffering=1, encoding="utf-8", errors="replace")
84
+ if title:
85
+ kernel32.SetConsoleTitleW(str(title))
86
+ return True
87
+
88
+
89
+ def reveal_console_for_pyw(log_file=None):
90
+ """Make the console visible for a crashing .pyw script.
91
+
92
+ Two situations:
93
+ - a console exists (script launched from a console, or legacy hidden-console
94
+ mode): just show its window again;
95
+ - no console at all (windowless mode, parent is pythonw.exe): create one on
96
+ the spot and replay the output captured so far in the log file.
97
+ """
98
+ if have_console():
99
+ User32.show_window(User32.Const.SW_SHOWDEFAULT)
100
+ return
101
+ # Flush what the script wrote to the redirected stdout/stderr (the log
102
+ # file) so the replay below is complete.
103
+ for stream in (sys.stdout, sys.stderr):
104
+ try:
105
+ stream.flush()
106
+ except (OSError, ValueError, AttributeError):
107
+ pass
108
+ if not ensure_console(title=os.path.basename(sys.argv[0]) + " -- pydblclick"):
109
+ return
110
+ if log_file:
111
+ try:
112
+ with open(log_file, encoding="utf-8", errors="replace") as f:
113
+ captured = f.read()
114
+ if captured:
115
+ print(captured, end="")
116
+ except OSError:
117
+ pass
118
+
119
+
120
+ def showtraceback(script_path, script_excepthook=None):
121
+ """
122
+ Displays the exception that just occurred, hiding the pydblclick/runpy
123
+ internal frames so the traceback starts at the user's script.
124
+
125
+ script_excepthook: the excepthook installed by the script itself (None if
126
+ the script did not change sys.excepthook).
127
+ """
128
+ sys.last_type, sys.last_value, last_tb = ei = sys.exc_info()
129
+ sys.last_traceback = last_tb
130
+ try:
131
+ # Walk down to the first frame that belongs to the user's script.
132
+ # Frames above it are pydblclick's own code and runpy internals.
133
+ tb = last_tb
134
+ while tb is not None and tb.tb_frame.f_code.co_filename != script_path:
135
+ tb = tb.tb_next
136
+ # tb is None for exceptions with no frame in the script (e.g. SyntaxError):
137
+ # format_exception then prints the exception part only, which for a
138
+ # SyntaxError still includes the file, line and caret indicator.
139
+ lines = traceback.format_exception(ei[0], ei[1], tb)
140
+ if script_excepthook is not None:
141
+ # The *script* installed its own excepthook: let it take precedence
142
+ # over our print. (Comparing against sys.__excepthook__ is not
143
+ # enough: some environments replace sys.excepthook globally, which
144
+ # would silently send our traceback elsewhere.)
145
+ script_excepthook(ei[0], ei[1], tb)
146
+ else:
147
+ print(''.join(lines))
148
+ finally:
149
+ tb = last_tb = ei = None
150
+
151
+
152
+ def display_pause_prompt_and_menu():
153
+ """
154
+ Pauses a script to let the user read stdout and/or strerr before the window gets closed
155
+ """
156
+ # Looping on the pause message as long as it is needed
157
+ while True:
158
+ wait = None
159
+ # Managing KeyboardInterrupt during the pausing message
160
+ while wait is None:
161
+ try:
162
+ wait = input("Press <Enter> to Quit. (<c> for cmd console. <i> for interactive python. <r> to restart.)\n")
163
+ except KeyboardInterrupt:
164
+ pass # The menu cannot be left using KeyboardInterrupt
165
+ except EOFError:
166
+ wait = "" # stdin closed (e.g. piped input) -- treat as Enter
167
+ except ValueError:
168
+ # stdin has been closed by the script (exit()/quit() does that):
169
+ # the parent supervisor must display the pause instead
170
+ raise StdinUnavailable()
171
+ except:
172
+ print(traceback.format_exc()) # Unexpected exception
173
+
174
+ # By default, the script is set to end after we break out of the "While True" loop displaying the pausing message
175
+ must_run_script_again = False
176
+ if wait.lower() == "c":
177
+ print('Opening a windows console (cmd.exe). Type "exit" to quit.\n\n')
178
+ try:
179
+ os.system("cmd /k")
180
+ except KeyboardInterrupt:
181
+ pass
182
+ except:
183
+ print(traceback.format_exc()) # Unexpected exception
184
+ print("\n")
185
+ elif wait.lower() == "i":
186
+ print('Opening python interactive console (python.exe). Type "Ctrl+Z to quit.\n\n')
187
+ try:
188
+ global globalsParameter
189
+ # Import useful debug modules
190
+ from pprint import pprint as pp
191
+ globalsParameter['pp'] = pp
192
+ globalsParameter['traceback'] = traceback
193
+ globalsParameter['os'] = os
194
+ globalsParameter['sys'] = sys
195
+ code.interact(local=globalsParameter)
196
+ except KeyboardInterrupt:
197
+ pass
198
+ except:
199
+ print(traceback.format_exc()) # Unexpected exception
200
+ print("\n")
201
+ elif wait.lower() == "r":
202
+ os.system("cls")
203
+ must_run_script_again = True
204
+ break
205
+ elif wait.lower() == "debug":
206
+ # Secret menu item to help developping new features
207
+ print("place any variable here to debug it: " + sys.executable)
208
+ elif wait.lower() == "pydblclick":
209
+ # Secret feature to open the tool and start editing the source for new cool features
210
+ os.system("explorer " + os.path.split(sys.argv[0])[0])
211
+ elif wait.lower() == "":
212
+ must_run_script_again = False
213
+ break # exits while True to end pydblclick
214
+ else:
215
+ # The commands must be typed accurately. Must retry...
216
+ wait = None
217
+
218
+ # Run after we brake out of the While True loop:
219
+ return must_run_script_again
220
+
221
+
222
+ def signed32(code):
223
+ """Convert a Windows exit code to the signed 32-bit range for sys.exit().
224
+
225
+ Codes like STATUS_CONTROL_C_EXIT (0xC000013A) exceed INT_MAX; before
226
+ Python 3.14, sys.exit() overflows on them and the process exits with -1.
227
+ Two's complement preserves the value seen by GetExitCodeProcess.
228
+ """
229
+ if isinstance(code, int) and code >= 2 ** 31:
230
+ return code - 2 ** 32
231
+ return code
232
+
233
+
234
+ def _normalize_exit_code(system_exit):
235
+ """Turn a SystemExit into a process exit code, like the interpreter does."""
236
+ code_value = system_exit.code
237
+ if code_value is None:
238
+ return 0
239
+ if isinstance(code_value, int):
240
+ return code_value
241
+ print(code_value) # sys.exit("message") prints the message and exits 1
242
+ return 1
243
+
244
+
245
+ def run_script(script_to_execute):
246
+ """Runs the target script and returns (pause_decision, exit_code)."""
247
+ ################ BEHAVIOUR CUSTOMIZATION ######
248
+ pydblclick_customizations = {}
249
+ pydblclick_customizations['must_pause_in_console'] = True # This can be changed dynamicaly by the enhanced scripts
250
+ pydblclick_must_change_title = True
251
+ pydblclick_verbose = False
252
+ # pydblclick_verbose = True # Uncomment to debug with verbose mode
253
+
254
+ if pydblclick_verbose: print("pydblclick activated.")
255
+
256
+ script_extension = os.path.splitext(script_to_execute)[1]
257
+ script_is_doubleclicked = (('PROMPT' not in os.environ)
258
+ or ('pydblclick_simulate_doubleclick' in os.environ)
259
+ or ('pyexewrap_simulate_doubleclick' in os.environ)) # legacy name
260
+ # script_is_doubleclicked = True # Uncomment this to simulate a double-clicked script even though you are using a console
261
+
262
+ exit_code = 0
263
+ # Snapshot to detect an excepthook installed by the script itself
264
+ hook_before_script = sys.excepthook
265
+
266
+ if "pythonw" in sys.executable:
267
+ err_msg = "Error : pydblclick should never be running with pythonw.exe !\n" + str(sys.executable) + "\n" + str(sys.argv)
268
+ print(err_msg)
269
+ with open("error.txt", "w", encoding="UTF-8") as f:
270
+ f.write(err_msg)
271
+
272
+ try:
273
+ ################ INITIALIZATION ##############
274
+ if pydblclick_verbose:
275
+ print("interpreter is " + sys.executable)
276
+ print("CLI is " + " ".join(sys.argv))
277
+ print("script extension is " + script_extension)
278
+ print("script_is_doubleclicked=" + str(script_is_doubleclicked))
279
+
280
+ # .pyw files should have no visible console unless an exception occurs.
281
+ # In windowless mode (parent is pythonw.exe) there is no console at all;
282
+ # otherwise (legacy/CLI) the existing console window is hidden.
283
+ if script_extension == ".pyw" and script_is_doubleclicked and have_console():
284
+ User32.show_window(User32.Const.SW_HIDE) # Use SW_SHOWMINIMIZED to debug
285
+
286
+ # if not run in console (but through double-click) the window title will be explicit
287
+ if script_is_doubleclicked and pydblclick_must_change_title and have_console():
288
+ os.system("title " + os.path.basename(script_to_execute) + " -- pydblclick " + script_to_execute)
289
+
290
+ ################ EXECUTION ####################
291
+ # runpy.run_path() executes the script with plain-Python semantics:
292
+ # a fresh __main__ module, __file__ set to the script path, real module
293
+ # globals. No namespace reconstruction, no exec() surgery.
294
+ # Like `python script.py`, the script's directory must be on sys.path:
295
+ script_dir = os.path.dirname(os.path.abspath(script_to_execute))
296
+ if script_dir not in sys.path:
297
+ sys.path.insert(0, script_dir)
298
+
299
+ # The customization dict is exposed through builtins so that scripts can
300
+ # write `pydblclick_customizations['must_pause_in_console'] = False`
301
+ # without pydblclick polluting their namespace.
302
+ import builtins
303
+ builtins.pydblclick_customizations = pydblclick_customizations
304
+ builtins.pyexewrap_customizations = pydblclick_customizations # legacy alias
305
+ try:
306
+ global globalsParameter
307
+ globalsParameter = runpy.run_path(script_to_execute, run_name="__main__")
308
+ finally:
309
+ del builtins.pydblclick_customizations
310
+ del builtins.pyexewrap_customizations
311
+
312
+ if pydblclick_verbose: print("must_pause_in_console=" + str(pydblclick_customizations['must_pause_in_console']))
313
+
314
+ except SystemExit as e:
315
+ # exit()/quit()/sys.exit() in the script: not an error, but the exit
316
+ # code must be propagated for CLI/batch callers.
317
+ exit_code = _normalize_exit_code(e)
318
+ except BaseException:
319
+ exit_code = 1
320
+ pydblclick_customizations['must_pause_in_console'] = True
321
+ if script_extension == ".pyw":
322
+ # From now on pydblclick will consider the script as a .py file (with a pausing message to display)
323
+ script_extension = ".py"
324
+ reveal_console_for_pyw(os.environ.get("PYDBLCLICK_PYW_LOG"))
325
+ # Expose the script's globals to the interactive console for post-mortem debugging
326
+ tb = sys.exc_info()[2]
327
+ while tb is not None:
328
+ if tb.tb_frame.f_code.co_filename == script_to_execute:
329
+ globalsParameter = tb.tb_frame.f_globals
330
+ break
331
+ tb = tb.tb_next
332
+ script_excepthook = sys.excepthook if sys.excepthook is not hook_before_script else None
333
+ showtraceback(script_to_execute, script_excepthook)
334
+ print("This exception has ended the script before the end.")
335
+
336
+ pause_decision = script_is_doubleclicked and pydblclick_customizations['must_pause_in_console'] and script_extension != ".pyw"
337
+ if pydblclick_verbose:
338
+ print("pausing message ?")
339
+ print("script_is_doubleclicked=" + str(script_is_doubleclicked))
340
+ print("must_pause_in_console=" + str(pydblclick_customizations['must_pause_in_console']))
341
+ print("script_extension=" + script_extension)
342
+ print("pause_decision=" + str(pause_decision))
343
+
344
+ return pause_decision, exit_code
345
+
346
+
347
+ def _write_status(status_file, text):
348
+ if not status_file:
349
+ return
350
+ try:
351
+ with open(status_file, "w", encoding="UTF-8") as f:
352
+ f.write(text)
353
+ except OSError:
354
+ pass
355
+
356
+
357
+ def main():
358
+ # sys.version_info(major=3, minor=11, micro=3, releaselevel='final', serial=0)
359
+ if sys.version_info.major < 3 or sys.version_info.minor < 10:
360
+ print("Warning: pydblclick has not been tested with Python version 3.9 and below.")
361
+ print("sys.version=" + sys.version)
362
+
363
+ if len(sys.argv) < 2:
364
+ print("Usage: python -m pydblclick <script.py> [args...]")
365
+ return 2
366
+
367
+ status_file = os.environ.pop("PYDBLCLICK_STATUS_FILE", None)
368
+
369
+ script_to_execute = sys.argv[1]
370
+ # The wrapped script must see the same sys.argv as if it was run directly
371
+ sys.argv = sys.argv[1:]
372
+
373
+ exit_code = 0
374
+ # Looping since the script can be run multiple times
375
+ must_run_script_again = True
376
+ while must_run_script_again:
377
+
378
+ # But in most cases the script is only run once
379
+ must_run_script_again = False
380
+
381
+ # The script is run, depending on the situation there should be a pausing prompt
382
+ pause_decision, exit_code = run_script(script_to_execute)
383
+
384
+ # Displaying the pausing prompt (and defining if the script must be run again)
385
+ if pause_decision:
386
+ try:
387
+ must_run_script_again = display_pause_prompt_and_menu()
388
+ except StdinUnavailable:
389
+ # The script closed stdin (exit()/quit() does that). No status
390
+ # marker is written: the parent supervisor will pause instead.
391
+ return signed32(exit_code)
392
+
393
+ _write_status(status_file, STATUS_HANDLED)
394
+ return signed32(exit_code)
395
+
396
+
397
+ if __name__ == "__main__":
398
+ sys.exit(signed32(main()))