Skip to content

Commit da1c361

Browse files
committed
Display outdated version WARNING in scan results #2653
This shows up 90 days after a release date. We now also track the release date Signed-off-by: Philippe Ombredanne <[email protected]>
1 parent 737d240 commit da1c361

File tree

5 files changed

+194
-50
lines changed

5 files changed

+194
-50
lines changed

src/scancode/cli.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,14 @@ def scancode(
439439
cliutils.validate_option_dependencies(ctx)
440440
pretty_params = get_pretty_params(ctx, generic_paths=test_mode)
441441

442+
# warn for outdated version and/or check for updates
443+
from scancode.outdated import check_scancode_version_locally
444+
outdated = check_scancode_version_locally()
445+
446+
if not outdated and check_version:
447+
from scancode.outdated import check_scancode_version_remotely
448+
outdated = check_scancode_version_remotely()
449+
442450
# run proper
443451
success, _results = run_scan(
444452
input=input,
@@ -459,17 +467,14 @@ def scancode(
459467
pretty_params=pretty_params,
460468
# results are saved to file, no need to get them back in a cli context
461469
return_results=False,
462-
echo_func=echo_stderr,
470+
echo_func=echo_func,
471+
outdated=outdated,
463472
*args,
464473
**kwargs
465474
)
466475

467-
# check for updates
468-
if check_version:
469-
from scancode.outdated import check_scancode_version
470-
outdated = check_scancode_version()
471-
if not quiet and outdated:
472-
echo_stderr(outdated, fg='yellow')
476+
if not quiet and outdated:
477+
echo_stderr(outdated, fg='yellow')
473478

474479
except click.UsageError as e:
475480
# this will exit
@@ -503,6 +508,7 @@ def run_scan(
503508
test_error_mode=False,
504509
pretty_params=None,
505510
plugin_options=plugin_options,
511+
outdated=None,
506512
*args,
507513
**kwargs
508514
):
@@ -844,6 +850,8 @@ def echo_func(*_args, **_kwargs):
844850
cle.notice = notice
845851
cle.options = pretty_params or {}
846852
cle.extra_data['spdx_license_list_version'] = scancode_config.spdx_license_list_version
853+
if outdated:
854+
cle.extra_data['OUTDATED'] = outdated
847855

848856
# TODO: this is weird: may be the timings should NOT be stored on the
849857
# codebase, since they exist in abstract of it??

src/scancode/outdated.py

Lines changed: 107 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
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
#
@@ -39,8 +39,19 @@
3939

4040
from scancode_config import scancode_cache_dir
4141
from scancode_config import __version__ as scancode_version
42+
from scancode_config import __release_date__ as scancode_release_date
4243
from 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+
4455
SELFCHECK_DATE_FMT = "%Y-%m-%dT%H:%M:%SZ"
4556

4657
logger = 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.\nYou 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

src/scancode_config.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
# See https://github.com/nexB/scancode-toolkit for support or download.
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
9+
10+
import datetime
911
import errno
1012
import os
1113
from os.path import abspath
@@ -75,16 +77,18 @@ def _create_dir(location):
7577

7678
# in case package is not installed or we do not have setutools/pkg_resources
7779
# on hand fall back to this version
78-
__version__ = '21.8.4'
80+
__version__ = '30.0.0'
81+
82+
# used to warn user when the version is out of date
83+
__release_date__ = datetime.datetime(2021, 9, 23)
7984

8085
# See https://github.com/nexB/scancode-toolkit/issues/2653 for more information
8186
# on the data format version
8287
__output_format_version__ = '1.0.0'
8388

84-
#
89+
#
8590
spdx_license_list_version = '3.14'
8691

87-
8892
try:
8993
from pkg_resources import get_distribution, DistributionNotFound
9094
try:

tests/scancode/test_cli.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ def test_can_call_run_scan_as_a_function():
112112
assert not results['headers'][0]['errors']
113113

114114

115+
def test_run_scan_includes_outdated_in_extra():
116+
from scancode.cli import run_scan
117+
test_dir = test_env.get_test_loc('license', copy=True)
118+
rc, results = run_scan(test_dir, outdated='out of date', return_results=True)
119+
assert rc
120+
assert results['headers'][0]['extra_data']['OUTDATED'] == 'out of date'
121+
122+
115123
def test_usage_and_help_return_a_correct_script_name_on_all_platforms():
116124
result = run_scan_click(['--help'])
117125
assert 'Usage: scancode [OPTIONS]' in result.output

0 commit comments

Comments
 (0)