예제 #1
0
def test_version_getitem():
    version = Version("3.8.6")
    assert version[0] == 3
    assert version[1] == 8
    assert version[2] == 6
    assert version[1:2] == Version("8")
    assert version[:-1] == Version("3.8")
예제 #2
0
def test_support_prerelease_version() -> None:
    assert not Version("3.9.0").is_prerelease
    v = Version("3.9.0a4")
    assert v.is_prerelease
    assert str(v) == "3.9.0a4"
    assert v.complete() == v
    assert v.bump() == Version("3.9.0a5")
    assert v.bump(2) == Version("3.9.1")
예제 #3
0
 def _populate_version_range(
     self, lower: Version, upper: Version
 ) -> Iterable[Version]:
     """Expand the version range to a collection of versions to exclude,
     taking the released python versions into consideration.
     """
     assert lower < upper
     prev = lower
     while prev < upper:
         if prev[-2:] == Version((0, 0)):  # X.0.0
             cur = prev.bump(0)  # X+1.0.0
             if cur <= upper:  # It is still within the range
                 yield Version((prev[0], "*"))  # Exclude the whole major series: X.*
                 prev = cur
                 continue
         if prev[-1] == 0:  # X.Y.0
             cur = prev.bump(1)  # X.Y+1.0
             if cur <= upper:  # It is still within the range
                 yield prev[:2].complete("*")  # Exclude X.Y.*
                 prev = (
                     prev.bump(0)
                     if cur.is_py2
                     and cast(int, cur[1]) > self.PY_MAX_MINOR_VERSION[cur[:1]]
                     else cur
                 )  # If prev is 2.7, next is 3.0, otherwise next is X.Y+1.0
                 continue
             while prev < upper:
                 # Iterate each version from X.Y.0(prev) to X.Y.Z(upper)
                 yield prev
                 prev = prev.bump()
             break
         # Can't produce any wildcard versions
         cur = prev.bump(1)
         if cur <= upper:  # X.Y+1.0 is still within the range
             current_max = self.PY_MAX_MINOR_VERSION[prev[:2]]
             for z in range(cast(int, prev[2]), current_max + 1):
                 yield prev[:2].complete(z)
             prev = (
                 prev.bump(0)
                 if cur.is_py2
                 and cast(int, cur[1]) > self.PY_MAX_MINOR_VERSION[cur[:1]]
                 else cur
             )
         else:  # Produce each version from X.Y.Z to X.Y.W
             while prev < upper:
                 yield prev
                 prev = prev.bump()
             break
예제 #4
0
def _normalize_op_specifier(op: str, version_str: str) -> Tuple[str, Version]:
    version = Version(version_str)
    if version.is_wildcard:
        if op == "==":
            op = "~="
            version[-1] = 0
        elif op == ">":  # >X.Y.* => >=X.Y+1.0
            op = ">="
            version = version.bump(-2)
        elif op in ("<", ">=", "<="):
            # <X.Y.* => <X.Y.0
            # >=X.Y.* => >=X.Y.0
            # <=X.Y.* => <X.Y.0
            version[-1] = 0
            if op == "<=":
                op = "<"
        elif op != "!=":
            raise InvalidPyVersion(f"Unsupported version specifier: {op}{version}")

    if op != "~=" and not (op == "!=" and version.is_wildcard):
        # Don't complete with .0 for ~=3.5 and !=3.4.*:
        version = version.complete()
    return op, version
예제 #5
0
def test_version_setitem():
    version = Version("3.8.*")
    version1 = version.complete()
    version1[-1] = 0
    assert version1 == Version("3.8.0")

    version2 = version.complete()
    version2[0] = 4
    assert version2 == Version("4.8.*")

    version3 = version.complete()
    with pytest.raises(TypeError):
        version3[:2] = (1, 2)
예제 #6
0
def test_version_startswith(version, other, result):
    assert Version(version).startswith(Version(other)) is result
예제 #7
0
def test_unsupported_prerelease_version():
    with pytest.raises(InvalidPyVersion):
        Version("3.9.0a4")
