gui.workers.build_worker

  1# Copyright (C) 2026  David Vaquero <pepesan@gmail.com>
  2#
  3# This program is free software: you can redistribute it and/or modify
  4# it under the terms of the GNU General Public License as published by
  5# the Free Software Foundation, either version 3 of the License, or
  6# (at your option) any later version.
  7#
  8# This program is distributed in the hope that it will be useful,
  9# but WITHOUT ANY WARRANTY; without even the implied warranty of
 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 11# GNU General Public License for more details.
 12#
 13# You should have received a copy of the GNU General Public License
 14# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 15
 16import sys
 17import traceback
 18from pathlib import Path
 19from types import SimpleNamespace
 20
 21from PySide6.QtCore import QObject, Signal
 22
 23from gui.utils.path_setup import setup_path
 24setup_path()
 25
 26from cli.__main__ import (
 27    generar, _cmd_clean, _cmd_version, _cmd_new,
 28    _cmd_generate, _cmd_import,
 29)
 30
 31
 32class _StdoutCapture:
 33    """Redirige sys.stdout → emite cada línea al callback."""
 34
 35    def __init__(self, callback):
 36        self._cb   = callback
 37        self._buf  = ''
 38        self._orig = sys.stdout
 39
 40    def write(self, text: str) -> None:
 41        self._buf += text
 42        while '\n' in self._buf:
 43            line, self._buf = self._buf.split('\n', 1)
 44            self._cb(line)
 45
 46    def flush(self) -> None:
 47        pass
 48
 49    def __enter__(self):
 50        sys.stdout = self
 51        return self
 52
 53    def __exit__(self, *_) -> None:
 54        if self._buf:
 55            self._cb(self._buf)
 56            self._buf = ''
 57        sys.stdout = self._orig
 58
 59
 60class BuildWorker(QObject):
 61    """Ejecuta operaciones de build/clean/version/new en un QThread.
 62
 63    Uso:
 64        worker = BuildWorker('build', yamls=[Path(...)])
 65        thread = QThread()
 66        worker.moveToThread(thread)
 67        thread.started.connect(worker.run)
 68        worker.finished.connect(thread.quit)
 69        thread.start()
 70    """
 71
 72    log_line        = Signal(str, str)   # (texto, nivel: 'info' | 'ok' | 'error')
 73    finished        = Signal(bool)       # True = éxito
 74    progress        = Signal(int, int)   # (actual, total)
 75    files_generated = Signal(list)       # list[tuple[Path, Path | None]]
 76
 77    def __init__(self, task: str, **kwargs):
 78        super().__init__()
 79        self._task   = task
 80        self._kwargs = kwargs
 81
 82    # ── Punto de entrada del hilo ─────────────────────────────────────────────
 83
 84    def run(self) -> None:
 85        try:
 86            with _StdoutCapture(self._on_stdout):
 87                self._dispatch()
 88            self.finished.emit(True)
 89        except SystemExit as exc:
 90            code = exc.code
 91            if code and code != 0:
 92                self.log_line.emit(f'Error: código de salida {code}', 'error')
 93                self.finished.emit(False)
 94            else:
 95                self.finished.emit(True)
 96        except Exception:
 97            for line in traceback.format_exc().splitlines():
 98                self.log_line.emit(line, 'error')
 99            self.finished.emit(False)
