Coverage for denofo/questionnaire/questionnaire_gui.py: 80%

384 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-09 15:27 +0200

1import sys 

2from typing import Any 

3from enum import Enum 

4from pathlib import Path 

5from pydantic import BaseModel, ValidationError 

6from denofo.utils.constants import SECTIONS, GoQBack 

7from denofo.converter.convert import convert_to_json 

8from denofo.models import ModelValidError 

9from denofo.utils.ncbiTaxDBcheck import check_NCBI_taxDB 

10from denofo.questionnaire.questions import DeNovoQuestionnaire 

11from PyQt6.QtCore import Qt, QObject 

12from PyQt6.QtWidgets import ( 

13 QApplication, 

14 QMainWindow, 

15 QWidget, 

16 QVBoxLayout, 

17 QHBoxLayout, 

18 QPushButton, 

19 QLabel, 

20 QListWidget, 

21 QLineEdit, 

22 QMessageBox, 

23 QListWidgetItem, 

24 QRadioButton, 

25 QButtonGroup, 

26 QDialog, 

27 QFileDialog, 

28) 

29 

30 

31def clearLayout(layout): 

32 """ 

33 Function to clear the layout of a QWidget. 

34 """ 

35 while layout.count(): 

36 child = layout.takeAt(0) 

37 if child.widget(): 

38 child.widget().deleteLater() 

39 

40 

41class QSingleton(type(QObject)): 

42 """Metaclass for Qt classes that are singletons.""" 

43 

44 def __init__(self, *args, **kwargs): 

45 super().__init__(*args, **kwargs) 

46 self.instance = None 

47 

48 def __call__(self, *args, **kwargs): 

49 if self.instance is None: 

50 self.instance = super().__call__(*args, **kwargs) 

51 return self.instance 

52 

53 

54class MainWindow(QMainWindow, metaclass=QSingleton): 

55 """ 

56 Main window class for the GUI application. 

57 """ 

58 

59 _geometry = None 

60 _centered = False 

61 

62 def __init__(self, *args, **kwargs): 

63 super().__init__(*args, **kwargs) 

64 self.setWindowTitle("DeNoFo Questionnaire") 

65 self.setStyleSheet("background-color: #454746; color: white;") 

66 self.resize(500, 250) # Set default size 

67 

68 if MainWindow._geometry is not None: 

69 self.setGeometry(*MainWindow._geometry) 

70 else: 

71 self.center_on_screen() 

72 

73 def center_on_screen(self): 

74 screen = QApplication.primaryScreen() 

75 if screen is not None: 

76 screen_geometry = screen.availableGeometry() 

77 window_geometry = self.frameGeometry() 

78 center_point = screen_geometry.center() 

79 window_geometry.moveCenter(center_point) 

80 self.move(window_geometry.topLeft()) 

81 MainWindow._geometry = self.geometry().getRect() 

82 MainWindow._centered = True 

83 else: 

84 # Fallback position if screen cannot be detected 

85 MainWindow._geometry = (100, 100, 500, 250) 

86 self.setGeometry(*MainWindow._geometry) 

87 MainWindow._centered = True 

88 

89 def closeEvent(self, event): 

90 MainWindow._geometry = self.geometry().getRect() 

91 if event.spontaneous(): 

92 if ( 

93 QMessageBox.question( 

94 self, 

95 "Quit", 

96 "Are you sure you want to quit?", 

97 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, 

98 ) 

99 == QMessageBox.StandardButton.Yes 

100 ): 

101 event.accept() 

102 QApplication.closeAllWindows() 

103 sys.exit(0) 

104 else: 

105 event.ignore() 

106 else: 

107 event.accept() 

108 

109 

110class ErrorDialog(QDialog): 

111 """ 

112 Error dialog class for the GUI application. 

113 """ 

114 

115 def __init__(self, err_warn_type: str = "Error", error_message: str = ""): 

116 super().__init__() 

117 

118 self.err_warn_type = err_warn_type 

119 self.error_message = error_message 

120 

121 self.initUI() 

122 

123 def initUI(self): 

124 self.setWindowTitle(self.err_warn_type) 

125 

126 layout = QVBoxLayout() 

127 error_label = QLabel(self.error_message) 

128 error_label.setStyleSheet("font-weight: bold; color: white;") 

