Coverage for denofo/converter/converter_gui.py: 72%

279 statements  

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

1import sys 

2import warnings 

3from pathlib import Path 

4from denofo.utils.ncbiTaxDBcheck import check_NCBI_taxDB 

5from denofo.utils.helpers import infer_format_from_extension 

6from PyQt6.QtWidgets import ( 

7 QApplication, 

8 QMainWindow, 

9 QWidget, 

10 QVBoxLayout, 

11 QHBoxLayout, 

12 QLabel, 

13 QLineEdit, 

14 QPushButton, 

15 QComboBox, 

16 QFileDialog, 

17 QMessageBox, 

18 QPlainTextEdit, 

19) 

20from denofo.converter.convert import ( 

21 load_from_json, 

22 load_from_pickle, 

23 load_from_fasta, 

24 load_from_gff, 

25 encode_short_str, 

26 decode_short_str, 

27 convert_to_json, 

28 convert_to_pickle, 

29 annotate_fasta, 

30 annotate_gff, 

31) 

32 

33 

34class DngfConverterGUI(QMainWindow): 

35 def __init__(self): 

36 super().__init__() 

37 self.setWindowTitle("DeNoFo Converter") 

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

39 self.resize(600, 200) 

40 qr = self.frameGeometry() 

41 cp = QApplication.primaryScreen().availableGeometry().center() 

42 qr.moveCenter(cp) 

43 self.move(qr.topLeft()) 

44 

45 # Create central widget and layout 

46 central_widget = QWidget() 

47 self.setCentralWidget(central_widget) 

48 layout = QVBoxLayout(central_widget) 

49 

50 label_width = 150 # Set a fixed width for all labels 

51 

52 # Input file selection 

53 input_layout = QHBoxLayout() 

54 self.input_info_button = QPushButton("?") 

55 self.input_info_button.setFixedSize(16, 16) 

56 self.input_info_button.clicked.connect( 

57 lambda: self.show_help( 

58 "Input File", 

59 ( 

60 "Select the input file to be converted. This file has to " 

61 "contain the DeNoFo annotation to be converted to another file " 

62 "format. The input format will be automatically inferred from " 

63 "the file extension if you choose a file with a known extension.\n" 

64 "If you want to annotate sequences in a FASTA/GFF file, please " 

65 "select the FASTA/GFF file to annotate in the 'Additional File' " 

66 "section." 

67 ), 

68 ) 

69 ) 

70 self.input_label = QLabel("Input File:") 

71 self.input_label.setFixedWidth(label_width) 

72 input_layout.addWidget(self.input_info_button) 

73 input_layout.addWidget(self.input_label) 

74 self.input_path = QLineEdit() 

75 self.input_path.textChanged.connect(self.on_input_path_changed) 

76 input_button = QPushButton("Browse Input") 

77 input_button.clicked.connect(self.browse_input) 

78 input_layout.addWidget(self.input_path) 

79 input_layout.addWidget(input_button) 

80 layout.addLayout(input_layout) 

81 

82 # Input format selection 

83 format_layout = QHBoxLayout() 

84 self.input_format_info_button = QPushButton("?") 

85 self.input_format_info_button.setFixedSize(16, 16) 

86 self.input_format_info_button.clicked.connect( 

87 lambda: self.show_help( 

88 "Input Format", 

89 ( 

90 "Select the format of the input file.\n" 

91 "The input format will be automatically inferred from the file " 

92 "extension if you select a file with a known extension." 

93 ), 

94 ) 

95 ) 

96 self.input_format_label = QLabel("Input Format:") 

97 self.input_format_label.setFixedWidth(label_width) 

98 format_layout.addWidget(self.input_format_info_button) 

99 format_layout.addWidget(self.input_format_label) 

100 self.input_format = QComboBox() 

101 self.input_format.addItems( 

102 ["auto", "dngf", "pickle", "fasta", "gff", "shortstr"] 

103 ) 

104 format_layout.addWidget(self.input_format) 

105 layout.addLayout(format_layout) 

106 

107 # Output file selection 

108 output_layout = QHBoxLayout() 