100
101    # ── Despacho de tareas ────────────────────────────────────────────────────
102
103    def _dispatch(self) -> None:
104        task = self._task
105
106        if task == 'build':
107            yamls = self._kwargs.get('yamls', [])
108            _cmd_generate(
109                SimpleNamespace(targets=yamls, template=None),
110                progress_callback=lambda cur, tot: self.progress.emit(cur, tot),
111                results_callback=lambda r: self.files_generated.emit(r),
112            )
113
114        elif task == 'clean':
115            _cmd_clean(SimpleNamespace(
116                targets=self._kwargs.get('course_dirs', []),
117                tipo=self._kwargs.get('tipo', None),
118            ))
119
120        elif task == 'version':
121            _cmd_version(SimpleNamespace(
122                targets=self._kwargs.get('course_dirs', []),
123            ))
124
125        elif task == 'new':
126            _cmd_new(SimpleNamespace(name=self._kwargs['name']))
127
128        elif task == 'import_odp':
129            _cmd_import(Path(self._kwargs['odp_path']))
130
131        elif task == 'watch':
132            self._run_watch()
133
134        else:
135            raise ValueError(f'Tarea desconocida: {self._task!r}')
136
137    def _run_watch(self) -> None:
138        """Watch reactivo: regenera YAMLs al guardar usando lib.watcher."""
139        from lib.watcher import start_watch, WatchdogNotAvailable
140        from cli.__main__ import generar, _course_dir_from_yaml
141
142        yamls: list[Path] = [Path(y) for y in self._kwargs.get('yamls', [])]
143        no_pdf   = self._kwargs.get('no_pdf', False)
144        template = self._kwargs.get('template', None)
145
146        def _on_change(yaml_path: Path) -> None:
147            self.log_line.emit(f'[watch] {yaml_path.name}', 'info')
148            try:
149                generar(yaml_path, template=template, no_pdf=no_pdf)
150                self.log_line.emit(f'✓ {yaml_path.name}', 'ok')
151            except Exception as exc:
152                self.log_line.emit(f'Error: {exc}', 'error')
153
154        try:
155            start_watch(yamls, _on_change)
156        except WatchdogNotAvailable as exc:
157            self.log_line.emit(str(exc), 'error')
158            raise SystemExit(1)
159
160    # ── Clasificador de nivel de log ──────────────────────────────────────────
161
162    def _on_stdout(self, line: str) -> None:
163        if not line.strip():
164            return
165        low = line.lower()
166        if any(k in low for k in ('error', 'traceback', 'exception')):
167            level = 'error'
168        elif any(k in low for k in ('generado', 'success', 'ok', 'creado', 'versión')):
169            level = 'ok'
170        else:
171            level = 'info'
172        self.log_line.emit(line, level)
class BuildWorker(PySide6.QtCore.QObject):
 61class BuildWorker(QObject):
 62    """Ejecuta operaciones de build/clean/version/new en un QThread.
 63
 64    Uso:
 65        worker = BuildWorker('build', yamls=[Path(...)])
 66        thread = QThread()
 67        worker.moveToThread(thread)
 68        thread.started.connect(worker.run)
 69        worker.finished.connect(thread.quit)
 70        thread.start()
 71    """
 72
 73    log_line        = Signal(str, str)   # (texto, nivel: 'info' | 'ok' | 'error')
 74    finished        = Signal(bool)       # True = éxito
 75    progress        = Signal(int, int)   # (actual, total)
 76    files_generated = Signal(list)       # list[tuple[Path, Path | None]]
 77
 78    def __init__(self, task: str, **kwargs):
 79        super().__init__()
 80        self._task   = task
 81        self._kwargs = kwargs
 82
 83    # ── Punto de entrada del hilo ─────────────────────────────────────────────
 84
 85    def run(self) -> None:
 86        try:
 87            with _StdoutCapture(self._on_stdout):
 88                self._dispatch()
 89            self.finished.emit(True)
 90        except SystemExit as exc:
 91            code = exc.code
 92            if code and code != 0:
 93                self.log_line.emit(f'Error: código de salida {code}', 'error')
 94                self.finished.emit(False)
 95            else:
 96                self.finished.emit(True)
 97        except Exception:
 98            for line in traceback.format_exc().splitlines():
 99                self.log_line.emit(line, 'error')
