pydblclick 0.2.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 TsKyrk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: pydblclick
3
+ Version: 0.2.0
4
+ Summary: Enhances the user experience of Python scripts run via double-click on Windows
5
+ Author: TsKyrk
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/TsKyrk/pydblclick
8
+ Keywords: windows,launcher,wrapper,double-click,console
9
+ Classifier: Operating System :: Microsoft :: Windows
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Environment :: Console
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE.md
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # What is pydblclick ?
20
+
21
+ *(formerly known as **pyexewrap** — renamed before the first PyPI release, since the old
22
+ name referenced the deprecated `py.exe` launcher and suggested exe-building tools)*
23
+
24
+ pydblclick makes Python scripts pleasant to run **by double-click** on Windows — for you,
25
+ your colleagues, or anyone you share a one-file script with:
26
+
27
+ - The console window **never flashes away**: a pause prompt appears at the end of the
28
+ script, *including* when an exception occurs (even a syntax error), so the traceback
29
+ is always readable.
30
+ - Scripts that declare their dependencies inline ([PEP 723](https://peps.python.org/pep-0723/))
31
+ are executed through [uv](https://docs.astral.sh/uv/): **dependencies are resolved
32
+ automatically** in an ephemeral environment — the recipient never manages venvs or
33
+ `pip install`.
34
+ - An interactive menu at the pause prompt: `<i>` opens a Python console **with the
35
+ script's real variables** (post-mortem debugging), `<c>` opens a cmd console,
36
+ `<r>` restarts the script.
37
+ - `.pyw` (windowed) scripts run with **no console at all** (they are registered with
38
+ `pythonw.exe`) — but if they crash, a console is created on the spot showing the
39
+ script's output and the traceback, instead of dying silently.
40
+ - When a script is run from a console, called by another script or a batch file,
41
+ pydblclick stays out of the way: no pause, exit codes and arguments faithfully
42
+ propagated.
43
+
44
+ ## Python's native problems for Windows users
45
+
46
+ - A double-clicked `.py` file pops a console that flashes away, unless the last line is
47
+ a blocking `input()` — and even then, any exception (a syntax error, a missing module)
48
+ skips that line and the window vanishes before the traceback can be read.
49
+ - That blocking `input()` becomes undesirable when the same script is run from a console
50
+ or called by another script.
51
+ - A `.pyw` script that crashes dies silently: there is no console to show the traceback.
52
+ - Sharing a script that needs `requests` or `pandas` means asking the recipient to
53
+ understand pip, venvs, and PATH — or it just crashes with `ModuleNotFoundError`.
54
+
55
+ # Installation
56
+
57
+ ```commandline
58
+ pip install <path-to-this-repo> (PyPI package coming soon)
59
+ pydblclick register
60
+ ```
61
+
62
+ `pydblclick register` sets pydblclick as the default handler for `.py`/`.pyw` double-clicks
63
+ using the standard Windows mechanism (ProgID + UserChoice). This works on **all** Python
64
+ installations — classic installer *and* MSIX Python Manager (see
65
+ [MSIX_COMPATIBILITY.md](MSIX_COMPATIBILITY.md)). A backup of the previous file
66
+ associations is saved automatically before any change.
67
+
68
+ To undo everything:
69
+
70
+ ```commandline
71
+ pydblclick unregister
72
+ ```
73
+
74
+ To inspect the Windows file association chain (and detect MSIX interference):
75
+
76
+ ```commandline
77
+ pydblclick diagnose
78
+ ```
79
+
80
+ For automatic dependency resolution (PEP 723 scripts), also install
81
+ [uv](https://docs.astral.sh/uv/getting-started/installation/). Without uv, PEP 723
82
+ scripts still run with plain Python, and a message explains what to install.
83
+
84
+ # Usage
85
+
86
+ ## Double-click (the main purpose)
87
+
88
+ Once registered, **every** `.py`/`.pyw` file you double-click runs enhanced. Nothing to
89
+ add to the scripts themselves. Try the scripts in the [examples](examples/) folder.
90
+
91
+ ## Sharing dependency-aware one-file scripts (PEP 723 + uv)
92
+
93
+ Declare dependencies at the top of the script, in standard PEP 723 format:
94
+
95
+ ```python
96
+ # /// script
97
+ # requires-python = ">=3.11"
98
+ # dependencies = ["requests", "rich"]
99
+ # ///
100
+ import requests
101
+ ...
102
+ ```
103
+
104
+ On a machine with pydblclick + uv, double-clicking this file "just works": uv resolves
105
+ the dependencies in an ephemeral environment, and pydblclick keeps the window open with
106
+ its usual menu. This is a standard format — the same file also runs with `uv run` alone
107
+ on any platform. Use `uv add --script myscript.py requests` to maintain the block.
108
+
109
+ ## Opting a script out
110
+
111
+ Add this comment anywhere in a script to make pydblclick step aside (plain Python
112
+ behavior, no pause):
113
+
114
+ ```python
115
+ # pydblclick: off
116
+ ```
117
+
118
+ ## Pause only on error
119
+
120
+ To make an individual script skip the final pause *unless an exception occurred*:
121
+
122
+ ```python
123
+ pydblclick_customizations['must_pause_in_console'] = False
124
+ ```
125
+
126
+ ## Command line
127
+
128
+ `pydblclick <script.py> [args...]` (or `python -m pydblclick <script.py> [args...]`) wraps
129
+ a script explicitly. In a console there is no pause; set the
130
+ `pydblclick_simulate_doubleclick` env var to force double-click behavior (useful in
131
+ batch files and tests).
132
+
133
+ ## Custom icons
134
+
135
+ Scripts launched via pydblclick show the registered Python icon. For a custom icon,
136
+ create a shortcut to the script (ALT+drag & drop) and set the icon in its properties.
137
+
138
+ # How it works
139
+
140
+ Two processes (see [CLAUDE.md](CLAUDE.md) architecture notes and [ROADMAP.md](ROADMAP.md)):
141
+
142
+ - a thin **parent supervisor** (`python -m pydblclick`) which guarantees the window
143
+ survives anything — even `os._exit()`, a native crash, or a script that closes stdin;
144
+ - a **child engine** (`python -m pydblclick._child`) which runs your script with exact
145
+ plain-Python semantics (`runpy`), shows clean tracebacks (no wrapper frames), and
146
+ owns the pause menu. For PEP 723 scripts the child runs inside the uv-provisioned
147
+ environment.
148
+
149
+ No monkey-patching, no code injection: `__name__`, `__file__`, `sys.argv`, exit codes
150
+ and `exit()`/`quit()` behave exactly as with plain Python.
151
+
152
+ # Legacy: the shebang method (deprecated)
153
+
154
+ Before the 2026 pivot (under the project's former name *pyexewrap*), scripts were enhanced
155
+ individually with a shebang line (`#!/usr/bin/env python -m pydblclick`) read by the classic
156
+ `py.exe` launcher, and installation went through a system-wide PYTHONPATH (the helper scripts
157
+ have since been removed). This mechanism **still works on classic-installer systems**
158
+ provided pydblclick is importable by the system Python (`pip install` does that), but it is
159
+ a dead end:
160
+
161
+ - the classic `py.exe` launcher is deprecated since Python 3.14 and will not be produced
162
+ for Python 3.16+;
163
+ - the MSIX Python Manager (Microsoft Store / "Python Install Manager" on python.org)
164
+ never reads shebangs on double-click, and its shebang support
165
+ [does not allow arguments](https://docs.python.org/3/using/windows.html) such as
166
+ `-m pydblclick`.
167
+
168
+ Use `pydblclick register` instead; per-script granularity is provided by the
169
+ `# pydblclick: off` directive. See [MSIX_COMPATIBILITY.md](MSIX_COMPATIBILITY.md) for the
170
+ full compatibility matrix and history.
171
+
172
+ # Compatibility with the MSIX Python Manager (python/pymanager)
173
+
174
+ The `PythonSoftwareFoundation.PythonManager` MSIX package intercepts `.py`/`.pyw`
175
+ double-clicks through Windows App Model activation, bypassing shebangs and registry
176
+ ftype settings. **`pydblclick register` works with it**: the MSIX launcher honors
177
+ UserChoice pointing to the `pydblclick.PyFile` ProgID (confirmed by testing).
178
+
179
+ If double-clicks don't reach pydblclick, run `pydblclick diagnose` — it detects MSIX
180
+ interference and tells you what to fix. Details in
181
+ [MSIX_COMPATIBILITY.md](MSIX_COMPATIBILITY.md).
182
+
183
+ # Note about py.exe
184
+
185
+ `py.exe` was the Windows wrapper for multiple Python interpreters, making pydblclick a
186
+ wrapper of a wrapper. Its pymanager successor confirms that launcher-level wrapping is
187
+ not extensible — which is why pydblclick now registers itself as the file handler, the
188
+ one mechanism every launcher must respect.
189
+
190
+ # Todos
191
+
192
+ - Publish to PyPI (`pip install pydblclick`)
193
+ - Standalone `pydblclick.exe` handler (no Python required to bootstrap; uv can even
194
+ provision Python itself)
195
+ - Offer to install uv when a PEP 723 script is double-clicked and uv is missing
196
+ - Context menu items "Run with pydblclick" / "Bypass pydblclick"
197
+
198
+ # Contributions
199
+
200
+ Your contributions would be greatly appreciated. Feel free to copy the project.
@@ -0,0 +1,182 @@
1
+ # What is pydblclick ?
2
+
3
+ *(formerly known as **pyexewrap** — renamed before the first PyPI release, since the old
4
+ name referenced the deprecated `py.exe` launcher and suggested exe-building tools)*
5
+
6
+ pydblclick makes Python scripts pleasant to run **by double-click** on Windows — for you,
7
+ your colleagues, or anyone you share a one-file script with:
8
+
9
+ - The console window **never flashes away**: a pause prompt appears at the end of the
10
+ script, *including* when an exception occurs (even a syntax error), so the traceback
11
+ is always readable.
12
+ - Scripts that declare their dependencies inline ([PEP 723](https://peps.python.org/pep-0723/))
13
+ are executed through [uv](https://docs.astral.sh/uv/): **dependencies are resolved
14
+ automatically** in an ephemeral environment — the recipient never manages venvs or
15
+ `pip install`.
16
+ - An interactive menu at the pause prompt: `<i>` opens a Python console **with the
17
+ script's real variables** (post-mortem debugging), `<c>` opens a cmd console,
18
+ `<r>` restarts the script.
19
+ - `.pyw` (windowed) scripts run with **no console at all** (they are registered with
20
+ `pythonw.exe`) — but if they crash, a console is created on the spot showing the
21
+ script's output and the traceback, instead of dying silently.
22
+ - When a script is run from a console, called by another script or a batch file,
23
+ pydblclick stays out of the way: no pause, exit codes and arguments faithfully
24
+ propagated.
25
+
26
+ ## Python's native problems for Windows users
27
+
28
+ - A double-clicked `.py` file pops a console that flashes away, unless the last line is
29
+ a blocking `input()` — and even then, any exception (a syntax error, a missing module)
30
+ skips that line and the window vanishes before the traceback can be read.
31
+ - That blocking `input()` becomes undesirable when the same script is run from a console
32
+ or called by another script.
33
+ - A `.pyw` script that crashes dies silently: there is no console to show the traceback.
34
+ - Sharing a script that needs `requests` or `pandas` means asking the recipient to
35
+ understand pip, venvs, and PATH — or it just crashes with `ModuleNotFoundError`.
36
+
37
+ # Installation
38
+
39
+ ```commandline
40
+ pip install <path-to-this-repo> (PyPI package coming soon)
41
+ pydblclick register
42
+ ```
43
+
44
+ `pydblclick register` sets pydblclick as the default handler for `.py`/`.pyw` double-clicks
45
+ using the standard Windows mechanism (ProgID + UserChoice). This works on **all** Python
46
+ installations — classic installer *and* MSIX Python Manager (see
47
+ [MSIX_COMPATIBILITY.md](MSIX_COMPATIBILITY.md)). A backup of the previous file
48
+ associations is saved automatically before any change.
49
+
50
+ To undo everything:
51
+
52
+ ```commandline
53
+ pydblclick unregister
54
+ ```
55
+
56
+ To inspect the Windows file association chain (and detect MSIX interference):
57
+
58
+ ```commandline
59
+ pydblclick diagnose
60
+ ```
61
+
62
+ For automatic dependency resolution (PEP 723 scripts), also install
63
+ [uv](https://docs.astral.sh/uv/getting-started/installation/). Without uv, PEP 723
64
+ scripts still run with plain Python, and a message explains what to install.
65
+
66
+ # Usage
67
+
68
+ ## Double-click (the main purpose)
69
+
70
+ Once registered, **every** `.py`/`.pyw` file you double-click runs enhanced. Nothing to
71
+ add to the scripts themselves. Try the scripts in the [examples](examples/) folder.
72
+
73
+ ## Sharing dependency-aware one-file scripts (PEP 723 + uv)
74
+
75
+ Declare dependencies at the top of the script, in standard PEP 723 format:
76
+
77
+ ```python
78
+ # /// script
79
+ # requires-python = ">=3.11"
80
+ # dependencies = ["requests", "rich"]
81
+ # ///
82
+ import requests
83
+ ...
84
+ ```
85
+
86
+ On a machine with pydblclick + uv, double-clicking this file "just works": uv resolves
87
+ the dependencies in an ephemeral environment, and pydblclick keeps the window open with
88
+ its usual menu. This is a standard format — the same file also runs with `uv run` alone
89
+ on any platform. Use `uv add --script myscript.py requests` to maintain the block.
90
+
91
+ ## Opting a script out
92
+
93
+ Add this comment anywhere in a script to make pydblclick step aside (plain Python
94
+ behavior, no pause):
95
+
96
+ ```python
97
+ # pydblclick: off
98
+ ```
99
+
100
+ ## Pause only on error
101
+
102
+ To make an individual script skip the final pause *unless an exception occurred*:
103
+
104
+ ```python
105
+ pydblclick_customizations['must_pause_in_console'] = False
106
+ ```
107
+
108
+ ## Command line
109
+
110
+ `pydblclick <script.py> [args...]` (or `python -m pydblclick <script.py> [args...]`) wraps
111
+ a script explicitly. In a console there is no pause; set the
112
+ `pydblclick_simulate_doubleclick` env var to force double-click behavior (useful in
113
+ batch files and tests).
114
+
115
+ ## Custom icons
116
+
117
+ Scripts launched via pydblclick show the registered Python icon. For a custom icon,
118
+ create a shortcut to the script (ALT+drag & drop) and set the icon in its properties.
119
+
120
+ # How it works
121
+
122
+ Two processes (see [CLAUDE.md](CLAUDE.md) architecture notes and [ROADMAP.md](ROADMAP.md)):
123
+
124
+ - a thin **parent supervisor** (`python -m pydblclick`) which guarantees the window
125
+ survives anything — even `os._exit()`, a native crash, or a script that closes stdin;
126
+ - a **child engine** (`python -m pydblclick._child`) which runs your script with exact
127
+ plain-Python semantics (`runpy`), shows clean tracebacks (no wrapper frames), and
128
+ owns the pause menu. For PEP 723 scripts the child runs inside the uv-provisioned
129
+ environment.
130
+
131
+ No monkey-patching, no code injection: `__name__`, `__file__`, `sys.argv`, exit codes
132
+ and `exit()`/`quit()` behave exactly as with plain Python.
133
+
134
+ # Legacy: the shebang method (deprecated)
135
+
136
+ Before the 2026 pivot (under the project's former name *pyexewrap*), scripts were enhanced
137
+ individually with a shebang line (`#!/usr/bin/env python -m pydblclick`) read by the classic
138
+ `py.exe` launcher, and installation went through a system-wide PYTHONPATH (the helper scripts
139
+ have since been removed). This mechanism **still works on classic-installer systems**
140
+ provided pydblclick is importable by the system Python (`pip install` does that), but it is
141
+ a dead end:
142
+
143
+ - the classic `py.exe` launcher is deprecated since Python 3.14 and will not be produced
144
+ for Python 3.16+;
145
+ - the MSIX Python Manager (Microsoft Store / "Python Install Manager" on python.org)
146
+ never reads shebangs on double-click, and its shebang support
147
+ [does not allow arguments](https://docs.python.org/3/using/windows.html) such as
148
+ `-m pydblclick`.
149
+
150
+ Use `pydblclick register` instead; per-script granularity is provided by the
151
+ `# pydblclick: off` directive. See [MSIX_COMPATIBILITY.md](MSIX_COMPATIBILITY.md) for the
152
+ full compatibility matrix and history.
153
+
154
+ # Compatibility with the MSIX Python Manager (python/pymanager)
155
+
156
+ The `PythonSoftwareFoundation.PythonManager` MSIX package intercepts `.py`/`.pyw`
157
+ double-clicks through Windows App Model activation, bypassing shebangs and registry
158
+ ftype settings. **`pydblclick register` works with it**: the MSIX launcher honors
159
+ UserChoice pointing to the `pydblclick.PyFile` ProgID (confirmed by testing).
160
+
161
+ If double-clicks don't reach pydblclick, run `pydblclick diagnose` — it detects MSIX
162
+ interference and tells you what to fix. Details in
163
+ [MSIX_COMPATIBILITY.md](MSIX_COMPATIBILITY.md).
164
+
165
+ # Note about py.exe
166
+
167
+ `py.exe` was the Windows wrapper for multiple Python interpreters, making pydblclick a
168
+ wrapper of a wrapper. Its pymanager successor confirms that launcher-level wrapping is
169
+ not extensible — which is why pydblclick now registers itself as the file handler, the
170
+ one mechanism every launcher must respect.
171
+
172
+ # Todos
173
+
174
+ - Publish to PyPI (`pip install pydblclick`)
175
+ - Standalone `pydblclick.exe` handler (no Python required to bootstrap; uv can even
176
+ provision Python itself)
177
+ - Offer to install uv when a PEP 723 script is double-clicked and uv is missing
178
+ - Context menu items "Run with pydblclick" / "Bypass pydblclick"
179
+
180
+ # Contributions
181
+
182
+ Your contributions would be greatly appreciated. Feel free to copy the project.
File without changes
@@ -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()))