109 self.output_info_button = QPushButton("?") 

110 self.output_info_button.setFixedSize(16, 16) 

111 self.output_info_button.clicked.connect( 

112 lambda: self.show_help( 

113 "Output File", 

114 ( 

115 "Select the destination for the converted file " 

116 "(optional). If no output file is specified, the output will be " 

117 "displayed in the text box below.\n" 

118 "If you want to annotate sequences in a FASTA/GFF file, please " 

119 "select the FASTA/GFF file to annotate in the 'Additional File' " 

120 "section and either another file location as output file to keep " 

121 "the original FASTA/GFF file unchanged (recommended) or select " 

122 "the same file as output, which overwrites the original " 

123 "FASTA/GFF file." 

124 ), 

125 ) 

126 ) 

127 self.output_label = QLabel("Output File:") 

128 self.output_label.setFixedWidth(label_width) 

129 output_layout.addWidget(self.output_info_button) 

130 output_layout.addWidget(self.output_label) 

131 self.output_path = QLineEdit() 

132 self.output_path.textChanged.connect(self.on_output_path_changed) 

133 output_button = QPushButton("Browse Output") 

134 output_button.clicked.connect(self.browse_output) 

135 output_layout.addWidget(self.output_path) 

136 output_layout.addWidget(output_button) 

137 layout.addLayout(output_layout) 

138 

139 # Output format selection 

140 out_format_layout = QHBoxLayout() 

141 self.output_format_info_button = QPushButton("?") 

142 self.output_format_info_button.setFixedSize(16, 16) 

143 self.output_format_info_button.clicked.connect( 

144 lambda: self.show_help( 

145 "Output Format", 

146 ( 

147 "Select the desired format for the output file.\n" 

148 "The output format will be automatically inferred from the file " 

149 "extension if you choose a file with a known extension." 

150 ), 

151 ) 

152 ) 

153 self.output_format_label = QLabel("Output Format:") 

154 self.output_format_label.setFixedWidth(label_width) 

155 out_format_layout.addWidget(self.output_format_info_button) 

156 out_format_layout.addWidget(self.output_format_label) 

157 self.output_format = QComboBox() 

158 self.output_format.addItems( 

159 ["auto", "dngf", "pickle", "fasta", "gff", "shortstr"] 

160 ) 

161 self.output_format.currentIndexChanged.connect(self.update_sections) 

162 out_format_layout.addWidget(self.output_format) 

163 layout.addLayout(out_format_layout) 

164 

165 # Identifiers file (optional) 

166 self.identifiers_layout = QHBoxLayout() 

167 self.identifiers_info_button = QPushButton("?") 

168 self.identifiers_info_button.setFixedSize(16, 16) 

169 self.identifiers_info_button.clicked.connect( 

170 lambda: self.show_help( 

171 "Identifiers File", 

172 ( 

173 "Optional file containing sequence identifiers for annotation/" 

174 "extraction in/from a FASTA/GFF file.\n" 

175 "If not provided, all FASTA/GFF entries will be considered.\n" 

176 "The file should contain one identifier per line.\n" 

177 "In FASTA format, the identifiers are matched with sequence IDs at " 

178 "the beginning of the fasta headers. In GFF format, existence of " 

179 "given identifiers is checked in the 9th attributes column." 

180 ), 

181 ) 

182 ) 

183 self.identifiers_label = QLabel("Identifiers File:") 

184 self.identifiers_label.setFixedWidth(label_width) 

185 self.identifiers_layout.addWidget(self.identifiers_info_button) 

186 self.identifiers_layout.addWidget(self.identifiers_label) 

187 self.identifiers_path = QLineEdit() 

188 identifiers_button = QPushButton("Browse Identifiers") 

189 identifiers_button.setStyleSheet( 

190 """ 

191 QPushButton:disabled { 

192 background-color: #454746; 

193 color: grey; 

194 } 

195 """ 

196 ) 

197 identifiers_button.clicked.connect(self.browse_identifiers) 

198 self.identifiers_layout.addWidget(self.identifiers_path) 

