From c833b97b4ede18ccf2e5b49e6a6b27b1abed2e25 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 24 Jul 2024 19:55:59 +0530 Subject: [PATCH] Refactor VersionRange normalization without Span Signed-off-by: Keshav Priyadarshi --- src/univers/normalized_range.py | 136 --------- src/univers/span.py | 494 -------------------------------- src/univers/spans.py.ABOUT | 16 -- src/univers/spans.py.LICENSE | 26 -- src/univers/version_range.py | 31 ++ tests/test_normalized_range.py | 82 ------ tests/test_version_range.py | 29 +- 7 files changed, 59 insertions(+), 755 deletions(-) delete mode 100644 src/univers/normalized_range.py delete mode 100644 src/univers/span.py delete mode 100644 src/univers/spans.py.ABOUT delete mode 100644 src/univers/spans.py.LICENSE delete mode 100644 tests/test_normalized_range.py diff --git a/src/univers/normalized_range.py b/src/univers/normalized_range.py deleted file mode 100644 index 8367943f..00000000 --- a/src/univers/normalized_range.py +++ /dev/null @@ -1,136 +0,0 @@ -# -# Copyright (c) nexB Inc. and others. -# SPDX-License-Identifier: Apache-2.0 -# -# Visit https://aboutcode.org and https://github.com/nexB/univers for support and download. - - -from typing import List - -import attr - -from univers.span import Span -from univers.version_constraint import VersionConstraint -from univers.version_range import RANGE_CLASS_BY_SCHEMES -from univers.version_range import VersionRange - - -@attr.s(frozen=True, order=False, eq=True, hash=True) -class NormalizedVersionRange: - """ - A normalized_range is a summation of the largest contiguous version ranges. - - For example, for an npm package with the version range "vers:npm/<=2.0.0|>=3.0.0|<3.1.0|4.0.0" - and available package versions ["1.0.0", "2.0.0", "3.0.0", "3.1.0", "4.0.0"], the normalized range - would be "vers:npm/>=1.0.0|<=3.0.0|4.0.0". - """ - - normalized_range = attr.ib(type=VersionRange, default=None) - - def __str__(self): - return str(self.normalized_range) - - @classmethod - def from_vers(cls, vers_range: VersionRange, all_versions: List): - """ - Return NormalizedVersionRange computed from vers_range and all the available version of package. - """ - version_class = vers_range.version_class - versions = sorted([version_class(i.lstrip("vV")) for i in all_versions]) - - bounded_span = None - total_span = None - for constraint in vers_range.constraints: - local_span = get_region(constraint=constraint, versions=versions) - - if bounded_span and constraint.comparator in ("<", "<="): - local_span = bounded_span.intersection(local_span) - elif constraint.comparator in (">", ">="): - bounded_span = local_span - continue - - total_span = local_span if not total_span else total_span.union(local_span) - bounded_span = None - - # If '<' or '<=' is the last constraint. - if bounded_span: - total_span = bounded_span if not total_span else total_span.union(bounded_span) - - normalized_version_range = get_version_range_from_span( - span=total_span, purl_type=vers_range.scheme, versions=versions - ) - return cls(normalized_range=normalized_version_range) - - -def get_version_range_from_span(span: Span, purl_type: str, versions: List): - """ - Return VersionRange computed from the span and all the versions available for package. - - For example:: - >>> from univers.versions import SemverVersion as SV - >>> versions = [SV("1.0"), SV("1.1"), SV("1.2"), SV("1.3")] - >>> span = Span(0,1).union(Span(3)) - >>> vr = get_version_range_from_span(span, "npm", versions) - >>> assert str(vr) == "vers:npm/>=1.0.0|<=1.1.0|1.3.0" - """ - - version_range_class = RANGE_CLASS_BY_SCHEMES[purl_type] - version_constraints = [] - spans = span.subspans() - for subspan in spans: - lower_bound = versions[subspan.start] - upper_bound = versions[subspan.end] - if lower_bound == upper_bound: - version_constraints.append( - VersionConstraint( - version=lower_bound, - ) - ) - continue - version_constraints.append(VersionConstraint(comparator=">=", version=lower_bound)) - version_constraints.append( - VersionConstraint( - comparator="<=", - version=upper_bound, - ) - ) - - return version_range_class(constraints=version_constraints) - - -def get_region(constraint: VersionConstraint, versions: List): - """ - Return a Span representing the region covered by the constraint on - the given universe of versions. - - For example:: - >>> from univers.versions import SemverVersion as SV - >>> versions = [SV("1.0"), SV("1.1"), SV("1.2"), SV("1.3")] - >>> constraint = VersionConstraint(comparator="<", version=SV("1.2")) - >>> get_region(constraint, versions) - Span(0, 1) - """ - - try: - index = 0 - if str(constraint.version) != "0": - index = versions.index(constraint.version) - except ValueError as err: - err.args = (f"'{constraint.version}' doesn't exist in the versions list.",) - raise - - last_index = len(versions) - 1 - comparator = constraint.comparator - - if comparator == "<": - return Span(0, index - 1) - if comparator == ">": - return Span(index + 1, last_index) - if comparator == ">=": - return Span(index, last_index) - if comparator == "<=": - return Span(0, index) - if comparator == "=": - return Span(index) - if comparator == "!=": - return Span(0, last_index).difference(Span(index)) diff --git a/src/univers/span.py b/src/univers/span.py deleted file mode 100644 index 022fd88b..00000000 --- a/src/univers/span.py +++ /dev/null @@ -1,494 +0,0 @@ -# -# Copyright (c) 2010 Matt Chaput. All rights reserved. -# Modifications by nexB Copyright (c) nexB Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY MATT CHAPUT ``AS IS'' AND ANY EXPRESS OR IMPLIED -# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -# EVENT SHALL MATT CHAPUT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of Matt Chaput. - -from collections.abc import Set -from itertools import count -from itertools import groupby - -try: - from intbitset import intbitset - - spanset = intbitset -except: - spanset = set - - -""" -Ranges and intervals of integers using bitmaps. -Used as a compact and faster data structure for token and position sets. -""" - - -class Span(Set): - """ - Represent ranges of integers (such as tokens positions) as a set of integers. - A Span is hashable and not meant to be modified once created, like a frozenset. - It is equivalent to a sparse closed interval. - Originally derived and heavily modified from Whoosh Span. - """ - - def __init__(self, *args): - """ - Create a new Span from a start and end ints or an iterable of ints. - - First form: - Span(start int, end int) : the span is initialized with a range(start, end+1) - - Second form: - Span(iterable of ints) : the span is initialized with the iterable - - Spans are hashable and immutable. - - For example: - >>> s = Span(1) - >>> s.start - 1 - >>> s = Span([1, 2]) - >>> s.start - 1 - >>> s.end - 2 - >>> s - Span(1, 2) - - >>> s = Span(1, 3) - >>> s.start - 1 - >>> s.end - 3 - >>> s - Span(1, 3) - - >>> s = Span([6, 5, 1, 2]) - >>> s.start - 1 - >>> s.end - 6 - >>> s - Span(1, 2)|Span(5, 6) - >>> len(s) - 4 - - >>> Span([5, 6, 7, 8, 9, 10 ,11, 12]) == Span([5, 6, 7, 8, 9, 10 ,11, 12]) - True - >>> hash(Span([5, 6, 7, 8, 9, 10 ,11, 12])) == hash(Span([5, 6, 7, 8, 9, 10 ,11, 12])) - True - >>> hash(Span([5, 6, 7, 8, 9, 10 ,11, 12])) == hash(Span(5, 12)) - True - """ - len_args = len(args) - - if len_args == 0: - self._set = spanset() - - elif len_args == 1: - # args0 is a single int or an iterable of ints - if isinstance(args[0], int): - self._set = spanset(args) - else: - # some sequence or iterable - self._set = spanset(list(args[0])) - - elif len_args == 2: - # args0 and args1 describe a start and end closed range - self._set = spanset(range(args[0], args[1] + 1)) - - else: - # args0 is a single int or args is an iterable of ints - # args is an iterable of ints - self._set = spanset(list(args)) - - @classmethod - def _from_iterable(cls, it): - return cls(list(it)) - - def __len__(self): - return len(self._set) - - def __iter__(self): - return iter(self._set) - - def __hash__(self): - return hash(tuple(self._set)) - - def __eq__(self, other): - return isinstance(other, Span) and self._set == other._set - - def __and__(self, *others): - return Span(self._set.intersection(*[o._set for o in others])) - - def __or__(self, *others): - return Span(self._set.union(*[o._set for o in others])) - - def union(self, *others): - """ - Return the union of this span with other spans as a new span. - (i.e. all positions that are in either spans.) - """ - return self.__or__(*others) - - def intersection(self, *others): - """ - Return the intersection of this span with other spans as a new span. - (i.e. all positions that are in both spans.) - """ - return self.__and__(*others) - - def difference(self, *others): - """ - Return the difference of two or more spans as a new span. - (i.e. all positions that are in this span but not the others.) - """ - return Span(self._set.difference(*[o._set for o in others])) - - def __repr__(self): - """ - Return a brief representation of this span by only listing contiguous - spans and not all items. - - For example: - >>> Span([1, 2, 3, 4, 5, 7, 8, 9, 10]) - Span(1, 5)|Span(7, 10) - """ - subspans_repr = [] - for subs in self.subspans(): - ls = len(subs) - if not ls: - subspans_repr.append("Span()") - elif ls == 1: - subspans_repr.append("Span(%d)" % subs.start) - else: - subspans_repr.append("Span(%d, %d)" % (subs.start, subs.end)) - return "|".join(subspans_repr) - - def __contains__(self, other): - """ - Return True if this span contains other span (where other is a Span, an - int or an ints set). - - For example: - >>> Span([5, 7]) in Span(5, 7) - True - >>> Span([5, 8]) in Span([5, 7]) - False - >>> 6 in Span([4, 5, 6, 7, 8]) - True - >>> 2 in Span([4, 5, 6, 7, 8]) - False - >>> 8 in Span([4, 8]) - True - >>> 5 in Span([4, 8]) - False - >>> set([4, 5]) in Span([4, 5, 6, 7, 8]) - True - >>> set([9]) in Span([4, 8]) - False - """ - if isinstance(other, Span): - return self._set.issuperset(other._set) - - if isinstance(other, int): - return self._set.__contains__(other) - - if isinstance(other, (set, frozenset)): - return self._set.issuperset(spanset(other)) - - if isinstance(other, spanset): - return self._set.issuperset(other) - - @property - def set(self): - return self._set - - def issubset(self, other): - return self._set.issubset(other._set) - - def issuperset(self, other): - return self._set.issuperset(other._set) - - @property - def start(self): - if not self._set: - raise TypeError("Empty Span has no start.") - if isinstance(self._set, set): - return sorted(self._set)[0] - return self._set[0] - - @property - def end(self): - if not self._set: - raise TypeError("Empty Span has no end.") - if isinstance(self._set, set): - return sorted(self._set)[-1] - return self._set[-1] - - @classmethod - def sort(cls, spans): - """ - Return a new sorted sequence of spans given a sequence of spans. - The primary sort is on start. The secondary sort is on length. - If two spans have the same start, the longer span will sort first. - - For example: - >>> spans = [Span([5, 6, 7, 8, 9, 10]), Span([1, 2]), Span([3, 4, 5]), Span([3, 4, 5, 6]), Span([8, 9, 10])] - >>> Span.sort(spans) - [Span(1, 2), Span(3, 6), Span(3, 5), Span(5, 10), Span(8, 10)] - - >>> spans = [Span([1, 2]), Span([3, 4, 5]), Span([3, 4, 5, 6]), Span([8, 9, 10])] - >>> Span.sort(spans) - [Span(1, 2), Span(3, 6), Span(3, 5), Span(8, 10)] - - >>> spans = [Span([1, 2]), Span([4, 5]), Span([7, 8]), Span([11, 12])] - >>> Span.sort(spans) - [Span(1, 2), Span(4, 5), Span(7, 8), Span(11, 12)] - - >>> spans = [Span([1, 2]), Span([7, 8]), Span([5, 6]), Span([12, 13])] - >>> Span.sort(spans) - [Span(1, 2), Span(5, 6), Span(7, 8), Span(12, 13)] - - """ - key = lambda s: ( - s.start, - -len(s), - ) - return sorted(spans, key=key) - - def magnitude(self): - """ - Return the maximal length represented by this span start and end. The - magnitude is the same as the length for a contiguous span. It will be - greater than the length for a span with non-contiguous int items. - An empty span has a zero magnitude. - - For example: - >>> Span([4, 8]).magnitude() - 5 - >>> len(Span([4, 8])) - 2 - >>> len(Span([4, 5, 6, 7, 8])) - 5 - - >>> Span([4, 5, 6, 14 , 12, 128]).magnitude() - 125 - - >>> Span([4, 5, 6, 7, 8]).magnitude() - 5 - >>> Span([0]).magnitude() - 1 - >>> Span([0]).magnitude() - 1 - """ - if not self._set: - return 0 - return self.end - self.start + 1 - - def density(self): - """ - Return the density of this span as a ratio of its length to its - magnitude, a float between 0 and 1. A dense Span has all its integer - items contiguous and a maximum density of one. A sparse low density span - has some non-contiguous integer items. An empty span has a zero density. - - For example: - >>> Span([4, 8]).density() - 0.4 - >>> Span([4, 5, 6, 7, 8]).density() - 1.0 - >>> Span([0]).density() - 1.0 - >>> Span().density() - 0 - """ - if not self._set: - return 0 - return len(self) / self.magnitude() - - def overlap(self, other): - """ - Return the count of overlapping items between this span and other span. - - For example: - >>> Span([1, 2]).overlap(Span([5, 6])) - 0 - >>> Span([5, 6]).overlap(Span([5, 6])) - 2 - >>> Span([4, 5, 6, 7]).overlap(Span([5, 6])) - 2 - >>> Span([4, 5, 6]).overlap(Span([5, 6, 7])) - 2 - >>> Span([4, 5, 6]).overlap(Span([6])) - 1 - >>> Span([4, 5]).overlap(Span([6, 7])) - 0 - """ - return len(self & other) - - def resemblance(self, other): - """ - Return a resemblance coefficient as a float between 0 and 1. - 0 means the spans are completely different and 1 identical. - """ - if self._set.isdisjoint(other._set): - return 0 - if self._set == other._set: - return 1 - resemblance = self.overlap(other) / len(self | other) - return resemblance - - def containment(self, other): - """ - Return a containment coefficient as a float between 0 and 1. This is an - indication of how much of the other span is contained in this span. - - 1 means the other span is entirely contained in this span. - - 0 means that the other span is not contained at all this span. - """ - if self._set.isdisjoint(other._set): - return 0 - if self._set == other._set: - return 1 - containment = self.overlap(other) / len(other) - return containment - - def surround(self, other): - """ - Return True if this span surrounds other span. - This is different from containment. A span can surround another span region - and have no positions in common with the surrounded. - - For example: - >>> Span([4, 8]).surround(Span([4, 8])) - True - >>> Span([3, 9]).surround(Span([4, 8])) - True - >>> Span([5, 8]).surround(Span([4, 8])) - False - >>> Span([4, 7]).surround(Span([4, 8])) - False - >>> Span([4, 5, 6, 7, 8]).surround(Span([5, 6, 7])) - True - """ - return self.start <= other.start and self.end >= other.end - - def is_before(self, other): - return self.end < other.start - - def is_after(self, other): - return self.start > other.end - - def touch(self, other): - """ - Return True if self sequence is contiguous with other span without overlap. - - For example: - >>> Span([5, 7]).touch(Span([5])) - False - >>> Span([5, 7]).touch(Span([5, 8])) - False - >>> Span([5, 7]).touch(Span([7, 8])) - False - >>> Span([5, 7]).touch(Span([8, 9])) - True - >>> Span([8, 9]).touch(Span([5, 7])) - True - """ - return self.start == other.end + 1 or self.end == other.start - 1 - - def distance_to(self, other): - """ - Return the absolute positive distance from this span to other span. - Overlapping spans have a zero distance. - Non-overlapping touching spans have a distance of one. - - For example: - >>> Span([8, 9]).distance_to(Span([5, 7])) - 1 - >>> Span([5, 7]).distance_to(Span([8, 9])) - 1 - >>> Span([5, 6]).distance_to(Span([8, 9])) - 2 - >>> Span([8, 9]).distance_to(Span([5, 6])) - 2 - >>> Span([5, 7]).distance_to(Span([5, 7])) - 0 - >>> Span([4, 5, 6]).distance_to(Span([5, 6, 7])) - 0 - >>> Span([5, 7]).distance_to(Span([10, 12])) - 3 - >>> Span([1, 2]).distance_to(Span(range(4, 52))) - 2 - """ - if self.overlap(other): - return 0 - - if self.touch(other): - return 1 - - if self.is_before(other): - return other.start - self.end - else: - return self.start - other.end - - @staticmethod - def from_ints(ints): - """ - Return a sequence of Spans from an iterable of ints. A new Span is - created for each group of monotonously increasing int items. - - >>> Span.from_ints([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) - [Span(1, 12)] - >>> Span.from_ints([1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12]) - [Span(1, 3), Span(5, 12)] - >>> Span.from_ints([0, 2, 3, 5, 6, 7, 8, 9, 10, 11, 13]) - [Span(0), Span(2, 3), Span(5, 11), Span(13)] - """ - ints = sorted(set(ints)) - groups = (group for _, group in groupby(ints, lambda group, c=count(): next(c) - group)) - return [Span(g) for g in groups] - - def subspans(self): - """ - Return a list of Spans creating one new Span for each set of contiguous - integer items. - - For example: - >>> span = Span(5, 6, 7, 8, 9, 10) | Span([1, 2]) | Span(3, 5) | Span(3, 6) | Span([8, 9, 10]) - >>> span.subspans() - [Span(1, 10)] - - When subspans are not touching they do not merge : - >>> span = Span([63, 64]) | Span([58, 58]) - >>> span.subspans() - [Span(58), Span(63, 64)] - - Overlapping subspans are merged as needed: - >>> span = Span([12, 17, 24]) | Span([15, 16, 17, 35]) | Span(58) | Span(63, 64) - >>> span.subspans() - [Span(12), Span(15, 17), Span(24), Span(35), Span(58), Span(63, 64)] - """ - return Span.from_ints(self) diff --git a/src/univers/spans.py.ABOUT b/src/univers/spans.py.ABOUT deleted file mode 100644 index 27ad909c..00000000 --- a/src/univers/spans.py.ABOUT +++ /dev/null @@ -1,16 +0,0 @@ -about_resource: spans.py -version: 2.4.1 -name: Whoosh Spans -vcs_url: hg+https://bitbucket.org/mchaput/whoosh@72e06bd0aac8 -home_url: https://bitbucket.org/mchaput/whoosh -owner: Matt Chaput -copyright: Copyright (c) 2011 Matt Chaput. -download_url: https://bitbucket.org/mchaput/whoosh/get/2.4.1.tar.gz -description: Manage spans of tokens and text. - This file was originally copied and modified from Whoosh. - -license_expression: bsd-simplified - -licenses: -- file: spans.py.LICENSE - key: bsd-simplified diff --git a/src/univers/spans.py.LICENSE b/src/univers/spans.py.LICENSE deleted file mode 100644 index b0266325..00000000 --- a/src/univers/spans.py.LICENSE +++ /dev/null @@ -1,26 +0,0 @@ -Copyright 2011 Matt Chaput. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY MATT CHAPUT ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL MATT CHAPUT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and documentation are -those of the authors and should not be interpreted as representing official -policies, either expressed or implied, of Matt Chaput. diff --git a/src/univers/version_range.py b/src/univers/version_range.py index 9c786d85..7f89949a 100644 --- a/src/univers/version_range.py +++ b/src/univers/version_range.py @@ -230,6 +230,37 @@ def __eq__(self, other): and self.constraints == other.constraints ) + def normalize(self, known_versions: List[str]): + """ + Return a new VersionRange normalized and simplified using the universe of + ``known_versions`` list of version strings. + """ + versions = sorted([self.version_class(i) for i in known_versions]) + + resolved = [] + contiguous = [] + for kv in versions: + if self.__contains__(kv): + contiguous.append(kv) + elif contiguous: + resolved.append(contiguous) + contiguous = [] + + if contiguous: + resolved.append(contiguous) + + version_constraints = [] + for contiguous_segment in resolved: + lower_bound = contiguous_segment[0] + upper_bound = contiguous_segment[-1] + if lower_bound == upper_bound: + version_constraints.append(VersionConstraint(version=lower_bound)) + else: + version_constraints.append(VersionConstraint(comparator=">=", version=lower_bound)) + version_constraints.append(VersionConstraint(comparator="<=", version=upper_bound)) + + return self.__class__(constraints=version_constraints) + def from_cve_v4(data, scheme): """ diff --git a/tests/test_normalized_range.py b/tests/test_normalized_range.py deleted file mode 100644 index 495bf042..00000000 --- a/tests/test_normalized_range.py +++ /dev/null @@ -1,82 +0,0 @@ -# -# Copyright (c) nexB Inc. and others. -# SPDX-License-Identifier: Apache-2.0 -# -# Visit https://aboutcode.org and https://github.com/nexB/univers for support and download. - -from unittest import TestCase - -from univers.normalized_range import NormalizedVersionRange -from univers.normalized_range import get_region -from univers.normalized_range import get_version_range_from_span -from univers.span import Span -from univers.version_constraint import VersionConstraint -from univers.version_range import VersionRange -from univers.versions import SemverVersion - - -class TestNormalizedVersionRange(TestCase): - purl_type = "pypi" - all_versions = versions = [ - "1.0.0", - "1.1.0", - "1.2.0", - "1.3.0", - "2.0.0", - "3.0.0", - ] - versions = [SemverVersion(i) for i in all_versions] - - def test_get_region(self): - - constraint1 = VersionConstraint(comparator="<=", version=SemverVersion("1.0.0")) - constraint2 = VersionConstraint(comparator="!=", version=SemverVersion("1.1.0")) - constraint3 = VersionConstraint(comparator=">", version=SemverVersion("1.3.0")) - - assert get_region(constraint=constraint1, versions=self.versions) == Span(0) - assert get_region(constraint=constraint2, versions=self.versions) == Span(0).union( - Span(2, 5) - ) - assert get_region(constraint=constraint3, versions=self.versions) == Span(4, 5) - - def test_get_version_range_from_span(self): - span1 = Span(1) - span2 = Span(1, 4) - span3 = Span(1, 4).intersection(Span(3, 5)) - span4 = Span(0).union(Span(2, 3)).union(Span(5)) - - vr1 = get_version_range_from_span( - span=span1, purl_type=self.purl_type, versions=self.versions - ) - vr2 = get_version_range_from_span( - span=span2, purl_type=self.purl_type, versions=self.versions - ) - vr3 = get_version_range_from_span( - span=span3, purl_type=self.purl_type, versions=self.versions - ) - vr4 = get_version_range_from_span( - span=span4, purl_type=self.purl_type, versions=self.versions - ) - - assert str(vr1) == "vers:pypi/1.1.0" - assert str(vr2) == "vers:pypi/>=1.1.0|<=2.0.0" - assert str(vr3) == "vers:pypi/>=1.3.0|<=2.0.0" - assert str(vr4) == "vers:pypi/1.0.0|>=1.2.0|<=1.3.0|3.0.0" - - def test_NormalizedVersionRange_from_vers(self): - vr1 = VersionRange.from_string("vers:pypi/<=1.1.0|>=1.2.0|<=1.3.0|3.0.0") - nvr1 = NormalizedVersionRange.from_vers(vers_range=vr1, all_versions=self.all_versions) - - vr2 = VersionRange.from_string("vers:pypi/>=1.0.0|<=1.1.0|>=1.2.0|<=1.3.0|3.0.0") - nvr2 = NormalizedVersionRange.from_vers(vers_range=vr2, all_versions=self.all_versions) - - vr3 = VersionRange.from_string("vers:pypi/<=1.3.0|3.0.0") - nvr3 = NormalizedVersionRange.from_vers(vers_range=vr3, all_versions=self.all_versions) - - vr4 = VersionRange.from_string("vers:pypi/<2.0.0|3.0.0") - nvr4 = NormalizedVersionRange.from_vers(vers_range=vr4, all_versions=self.all_versions) - - assert str(nvr1) == "vers:pypi/>=1.0.0|<=1.3.0|3.0.0" - assert str(nvr2) == "vers:pypi/>=1.0.0|<=1.3.0|3.0.0" - assert str(nvr3) == "vers:pypi/>=1.0.0|<=1.3.0|3.0.0" - assert str(nvr4) == "vers:pypi/>=1.0.0|<=1.3.0|3.0.0" diff --git a/tests/test_version_range.py b/tests/test_version_range.py index 5449acb9..a8320bda 100644 --- a/tests/test_version_range.py +++ b/tests/test_version_range.py @@ -43,7 +43,7 @@ def test_VersionRange_to_string(self): assert str(version_range) == "vers:pypi/>=0.0.0|0.0.1|0.0.2|0.0.3|0.0.4|0.0.5|0.0.6" def test_VersionRange_pypi_does_not_contain_basic(self): - vers = "vers:pypi/0.0.2|0.0.6|>=0.0.0|0.0.1|0.0.4|0.0.5|0.0.3" + vers = "vers:pypi/0.0.2|0.0.6|>=3.0.0|0.0.1|0.0.4|0.0.5|0.0.3" version_range = VersionRange.from_string(vers) assert not version_range.contains(PypiVersion("2.0.3")) @@ -456,3 +456,30 @@ def test_mattermost_version_range(): VersionConstraint(comparator=">=", version=SemverVersion("5.0")), ] ) == VersionRange.from_string("vers:mattermost/>=5.0") + + +def test_version_range_normalize_case1(): + known_versions = ["3.0.0", "1.0.0", "2.0.0", "1.3.0", "1.1.0", "1.2.0"] + + vr = VersionRange.from_string("vers:pypi/<=1.1.0|>=1.2.0|<=1.3.0|3.0.0") + nvr = vr.normalize(known_versions=known_versions) + + assert str(nvr) == "vers:pypi/>=1.0.0|<=1.3.0|3.0.0" + + +def test_version_range_normalize_case2(): + known_versions = ["3.0.0", "1.0.0", "2.0.0", "1.3.0", "1.1.0", "1.2.0"] + + vr = VersionRange.from_string("vers:pypi/<=1.3.0|3.0.0") + nvr = vr.normalize(known_versions=known_versions) + + assert str(nvr) == "vers:pypi/>=1.0.0|<=1.3.0|3.0.0" + + +def test_version_range_normalize_case3(): + known_versions = ["3.0.0", "1.0.0", "2.0.0", "1.3.0", "1.1.0", "1.2.0"] + + vr = VersionRange.from_string("vers:pypi/<2.0.0|3.0.0") + nvr = vr.normalize(known_versions=known_versions) + + assert str(nvr) == "vers:pypi/>=1.0.0|<=1.3.0|3.0.0"