129 error_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 

130 layout.addWidget(error_label) 

131 ok_button = QPushButton("Ok") 

132 layout.addWidget(ok_button) 

133 

134 if self.err_warn_type == "Error": 

135 self.setStyleSheet("background-color: #8D3832;") 

136 else: 

137 self.setStyleSheet("background-color: #4F6B90;") 

138 

139 self.setLayout(layout) 

140 ok_button.clicked.connect(self.close) 

141 

142 

143def show_error_message(warn_type: str = "Error", message: str = ""): 

144 """ 

145 Function to show an error message to the user. 

146 """ 

147 # Create main window 

148 main_window = MainWindow() 

149 main_layout = QVBoxLayout() 

150 

151 # Create a central widget 

152 central_widget = QWidget(parent=main_window) 

153 

154 # Add ErrorDialog widget 

155 error_dialog = ErrorDialog(warn_type, message) 

156 main_layout.addWidget(error_dialog, stretch=1) 

157 

158 # Set the layout 

159 central_widget.setLayout(main_layout) 

160 main_window.setCentralWidget(central_widget) 

161 

162 error_dialog.exec() 

163 

164 return 

165 

166 

167class ProgressBar(QWidget): 

168 """ 

169 Progress bar class for the GUI application. 

170 """ 

171 

172 def __init__(self, section_idx): 

173 super().__init__() 

174 self.section_idx = section_idx 

175 self.initUI() 

176 

177 def initUI(self): 

178 layout = QHBoxLayout() 

179 for idx, section in enumerate(SECTIONS): 

180 section_button = QPushButton(section) 

181 section_button.setStyleSheet( 

182 "background-color: #545A61; font-weight: bold;" # lowlight 

183 ) 

184 if idx == self.section_idx: 

185 section_button.setStyleSheet( 

186 "background-color: #4F6B90; font-weight: bold;" # highlight 

187 ) 

188 layout.addWidget(section_button) 

189 if idx < len(SECTIONS) - 1: 

190 dot_label = QLabel(" • ") 

191 layout.addWidget(dot_label) 

192 self.setLayout(layout) 

193 

194 

195class Back_button(QPushButton): 

196 """ 

197 Back button class for the GUI application. 

198 """ 

199 

200 def __init__(self): 

201 super().__init__() 

202 self.choice = None 

203 self.initUI() 

204 

205 def initUI(self): 

206 self.setText("← Go Back") 

207 self.setStyleSheet("background-color: #8D3832; padding: 5px;") 

208 self.clicked.connect(self.on_click) 

209 

210 def on_click(self): 

211 self.choice = GoQBack() 

212 # clearLayout removes all widgets from the layout of the center_widget (such as ProgressBar, Enum_choice_selection and other open Dialogs) 

213 # without this the Back_button just disappears, but the center_widget stays open 

214 clearLayout(self.parent().layout()) 

215 

216 

217class Enum_choice_selection(QDialog): 

218 """ 

219 Enum choice selection class for the GUI application. 

220 """ 

221 

222 def __init__(self, enum_choices, question, multi_choice=False, prev_answer=None): 

223 super().__init__() 

224 

225 self.enum_choices = enum_choices 

226 self.question = question 

227 self.multi_choice = multi_choice 

228 self.prev_answer = prev_answer 

229 

230 self.choice = None 

231 

232 self.initUI() 

233 

234 def initUI(self): 

235 self.setWindowTitle("Select an option") 

236 

237 layout = QVBoxLayout() 

238 if self.question: 

239 question_label = QLabel(self.question) 

240 layout.addWidget(question_label) 

241 

242 if self.multi_choice: 

243 self.choices = QListWidget() 

244 self.choices.setSelectionMode(QListWidget.SelectionMode.MultiSelection) 

245 self.choices.setStyleSheet( 

246 "QListWidget::item:selected { background-color: #7E8A97; }" 

247 ) 

248 for choice in self.enum_choices: 

249 item = QListWidgetItem(choice.value) 

250 self.choices.addItem(item) 

251 if self.prev_answer and choice.value in self.prev_answer: 

252 self.choices.setCurrentItem(item) 

253 layout.addWidget(self.choices) 

254 self.choices.itemSelectionChanged.connect(self.update_submit_button) 

255 else: 

256 self.choices = QButtonGroup() 