199 self.identifiers_layout.addWidget(identifiers_button) 

200 layout.addLayout(self.identifiers_layout) 

201 

202 # Feature type for GFF 

203 self.feature_layout = QHBoxLayout() 

204 self.feature_info_button = QPushButton("?") 

205 self.feature_info_button.setFixedSize(16, 16) 

206 self.feature_info_button.clicked.connect( 

207 lambda: self.show_help( 

208 "Feature Type", 

209 ( 

210 "Specify the feature type for GFF annotation/extraction. " 

211 "Only sequences with this feature type are considered " 

212 "(default: gene)." 

213 ), 

214 ) 

215 ) 

216 self.feature_label = QLabel("Feature Type:") 

217 self.feature_label.setFixedWidth(label_width) 

218 self.feature_layout.addWidget(self.feature_info_button) 

219 self.feature_layout.addWidget(self.feature_label) 

220 self.feature_type = QLineEdit("gene") 

221 self.feature_type.setStyleSheet( 

222 """ 

223 QLineEdit:disabled { 

224 color: grey; 

225 } 

226 """ 

227 ) 

228 self.feature_layout.addWidget(self.feature_type) 

229 layout.addLayout(self.feature_layout) 

230 

231 # Additional Input file selection 

232 self.additional_layout = QHBoxLayout() 

233 self.additional_info_button = QPushButton("?") 

234 self.additional_info_button.setFixedSize(16, 16) 

235 self.additional_info_button.clicked.connect( 

236 lambda: self.show_help( 

237 "Additional File", 

238 ( 

239 "Select an additional input file if required by the output " 

240 "format (only for FASTA or GFF).\n" 

241 "If the output file is the same as the additional input file, " 

242 "the original FASTA/GFF file will be overwritten with the " 

243 "annotated version of the file. It is recommended to select a " 

244 "different output file to keep the original FASTA/GFF file unchanged." 

245 ), 

246 ) 

247 ) 

248 self.additional_label = QLabel("Additional File:") 

249 self.additional_label.setFixedWidth(label_width) 

250 self.additional_layout.addWidget(self.additional_info_button) 

251 self.additional_layout.addWidget(self.additional_label) 

252 self.additional_path = QLineEdit() 

253 self.additional_path.textChanged.connect(self.update_sections) 

254 self.additional_button = QPushButton("Browse Additional File") 

255 self.additional_button.setStyleSheet( 

256 """ 

257 QPushButton:disabled { 

258 background-color: #454746; 

259 color: grey; 

260 } 

261 """ 

262 ) 

263 self.additional_button.clicked.connect(self.browse_additional) 

264 self.additional_layout.addWidget(self.additional_path) 

265 self.additional_layout.addWidget(self.additional_button) 

266 layout.addLayout(self.additional_layout) 

267 

268 # Convert button 

269 self.convert_button = QPushButton("Convert") 

270 self.convert_button.setStyleSheet( 

271 """ 

272 QPushButton { 

273 background-color: #545A61; 

274 color: white; 

275 } 

276 QPushButton:disabled { 

277 background-color: #454746; 

278 color: grey; 

279 } 

280 QPushButton:enabled { 

281 background-color: #545A61; 

282 color: white; 

283 } 

284 """ 

285 ) 

286 self.convert_button.clicked.connect(self.convert) 

287 self.convert_button.setEnabled(False) 

288 layout.addWidget(self.convert_button) 

289 

290 # Output text box 

291 self.output_text = QPlainTextEdit() 

292 self.output_text.setReadOnly(True) 

293 self.centralWidget().layout().addWidget(self.output_text) 

294 

295 # Initialize section states 

296 self.update_sections() 

297 

298 def show_help(self, title, message): 

299 QMessageBox.information(self, title, message) 

300 

301 def browse_input(self): 

302 filename, _ = QFileDialog.getOpenFileName(self, "Select Input File") 

303 if filename: 

304 self.input_path.setText(filename) 

305 if self.input_format.currentText() == "auto": 

306 fmt = infer_format_from_extension(Path(filename)) 

307 if fmt: 

