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 +0 -0
- pydblclick/__main__.py +208 -0
- pydblclick/_child.py +398 -0
- pydblclick/_cli.py +299 -0
- pydblclick/_script_meta.py +108 -0
- pydblclick/winpyfiles/__init__.py +22 -0
- pydblclick/winpyfiles/__main__.py +295 -0
- pydblclick/winpyfiles/_assoc.py +242 -0
- pydblclick/winpyfiles/_backup.py +77 -0
- pydblclick/winpyfiles/_elevation.py +25 -0
- pydblclick/winpyfiles/_registry.py +66 -0
- pydblclick-0.2.0.dist-info/METADATA +200 -0
- pydblclick-0.2.0.dist-info/RECORD +17 -0
- pydblclick-0.2.0.dist-info/WHEEL +5 -0
- pydblclick-0.2.0.dist-info/entry_points.txt +2 -0
- pydblclick-0.2.0.dist-info/licenses/LICENSE.md +21 -0
- pydblclick-0.2.0.dist-info/top_level.txt +1 -0
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()))
|