From 6abf99dd23e8d78cfe15bb31e179f4860f570ba4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 07:37:23 +0000 Subject: [PATCH 1/3] Add uv-aware self-update abstraction --- CONTRIBUTING.md | 19 ++- README.md | 26 +++- plextraktsync/cli.py | 6 +- plextraktsync/commands/self_update.py | 46 +++--- plextraktsync/factory/Factory.py | 6 +- plextraktsync/util/Version.py | 13 +- plextraktsync/util/packaging.py | 210 ++++++++++++++++++++++++-- tests/test_self_update_packaging.py | 198 ++++++++++++++++++++++++ 8 files changed, 473 insertions(+), 51 deletions(-) create mode 100644 tests/test_self_update_packaging.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b75dba4052..9c6d64a635 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,14 +111,15 @@ pipenv run plextraktsync ### Install code from Pull request -This requires prior installation with `pipx`. +This requires prior installation with `pipx` or `uv`. Replace `838` with a pull request you intend to install. ``` plextraktsync self-update --pr 838 ``` -It will create new binary `plextraktsync@838` for that pull request. You need to run this binary instead of `plextraktsync`. +With `pipx`, it creates a new binary `plextraktsync@838` for that pull request. You need to run this binary instead of `plextraktsync`. +With `uv`, it updates your `plextraktsync` tool install to the pull request ref. To pull new changes for the same pull request: @@ -126,6 +127,12 @@ To pull new changes for the same pull request: plextraktsync@838 self-update ``` +or with `uv`: + +``` +plextraktsync self-update --pr 838 +``` + If you need to do the same in docker container, you should: ͏ ͏ ͏1. first prepare the container with: @@ -134,6 +141,8 @@ If you need to do the same in docker container, you should: $ docker-compose run --rm --entrypoint sh plextraktsync /app # pip install pipx /app # pipx install plextraktsync +/app # pip install uv +/app # uv tool install plextraktsync /app # apk add git /app # plextraktsync self-update --pr 969 /app # plextraktsync@969 info @@ -167,6 +176,12 @@ uninstalled PlexTraktSync@984! ✨ 🌟 ✨ $ ``` +For `uv`, remove the tool with: + +``` +uv tool uninstall plextraktsync +``` + ## Building docker image You can build docker image from default branch: diff --git a/README.md b/README.md index 5ea8faeded..b514e286ca 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ have a file containing those on your harddrive, you can not use this project. - [Pre-requisites](#pre-requisites) - [Installation](#installation) - [pipx](#pipx) + - [uv](#uv) - [Docker Compose](#docker-compose) - [Install code from Pull request](#install-code-from-pull-request), development - [Windows Setup (optional alternative)](#windows-setup-optional-alternative), unsupported @@ -104,6 +105,28 @@ which just calls `pipx` with: pipx upgrade PlexTraktSync ``` +### uv + +Installation with [uv][install-uv]: + +``` +uv tool install PlexTraktSync +``` + +and to upgrade: + +``` +plextraktsync self-update +``` + +which calls `uv` with: + +``` +uv tool upgrade plextraktsync +``` + +If both `pipx` and `uv` installs exist, `plextraktsync self-update` updates all detected installs. + to run: ``` @@ -115,6 +138,7 @@ NOTE: `pipx` install will use OS specific paths for Config, Logs, Cache, see [platformdirs]: https://pypi.org/project/platformdirs [install-pipx]: https://github.com/pypa/pipx#install-pipx +[install-uv]: https://docs.astral.sh/uv/getting-started/installation/ ### Docker Compose @@ -554,7 +578,7 @@ Commands: inspect Inspect details of an object login Log in to Plex and Trakt if needed plex-login Log in to Plex Account to obtain Access Token. - self-update Update PlexTraktSync to the latest version using pipx + self-update Update PlexTraktSync to the latest version using pipx and/or uv sync Perform sync between Plex and Trakt trakt-login Log in to Trakt Account to obtain Access Token. unmatched List media that has no match in Trakt or Plex diff --git a/plextraktsync/cli.py b/plextraktsync/cli.py index 36bc630654..97e8e49f06 100644 --- a/plextraktsync/cli.py +++ b/plextraktsync/cli.py @@ -321,12 +321,12 @@ def download(): ) def self_update(): """ - Update PlexTraktSync to the latest version using pipx + Update PlexTraktSync to the latest version using pipx and/or uv \b $ plextraktsync self-update - Updating PlexTraktSync to latest using pipx - upgraded package plextraktsync from 0.15.3 to 0.18.5 (location: /Users/glen/.local/pipx/venvs/plextraktsync) + Updating PlexTraktSync install 'plextraktsync' using pipx + Updating PlexTraktSync install 'plextraktsync' using uv """ diff --git a/plextraktsync/commands/self_update.py b/plextraktsync/commands/self_update.py index 754cb32da0..16e06750e4 100644 --- a/plextraktsync/commands/self_update.py +++ b/plextraktsync/commands/self_update.py @@ -2,14 +2,7 @@ from plextraktsync.factory import factory from plextraktsync.util.execp import execp - - -def has_previous_pr(pr: int): - from plextraktsync.util.packaging import pipx_installed - - package = pipx_installed(f"plextraktsync@{pr}") - - return package is not None +from plextraktsync.util.packaging import backend_for_name, list_managed_installs, managed_installs_by_backend, program_name def pr_number() -> int | None: @@ -17,10 +10,8 @@ def pr_number() -> int | None: Check if current executable is named plextraktsync@ """ - import sys - try: - pr = sys.argv[0].split("@")[1] + pr = program_name().split("@")[1] except IndexError: return None @@ -37,16 +28,29 @@ def self_update(pr: int): if pr: print(f"Installed as pr #{pr}, enabling pr mode") + installs = list_managed_installs() + if not installs: + print("No managed PlexTraktSync installation found in pipx or uv") + return + if pr: - if has_previous_pr(pr): - # Uninstall because pipx doesn't update otherwise: - # - https://github.com/pypa/pipx/issues/902 - print(f"Uninstalling previous plextraktsync@{pr}") - execp(f"pipx uninstall plextraktsync@{pr}") - - print(f"Updating PlexTraktSync to the pull request #{pr} version using pipx") - execp(f"pipx install --suffix=@{pr} --force git+https://github.com/Taxel/PlexTraktSync@refs/pull/{pr}/head") + for backend_name, backend_installs in managed_installs_by_backend().items(): + backend = backend_for_name(backend_name) + if backend is None: + continue + + print(f"Updating PlexTraktSync in {backend_name} to pull request #{pr}") + commands = backend.pr_update_commands(pr, backend_installs) + for command in commands: + if backend_name == "pipx" and command.startswith("pipx uninstall"): + print(f"Uninstalling previous plextraktsync@{pr}") + execp(command) return - print("Updating PlexTraktSync to the latest version using pipx") - execp("pipx upgrade PlexTraktSync") + for install in installs: + backend = backend_for_name(install.backend) + if backend is None: + continue + + print(f"Updating PlexTraktSync install '{install.app_name}' using {install.backend}") + execp(backend.latest_update_command(install)) diff --git a/plextraktsync/factory/Factory.py b/plextraktsync/factory/Factory.py index 10bd6e8df5..ead602bed5 100644 --- a/plextraktsync/factory/Factory.py +++ b/plextraktsync/factory/Factory.py @@ -201,11 +201,9 @@ def walker(self): @cached_property def enable_self_update(self): - from plextraktsync.util.packaging import pipx_installed, program_name + from plextraktsync.util.packaging import self_update_available - package = pipx_installed(program_name()) - - return package is not None + return self_update_available() @cached_property def web_socket_listener(self): diff --git a/plextraktsync/util/Version.py b/plextraktsync/util/Version.py index 5f5d8b9953..f24553b0cf 100644 --- a/plextraktsync/util/Version.py +++ b/plextraktsync/util/Version.py @@ -19,9 +19,10 @@ def full_version(self): return __version__ # Print version from pip - if self.pipx_installed: + if self.managed_installed: v = self.vcs_info - return f"{__version__[0:-4]}@pr/{v['pr']}#{v['short_commit_id']}" + if v: + return f"{__version__[0:-4]}@pr/{v['pr']}#{v['short_commit_id']}" # If installed with Git gv = self.git_version_info @@ -73,15 +74,15 @@ def vcs_info(self): return vcs_info("PlexTraktSync") @property - def pipx_installed(self): + def managed_installed(self): if not self.installed: return False - from plextraktsync.util.packaging import pipx_installed, program_name + from plextraktsync.util.packaging import managed_install_for_program - package = pipx_installed(program_name()) + install = managed_install_for_program() - return package is not None + return install is not None @property def installed(self): diff --git a/plextraktsync/util/packaging.py b/plextraktsync/util/packaging.py index 49ce1ffbb6..a4c0248735 100644 --- a/plextraktsync/util/packaging.py +++ b/plextraktsync/util/packaging.py @@ -1,7 +1,9 @@ from __future__ import annotations import json +import re import site +from dataclasses import dataclass from json import JSONDecodeError from os.path import dirname @@ -40,6 +42,64 @@ def pip_installed(name: str): def pipx_installed(package: str): + for install in PIPX_BACKEND.list_installs(): + if install.app_name.lower() == package.lower(): + return {"package": install.package_name, "package_or_url": install.source} + + return None + + +def program_name(): + """ + Return current program name: + - pipx: plextraktsync + - pipx for pr 1000: plextraktsync@1000 + """ + + import sys + from os.path import basename + + return basename(sys.argv[0]) + + +@dataclass(frozen=True) +class ManagedInstall: + backend: str + app_name: str + package_name: str + source: str | None = None + pr: int | None = None + + +class InstallBackend: + name = "unknown" + + def list_installs(self) -> list[ManagedInstall]: + return [] + + def latest_update_command(self, install: ManagedInstall) -> str: + raise NotImplementedError + + def pr_update_commands(self, pr: int, installs: list[ManagedInstall]) -> list[str]: + raise NotImplementedError + + +def _extract_pr(source: str | None = None, app_name: str | None = None) -> int | None: + values = [source or "", app_name or ""] + for value in values: + match = re.search(r"refs/pull/(\d+)/head", value) + if match: + return int(match.group(1)) + + if app_name: + suffix = app_name.split("@") + if len(suffix) > 1 and suffix[-1].isnumeric(): + return int(suffix[-1]) + + return None + + +def _pipx_list_data(): try: output = execx("pipx list --json") except FileNotFoundError: @@ -54,25 +114,147 @@ def pipx_installed(package: str): if install_data is None: return None - try: - package = install_data["venvs"][package]["metadata"]["main_package"] - except KeyError: - return None + return install_data - return package +class PipxInstallBackend(InstallBackend): + name = "pipx" -def program_name(): - """ - Return current program name: - - pipx: plextraktsync - - pipx for pr 1000: plextraktsync@1000 - """ + def list_installs(self) -> list[ManagedInstall]: + install_data = _pipx_list_data() + if not install_data: + return [] - import sys - from os.path import basename + installs = [] + venvs = install_data.get("venvs", {}) + for data in venvs.values(): + main_package = data.get("metadata", {}).get("main_package", {}) + package_name = main_package.get("package", "") + source = main_package.get("package_or_url") + if not package_name and not source: + continue - return basename(sys.argv[0]) + if "plextraktsync" not in package_name.lower() and "plextraktsync" not in (source or "").lower(): + continue + + apps = main_package.get("apps") or [] + if not apps: + apps = ["plextraktsync"] + + for app in apps: + installs.append( + ManagedInstall( + backend=self.name, + app_name=app, + package_name=package_name or "PlexTraktSync", + source=source, + pr=_extract_pr(source=source, app_name=app), + ) + ) + + return installs + + def latest_update_command(self, install: ManagedInstall) -> str: + return f"pipx upgrade {install.app_name}" + + def pr_update_commands(self, pr: int, installs: list[ManagedInstall]) -> list[str]: + install_name = f"plextraktsync@{pr}" + commands = [] + if any(install.app_name.lower() == install_name.lower() for install in installs): + commands.append(f"pipx uninstall {install_name}") + commands.append(f"pipx install --suffix=@{pr} --force git+https://github.com/Taxel/PlexTraktSync@refs/pull/{pr}/head") + + return commands + + +class UvInstallBackend(InstallBackend): + name = "uv" + + def list_installs(self) -> list[ManagedInstall]: + try: + output = execx("uv tool list --show-version-specifiers --show-paths --show-python") + except FileNotFoundError: + return [] + if not output: + return [] + + installs = [] + lines = output.decode("utf-8", errors="ignore").splitlines() + for line in lines: + if not line or line.startswith("- "): + continue + + match = re.match(r"^([^\s]+)\s+v[^\s]+(?:\s+\[required:\s*(.+?)\])?", line) + if not match: + continue + + app_name = match.group(1) + source = match.group(2) + if "plextraktsync" not in app_name.lower() and "plextraktsync" not in (source or "").lower(): + continue + + installs.append( + ManagedInstall( + backend=self.name, + app_name=app_name, + package_name="PlexTraktSync", + source=source, + pr=_extract_pr(source=source, app_name=app_name), + ) + ) + + return installs + + def latest_update_command(self, install: ManagedInstall) -> str: + return f"uv tool upgrade {install.app_name}" + + def pr_update_commands(self, pr: int, installs: list[ManagedInstall]) -> list[str]: + return [f"uv tool install --force git+https://github.com/Taxel/PlexTraktSync@refs/pull/{pr}/head"] + + +PIPX_BACKEND = PipxInstallBackend() +UV_BACKEND = UvInstallBackend() +INSTALL_BACKENDS: dict[str, InstallBackend] = { + PIPX_BACKEND.name: PIPX_BACKEND, + UV_BACKEND.name: UV_BACKEND, +} + + +def install_backends() -> list[InstallBackend]: + return list(INSTALL_BACKENDS.values()) + + +def backend_for_name(name: str): + return INSTALL_BACKENDS.get(name) + + +def list_managed_installs() -> list[ManagedInstall]: + installs = [] + for backend in install_backends(): + installs.extend(backend.list_installs()) + + return installs + + +def managed_installs_by_backend() -> dict[str, list[ManagedInstall]]: + installs = {} + for install in list_managed_installs(): + installs.setdefault(install.backend, []).append(install) + + return installs + + +def managed_install_for_program(name: str | None = None): + name = name or program_name() + for install in list_managed_installs(): + if install.app_name.lower() == name.lower(): + return install + + return None + + +def self_update_available(): + return bool(list_managed_installs()) def vcs_info(package: str): diff --git a/tests/test_self_update_packaging.py b/tests/test_self_update_packaging.py new file mode 100644 index 0000000000..9ebd369e3d --- /dev/null +++ b/tests/test_self_update_packaging.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 -m pytest +from __future__ import annotations + +import json + +import pytest + +from plextraktsync.commands import self_update as self_update_command +from plextraktsync.factory.Factory import Factory +from plextraktsync.util import packaging +from plextraktsync.util.packaging import ManagedInstall + + +def test_pipx_detection(monkeypatch): + pipx_data = { + "venvs": { + "PlexTraktSync": { + "metadata": { + "main_package": { + "package": "PlexTraktSync", + "package_or_url": "PlexTraktSync", + "apps": ["plextraktsync"], + } + } + }, + "OtherTool": { + "metadata": { + "main_package": { + "package": "OtherTool", + "package_or_url": "OtherTool", + "apps": ["othertool"], + } + } + }, + } + } + + def fake_execx(command): + if command == "pipx list --json": + return json.dumps(pipx_data).encode("utf-8") + if command.startswith("uv tool list"): + return b"" + raise AssertionError(command) + + monkeypatch.setattr(packaging, "execx", fake_execx) + + installs = packaging.list_managed_installs() + assert len(installs) == 1 + assert installs[0].backend == "pipx" + assert installs[0].app_name == "plextraktsync" + + +def test_uv_detection(monkeypatch): + uv_output = "\n".join( + [ + "plextraktsync v0.35.0 [required: PlexTraktSync] [CPython 3.12.3] (/tmp/uv/plextraktsync)", + "- plextraktsync (/tmp/bin/plextraktsync)", + "plextraktsync-pr v0.35.0.dev0 [required: git+https://github.com/Taxel/PlexTraktSync@refs/pull/838/head] [CPython 3.12.3] (/tmp/uv/plextraktsync-pr)", + "- plextraktsync-pr (/tmp/bin/plextraktsync-pr)", + ] + ).encode("utf-8") + + def fake_execx(command): + if command == "pipx list --json": + return b"" + if command.startswith("uv tool list"): + return uv_output + raise AssertionError(command) + + monkeypatch.setattr(packaging, "execx", fake_execx) + + installs = packaging.list_managed_installs() + assert len(installs) == 2 + assert installs[0].backend == "uv" + assert installs[0].pr is None + assert installs[1].backend == "uv" + assert installs[1].pr == 838 + + +@pytest.mark.parametrize( + ("installs", "enabled"), + [ + ([ManagedInstall(backend="pipx", app_name="plextraktsync", package_name="PlexTraktSync")], True), + ([ManagedInstall(backend="uv", app_name="plextraktsync", package_name="PlexTraktSync")], True), + ( + [ + ManagedInstall(backend="pipx", app_name="plextraktsync", package_name="PlexTraktSync"), + ManagedInstall(backend="uv", app_name="plextraktsync", package_name="PlexTraktSync"), + ], + True, + ), + ([], False), + ], +) +def test_enable_self_update(monkeypatch, installs, enabled): + monkeypatch.setattr(packaging, "list_managed_installs", lambda: installs) + monkeypatch.setattr(packaging, "managed_install_for_program", lambda name=None: installs[0] if installs else None) + assert Factory().enable_self_update is enabled + + +def test_self_update_latest_across_backends(monkeypatch): + installs = [ + ManagedInstall(backend="pipx", app_name="plextraktsync", package_name="PlexTraktSync"), + ManagedInstall(backend="uv", app_name="plextraktsync", package_name="PlexTraktSync"), + ] + executed = [] + + class DummyBackend: + def __init__(self, command): + self.command = command + + def latest_update_command(self, install): + return self.command + + backends = { + "pipx": DummyBackend("pipx upgrade plextraktsync"), + "uv": DummyBackend("uv tool upgrade plextraktsync"), + } + + monkeypatch.setattr(self_update_command, "list_managed_installs", lambda: installs) + monkeypatch.setattr(self_update_command, "backend_for_name", lambda name: backends[name]) + monkeypatch.setattr(self_update_command, "execp", lambda command: executed.append(command)) + monkeypatch.setattr(self_update_command.factory, "print", lambda *_args, **_kwargs: None) + + self_update_command.self_update(pr=False) + + assert executed == ["pipx upgrade plextraktsync", "uv tool upgrade plextraktsync"] + + +def test_self_update_pr_across_backends(monkeypatch): + executed = [] + + class DummyBackend: + def __init__(self, commands): + self.commands = commands + + def pr_update_commands(self, pr, installs): + return list(self.commands) + + backends = { + "pipx": DummyBackend(["pipx install --suffix=@838 --force git+https://github.com/Taxel/PlexTraktSync@refs/pull/838/head"]), + "uv": DummyBackend(["uv tool install --force git+https://github.com/Taxel/PlexTraktSync@refs/pull/838/head"]), + } + + monkeypatch.setattr( + self_update_command, + "managed_installs_by_backend", + lambda: { + "pipx": [ManagedInstall(backend="pipx", app_name="plextraktsync", package_name="PlexTraktSync")], + "uv": [ManagedInstall(backend="uv", app_name="plextraktsync", package_name="PlexTraktSync")], + }, + ) + monkeypatch.setattr( + self_update_command, + "list_managed_installs", + lambda: [ManagedInstall(backend="pipx", app_name="plextraktsync", package_name="PlexTraktSync")], + ) + monkeypatch.setattr(self_update_command, "backend_for_name", lambda name: backends[name]) + monkeypatch.setattr(self_update_command, "execp", lambda command: executed.append(command)) + monkeypatch.setattr(self_update_command.factory, "print", lambda *_args, **_kwargs: None) + + self_update_command.self_update(pr=838) + + assert executed == [ + "pipx install --suffix=@838 --force git+https://github.com/Taxel/PlexTraktSync@refs/pull/838/head", + "uv tool install --force git+https://github.com/Taxel/PlexTraktSync@refs/pull/838/head", + ] + + +def test_pipx_pr_reinstall_workaround(): + backend = packaging.PIPX_BACKEND + installs = [ManagedInstall(backend="pipx", app_name="plextraktsync@838", package_name="PlexTraktSync")] + commands = backend.pr_update_commands(838, installs) + assert commands == [ + "pipx uninstall plextraktsync@838", + "pipx install --suffix=@838 --force git+https://github.com/Taxel/PlexTraktSync@refs/pull/838/head", + ] + + +def test_self_update_partial_backend_availability(monkeypatch): + executed = [] + + class DummyBackend: + def latest_update_command(self, install): + return "uv tool upgrade plextraktsync" + + installs = [ + ManagedInstall(backend="pipx", app_name="plextraktsync", package_name="PlexTraktSync"), + ManagedInstall(backend="uv", app_name="plextraktsync", package_name="PlexTraktSync"), + ] + + monkeypatch.setattr(self_update_command, "list_managed_installs", lambda: installs) + monkeypatch.setattr(self_update_command, "backend_for_name", lambda name: None if name == "pipx" else DummyBackend()) + monkeypatch.setattr(self_update_command, "execp", lambda command: executed.append(command)) + monkeypatch.setattr(self_update_command.factory, "print", lambda *_args, **_kwargs: None) + + self_update_command.self_update(pr=False) + assert executed == ["uv tool upgrade plextraktsync"] From fc9a5242ed2ddae4a5b3ace3f53ab5d3f789aa14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 07:39:08 +0000 Subject: [PATCH 2/3] Finalize uv and pipx self-update coverage --- tests/test_self_update_packaging.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_self_update_packaging.py b/tests/test_self_update_packaging.py index 9ebd369e3d..f5e228252d 100644 --- a/tests/test_self_update_packaging.py +++ b/tests/test_self_update_packaging.py @@ -55,7 +55,9 @@ def test_uv_detection(monkeypatch): [ "plextraktsync v0.35.0 [required: PlexTraktSync] [CPython 3.12.3] (/tmp/uv/plextraktsync)", "- plextraktsync (/tmp/bin/plextraktsync)", - "plextraktsync-pr v0.35.0.dev0 [required: git+https://github.com/Taxel/PlexTraktSync@refs/pull/838/head] [CPython 3.12.3] (/tmp/uv/plextraktsync-pr)", + "plextraktsync-pr v0.35.0.dev0 " + "[required: git+https://github.com/Taxel/PlexTraktSync@refs/pull/838/head] " + "[CPython 3.12.3] (/tmp/uv/plextraktsync-pr)", "- plextraktsync-pr (/tmp/bin/plextraktsync-pr)", ] ).encode("utf-8") From 39f58889203c6962f83672f42f8340a9ac6297d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 07:42:48 +0000 Subject: [PATCH 3/3] Polish uv self-update backend integration --- plextraktsync/commands/self_update.py | 5 ++--- plextraktsync/util/packaging.py | 12 +++++++----- tests/test_self_update_packaging.py | 7 ++++++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/plextraktsync/commands/self_update.py b/plextraktsync/commands/self_update.py index 16e06750e4..9ccad3e166 100644 --- a/plextraktsync/commands/self_update.py +++ b/plextraktsync/commands/self_update.py @@ -39,11 +39,10 @@ def self_update(pr: int): if backend is None: continue - print(f"Updating PlexTraktSync in {backend_name} to pull request #{pr}") + print(f"Updating PlexTraktSync using {backend_name} to pull request #{pr}") commands = backend.pr_update_commands(pr, backend_installs) for command in commands: - if backend_name == "pipx" and command.startswith("pipx uninstall"): - print(f"Uninstalling previous plextraktsync@{pr}") + print(f"Running [{backend_name}] {command}") execp(command) return diff --git a/plextraktsync/util/packaging.py b/plextraktsync/util/packaging.py index a4c0248735..3a6c9aafd3 100644 --- a/plextraktsync/util/packaging.py +++ b/plextraktsync/util/packaging.py @@ -9,6 +9,8 @@ from plextraktsync.util.execx import execx +MANAGED_PACKAGE_FILTER = "plextraktsync" + def installed(): """ @@ -134,7 +136,7 @@ def list_installs(self) -> list[ManagedInstall]: if not package_name and not source: continue - if "plextraktsync" not in package_name.lower() and "plextraktsync" not in (source or "").lower(): + if MANAGED_PACKAGE_FILTER not in package_name.lower() and MANAGED_PACKAGE_FILTER not in (source or "").lower(): continue apps = main_package.get("apps") or [] @@ -158,10 +160,10 @@ def latest_update_command(self, install: ManagedInstall) -> str: return f"pipx upgrade {install.app_name}" def pr_update_commands(self, pr: int, installs: list[ManagedInstall]) -> list[str]: - install_name = f"plextraktsync@{pr}" + target_name = f"plextraktsync@{pr}" commands = [] - if any(install.app_name.lower() == install_name.lower() for install in installs): - commands.append(f"pipx uninstall {install_name}") + if any(install.app_name.lower() == target_name for install in installs): + commands.append(f"pipx uninstall {target_name}") commands.append(f"pipx install --suffix=@{pr} --force git+https://github.com/Taxel/PlexTraktSync@refs/pull/{pr}/head") return commands @@ -190,7 +192,7 @@ def list_installs(self) -> list[ManagedInstall]: app_name = match.group(1) source = match.group(2) - if "plextraktsync" not in app_name.lower() and "plextraktsync" not in (source or "").lower(): + if MANAGED_PACKAGE_FILTER not in app_name.lower() and MANAGED_PACKAGE_FILTER not in (source or "").lower(): continue installs.append( diff --git a/tests/test_self_update_packaging.py b/tests/test_self_update_packaging.py index f5e228252d..8d3ed222e6 100644 --- a/tests/test_self_update_packaging.py +++ b/tests/test_self_update_packaging.py @@ -48,6 +48,7 @@ def fake_execx(command): assert len(installs) == 1 assert installs[0].backend == "pipx" assert installs[0].app_name == "plextraktsync" + assert installs[0].package_name == "PlexTraktSync" def test_uv_detection(monkeypatch): @@ -96,7 +97,11 @@ def fake_execx(command): ) def test_enable_self_update(monkeypatch, installs, enabled): monkeypatch.setattr(packaging, "list_managed_installs", lambda: installs) - monkeypatch.setattr(packaging, "managed_install_for_program", lambda name=None: installs[0] if installs else None) + monkeypatch.setattr( + packaging, + "managed_install_for_program", + lambda name=None: next((install for install in installs if name is None or install.app_name == name), None), + ) assert Factory().enable_self_update is enabled