Skip to content

Import Export

app.ui.qt.dialogs.import_export

View source code

Import / Export layer data dialog.

Shown when the user clicks either "Import / Export" button on the right panel. The dialog is scope-aware: the detection button opens it scoped to layers A/B, while the tracking button opens it scoped to layers C/D.

See Also
  • ImportMode — controls how imported boxes are merged
  • VideoDataLayer — identifies the layer scope for import and export

Result

After the user clicks OK, call ImportExportDialog.get_config to receive an ImportExportConfig dataclass. If the dialog was rejected or the user cancelled the overwrite-warning confirmation, get_config() returns None.

⚠️ KNOWN QT QVARIANT COERCION ISSUE — READ BEFORE EDITING

VideoDataLayer and ImportMode are both str-based enums: class VideoDataLayer(str, Enum): A = "a" … class ImportMode(str, Enum): OVERWRITE = "overwrite" …

When any Python object that is a subclass of str is stored as userData in a QComboBox via addItem(label, userData) and retrieved via currentData(), PySide6's internal Qt QVariant machinery sees a plain string (because the object IS-A str), discards the Python subclass identity, and returns a bare str.

Concretely

self._layer_combo.addItem("Layer A", VideoDataLayer.A) # enum IN self._layer_combo.currentData() # "a" OUT ← coerced!

This caused

cfg.layer = "a" # plain str, not VideoDataLayer.A cfg.layer.name # AttributeError: 'str' object has no attribute 'name'

Observed in the logs as

WARNING | DetectionExportService was provided with string literal 'a' ERROR | AttributeError: 'str' object has no attribute 'name'

Fix applied in THREE places (belt-and-suspenders): 1. PRIMARY: _build_config() calls ensure_layer_enum() and ensure_import_mode() immediately after currentData() so coerced strings are converted back to enums before leaving the dialog. 2. SECONDARY: All import/export services coerce incoming layer/mode values at their entry points as a safety net for programmatic callers. 3. TERTIARY: Application facade methods coerce at the boundary before dispatching to detection/tracking service implementations.

No signals/slots are involved in the coercion — it is a synchronous round-trip through Qt's QVariant heap: QDialogButtonBox.accepted → _on_accepted() → _build_config() → currentData()

DO NOT remove ensure_layer_enum()/ensure_import_mode() calls without understanding this. See: app/application/services/layer_coercion.py

Layer: Presentation. Context: Contains concrete Qt presentation components and supporting utilities.

ImportExportConfig dataclass

Store the import/export choices collected by ImportExportDialog.get_config.

ATTRIBUTE DESCRIPTION
is_import

True → the user chose Import. False → the user chose Export.

TYPE: bool

layer

The target/source VideoDataLayer.

TYPE: VideoDataLayer

fmt

"json" or "csv".

TYPE: str

file_path

Absolute path selected by the user.

TYPE: str

import_mode

The chosen ImportMode; None for exports.

TYPE: ImportMode | None

Layer: Presentation. Context: Supports user interaction and delegates application work through presentation contracts.

file_path: str instance-attribute

fmt: str instance-attribute

import_mode: ImportMode | None instance-attribute

is_import: bool instance-attribute

layer: VideoDataLayer instance-attribute

__init__(is_import: bool, layer: VideoDataLayer, fmt: str, file_path: str, import_mode: ImportMode | None) -> None

ImportExportDialog

Bases: QDialog

Modal dialog for importing or exporting bounding-box layer data.

PARAMETER DESCRIPTION
scope_layers

Layers to offer in the layer combo box.

TYPE: list[VideoDataLayer]

scope_title

Human-readable scope label shown in the window title.

TYPE: str DEFAULT: 'Layer'

parent

Optional Qt parent widget.

TYPE: QWidget | None DEFAULT: None

Example
dlg = ImportExportDialog(
    scope_layers=[VideoDataLayer.A, VideoDataLayer.B],
    scope_title="Detection",
    parent=self,
)
if dlg.exec() == QDialog.DialogCode.Accepted:
    cfg = dlg.get_config()
    if cfg:  # None if the user cancelled the overwrite warning
        ...
Note

The dialog enforces the overwrite warning itself (before calling accept()) so the handler does not need to show a second dialog.

Consider future improvement: persist the last-used directory per scope (detection / tracking) using QSettings so the file browser opens in a sensible location across sessions.

Layer: Presentation. Context: Supports user interaction and delegates application work through presentation contracts.

__init__(scope_layers: list[VideoDataLayer], scope_title: str = 'Layer', initial_import_directory: str = '', initial_export_directory: str = '', parent: QWidget | None = None) -> None

get_config() -> ImportExportConfig | None

Return the user's choices after the dialog was accepted.

RETURNS DESCRIPTION
ImportExportConfig | None

ImportExportConfig if the user confirmed, otherwise None.