Source code for spacr.qt.setup_screen

"""Manage first-run setup questions and their saved answers.

Every question has a usable default, so setup can be dismissed without
leaving the application unconfigured. The screen is offered once per spaCR
version, allowing new questions to appear after an update while preserving
answers saved for existing settings.
"""
from __future__ import annotations

from typing import Any, Callable, Dict, List, Optional, Tuple

#: Where the version that last answered these questions is remembered.
_KEY_ANSWERED_VERSION = "onboarding/setup_answered_version"


def _settings():
    """Open spaCR's ``QSettings``.

    :returns: the settings store.
    """
    from .preferences import _settings as store

    return store()


[docs] def answered_version() -> str: """The spaCR version that last completed the setup screen, or ``""``.""" return str(_settings().value(_KEY_ANSWERED_VERSION, "") or "")
[docs] def mark_answered(version: str) -> None: """Record that this version's setup has been seen. :param version: the spaCR version whose setup was seen, normally :func:`current_version`; stored as ``str``. """ _settings().setValue(_KEY_ANSWERED_VERSION, str(version))
[docs] def current_version() -> str: """The running spaCR version, or ``"unknown"``.""" try: from .. import __version__ return str(__version__) except Exception: # noqa: BLE001 return "unknown"
[docs] def should_open(version: Optional[str] = None) -> bool: """Should the setup screen open now? :param version: the running version. Defaults to :func:`current_version`. True on a profile that has never answered, and again whenever the running version differs from the answered one (after an update, for example). False otherwise, so a user who dismissed it is not asked again until something changes. """ running = str(version or current_version()) return answered_version() != running
#: The flag a launcher passes, and the variable a server sets, to go #: straight to the application. Both spellings, because the flag is typed by #: hand and the variable is set in a job script. SKIP_FLAGS = ("--no-setup", "--skip-setup", "--headless-setup") SKIP_ENV = "SPACR_NO_SETUP"
[docs] def skipped_on_purpose(environ=None) -> bool: """Has this launch asked not to be shown the setup screen? The setup screen is modal and is the first thing a launch draws. On a server, a batch job that inherits a stale profile would wait on an invisible modal dialog until it was killed, and the only symptom would be a run that never starts. So a launch can say no, and one already has when it runs under the offscreen or minimal platform plugin -- nobody is there to answer a question drawn into a buffer nothing displays. """ import os environ = os.environ if environ is None else environ said = str(environ.get(SKIP_ENV, "")).strip().lower() if said in ("1", "true", "yes", "on"): return True if said in ("0", "false", "no", "off"): return False return str(environ.get("QT_QPA_PLATFORM", "")).strip().lower() in ( "offscreen", "minimal", "vnc")
[docs] def take_the_setup_flags(argv): """(remaining argv, asked to skip). Consumes the flags it recognises. They are consumed rather than ignored because `launch` reads the first argument as the module to open into, and an unconsumed `--no-setup` would be looked up as a module name and quietly open nothing. :param argv: the command-line words after the program name, or ``None``; a word in :data:`SKIP_FLAGS` (compared stripped and lower-cased) is removed and counts as asking to skip. """ kept, asked = [], False for word in list(argv or []): if str(word).strip().lower() in SKIP_FLAGS: asked = True else: kept.append(word) return kept, asked
#: The questions, in the order the request listed them, each as #: ``(key, label, getter, setter, choices)``. #: #: `choices` is ``[(value, label)]`` for a chooser, or ``None`` for a #: toggle. THE ACCESSORS ARE THE PREFERENCE MODULE'S OWN, not a copy: this #: screen writes the same store every other panel reads, so an answer given #: here and an answer given in Preferences are the same answer.
[docs] def questions() -> List[Tuple[str, str, Callable, Callable, Any]]: """Build the question list against the live preference module. Setup asks nothing about AI providers, GitHub or error reports: spaCR AI (free) and anonymised error reports are on by default, the terms of use describe both, and Preferences → AI changes them. """ from . import preferences as prefs def choices_of(names): """Names paired with their readable spellings, for a picker.""" return [(n, str(n).replace("_", " ")) for n in names] out: List[Tuple[str, str, Callable, Callable, Any]] = [ ("language", "Language", prefs.get_language, prefs.set_language, _language_choices()), ("theme", "Theme", prefs.get_theme_choice, prefs.set_theme_choice, [(value, caption) for caption, value in prefs.theme_choices()]), ("colour_blind", "Colour-blind mode", prefs.get_color_blind_mode, prefs.set_color_blind_mode, choices_of(prefs.VALID_CB_MODES)), ("spacr_mode", "Performance", prefs.get_performance_level, prefs.set_performance_level, [(level, prefs.PERFORMANCE_LABELS.get( level, str(level).replace("_", " "))) for level in prefs.PERFORMANCE_LEVELS]), ("hash_inputs", "Reproducibility hash", prefs.get_hash_inputs, prefs.set_hash_inputs, None), ] return [q for q in out if q[4] is None or q[4]]
def _language_choices(): """``[(code, native name)]`` for every language spaCR is translated to. IN THEIR OWN SCRIPT, because the reader of this list is by definition somebody who may not read the current one. """ try: from .i18n import LANGUAGES return [(one.code, one.native_name) for one in LANGUAGES] except Exception: # noqa: BLE001 return [("en", "English")]
[docs] def apply(answers: Dict[str, Any]) -> List[str]: """Write the answers through the preference module. Returns what failed. Each answer is written on its own, so a value the preference module rejects is reported and the other answers are still saved. :param answers: ``{question key: value}`` from the setup screen, keyed as in :func:`questions` (e.g. ``"language"``, ``"theme"``); keys that are not questions are ignored, and each value is passed to its question's setter. """ trouble: List[str] = [] for key, _label, _get, setter, _choices in questions(): if key not in answers: continue try: setter(answers[key]) except Exception as exc: # noqa: BLE001 trouble.append(f"{key}: {exc}") return trouble
[docs] def current() -> Dict[str, Any]: """What the answers are now, for the screen to open on.""" out: Dict[str, Any] = {} for key, _label, getter, _set, _choices in questions(): try: out[key] = getter() except Exception: # noqa: BLE001 continue return out