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

1#!/usr/bin/env python3 

2 

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 

9 

10# Modules libraries 

11from packaging.version import Version as PackageVersion 

12 

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 

21 

22# Updates class 

23class Updates: 

24 

25 # Members 

26 __enabled: bool 

27 __name: str 

28 __settings: Settings 

29 

30 # Constructor 

31 def __init__( 

32 self, 

33 name: str, 

34 settings: Settings, 

35 ) -> None: 

36 

37 # Initialize members 

38 self.__name = name 

39 self.__settings = settings 

40 

41 # Detect migration 

42 self.__migration() 

43 

44 # Prepare enabled 

45 if not self.__settings.has('updates', 'enabled'): 

46 self.__settings.set_bool('updates', 'enabled', True) 

47 

48 # Check enabled 

49 self.__enabled = bool(self.__settings.get_bool('updates', 'enabled')) and \ 

50 not environ.get(Bundle.ENV_DEBUG_UPDATES_DISABLE, '') 

51 

52 # Migration 

53 def __migration(self) -> None: 

54 

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' 

60 

61 # Refresh package version 

62 if not package_version or current_version != package_version: 

63 self.__settings.set('package', 'version', current_version) 

64 

65 # Checker 

66 def check( 

67 self, 

68 older: bool = False, 

69 ) -> bool: 

70 

71 # Reference version 

72 version = '0.0.0' if older else Version.get() 

73 

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

78 

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 

88 

89 # Check if not offline 

90 if not environ.get(Bundle.ENV_DEBUG_UPDATES_OFFLINE, ''): 

91 

92 # Modules libraries, pylint: disable=import-outside-toplevel 

93 from update_checker import UpdateChecker 

94 

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 

101 

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 

110 

111 # Older offline failure 

112 if older: 

113 

114 # Show offline message 

115 Updates.message( 

116 name=self.__name, 

117 offline=True, 

118 ) 

119 return True 

120 

121 # Result 

122 return False 

123 

124 # Daily 

125 @property 

126 def daily(self) -> bool: 

127 

128 # Acquire updates check last timestamp 

129 last = self.__settings.get('updates', 'last_timestamp') 

130 

131 # Fake test updates 

132 if Bundle.ENV_DEBUG_UPDATES_DAILY in environ: 

133 last = None 

134 

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 

141 

142 # Default fallback 

143 return False 

144 

145 # Enabled 

146 @property 

147 def enabled(self) -> bool: 

148 return self.__enabled 

149 

150 # Installer 

151 @staticmethod 

152 def installer(name: str, available: Optional[str]) -> Tuple[str, str]: 

153 

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 

167 

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 

173 

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 

182 

183 # Result 

184 return package_install, package_specification 

185 

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: 

195 

196 # Modules libraries, pylint: disable=import-outside-toplevel 

197 from update_checker import pretty_date 

198 

199 # Create message box 

200 box = Boxes() 

201 

202 # Acquire current version 

203 version = Version.get() 

204 

205 # Detect package installer 

206 package_install, package_specification = Updates.installer( 

207 name, 

208 available, 

209 ) 

210 

211 # Evaluate same version 

212 same = available and available == version 

213 

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}' 

218 

219 # Offline version message 

220 if offline: 

221 box.add(f'{version_prefix} {Colors.BOLD}not found, network might be down') 

222 

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 ) 

229 

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 ) 

237 

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 ) 

245 

246 # Changelog message 

247 box.add( 

248 f'{Colors.YELLOW_LIGHT}Changelog: {Colors.CYAN}{Bundle.REPOSITORY}/-/releases' 

249 ) 

250 

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 ) 

257 

258 # Print message box 

259 box.print()