Skip to content

Commit 21f0dda

Browse files
committed
refactor(updater): add VersionManager class with prerelease support
- Refactored update logic into a `VersionManager` class for better organization and reuse. - Added support for detecting and optionally installing pre-release versions. - Improved version parsing using `packaging.version`. - Enhanced error handling and pip output parsing. - Maintained compatibility with existing CLI behavior including optional user confirmation.
1 parent 57d78fd commit 21f0dda

File tree

1 file changed

+132
-72
lines changed

1 file changed

+132
-72
lines changed

bugscanx/modules/others/script_updater.py

Lines changed: 132 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,113 +2,173 @@
22
import subprocess
33
import sys
44
import time
5+
from dataclasses import dataclass
56
from importlib.metadata import version
7+
from packaging.version import Version, parse as parse_version
68

79
from rich.console import Console
8-
910
from bugscanx.utils.common import get_confirm
1011

11-
PACKAGE_NAME = "bugscan-x"
12-
console = Console()
13-
1412

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+
):
2257
result = subprocess.run(
2358
[
2459
sys.executable,
2560
"-m",
2661
"pip",
2762
"index",
2863
"versions",
29-
PACKAGE_NAME,
64+
self.package_name,
65+
"--pre",
3066
],
3167
capture_output=True,
3268
text=True,
3369
check=True,
3470
timeout=15,
3571
)
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
4272

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+
)
4998
)
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-
5899

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+
63122
subprocess.run(
64-
[
65-
sys.executable,
66-
"-m",
67-
"pip",
68-
"install",
69-
"--upgrade",
70-
PACKAGE_NAME,
71-
],
123+
cmd,
72124
capture_output=True,
73125
text=True,
74126
check=True,
75127
timeout=60,
76128
)
77-
console.print("[green] Update successful!")
129+
self.console.print("[green] Update successful!")
78130
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
88134

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)
94139

95140

96141
def main():
142+
manager = VersionManager()
97143
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:
106146
return
107147

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()
110170

111171
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

Comments
 (0)