Skip to content

Commit fc38c2b

Browse files
author
Charles PIGNEROL
committed
Pré-version 7.0.0 : API QtPython3_v2 (refonte totale de la console). Correctif zone visible du viewport, possibilité d'ajouter/retirer des breakpoints lors des pauses en mode debug. Toujours des possibilités de bugs dus au multi-threading, une option de fonctionnement dans le thread courant en cours d'étude.
1 parent 7ce05f1 commit fc38c2b

4 files changed

Lines changed: 166 additions & 51 deletions

File tree

‎src/QtPython3_v2/CodeEditor.cpp‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,18 @@ void CodeEditor::setBreakpointMarks(const QSet<int>& lines)
185185
void CodeEditor::setExecuting(bool executing)
186186
{
187187
m_executing = executing;
188+
if (!executing) {
189+
m_paused = false; // pas de pause hors exécution
190+
}
188191
// Pendant l'exécution, tout le document est protégé (y compris le code
189192
// non encore exécuté), conformément au cahier des charges.
190193
}
191194

195+
void CodeEditor::setPaused(bool paused)
196+
{
197+
m_paused = paused;
198+
}
199+
192200
void CodeEditor::onContentsChange(int position, int charsRemoved, int charsAdded)
193201
{
194202
Q_UNUSED(charsRemoved);
@@ -443,9 +451,11 @@ void CodeEditor::mousePressEvent(QMouseEvent* event)
443451

444452
void CodeEditor::handleGutterClick(int y)
445453
{
446-
// Ne permet de poser/enlever un point d'arrêt que hors exécution et
447-
// uniquement sur une ligne de code non encore exécuté.
448-
if (m_executing)
454+
// Ne permet de poser/enlever un point d'arrêt que hors exécution, OU
455+
// pendant une pause en cours de débogage (m_paused) -- contrairement à
456+
// l'édition générale du texte, qui reste bloquée dans ce second cas --
457+
// et uniquement sur une ligne de code non encore exécuté.
458+
if (m_executing && !m_paused)
449459
return;
450460

451461
QTextCursor c = cursorForPosition(QPoint(0, y));

‎src/QtPython3_v2/PyConsoleWidget.cpp‎

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ void PyConsoleWidget::setRunningState(bool running)
139139
m_running = running;
140140
m_paused = false;
141141
m_editor->setExecuting(running);
142+
m_editor->setPaused(false);
142143
updateButtonsEnabled();
143144
m_stateLabel->setText(running ? tr("Exécution en cours...") : tr("Prêt"));
144145
}
@@ -170,6 +171,7 @@ void PyConsoleWidget::onStopClicked()
170171
// dès le premier "continue" ; l'arrêt ne prend alors effet qu'à la fin
171172
// naturelle de l'exécution (limitation connue).
172173
m_paused = false;
174+
m_editor->setPaused(false);
173175
updateButtonsEnabled();
174176
PyInterpreterBridge::instance().stopExec();
175177
}
@@ -205,6 +207,8 @@ void PyConsoleWidget::onRunClicked()
205207
void PyConsoleWidget::resumeFromPause()
206208
{
207209
m_paused = false;
210+
m_editor->setPaused(false);
211+
m_stateLabel->setText(tr("Exécution en cours..."));
208212
updateButtonsEnabled();
209213
PyInterpreterBridge::instance().continueExec();
210214
}
@@ -273,6 +277,8 @@ void PyConsoleWidget::onStepClicked()
273277
{
274278
if (m_paused) {
275279
m_paused = false;
280+
m_editor->setPaused(false);
281+
m_stateLabel->setText(tr("Exécution en cours..."));
276282
updateButtonsEnabled();
277283
PyInterpreterBridge::instance().step();
278284
return;
@@ -345,14 +351,37 @@ void PyConsoleWidget::execInstruction(const std::string& instruction, const std:
345351
void PyConsoleWidget::onBreakpointToggleRequested(int line)
346352
{
347353
QSet<int> bps = m_editor->breakpoints();
348-
if (bps.contains(line)) {
354+
const bool wasSet = bps.contains(line);
355+
if (wasSet) {
349356
bps.remove(line);
350357
} else {
351358
bps.insert(line);
352359
}
353360
m_editor->setBreakpointMarks(bps);
354-
// La validité réelle (ligne exécutable) n'est vérifiée qu'au lancement
355-
// (onRunClicked), une fois le code compilé.
361+
// La validité réelle (ligne exécutable) est de toute façon revérifiée
362+
// à chaque lancement (onRunClicked/startExecution), une fois le code
363+
// compilé -- mais si une session est déjà en cours (ce qui, la
364+
// gouttière n'acceptant les clics que hors exécution ou en pause,
365+
// signifie qu'on est nécessairement en pause), on répercute aussi
366+
// immédiatement le changement sur le débogueur Python déjà démarré,
367+
// pour qu'il prenne effet dès la reprise (Step/Run-Continuer) sans
368+
// attendre un nouveau Run.
369+
if (m_running) {
370+
auto& bridge = PyInterpreterBridge::instance();
371+
const int relative = line - m_executedBoundaryAtRunStart;
372+
if (wasSet) {
373+
bridge.clearBreakpoint(relative);
374+
} else if (relative >= 1) {
375+
if (!bridge.setBreakpoint(relative)) {
376+
// Rejeté par la validation Python (pas réellement une
377+
// instruction, ex: suite d'une ligne multi-lignes) :
378+
// annule l'ajout visuel qu'on venait de faire.
379+
bps.remove(line);
380+
m_editor->setBreakpointMarks(bps);
381+
m_output->appendStderr(tr("# Point d'arrêt ligne %1 invalide (pas une instruction)\n").arg(line));
382+
}
383+
}
384+
}
356385
}
357386

358387
void PyConsoleWidget::onCompletionRequested(const QString& prefix)
@@ -388,15 +417,30 @@ void PyConsoleWidget::onLineReached(int line)
388417
m_editor->setExecutedLineCount(docLine - 1);
389418
m_editor->setCurrentExecLine(docLine);
390419
recordExecutedLines(docLine - 1);
420+
421+
// Garde l'instruction courante visible sans que l'utilisateur ait à
422+
// faire défiler -- y compris lors d'une pause intermédiaire en cours
423+
// de session (pas seulement en toute fin d'exécution, où c'était déjà
424+
// fait par onExecutionFinished()) : docLine est recalculé à chaque
425+
// appel à partir de m_executedBoundaryAtRunStart (rafraîchi à chaque
426+
// Run), donc correct même si du texte a été inséré avant coup (via
427+
// insertExecutedInstructions() ou autre) et a décalé les lignes.
428+
m_editor->ensureLineVisible(docLine);
391429
}
392430

393431
void PyConsoleWidget::onPaused(int line)
394432
{
395433
// Emis uniquement quand l'exécution est réellement bloquée à cette
396-
// ligne, en attente d'un clic "Pas à pas" ou "Continuer".
397-
Q_UNUSED(line);
434+
// ligne, en attente d'un clic "Pas à pas" ou "Run" (agissant comme
435+
// "Continuer"). L'étiquette d'état passe alors de "Exécution en
436+
// cours..." à un état distinct "En pause" : sans ça, rien ne
437+
// distinguait visuellement une exécution qui tourne encore d'une
438+
// exécution qui attend une action de l'utilisateur.
398439
if (m_debugMode) {
399440
m_paused = true;
441+
m_editor->setPaused(true);
442+
const int docLine = m_executedBoundaryAtRunStart + line;
443+
m_stateLabel->setText(tr("En pause -- ligne %1").arg(docLine));
400444
updateButtonsEnabled();
401445
}
402446
}

‎src/QtPython3_v2/include/pyconsole/CodeEditor.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ class CodeEditor : public QPlainTextEdit
9595
void setExecuting(bool executing);
9696
bool isExecuting() const { return m_executing; }
9797

98+
// En pause en cours de débogage (sous-état de "en cours d'exécution") :
99+
// contrairement à l'édition générale du texte, qui reste interdite,
100+
// le clic gouttière (points d'arrêt) redevient autorisé -- cf.
101+
// handleGutterClick(). Sans effet si !isExecuting().
102+
void setPaused(bool paused);
103+
bool isPaused() const { return m_paused; }
104+
98105
// Appelé par la gouttière (LineNumberArea) lors d'un clic ; public pour
99106
// rester un simple composant interne sans dépendance friend.
100107
void handleGutterClick(int y);
@@ -186,6 +193,7 @@ private slots:
186193
int m_errorCommentLine = 0;
187194
int m_errorCommentLineCount = 1;
188195
bool m_executing = false;
196+
bool m_paused = false;
189197
QSet<int> m_breakpoints;
190198
int m_lastKnownBlockCount = 1; // pour détecter les insertions/suppressions de lignes
191199

‎src/QtPython3_v2/python/console_bridge.py‎

Lines changed: 96 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -58,38 +58,44 @@ def flush(self):
5858
# 1/2 du processus -- sans capture au niveau fd, ce texte n'apparaîtrait
5959
# jamais dans la console.
6060
# On duplique donc les fds réels vers des tubes (pipes), lus en continu par
61-
# des threads dédiés qui relaient chaque fragment via notify_output. Une
62-
# fois cette capture active, sys.stdout/sys.stderr Python restent les
63-
# objets standard (liés au fd 1/2, désormais redirigés vers nos tubes) --
64-
# on se garde bien de les remplacer par un objet Python personnalisé
65-
# séparé (cf. init_native ci-dessous) : TOUT (print() Python ET C/C++)
66-
# passe alors par le MÊME tube, lu par le MÊME thread, dans l'ordre
67-
# chronologique réel des écritures. Avoir deux chemins distincts (un appel
68-
# Python synchrone d'un côté, un tube lu de façon asynchrone par un thread
69-
# dédié de l'autre) faisait que les sorties C++ et Python pouvaient
70-
# apparaître dans un ordre différent de celui où elles avaient réellement
71-
# eu lieu.
72-
# NOTE : une fois les fds redirigés, TOUTE écriture sur stdout/stderr faite
73-
# par le PROCESSUS entier (pas seulement par le code exécuté dans la
74-
# console) passe par ce mécanisme -- c'est le prix à payer pour intercepter
75-
# des écritures C/C++ de bas niveau, qui n'ont pas d'autre point d'accroche.
61+
# des threads dédiés qui relaient chaque fragment via notify_output. Cette
62+
# capture est activée UNIQUEMENT pendant la durée d'une exécution
63+
# déclenchée par la console (start()/exec_already_executed()) et désactivée
64+
# (fds restaurés) dès qu'elle se termine -- ainsi, seules les sorties
65+
# produites par le code exécuté PAR LA CONSOLE sont récupérées, pas celles
66+
# d'une autre partie de l'application pendant le reste du temps.
67+
# Pendant la capture, sys.stdout/sys.stderr Python restent les objets
68+
# standard (liés au fd 1/2, temporairement redirigés vers nos tubes) -- on
69+
# se garde bien de les remplacer par un objet Python personnalisé séparé
70+
# (cf. init_native ci-dessous) : TOUT (print() Python ET C/C++) passe alors
71+
# par le MÊME tube, lu par le MÊME thread, dans l'ordre chronologique réel
72+
# des écritures. Avoir deux chemins distincts (un appel Python synchrone
73+
# d'un côté, un tube lu de façon asynchrone par un thread dédié de l'autre)
74+
# faisait que les sorties C++ et Python pouvaient apparaître dans un ordre
75+
# différent de celui où elles avaient réellement eu lieu.
7676
# Limite connue : le stdout/stderr C++ est généralement bufferisé en mode
7777
# "pleine mémoire tampon" (non ligne-par-ligne) dès lors qu'il ne pointe
7878
# plus sur un terminal ; le texte peut donc n'apparaître qu'après un flush
7979
# explicite (std::endl, std::flush, ou la fin du programme) côté code
8080
# wrappé -- ce n'est pas quelque chose que l'on puisse forcer depuis ici.
81-
_fd_capture_started = False
82-
83-
84-
def _start_fd_capture():
85-
"""Retourne True si la capture fd a bien démarré, False sinon
86-
(environnement sans vrais descripteurs de fichier standards, rare)."""
87-
global _fd_capture_started
88-
if _fd_capture_started:
81+
_saved_stdout_fd = None
82+
_saved_stderr_fd = None
83+
_capture_depth = 0 # compteur de réentrance (imbrication par précaution)
84+
85+
86+
def _enable_fd_capture():
87+
"""Redirige fd 1/2 vers des tubes pour la durée d'une exécution.
88+
Retourne True si la capture est active (déjà activée par un appel
89+
englobant, ou tout juste démarrée), False si les vrais descripteurs de
90+
fichier standards ne sont pas disponibles (environnement rare)."""
91+
global _saved_stdout_fd, _saved_stderr_fd, _capture_depth
92+
if _capture_depth > 0:
93+
_capture_depth += 1
8994
return True
90-
_fd_capture_started = True
9195

9296
try:
97+
_saved_stdout_fd = os.dup(1)
98+
_saved_stderr_fd = os.dup(2)
9399
stdout_r, stdout_w = os.pipe()
94100
stderr_r, stderr_w = os.pipe()
95101
os.dup2(stdout_w, 1)
@@ -112,9 +118,36 @@ def reader(fd, kind):
112118

113119
threading.Thread(target=reader, args=(stdout_r, "stdout"), daemon=True).start()
114120
threading.Thread(target=reader, args=(stderr_r, "stderr"), daemon=True).start()
121+
_capture_depth = 1
115122
return True
116123

117124

125+
def _disable_fd_capture():
126+
"""Restaure fd 1/2 à leur état d'avant _enable_fd_capture() (annule le
127+
dernier niveau d'imbrication uniquement)."""
128+
global _saved_stdout_fd, _saved_stderr_fd, _capture_depth
129+
if _capture_depth <= 0:
130+
return
131+
_capture_depth -= 1
132+
if _capture_depth > 0:
133+
return
134+
135+
try:
136+
sys.stdout.flush()
137+
sys.stderr.flush()
138+
except Exception:
139+
pass
140+
try:
141+
os.dup2(_saved_stdout_fd, 1)
142+
os.dup2(_saved_stderr_fd, 2)
143+
os.close(_saved_stdout_fd)
144+
os.close(_saved_stderr_fd)
145+
except OSError:
146+
pass
147+
_saved_stdout_fd = None
148+
_saved_stderr_fd = None
149+
150+
118151
# ---------------------------------------------------------------------------
119152
# Validation des lignes pouvant recevoir un point d'arrêt : uniquement les
120153
# lignes qui correspondent réellement à une instruction (bytecode), jamais
@@ -303,25 +336,17 @@ def init_native(native_module):
303336
global _native
304337
_native = native_module
305338

306-
if _start_fd_capture():
307-
# Tout (Python ET C/C++) transite désormais par le même tube :
308-
# sys.stdout/sys.stderr restent les objets standard, mais on les
309-
# passe en ligne-bufferisé pour que le texte apparaisse rapidement
310-
# dans la console (par défaut, un flux non lié à un terminal est
311-
# bufferisé en pleine mémoire tampon, ce qui retarderait
312-
# l'affichage jusqu'à ce que le tampon se remplisse).
313-
try:
314-
sys.stdout.reconfigure(line_buffering=True)
315-
sys.stderr.reconfigure(line_buffering=True)
316-
except (AttributeError, ValueError):
317-
pass # interpréteur trop ancien / flux non reconfigurable : tant pis
318-
else:
319-
# Environnement sans vrais descripteurs de fichier standards
320-
# (rare) : repli sur la redirection Python simple -- elle ne
321-
# capture que print()/write() Python (pas le C/C++ de bas niveau),
322-
# mais reste préférable à une absence totale de sortie.
323-
sys.stdout = _StreamRedirector("stdout")
324-
sys.stderr = _StreamRedirector("stderr")
339+
# Reconfigure sys.stdout/sys.stderr en ligne-bufferisé une bonne fois
340+
# pour toutes (réglage indépendant de ce que fd 1/2 pointe réellement à
341+
# un instant donné -- il s'applique aussi bien avant qu'après une
342+
# redirection temporaire via _enable_fd_capture()) : par défaut, un
343+
# flux non lié à un terminal est bufferisé en pleine mémoire tampon, ce
344+
# qui retarderait l'affichage jusqu'à ce que le tampon se remplisse.
345+
try:
346+
sys.stdout.reconfigure(line_buffering=True)
347+
sys.stderr.reconfigure(line_buffering=True)
348+
except (AttributeError, ValueError):
349+
pass # interpréteur trop ancien / flux non reconfigurable : tant pis
325350

326351

327352
def set_mode(mode):
@@ -402,6 +427,19 @@ def start(debug, step_first=False):
402427
_debugger._auto_steps_remaining = 1 if (debug and step_first) else 0
403428
code = _pending_code
404429

430+
# Capture des flux sortants scopée à la durée de CETTE exécution (voir
431+
# commentaire plus haut) : activée ici, désactivée dans le `finally`
432+
# de target() ci-dessous, une fois la session terminée (elle couvre
433+
# donc aussi bien toutes les pauses intermédiaires que la fin réelle).
434+
fd_capture_active = _enable_fd_capture()
435+
if not fd_capture_active:
436+
# Environnement sans vrais descripteurs de fichier standards
437+
# (rare) : repli sur la redirection Python simple -- elle ne
438+
# capture que print()/write() Python (pas le C/C++ de bas niveau),
439+
# mais reste préférable à une absence totale de sortie.
440+
sys.stdout = _StreamRedirector("stdout")
441+
sys.stderr = _StreamRedirector("stderr")
442+
405443
def target():
406444
# Distingue, pour le C++, un arrêt VOLONTAIRE (bouton "Arrêter",
407445
# via request_stop()) d'une fin normale (tout le code envoyé a
@@ -457,6 +495,11 @@ def target():
457495
_native.notify_exception(lineno, text)
458496
finally:
459497
_finish(stopped_early)
498+
if fd_capture_active:
499+
_disable_fd_capture()
500+
else:
501+
sys.stdout = sys.__stdout__
502+
sys.stderr = sys.__stderr__
460503

461504
_run_thread = threading.Thread(target=target, daemon=True)
462505
_run_thread.start()
@@ -498,10 +541,20 @@ def exec_already_executed(source):
498541
appelé que lorsque la console n'est pas en cours d'exécution.
499542
Retourne None en cas de succès, ou le texte de l'exception sinon.
500543
"""
544+
fd_capture_active = _enable_fd_capture()
545+
if not fd_capture_active:
546+
sys.stdout = _StreamRedirector("stdout")
547+
sys.stderr = _StreamRedirector("stderr")
501548
try:
502549
code = compile(source, CONSOLE_FILENAME, "exec")
503550
exec(code, _console_globals, _console_globals)
504551
return None
505552
except BaseException:
506553
etype, evalue, _etb = sys.exc_info()
507554
return "".join(traceback.format_exception_only(etype, evalue)).strip()
555+
finally:
556+
if fd_capture_active:
557+
_disable_fd_capture()
558+
else:
559+
sys.stdout = sys.__stdout__
560+
sys.stderr = sys.__stderr__

0 commit comments

Comments
 (0)