예제 #8
0
def test_version_bump(version, idx, result):
    assert str(Version(version).bump(idx)) == result
예제 #9
0
def test_version_complete(version, args, result):
    assert str(Version(version).complete(*args)) == result
예제 #10
0
def test_version_comparison():
    assert Version("3.9.0") < Version("3.9.1")
    assert Version("3.4") < Version("3.9.1")
    assert Version("3.7.*") < Version("3.7.5")
    assert Version("3.7") == Version((3, 7))

    assert Version("3.9.0a") != Version("3.9.0")
    assert Version("3.9.0a") == Version("3.9.0a0")
    assert Version("3.10.0a9") < Version("3.10.0a12")
    assert Version("3.10.0a12") < Version("3.10.0b1")
    assert Version("3.7.*") < Version("3.7.1b")
예제 #11
0
def test_version_is_wildcard():
    assert not Version("3").is_wildcard
    assert Version("3.*").is_wildcard
예제 #12
0
def test_version_comparison():
    assert Version("3.9.0") < Version("3.9.1")
    assert Version("3.4") < Version("3.9.1")
    assert Version("3.7.*") < Version("3.7.5")
    assert Version("3.7") == Version((3, 7))
예제 #13
0
def test_normalize_non_standard_version():
    version = Version("3.9*")
    assert str(version) == "3.9.*"
예제 #14
0
def test_unsupported_post_version() -> None:
    with pytest.raises(InvalidPyVersion):
        Version("3.10.0post1")
예제 #15
0
    def _merge_bounds_and_excludes(
        cls,
        lower: Version,
        upper: Version,
        excludes: Iterable[Version],
    ) -> Tuple[Version, Version, List[Version]]:
        sorted_excludes = sorted(excludes)
        wildcard_excludes = {
            version[:-1]
            for version in sorted_excludes if version.is_wildcard
        }
        # Remove versions that are already excluded by another wildcard exclude.
        sorted_excludes = [
            version for version in sorted_excludes
            if version.is_wildcard or not any(
                version.startswith(wv) for wv in wildcard_excludes)
        ]

        if lower == Version.MIN and upper == Version.MAX:
            # Nothing we can do here, it is a non-constraint.
            return lower, upper, sorted_excludes

        for version in list(sorted_excludes):  # from to low to high
            if version >= upper:
                sorted_excludes[:] = []
                break

            if version.is_wildcard:
                valid_length = len(version._version) - 1
                valid_version = version[:valid_length]

                if valid_version < lower[:valid_length]:
                    # Useless excludes
                    sorted_excludes.remove(version)
                elif lower.startswith(valid_version):
                    # The lower bound is excluded, e.g: >=3.7.3,!=3.7.*
                    # bump the lower version in the last common bit: >=3.8.0
                    lower = version.bump(-2)
                    sorted_excludes.remove(version)
                else:
                    break
            else:
                if version < lower:
                    sorted_excludes.remove(version)
                elif version == lower:
                    lower = version.bump()
                    sorted_excludes.remove(version)
                else:
                    break
        for version in reversed(sorted_excludes):  # from high to low
            if version >= upper:
                sorted_excludes.remove(version)
                continue

            if not version.is_wildcard:
                break
            valid_length = len(version._version) - 1
            valid_version = version[:valid_length]

            if upper.startswith(valid_version) or version.bump(-2) == upper:
                # Case 1: The upper bound is excluded, e.g: <3.7.3,!=3.7.*
                # set the upper to the zero version: <3.7.0
                # Case 2: The upper bound is adjacent to the excluded one,
                # e.g: <3.7.0,!=3.6.*
                # Move the upper bound to below the excluded: <3.6.0
                upper = valid_version.complete()
                sorted_excludes.remove(version)
            else:
                break

        return lower, upper, sorted_excludes