257 for idx, choice in enumerate(self.enum_choices): 

258 radio_button = QRadioButton(choice.value) 

259 if self.prev_answer and choice.value == self.prev_answer.value: 

260 radio_button.setChecked(True) 

261 self.choices.addButton(radio_button, idx) 

262 layout.addWidget(radio_button) 

263 radio_button.toggled.connect(self.update_submit_button) 

264 

265 submit_button = QPushButton("Continue →") 

266 submit_button.setStyleSheet( 

267 """ 

268 QPushButton { 

269 background-color: #454746; 

270 color: grey; 

271 padding: 5px; 

272 } 

273 QPushButton:enabled { 

274 background-color: #545A61; 

275 color: white; 

276 } 

277 QPushButton:disabled { 

278 background-color: #454746; 

279 color: gray; 

280 } 

281 """ 

282 ) 

283 submit_button.setEnabled(bool(self.prev_answer)) 

284 layout.addWidget(submit_button) 

285 self.setLayout(layout) 

286 

287 submit_button.clicked.connect(self.on_submit) 

288 self.submit_button = submit_button 

289 

290 def update_submit_button(self): 

291 if self.multi_choice: 

292 has_selection = len(self.choices.selectedItems()) > 0 

293 else: 

294 has_selection = self.choices.checkedId() != -1 

295 self.submit_button.setEnabled(has_selection) 

296 

297 def on_submit(self): 

298 if self.multi_choice: 

299 self.choice = [item.text() for item in self.choices.selectedItems()] 

300 else: 

301 self.choice = list(self.enum_choices)[self.choices.checkedId()] 

302 self.close() 

303 

304 

305class Custom_entry(QDialog): 

306 """ 

307 Custom entry class for the GUI application. 

308 """ 

309 

310 def __init__( 

311 self, question, multi_choice: bool, prev_answer: str | list[str] | None = None 

312 ): 

313 super().__init__() 

314 

315 self.question = question 

316 self.multi_choice = multi_choice 

317 self.prev_answer = prev_answer 

318 

319 self.choice = None 

320 

321 self.initUI() 

322 

323 def initUI(self): 

324 self.setWindowTitle("Enter a value") 

325 

326 layout = QVBoxLayout() 

327 if self.question: 

328 question_label = QLabel(self.question) 

329 layout.addWidget(question_label) 

330 

331 if self.multi_choice: 

332 self.entries = [] 

333 self.add_entry_button = QPushButton("+") 

334 self.remove_entry_button = QPushButton("-") 

335 self.entry_layout = QVBoxLayout() 

336 entry = QLineEdit() 

337 self.entries.append(entry) 

338 self.entry_layout.addWidget(entry) 

339 layout.addLayout(self.entry_layout) 

340 button_layout = QHBoxLayout() 

341 button_layout.addWidget(self.add_entry_button) 

342 button_layout.addWidget(self.remove_entry_button) 

343 layout.addLayout(button_layout) 

344 self.add_entry_button.clicked.connect(self.add_entry) 

345 self.remove_entry_button.clicked.connect(self.remove_entry) 

346 if self.prev_answer: 

347 self.remove_entry(ignore_warning=True) 

348 for entry_text in self.prev_answer: 

349 self.add_entry(ignore_warning=True) 

350 self.entries[-1].setText(entry_text) 

351 else: 

352 self.entry = QLineEdit() 

353 if self.prev_answer: 

354 self.entry.setText(self.prev_answer) 

355 layout.addWidget(self.entry) 

356 

357 submit_button = QPushButton("Continue →") 

358 submit_button.setStyleSheet( 

359 """ 

360 QPushButton { 

361 background-color: #454746; 

362 color: grey; 

363 padding: 5px; 

364 } 

365 QPushButton:enabled { 

366 background-color: #545A61; 

367 color: white; 

368 } 

369 QPushButton:disabled { 

370 background-color: #454746; 

371 color: gray; 

372 } 

373 """ 

374 ) 

375 layout.addWidget(submit_button) 

376 self.setLayout(layout) 

377 submit_button.clicked.connect(self.on_submit) 

378 self.submit_button = submit_button 

379 self.update_submit_button() 

380 self.connect_signals() 

381 

382 def connect_signals(self): 

383 if self.multi_choice: 