100            self.finished.emit(False)
101
102    # ── Despacho de tareas ────────────────────────────────────────────────────
103
104    def _dispatch(self) -> None:
105        task = self._task
106
107        if task == 'build':
108            yamls = self._kwargs.get('yamls', [])
109            _cmd_generate(
110                SimpleNamespace(targets=yamls, template=None),
111                progress_callback=lambda cur, tot: self.progress.emit(cur, tot),
112                results_callback=lambda r: self.files_generated.emit(r),
113            )
114
115        elif task == 'clean':
116            _cmd_clean(SimpleNamespace(
117                targets=self._kwargs.get('course_dirs', []),
118                tipo=self._kwargs.get('tipo', None),
119            ))
120
121        elif task == 'version':
122            _cmd_version(SimpleNamespace(
123                targets=self._kwargs.get('course_dirs', []),
124            ))
125
126        elif task == 'new':
127            _cmd_new(SimpleNamespace(name=self._kwargs['name']))
128
129        elif task == 'import_odp':
130            _cmd_import(Path(self._kwargs['odp_path']))
131
132        elif task == 'watch':
133            self._run_watch()
134
135        else:
136            raise ValueError(f'Tarea desconocida: {self._task!r}')
137
138    def _run_watch(self) -> None:
139        """Watch reactivo: regenera YAMLs al guardar usando lib.watcher."""
140        from lib.watcher import start_watch, WatchdogNotAvailable
141        from cli.__main__ import generar, _course_dir_from_yaml
142
143        yamls: list[Path] = [Path(y) for y in self._kwargs.get('yamls', [])]
144        no_pdf   = self._kwargs.get('no_pdf', False)
145        template = self._kwargs.get('template', None)
146
147        def _on_change(yaml_path: Path) -> None:
148            self.log_line.emit(f'[watch] {yaml_path.name}', 'info')
149            try:
150                generar(yaml_path, template=template, no_pdf=no_pdf)
151                self.log_line.emit(f'✓ {yaml_path.name}', 'ok')
152            except Exception as exc:
153                self.log_line.emit(f'Error: {exc}', 'error')
154
155        try:
156            start_watch(yamls, _on_change)
157        except WatchdogNotAvailable as exc:
158            self.log_line.emit(str(exc), 'error')
159            raise SystemExit(1)
160
161    # ── Clasificador de nivel de log ──────────────────────────────────────────
162
163    def _on_stdout(self, line: str) -> None:
164        if not line.strip():
165            return
166        low = line.lower()
167        if any(k in low for k in ('error', 'traceback', 'exception')):
168            level = 'error'
169        elif any(k in low for k in ('generado', 'success', 'ok', 'creado', 'versión')):
170            level = 'ok'
171        else:
172            level = 'info'
173        self.log_line.emit(line, level)

Ejecuta operaciones de build/clean/version/new en un QThread.

Uso:

worker = BuildWorker('build', yamls=[Path(...)]) thread = QThread() worker.moveToThread(thread) thread.started.connect(worker.run) worker.finished.connect(thread.quit) thread.start()

BuildWorker(task: str, **kwargs)
78    def __init__(self, task: str, **kwargs):
79        super().__init__()
80        self._task   = task
81        self._kwargs = kwargs
def log_line(unknown):
def finished(unknown):
def progress(unknown):
def files_generated(unknown):
def run(self) -> None:
 85    def run(self) -> None:
 86        try:
 87            with _StdoutCapture(self._on_stdout):
 88                self._dispatch()
 89            self.finished.emit(True)
 90        except SystemExit as exc:
 91            code = exc.code
 92            if code and code != 0:
 93                self.log_line.emit(f'Error: código de salida {code}', 'error')
 94                self.finished.emit(False)
 95            else:
 96                self.finished.emit(True)
 97        except Exception:
 98            for line in traceback.format_exc().splitlines():
 99                self.log_line.emit(line, 'error')
100            self.finished.emit(False)
staticMetaObject = PySide6.QtCore.QMetaObject("BuildWorker" inherits "QObject": Methods: #4 type=Signal, signature=log_line(QString,QString), parameters=QString, QString #5 type=Signal, signature=finished(bool), parameters=bool #6 type=Signal, signature=progress(int,int), parameters=int, int #7 type=Signal, signature=files_generated(QVariantList), parameters=QVariantList )