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

1 import sys

2 import warnings

3 from pathlib import Path

4 from denofo.utils.ncbiTaxDBcheck import check_NCBI_taxDB

5 from denofo.utils.helpers import infer_format_from_extension

6 from 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)

20 from 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

34 class 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

530 def 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

541 if __name__ == "__main__":

542 main()