15
15
import json
16
16
import re
17
17
import sys
18
+ import urllib .request
19
+ from typing import Optional
18
20
19
21
import requests
20
22
21
23
import synapseclient
22
24
23
25
_VERSION_URL = "https://raw.githubusercontent.com/Sage-Bionetworks/synapsePythonClient/master/synapseclient/synapsePythonClient" # noqa
26
+ _PYPI_JSON_URL = "https://pypi.org/pypi/synapseclient/json"
27
+ _RELEASE_NOTES_URL = "https://python-docs.synapse.org/news/"
24
28
25
29
26
30
def version_check (
27
- current_version = None , version_url = _VERSION_URL , check_for_point_releases = False
28
- ):
31
+ current_version : Optional [str ] = None ,
32
+ check_for_point_releases : bool = False ,
33
+ use_local_metadata : bool = False ,
34
+ ) -> bool :
29
35
"""
30
36
Gets the latest version information from version_url and check against the current version.
31
37
Recommends upgrade, if a newer version exists.
32
38
33
- Arguments:
34
- current_version: The current version of the entity
35
- version_url: The URL of the entity version
36
- check_for_point_releases: Bool.
39
+ This wraps the _version_check function in a try except block.
40
+ The purpose of this is so that no exception caught running the version check stop the client from running.
41
+
42
+ Args:
43
+ current_version (Optional[str], optional): The current version of the package.
44
+ Defaults to None.
45
+ This argument is mainly used for testing.
46
+ check_for_point_releases (bool, optional):
47
+ Defaults to False.
48
+ If True, The whole package versions will be compared (ie. 1.0.0)
49
+ If False, only the major and minor package version will be compared (ie. 1.0)
50
+ use_local_metadata (bool, optional):
51
+ Defaults to False.
52
+ If True, importlib.resources will be used to get the latest version fo the package
53
+ If False, the latest version fo the package will be taken from Pypi
37
54
38
55
Returns:
39
- True if current version is the latest release (or higher) version, otherwise False.
56
+ bool: True if current version is the latest release (or higher) version, otherwise False.
40
57
"""
41
-
42
58
try :
43
- if not current_version :
44
- current_version = synapseclient .__version__
59
+ if not _version_check (
60
+ current_version , check_for_point_releases , use_local_metadata
61
+ ):
62
+ return False
45
63
46
- version_info = _get_version_info (version_url )
64
+ except Exception as e :
65
+ # Don't prevent the client from running if something goes wrong
66
+ sys .stderr .write (f"Exception in version check: { str (e )} \n " )
67
+ return False
47
68
48
- current_base_version = _strip_dev_suffix ( current_version )
69
+ return True
49
70
50
- # Check blacklist
51
- if (
52
- current_base_version in version_info ["blacklist" ]
53
- or current_version in version_info ["blacklist" ]
54
- ):
55
- msg = (
56
- "\n PLEASE UPGRADE YOUR CLIENT\n \n Upgrading your SynapseClient is"
57
- " required. Please upgrade your client by typing:\n pip install"
58
- " --upgrade synapseclient\n \n "
59
- )
60
- raise SystemExit (msg )
61
71
62
- if "message" in version_info :
63
- sys .stderr .write (version_info ["message" ] + "\n " )
72
+ def _version_check (
73
+ current_version : Optional [str ] = None ,
74
+ check_for_point_releases : bool = False ,
75
+ use_local_metadata : bool = False ,
76
+ ) -> bool :
77
+ """
78
+ Gets the latest version information from version_url and check against the current version.
79
+ Recommends upgrade, if a newer version exists.
64
80
65
- levels = 3 if check_for_point_releases else 2
81
+ This has been split of from the version_check function to make testing easier.
82
+
83
+ Args:
84
+ current_version (Optional[str], optional): The current version of the package.
85
+ Defaults to None.
86
+ This argument is mainly used for testing.
87
+ check_for_point_releases (bool, optional):
88
+ Defaults to False.
89
+ If True, The whole package versions will be compared (ie. 1.0.0)
90
+ If False, only the major and minor package version will be compared (ie. 1.0)
91
+ use_local_metadata (bool, optional):
92
+ Defaults to False.
93
+ If True, importlib.resources will be used to get the latest version fo the package
94
+ If False, the latest version fo the package will be taken from Pypi
66
95
67
- # Compare with latest version
68
- if _version_tuple (current_version , levels = levels ) < _version_tuple (
69
- version_info ["latestVersion" ], levels = levels
70
- ):
71
- sys .stderr .write (
72
- "\n UPGRADE AVAILABLE\n \n A more recent version of the Synapse Client"
73
- " (%s) is available. Your version (%s) can be upgraded by typing:\n "
74
- " pip install --upgrade synapseclient\n \n "
75
- % (
76
- version_info ["latestVersion" ],
77
- current_version ,
78
- )
79
- )
80
- if "releaseNotes" in version_info :
81
- sys .stderr .write (
82
- "Python Synapse Client version %s release notes\n \n "
83
- % version_info ["latestVersion" ]
84
- )
85
- sys .stderr .write (version_info ["releaseNotes" ] + "\n \n " )
86
- return False
96
+ Returns:
97
+ bool: True if current version is the latest release (or higher) version, otherwise False.
98
+ """
99
+ if not current_version :
100
+ current_version = synapseclient .__version__
101
+ assert isinstance (current_version , str )
102
+
103
+ if use_local_metadata :
104
+ metadata = _get_local_package_metadata ()
105
+ latest_version = metadata ["latestVersion" ]
106
+ assert isinstance (latest_version , str )
107
+ else :
108
+ latest_version = _get_version_info_from_pypi ()
87
109
88
- except Exception as e :
89
- # Don't prevent the client from running if something goes wrong
90
- sys .stderr .write ("Exception in version check: %s\n " % (str (e ),))
91
- return False
110
+ levels = 3 if check_for_point_releases else 2
92
111
112
+ if _is_current_version_behind (current_version , latest_version , levels ):
113
+ _write_package_behind_messages (current_version , latest_version )
114
+ return False
93
115
return True
94
116
95
117
118
+ def _get_version_info_from_pypi () -> str :
119
+ """Gets the current release version from PyPi
120
+
121
+ Returns:
122
+ str: The current release version
123
+ """
124
+ with urllib .request .urlopen (_PYPI_JSON_URL ) as url :
125
+ data = json .load (url )
126
+ version = data ["info" ]["version" ]
127
+ assert isinstance (version , str )
128
+ return version
129
+
130
+
131
+ def _is_current_version_behind (
132
+ current_version : str , latest_version : str , levels : int
133
+ ) -> bool :
134
+ """
135
+ Tests if the current version of the package is behind the latest version.
136
+
137
+ Args:
138
+ current_version (str): The current version of a package
139
+ latest_version (str): The latest version of a package
140
+ levels (int): The levels of the packages to check. For example:
141
+ level 1: major versions
142
+ level 2: minor versions
143
+ level 3: patch versions
144
+
145
+ Returns:
146
+ bool: True if current version of package is up to date
147
+ """
148
+ current_version_str_tuple = _version_tuple (current_version , levels = levels )
149
+ latest_version_str_tuple = _version_tuple (latest_version , levels = levels )
150
+
151
+ # strings are converted to ints because comparisons of versions of different magnitudes
152
+ # don't work as strings
153
+ # for example 10 > 2, but "10" < "2"
154
+ current_version_int_tuple = tuple (
155
+ int (version_level ) for version_level in current_version_str_tuple
156
+ )
157
+ latest_version_int_tuple = tuple (
158
+ int (version_level ) for version_level in latest_version_str_tuple
159
+ )
160
+
161
+ return current_version_int_tuple < latest_version_int_tuple
162
+
163
+
164
+ def _write_package_behind_messages (
165
+ current_version : str ,
166
+ latest_version : str ,
167
+ ) -> None :
168
+ """_summary_
169
+
170
+ Args:
171
+ current_version (str): The current version of a package
172
+ latest_version (str): The latest version of a package
173
+ """
174
+ sys .stderr .write (
175
+ "\n UPGRADE AVAILABLE\n \n A more recent version of the Synapse Client"
176
+ f" ({ latest_version } ) is available."
177
+ f" Your version ({ current_version } ) can be upgraded by typing:\n "
178
+ " pip install --upgrade synapseclient\n \n "
179
+ )
180
+ sys .stderr .write (
181
+ f"Python Synapse Client version { latest_version } " " release notes\n \n "
182
+ )
183
+ sys .stderr .write (f"{ _RELEASE_NOTES_URL } \n \n " )
184
+
185
+
96
186
def check_for_updates ():
97
187
"""
98
188
Check for the existence of newer versions of the client, reporting both current release version and development
@@ -148,13 +238,32 @@ def _strip_dev_suffix(version):
148
238
return re .sub (r"\.dev\d+" , "" , version )
149
239
150
240
151
- def _version_tuple (version , levels = 2 ) :
241
+ def _version_tuple (version : str , levels : int = 2 ) -> tuple :
152
242
"""
153
- Take a version number as a string delimited by periods and return a tuple with the desired number of levels.
243
+ Take a version number as a string delimited by periods and return a tuple with
244
+ the desired number of levels.
154
245
For example:
155
246
156
247
print(version_tuple('0.5.1.dev1', levels=2))
157
248
('0', '5')
249
+
250
+ First the version string is split into version levels.
251
+ If the number of levels is greater than the levels argument(x),
252
+ only x levels are returned.
253
+ If the number of levels is lesser than the levels argument(x),
254
+ "0" strings are used to pad out the return value.
255
+
256
+ Args:
257
+ version (str): A package version in string form such as "1.0.0"
258
+ levels (int, optional):
259
+ Defaults to 2.
260
+ The number of levels deep in the package version to return. "1.0.0", for example:
261
+ levels=1: only the major version ("1")
262
+ levels=2: the major and minor version ("1", "0")
263
+ levels=2: the major, minor, and patch version ("1", "0", "0")
264
+
265
+ Returns:
266
+ Tuple: A tuple of strings where the length is equal to the levels argument.
158
267
"""
159
268
v = _strip_dev_suffix (version ).split ("." )
160
269
v = v [0 : min (len (v ), levels )]
@@ -163,16 +272,37 @@ def _version_tuple(version, levels=2):
163
272
return tuple (v )
164
273
165
274
166
- def _get_version_info (version_url = _VERSION_URL ):
275
+ def _get_version_info (version_url : Optional [str ] = _VERSION_URL ) -> dict :
276
+ """
277
+ Gets version info from the version_url argument, or locally
278
+ By default this is the Github for the python client
279
+ If the version_url argument is None the version info will be obtained locally.
280
+
281
+ Args:
282
+ version_url (str, optional):
283
+ Defaults to _VERSION_URL.
284
+ The url to get version info from
285
+
286
+ Returns:
287
+ dict: This will have various fields relating the version of the client
288
+ """
167
289
if version_url is None :
168
- ref = importlib .resources .files ("synapseclient" ).joinpath ("synapsePythonClient" )
169
- with ref .open ("r" ) as fp :
170
- pkg_metadata = json .loads (fp .read ())
171
- return pkg_metadata
172
- else :
173
- headers = {"Accept" : "application/json; charset=UTF-8" }
174
- headers .update (synapseclient .USER_AGENT )
175
- return requests .get (version_url , headers = headers ).json ()
290
+ return _get_local_package_metadata ()
291
+ headers = {"Accept" : "application/json; charset=UTF-8" }
292
+ headers .update (synapseclient .USER_AGENT )
293
+ return requests .get (version_url , headers = headers ).json ()
294
+
295
+
296
+ def _get_local_package_metadata () -> dict :
297
+ """Gets version info locally, using importlib.resources
298
+
299
+ Returns:
300
+ dict: This will have various fields relating the version of the client
301
+ """
302
+ ref = importlib .resources .files ("synapseclient" ).joinpath ("synapsePythonClient" )
303
+ with ref .open ("r" ) as fp :
304
+ pkg_metadata = json .loads (fp .read ())
305
+ return pkg_metadata
176
306
177
307
178
308
# If this file is run as a script, print current version
@@ -187,5 +317,5 @@ def _get_version_info(version_url=_VERSION_URL):
187
317
print ("ok" )
188
318
189
319
print ("Check against local copy of version file:" )
190
- if version_check (version_url = None ):
320
+ if version_check (use_local_metadata = True ):
191
321
print ("ok" )
0 commit comments