Skip to content

ui_state

anomaly_match_ui.utils.ui_state

Per-user UI state persistence for AnomalyMatch.

Stores small bits of UX state (chooser last-browsed directories, window-level preferences) in a JSON file under the user's home directory. Anything that the user expects to "remember" between notebook sessions but that is not part of the current run's config belongs here.

Schema (schema_version=1):

{
    "schema_version": 1,
    "last_browsed_dirs": {
        "model_chooser": "/path/last/visited",
        "source_chooser": "/path/last/visited",
        ...
    },
    "normalisation_settings": {
        "training_setup": {
            "extensions": ["VIS", "NIR-H"],
            "settings": {"normalisation_method": 3, "image_size": [150, 150], ...}
        }
    }
}

Storage location: ~/.config/anomalymatch/ui_state.json. We use the same path on every platform so the test suite and behaviour stay cross-platform consistent without pulling in platformdirs for one small dotfile.

Override location: set ANOMALYMATCH_CONFIG_DIR to point the state file at a different directory (the same hook the test suite uses to keep its writes in tmp_path). Useful for portable installs and for users who prefer a non-default state location.

Failure mode: the helpers swallow any I/O or JSON error and return None / no-op. Persistence is a UX nicety — a corrupt state file must never crash the screen build path.

get_last_browsed_dir(purpose)

Return the last directory the user finalised for purpose.

Parameters:

Name Type Description Default
purpose str

A stable key identifying the chooser site (e.g. "model_chooser", "source_chooser").

required

Returns:

Type Description
str | None

The persisted directory path, or None if nothing has been

str | None

recorded for purpose yet, or the recorded path no longer

str | None

exists on disk.

Source code in anomaly_match_ui/utils/ui_state.py
def get_last_browsed_dir(purpose: str) -> str | None:
    """Return the last directory the user finalised for *purpose*.

    Args:
        purpose: A stable key identifying the chooser site (e.g.
            ``"model_chooser"``, ``"source_chooser"``).

    Returns:
        The persisted directory path, or ``None`` if nothing has been
        recorded for *purpose* yet, or the recorded path no longer
        exists on disk.
    """
    state = _load()
    dirs = state.get("last_browsed_dirs", {})
    candidate = dirs.get(purpose)
    if not candidate:
        return None
    if not os.path.isdir(candidate):
        return None
    return candidate

set_last_browsed_dir(purpose, path)

Record path as the last directory the user finalised for purpose.

path may point at a file or a directory; in both cases the parent directory is what gets stored, since that is what the chooser should restore on next open.

Parameters:

Name Type Description Default
purpose str

A stable key identifying the chooser site.

required
path str

The selection the user just confirmed (file or directory).

required
Source code in anomaly_match_ui/utils/ui_state.py
def set_last_browsed_dir(purpose: str, path: str) -> None:
    """Record *path* as the last directory the user finalised for *purpose*.

    *path* may point at a file or a directory; in both cases the
    parent directory is what gets stored, since that is what the
    chooser should restore on next open.

    Args:
        purpose: A stable key identifying the chooser site.
        path: The selection the user just confirmed (file or directory).
    """
    if not path:
        return
    if os.path.isdir(path):
        directory = path
    else:
        directory = os.path.dirname(path)
    # ipyfilechooser appends an OS separator when the user clicks
    # Select on a folder (its ``selected`` is ``path + sep + filename``
    # with an empty filename), so normalise away trailing separators
    # before storing — otherwise round-trip comparisons fail.
    directory = os.path.normpath(directory) if directory else directory
    if not directory:
        return

    state = _load()
    state["schema_version"] = _SCHEMA_VERSION
    dirs = state.setdefault("last_browsed_dirs", {})
    dirs[purpose] = directory
    _save(state)

get_normalisation_settings(purpose)

Return the normalisation settings last confirmed for purpose.

Parameters:

Name Type Description Default
purpose str

A stable key identifying the screen (e.g. "training_setup").

required

Returns:

Type Description
dict | None

{"settings": {...}, "extensions": [...]} exactly as recorded by

dict | None

func:set_normalisation_settings, or None when nothing has been

dict | None

recorded yet or the stored record does not have that shape.

dict | None

extensions names the input channels the settings were chosen

dict | None

against; a caller must not apply a stored channel_combination

dict | None

matrix to a source exposing a different set of channels.

Source code in anomaly_match_ui/utils/ui_state.py
def get_normalisation_settings(purpose: str) -> dict | None:
    """Return the normalisation settings last confirmed for *purpose*.

    Args:
        purpose: A stable key identifying the screen (e.g. ``"training_setup"``).

    Returns:
        ``{"settings": {...}, "extensions": [...]}`` exactly as recorded by
        :func:`set_normalisation_settings`, or ``None`` when nothing has been
        recorded yet or the stored record does not have that shape.
        ``extensions`` names the input channels the settings were chosen
        against; a caller must not apply a stored ``channel_combination``
        matrix to a source exposing a different set of channels.
    """
    state = _load()
    records = state.get(_NORMALISATION_KEY, {})
    record = records.get(purpose)
    if record is None:
        return None
    if not isinstance(record, dict):
        logger.debug("Normalisation state for {} is not a dict; ignoring", purpose)
        return None
    settings = record.get("settings")
    extensions = record.get("extensions")
    if not isinstance(settings, dict) or not isinstance(extensions, list):
        logger.debug("Normalisation state for {} is malformed; ignoring", purpose)
        return None
    return {"settings": settings, "extensions": extensions}

set_normalisation_settings(purpose, settings, extensions)

Record the normalisation settings the user confirmed for purpose.

Parameters:

Name Type Description Default
purpose str

A stable key identifying the screen.

required
settings dict

Normalisation config dict. Values must already be JSON-serialisable — run a widget config through serialisable_config first so numpy matrices arrive as nested lists.

required
extensions list[str]

Input-channel names the channel-combination matrix was built against, stored so a later restore can tell whether that matrix still describes the selected source.

required
Source code in anomaly_match_ui/utils/ui_state.py
def set_normalisation_settings(purpose: str, settings: dict, extensions: list[str]) -> None:
    """Record the normalisation settings the user confirmed for *purpose*.

    Args:
        purpose: A stable key identifying the screen.
        settings: Normalisation config dict.  Values must already be
            JSON-serialisable — run a widget config through
            ``serialisable_config`` first so numpy matrices arrive as
            nested lists.
        extensions: Input-channel names the channel-combination matrix was
            built against, stored so a later restore can tell whether that
            matrix still describes the selected source.
    """
    state = _load()
    state["schema_version"] = _SCHEMA_VERSION
    records = state.setdefault(_NORMALISATION_KEY, {})
    records[purpose] = {"settings": dict(settings), "extensions": list(extensions)}
    _save(state)