plesty.server.services.updates ============================== .. py:module:: plesty.server.services.updates .. autoapi-nested-parse:: Updating the application from inside it: *Help ▸ Check for updates…* (#29). Two installs can update themselves, the two the installers make: - **Windows** — a virtual environment of its own, ``%LOCALAPPDATA%\PLESTY Bench\app``, updated with ``pip install`` into it. - **macOS and Linux** — a ``uv tool`` environment (it carries uv's ``uv-receipt.toml``), updated with ``uv tool install --force``. Neither update runs inside the console. On Windows it cannot: the console is that environment's interpreter, Windows will not replace a file a running process holds, and pip fails on the first one it meets. Elsewhere it could, but the console would go on running the old code until restarted. So the console hands the update to a helper outside the environment — ``assets/update.ps1`` or ``assets/update.sh``, copied out first — which waits for the console to exit and installs. Beside it the console starts the update window, ``assets/update_window.py``, on the interpreter the environment was made from: it shows the install as it runs and, when it has finished, offers to open the new console. Where no window can be shown, the helper starts the console again itself. The result is left in two files the console reads on its next start, so a failure is reported even if the window was closed early, and is kept as the last update, which Help ▸ About shows. A console run from a source checkout is not an install, and is told so. Attributes ---------- .. autoapisummary:: plesty.server.services.updates.PACKAGE plesty.server.services.updates.REQUIREMENT plesty.server.services.updates.PYPI_URL plesty.server.services.updates.INSTALL_DIR plesty.server.services.updates.UV_RECEIPT plesty.server.services.updates.PENDING plesty.server.services.updates.RECORD plesty.server.services.updates.CHECKOUT_NOTE plesty.server.services.updates.STARTUP_CHECK plesty.server.services.updates.CHECK_EVERY Exceptions ---------- .. autoapisummary:: plesty.server.services.updates.UpdateError Classes ------- .. autoapisummary:: plesty.server.services.updates.Install plesty.server.services.updates.LastUpdate plesty.server.services.updates.UpdateRecord Functions --------- .. autoapisummary:: plesty.server.services.updates.installed_version plesty.server.services.updates.latest_version plesty.server.services.updates._fetch plesty.server.services.updates.is_newer plesty.server.services.updates._release plesty.server.services.updates._is_prerelease plesty.server.services.updates.install_dir plesty.server.services.updates.state_dir plesty.server.services.updates.current_install plesty.server.services.updates.describe_install plesty.server.services.updates._same_path plesty.server.services.updates._on_windows plesty.server.services.updates.update_log plesty.server.services.updates.launcher plesty.server.services.updates.window_python plesty.server.services.updates.start_update plesty.server.services.updates._find_uv plesty.server.services.updates._beside plesty.server.services.updates.last_update plesty.server.services.updates._keep plesty.server.services.updates.update_record plesty.server.services.updates.startup_update Module Contents --------------- .. py:data:: PACKAGE :value: 'plesty-server' .. py:data:: REQUIREMENT :value: 'plesty-server[gui]' .. py:data:: PYPI_URL :value: 'https://pypi.org/pypi/plesty-server/json' .. py:data:: INSTALL_DIR :value: 'PLESTY Bench' .. py:data:: UV_RECEIPT :value: 'uv-receipt.toml' .. py:data:: PENDING :value: 'update.json' .. py:data:: RECORD :value: 'last-update.json' .. py:data:: CHECKOUT_NOTE :value: 'This copy runs from a source checkout, not from an install, so it cannot update itself. Update... .. py:exception:: UpdateError Bases: :py:obj:`RuntimeError` The newest version could not be read, or the update could not be started. Initialize self. See help(type(self)) for accurate signature. .. py:class:: Install An install this console can update. :ivar kind: ``windows`` (the installer's environment) or ``uv-tool``. :ivar env: Its environment. :ivar state: Where the helper and its log go — outside *env*. .. py:attribute:: kind :type: str .. py:attribute:: env :type: pathlib.Path .. py:attribute:: state :type: pathlib.Path .. py:class:: LastUpdate What the helper left of the last update, read once on the next start. :ivar ok: Whether the install succeeded. :ivar reason: Why it failed, in one line; empty on success. :ivar log: The helper's log, for the details. :ivar shown: Whether the update window already showed the operator how it went. .. py:attribute:: ok :type: bool .. py:attribute:: reason :type: str .. py:attribute:: log :type: pathlib.Path .. py:attribute:: shown :type: bool :value: False .. py:class:: UpdateRecord The last update that finished, as Help ▸ About shows it. :ivar from_version: The version that was installed before it; ``""`` if unknown. :ivar to_version: The version it installed, or tried to. :ivar asked: When it was asked for, ISO 8601; ``""`` if unknown. :ivar finished: When the helper finished, ISO 8601. :ivar ok: Whether the install succeeded. :ivar reason: Why it failed; empty on success. :ivar log: The helper's log. .. py:attribute:: from_version :type: str .. py:attribute:: to_version :type: str .. py:attribute:: asked :type: str .. py:attribute:: finished :type: str .. py:attribute:: ok :type: bool .. py:attribute:: reason :type: str .. py:attribute:: log :type: str .. py:function:: installed_version() -> str The version of the console that is running; ``"dev"`` for a checkout never installed. .. py:function:: latest_version(fetch: collections.abc.Callable[[str], bytes] | None = None) -> str The newest release of the console on PyPI. :param fetch: Reads a URL; a 10-second request by default. :returns: Its version. :raises UpdateError: When PyPI cannot be reached or does not answer as expected. .. py:function:: _fetch(url: str) -> bytes .. py:function:: is_newer(candidate: str, current: str) -> bool Whether release *candidate* is later than *current*. Compared by their release numbers, which is all a PyPI release carries. A *current* that is not a plain release — ``dev``, or a checkout's ``0.5.0.dev3+abc`` — is older than the release it leads to. .. py:function:: _release(version: str) -> tuple[int, Ellipsis] | None .. py:function:: _is_prerelease(version: str) -> bool .. py:function:: install_dir(environ: collections.abc.Mapping[str, str] | None = None) -> pathlib.Path | None The Windows installer's directory, ``%LOCALAPPDATA%\PLESTY Bench``, if any. .. py:function:: state_dir(environ: collections.abc.Mapping[str, str] | None = None, platform: str | None = None) -> pathlib.Path | None Where the update helper and its log go on this platform. Windows: beside the install log (:func:`install_dir`). Elsewhere: the user's state directory, ``$XDG_STATE_HOME/plesty-bench`` or ``~/.local/state/plesty-bench`` — never inside the environment being replaced. .. py:function:: current_install(prefix: str | None = None, environ: collections.abc.Mapping[str, str] | None = None, platform: str | None = None) -> Install | None The install this console runs from, when it is one that can update itself. :param prefix: The running interpreter's environment; ``sys.prefix`` by default. :param environ: The environment variables; this process's by default. :param platform: The platform; this one by default. :returns: The Windows installer's environment, ``%LOCALAPPDATA%\PLESTY Bench\app``; a ``uv tool`` environment (one holding :data:`UV_RECEIPT`); or ``None`` for anything else — a source checkout, above all. .. py:function:: describe_install(install: Install | None, prefix: str | None = None) -> str How this console was installed, and where, in a line for Help ▸ About. .. py:function:: _same_path(one: pathlib.Path, other: pathlib.Path) -> bool .. py:function:: _on_windows(platform: str | None) -> bool Whether *platform* — this one when ``None`` — is Windows, as :mod:`host` decides. .. py:function:: update_log(base: pathlib.Path) -> pathlib.Path Where the helper writes its log; its ``.exit`` and ``.reason`` lie beside it. .. py:function:: launcher(install: Install) -> pathlib.Path The console's launcher once the update is in: what *Open PLESTY Bench* starts. Windows: the copy the installer put in ``~\.local\bin``, which the shortcuts start, else the environment's own. A ``uv tool``: the environment's, which ``uv tool install --force`` puts back in place. .. py:function:: window_python() -> pathlib.Path | None The interpreter to show the update window with, or ``None`` when it has no Tk. The one the environment was made from: it lies outside the environment the update replaces. Asked whether it can import :mod:`tkinter` first, so an interpreter without it leaves the helper to start the console again. .. py:function:: start_update(version: str, install: Install, pid: int | None = None, spawn: collections.abc.Callable[Ellipsis, Any] | None = None, uv: collections.abc.Callable[[], str] | None = None, python: collections.abc.Callable[[], pathlib.Path | None] | None = None, installed: str | None = None, theme: collections.abc.Mapping[str, Any] | None = None) -> pathlib.Path Hand the update to the helper, which runs once this console has exited. :param version: The release to install. :param install: The install to update (:func:`current_install`). :param pid: The console's process, which the helper waits out; this one by default. :param spawn: Starts the helper and the window; :class:`subprocess.Popen` by default. :param uv: Finds ``uv`` for a ``uv tool`` install; the bench's own search by default. :param python: Finds the interpreter for the update window, ``None`` for none; :func:`window_python` by default. :param installed: The version being replaced, for the record; this one by default. :param theme: The console's palette and fonts, for the window to be drawn in (``tokens``, ``ui_font``, ``mono_font``, ``mark``); its own copy of the palette when ``None``. :returns: The helper's log. :raises UpdateError: When the helper cannot be prepared or started. .. py:function:: _find_uv() -> str .. py:function:: _beside(log: pathlib.Path, suffix: str) -> pathlib.Path .. py:function:: last_update(base: pathlib.Path | None) -> LastUpdate | None What the last update left, once: its marker files are removed as they are read. What it left is kept as the last update (:func:`update_record`) for Help ▸ About, which shows it for as long as no newer update finishes. :param base: The helper's directory (:func:`state_dir`). :returns: The result, or ``None`` when no update has finished since the last start. .. py:function:: _keep(base: pathlib.Path, result: LastUpdate, finished: datetime.datetime) -> None Keep *result*, with what was asked for, as the last update. .. py:function:: update_record(base: pathlib.Path | None) -> UpdateRecord | None The last update that finished, or ``None`` when there has been none to tell of. :param base: The helper's directory (:func:`state_dir`). .. py:data:: STARTUP_CHECK :value: 'update-check.json' .. py:data:: CHECK_EVERY .. py:function:: startup_update(home: pathlib.Path, installed: str, now: datetime.datetime | None = None, fetch: collections.abc.Callable[[str], bytes] | None = None) -> str | None The release newer than *installed* to tell of when the console starts, or ``None``. PyPI is asked at most once per :data:`CHECK_EVERY`; within it, the release the last check heard of is used again, so the line stays on every start until the bench is updated. Only a check that answered is written down: one with no network is tried again on the next start. Never raises — a bench with no network says nothing. :param home: The bench home, where :data:`STARTUP_CHECK` is kept. :param installed: The version running. :param now: The time; injected by tests. :param fetch: Reads a URL; injected by tests.