spacr.updater

Auto-updater — compare local spacr to PyPI + the nightly branch.

Exposes a small API the Qt GUI’s Help → “Check for updates” menu entry can call. Nothing runs automatically; users always trigger a check + confirm any upgrade.

The updater talks to two sources:

  • PyPI — https://pypi.org/pypi/spacr/json for the latest released version.

  • GitHub — the nightly branch’s HEAD commit hash, so nightly users see how many commits they’re behind.

It also answers Home’s News panel, through fetch_release_notes(): the published releases, cached for a day, so a running copy can show a release newer than the one bundled in its own wheel.

Both fetches use urllib from the stdlib to avoid pulling in an extra HTTP dependency. Timeouts are short (3 s) so a slow / offline network doesn’t block the UI. Errors are absorbed and surfaced as “couldn’t check” — never a crash.

Public API:

from spacr.updater import check_for_updates, run_pip_upgrade

info = check_for_updates()   # UpdateInfo
if info.upgrade_available:
    run_pip_upgrade()

Classes

DryRun

Represent a parsed pip install --dry-run result.

InstallOffer

What pressing Install on a greyed-out option should do.

PackageChange

One line of a dry-run report: what a package is now, and would be.

UpdateInfo

Result of a version check.

Functions

canonical_package_name(→ str)

PEP 503 normalisation, so scikit_learn and Scikit-Learn match.

check_for_updates(→ UpdateInfo)

Query PyPI + GitHub and return an UpdateInfo.

dry_run_command(→ list)

Return the command that previews installation of requirement.

dry_run_install(→ DryRun)

Ask the packaging tool what installing requirement would change.

editable_install_location(→ Optional[str])

Return the active spaCR checkout, or None for a regular install.

fetch_release_notes(→ list)

The repository's releases, newest first, or [].

find_uv(→ Optional[str])

The uv the desktop installers bootstrap, if this is such an install.

install_decision(→ dict)

Whether a plan may proceed, and what a second confirmation must say.

install_requirement_command(→ list)

Return the command that installs requirement in this environment.

installed_version(→ Optional[str])

The version of name installed here, or None if it is absent.

launch_updated_app(→ int)

Launch the updated Qt application after the old event loop has stopped.

news_cache_path(→ pathlib.Path)

Where the fetched release list is remembered between launches.

offer_elsewhere(→ InstallOffer)

An offer that names another environment and runs nothing.

offer_impossible(→ InstallOffer)

An offer that says installing cannot help, and why.

offer_install(→ InstallOffer)

An offer that may run pip here, after a dry run and a confirmation.

offer_ready(→ InstallOffer)

An offer for something that is already available.

pip_available(→ bool)

Is python -m pip usable in this interpreter?

run_install_command(args[, timeout])

Run one packaging command, capturing everything it said.

run_pip_upgrade([pre_release, target_version])

Upgrade spacr in place, capturing what the packaging tool said.

upgrade_command(→ list)

Upgrade this interpreter, optionally to the exact version offered.

Module Contents

class spacr.updater.DryRun[source]

Represent a parsed pip install --dry-run result.

ok is False when the resolver refused, when the tool could not be run, or when it returned no machine-readable plan.

Parameters:
  • requirement – pip requirement string that was resolved.

  • ok – whether the packaging command succeeded and returned a readable machine plan.

  • changes – parsed resolver entries, including additions, version moves, and removals as PackageChange records.

  • error – resolver or launch failure detail when ok is false, otherwise None.

  • raw – concatenated resolver stdout and stderr retained for diagnostics.

summary() → str[source]

Return the report shown before installation confirmation.

property additions: Tuple[PackageChange, ...][source]

Packages that are not here at all today.

property moves: Tuple[PackageChange, ...][source]

Packages already installed whose version would change.

property protected_changes: Tuple[PackageChange, ...][source]

Protected packages whose version would change or be removed.

property protected_moves: Tuple[PackageChange, ...][source]

The moves that land on PROTECTED_PACKAGES.

property removals: Tuple[PackageChange, ...][source]

Packages already installed that the resolver would remove.

class spacr.updater.InstallOffer[source]

What pressing Install on a greyed-out option should do.

