Skip to content

Commit f3d17c7

Browse files
committed
refactored version check to use Pypi for version info
1 parent 2ea5fa5 commit f3d17c7

File tree

5 files changed

+345
-103
lines changed

5 files changed

+345
-103
lines changed

synapseclient/core/version_check.py

Lines changed: 192 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -15,84 +15,174 @@
1515
import json
1616
import re
1717
import sys
18+
import urllib.request
19+
from typing import Optional
1820

1921
import requests
2022

2123
import synapseclient
2224

2325
_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/"
2428

2529

2630
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:
2935
"""
3036
Gets the latest version information from version_url and check against the current version.
3137
Recommends upgrade, if a newer version exists.
3238
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
3754
3855
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.
4057
"""
41-
4258
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
4563

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
4768

48-
current_base_version = _strip_dev_suffix(current_version)
69+
return True
4970

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-
"\nPLEASE UPGRADE YOUR CLIENT\n\nUpgrading your SynapseClient is"
57-
" required. Please upgrade your client by typing:\n pip install"
58-
" --upgrade synapseclient\n\n"
59-
)
60-
raise SystemExit(msg)
6171

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.
6480
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
6695
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-
"\nUPGRADE AVAILABLE\n\nA 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()
87109

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
92111

112+
if _is_current_version_behind(current_version, latest_version, levels):
113+
_write_package_behind_messages(current_version, latest_version)
114+
return False
93115
return True
94116

95117

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+
"\nUPGRADE AVAILABLE\n\nA 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+
96186
def check_for_updates():
97187
"""
98188
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):
148238
return re.sub(r"\.dev\d+", "", version)
149239

150240

151-
def _version_tuple(version, levels=2):
241+
def _version_tuple(version: str, levels: int = 2) -> tuple:
152242
"""
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.
154245
For example:
155246
156247
print(version_tuple('0.5.1.dev1', levels=2))
157248
('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.
158267
"""
159268
v = _strip_dev_suffix(version).split(".")
160269
v = v[0 : min(len(v), levels)]
@@ -163,16 +272,37 @@ def _version_tuple(version, levels=2):
163272
return tuple(v)
164273

165274

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+
"""
167289
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
176306

177307

178308
# If this file is run as a script, print current version
@@ -187,5 +317,5 @@ def _get_version_info(version_url=_VERSION_URL):
187317
print("ok")
188318

189319
print("Check against local copy of version file:")
190-
if version_check(version_url=None):
320+
if version_check(use_local_metadata=True):
191321
print("ok")
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Integration tests for version checking"""
2+
3+
from synapseclient.core.version_check import _get_version_info_from_pypi, version_check
4+
5+
6+
async def test_version_check():
7+
"""Integration checks for version_check"""
8+
# Check current version against pypi version file
9+
version_check()
10+
11+
# Should be higher than current version and return true
12+
assert version_check(current_version="999.999.999")
13+
14+
# Test out of date version
15+
assert not version_check(current_version="0.0.1")
16+
17+
18+
def test_get_version_info_from_pypi():
19+
"""Integration test for _get_version_info_from_pypi"""
20+
assert _get_version_info_from_pypi()

tests/integration/synapseclient/integration_test.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -327,39 +327,6 @@ async def test_downloadFile(schedule_for_cleanup):
327327
assert os.path.exists(filename)
328328

329329

330-
async def test_version_check():
331-
# Check current version against dev-synapsePythonClient version file
332-
version_check(
333-
version_url="http://dev-versions.synapse.sagebase.org/synapsePythonClient"
334-
)
335-
336-
# Should be higher than current version and return true
337-
assert version_check(
338-
current_version="999.999.999",
339-
version_url="http://dev-versions.synapse.sagebase.org/synapsePythonClient",
340-
)
341-
342-
# Test out of date version
343-
assert not version_check(
344-
current_version="0.0.1",
345-
version_url="http://dev-versions.synapse.sagebase.org/synapsePythonClient",
346-
)
347-
348-
# Test blacklisted version
349-
pytest.raises(
350-
SystemExit,
351-
version_check,
352-
current_version="0.0.0",
353-
version_url="http://dev-versions.synapse.sagebase.org/synapsePythonClient",
354-
)
355-
356-
# Test bad URL
357-
assert not version_check(
358-
current_version="999.999.999",
359-
version_url="http://dev-versions.synapse.sagebase.org/bad_filename_doesnt_exist",
360-
)
361-
362-
363330
async def test_provenance(syn, project, schedule_for_cleanup):
364331
# Create a File Entity
365332
fname = utils.make_bogus_data_file()

tests/unit/synapseclient/core/unit_test_utils.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,14 +229,6 @@ def test_extract_filename() -> None:
229229
assert utils.extract_filename(None, "fname.ext") == "fname.ext"
230230

231231

232-
def test_version_check() -> None:
233-
from synapseclient.core.version_check import _version_tuple
234-
235-
assert _version_tuple("0.5.1.dev200", levels=2) == ("0", "5")
236-
assert _version_tuple("0.5.1.dev200", levels=3) == ("0", "5", "1")
237-
assert _version_tuple("1.6", levels=3) == ("1", "6", "0")
238-
239-
240232
def test_normalize_path() -> None:
241233
# tests should pass on reasonable OSes and also on windows
242234

0 commit comments

Comments
 (0)