384 for entry in self.entries: 

385 entry.textChanged.connect(self.update_submit_button) 

386 self.add_entry_button.clicked.connect(self.update_submit_button) 

387 self.remove_entry_button.clicked.connect(self.update_submit_button) 

388 else: 

389 self.entry.textChanged.connect(self.update_submit_button) 

390 

391 def update_submit_button(self): 

392 if self.multi_choice: 

393 has_text = all(entry.text().strip() for entry in self.entries) 

394 else: 

395 has_text = bool(self.entry.text().strip()) 

396 self.submit_button.setEnabled(has_text) 

397 

398 def on_submit(self): 

399 if self.multi_choice: 

400 if any(not entry.text().strip() for entry in self.entries): 

401 QMessageBox.warning( 

402 self, 

403 "Incomplete Entries", 

404 "Please fill in all entries before submitting.", 

405 ) 

406 return 

407 self.choice = [entry.text() for entry in self.entries] 

408 else: 

409 if not self.entry.text().strip(): 

410 QMessageBox.warning( 

411 self, 

412 "Empty Field", 

413 "Please fill in the text field before submitting.", 

414 ) 

415 return 

416 self.choice = self.entry.text() 

417 self.close() 

418 

419 def add_entry(self, ignore_warning: bool = False): 

420 if all(entry.text().strip() for entry in self.entries) or ignore_warning: 

421 entry = QLineEdit() 

422 self.entries.append(entry) 

423 self.entry_layout.addWidget(entry) 

424 if not ignore_warning: 

425 entry.textChanged.connect(self.update_submit_button) 

426 else: 

427 QMessageBox.warning( 

428 self, 

429 "Incomplete Entries", 

430 "Please fill in all existing entries before adding a new one.", 

431 ) 

432 

433 def remove_entry(self, ignore_warning: bool = False): 

434 if (self.multi_choice and len(self.entries) > 1) or ignore_warning: 

435 entry = self.entries.pop() 

436 self.entry_layout.removeWidget(entry) 

437 entry.deleteLater() 

438 if not ignore_warning: 

439 self.update_submit_button() 

440 else: 

441 QMessageBox.warning( 

442 self, 

443 "Cannot Remove", 

444 "At least one entry is required.", 

445 ) 

446 

447 

448class Yes_no(QDialog): 

449 """ 

450 Yes or No class for the GUI application. 

451 """ 

452 

453 def __init__(self, question, prev_answer=None): 

454 super().__init__() 

455 

456 self.question = question 

457 self.prev_answer = prev_answer 

458 

459 self.choice = None 

460 

461 self.initUI() 

462 

463 def initUI(self): 

464 self.setWindowTitle("Yes or No") 

465 

466 layout = QVBoxLayout() 

467 if self.question: 

468 question_label = QLabel(self.question) 

469 layout.addWidget(question_label) 

470 self.yes_button = QPushButton("Yes") 

471 self.no_button = QPushButton("No") 

472 if self.prev_answer is not None: 

473 if self.prev_answer is True: 

474 self.yes_button.setStyleSheet("background-color: #2E5539") 

475 elif self.prev_answer is False: 

476 self.no_button.setStyleSheet("background-color: #73403F") 

477 layout.addWidget(self.yes_button) 

478 layout.addWidget(self.no_button) 

479 self.setLayout(layout) 

480 

481 self.yes_button.clicked.connect(self.on_yes) 

482 self.no_button.clicked.connect(self.on_no) 

483 

484 def on_yes(self): 

485 self.choice = True 

486 self.close() 

487 

488 def on_no(self): 

489 self.choice = False 

490 self.close() 

491 

492 

493def get_enum_choice_conversion( 

494 my_enum: Enum, 

495 question: str = "", 

496 multi_choice: bool = False, 

497 section_idx: int = 0, 

498 prev_answer: Any = None, 

499) -> Any: 

500 """ 

501 Function to get the user's choice for an Enum. 

502 

503 :param my_enum: Enum class to get the choices from 

504 :type my_enum: Enum 

505 :param question: Question to ask the user 

506 :type question: str 

507 :param multi_choice: Whether the user can select multiple choices 

508 :type multi_choice: bool 

509 :param section_idx: Index of the section in the progress bar 

510 :type section_idx: int 

511 :param prev_answer: Previous answer given by the user 

512 :type prev_answer: Any 

513 :return: User's choice 

514 :rtype: Any 

515 """ 

