Skip to content

Commit 9b75306

Browse files
committed
Decouple le bootstrap CLI de Qt
Evite d'initialiser Qt au demarrage de la CLI lorsque la commande demandee est purement headless, afin de permettre l'execution de l'outil sans charger PySide6 pour les cas non GUI. Introduit une decision explicite via should_enable_qt dans le runtime, puis branche l'entrypoint pour n'activer les metadonnees et handlers Qt que lorsque la commande cible ouvre effectivement une interface graphique. Supprime egalement les imports module-level qui tiraient EngineLoader et les launchers GUI trop tot dans click_app, fallback et la CLI dediee, en les remplacant par des helpers lazy centralises dans cli/lazy_ops.py. Le chemin headless couvre desormais correctement les commandes telles que --help, --version, --info, --cli, unload, engine ..., ainsi que engines --dry-run sans bootstrap Qt prealable. Ajoute enfin un test cible pour verifier la decision d'activation de Qt et garantir que les modules CLI principaux restent importables sans effet de bord GUI immediat.
1 parent 52b8618 commit 9b75306

7 files changed

Lines changed: 167 additions & 46 deletions

File tree

cli/click_app.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
from pathlib import Path
77
import sys
88

9-
from EngineLoader import unload_all
10-
119
from .dedicated import run_dedicated_cli
12-
from .launchers import (
13-
launch_bcasl_standalone,
14-
launch_engines_only_standalone,
15-
launch_main_application,
10+
from .lazy_ops import (
11+
available_engine_ids,
12+
launch_bcasl_gui,
13+
launch_engines_gui,
14+
launch_main_gui,
15+
unload_all_engines,
1616
)
1717
from .output import error, info, plain, warn
1818
from .system_info import print_system_info
@@ -109,7 +109,7 @@ def cli(
109109
ctx.exit(run_dedicated_cli(app_version))
110110

111111
if unload_engines_flag:
112-
result = unload_all()
112+
result = unload_all_engines()
113113
if result["status"] == "success":
114114
info(f"{result['message']}")
115115
if result["unloaded"]:
@@ -137,7 +137,7 @@ def cli(
137137

138138
if ctx.invoked_subcommand is None:
139139
ctx.exit(
140-
launch_main_application(
140+
launch_main_gui(
141141
no_splash=ctx.obj.get("no_splash", False),
142142
ide_gui=ctx.obj.get("ide_gui", False),
143143
classic_gui=ctx.obj.get("classic_gui", False),
@@ -166,7 +166,7 @@ def bcasl(workspace):
166166
error(f"Failed to create directory: {exc}")
167167
sys.exit(1)
168168

169-
sys.exit(launch_bcasl_standalone(workspace_dir))
169+
sys.exit(launch_bcasl_gui(workspace_dir))
170170

171171
@cli.command(context_settings=dict(help_option_names=["-h", "--help"]))
172172
@click.argument("workspace", required=False, type=click.Path(exists=False))
@@ -206,23 +206,21 @@ def engines(workspace, dry_run, language, theme):
206206
sys.exit(1)
207207

208208
if dry_run:
209-
from EngineLoader import available_engines
210-
211-
engines = available_engines()
209+
engines = available_engine_ids()
212210
plain(f"Available engines ({len(engines)}):")
213211
for eid in engines:
214212
plain(f" • {eid}")
215213
sys.exit(0)
216214

217-
sys.exit(launch_engines_only_standalone(workspace_dir))
215+
sys.exit(launch_engines_gui(workspace_dir))
218216

219217
@cli.command(context_settings=dict(help_option_names=["-h", "--help"]))
220218
def main_app():
221219
"""Launch the main PyCompiler ARK application."""
222220
ctx = click.get_current_context(silent=True)
223221
ctx_obj = ctx.obj if ctx is not None else {}
224222
sys.exit(
225-
launch_main_application(
223+
launch_main_gui(
226224
no_splash=bool(ctx_obj.get("no_splash", False)),
227225
ide_gui=bool(ctx_obj.get("ide_gui", False)),
228226
)
@@ -233,7 +231,7 @@ def main_app():
233231
)
234232
def unload_engines_cmd():
235233
"""Unload all registered engines."""
236-
result = unload_all()
234+
result = unload_all_engines()
237235
if result["status"] == "success":
238236
info(f"{result['message']}")
239237
if result["unloaded"]:

cli/dedicated.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import shlex
77
from pathlib import Path
88

9-
from EngineLoader import available_engines, unload_all
10-
11-
from .launchers import (
12-
launch_bcasl_standalone,
13-
launch_engines_only_standalone,
14-
launch_main_application,
9+
from .lazy_ops import (
10+
available_engine_ids,
11+
launch_bcasl_gui,
12+
launch_engines_gui,
13+
launch_main_gui,
14+
unload_all_engines,
1515
)
1616
from .output import error, info, plain, success, warn
1717
from .system_info import print_system_info
@@ -222,7 +222,7 @@ def _resolve_workspace(raw: str | None) -> str | None:
222222

223223
def _run_engines(args: list[str]) -> int:
224224
if "--dry-run" in args or "-d" in args:
225-
engines = available_engines()
225+
engines = available_engine_ids()
226226
if _RICH_CONSOLE is not None and Table is not None:
227227
table = Table(
228228
title=f"Available engines ({len(engines)})", header_style="bold magenta"
@@ -238,12 +238,12 @@ def _run_engines(args: list[str]) -> int:
238238
plain(f" • {eid}")
239239
return 0
240240
workspace = _resolve_workspace(args[0]) if args else None
241-
return launch_engines_only_standalone(workspace)
241+
return launch_engines_gui(workspace)
242242

243243

244244
def _run_bcasl(args: list[str]) -> int:
245245
workspace = _resolve_workspace(args[0]) if args else None
246-
return launch_bcasl_standalone(workspace)
246+
return launch_bcasl_gui(workspace)
247247

248248

249249
def _new_bcasl_app(workspace: str | None = None):
@@ -591,7 +591,7 @@ def _run_engine_command(args: list[str]) -> int:
591591

592592

593593
def _run_unload() -> int:
594-
result = unload_all()
594+
result = unload_all_engines()
595595
if result["status"] == "success":
596596
success(result["message"])
597597
if result["unloaded"]:
@@ -720,7 +720,7 @@ def run_dedicated_cli(app_version: str) -> int:
720720
plain("Usage: main [--ide-gui]")
721721
ide_gui = False
722722
break
723-
launch_main_application(no_splash=False, ide_gui=ide_gui)
723+
launch_main_gui(no_splash=False, ide_gui=ide_gui)
724724
continue
725725
if cmd == "bcasl":
726726
if args and args[0].lower() in ("help", "list", "run"):

cli/entrypoint.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from .click_app import build_cli, has_click
1010
from .fallback import run as run_fallback
11-
from .runtime import install_runtime, handle_fatal
11+
from .runtime import install_runtime, handle_fatal, should_enable_qt
1212

1313

1414
def _resolve_app_version() -> str:
@@ -27,12 +27,12 @@ def _resolve_app_version() -> str:
2727

2828
def main(argv: list[str] | None = None) -> int:
2929
app_version = _resolve_app_version()
30-
args = list(argv) if argv is not None else None
30+
args = list(argv) if argv is not None else list(sys.argv[1:])
3131
if args and "--verbose" in args:
3232
import os
3333

3434
os.environ["PYCOMPILER_VERBOSE"] = "1"
35-
install_runtime(app_version)
35+
install_runtime(app_version, enable_qt=should_enable_qt(args))
3636

3737
if has_click():
3838
try:

cli/fallback.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
from pathlib import Path
88
from typing import Iterable, Optional
99

10-
from EngineLoader import unload_all
11-
12-
from .launchers import (
13-
launch_bcasl_standalone,
14-
launch_engines_only_standalone,
15-
launch_main_application,
10+
from .lazy_ops import (
11+
available_engine_ids,
12+
launch_bcasl_gui,
13+
launch_engines_gui,
14+
launch_main_gui,
15+
unload_all_engines,
1616
)
1717
from .dedicated import run_dedicated_cli
1818
from .output import error, info, plain, warn
@@ -85,7 +85,7 @@ def run(argv: Optional[list[str]], app_version: str) -> int:
8585
print_system_info(app_version)
8686
return 0
8787
if args[0] == "--unload":
88-
result = unload_all()
88+
result = unload_all_engines()
8989
if result["status"] == "success":
9090
info(f"{result['message']}")
9191
if result["unloaded"]:
@@ -97,7 +97,7 @@ def run(argv: Optional[list[str]], app_version: str) -> int:
9797
return 0 if result["status"] == "success" else 1
9898
if args[0] == "bcasl":
9999
workspace_dir = args[1] if len(args) > 1 else None
100-
return launch_bcasl_standalone(workspace_dir)
100+
return launch_bcasl_gui(workspace_dir)
101101
if args[0] == "engines":
102102
sub_args = args[1:]
103103
if (
@@ -106,18 +106,16 @@ def run(argv: Optional[list[str]], app_version: str) -> int:
106106
or "--dry-run" in sub_args
107107
or "-d" in sub_args
108108
):
109-
from EngineLoader import available_engines
110-
111-
engines = available_engines()
109+
engines = available_engine_ids()
112110
plain(f"Available engines ({len(engines)}):")
113111
for eid in engines:
114112
plain(f" - {eid}")
115113
return 0
116114

117115
workspace_dir = _maybe_workspace(sub_args)
118-
return launch_engines_only_standalone(workspace_dir)
116+
return launch_engines_gui(workspace_dir)
119117
if args[0] == "unload":
120-
result = unload_all()
118+
result = unload_all_engines()
121119
if result["status"] == "success":
122120
info(f"{result['message']}")
123121
if result["unloaded"]:
@@ -132,6 +130,6 @@ def run(argv: Optional[list[str]], app_version: str) -> int:
132130
plain(USAGE)
133131
return 1
134132

135-
return launch_main_application(
133+
return launch_main_gui(
136134
no_splash=no_splash, ide_gui=ide_gui, classic_gui=classic_gui
137135
)

cli/lazy_ops.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2026 Ague Samuel Amen
3+
4+
from __future__ import annotations
5+
6+
7+
def unload_all_engines():
8+
from EngineLoader import unload_all
9+
10+
return unload_all()
11+
12+
13+
def available_engine_ids() -> list[str]:
14+
from EngineLoader import available_engines
15+
16+
return list(available_engines())
17+
18+
19+
def launch_bcasl_gui(workspace_dir: str | None = None) -> int:
20+
from .launchers import launch_bcasl_standalone
21+
22+
return launch_bcasl_standalone(workspace_dir)
23+
24+
25+
def launch_engines_gui(workspace_dir: str | None = None) -> int:
26+
from .launchers import launch_engines_only_standalone
27+
28+
return launch_engines_only_standalone(workspace_dir)
29+
30+
31+
def launch_main_gui(
32+
no_splash: bool = False, ide_gui: bool = False, classic_gui: bool = False
33+
) -> int:
34+
from .launchers import launch_main_application
35+
36+
return launch_main_application(
37+
no_splash=no_splash,
38+
ide_gui=ide_gui,
39+
classic_gui=classic_gui,
40+
)

cli/runtime.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,11 +215,46 @@ def handle_fatal(exc_info) -> None:
215215
_excepthook(*exc_info)
216216

217217

218-
def install_runtime(app_version: str) -> None:
218+
def should_enable_qt(argv: list[str] | None) -> bool:
219+
"""Return True only when the requested command path really needs Qt startup."""
220+
args = list(argv or [])
221+
if not args:
222+
return True
223+
224+
if any(flag in args for flag in ("--help", "-h", "--version", "-v", "--info", "--cli", "--help-all")):
225+
return False
226+
227+
if "--classic-gui" in args or "--ide-gui" in args:
228+
return True
229+
230+
if "--unload" in args:
231+
return False
232+
233+
cmd = None
234+
for token in args:
235+
if not token.startswith("-"):
236+
cmd = token
237+
break
238+
239+
if cmd is None:
240+
return True
241+
if cmd in ("main",):
242+
return True
243+
if cmd in ("unload", "version", "help", "info", "engine"):
244+
return False
245+
if cmd == "engines":
246+
return not any(flag in args for flag in ("--dry-run", "-d"))
247+
if cmd == "bcasl":
248+
return False
249+
return False
250+
251+
252+
def install_runtime(app_version: str, enable_qt: bool = True) -> None:
219253
ensure_sys_path()
220254
configure_logging()
221255
configure_env()
222256
enable_faulthandler()
223-
install_qt_metadata(app_version)
224-
install_qt_handlers()
257+
if enable_qt:
258+
install_qt_metadata(app_version)
259+
install_qt_handlers()
225260
install_signal_handlers()

tests/test_cli_headless_runtime.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2026 Ague Samuel Amen
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from __future__ import annotations
17+
18+
import sys
19+
from pathlib import Path
20+
21+
ROOT = Path(__file__).resolve().parents[1]
22+
if str(ROOT) not in sys.path:
23+
sys.path.insert(0, str(ROOT))
24+
25+
from cli import click_app, dedicated, fallback
26+
from cli.runtime import should_enable_qt
27+
28+
29+
def test_should_enable_qt_for_headless_commands() -> None:
30+
assert should_enable_qt(["--help"]) is False
31+
assert should_enable_qt(["--version"]) is False
32+
assert should_enable_qt(["--info"]) is False
33+
assert should_enable_qt(["--cli"]) is False
34+
assert should_enable_qt(["--unload"]) is False
35+
assert should_enable_qt(["engines", "--dry-run"]) is False
36+
assert should_enable_qt(["engine", "list"]) is False
37+
assert should_enable_qt(["bcasl", "list"]) is False
38+
39+
40+
def test_should_enable_qt_for_gui_commands() -> None:
41+
assert should_enable_qt([]) is True
42+
assert should_enable_qt(["main"]) is True
43+
assert should_enable_qt(["--ide-gui"]) is True
44+
assert should_enable_qt(["engines"]) is True
45+
46+
47+
def test_cli_modules_import_without_qt_bootstrap_side_effects() -> None:
48+
assert callable(click_app.build_cli)
49+
assert callable(fallback.run)
50+
assert callable(dedicated.run_dedicated_cli)

0 commit comments

Comments
 (0)