One shape, two callers – the regression backend picker (spacr.regression_backends.backend_install_offer()) and the Image UMAP’s GPU acceleration (spacr.gpu_reduce.install_offer()) – so the panel that shows it does not have to know which asked.

Parameters:
  • action – offer state, normally one of OFFER_ACTIONS; only "install" with a nonempty requirement can produce a command.

  • title – short capability heading shown by the availability interface.

  • message – primary explanation shown with the offer.

  • requirement – pip requirement used to build a local install command for an install action, or None when no local command is available.

  • recipe – optional setup or external-environment instructions appended to the message.

  • runs_anything – informational local-install marker set by offer_install(); command, not this flag, controls execution.

as_text() → str[source]

Message and recipe as one block, for a dialog or a log.

property command: list | None[source]

The install command, or None when nothing may be run.

class spacr.updater.PackageChange[source]

One line of a dry-run report: what a package is now, and would be.

Parameters:
  • name – distribution name as reported by the resolver; its spelling is retained.

  • current – installed version, or the version reported as removed by uv, or None when the distribution is absent.

  • proposed – version the resolver would install, or None when it would remove the distribution.

describe() → str[source]

'numpy 1.26.4 -> 2.2.6' or 'cuml-cu12 26.8.0 (new)'.

property is_addition: bool[source]

Nothing is installed under this name today.

property is_move: bool[source]

A version already here would change.

property is_removal: bool[source]

A distribution installed today would be removed.

property protected: bool[source]

Is this one of PROTECTED_PACKAGES?

class spacr.updater.UpdateInfo[source]

Result of a version check.

Parameters:
  • installed_version – locally installed spaCR version, or "unknown" when neither distribution’s metadata is readable.

  • latest_release – latest spaCR version returned by PyPI, or None when it is missing or unavailable.

  • nightly_sha – first seven characters of the nightly branch head returned by GitHub, or None when unavailable.

  • error – first PyPI or GitHub request failure, prefixed by service name, or None when neither request failed.

property upgrade_available: bool[source]

Return whether PyPI advertises a version newer than this install.

spacr.updater.canonical_package_name(name) → str[source]

PEP 503 normalisation, so scikit_learn and Scikit-Learn match.

Parameters:

name – any spelling of a distribution name.

Returns:

lower-case with runs of -, _ and . collapsed to a single -.

spacr.updater.check_for_updates(timeout: float = 3.0) → UpdateInfo[source]

Query PyPI + GitHub and return an UpdateInfo.

Parameters:

timeout – per-request timeout in seconds.

spacr.updater.dry_run_command(requirement) → list[source]

Return the command that previews installation of requirement.

pip’s --report - writes a JSON document to stdout and installs nothing; uv pip install --dry-run prints + name==version lines. Both are parsed by dry_run_install(), because the second is the only one available on the desktop installs whose venv has no pip.

Parameters:

requirement – pip requirement string whose installation to preview.

spacr.updater.dry_run_install(requirement, timeout: float = 600.0, runner=None) → DryRun[source]

Ask the packaging tool what installing requirement would change.

This function does not install packages. It resolves and reports proposed additions and version changes so they can be reviewed before installation.

Parameters:
  • requirement – a pip requirement string.

  • timeout – seconds before the resolver is given up on.

  • runner – injected for tests; defaults to subprocess.run().

Returns:

a DryRun.

spacr.updater.editable_install_location() → str | None[source]

Return the active spaCR checkout, or None for a regular install.

Returns:

the absolute path of the working tree, when this interpreter is running spaCR out of one.

Detection first reads the PEP 610 direct_url.json editable-install record. If that metadata is unavailable, it checks whether the imported package resides outside site-packages and inside a directory containing .git or pyproject.toml.

spacr.updater.fetch_release_notes(timeout: float = 4.0, max_age: float = NEWS_MAX_AGE_S) → list[source]

The repository’s releases, newest first, or [].

NEVER CALL THIS ON THE GUI THREAD. It opens a socket. Home’s News panel asks for it through the same _UpdateWorker the manual update check runs on, after the page has been shown.

Unauthenticated, because the alternative is a token the user does not have. That means the shared, per-address rate limit, which is why the answer is cached for a day and why every failure is silent: the bundled spacr/resources/release_notes.json remains the offline source of truth and an empty list simply leaves it alone.

