diff --git a/jupyterlab/commands.py b/jupyterlab/commands.py index b294f9fe5569..3819da821180 100644 --- a/jupyterlab/commands.py +++ b/jupyterlab/commands.py @@ -8,6 +8,7 @@ import errno import glob import hashlib +import itertools import json import logging import os @@ -94,7 +95,6 @@ def wait(self): cache = [] proc = self.proc kill_event = self._kill_event - import itertools spinner = itertools.cycle(['-', '\\', '|', '/']) while proc.poll() is None: sys.stdout.write(next(spinner)) # write the next character @@ -1965,51 +1965,75 @@ def _compare_ranges(spec1, spec2, drop_prerelease1=False, drop_prerelease2=False if not r1.range or not r2.range: return - x1 = r1.set[0][0].semver - x2 = r1.set[0][-1].semver - y1 = r2.set[0][0].semver - y2 = r2.set[0][-1].semver + # Set return_value to a sentinel value + return_value = False - if x1.prerelease and drop_prerelease1: - x1 = x1.inc('patch') + # r1.set may be a list of ranges if the range involved an ||, so we need to test for overlaps between each pair. + for r1set, r2set in itertools.product(r1.set, r2.set): + x1 = r1set[0].semver + x2 = r1set[-1].semver + y1 = r2set[0].semver + y2 = r2set[-1].semver - if y1.prerelease and drop_prerelease2: - y1 = y1.inc('patch') + if x1.prerelease and drop_prerelease1: + x1 = x1.inc('patch') - o1 = r1.set[0][0].operator - o2 = r2.set[0][0].operator + if y1.prerelease and drop_prerelease2: + y1 = y1.inc('patch') - # We do not handle (<) specifiers. - if (o1.startswith('<') or o2.startswith('<')): - return + o1 = r1set[0].operator + o2 = r2set[0].operator - # Handle single value specifiers. - lx = lte if x1 == x2 else lt - ly = lte if y1 == y2 else lt - gx = gte if x1 == x2 else gt - gy = gte if x1 == x2 else gt + # We do not handle (<) specifiers. + if (o1.startswith('<') or o2.startswith('<')): + continue - # Handle unbounded (>) specifiers. - def noop(x, y, z): - return True + # Handle single value specifiers. + lx = lte if x1 == x2 else lt + ly = lte if y1 == y2 else lt + gx = gte if x1 == x2 else gt + gy = gte if x1 == x2 else gt - if x1 == x2 and o1.startswith('>'): - lx = noop - if y1 == y2 and o2.startswith('>'): - ly = noop - - # Check for overlap. - if (gte(x1, y1, True) and ly(x1, y2, True) or - gy(x2, y1, True) and ly(x2, y2, True) or - gte(y1, x1, True) and lx(y1, x2, True) or - gx(y2, x1, True) and lx(y2, x2, True) - ): - return 0 - if gte(y1, x2, True): - return 1 - if gte(x1, y2, True): - return -1 - raise AssertionError('Unexpected case comparing version ranges') + # Handle unbounded (>) specifiers. + def noop(x, y, z): + return True + + if x1 == x2 and o1.startswith('>'): + lx = noop + if y1 == y2 and o2.startswith('>'): + ly = noop + + # Check for overlap. + if (gte(x1, y1, True) and ly(x1, y2, True) or + gy(x2, y1, True) and ly(x2, y2, True) or + gte(y1, x1, True) and lx(y1, x2, True) or + gx(y2, x1, True) and lx(y2, x2, True) + ): + # if we ever find an overlap, we can return immediately + return 0 + + if gte(y1, x2, True): + if return_value is False: + # We can possibly return 1 + return_value = 1 + elif return_value == -1: + # conflicting information, so we must return None + return_value = None + continue + + if gte(x1, y2, True): + if return_value is False: + return_value = -1 + elif return_value == 1: + # conflicting information, so we must return None + return_value = None + continue + + raise AssertionError('Unexpected case comparing version ranges') + + if return_value is False: + return_value = None + return return_value def _is_disabled(name, disabled=[]): @@ -2051,7 +2075,7 @@ def _format_compatibility_errors(name, version, errors): def _log_multiple_compat_errors(logger, errors_map): - """Log compatability errors for multiple extensions at once""" + """Log compatibility errors for multiple extensions at once""" outdated = [] others = [] diff --git a/jupyterlab/tests/test_jupyterlab.py b/jupyterlab/tests/test_jupyterlab.py index a4fd6002b35f..0c3cbafb76cb 100644 --- a/jupyterlab/tests/test_jupyterlab.py +++ b/jupyterlab/tests/test_jupyterlab.py @@ -28,7 +28,7 @@ install_extension, uninstall_extension, list_extensions, build, link_package, unlink_package, build_check, disable_extension, enable_extension, get_app_info, - check_extension, _test_overlap, update_extension, + check_extension, _test_overlap, _compare_ranges, update_extension, AppOptions ) from jupyterlab.coreconfig import CoreConfig, _get_default_core_data @@ -613,6 +613,22 @@ def test_compatibility(self): assert _test_overlap('*', '0.6') is None assert _test_overlap('<0.6', '0.1') is None + assert _test_overlap('^1 || ^2', '^1') + assert _test_overlap('^1 || ^2', '^2') + assert _test_overlap('^1', '^1 || ^2') + assert _test_overlap('^2', '^1 || ^2') + assert _test_overlap('^1 || ^2', '^2 || ^3') + assert not _test_overlap('^1 || ^2', '^3 || ^4') + assert not _test_overlap('^2', '^1 || ^3') + + def test_compare_ranges(self): + assert _compare_ranges('^1 || ^2', '^1') == 0 + assert _compare_ranges('^1 || ^2', '^2 || ^3') == 0 + assert _compare_ranges('^1 || ^2', '^3 || ^4') == 1 + assert _compare_ranges('^3 || ^4', '^1 || ^2') == -1 + assert _compare_ranges('^2 || ^3', '^1 || ^4') is None + + def test_install_compatible(self): core_data = _get_default_core_data() current_app_dep = core_data['dependencies']['@jupyterlab/application']