Skip to content

File Handler

TraceFileHandler(plot, parent=None)

Bases: QObject

Manage import/export of Trace save files and update the plot/UI.

This QObject coordinates file dialogs, format conversion, and plot updates for Trace configuration files. It uses TraceFileConverter to read and write various supported formats (.trc native, .xml from Java Archive Viewer, and .stp from StripTool), validates the archiver URL, parses time ranges via IOTimeParser, and emits signals that other components consume to update axes, curves, plot settings, and the x-axis range.

Parameters:

Name Type Description Default
plot PyDMArchiverTimePlot

Target plot whose configuration and data are exported/imported.

required
parent QObject | None

Parent QObject for Qt ownership, by default None.

None
Source code in trace/file_io/file_handler.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def __init__(self, plot: PyDMArchiverTimePlot, parent=None):
    """Initialize the File IO Manager, which is responsible for managing
    the import and export of Trace save files

    Parameters
    ----------
    plot : PyDMArchiverTimePlot
        Target plot whose configuration and data are exported/imported.
    parent : QObject | None, optional
        Parent QObject for Qt ownership, by default None.
    """
    super().__init__(parent)
    self.plot = plot
    self.current_file = None
    self.current_dir = save_file_dir
    self.converter = TraceFileConverter()

save_file()

Export the current plot data to the current file

Source code in trace/file_io/file_handler.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@Slot()
def save_file(self) -> None:
    """Export the current plot data to the current file"""
    if self.current_file is None:
        logger.debug("No current file set, prompting for save location")
        self.save_as()
        return
    elif not self.current_file.match("*.trc"):
        self.current_file = self.current_file.with_suffix(".trc")

    try:
        logger.debug(f"Attempting to export to file: {self.current_file}")
        self.converter.export_file(self.current_file, self.plot)
    except FileNotFoundError as e:
        logger.error(str(e))
        self.save_as()

save_as()

Prompt the user for a file to export config data to

Source code in trace/file_io/file_handler.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@Slot()
def save_as(self) -> None:
    """Prompt the user for a file to export config data to"""
    file_name, _ = QFileDialog.getSaveFileName(
        self.parent(), "Save Trace", str(self.current_dir), "Trace Save File (*.trc)"
    )
    file_path = Path(file_name)
    if file_path.is_dir():
        logger.warning("No file name provided to export save file to")
        return

    self.current_file = file_path
    self.current_dir = file_path.parent

    self.save_file()

open_file(file_name=None)

Prompt the user for which config file to load from

Source code in trace/file_io/file_handler.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
@Slot()
@Slot(str)
@Slot(Path)
def open_file(self, file_name: str | Path = None) -> None:
    """Prompt the user for which config file to load from"""
    # Get the save file from the user
    if not file_name:
        file_name, _ = QFileDialog.getOpenFileName(
            self.parent(),
            "Open Trace",
            str(self.current_dir),
            "Trace Save File (*.trc *.xml *.stp);;Java Archive Viewer (*.xml);;"
            + "StripTool File (*.stp);;All Files (*)",
        )
    file_path = Path(file_name)
    if not file_path.is_file():
        logger.warning(f"Attempted import is not a file: {file_path}")
        return

    # Import the given file, and convert it from Java Archive Viewer's
    # format to Trace's format if necessary
    try:
        logger.debug(f"Attempting to import file: {file_path}")
        file_data = self.converter.import_file(file_path)
        self.current_file = file_path
        self.current_dir = file_path.parent
        logger.info(f"Successfully loaded file: {file_path}")
    except (FileNotFoundError, ValueError) as e:
        logger.error(str(e))
        self.open_file()
        return

    # Confirm the PYDM_ARCHIVER_URL is the same as the imported Archiver URL
    # If they are not the same, prompt the user to confirm continuing
    import_url = urlparse(file_data["archiver_url"])
    archiver_url = urlparse(getenv("PYDM_ARCHIVER_URL"))
    if import_url.hostname != archiver_url.hostname:
        logger.warning(f"Attempting to import save file using different Archiver URL: {import_url.hostname}")
        ret = QMessageBox.warning(
            self.parent(),
            "Import Error",
            "The config file you tried to open reads from a different archiver.\n"
            f"\nCurrent archiver is:\n{archiver_url.hostname}\n"
            f"\nAttempted import uses:\n{import_url.hostname}\n\n"
            "\nContinue?",
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No,
        )
        if ret == QMessageBox.No:
            return

    # Parse the time range for the X-Axis; check validity before prompting changes
    try:
        start_str = file_data["time_axis"]["start"]
        end_str = file_data["time_axis"]["end"]
        start_dt, end_dt = IOTimeParser.parse_times(start_str, end_str)
        logger.debug(f"Starting time: {start_dt}")
        logger.debug(f"Ending time: {end_dt}")
    except ValueError as e:
        logger.error(str(e))
        self.open_file()
        return

    # Prompt a change to the plot's axes, curves, and settings
    self.axes_signal.emit(file_data["y-axes"])
    self.curves_signal.emit(file_data["curves"] + file_data["formula"])
    self.plot_settings_signal.emit(file_data["plot"])
    self.file_loaded_signal.emit(file_path)

    # Prompt a change to the X-axis timerange
    if end_str == "now":
        delta = end_dt - start_dt
        timespan = delta.total_seconds()
        self.auto_scroll_span_signal.emit(timespan)
    else:
        x_range = (start_dt.timestamp(), end_dt.timestamp())
        self.timerange_signal.emit(x_range)