Parameters:
  • timeout – request timeout in seconds. Short on purpose.

  • max_age – how old a cached answer may be before it is asked again, in seconds.

Returns:

release records shaped like the bundled resource’s, or an empty list when the answer is unavailable for any reason at all.

spacr.updater.find_uv() → str | None[source]

The uv the desktop installers bootstrap, if this is such an install.

The native installers build their environment with uv venv, which does not seed pip. On those installs python -m pip fails before it starts, so the updater has to use the same tool the installer did. uv is bootstrapped one level above the venv:

<install root>/bootstrap/uv
<install root>/venv/            <- sys.prefix
Returns:

an executable path, or None when this is an ordinary pip-managed environment.

spacr.updater.install_decision(dry_run: DryRun) → dict[source]

Whether a plan may proceed, and what a second confirmation must say.

An install that would move or remove NumPy, PyTorch, pandas, or scikit-learn is refused by default and needs a second confirmation naming what changes.

Parameters:

dry_run – the result of dry_run_install().

Returns:

{allowed, needs_second_confirmation, moves, headline, report}. allowed is False when the dry run did not answer – an install whose consequences are unknown is not offered.

spacr.updater.install_requirement_command(requirement) → list[source]

Return the command that installs requirement in this environment.

The same tool choice upgrade_command() makes – uv when this is a desktop install whose venv has no pip, python -m pip otherwise.

Parameters:

requirement – a pip requirement string, e.g. 'cuml-cu12'.

spacr.updater.installed_version(name) → str | None[source]

The version of name installed here, or None if it is absent.

Parameters:

name – distribution name to query from installed package metadata.

spacr.updater.launch_updated_app() → int[source]

Launch the updated Qt application after the old event loop has stopped.

Returns:

process identifier of the detached replacement application.

Raises:

OSError – the replacement could not be started.

spacr.updater.news_cache_path() → pathlib.Path[source]

Where the fetched release list is remembered between launches.

spacr.updater.offer_elsewhere(title: str, message: str, recipe: str) → InstallOffer[source]

An offer that names another environment and runs nothing.

Parameters:
  • title – short heading shown for the optional capability.

  • message – explanation of why installation must happen elsewhere.

  • recipe – instructions for preparing the external environment.

spacr.updater.offer_impossible(title: str, message: str, recipe: str = '') → InstallOffer[source]

An offer that says installing cannot help, and why.

Parameters:
  • title – short heading shown for the unavailable capability.

  • message – explanation of why installation cannot satisfy it.

spacr.updater.offer_install(title: str, message: str, requirement: str, recipe: str = '') → InstallOffer[source]

An offer that may run pip here, after a dry run and a confirmation.

Parameters:
  • title – short heading shown for the optional capability.

  • message – explanation shown with the install offer.

  • requirement – pip requirement string that can satisfy the feature.

spacr.updater.offer_ready(title: str, message: str) → InstallOffer[source]

An offer for something that is already available.

Parameters:
  • title – short heading shown for the available capability.

  • message – explanation shown with the offer.

spacr.updater.pip_available() → bool[source]

Is python -m pip usable in this interpreter?

uv venv does not seed pip, which is the case find_uv() exists for. It matters here because pip is the only one of the two that can produce a machine-readable --report.

spacr.updater.run_install_command(args, timeout: float = 1800.0)[source]

Run one packaging command, capturing everything it said.

Install offers use the same capture behavior as run_pip_upgrade(), including when the application was launched without a terminal.

Parameters:
Returns:

(exit_code, output) with stdout and stderr combined.

spacr.updater.run_pip_upgrade(pre_release: bool = False, *, target_version=None)[source]

Upgrade spacr in place, capturing what the packaging tool said.

Parameters:
  • pre_release – pass --pre so pre-releases and .postN versions are considered.

  • target_version – install this exact offered version and verify it in a fresh process before reporting success. None retains an unpinned upgrade.

Returns:

(exit_code, output) with combined stdout and stderr. Captured output remains available to desktop installations launched without a terminal.

spacr.updater.upgrade_command(pre_release: bool = False, *, target_version=None) → list[source]

Upgrade this interpreter, optionally to the exact version offered.

Parameters:
  • pre_release – allow prerelease packages.

  • target_version – explicit version selected by the update check.