516 choice = None 

517 

518 # Create main window 

519 main_window = MainWindow() 

520 main_layout = QVBoxLayout() 

521 

522 # Create a central widget and set the layout 

523 central_widget = QWidget(parent=main_window) 

524 

525 # Add progress bar 

526 progress_bar = ProgressBar(section_idx) 

527 main_layout.addWidget(progress_bar, stretch=2) 

528 

529 # Add Enum_choice_selection widget 

530 enum_selection_widget = Enum_choice_selection( 

531 my_enum, question, multi_choice, prev_answer 

532 ) 

533 main_layout.addWidget(enum_selection_widget, stretch=7) 

534 

535 # Add Back_button widget 

536 back_button = Back_button() 

537 main_layout.addWidget(back_button, stretch=1) 

538 

539 # Set the layout 

540 central_widget.setLayout(main_layout) 

541 main_window.setCentralWidget(central_widget) 

542 

543 # Execute the Yes_no dialog 

544 enum_selection_widget.exec() 

545 

546 if isinstance(back_button.choice, GoQBack): 

547 choice = GoQBack() 

548 else: 

549 choice = enum_selection_widget.choice 

550 

551 return choice 

552 

553 

554def get_custom_entry( 

555 question: str = "", 

556 multi_choice: bool = False, 

557 section_idx: int = 0, 

558 prev_answer: any = None, 

559) -> Any: 

560 """ 

561 Function to get the user's custom entry. 

562 

563 :param question: Question to ask the user 

564 :type question: str 

565 :param multi_choice: Whether the user can enter multiple values 

566 :type multi_choice: bool 

567 :param section_idx: Index of the section in the progress bar 

568 :type section_idx: int 

569 :param prev_answer: Previous answer given by the user 

570 :type prev_answer: any 

571 :return: User's choice 

572 :rtype: Any 

573 """ 

574 choice = None 

575 

576 # Create main window 

577 main_window = MainWindow() 

578 main_layout = QVBoxLayout() 

579 

580 # Create a central widget 

581 central_widget = QWidget(parent=main_window) 

582 

583 # Add progress bar 

584 progress_bar = ProgressBar(section_idx) 

585 main_layout.addWidget(progress_bar, stretch=2) 

586 

587 # Add Custom_entry widget 

588 custom_entry_widget = Custom_entry(question, multi_choice, prev_answer) 

589 main_layout.addWidget(custom_entry_widget, stretch=7) 

590 

591 # Add Back_button widget 

592 back_button = Back_button() 

593 main_layout.addWidget(back_button, stretch=1) 

594 

595 # Set the layout 

596 central_widget.setLayout(main_layout) 

597 main_window.setCentralWidget(central_widget) 

598 

599 # Execute the Yes_no dialog 

600 custom_entry_widget.exec() 

601 

602 if isinstance(back_button.choice, GoQBack): 

603 choice = GoQBack() 

604 else: 

605 choice = custom_entry_widget.choice 

606 

607 return choice 

608 

609 

610def get_yes_no( 

611 question: str = "", section_idx: int = 0, prev_answer: any = None 

612) -> bool: 

613 """ 

614 Function to get the user's choice for Yes or No. 

615 

616 :param question: Question to ask the user 

617 :type question: str 

618 :param section_idx: Index of the section in the progress bar 

619 :type section_idx: int 

620 :param prev_answer: Previous answer given by the user 

621 :type prev_answer: any 

622 :return: User's choice 

623 :rtype: bool 

624 """ 

625 choice = None 

626 

627 # Create main window 

628 main_window = MainWindow() 

629 main_layout = QVBoxLayout() 

630 

631 # Create a central widget 

632 central_widget = QWidget(parent=main_window) 

633 

634 # Add progress bar 

635 progress_bar = ProgressBar(section_idx) 

636 main_layout.addWidget(progress_bar, stretch=2) 

637 

638 # Add Yes_no widget 

639 yes_no_widget = Yes_no(question, prev_answer) 

640 main_layout.addWidget(yes_no_widget, stretch=7) 

641 

642 # Add Back_button widget 

643 back_button = Back_button() 

644 main_layout.addWidget(back_button, stretch=1) 

