55# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
66# See https://github.com/nexB/scancode-toolkit for support or download.
77# See https://aboutcode.org for more information about nexB OSS projects.
8- ##
8+ # #
99# This code was in part derived from the pip library:
1010# Copyright (c) 2008-2014 The pip developers (see outdated.NOTICE file)
1111#
3939
4040from scancode_config import scancode_cache_dir
4141from scancode_config import __version__ as scancode_version
42+ from scancode_config import __release_date__ as scancode_release_date
4243from scancode import lockfile
4344
45+ """
46+ Utilities to check if the installed version of ScanCode is out of date.
47+ The check is done either:
48+ - locally based on elapsed time of 90 days
49+ - remotely based on an API check for PyPI releases at the Python Software
50+ Foundation PyPI.org. At most once a week
51+
52+ This code is based on a pip module and heavilty modified for use here.
53+ """
54+
4455SELFCHECK_DATE_FMT = "%Y-%m-%dT%H:%M:%SZ"
4556
4657logger = logging .getLogger (__name__ )
@@ -56,7 +67,26 @@ def total_seconds(td):
5667 return val / 10 ** 6
5768
5869
59- class VersionCheckState (object ):
70+ def is_outdated (release_date ):
71+ """
72+ Return True if 90 days have passed since `release_date` datetime object.
73+
74+ For example:
75+
76+ >>> release_date = datetime.datetime(2020, 9, 23)
77+ >>> is_outdated(release_date)
78+ True
79+ >>> release_date = datetime.datetime.utcnow()
80+ >>> is_outdated(release_date)
81+ False
82+ """
83+ current_time = datetime .datetime .utcnow ()
84+ seconds_since_last_check = total_seconds (current_time - release_date )
85+ ninety_days = 90 * 24 * 60 * 60
86+ return seconds_since_last_check > ninety_days
87+
88+
89+ class VersionCheckState :
6090
6191 def __init__ (self ):
6292 self .statefile_path = path .join (
@@ -81,8 +111,48 @@ def save(self, latest_version, current_time):
81111 separators = (',' , ':' ))
82112
83113
84- def check_scancode_version (
114+ def build_outdated_message (installed_version , release_date , newer_version = '' ):
115+ """
116+ Return a message about outdated version for display.
117+ """
118+ rel_date , _ , _ = release_date .isoformat ().partition ('T' )
119+
120+ newer_version = newer_version or ''
121+ newer_version = newer_version .strip ()
122+ if newer_version :
123+ newer_version = f'{ newer_version } '
124+
125+ msg = (
126+ 'WARNING: Outdated ScanCode Toolkit version! '
127+ f'You are using an outdated version of ScanCode Toolkit: { installed_version } '
128+ f'released on: { rel_date } . '
129+ f'A new version { newer_version } is available with important '
130+ f'improvements including bug and security fixes, updated license, '
131+ f'copyright and package detection, and improved scanning accuracy. '
132+ 'Please download and install the latest version of ScanCode. '
133+ 'Visit https://github.com/nexB/scancode-toolkit/releases for details.'
134+ )
135+ return msg
136+
137+
138+ def check_scancode_version_locally (
85139 installed_version = scancode_version ,
140+ release_date = scancode_release_date ,
141+ ):
142+ """
143+ Return a message to display if outdated or None. Work offline, without a
144+ PyPI remote check.
145+ """
146+ if is_outdated (release_date ):
147+ return build_outdated_message (
148+ installed_version = installed_version ,
149+ release_date = release_date ,
150+ )
151+
152+
153+ def check_scancode_version_remotely (
154+ installed_version = scancode_version ,
155+ release_date = scancode_release_date ,
86156 new_version_url = 'https://pypi.org/pypi/scancode-toolkit/json' ,
87157 force = False ,
88158):
@@ -92,10 +162,33 @@ def check_scancode_version(
92162 State is stored in the scancode_cache_dir. If `force` is True, redo a PyPI
93163 remote check.
94164 """
95- installed_version = packaging_version .parse (installed_version )
96- latest_version = None
97- msg = None
165+ newer_version = fetch_newer_version (
166+ installed_version = installed_version ,
167+ new_version_url = new_version_url ,
168+ force = force ,
169+ )
170+ if newer_version :
171+ return build_outdated_message (
172+ installed_version = installed_version ,
173+ release_date = release_date ,
174+ newer_version = newer_version ,
175+ )
98176
177+
178+ def fetch_newer_version (
179+ installed_version = scancode_version ,
180+ new_version_url = 'https://pypi.org/pypi/scancode-toolkit/json' ,
181+ force = False ,
182+ ):
183+ """
184+ Return a version string if there is an updated version of scancode-toolkit
185+ newer than the installed version and available on PyPI. Return None
186+ otherwise.
187+ Limit the frequency of update checks to once per week.
188+ State is stored in the scancode_cache_dir.
189+ If `force` is True, redo a PyPI remote check.
190+ """
191+ installed_version = packaging_version .parse (installed_version )
99192 try :
100193 state = VersionCheckState ()
101194
@@ -117,7 +210,7 @@ def check_scancode_version(
117210 # Refresh the version if we need to or just see if we need to warn
118211 if latest_version is None :
119212 try :
120- latest_version = get_latest_version (new_version_url )
213+ latest_version = fetch_latest_version (new_version_url )
121214 state .save (latest_version , current_time )
122215 except Exception :
123216 # save an empty version to avoid checking more than once a week
@@ -126,18 +219,9 @@ def check_scancode_version(
126219
127220 latest_version = packaging_version .parse (latest_version )
128221
129- outdated_msg = ('WARNING: '
130- 'You are using ScanCode Toolkit version %s, however the newer '
131- 'version %s is available.\n You should download and install the '
132- 'latest version of ScanCode with bug and security fixes and the '
133- 'latest license detection data for accurate scanning.\n '
134- 'Visit https://github.com/nexB/scancode-toolkit/releases for details.'
135- % (installed_version , latest_version )
136- )
137-
138- # Our git version string is not PEP 440 compliant, and thus improperly parsed via
139- # most 3rd party version parsers. We handle this case by pulling out the "base"
140- # release version by split()-ting on "post".
222+ # Our git version string is not PEP 440 compliant, and thus improperly
223+ # parsed via most 3rd party version parsers. We handle this case by
224+ # pulling out the "base" release version by split()-ting on "post".
141225 #
142226 # For example, "3.1.2.post351.850399ba3" becomes "3.1.2"
143227 if isinstance (installed_version , packaging_version .LegacyVersion ):
@@ -148,14 +232,14 @@ def check_scancode_version(
148232 # Determine if our latest_version is older
149233 if (installed_version < latest_version
150234 and installed_version .base_version != latest_version .base_version ):
151- return outdated_msg
235+ return str ( latest_version )
152236
153237 except Exception :
154238 msg = 'There was an error while checking for the latest version of ScanCode'
155239 logger .debug (msg , exc_info = True )
156240
157241
158- def get_latest_version (new_version_url = 'https://pypi.org/pypi/scancode-toolkit/json' ):
242+ def fetch_latest_version (new_version_url = 'https://pypi.org/pypi/scancode-toolkit/json' ):
159243 """
160244 Fetch `new_version_url` and return the latest version of scancode as a
161245 string.
@@ -168,12 +252,12 @@ def get_latest_version(new_version_url='https://pypi.org/pypi/scancode-toolkit/j
168252 try :
169253 response = requests .get (new_version_url , ** requests_args )
170254 except (ConnectionError ) as e :
171- logger .debug ('get_latest_version : Download failed for %(url)r' % locals ())
255+ logger .debug ('fetch_latest_version : Download failed for %(url)r' % locals ())
172256 raise
173257
174258 status = response .status_code
175259 if status != 200 :
176- msg = 'get_latest_version : Download failed for %(url)r with %(status)r' % locals ()
260+ msg = 'fetch_latest_version : Download failed for %(url)r with %(status)r' % locals ()
177261 logger .debug (msg )
178262 raise Exception (msg )
179263
0 commit comments