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
« 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)
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())
45 # Create central widget and layout
46 central_widget = QWidget()
47 self.setCentralWidget(central_widget)
48 layout = QVBoxLayout(central_widget)
50 label_width = 150 # Set a fixed width for all labels
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)
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)
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)
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)
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)
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)
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)
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)
290 # Output text box
291 self.output_text = QPlainTextEdit()
292 self.output_text.setReadOnly(True)
293 self.centralWidget().layout().addWidget(self.output_text)
295 # Initialize section states
296 self.update_sections()
298 def show_help(self, title, message):
299 QMessageBox.information(self, title, message)
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 )
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 )
331 def browse_identifiers(self):
332 filename, _ = QFileDialog.getOpenFileName(self, "Select Identifiers File")
333 if filename:
334 self.identifiers_path.setText(filename)
336 def browse_additional(self):
337 filename, _ = QFileDialog.getOpenFileName(self, "Select Additional File")
338 if filename:
339 self.additional_path.setText(filename)
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"))
350 self.update_sections()
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"))
361 self.update_sections()
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()
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")
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 )
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)
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())
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()
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")
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)
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)
497 # Show output in text box
498 self.output_text.setPlainText(out_str)
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 )
514 except Exception as e:
515 QMessageBox.critical(self, "Error", str(e))
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()
530def main():
531 # Check the NCBI Taxonomy Database
532 check_NCBI_taxDB()
534 warnings.filterwarnings("ignore")
535 app = QApplication([])
536 window = DngfConverterGUI()
537 window.show()
538 sys.exit(app.exec())
541if __name__ == "__main__":
542 main()