645 

646 # Set the layout 

647 central_widget.setLayout(main_layout) 

648 main_window.setCentralWidget(central_widget) 

649 

650 # Execute the Yes_no dialog 

651 yes_no_widget.exec() 

652 

653 if isinstance(back_button.choice, GoQBack): 

654 choice = GoQBack() 

655 else: 

656 choice = yes_no_widget.choice 

657 

658 return choice 

659 

660 

661def valid_input_for_pydmodel( 

662 pydmodel: BaseModel, field_name: str, inp_val: Any 

663) -> bool: 

664 """ 

665 Validate the input value with a certain pydantic model and model field 

666 to ask the user for input again if the input is invalid. 

667 

668 :param pydmodel: Pydantic model to validate the input with 

669 :type pydmodel: BaseModel 

670 :param field_name: Field name of the model to validate the input with 

671 :type field_name: str 

672 :param inp_val: Input value to validate 

673 :type inp_val: Any 

674 :return: Whether the input is valid 

675 :rtype: bool 

676 """ 

677 try: 

678 # pydmodel.validate({field_name: inp_val}) 

679 pydmodel.__pydantic_validator__.validate_assignment( 

680 pydmodel.model_construct(), field_name, inp_val 

681 ) 

682 return True 

683 except UserWarning as w: 

684 warning = str(w) 

685 show_error_message("Warning", warning) 

686 return True 

687 except ValidationError as e: 

688 errors = e.errors() 

689 modelValErr = errors[0].get("ctx", dict()).get("error", None) 

690 if isinstance(modelValErr, ModelValidError): 

691 return True 

692 else: 

693 val_err = e 

694 err_msg = ", ".join(val_err.errors()[0]["msg"].split(",")[1:]) 

695 show_error_message("Error", err_msg) 

696 return False 

697 

698 

699def save_annotation(gene_annotation: BaseModel): 

700 """ 

701 Function to save the gene annotation in the dngf format into a file. 

702 

703 :param gene_annotation: Gene annotation to save 

704 :type gene_annotation: BaseModel 

705 """ 

706 fileName = None 

707 main_window = MainWindow() 

708 

709 while not fileName: 

710 fileName, _ = QFileDialog.getSaveFileName( 

711 main_window, "Save File", "my_model.dngf", "dngf (*.dngf)" 

712 ) 

713 if not fileName: 

714 reply = QMessageBox.question( 

715 main_window, 

716 "Quit without Saving?", 

717 "Do you want to close the application without saving the annotation file?", 

718 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, 

719 ) 

720 if reply == QMessageBox.StandardButton.Yes: 

721 # quit without saving file 

722 return None 

723 

724 convert_to_json(gene_annotation, Path(fileName)) 

725 

726 

727def main_app(): 

728 """ 

729 The main function for the GUI application. Calls questionnaire functions 

730 and saves the gene annotation, while showing Widgets in the privously initiated 

731 MainWindow. 

732 """ 

733 GUI_INTERFACE_FUNCTS = { 

734 "get_enum_choice_conversion": get_enum_choice_conversion, 

735 "get_custom_entry": get_custom_entry, 

736 "get_yes_no": get_yes_no, 

737 "valid_input_for_pydmodel": valid_input_for_pydmodel, 

738 } 

739 

740 try: 

741 # Start of the questionnaire 

742 de_novo_questionnaire = DeNovoQuestionnaire(GUI_INTERFACE_FUNCTS) 

743 gene_annotation = de_novo_questionnaire.deNovoGeneAnnotation 

744 

745 # save model in the dngf (de novo gene format) format (JSON) 

746 save_annotation(gene_annotation) 

747 

748 # close all windows and quit the application 

749 QApplication.closeAllWindows() 

750 sys.exit(0) 

751 

752 except Exception as e: 

753 raise NotImplementedError(f"Error: {e}") 

754 

755 

756def main(): 

757 """ 

758 The main function of the program. Entry point for the GUI executable. 

759 """ 

760 # Check the NCBI Taxonomy Database 

761 check_NCBI_taxDB() 

762 

763 app = QApplication([]) 

764 main_window = MainWindow() 

765 main_window.show() 

766 main_app() 

767 sys.exit(app.exec()) 

768 

769 

770if __name__ == "__main__": 

771 main()