예제 #16
0
class PySpecSet(SpecifierSet):
    """A custom SpecifierSet that supports merging with logic operators (&, |)."""

    PY_MAX_MINOR_VERSION = {
        Version(key): value
        for key, value in json.loads(MAX_VERSIONS_FILE.read_text()).items()
    }
    MAX_MAJOR_VERSION = max(PY_MAX_MINOR_VERSION)[:1].bump()

    def __init__(self, version_str: str = "", analyze: bool = True) -> None:
        if version_str == "*":
            version_str = ""
        super().__init__(version_str)
        self._lower_bound = Version.MIN
        self._upper_bound = Version.MAX
        self._excludes: List[Version] = []
        if version_str and analyze:
            self._analyze_specifiers()

    def _analyze_specifiers(self) -> None:
        lower_bound, upper_bound = Version.MIN, Version.MAX
        excludes: Set[Version] = set()
        for spec in self:
            op, version = _normalize_op_specifier(spec.operator, spec.version)

            if op in ("==", "==="):
                lower_bound = version
                upper_bound = version.bump()
                break
            if op == "!=":
                excludes.add(version)
            elif op[0] == ">":
                lower_bound = max(lower_bound,
                                  version if op == ">=" else version.bump())
            elif op[0] == "<":
                upper_bound = min(upper_bound,
                                  version.bump() if op == "<=" else version)
            elif op == "~=":
                new_lower = version.complete()
                new_upper = version.bump(-2)
                if new_upper < upper_bound:
                    upper_bound = new_upper
                if new_lower > lower_bound:
                    lower_bound = new_lower
            else:
                raise InvalidPyVersion(
                    f"Unsupported version specifier: {op}{version}")
        self._rearrange(lower_bound, upper_bound, excludes)

    @classmethod
    def _merge_bounds_and_excludes(
        cls,
        lower: Version,
        upper: Version,
        excludes: Iterable[Version],
    ) -> Tuple[Version, Version, List[Version]]:
        sorted_excludes = sorted(excludes)
        wildcard_excludes = {
            version[:-1]
            for version in sorted_excludes if version.is_wildcard
        }
        # Remove versions that are already excluded by another wildcard exclude.
        sorted_excludes = [
            version for version in sorted_excludes
            if version.is_wildcard or not any(
                version.startswith(wv) for wv in wildcard_excludes)
        ]

        if lower == Version.MIN and upper == Version.MAX:
            # Nothing we can do here, it is a non-constraint.
            return lower, upper, sorted_excludes

        for version in list(sorted_excludes):  # from to low to high
            if version >= upper:
                sorted_excludes[:] = []
                break

            if version.is_wildcard:
                valid_length = len(version._version) - 1
                valid_version = version[:valid_length]

                if valid_version < lower[:valid_length]:
                    # Useless excludes
                    sorted_excludes.remove(version)
                elif lower.startswith(valid_version):
                    # The lower bound is excluded, e.g: >=3.7.3,!=3.7.*
                    # bump the lower version in the last common bit: >=3.8.0
                    lower = version.bump(-2)
                    sorted_excludes.remove(version)
                else:
                    break
            else:
                if version < lower:
                    sorted_excludes.remove(version)
                elif version == lower:
                    lower = version.bump()
                    sorted_excludes.remove(version)
                else:
                    break
        for version in reversed(sorted_excludes):  # from high to low
            if version >= upper:
                sorted_excludes.remove(version)
                continue

            if not version.is_wildcard:
                break
            valid_length = len(version._version) - 1
            valid_version = version[:valid_length]

            if upper.startswith(valid_version) or version.bump(-2) == upper:
                # Case 1: The upper bound is excluded, e.g: <3.7.3,!=3.7.*
                # set the upper to the zero version: <3.7.0
                # Case 2: The upper bound is adjacent to the excluded one,
                # e.g: <3.7.0,!=3.6.*
                # Move the upper bound to below the excluded: <3.6.0
                upper = valid_version.complete()
                sorted_excludes.remove(version)
            else:
                break

        return lower, upper, sorted_excludes

    def _rearrange(self, lower_bound: Version, upper_bound: Version,
                   excludes: Iterable[Version]) -> None:
        """Rearrange the version bounds with the given inputs."""
        (
            self._lower_bound,
            self._upper_bound,
            self._excludes,
        ) = self._merge_bounds_and_excludes(lower_bound, upper_bound, excludes)
        if not self.is_impossible:
            super().__init__(str(self))

    def _comp_key(self) -> Tuple[Version, Version, Tuple[Version, ...]]:
        return (self._lower_bound, self._upper_bound, tuple(self._excludes))

    def __hash__(self) -> int:
        return hash(self._comp_key())

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, PySpecSet):
            return False
        return self._comp_key() == other._comp_key()

    @property
    def is_impossible(self) -> bool:
        """Check whether the specifierset contains any valid versions."""
        if self._lower_bound == Version.MIN or self._upper_bound == Version.MAX:
            return False
        return self._lower_bound >= self._upper_bound

    @property
    def is_allow_all(self) -> bool:
        """Return True if the specifierset accepts all versions."""
        if self.is_impossible:
            return False
        return (self._lower_bound == Version.MIN
                and self._upper_bound == Version.MAX and not self._excludes)

    def __bool__(self) -> bool:
        return not self.is_allow_all

    def __str__(self) -> str:
        if self.is_impossible:
            return "impossible"
        if self.is_allow_all:
            return ""
        lower = self._lower_bound
        upper = self._upper_bound
        if lower[-1] == 0 and not lower.is_prerelease:
            lower = lower[:-1]
        if upper[-1] == 0 and not upper.is_prerelease:
            upper = upper[:-1]
        lower_str = "" if lower == Version.MIN else f">={lower}"
        upper_str = "" if upper == Version.MAX else f"<{upper}"
        excludes_str = ",".join(f"!={version}" for version in self._excludes)

        return ",".join(filter(None, [lower_str, upper_str, excludes_str]))

    def __repr__(self) -> str:
        return f"<PySpecSet {self}>"

    def copy(self) -> "PySpecSet":
        """Create a new specifierset that is same as the original one."""
        if self.is_impossible:
            return ImpossiblePySpecSet()
        instance = self.__class__(str(self), False)
        instance._lower_bound = self._lower_bound
        instance._upper_bound = self._upper_bound
        instance._excludes = self._excludes[:]
        return instance

    @lru_cache()
    def __and__(self, other: "PySpecSet") -> "PySpecSet":
        if any(s.is_impossible for s in (self, other)):
            return ImpossiblePySpecSet()
        if self.is_allow_all:
            return other.copy()
        elif other.is_allow_all:
            return self.copy()
        rv = self.copy()
        excludes = set(rv._excludes) | set(other._excludes)
        lower = max(rv._lower_bound, other._lower_bound)
        upper = min(rv._upper_bound, other._upper_bound)
        rv._rearrange(lower, upper, excludes)
        return rv

    @lru_cache()
    def __or__(self, other: "PySpecSet") -> "PySpecSet":
        if self.is_impossible:
            return other.copy()
        elif other.is_impossible:
            return self.copy()
        if self.is_allow_all:
            return self.copy()
        elif other.is_allow_all:
            return other.copy()
        rv = self.copy()
        left, right = sorted([rv, other], key=lambda x: x._lower_bound)
        excludes = set(left._excludes) & set(right._excludes)
        lower = left._lower_bound
        upper = max(left._upper_bound, right._upper_bound)
        if right._lower_bound > left._upper_bound:  # two ranges has no overlap
            excludes.update(
                self._populate_version_range(left._upper_bound,
                                             right._lower_bound))
        rv._rearrange(lower, upper, excludes)
        return rv

    def _populate_version_range(self, lower: Version,
                                upper: Version) -> Iterable[Version]:
        """Expand the version range to a collection of versions to exclude,
        taking the released python versions into consideration.
        """
        assert lower < upper
        prev = lower
        while prev < upper:
            if prev[-2:] == Version((0, 0)):  # X.0.0
                cur = prev.bump(0)  # X+1.0.0
                if cur <= upper:  # It is still within the range
                    yield Version(
                        (prev[0], "*"))  # Exclude the whole major series: X.*
                    prev = cur
                    continue
            if prev[-1] == 0:  # X.Y.0
                cur = prev.bump(1)  # X.Y+1.0
                if cur <= upper:  # It is still within the range
                    yield prev[:2].complete("*")  # Exclude X.Y.*
                    prev = (
                        prev.bump(0) if cur.is_py2 and
                        cast(int, cur[1]) > self.PY_MAX_MINOR_VERSION[cur[:1]]
                        else cur
                    )  # If prev is 2.7, next is 3.0, otherwise next is X.Y+1.0
                    continue
                while prev < upper:
                    # Iterate each version from X.Y.0(prev) to X.Y.Z(upper)
                    yield prev
                    prev = prev.bump()
                break
            # Can't produce any wildcard versions
            cur = prev.bump(1)
            if cur <= upper:  # X.Y+1.0 is still within the range
                current_max = self.PY_MAX_MINOR_VERSION[prev[:2]]
                for z in range(cast(int, prev[2]), current_max + 1):
                    yield prev[:2].complete(z)
                prev = (prev.bump(0) if cur.is_py2 and
                        cast(int, cur[1]) > self.PY_MAX_MINOR_VERSION[cur[:1]]
                        else cur)
            else:  # Produce each version from X.Y.Z to X.Y.W
                while prev < upper:
                    yield prev
                    prev = prev.bump()
                break

    @lru_cache()
    def is_superset(self, other: Union[str, SpecifierSet]) -> bool:
        if self.is_impossible:
            return False
        if self.is_allow_all:
            return True
        other = type(self)(str(other))
        if other._upper_bound >= self.MAX_MAJOR_VERSION:
            # XXX: narrow down the upper bound to ``MAX_MAJOR_VERSION``
            # So that `>=3.6,<4.0` is considered a superset of `>=3.7`, see issues/66
            other._upper_bound = self.MAX_MAJOR_VERSION
        lower, upper, excludes = self._merge_bounds_and_excludes(
            other._lower_bound, other._upper_bound, self._excludes)
        if (self._lower_bound > other._lower_bound
                or self._upper_bound < other._upper_bound):
            return False
        return (lower <= other._lower_bound and upper >= other._upper_bound
                and set(excludes) <= set(other._excludes))

    @lru_cache()
    def is_subset(self, other: Union[str, SpecifierSet]) -> bool:
        if self.is_impossible:
            return False
        other = type(self)(str(other))
        if other._upper_bound >= self.MAX_MAJOR_VERSION:
            # Relax the upper bound to max version
            other._upper_bound = Version.MAX
        if other.is_allow_all:
            return True
        lower, upper, excludes = self._merge_bounds_and_excludes(
            self._lower_bound, self._upper_bound, other._excludes)
        if (self._lower_bound < other._lower_bound
                or self._upper_bound > other._upper_bound):
            return False
        return (lower <= self._lower_bound and upper >= self._upper_bound
                and set(self._excludes) >= set(excludes))

    def as_marker_string(self) -> str:
        if self.is_allow_all:
            return ""
        result = []
        excludes = []
        full_excludes = []
        for spec in sorted(self, key=attrgetter("version")):
            op, version = spec.operator, spec.version
            if len(version.split(".")) < 3:
                key = "python_version"
            else:
                key = "python_full_version"
                if version[-2:] == ".*":
                    version = version[:-2]
                    key = "python_version"
            if op == "!=":
                if key == "python_version":
                    excludes.append(version)
                else:
                    full_excludes.append(version)
            else:
                result.append(f"{key}{op}{version!r}")
        if excludes:
            result.append("python_version not in {!r}".format(", ".join(
                sorted(excludes))))
        if full_excludes:
            result.append("python_full_version not in {!r}".format(", ".join(
                sorted(full_excludes))))
        return " and ".join(result)

    def supports_py2(self) -> bool:
        return self._lower_bound.is_py2
예제 #17
0
def test_version_is_py2():
    assert not Version("3.8").is_py2
    assert Version("2.7").is_py2