Coverage for src/package/updates.py: 100.00%
90 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-08 13:49 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-08 13:49 +0000
1#!/usr/bin/env python3
3# Standard libraries
4from datetime import datetime
5from os import access, environ, W_OK
6from shlex import quote
7from time import localtime, strftime, time
8from typing import Optional, Tuple
10# Modules libraries
11from packaging.version import Version as PackageVersion
13# Components
14from ..features.mise import Mise
15from ..prints.boxes import Boxes
16from ..prints.colors import Colors
17from ..system.platform import Platform
18from .bundle import Bundle
19from .settings import Settings
20from .version import Version
22# Updates class
23class Updates:
25 # Members
26 __enabled: bool
27 __name: str
28 __settings: Settings
30 # Constructor
31 def __init__(
32 self,
33 name: str,
34 settings: Settings,
35 ) -> None:
37 # Initialize members
38 self.__name = name
39 self.__settings = settings
41 # Detect migration
42 self.__migration()
44 # Prepare enabled
45 if not self.__settings.has('updates', 'enabled'):
46 self.__settings.set_bool('updates', 'enabled', True)
48 # Check enabled
49 self.__enabled = bool(self.__settings.get_bool('updates', 'enabled')) and \
50 not environ.get(Bundle.ENV_DEBUG_UPDATES_DISABLE, '')
52 # Migration
53 def __migration(self) -> None:
55 # Acquire versions
56 current_version = Version.get()
57 package_version = self.__settings.get('package', 'version')
58 if not package_version:
59 package_version = '0.0.0'
61 # Refresh package version
62 if not package_version or current_version != package_version:
63 self.__settings.set('package', 'version', current_version)
65 # Checker
66 def check(
67 self,
68 older: bool = False,
69 ) -> bool:
71 # Reference version
72 version = '0.0.0' if older else Version.get()
74 # Fake test updates
75 if Bundle.ENV_DEBUG_UPDATES_FAKE in environ:
76 available = environ[Bundle.ENV_DEBUG_UPDATES_FAKE]
77 if PackageVersion(available) >= PackageVersion(version):
79 # Show updates message
80 release_date = datetime.utcfromtimestamp(Bundle.RELEASE_FIRST_TIMESTAMP)
81 Updates.message(
82 name=self.__name,
83 older=older,
84 available=available,
85 date=release_date,
86 )
87 return True
89 # Check if not offline
90 if not environ.get(Bundle.ENV_DEBUG_UPDATES_OFFLINE, ''):
92 # Modules libraries, pylint: disable=import-outside-toplevel
93 from update_checker import UpdateChecker
95 # Check for updates
96 check = UpdateChecker(bypass_cache=True).check(
97 package_name=self.__name,
98 package_version=version,
99 )
100 if check: # pragma: no cover
102 # Show updates message
103 Updates.message(
104 name=self.__name,
105 older=older,
106 available=check.available_version,
107 date=check.release_date,
108 )
109 return True
111 # Older offline failure
112 if older:
114 # Show offline message
115 Updates.message(
116 name=self.__name,
117 offline=True,
118 )
119 return True
121 # Result
122 return False
124 # Daily
125 @property
126 def daily(self) -> bool:
128 # Acquire updates check last timestamp
129 last = self.__settings.get('updates', 'last_timestamp')
131 # Fake test updates
132 if Bundle.ENV_DEBUG_UPDATES_DAILY in environ:
133 last = None
135 # Handle daily checks
136 current = int(time())
137 if not last or strftime('%Y-%m-%d', localtime(current)) != strftime(
138 '%Y-%m-%d', localtime(int(last))):
139 self.__settings.set('updates', 'last_timestamp', current)
140 return True
142 # Default fallback
143 return False
145 # Enabled
146 @property
147 def enabled(self) -> bool:
148 return self.__enabled
150 # Installer
151 @staticmethod
152 def installer(name: str, available: Optional[str]) -> Tuple[str, str]:
154 # Detect package installer
155 package_install: str = ''
156 if Mise.managed():
157 package_install = ' '.join(['mise', 'use'] + [ # pragma: no cover
158 quote(argument) for argument in Mise.arguments(
159 Mise.config(),
160 f'{Mise.package(name)}@latest',
161 )
162 ])
163 elif Platform.PATH_SEPARATOR + 'pipx' + Platform.PATH_SEPARATOR in Version.path():
164 package_install = 'pipx upgrade' # pragma: no cover
165 else:
166 package_install = 'pip3 install -U' # pragma: no cover
168 # Detect package ownership
169 writable = access(__file__, W_OK)
170 if not package_install.startswith('mise') and (Platform.IS_USER_SUDO
171 or not writable):
172 package_install = 'sudo ' + package_install # pragma: no cover
174 # Prepare package specification
175 package_specification: str = ''
176 if package_install.startswith('mise'):
177 package_specification = f'{Mise.package(name)}@latest' # pragma: no cover
178 elif 'pipx' in package_install:
179 package_specification = f'{name}' # pragma: no cover
180 else:
181 package_specification = f'{name}>={available}' # pragma: no cover
183 # Result
184 return package_install, package_specification
186 # Message, pylint: disable=too-many-arguments
187 @staticmethod
188 def message(
189 name: str,
190 offline: bool = False,
191 older: bool = False,
192 available: Optional[str] = None,
193 date: Optional[datetime] = None,
194 ) -> None:
196 # Modules libraries, pylint: disable=import-outside-toplevel
197 from update_checker import pretty_date
199 # Create message box
200 box = Boxes()
202 # Acquire current version
203 version = Version.get()
205 # Detect package installer
206 package_install, package_specification = Updates.installer(
207 name,
208 available,
209 )
211 # Evaluate same version
212 same = available and available == version
214 # Version message prefix
215 version_outdated = not offline and available and not older and not same
216 version_prefix = f'{Colors.YELLOW_LIGHT}Version: {Colors.BOLD}{name}' \
217 f' {Colors.RED if version_outdated else Colors.GREEN}{version}'
219 # Offline version message
220 if offline:
221 box.add(f'{version_prefix} {Colors.BOLD}not found, network might be down')
223 # Updated version message
224 elif same:
225 assert date is not None
226 box.add(
227 f'{version_prefix} {Colors.BOLD}was released {pretty_date(date)}{Colors.BOLD}!'
228 )
230 # Older version message
231 elif older:
232 assert date is not None
233 box.add(
234 f'{version_prefix} {Colors.BOLD}newer than {Colors.RED}{available}' \
235 f' {Colors.BOLD}from {pretty_date(date)}{Colors.BOLD}!'
236 )
238 # Newer version message
239 else:
240 assert date is not None
241 box.add(
242 f'{version_prefix} {Colors.BOLD}updated {pretty_date(date)}' \
243 f' to {Colors.GREEN}{available}{Colors.BOLD}!'
244 )
246 # Changelog message
247 box.add(
248 f'{Colors.YELLOW_LIGHT}Changelog: {Colors.CYAN}{Bundle.REPOSITORY}/-/releases'
249 )
251 # Update message
252 if available:
253 box.add(
254 f'{Colors.YELLOW_LIGHT}Update: {Colors.BOLD}' \
255 f"Run {Colors.GREEN}{package_install} '{package_specification}'"
256 )
258 # Print message box
259 box.print()