Qualite : pyproject.toml (ruff/pytest/pyright) + CI Gitea (lint/tests/build)

pyproject.toml devient le point d'entree unique de configuration des outils
(pytest.ini supprime, sa config y est deplacee). Les dependances restent dans
requirements.txt/requirements-dev.txt (deja utilises par setup.bat) plutot que
dupliquees ici : ce projet se distribue par PyInstaller, pas par "pip install".

ruff : configure puis passe sur tout app/ + tests/ (154 corrections
automatiques, purement mecaniques - tri des imports, imports multiples sur une
ligne separes, alias depreciees) suivies de 7 corrections manuelles ciblees :
  - app/ssh_connection.py : `raise ... from None` pour le refus explicite de
    passphrase (ce n'est pas un echec a diagnostiquer).
  - app/controllers/workspace_manager.py : zip() sans strict= documente en
    commentaire (deliberement tolerant a un workspace legerement perime).
  - tests/test_ai_redaction.py : nom de variable ambigu renomme.
  - E702 (construction compacte "widget = X(); widget.setY(...)" dans
    sftp_panel.py/tunnel_dialog.py) et E402 (quelques tests qui importent un
    symbole juste avant la section qui l'utilise) : ignores explicitement, ce
    sont des styles deliberes preexistants, pas des oublis.
`ruff check app main.py tests` est desormais propre.

pyright : mode "basic" (le code n'a jamais ete type). Plus de 200 signalements
sur ~300 etaient le meme faux positif connu de l'ecosysteme PySide6 (acces
plat aux enums Qt, ex. Qt.UserRole, valide a l'execution mais que les stubs ne
reconnaissent que sous leur forme imbriquee) : desactive avec justification en
commentaire. Reste un socle de 77 signalements reels (surtout des acces
Optional non filtres dans app/ui/tabs.py et app/ssh_connection.py) : non
corrige dans cette phase - resserrer le typage la ou pyright a raison est un
travail a part, pas une consequence automatique de l'ajout de l'outil. La CI
le lance en mode informatif (continue-on-error) tant que ce socle n'est pas
resorbe.

.gitea/workflows/ci.yml : lint (ruff, bloquant), tests (pytest, bloquant),
typecheck (pyright, informatif), build (PyInstaller, sur main uniquement,
artefact du dossier portable). Necessite un runner Gitea Actions Windows
enregistre - PySide6/pywinpty/Credential Manager/ACL NTFS sont Windows
uniquement, comme l'application elle-meme ; non verifiable depuis ce poste.

Suite complete revalidee apres coup (33/33) pour confirmer qu'aucune des 154
corrections automatiques n'a modifie de comportement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Florian
2026-09-05 07:46:50 +02:00
parent 09b7ed092c
commit c39ef9c038
72 changed files with 697 additions and 268 deletions

View File

@@ -2,7 +2,7 @@
aussi vers tous les autres onglets terminal ouverts, quand le mode est actif."""
from __future__ import annotations
from typing import Callable, Iterable
from collections.abc import Callable, Iterable
from PySide6.QtCore import QObject
from PySide6.QtWidgets import QToolButton, QWidget

View File

@@ -2,21 +2,21 @@
ouverture, connexion rapide, renommage, fermeture."""
from __future__ import annotations
from PySide6.QtCore import Qt, Signal, QObject
from PySide6.QtWidgets import QTabWidget, QWidget, QMenu, QMessageBox, QInputDialog
from PySide6.QtCore import QObject, Qt, Signal
from PySide6.QtWidgets import QInputDialog, QMenu, QMessageBox, QTabWidget, QWidget
from app.session_manager import SessionStore, Session, AUTH_PASSWORD, PASSWORD_STORAGE_SECURE
from app.bastion import BastionResolutionError, BastionTarget, resolve_bastion_target
from app.controllers.broadcast_manager import BroadcastManager
from app.credentials import CredentialStore, NullCredentialStore, session_credential_key
from app.dialogs.connect_prompt import ask_secret
from app.dialogs.session_dialog import SessionDialog
from app.key_manager import KeyManager
from app.local_terminal import DEFAULT_SHELL, SHELL_LABELS
from app.dialogs.session_dialog import SessionDialog
from app.dialogs.connect_prompt import ask_secret
from app.services.ssh_target import detect_ssh_target
from app.openssh.config import OpenSSHHost, session_from_openssh_host
from app.bastion import BastionTarget, resolve_bastion_target, BastionResolutionError
from app.credentials import CredentialStore, NullCredentialStore, session_credential_key
from app.controllers.broadcast_manager import BroadcastManager
from app.ui.tabs import BaseTerminalTab, SessionTabWidget, SFTPTabWidget, LocalTerminalTabWidget
from app.ui.split_pane import SplitContainer, PaneSlot, LAYOUT_LABELS
from app.services.ssh_target import detect_ssh_target
from app.session_manager import AUTH_PASSWORD, PASSWORD_STORAGE_SECURE, Session, SessionStore
from app.ui.split_pane import LAYOUT_LABELS, PaneSlot, SplitContainer
from app.ui.tabs import BaseTerminalTab, LocalTerminalTabWidget, SessionTabWidget, SFTPTabWidget
class ConnectionController(QObject):

View File

@@ -6,22 +6,33 @@ from __future__ import annotations
import copy
import uuid
from PySide6.QtCore import Qt, Signal, QObject
from PySide6.QtCore import QObject, Qt, Signal
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QToolButton, QTreeWidget, QTreeWidgetItem,
QMenu, QMessageBox, QComboBox, QLabel,
QComboBox,
QHBoxLayout,
QLabel,
QMenu,
QMessageBox,
QToolButton,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
)
from app.session_manager import SessionStore, Session
from app.key_manager import KeyManager
from app.groups import GroupSettingsStore
from app.credentials import CredentialStore, NullCredentialStore, session_credential_key
from app.local_terminal import SHELL_LABELS
from app.dialogs.session_dialog import SessionDialog
from app.dialogs.group_settings_dialog import GroupSettingsDialog
from app.dialogs.session_dialog import SessionDialog
from app.groups import GroupSettingsStore
from app.key_manager import KeyManager
from app.local_terminal import SHELL_LABELS
from app.openssh.config import (
OpenSSHHost, default_config_path, load_openssh_hosts, session_from_openssh_host,
OpenSSHHost,
default_config_path,
load_openssh_hosts,
session_from_openssh_host,
)
from app.session_manager import Session, SessionStore
SESSION_ROLE = Qt.UserRole + 10
LOCAL_SHELL_ROLE = Qt.UserRole + 11

View File

@@ -7,15 +7,20 @@ from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QWidget
from app.app_settings import get_setting, set_setting
from app.controllers.connection_controller import ConnectionController
from app.local_terminal import DEFAULT_SHELL
from app.session_manager import SessionStore
from app.ui.split_pane import LAYOUT_SINGLE, SplitContainer
from app.ui.tabs import LocalTerminalTabWidget, SessionTabWidget, SFTPTabWidget
from app.workspace import (
Workspace, WorkspaceStore, WorkspaceTab, WorkspaceItem,
ITEM_SESSION_SSH, ITEM_SESSION_SFTP, ITEM_LOCAL_SHELL,
ITEM_LOCAL_SHELL,
ITEM_SESSION_SFTP,
ITEM_SESSION_SSH,
Workspace,
WorkspaceItem,
WorkspaceStore,
WorkspaceTab,
)
from app.ui.tabs import SessionTabWidget, SFTPTabWidget, LocalTerminalTabWidget
from app.ui.split_pane import SplitContainer, LAYOUT_SINGLE
from app.controllers.connection_controller import ConnectionController
SETTING_RESTORE_ON_STARTUP = "restore_last_workspace"
SETTING_LAST_WORKSPACE_ID = "last_workspace_id"
@@ -152,7 +157,12 @@ class WorkspaceManager(QObject):
self.cc.split_tab(index, wtab.layout)
container = self.cc.tabs.widget(index)
skipped = 0
for pane, item in zip(container.panes[1:], wtab.items[1:]):
# strict=True est volontairement evite : un workspace legerement perime
# (enregistre par une version anterieure, ou modifie a la main) peut avoir
# une liste d'items plus courte que le nombre de panneaux - on restaure
# alors ce qui correspond et on laisse les panneaux excedentaires a leur
# terminal local par defaut, plutot que d'abandonner toute la restauration.
for pane, item in zip(container.panes[1:], wtab.items[1:]): # noqa: B905
widget = self._widget_for_item(item)
if widget is None:
skipped += 1