Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support comparing semver ranges that have top-level || operators. #7919

Merged
merged 2 commits into from Feb 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
104 changes: 64 additions & 40 deletions jupyterlab/commands.py
Expand Up @@ -8,6 +8,7 @@
import errno
import glob
import hashlib
import itertools
import json
import logging
import os
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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=[]):
Expand Down Expand Up @@ -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 = []
Expand Down
18 changes: 17 additions & 1 deletion jupyterlab/tests/test_jupyterlab.py
Expand Up @@ -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
Expand Down Expand Up @@ -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']
Expand Down