308 index = self.input_format.findText(fmt) 

309 if index >= 0: 

310 self.input_format.setCurrentIndex(index) 

311 else: 

312 self.input_format.setCurrentIndex( 

313 self.input_format.findText("auto") 

314 ) 

315 

316 def browse_output(self): 

317 filename, _ = QFileDialog.getSaveFileName(self, "Select Output File") 

318 if filename: 

319 self.output_path.setText(filename) 

320 if self.output_format.currentText() == "auto": 

321 fmt = infer_format_from_extension(Path(filename)) 

322 if fmt: 

323 index = self.output_format.findText(fmt) 

324 if index >= 0: 

325 self.output_format.setCurrentIndex(index) 

326 else: 

327 self.output_format.setCurrentIndex( 

328 self.output_format.findText("auto") 

329 ) 

330 

331 def browse_identifiers(self): 

332 filename, _ = QFileDialog.getOpenFileName(self, "Select Identifiers File") 

333 if filename: 

334 self.identifiers_path.setText(filename) 

335 

336 def browse_additional(self): 

337 filename, _ = QFileDialog.getOpenFileName(self, "Select Additional File") 

338 if filename: 

339 self.additional_path.setText(filename) 

340 

341 def on_input_path_changed(self, text): 

342 inp_fmt = infer_format_from_extension(Path(self.input_path.text())) 

343 if inp_fmt: 

344 index = self.input_format.findText(inp_fmt) 

345 if index >= 0: 

346 self.input_format.setCurrentIndex(index) 

347 else: 

348 self.input_format.setCurrentIndex(self.input_format.findText("auto")) 

349 

350 self.update_sections() 

351 

352 def on_output_path_changed(self, text): 

353 out_fmt = infer_format_from_extension(Path(self.output_path.text())) 

354 if out_fmt: 

355 index = self.output_format.findText(out_fmt) 

356 if index >= 0: 

357 self.output_format.setCurrentIndex(index) 

358 else: 

359 self.output_format.setCurrentIndex(self.output_format.findText("auto")) 

360 

361 self.update_sections() 

362 

363 def update_sections(self): 

364 fmt = self.output_format.currentText() 

365 input_file_exists = ( 

366 self.input_path.text() and Path(self.input_path.text()).is_file() 

367 ) 

368 additional_input_selected = self.additional_path.text() 

369 

370 if fmt == "gff": 

371 identifiers_enabled = True 

372 feature_enabled = True 

373 additional_enabled = True 

374 self.additional_label.setText("GFF File:") 

375 self.additional_button.setText("Browse GFF File") 

376 elif fmt == "fasta": 

377 identifiers_enabled = True 

378 feature_enabled = False 

379 additional_enabled = True 

380 self.additional_label.setText("FASTA File:") 

381 self.additional_button.setText("Browse FASTA File") 

382 else: 

383 identifiers_enabled = False 

384 feature_enabled = False 

385 additional_enabled = False 

386 self.additional_label.setText("Additional File:") 

387 self.additional_button.setText("Browse Additional File") 

388 

389 self.identifiers_path.setEnabled(identifiers_enabled) 

390 self.identifiers_layout.itemAt(3).widget().setEnabled(identifiers_enabled) 

391 self.identifiers_label.setStyleSheet( 

392 "color: white;" if identifiers_enabled else "color: grey;" 

393 ) 

394 self.feature_type.setEnabled(feature_enabled) 

395 self.feature_label.setStyleSheet( 

396 "color: white;" if feature_enabled else "color: grey;" 

397 ) 

398 self.additional_path.setEnabled(additional_enabled) 

399 self.additional_layout.itemAt(3).widget().setEnabled(additional_enabled) 

400 self.additional_label.setStyleSheet( 

401 "color: white;" if additional_enabled else "color: grey;" 

402 ) 

403 

404 # Enable convert button if output_format is not 'auto', input file exists, 

405 # and if output_format is 'fasta' or 'gff', additional input file is selected 

406 if ( 

407 fmt != "auto" 

408 and input_file_exists 

409 and (fmt not in ["fasta", "gff"] or additional_input_selected) 

410 ): 

