|
2 | 2 | import subprocess |
3 | 3 | import sys |
4 | 4 | import time |
| 5 | +from dataclasses import dataclass |
5 | 6 | from importlib.metadata import version |
| 7 | +from packaging.version import Version, parse as parse_version |
6 | 8 |
|
7 | 9 | from rich.console import Console |
8 | | - |
9 | 10 | from bugscanx.utils.common import get_confirm |
10 | 11 |
|
11 | | -PACKAGE_NAME = "bugscan-x" |
12 | | -console = Console() |
13 | | - |
14 | 12 |
|
15 | | -def check_for_updates(): |
16 | | - try: |
17 | | - with console.status( |
18 | | - "[yellow]Checking for updates...", spinner="dots" |
19 | | - ): |
20 | | - current_version = version(PACKAGE_NAME) |
21 | | - try: |
| 13 | +@dataclass |
| 14 | +class VersionInfo: |
| 15 | + current_version: str |
| 16 | + latest_stable: str = None |
| 17 | + latest_prerelease: str = None |
| 18 | + prerelease_is_newer: bool = False |
| 19 | + |
| 20 | + |
| 21 | +class VersionManager: |
| 22 | + def __init__(self, package_name="bugscan-x"): |
| 23 | + self.package_name = package_name |
| 24 | + self.console = Console() |
| 25 | + |
| 26 | + def _is_prerelease(self, version_str): |
| 27 | + try: |
| 28 | + return Version(version_str).is_prerelease |
| 29 | + except Exception: |
| 30 | + return False |
| 31 | + |
| 32 | + def _parse_pip_output(self, output): |
| 33 | + lines = output.splitlines() |
| 34 | + versions = {} |
| 35 | + available_versions = [] |
| 36 | + |
| 37 | + for line in lines: |
| 38 | + line = line.strip() |
| 39 | + if line.startswith('Available versions:'): |
| 40 | + available_versions = [ |
| 41 | + v.strip(' ,') |
| 42 | + for v in line.split(':', 1)[1].split() |
| 43 | + ] |
| 44 | + elif line.startswith('INSTALLED:'): |
| 45 | + versions['installed'] = line.split(':')[1].strip() |
| 46 | + elif line.startswith('LATEST:'): |
| 47 | + versions['latest'] = line.split(':')[1].strip() |
| 48 | + |
| 49 | + return versions, available_versions |
| 50 | + |
| 51 | + def check_updates(self): |
| 52 | + try: |
| 53 | + with self.console.status( |
| 54 | + "[yellow]Checking for updates...", |
| 55 | + spinner="dots" |
| 56 | + ): |
22 | 57 | result = subprocess.run( |
23 | 58 | [ |
24 | 59 | sys.executable, |
25 | 60 | "-m", |
26 | 61 | "pip", |
27 | 62 | "index", |
28 | 63 | "versions", |
29 | | - PACKAGE_NAME, |
| 64 | + self.package_name, |
| 65 | + "--pre", |
30 | 66 | ], |
31 | 67 | capture_output=True, |
32 | 68 | text=True, |
33 | 69 | check=True, |
34 | 70 | timeout=15, |
35 | 71 | ) |
36 | | - lines = result.stdout.splitlines() |
37 | | - latest_version = lines[-1].split()[-1] if lines else "0.0.0" |
38 | | - |
39 | | - if not latest_version or latest_version <= current_version: |
40 | | - console.print(f"[green] You're up to date: {current_version}") |
41 | | - return False, None, None |
42 | 72 |
|
43 | | - return True, current_version, latest_version |
44 | | - |
45 | | - except subprocess.TimeoutExpired: |
46 | | - console.print( |
47 | | - "[red] Update check timed out. " |
48 | | - "Please check your internet connection." |
| 73 | + versions_info, all_versions = self._parse_pip_output(result.stdout) |
| 74 | + if not all_versions: |
| 75 | + self.console.print("[red] No version information found") |
| 76 | + return None |
| 77 | + |
| 78 | + current_version = versions_info.get('installed') or version(self.package_name) |
| 79 | + stable_versions = [v for v in all_versions if not self._is_prerelease(v)] |
| 80 | + |
| 81 | + latest_stable = stable_versions[0] if stable_versions else None |
| 82 | + latest_prerelease = all_versions[0] if all_versions else None |
| 83 | + |
| 84 | + if not latest_prerelease: |
| 85 | + self.console.print(f"[green] You're up to date: {current_version}") |
| 86 | + return None |
| 87 | + |
| 88 | + return VersionInfo( |
| 89 | + current_version=current_version, |
| 90 | + latest_stable=latest_stable, |
| 91 | + latest_prerelease=latest_prerelease, |
| 92 | + prerelease_is_newer=( |
| 93 | + latest_stable |
| 94 | + and latest_prerelease |
| 95 | + and self._is_prerelease(latest_prerelease) |
| 96 | + and parse_version(latest_prerelease) > parse_version(latest_stable) |
| 97 | + ) |
49 | 98 | ) |
50 | | - return False, None, None |
51 | | - except subprocess.CalledProcessError: |
52 | | - console.print("[red] Failed to check updates") |
53 | | - return False, None, None |
54 | | - except Exception: |
55 | | - console.print("[red] Error checking updates") |
56 | | - return False, None, None |
57 | | - |
58 | 99 |
|
59 | | -def install_update(): |
60 | | - try: |
61 | | - with console.status("[yellow]Installing update...", spinner="point"): |
62 | | - try: |
| 100 | + except subprocess.TimeoutExpired: |
| 101 | + self.console.print("[red] Update check timed out. Please check your internet connection.") |
| 102 | + except subprocess.CalledProcessError: |
| 103 | + self.console.print("[red] Failed to check updates") |
| 104 | + except Exception: |
| 105 | + self.console.print("[red] Error checking updates") |
| 106 | + return None |
| 107 | + |
| 108 | + def install_update(self, install_prerelease=False): |
| 109 | + try: |
| 110 | + with self.console.status("[yellow]Installing update...", spinner="point"): |
| 111 | + cmd = [ |
| 112 | + sys.executable, |
| 113 | + "-m", |
| 114 | + "pip", |
| 115 | + "install", |
| 116 | + "--upgrade", |
| 117 | + self.package_name, |
| 118 | + ] |
| 119 | + if install_prerelease: |
| 120 | + cmd.insert(-1, "--pre") |
| 121 | + |
63 | 122 | subprocess.run( |
64 | | - [ |
65 | | - sys.executable, |
66 | | - "-m", |
67 | | - "pip", |
68 | | - "install", |
69 | | - "--upgrade", |
70 | | - PACKAGE_NAME, |
71 | | - ], |
| 123 | + cmd, |
72 | 124 | capture_output=True, |
73 | 125 | text=True, |
74 | 126 | check=True, |
75 | 127 | timeout=60, |
76 | 128 | ) |
77 | | - console.print("[green] Update successful!") |
| 129 | + self.console.print("[green] Update successful!") |
78 | 130 | return True |
79 | | - except subprocess.TimeoutExpired: |
80 | | - console.print("[red] Installation timed out. Please try again.") |
81 | | - return False |
82 | | - except subprocess.CalledProcessError: |
83 | | - console.print("[red] Installation failed") |
84 | | - return False |
85 | | - except Exception: |
86 | | - console.print("[red] Error during installation") |
87 | | - return False |
| 131 | + except Exception as e: |
| 132 | + self.console.print(f"[red] Installation failed: {str(e)}") |
| 133 | + return False |
88 | 134 |
|
89 | | - |
90 | | -def restart_application(): |
91 | | - console.print("[yellow] Restarting application...") |
92 | | - time.sleep(1) |
93 | | - os.execv(sys.executable, [sys.executable] + sys.argv) |
| 135 | + def restart_application(self): |
| 136 | + self.console.print("[yellow] Restarting application...") |
| 137 | + time.sleep(1) |
| 138 | + os.execv(sys.executable, [sys.executable] + sys.argv) |
94 | 139 |
|
95 | 140 |
|
96 | 141 | def main(): |
| 142 | + manager = VersionManager() |
97 | 143 | try: |
98 | | - has_update, current_version, latest_version = check_for_updates() |
99 | | - if not has_update: |
100 | | - return |
101 | | - |
102 | | - console.print( |
103 | | - f"[yellow] Update available: {current_version} → {latest_version}" |
104 | | - ) |
105 | | - if not get_confirm(" Update now"): |
| 144 | + version_info = manager.check_updates() |
| 145 | + if not version_info: |
106 | 146 | return |
107 | 147 |
|
108 | | - if install_update(): |
109 | | - restart_application() |
| 148 | + if version_info.prerelease_is_newer: |
| 149 | + manager.console.print( |
| 150 | + f"[yellow] Pre-release update available: {version_info.current_version} → {version_info.latest_prerelease}" |
| 151 | + ) |
| 152 | + manager.console.print("[red] Warning: Pre-release versions may be unstable and contain bugs.") |
| 153 | + if not get_confirm(" I understand the risks, update anyway"): |
| 154 | + if version_info.latest_stable and parse_version(version_info.latest_stable) > parse_version(version_info.current_version): |
| 155 | + if get_confirm(f" Update to stable version {version_info.latest_stable}"): |
| 156 | + if manager.install_update(install_prerelease=False): |
| 157 | + manager.restart_application() |
| 158 | + return |
| 159 | + else: |
| 160 | + if manager.install_update(install_prerelease=True): |
| 161 | + manager.restart_application() |
| 162 | + else: |
| 163 | + manager.console.print( |
| 164 | + f"[yellow] Stable update available: {version_info.current_version} → {version_info.latest_stable}" |
| 165 | + ) |
| 166 | + if not get_confirm(" Update now"): |
| 167 | + return |
| 168 | + if manager.install_update(install_prerelease=False): |
| 169 | + manager.restart_application() |
110 | 170 |
|
111 | 171 | except KeyboardInterrupt: |
112 | | - console.print("[yellow] Update cancelled by user.") |
113 | | - except Exception: |
114 | | - console.print("[red] Error during update process") |
| 172 | + manager.console.print("[yellow] Update cancelled by user.") |
| 173 | + except Exception as e: |
| 174 | + manager.console.print(f"[red] Error during update process: {str(e)}") |
0 commit comments