Coverage for src/package/updates.py: 100.00%

85 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-10-18 15:02 +0000

1#!/usr/bin/env python3 

2 

3# Standard libraries 

4from datetime import datetime 

5from os import access, environ, W_OK 

6from time import localtime, strftime, time 

7from typing import Optional 

8 

9# Modules libraries 

10from packaging.version import Version as PackageVersion 

11 

12# Components 

13from ..prints.boxes import Boxes 

14from ..prints.colors import Colors 

15from ..system.platform import Platform 

16from .bundle import Bundle 

17from .settings import Settings 

18from .version import Version 

19 

20# Updates class 

21class Updates: 

22 

23 # Members 

24 __enabled: bool 

25 __name: str 

26 __settings: Settings 

27 

28 # Constructor 

29 def __init__( 

30 self, 

31 name: str, 

32 settings: Settings, 

33 ) -> None: 

34 

35 # Initialize members 

36 self.__name = name 

37 self.__settings = settings 

38 

39 # Detect migration 

40 self.__migration() 

41 

42 # Prepare enabled 

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

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

45 

46 # Check enabled 

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

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

49 

50 # Migration 

51 def __migration(self) -> None: 

52 

53 # Acquire versions 

54 current_version = Version.get() 

55 package_version = self.__settings.get('package', 'version') 

56 if not package_version: 

57 package_version = '0.0.0' 

58 

59 # Refresh package version 

60 if not package_version or current_version != package_version: 

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

62 

63 # Checker 

64 def check( 

65 self, 

66 older: bool = False, 

67 ) -> bool: 

68 

69 # Reference version 

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

71 

72 # Fake test updates 

73 if Bundle.ENV_DEBUG_UPDATES_FAKE in environ: 

74 available = environ[Bundle.ENV_DEBUG_UPDATES_FAKE] 

75 if PackageVersion(available) >= PackageVersion(version): 

76 

77 # Show updates message 

78 release_date = datetime.utcfromtimestamp(Bundle.RELEASE_FIRST_TIMESTAMP) 

79 Updates.message( 

80 name=self.__name, 

81 older=older, 

82 available=available, 

83 date=release_date, 

84 ) 

85 return True 

86 

87 # Check if not offline 

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

89 

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

91 from update_checker import UpdateChecker 

92 

93 # Check for updates 

94 check = UpdateChecker(bypass_cache=True).check( 

95 package_name=self.__name, 

96 package_version=version, 

97 ) 

98 if check: # pragma: no cover 

99 

100 # Show updates message 

101 Updates.message( 

102 name=self.__name, 

103 older=older, 

104 available=check.available_version, 

105 date=check.release_date, 

106 ) 

107 return True 

108 

109 # Older offline failure 

110 if older: 

111 

112 # Show offline message 

113 Updates.message( 

114 name=self.__name, 

115 offline=True, 

116 ) 

117 return True 

118 

119 # Result 

120 return False 

121 

122 # Daily 

123 @property 

124 def daily(self) -> bool: 

125 

126 # Acquire updates check last timestamp 

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

128 

129 # Fake test updates 

130 if Bundle.ENV_DEBUG_UPDATES_DAILY in environ: 

131 last = None 

132 

133 # Handle daily checks 

134 current = int(time()) 

135 if not last or strftime('%Y-%m-%d', localtime(current)) != strftime( 

136 '%Y-%m-%d', localtime(int(last))): 

137 self.__settings.set('updates', 'last_timestamp', current) 

138 return True 

139 

140 # Default fallback 

141 return False 

142 

143 # Enabled 

144 @property 

145 def enabled(self) -> bool: 

146 return self.__enabled 

147 

148 # Message, pylint: disable=too-many-arguments,too-many-positional-arguments 

149 @staticmethod 

150 def message( 

151 name: str, 

152 offline: bool = False, 

153 older: bool = False, 

154 available: Optional[str] = None, 

155 date: Optional[datetime] = None, 

156 recommended: Optional[str] = None, 

157 ) -> None: 

158 

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

160 from update_checker import pretty_date 

161 

162 # Create message box 

163 box = Boxes() 

164 

165 # Acquire current version 

166 version = Version.get() 

167 

168 # Detect package installer 

169 package_install: str = '' 

170 if Platform.PATH_SEPARATOR + 'pipx' + Platform.PATH_SEPARATOR in Version.path(): 

171 package_install = 'pipx upgrade' # pragma: no cover 

172 else: 

173 package_install = 'pip3 install -U' # pragma: no cover 

174 

175 # Detect package ownership 

176 writable = access(__file__, W_OK) 

177 if Platform.IS_USER_SUDO or not writable: 

178 package_install = 'sudo ' + package_install # pragma: no cover 

179 

180 # Prepare package specification 

181 package_specification: str = '' 

182 if 'pipx' in package_install: 

183 package_specification = f'{name}' # pragma: no cover 

184 else: 

185 package_specification = f'{name}>={available}' # pragma: no cover 

186 

187 # Evaluate same version 

188 same = available and available == version 

189 

190 # Version message prefix 

191 version_outdated = not offline and available and not older and not same 

192 version_prefix = f'{Colors.YELLOW_LIGHT}Version: {Colors.BOLD}{name}' \ 

193 f' {Colors.RED if version_outdated else Colors.GREEN}{version}' 

194 

195 # Recommended version message 

196 if recommended: 

197 available = recommended 

198 box.add(f'{version_prefix} {Colors.BOLD}older than recommended version ' 

199 f'{Colors.RED}{recommended}{Colors.BOLD}') 

200 

201 # Offline version message 

202 elif offline: 

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

204 

205 # Updated version message 

206 elif same: 

207 assert date is not None 

208 box.add( 

209 f'{version_prefix} {Colors.BOLD}was released {pretty_date(date)}{Colors.BOLD}!' 

210 ) 

211 

212 # Older version message 

213 elif older: 

214 assert date is not None 

215 box.add( 

216 f'{version_prefix} {Colors.BOLD}newer than {Colors.RED}{available}' \ 

217 f' {Colors.BOLD}from {pretty_date(date)}{Colors.BOLD}!' 

218 ) 

219 

220 # Newer version message 

221 else: 

222 assert date is not None 

223 box.add( 

224 f'{version_prefix} {Colors.BOLD}updated {pretty_date(date)}' \ 

225 f' to {Colors.GREEN}{available}{Colors.BOLD}!' 

226 ) 

227 

228 # Changelog message 

229 box.add( 

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

231 ) 

232 

233 # Update message 

234 if available: 

235 box.add( 

236 f'{Colors.YELLOW_LIGHT}Update: {Colors.BOLD}' \ 

237 f"Run {Colors.GREEN}{package_install} '{package_specification}'" 

238 ) 

239 

240 # Print message box 

241 box.print()