411 self.convert_button.setEnabled(True) 

412 else: 

413 self.convert_button.setEnabled(False) 

414 

415 def get_identifiers(self): 

416 identifiers = self.identifiers_path.text() 

417 if not identifiers: 

418 return None 

419 with open(identifiers, "r") as infile: 

420 return set(line.strip() for line in infile if line.strip()) 

421 

422 def convert(self): 

423 try: 

424 input_file = Path(self.input_path.text()) 

425 output_file = ( 

426 Path(self.output_path.text()) if self.output_path.text() else None 

427 ) 

428 input_format = self.input_format.currentText() 

429 output_format = self.output_format.currentText() 

430 identifiers = self.get_identifiers() 

431 feature = self.feature_type.text() 

432 additional_input = self.additional_path.text() 

433 

434 # Load input 

435 if input_format == "auto": 

436 input_format = infer_format_from_extension(input_file) 

437 if not input_format: 

438 raise ValueError("Could not infer input format from file extension") 

439 

440 model = None 

441 if input_format == "dngf": 

442 model = load_from_json(input_file) 

443 elif input_format == "pickle": 

444 model = load_from_pickle(input_file) 

445 elif input_format == "fasta": 

446 model = load_from_fasta(input_file) 

447 elif input_format == "gff": 

448 model = load_from_gff(input_file, feature=feature) 

449 elif input_format == "shortstr": 

450 model = decode_short_str(input_file) 

451 

452 # Convert and save 

453 if output_format == "dngf": 

454 if output_file: 

455 out_str = convert_to_json(model, output_file) 

456 else: 

457 out_str = convert_to_json(model) 

458 elif output_format == "pickle": 

459 if output_file: 

460 out_str = str(convert_to_pickle(model, output_file)) 

461 else: 

462 out_str = str(convert_to_pickle(model)) 

463 elif output_format == "fasta": 

464 if output_file: 

465 out_str = annotate_fasta( 

466 model, 

467 fasta_file=additional_input, 

468 outf=output_file, 

469 identifiers=identifiers, 

470 ) 

471 else: 

472 out_str = annotate_fasta( 

473 model, fasta_file=additional_input, identifiers=identifiers 

474 ) 

475 elif output_format == "gff": 

476 if output_file: 

477 out_str = annotate_gff( 

478 model, 

479 gff_file=additional_input, 

480 outf=output_file, 

481 identifiers=identifiers, 

482 feature=feature, 

483 ) 

484 else: 

485 out_str = annotate_gff( 

486 model, 

487 gff_file=additional_input, 

488 identifiers=identifiers, 

489 feature=feature, 

490 ) 

491 elif output_format == "shortstr": 

492 out_str = encode_short_str(model) 

493 if output_file: 

494 with open(output_file, "w") as outfile: 

495 outfile.write(out_str) 

496 

497 # Show output in text box 

498 self.output_text.setPlainText(out_str) 

499 

500 if output_file: 

501 QMessageBox.information( 

502 self, 

503 "File saved", 

504 ( 

505 "Conversion completed successfully!\n" 

506 f"File was saved to:\n{output_file}" 

507 ), 

508 ) 

509 else: 

510 QMessageBox.information( 

511 self, "Success", "Conversion completed successfully!" 

512 ) 

513 

514 except Exception as e: 

515 QMessageBox.critical(self, "Error", str(e)) 

516 

517 def closeEvent(self, event): 

518 reply = QMessageBox.question( 

519 self, 

520 "Quit", 

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

522 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, 

523 ) 

524 if reply == QMessageBox.StandardButton.Yes: 

525 event.accept() 

526 else: 

527 event.ignore() 

528 

529 

530def main(): 

531 # Check the NCBI Taxonomy Database 

532 check_NCBI_taxDB() 

533 

534 warnings.filterwarnings("ignore") 

535 app = QApplication([]) 

536 window = DngfConverterGUI() 

537 window.show() 

538 sys.exit(app.exec()) 

539 

540 

541if __name__ == "__main__": 

542 main()