diff --git a/poetry.lock b/poetry.lock index 0b585234d..e4fdb348c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -106,7 +106,7 @@ toml = ["toml"] name = "dataclasses" version = "0.8" description = "A backport of the dataclasses module for Python 3.6" -category = "dev" +category = "main" optional = false python-versions = ">=3.6, <3.7" @@ -198,6 +198,7 @@ python-versions = "*" [package.dependencies] attrs = ">=17.4.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} pyrsistent = ">=0.14.0" six = ">=1.11.0" @@ -536,7 +537,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "317b9ea5e19fcb7b559aaa52f325fe5dddb30ec5f938446b79608c6b8b10fcba" +content-hash = "9a3dbd14f45aa7c96293a0b4286c71f0b6501bc5fda78d834a9212c0c55cf3cd" [metadata.files] appdirs = [ diff --git a/poetry/core/packages/dependency.py b/poetry/core/packages/dependency.py index 75a89da16..b090aed77 100644 --- a/poetry/core/packages/dependency.py +++ b/poetry/core/packages/dependency.py @@ -62,7 +62,7 @@ def __init__( if isinstance(self._constraint, VersionRange) and self._constraint.min: allows_prereleases = ( - allows_prereleases or self._constraint.min.is_prerelease() + allows_prereleases or self._constraint.min.is_unstable() ) self._allows_prereleases = allows_prereleases @@ -551,17 +551,17 @@ def create_from_pep_508( if parsed_version.precision == 1: if op == "<=": op = "<" - version = parsed_version.next_major.text + version = parsed_version.next_major().text elif op == ">": op = ">=" - version = parsed_version.next_major.text + version = parsed_version.next_major().text elif parsed_version.precision == 2: if op == "<=": op = "<" - version = parsed_version.next_minor.text + version = parsed_version.next_minor().text elif op == ">": op = ">=" - version = parsed_version.next_minor.text + version = parsed_version.next_minor().text elif op in ("in", "not in"): versions = [] for v in re.split("[ ,]+", version): diff --git a/poetry/core/packages/package.py b/poetry/core/packages/package.py index 4a8152e60..01aa8fbbd 100644 --- a/poetry/core/packages/package.py +++ b/poetry/core/packages/package.py @@ -305,7 +305,7 @@ def urls(self) -> Dict[str, str]: return urls def is_prerelease(self) -> bool: - return self._version.is_prerelease() + return self._version.is_unstable() def is_root(self) -> bool: return False diff --git a/poetry/core/packages/utils/utils.py b/poetry/core/packages/utils/utils.py index f2514b1bf..c97604c06 100644 --- a/poetry/core/packages/utils/utils.py +++ b/poetry/core/packages/utils/utils.py @@ -313,17 +313,17 @@ def get_python_constraint_from_marker( if parsed_version.precision == 1: if op == "<=": op = "<" - version = parsed_version.next_major.text + version = parsed_version.next_major().text elif op == ">": op = ">=" - version = parsed_version.next_major.text + version = parsed_version.next_major().text elif parsed_version.precision == 2: if op == "<=": op = "<" - version = parsed_version.next_minor.text + version = parsed_version.next_minor().text elif op == ">": op = ">=" - version = parsed_version.next_minor.text + version = parsed_version.next_minor().text elif op in ("in", "not in"): versions = [] for v in re.split("[ ,]+", version): diff --git a/poetry/core/semver/exceptions.py b/poetry/core/semver/exceptions.py index b24323997..cfdbe5b56 100644 --- a/poetry/core/semver/exceptions.py +++ b/poetry/core/semver/exceptions.py @@ -1,6 +1,2 @@ -class ParseVersionError(ValueError): - pass - - class ParseConstraintError(ValueError): pass diff --git a/poetry/core/semver/helpers.py b/poetry/core/semver/helpers.py index e20008f2e..7d9ca49c4 100644 --- a/poetry/core/semver/helpers.py +++ b/poetry/core/semver/helpers.py @@ -69,10 +69,9 @@ def parse_single_constraint(constraint: str) -> VersionTypes: m = TILDE_CONSTRAINT.match(constraint) if m: version = Version.parse(m.group(1)) - - high = version.stable.next_minor + high = version.stable.next_minor() if len(m.group(1).split(".")) == 1: - high = version.stable.next_major + high = version.stable.next_major() return VersionRange(version, high, include_min=True) @@ -89,9 +88,9 @@ def parse_single_constraint(constraint: str) -> VersionTypes: version = Version.parse(m.group(1)) if precision == 2: - high = version.stable.next_major + high = version.stable.next_major() else: - high = version.stable.next_minor + high = version.stable.next_minor() return VersionRange(version, high, include_min=True) @@ -100,7 +99,7 @@ def parse_single_constraint(constraint: str) -> VersionTypes: if m: version = Version.parse(m.group(1)) - return VersionRange(version, version.next_breaking, include_min=True) + return VersionRange(version, version.next_breaking(), include_min=True) # X Range m = X_CONSTRAINT.match(constraint) @@ -110,16 +109,15 @@ def parse_single_constraint(constraint: str) -> VersionTypes: minor = m.group(3) if minor is not None: - version = Version(major, int(minor), 0) - - result = VersionRange(version, version.next_minor, include_min=True) + version = Version.from_parts(major, int(minor), 0) + result = VersionRange(version, version.next_minor(), include_min=True) else: if major == 0: - result = VersionRange(max=Version(1, 0, 0)) + result = VersionRange(max=Version.from_parts(1, 0, 0)) else: - version = Version(major, 0, 0) + version = Version.from_parts(major, 0, 0) - result = VersionRange(version, version.next_major, include_min=True) + result = VersionRange(version, version.next_major(), include_min=True) if op == "!=": result = VersionRange().difference(result) diff --git a/poetry/core/semver/version.py b/poetry/core/semver/version.py index e20764d8c..6ed622fc9 100644 --- a/poetry/core/semver/version.py +++ b/poetry/core/semver/version.py @@ -1,184 +1,57 @@ -import re +import dataclasses from typing import TYPE_CHECKING -from typing import List from typing import Optional +from typing import Tuple from typing import Union -from .empty_constraint import EmptyConstraint -from .exceptions import ParseVersionError -from .patterns import COMPLETE_VERSION -from .version_constraint import VersionConstraint -from .version_range import VersionRange -from .version_union import VersionUnion +from poetry.core.semver.empty_constraint import EmptyConstraint +from poetry.core.semver.version_range_constraint import VersionRangeConstraint +from poetry.core.semver.version_union import VersionUnion +from poetry.core.version.pep440 import Release +from poetry.core.version.pep440 import ReleaseTag +from poetry.core.version.pep440.version import PEP440Version if TYPE_CHECKING: - from . import VersionTypes # noqa + from poetry.core.semver.helpers import VersionTypes + from poetry.core.semver.version_range import VersionRange + from poetry.core.version.pep440 import LocalSegmentType -class Version(VersionRange): +@dataclasses.dataclass(frozen=True) +class Version(PEP440Version, VersionRangeConstraint): """ A parsed semantic version number. """ - def __init__( - self, - major: int, - minor: Optional[int] = None, - patch: Optional[int] = None, - rest: Optional[int] = None, - pre: Optional[str] = None, - build: Optional[str] = None, - text: Optional[str] = None, - precision: Optional[int] = None, - ) -> None: - self._major = int(major) - self._precision = None - if precision is None: - self._precision = 1 - - if minor is None: - minor = 0 - else: - if self._precision is not None: - self._precision += 1 - - self._minor = int(minor) - - if patch is None: - patch = 0 - else: - if self._precision is not None: - self._precision += 1 - - if rest is None: - rest = 0 - else: - if self._precision is not None: - self._precision += 1 - - if precision is not None: - self._precision = precision - - self._patch = int(patch) - self._rest = int(rest) - - if text is None: - parts = [str(major)] - if self._precision >= 2 or minor != 0: - parts.append(str(minor)) - - if self._precision >= 3 or patch != 0: - parts.append(str(patch)) - - if self._precision >= 4 or rest != 0: - parts.append(str(rest)) - - text = ".".join(parts) - if pre: - text += "-{}".format(pre) - - if build: - text += "+{}".format(build) - - self._text = text - - pre = self._normalize_prerelease(pre) - - self._prerelease = [] - if pre is not None: - self._prerelease = self._split_parts(pre) - - build = self._normalize_build(build) - - self._build = [] - if build is not None: - if build.startswith(("-", "+")): - build = build[1:] - - self._build = self._split_parts(build) - - @property - def major(self) -> int: - return self._major - - @property - def minor(self) -> int: - return self._minor - - @property - def patch(self) -> int: - return self._patch - - @property - def rest(self) -> int: - return self._rest - - @property - def prerelease(self) -> List[str]: - return self._prerelease - - @property - def build(self) -> List[str]: - return self._build - - @property - def text(self) -> str: - return self._text - @property def precision(self) -> int: - return self._precision + return self.release.precision @property def stable(self) -> "Version": - if not self.is_prerelease(): + if self.is_stable(): return self - return self.next_patch - - @property - def next_major(self) -> "Version": - if self.is_prerelease() and self.minor == 0 and self.patch == 0: - return Version(self.major, self.minor, self.patch) - - return self._increment_major() - - @property - def next_minor(self) -> "Version": - if self.is_prerelease() and self.patch == 0: - return Version(self.major, self.minor, self.patch) - - return self._increment_minor() - - @property - def next_patch(self) -> "Version": - if self.is_prerelease(): - return Version(self.major, self.minor, self.patch) - - return self._increment_patch() + return self.next_patch() - @property def next_breaking(self) -> "Version": if self.major == 0: if self.minor != 0: - return self._increment_minor() + return self.next_minor() - if self._precision == 1: - return self._increment_major() - elif self._precision == 2: - return self._increment_minor() + if self.precision == 1: + return self.next_major() + elif self.precision == 2: + return self.next_minor() - return self._increment_patch() + return self.next_patch() - return self._increment_major() + return self.next_major() - @property - def first_prerelease(self) -> "Version": - return Version.parse( - "{}.{}.{}-alpha.0".format(self.major, self.minor, self.patch) - ) + def first_pre_release(self) -> "Version": + return self.__class__(release=self.release, pre=ReleaseTag("alpha")) @property def min(self) -> "Version": @@ -200,40 +73,12 @@ def include_min(self) -> bool: def include_max(self) -> bool: return True - @classmethod - def parse(cls, text: str) -> "Version": - try: - match = COMPLETE_VERSION.match(text) - except TypeError: - match = None - - if match is None: - raise ParseVersionError('Unable to parse "{}".'.format(text)) - - text = text.rstrip(".") - - major = int(match.group(1)) - minor = int(match.group(2)) if match.group(2) else None - patch = int(match.group(3)) if match.group(3) else None - rest = int(match.group(4)) if match.group(4) else None - - pre = match.group(5) - build = match.group(6) - - if build: - build = build.lstrip("+") - - return Version(major, minor, patch, rest, pre, build, text) - def is_any(self) -> bool: return False def is_empty(self) -> bool: return False - def is_prerelease(self) -> bool: - return len(self._prerelease) > 0 - def allows(self, version: "Version") -> bool: return self == version @@ -250,12 +95,12 @@ def intersect(self, other: "VersionTypes") -> Union["Version", EmptyConstraint]: return EmptyConstraint() def union(self, other: "VersionTypes") -> "VersionTypes": - from .version_range import VersionRange + from poetry.core.semver.version_range import VersionRange if other.allows(self): return other - if isinstance(other, VersionRange): + if isinstance(other, VersionRangeConstraint): if other.min == self: return VersionRange( other.min, @@ -280,193 +125,39 @@ def difference(self, other: "VersionTypes") -> Union["Version", EmptyConstraint] return self - def equals_without_prerelease(self, other: "Version") -> bool: - return ( - self.major == other.major - and self.minor == other.minor - and self.patch == other.patch - ) - - def _increment_major(self) -> "Version": - return Version(self.major + 1, 0, 0, precision=self._precision) - - def _increment_minor(self) -> "Version": - return Version(self.major, self.minor + 1, 0, precision=self._precision) - - def _increment_patch(self) -> "Version": - return Version( - self.major, self.minor, self.patch + 1, precision=self._precision - ) - - def _normalize_prerelease(self, pre: str) -> Optional[str]: - if not pre: - return - - m = re.match(r"(?i)^(a|alpha|b|beta|c|pre|rc|dev)[-.]?(\d+)?$", pre) - if not m: - return - - modifier = m.group(1) - number = m.group(2) - - if number is None: - number = 0 - - if modifier == "a": - modifier = "alpha" - elif modifier == "b": - modifier = "beta" - elif modifier in {"c", "pre"}: - modifier = "rc" - elif modifier == "dev": - modifier = "alpha" - - return "{}.{}".format(modifier, number) - - def _normalize_build(self, build: str) -> Optional[str]: - if not build: - return - - if build.startswith("post"): - build = build.lstrip("post") - - if not build: - return - - return build - - def _split_parts(self, text: str) -> List[Union[str, int]]: - parts = text.split(".") - - for i, part in enumerate(parts): - try: - parts[i] = int(part) - except (TypeError, ValueError): - continue - - return parts - - def __lt__(self, other: "Version") -> int: - return self._cmp(other) < 0 - - def __le__(self, other: "Version") -> int: - return self._cmp(other) <= 0 - - def __gt__(self, other: "Version") -> int: - return self._cmp(other) > 0 - - def __ge__(self, other: "Version") -> int: - return self._cmp(other) >= 0 - - def _cmp(self, other: "Version") -> int: - if not isinstance(other, VersionConstraint): - return NotImplemented - - if not isinstance(other, Version): - return -other._cmp(self) - - if self.major != other.major: - return self._cmp_parts(self.major, other.major) - - if self.minor != other.minor: - return self._cmp_parts(self.minor, other.minor) - - if self.patch != other.patch: - return self._cmp_parts(self.patch, other.patch) - - if self.rest != other.rest: - return self._cmp_parts(self.rest, other.rest) - - # Pre-releases always come before no pre-release string. - if not self.is_prerelease() and other.is_prerelease(): - return 1 - - if not other.is_prerelease() and self.is_prerelease(): - return -1 - - comparison = self._cmp_lists(self.prerelease, other.prerelease) - if comparison != 0: - return comparison - - # Builds always come after no build string. - if not self.build and other.build: - return -1 - - if not other.build and self.build: - return 1 - - return self._cmp_lists(self.build, other.build) - - def _cmp_parts(self, a: Optional[int], b: Optional[int]) -> int: - if a < b: - return -1 - elif a > b: - return 1 - - return 0 - - def _cmp_lists(self, a: List, b: List) -> int: - for i in range(max(len(a), len(b))): - a_part = None - if i < len(a): - a_part = a[i] - - b_part = None - if i < len(b): - b_part = b[i] - - if a_part == b_part: - continue - - # Missing parts come after present ones. - if a_part is None: - return -1 - - if b_part is None: - return 1 - - if isinstance(a_part, int): - if isinstance(b_part, int): - return self._cmp_parts(a_part, b_part) - - return -1 - else: - if isinstance(b_part, int): - return 1 - - return self._cmp_parts(a_part, b_part) - - return 0 - - def __eq__(self, other: "Version") -> bool: - if not isinstance(other, Version): - return NotImplemented - - return ( - self._major == other.major - and self._minor == other.minor - and self._patch == other.patch - and self._rest == other.rest - and self._prerelease == other.prerelease - and self._build == other.build - ) - - def __ne__(self, other: "VersionTypes") -> bool: - return not self == other - def __str__(self) -> str: - return self._text + return self.text def __repr__(self) -> str: return "".format(str(self)) - def __hash__(self) -> int: - return hash( - ( - self.major, - self.minor, - self.patch, - ".".join(str(p) for p in self.prerelease), - ".".join(str(p) for p in self.build), + def __eq__(self, other: Union["Version", "VersionRange"]) -> bool: + from poetry.core.semver.version_range import VersionRange + + if isinstance(other, VersionRange): + return ( + self == other.min + and self == other.max + and (other.include_min or other.include_max) ) + return super().__eq__(other) + + @classmethod + def from_parts( + cls, + major: int, + minor: Optional[int] = None, + patch: Optional[int] = None, + extra: Optional[Union[int, Tuple[int, ...]]] = None, + pre: Optional[ReleaseTag] = None, + post: Optional[ReleaseTag] = None, + dev: Optional[ReleaseTag] = None, + local: "LocalSegmentType" = None, + ): + return cls( + release=Release(major=major, minor=minor, patch=patch, extra=extra), + pre=pre, + post=post, + dev=dev, + local=local, ) diff --git a/poetry/core/semver/version_constraint.py b/poetry/core/semver/version_constraint.py index 127878779..4dcf7f1dd 100644 --- a/poetry/core/semver/version_constraint.py +++ b/poetry/core/semver/version_constraint.py @@ -1,31 +1,40 @@ +from abc import abstractmethod from typing import TYPE_CHECKING if TYPE_CHECKING: - from poetry.core.semver.version import Version # noqa + from poetry.core.semver.version import Version class VersionConstraint: + @abstractmethod def is_empty(self) -> bool: raise NotImplementedError() + @abstractmethod def is_any(self) -> bool: raise NotImplementedError() + @abstractmethod def allows(self, version: "Version") -> bool: raise NotImplementedError() + @abstractmethod def allows_all(self, other: "VersionConstraint") -> bool: raise NotImplementedError() + @abstractmethod def allows_any(self, other: "VersionConstraint") -> bool: raise NotImplementedError() + @abstractmethod def intersect(self, other: "VersionConstraint") -> "VersionConstraint": raise NotImplementedError() + @abstractmethod def union(self, other: "VersionConstraint") -> "VersionConstraint": raise NotImplementedError() + @abstractmethod def difference(self, other: "VersionConstraint") -> "VersionConstraint": raise NotImplementedError() diff --git a/poetry/core/semver/version_range.py b/poetry/core/semver/version_range.py index e07904f04..6b77e58d1 100644 --- a/poetry/core/semver/version_range.py +++ b/poetry/core/semver/version_range.py @@ -3,18 +3,17 @@ from typing import List from typing import Optional -from .empty_constraint import EmptyConstraint -from .version_constraint import VersionConstraint -from .version_union import VersionUnion +from poetry.core.semver.empty_constraint import EmptyConstraint +from poetry.core.semver.version_range_constraint import VersionRangeConstraint +from poetry.core.semver.version_union import VersionUnion if TYPE_CHECKING: + from poetry.core.semver.helpers import VersionTypes from poetry.core.semver.version import Version - from . import VersionTypes # noqa - -class VersionRange(VersionConstraint): +class VersionRange(VersionRangeConstraint): def __init__( self, min: Optional["Version"] = None, @@ -28,15 +27,11 @@ def __init__( not always_include_max_prerelease and not include_max and full_max is not None - and not full_max.is_prerelease() - and not full_max.build - and ( - min is None - or not min.is_prerelease() - or not min.equals_without_prerelease(full_max) - ) + and full_max.is_stable() + and not full_max.is_postrelease() + and (min is None or min.is_stable() or min.release != full_max.release) ): - full_max = full_max.first_prerelease + full_max = full_max.first_pre_release() self._min = min self._max = max @@ -320,62 +315,6 @@ def difference(self, other: "VersionTypes") -> "VersionTypes": raise ValueError("Unknown VersionConstraint type {}.".format(other)) - def allows_lower(self, other: "VersionRange") -> bool: - if self.min is None: - return other.min is not None - - if other.min is None: - return False - - if self.min < other.min: - return True - - if self.min > other.min: - return False - - return self.include_min and not other.include_min - - def allows_higher(self, other: "VersionRange") -> bool: - if self.full_max is None: - return other.max is not None - - if other.full_max is None: - return False - - if self.full_max < other.full_max: - return False - - if self.full_max > other.full_max: - return True - - return self.include_max and not other.include_max - - def is_strictly_lower(self, other: "VersionRange") -> bool: - if self.full_max is None or other.min is None: - return False - - if self.full_max < other.min: - return True - - if self.full_max > other.min: - return False - - return not self.include_max or not other.include_min - - def is_strictly_higher(self, other: "VersionRange") -> bool: - return other.is_strictly_lower(self) - - def is_adjacent_to(self, other: "VersionRange") -> bool: - if self.max != other.min: - return False - - return ( - self.include_max - and not other.include_min - or not self.include_max - and other.include_min - ) - def __eq__(self, other: Any) -> int: if not isinstance(other, VersionRange): return False @@ -408,16 +347,17 @@ def _cmp(self, other: "VersionRange") -> int: elif other.min is None: return 1 - result = self.min._cmp(other.min) - if result != 0: - return result + if self.min > other.min: + return 1 + elif self.min < other.min: + return -1 if self.include_min != other.include_min: return -1 if self.include_min else 1 return self._compare_max(other) - def _compare_max(self, other: "VersionRange") -> int: + def _compare_max(self, other: "VersionRangeConstraint") -> int: if self.max is None: if other.max is None: return 0 @@ -426,9 +366,10 @@ def _compare_max(self, other: "VersionRange") -> int: elif other.max is None: return -1 - result = self.max._cmp(other.max) - if result != 0: - return result + if self.max > other.max: + return 1 + elif self.max < other.max: + return -1 if self.include_max != other.include_max: return 1 if self.include_max else -1 diff --git a/poetry/core/semver/version_range_constraint.py b/poetry/core/semver/version_range_constraint.py new file mode 100644 index 000000000..30c8c2a09 --- /dev/null +++ b/poetry/core/semver/version_range_constraint.py @@ -0,0 +1,91 @@ +from abc import abstractmethod +from typing import TYPE_CHECKING + +from poetry.core.semver.version_constraint import VersionConstraint + + +if TYPE_CHECKING: + from poetry.core.semver.version import Version + + +class VersionRangeConstraint(VersionConstraint): + @property + @abstractmethod + def min(self) -> "Version": + raise NotImplementedError() + + @property + @abstractmethod + def max(self) -> "Version": + raise NotImplementedError() + + @property + @abstractmethod + def full_max(self) -> "Version": + raise NotImplementedError() + + @property + @abstractmethod + def include_min(self) -> bool: + raise NotImplementedError() + + @property + @abstractmethod + def include_max(self) -> bool: + raise NotImplementedError() + + def allows_lower(self, other: "VersionRangeConstraint") -> bool: + if self.min is None: + return other.min is not None + + if other.min is None: + return False + + if self.min < other.min: + return True + + if self.min > other.min: + return False + + return self.include_min and not other.include_min + + def allows_higher(self, other: "VersionRangeConstraint") -> bool: + if self.full_max is None: + return other.max is not None + + if other.full_max is None: + return False + + if self.full_max < other.full_max: + return False + + if self.full_max > other.full_max: + return True + + return self.include_max and not other.include_max + + def is_strictly_lower(self, other: "VersionRangeConstraint") -> bool: + if self.full_max is None or other.min is None: + return False + + if self.full_max < other.min: + return True + + if self.full_max > other.min: + return False + + return not self.include_max or not other.include_min + + def is_strictly_higher(self, other: "VersionRangeConstraint") -> bool: + return other.is_strictly_lower(self) + + def is_adjacent_to(self, other: "VersionRangeConstraint") -> bool: + if self.max != other.min: + return False + + return ( + self.include_max + and not other.include_min + or not self.include_max + and other.include_min + ) diff --git a/poetry/core/semver/version_union.py b/poetry/core/semver/version_union.py index b37e99e8f..42bae9981 100644 --- a/poetry/core/semver/version_union.py +++ b/poetry/core/semver/version_union.py @@ -4,12 +4,13 @@ from .empty_constraint import EmptyConstraint from .version_constraint import VersionConstraint +from .version_range_constraint import VersionRangeConstraint if TYPE_CHECKING: - from . import VersionTypes # noqa - from .version import Version - from .version_range import VersionRange + from poetry.core.semver.helpers import VersionTypes + from poetry.core.semver.version import Version + from poetry.core.semver.version_range import VersionRange class VersionUnion(VersionConstraint): @@ -53,7 +54,7 @@ def of(cls, *ranges: "VersionTypes") -> "VersionTypes": # about everything in flattened. _EmptyVersions and VersionUnions are # filtered out above. for constraint in flattened: - if isinstance(constraint, VersionRange): + if isinstance(constraint, VersionRangeConstraint): continue raise ValueError("Unknown VersionConstraint type {}.".format(constraint)) @@ -222,16 +223,14 @@ def our_next_range(include_current: bool = True) -> bool: return VersionUnion.of(*new_ranges) - def _ranges_for(self, constraint: "VersionTypes") -> List["VersionRange"]: - from .version_range import VersionRange - + def _ranges_for(self, constraint: "VersionTypes") -> List["VersionRangeConstraint"]: if constraint.is_empty(): return [] if isinstance(constraint, VersionUnion): return constraint.ranges - if isinstance(constraint, VersionRange): + if isinstance(constraint, VersionRangeConstraint): return [constraint] raise ValueError("Unknown VersionConstraint type {}".format(constraint)) diff --git a/poetry/core/utils/helpers.py b/poetry/core/utils/helpers.py index 22ccff21f..9eb47e6fd 100644 --- a/poetry/core/utils/helpers.py +++ b/poetry/core/utils/helpers.py @@ -11,7 +11,7 @@ from typing import List from typing import Union -from poetry.core.version import Version +from poetry.core.version.pep440 import PEP440Version try: @@ -32,7 +32,7 @@ def module_name(name: str) -> str: def normalize_version(version: str) -> str: - return str(Version(version)) + return PEP440Version.parse(version).to_string(short=True) @contextmanager diff --git a/poetry/core/version/__init__.py b/poetry/core/version/__init__.py index 3b106577e..e69de29bb 100644 --- a/poetry/core/version/__init__.py +++ b/poetry/core/version/__init__.py @@ -1,45 +0,0 @@ -import operator - -from typing import Union - -from .exceptions import InvalidVersion -from .legacy_version import LegacyVersion -from .version import Version - - -OP_EQ = operator.eq -OP_LT = operator.lt -OP_LE = operator.le -OP_GT = operator.gt -OP_GE = operator.ge -OP_NE = operator.ne - -_trans_op = { - "=": OP_EQ, - "==": OP_EQ, - "<": OP_LT, - "<=": OP_LE, - ">": OP_GT, - ">=": OP_GE, - "!=": OP_NE, -} - - -def parse( - version: str, - strict: bool = False, -) -> Union[Version, LegacyVersion]: - """ - Parse the given version string and return either a :class:`Version` object - or a LegacyVersion object depending on if the given version is - a valid PEP 440 version or a legacy version. - - If strict=True only PEP 440 versions will be accepted. - """ - try: - return Version(version) - except InvalidVersion: - if strict: - raise - - return LegacyVersion(version) diff --git a/poetry/core/version/base.py b/poetry/core/version/base.py deleted file mode 100644 index 78197da90..000000000 --- a/poetry/core/version/base.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Callable - - -class BaseVersion: - def __init__(self, version: str) -> None: - self._version = str(version) - self._key = None - - def __hash__(self) -> int: - return hash(self._key) - - def __lt__(self, other: "BaseVersion") -> bool: - return self._compare(other, lambda s, o: s < o) - - def __le__(self, other: "BaseVersion") -> bool: - return self._compare(other, lambda s, o: s <= o) - - def __eq__(self, other: "BaseVersion") -> bool: - return self._compare(other, lambda s, o: s == o) - - def __ge__(self, other: "BaseVersion") -> bool: - return self._compare(other, lambda s, o: s >= o) - - def __gt__(self, other: "BaseVersion") -> bool: - return self._compare(other, lambda s, o: s > o) - - def __ne__(self, other: "BaseVersion") -> bool: - return self._compare(other, lambda s, o: s != o) - - def _compare(self, other: "BaseVersion", method: Callable) -> bool: - if not isinstance(other, BaseVersion): - return NotImplemented - - return method(self._key, other._key) diff --git a/poetry/core/version/grammars/parser.py b/poetry/core/version/grammars/parser.py index 13b8d76e3..2ccf244d0 100644 --- a/poetry/core/version/grammars/parser.py +++ b/poetry/core/version/grammars/parser.py @@ -1,26 +1,20 @@ -import os +from pathlib import Path -from typing import TYPE_CHECKING -from typing import Optional +from lark import Lark -if TYPE_CHECKING: - from lark import Lark # noqa - from lark import Tree # noqa +GRAMMAR_DIR = Path(__file__).parent +# Parser: PEP 440 +# we use earley because the grammar is ambiguous +PARSER_PEP_440 = Lark.open(GRAMMAR_DIR / "pep440.lark", parser="earley", debug=False) -class Parser: - def __init__(self, grammar: str) -> None: - self._grammar = grammar - self._parser: Optional["Lark"] = None +# Parser: PEP 508 Constraints +PARSER_PEP_508_CONSTRAINTS = Lark.open( + GRAMMAR_DIR / "pep508.lark", parser="lalr", debug=False +) - def parse(self, string: str) -> "Tree": - from lark import Lark - - if self._parser is None: - self._parser = Lark.open( - os.path.join(os.path.dirname(__file__), f"{self._grammar}.lark"), - parser="lalr", - ) - - return self._parser.parse(string) +# Parser: PEP 508 Environment Markers +PARSER_PEP_508_MARKERS = Lark.open( + GRAMMAR_DIR / "markers.lark", parser="lalr", debug=False +) diff --git a/poetry/core/version/grammars/pep440.lark b/poetry/core/version/grammars/pep440.lark new file mode 100644 index 000000000..62749f3b7 --- /dev/null +++ b/poetry/core/version/grammars/pep440.lark @@ -0,0 +1,32 @@ +// this is a modified version of the semver 2.0 specification grammar, specificially +// crafted for use with Python PEP 440 version specifiers. +start: version + +version: "v"? epoch? release pre_release? post_release? dev_release? ("+" local)? +release: epoch? NUMERIC_IDENTIFIER (("." NUMERIC_IDENTIFIER)+)? + +major: NUMERIC_IDENTIFIER +minor: NUMERIC_IDENTIFIER +patch: NUMERIC_IDENTIFIER + +epoch: INT "!" + +pre_release: _SEPERATOR? PRE_RELEASE_TAG _SEPERATOR? NUMERIC_IDENTIFIER? +PRE_RELEASE_TAG: "a" "lpha"? | "b" "eta"? | "c" | "rc" | "pre" "view"? + +post_release: "-" NUMERIC_IDENTIFIER | _SEPERATOR? POST_RELEASE_TAG _SEPERATOR? NUMERIC_IDENTIFIER? +POST_RELEASE_TAG: "post" | "r" "ev"? + +dev_release: _SEPERATOR? DEV_RELEASE_TAG _SEPERATOR? NUMERIC_IDENTIFIER? +DEV_RELEASE_TAG: "dev" + +local: LOCAL_IDENTIFIER ((_SEPERATOR LOCAL_IDENTIFIER)+)? +LOCAL_IDENTIFIER: (LETTER | INT)+ + +NUMERIC_IDENTIFIER: INT + +_SEPERATOR: "-" | "." | "_" + +%import common.LETTER +%import common.DIGIT +%import common.INT diff --git a/poetry/core/version/legacy_version.py b/poetry/core/version/legacy_version.py deleted file mode 100644 index 49b62aeb3..000000000 --- a/poetry/core/version/legacy_version.py +++ /dev/null @@ -1,92 +0,0 @@ -import re - -from typing import Tuple - -from .base import BaseVersion - - -class LegacyVersion(BaseVersion): - def __init__(self, version: str) -> None: - self._version = str(version) - self._key = _legacy_cmpkey(self._version) - - def __str__(self) -> str: - return self._version - - def __repr__(self) -> str: - return "".format(repr(str(self))) - - @property - def public(self) -> str: - return self._version - - @property - def base_version(self) -> str: - return self._version - - @property - def local(self) -> None: - return None - - @property - def is_prerelease(self) -> bool: - return False - - @property - def is_postrelease(self) -> bool: - return False - - -_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE) - -_legacy_version_replacement_map = { - "pre": "c", - "preview": "c", - "-": "final-", - "rc": "c", - "dev": "@", -} - - -def _parse_version_parts(s: str) -> str: - for part in _legacy_version_component_re.split(s): - part = _legacy_version_replacement_map.get(part, part) - - if not part or part == ".": - continue - - if part[:1] in "0123456789": - # pad for numeric comparison - yield part.zfill(8) - else: - yield "*" + part - - # ensure that alpha/beta/candidate are before final - yield "*final" - - -def _legacy_cmpkey(version: str) -> Tuple[int, Tuple[str]]: - # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch - # greater than or equal to 0. This will effectively put the LegacyVersion, - # which uses the defacto standard originally implemented by setuptools, - # as before all PEP 440 versions. - epoch = -1 - - # This scheme is taken from pkg_resources.parse_version setuptools prior to - # it's adoption of the packaging library. - parts = [] - for part in _parse_version_parts(version.lower()): - if part.startswith("*"): - # remove "-" before a prerelease tag - if part < "*final": - while parts and parts[-1] == "*final-": - parts.pop() - - # remove trailing zeros from each series of numeric parts - while parts and parts[-1] == "00000000": - parts.pop() - - parts.append(part) - parts = tuple(parts) - - return epoch, parts diff --git a/poetry/core/version/markers.py b/poetry/core/version/markers.py index af7d86e46..7a05e42e5 100644 --- a/poetry/core/version/markers.py +++ b/poetry/core/version/markers.py @@ -7,7 +7,7 @@ from typing import List from typing import Union -from .grammars.parser import Parser +from .grammars.parser import PARSER_PEP_508_MARKERS if TYPE_CHECKING: @@ -49,7 +49,7 @@ class UndefinedEnvironmentName(ValueError): } -_parser = Parser("markers") +_parser = PARSER_PEP_508_MARKERS class BaseMarker(object): diff --git a/poetry/core/version/pep440/__init__.py b/poetry/core/version/pep440/__init__.py new file mode 100644 index 000000000..dff35f255 --- /dev/null +++ b/poetry/core/version/pep440/__init__.py @@ -0,0 +1,4 @@ +from poetry.core.version.pep440.segments import LocalSegmentType +from poetry.core.version.pep440.segments import Release +from poetry.core.version.pep440.segments import ReleaseTag +from poetry.core.version.pep440.version import PEP440Version diff --git a/poetry/core/version/pep440/parser.py b/poetry/core/version/pep440/parser.py new file mode 100644 index 000000000..ea9eee308 --- /dev/null +++ b/poetry/core/version/pep440/parser.py @@ -0,0 +1,90 @@ +from typing import TYPE_CHECKING +from typing import List +from typing import Optional +from typing import Type + +from lark import LarkError +from lark import Transformer + +from poetry.core.version.exceptions import InvalidVersion +from poetry.core.version.grammars.parser import PARSER_PEP_440 +from poetry.core.version.pep440 import Release +from poetry.core.version.pep440 import ReleaseTag + + +if TYPE_CHECKING: + from poetry.core.version.pep440.version import PEP440Version + + +class _Transformer(Transformer): + def NUMERIC_IDENTIFIER(self, data: "Token"): # noqa + return int(data.value) + + def LOCAL_IDENTIFIER(self, data: "Token"): # noqa + try: + return int(data.value) + except ValueError: + return data.value + + def POST_RELEASE_TAG(self, data: "Token"): # noqa + return data.value + + def PRE_RELEASE_TAG(self, data: "Token"): # noqa + return data.value + + def DEV_RELEASE_TAG(self, data: "Token"): # noqa + return data.value + + def LOCAL(self, data: "Token"): # noqa + return data.value + + def INT(self, data: "Token"): # noqa + return int(data.value) + + def version(self, children: List["Tree"]): # noqa + epoch, release, dev, pre, post, local = 0, None, None, None, None, None + + for child in children: + if child.data == "epoch": + # epoch is always a single numeric value + epoch = child.children[0] + elif child.data == "release": + # release segment is of the form N(.N)* + release = Release.from_parts(*child.children) + elif child.data == "pre_release": + # pre-release tag is of the form (a|b|rc)N + pre = ReleaseTag(*child.children) + elif child.data == "post_release": + # post-release tags are of the form N (shortened) or post(N)* + if len(child.children) == 1 and isinstance(child.children[0], int): + post = ReleaseTag("post", child.children[0]) + else: + post = ReleaseTag(*child.children) + elif child.data == "dev_release": + # dev-release tag is of the form dev(N)* + dev = ReleaseTag(*child.children) + elif child.data == "local": + local = tuple(child.children) + + return epoch, release, pre, post, dev, local + + def start(self, children: List["Tree"]): # noqa + return children[0] + + +_TRANSFORMER = _Transformer() + + +def parse_pep440( + value: str, version_class: Optional[Type["PEP440Version"]] = None +) -> "PEP440Version": + if version_class is None: + from poetry.core.version.pep440.version import PEP440Version + + version_class = PEP440Version + + try: + tree = PARSER_PEP_440.parse(text=value) + return version_class(*_TRANSFORMER.transform(tree), text=value) + except (TypeError, LarkError): + raise InvalidVersion(f"Invalid PEP 440 version: '{value}'") diff --git a/poetry/core/version/pep440/segments.py b/poetry/core/version/pep440/segments.py new file mode 100644 index 000000000..cbc7ca094 --- /dev/null +++ b/poetry/core/version/pep440/segments.py @@ -0,0 +1,151 @@ +import dataclasses + +from typing import Optional +from typing import Tuple +from typing import Union + + +RELEASE_PHASE_ALPHA = "alpha" +RELEASE_PHASE_BETA = "beta" +RELEASE_PHASE_RC = "rc" +RELEASE_PHASE_PREVIEW = "preview" +RELEASE_PHASE_POST = "post" +RELEASE_PHASE_REV = "rev" +RELEASE_PHASE_DEV = "dev" +RELEASE_PHASES = { + RELEASE_PHASE_ALPHA: "a", + RELEASE_PHASE_BETA: "b", + RELEASE_PHASE_RC: "c", + RELEASE_PHASE_PREVIEW: "pre", + RELEASE_PHASE_POST: "-", # shorthand of 1.2.3-post1 is 1.2.3-1 + RELEASE_PHASE_REV: "r", + RELEASE_PHASE_DEV: "dev", +} +RELEASE_PHASES_SHORT = {v: k for k, v in RELEASE_PHASES.items() if k != "post"} + + +@dataclasses.dataclass(frozen=True, eq=True, order=True) +class Release: + major: int = dataclasses.field(default=0, compare=False) + minor: Optional[int] = dataclasses.field(default=None, compare=False) + patch: Optional[int] = dataclasses.field(default=None, compare=False) + # some projects use non-semver versioning schemes, eg: 1.2.3.4 + extra: Optional[Union[int, Tuple[int, ...]]] = dataclasses.field( + default=None, compare=False + ) + precision: int = dataclasses.field(default=None, init=False, compare=False) + text: str = dataclasses.field(default=None, init=False, compare=False) + _compare_key: Tuple[int, ...] = dataclasses.field( + default=None, init=False, compare=True + ) + + def __post_init__(self): + if self.extra is None: + object.__setattr__(self, "extra", tuple()) + elif not isinstance(self.extra, tuple): + object.__setattr__(self, "extra", (self.extra,)) + + parts = list( + map( + str, + filter( + lambda x: x is not None, + [self.major, self.minor, self.patch, *self.extra], + ), + ) + ) + object.__setattr__(self, "text", ".".join(parts)) + object.__setattr__(self, "precision", len(parts)) + object.__setattr__( + self, + "_compare_key", + (self.major, self.minor or 0, self.patch or 0, *self.extra), + ) + + @classmethod + def from_parts(cls, *parts: int) -> "Release": + if not parts: + return cls() + + return cls( + major=parts[0], + minor=parts[1] if len(parts) > 1 else None, + patch=parts[2] if len(parts) > 2 else None, + extra=parts[3:] if len(parts) > 3 else tuple(), + ) + + def to_string(self) -> str: + return self.text + + def next_major(self) -> "Release": + return dataclasses.replace( + self, + major=self.major + 1, + minor=0 if self.minor is not None else None, + patch=0 if self.patch is not None else None, + extra=tuple(0 for _ in self.extra), + ) + + def next_minor(self) -> "Release": + return dataclasses.replace( + self, + major=self.major, + minor=self.minor + 1 if self.minor is not None else 1, + patch=0 if self.patch is not None else None, + extra=tuple(0 for _ in self.extra), + ) + + def next_patch(self) -> "Release": + return dataclasses.replace( + self, + major=self.major, + minor=self.minor if self.minor is not None else 0, + patch=self.patch + 1 if self.patch is not None else 1, + extra=tuple(0 for _ in self.extra), + ) + + +@dataclasses.dataclass(frozen=True, eq=True, order=True) +class ReleaseTag: + phase: str + number: int = dataclasses.field(default=0) + + def __post_init__(self): + object.__setattr__(self, "phase", self.expand(self.phase)) + + @classmethod + def shorten(cls, phase: str) -> str: + return RELEASE_PHASES.get(phase, phase) + + @classmethod + def expand(cls, phase: str) -> str: + return RELEASE_PHASES_SHORT.get(phase, phase) + + def to_string(self, short: bool = False) -> str: + if short: + return f"{self.shorten(self.phase)}{self.number}" + return f"{self.phase}.{self.number}" + + def next(self) -> "ReleaseTag": + return dataclasses.replace(self, phase=self.phase, number=self.number + 1) + + def next_phase(self) -> Optional["ReleaseTag"]: + if self.phase in [ + RELEASE_PHASE_POST, + RELEASE_PHASE_RC, + RELEASE_PHASE_REV, + RELEASE_PHASE_DEV, + ]: + return None + + if self.phase == RELEASE_PHASE_ALPHA: + _phase = RELEASE_PHASE_BETA + elif self.phase == RELEASE_PHASE_BETA: + _phase = RELEASE_PHASE_RC + else: + return None + + return self.__class__(phase=_phase, number=0) + + +LocalSegmentType = Optional[Union[str, int, Tuple[Union[str, int], ...]]] diff --git a/poetry/core/version/pep440/version.py b/poetry/core/version/pep440/version.py new file mode 100644 index 000000000..e84a87a72 --- /dev/null +++ b/poetry/core/version/pep440/version.py @@ -0,0 +1,202 @@ +import dataclasses +import math + +from typing import Optional +from typing import Tuple +from typing import Union + +from poetry.core.version.pep440.segments import RELEASE_PHASE_ALPHA +from poetry.core.version.pep440.segments import RELEASE_PHASE_DEV +from poetry.core.version.pep440.segments import RELEASE_PHASE_POST +from poetry.core.version.pep440.segments import LocalSegmentType +from poetry.core.version.pep440.segments import Release +from poetry.core.version.pep440.segments import ReleaseTag + + +# we use the phase "z" to ensure we always sort this after other phases +_INF_TAG = ReleaseTag("z", math.inf) # noqa +# we use the phase "" to ensure we always sort this before other phases +_NEG_INF_TAG = ReleaseTag("", -math.inf) # noqa + + +@dataclasses.dataclass(frozen=True, eq=True, order=True) +class PEP440Version: + epoch: int = dataclasses.field(default=0, compare=False) + release: Release = dataclasses.field(default_factory=Release, compare=False) + pre: Optional[ReleaseTag] = dataclasses.field(default=None, compare=False) + post: Optional[ReleaseTag] = dataclasses.field(default=None, compare=False) + dev: Optional[ReleaseTag] = dataclasses.field(default=None, compare=False) + local: LocalSegmentType = dataclasses.field(default=None, compare=False) + text: str = dataclasses.field(default=None, compare=False) + _compare_key: Tuple[ + int, Release, ReleaseTag, ReleaseTag, ReleaseTag, Tuple[Union[int, str], ...] + ] = dataclasses.field(default=None, init=False, compare=True) + + def __post_init__(self): + if self.local is not None and not isinstance(self.local, tuple): + object.__setattr__(self, "local", (self.local,)) + + # we do this here to handle both None and tomlkit string values + object.__setattr__( + self, "text", self.to_string() if not self.text else str(self.text) + ) + + object.__setattr__(self, "_compare_key", self._make_compare_key()) + + def _make_compare_key(self): + """ + This code is based on the implementation of packaging.version._cmpkey(..) + """ + # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. + # We'll do this by abusing the pre segment, but we _only_ want to do this + # if there is not a pre or a post segment. If we have one of those then + # the normal sorting rules will handle this case correctly. + if self.pre is None and self.post is None and self.dev is not None: + _pre = _NEG_INF_TAG + # Versions without a pre-release (except as noted above) should sort after + # those with one. + elif self.pre is None: + _pre = _INF_TAG + else: + _pre = self.pre + + # Versions without a post segment should sort before those with one. + _post = _NEG_INF_TAG if self.post is None else self.post + + # Versions without a development segment should sort after those with one. + _dev = _INF_TAG if self.dev is None else self.dev + + if self.local is None: + # Versions without a local segment should sort before those with one. + _local = ((-math.inf, ""),) + else: + # Versions with a local segment need that segment parsed to implement + # the sorting rules in PEP440. + # - Alpha numeric segments sort before numeric segments + # - Alpha numeric segments sort lexicographically + # - Numeric segments sort numerically + # - Shorter versions sort before longer versions when the prefixes + # match exactly + _local = tuple( + (i, "") if isinstance(i, int) else (-math.inf, i) for i in self.local + ) + return self.epoch, self.release, _pre, _post, _dev, _local + + @property + def major(self) -> int: + return self.release.major + + @property + def minor(self) -> Optional[int]: + return self.release.minor + + @property + def patch(self) -> Optional[int]: + return self.release.patch + + @property + def non_semver_parts(self) -> Optional[Tuple[int]]: + return self.release.extra + + def to_string(self, short=False): + dash = "-" if not short else "" + + version_string = dash.join( + filter( + bool, + [ + self.release.to_string(), + self.pre.to_string(short) if self.pre else self.pre, + self.post.to_string(short) if self.post else self.post, + self.dev.to_string(short) if self.dev else self.dev, + ], + ) + ) + + if self.epoch: + # if epoch is non-zero we should include it + version_string = "{}!{}".format(self.epoch, version_string) + + if self.local: + version_string += "+{}".format(".".join(map(str, self.local))) + + return version_string + + @classmethod + def parse(cls, value: str) -> "PEP440Version": + from poetry.core.version.pep440.parser import parse_pep440 + + return parse_pep440(value, cls) + + def is_prerelease(self) -> bool: + return self.pre is not None + + def is_postrelease(self) -> bool: + return self.post is not None + + def is_devrelease(self) -> bool: + return self.dev is not None + + def is_no_suffix_release(self) -> bool: + return not (self.pre or self.post or self.dev) + + def is_unstable(self) -> bool: + return self.is_prerelease() or self.is_devrelease() + + def is_stable(self) -> bool: + return not self.is_unstable() + + def next_major(self) -> "PEP440Version": + release = self.release + if self.is_stable() or Release(self.release.major, 0, 0) < self.release: + release = self.release.next_major() + return self.__class__(epoch=self.epoch, release=release) + + def next_minor(self) -> "PEP440Version": + release = self.release + if ( + self.is_stable() + or Release(self.release.major, self.release.minor, 0) < self.release + ): + release = self.release.next_minor() + return self.__class__(epoch=self.epoch, release=release) + + def next_patch(self) -> "PEP440Version": + return self.__class__( + epoch=self.epoch, + release=self.release.next_patch() if self.is_stable() else self.release, + ) + + def next_prerelease(self, next_phase: bool = False) -> "PEP440Version": + if self.is_prerelease(): + pre = self.pre.next_phase() if next_phase else self.pre.next() + else: + pre = ReleaseTag(RELEASE_PHASE_ALPHA) + return self.__class__(epoch=self.epoch, release=self.release, pre=pre) + + def next_postrelease(self) -> "PEP440Version": + if self.is_prerelease(): + post = self.post.next() + else: + post = ReleaseTag(RELEASE_PHASE_POST) + return self.__class__( + epoch=self.epoch, + release=self.release, + pre=self.pre, + dev=self.dev, + post=post, + ) + + def next_devrelease(self) -> "PEP440Version": + if self.is_prerelease(): + dev = self.dev.next() + else: + dev = ReleaseTag(RELEASE_PHASE_DEV) + return self.__class__( + epoch=self.epoch, release=self.release, pre=self.pre, dev=dev + ) + + def first_prerelease(self) -> "PEP440Version": + return self.__class__( + epoch=self.epoch, release=self.release, pre=ReleaseTag(RELEASE_PHASE_ALPHA) + ) diff --git a/poetry/core/version/requirements.py b/poetry/core/version/requirements.py index 1016bf612..2d6d1ce96 100644 --- a/poetry/core/version/requirements.py +++ b/poetry/core/version/requirements.py @@ -3,7 +3,7 @@ from poetry.core.semver.exceptions import ParseConstraintError from poetry.core.semver.helpers import parse_constraint -from .grammars.parser import Parser +from .grammars.parser import PARSER_PEP_508_CONSTRAINTS from .markers import _compact_markers @@ -13,7 +13,7 @@ class InvalidRequirement(ValueError): """ -_parser = Parser("pep508") +_parser = PARSER_PEP_508_CONSTRAINTS class Requirement(object): diff --git a/poetry/core/version/utils.py b/poetry/core/version/utils.py deleted file mode 100644 index 761fe3d36..000000000 --- a/poetry/core/version/utils.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import Any - - -class Infinity(object): - def __repr__(self) -> str: - return "Infinity" - - def __hash__(self) -> int: - return hash(repr(self)) - - def __lt__(self, other: Any) -> bool: - return False - - def __le__(self, other: Any) -> bool: - return False - - def __eq__(self, other: Any) -> bool: - return isinstance(other, self.__class__) - - def __ne__(self, other: Any) -> bool: - return not isinstance(other, self.__class__) - - def __gt__(self, other: Any) -> bool: - return True - - def __ge__(self, other: Any) -> bool: - return True - - def __neg__(self) -> "NegativeInfinity": - return NegativeInfinity - - -Infinity = Infinity() # type: ignore - - -class NegativeInfinity(object): - def __repr__(self) -> str: - return "-Infinity" - - def __hash__(self) -> int: - return hash(repr(self)) - - def __lt__(self, other: Any) -> bool: - return True - - def __le__(self, other: Any) -> bool: - return True - - def __eq__(self, other: Any) -> bool: - return isinstance(other, self.__class__) - - def __ne__(self, other: Any) -> bool: - return not isinstance(other, self.__class__) - - def __gt__(self, other: Any) -> bool: - return False - - def __ge__(self, other: Any) -> bool: - return False - - def __neg__(self) -> Infinity: - return Infinity - - -NegativeInfinity = NegativeInfinity() # type: ignore diff --git a/poetry/core/version/version.py b/poetry/core/version/version.py deleted file mode 100644 index bbe38a406..000000000 --- a/poetry/core/version/version.py +++ /dev/null @@ -1,258 +0,0 @@ -import re - -from collections import namedtuple -from itertools import dropwhile -from typing import Any -from typing import Optional -from typing import Tuple -from typing import Type -from typing import Union - -from .base import BaseVersion -from .exceptions import InvalidVersion -from .utils import Infinity -from .utils import NegativeInfinity - - -_Version = namedtuple("_Version", ["epoch", "release", "dev", "pre", "post", "local"]) - - -VERSION_PATTERN = re.compile( - r""" - ^ - v? - (?: - (?:(?P[0-9]+)!)? # epoch - (?P[0-9]+(?:\.[0-9]+)*) # release segment - (?P
                                          # pre-release
-            [-_.]?
-            (?P(a|b|c|rc|alpha|beta|pre|preview))
-            [-_.]?
-            (?P[0-9]+)?
-        )?
-        (?P                                         # post release
-            (?:-(?P[0-9]+))
-            |
-            (?:
-                [-_.]?
-                (?Ppost|rev|r)
-                [-_.]?
-                (?P[0-9]+)?
-            )
-        )?
-        (?P                                          # dev release
-            [-_.]?
-            (?Pdev)
-            [-_.]?
-            (?P[0-9]+)?
-        )?
-    )
-    (?:\+(?P[a-z0-9]+(?:[-_.][a-z0-9]+)*))?       # local version
-    $
-""",
-    re.IGNORECASE | re.VERBOSE,
-)
-
-
-class Version(BaseVersion):
-    def __init__(self, version: str) -> None:
-        # Validate the version and parse it into pieces
-        match = VERSION_PATTERN.match(version)
-        if not match:
-            raise InvalidVersion("Invalid version: '{0}'".format(version))
-
-        # Store the parsed out pieces of the version
-        self._version = _Version(
-            epoch=int(match.group("epoch")) if match.group("epoch") else 0,
-            release=tuple(int(i) for i in match.group("release").split(".")),
-            pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
-            post=_parse_letter_version(
-                match.group("post_l"), match.group("post_n1") or match.group("post_n2")
-            ),
-            dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
-            local=_parse_local_version(match.group("local")),
-        )
-
-        # Generate a key which will be used for sorting
-        self._key = _cmpkey(
-            self._version.epoch,
-            self._version.release,
-            self._version.pre,
-            self._version.post,
-            self._version.dev,
-            self._version.local,
-        )
-
-    def __repr__(self) -> str:
-        return "".format(repr(str(self)))
-
-    def __str__(self) -> str:
-        parts = []
-
-        # Epoch
-        if self._version.epoch != 0:
-            parts.append("{0}!".format(self._version.epoch))
-
-        # Release segment
-        parts.append(".".join(str(x) for x in self._version.release))
-
-        # Pre-release
-        if self._version.pre is not None:
-            parts.append("".join(str(x) for x in self._version.pre))
-
-        # Post-release
-        if self._version.post is not None:
-            parts.append(".post{0}".format(self._version.post[1]))
-
-        # Development release
-        if self._version.dev is not None:
-            parts.append(".dev{0}".format(self._version.dev[1]))
-
-        # Local version segment
-        if self._version.local is not None:
-            parts.append("+{0}".format(".".join(str(x) for x in self._version.local)))
-
-        return "".join(parts)
-
-    @property
-    def public(self) -> str:
-        return str(self).split("+", 1)[0]
-
-    @property
-    def base_version(self) -> str:
-        parts = []
-
-        # Epoch
-        if self._version.epoch != 0:
-            parts.append("{0}!".format(self._version.epoch))
-
-        # Release segment
-        parts.append(".".join(str(x) for x in self._version.release))
-
-        return "".join(parts)
-
-    @property
-    def local(self) -> str:
-        version_string = str(self)
-        if "+" in version_string:
-            return version_string.split("+", 1)[1]
-
-    @property
-    def is_prerelease(self) -> bool:
-        return bool(self._version.dev or self._version.pre)
-
-    @property
-    def is_postrelease(self) -> bool:
-        return bool(self._version.post)
-
-
-def _parse_letter_version(letter: str, number: Optional[str]) -> Tuple[str, int]:
-    if letter:
-        # We consider there to be an implicit 0 in a pre-release if there is
-        # not a numeral associated with it.
-        if number is None:
-            number = 0
-
-        # We normalize any letters to their lower case form
-        letter = letter.lower()
-
-        # We consider some words to be alternate spellings of other words and
-        # in those cases we want to normalize the spellings to our preferred
-        # spelling.
-        if letter == "alpha":
-            letter = "a"
-        elif letter == "beta":
-            letter = "b"
-        elif letter in ["c", "pre", "preview"]:
-            letter = "rc"
-        elif letter in ["rev", "r"]:
-            letter = "post"
-
-        return letter, int(number)
-    if not letter and number:
-        # We assume if we are given a number, but we are not given a letter
-        # then this is using the implicit post release syntax (e.g. 1.0-1)
-        letter = "post"
-
-        return letter, int(number)
-
-
-_local_version_seperators = re.compile(r"[._-]")
-
-
-def _parse_local_version(local: Optional[str]) -> Tuple[Union[str, int], ...]:
-    """
-    Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
-    """
-    if local is not None:
-        return tuple(
-            part.lower() if not part.isdigit() else int(part)
-            for part in _local_version_seperators.split(local)
-        )
-
-
-def _cmpkey(
-    epoch: int,
-    release: Optional[Tuple[int, ...]],
-    pre: Optional[Tuple[str, int]],
-    post: Optional[Tuple[str, int]],
-    dev: Optional[Tuple[str, int]],
-    local: Optional[Tuple[Union[str, int], ...]],
-) -> Tuple[
-    int,
-    Tuple[int, ...],
-    Union[Infinity.__class__, NegativeInfinity.__class__, Tuple[str, int], Any],
-    Union[NegativeInfinity.__class__, Tuple[str, int]],
-    Union[Infinity.__class__, Tuple[str, int], Any],
-    Union[
-        NegativeInfinity.__class__,
-        Tuple[
-            Union[
-                Tuple[int, str],
-                Tuple[Type[NegativeInfinity.__class__], Union[str, int]],
-            ],
-            ...,
-        ],
-    ],
-]:
-    # When we compare a release version, we want to compare it with all of the
-    # trailing zeros removed. So we'll use a reverse the list, drop all the now
-    # leading zeros until we come to something non zero, then take the rest
-    # re-reverse it back into the correct order and make it a tuple and use
-    # that for our sorting key.
-    release = tuple(reversed(list(dropwhile(lambda x: x == 0, reversed(release)))))
-
-    # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
-    # We'll do this by abusing the pre segment, but we _only_ want to do this
-    # if there is not a pre or a post segment. If we have one of those then
-    # the normal sorting rules will handle this case correctly.
-    if pre is None and post is None and dev is not None:
-        pre = -Infinity
-
-    # Versions without a pre-release (except as noted above) should sort after
-    # those with one.
-    elif pre is None:
-        pre = Infinity
-
-    # Versions without a post segment should sort before those with one.
-    if post is None:
-        post = -Infinity
-
-    # Versions without a development segment should sort after those with one.
-    if dev is None:
-        dev = Infinity
-
-    if local is None:
-        # Versions without a local segment should sort before those with one.
-        local = -Infinity
-    else:
-        # Versions with a local segment need that segment parsed to implement
-        # the sorting rules in PEP440.
-        # - Alpha numeric segments sort before numeric segments
-        # - Alpha numeric segments sort lexicographically
-        # - Numeric segments sort numerically
-        # - Shorter versions sort before longer versions when the prefixes
-        #   match exactly
-        local = tuple((i, "") if isinstance(i, int) else (-Infinity, i) for i in local)
-
-    return epoch, release, pre, post, dev, local
diff --git a/pyproject.toml b/pyproject.toml
index 020ad8bed..45256ad1c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -34,6 +34,7 @@ python = "^3.6"
 
 # required for compatibility
 importlib-metadata = {version = "^1.7.0", python = ">=3.5, <3.8"}
+dataclasses = {version = "^0.8", python = "~3.6"}
 
 [tool.poetry.dev-dependencies]
 pre-commit = {version = "^2.10.0", python = "^3.6.1"}
@@ -108,6 +109,6 @@ appdirs = []
 pyrsistent = "https://raw.githubusercontent.com/tobgu/pyrsistent/master/LICENSE.mit"
 
 [build-system]
-requires = []
+requires = ["dataclasses>=0.6;python_version < '3.7'"]
 build-backend = "poetry.core.masonry.api"
 backend-path = ["."]
diff --git a/tests/semver/test_helpers.py b/tests/semver/test_helpers.py
index a9f193b1a..03a285735 100644
--- a/tests/semver/test_helpers.py
+++ b/tests/semver/test_helpers.py
@@ -4,6 +4,7 @@
 from poetry.core.semver.version import Version
 from poetry.core.semver.version_range import VersionRange
 from poetry.core.semver.version_union import VersionUnion
+from poetry.core.version.pep440 import ReleaseTag
 
 
 @pytest.mark.parametrize(
@@ -15,16 +16,19 @@
         ("*.x.*", VersionRange()),
         ("x.X.x.*", VersionRange()),
         # ('!=1.0.0', Constraint('!=', '1.0.0.0')),
-        (">1.0.0", VersionRange(min=Version(1, 0, 0))),
-        ("<1.2.3", VersionRange(max=Version(1, 2, 3))),
-        ("<=1.2.3", VersionRange(max=Version(1, 2, 3), include_max=True)),
-        (">=1.2.3", VersionRange(min=Version(1, 2, 3), include_min=True)),
-        ("=1.2.3", Version(1, 2, 3)),
-        ("1.2.3", Version(1, 2, 3)),
-        ("=1.0", Version(1, 0, 0)),
-        ("1.2.3b5", Version(1, 2, 3, pre="b5")),
-        (">= 1.2.3", VersionRange(min=Version(1, 2, 3), include_min=True)),
-        (">dev", VersionRange(min=Version(0, 0, pre="dev"))),  # Issue 206
+        (">1.0.0", VersionRange(min=Version.from_parts(1, 0, 0))),
+        ("<1.2.3", VersionRange(max=Version.from_parts(1, 2, 3))),
+        ("<=1.2.3", VersionRange(max=Version.from_parts(1, 2, 3), include_max=True)),
+        (">=1.2.3", VersionRange(min=Version.from_parts(1, 2, 3), include_min=True)),
+        ("=1.2.3", Version.from_parts(1, 2, 3)),
+        ("1.2.3", Version.from_parts(1, 2, 3)),
+        ("=1.0", Version.from_parts(1, 0, 0)),
+        ("1.2.3b5", Version.from_parts(1, 2, 3, pre=ReleaseTag("beta", 5))),
+        (">= 1.2.3", VersionRange(min=Version.from_parts(1, 2, 3), include_min=True)),
+        (
+            ">dev",
+            VersionRange(min=Version.from_parts(0, 0, dev=ReleaseTag("dev"))),
+        ),  # Issue 206
     ],
 )
 def test_parse_constraint(input, constraint):
@@ -34,17 +38,57 @@ def test_parse_constraint(input, constraint):
 @pytest.mark.parametrize(
     "input,constraint",
     [
-        ("v2.*", VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)),
-        ("2.*.*", VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)),
-        ("20.*", VersionRange(Version(20, 0, 0), Version(21, 0, 0), True)),
-        ("20.*.*", VersionRange(Version(20, 0, 0), Version(21, 0, 0), True)),
-        ("2.0.*", VersionRange(Version(2, 0, 0), Version(2, 1, 0), True)),
-        ("2.x", VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)),
-        ("2.x.x", VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)),
-        ("2.2.X", VersionRange(Version(2, 2, 0), Version(2, 3, 0), True)),
-        ("0.*", VersionRange(max=Version(1, 0, 0))),
-        ("0.*.*", VersionRange(max=Version(1, 0, 0))),
-        ("0.x", VersionRange(max=Version(1, 0, 0))),
+        (
+            "v2.*",
+            VersionRange(
+                Version.from_parts(2, 0, 0), Version.from_parts(3, 0, 0), True
+            ),
+        ),
+        (
+            "2.*.*",
+            VersionRange(
+                Version.from_parts(2, 0, 0), Version.from_parts(3, 0, 0), True
+            ),
+        ),
+        (
+            "20.*",
+            VersionRange(
+                Version.from_parts(20, 0, 0), Version.from_parts(21, 0, 0), True
+            ),
+        ),
+        (
+            "20.*.*",
+            VersionRange(
+                Version.from_parts(20, 0, 0), Version.from_parts(21, 0, 0), True
+            ),
+        ),
+        (
+            "2.0.*",
+            VersionRange(
+                Version.from_parts(2, 0, 0), Version.from_parts(2, 1, 0), True
+            ),
+        ),
+        (
+            "2.x",
+            VersionRange(
+                Version.from_parts(2, 0, 0), Version.from_parts(3, 0, 0), True
+            ),
+        ),
+        (
+            "2.x.x",
+            VersionRange(
+                Version.from_parts(2, 0, 0), Version.from_parts(3, 0, 0), True
+            ),
+        ),
+        (
+            "2.2.X",
+            VersionRange(
+                Version.from_parts(2, 2, 0), Version.from_parts(2, 3, 0), True
+            ),
+        ),
+        ("0.*", VersionRange(max=Version.from_parts(1, 0, 0))),
+        ("0.*.*", VersionRange(max=Version.from_parts(1, 0, 0))),
+        ("0.x", VersionRange(max=Version.from_parts(1, 0, 0))),
     ],
 )
 def test_parse_constraint_wildcard(input, constraint):
@@ -54,23 +98,83 @@ def test_parse_constraint_wildcard(input, constraint):
 @pytest.mark.parametrize(
     "input,constraint",
     [
-        ("~v1", VersionRange(Version(1, 0, 0), Version(2, 0, 0), True)),
-        ("~1.0", VersionRange(Version(1, 0, 0), Version(1, 1, 0), True)),
-        ("~1.0.0", VersionRange(Version(1, 0, 0), Version(1, 1, 0), True)),
-        ("~1.2", VersionRange(Version(1, 2, 0), Version(1, 3, 0), True)),
-        ("~1.2.3", VersionRange(Version(1, 2, 3), Version(1, 3, 0), True)),
+        (
+            "~v1",
+            VersionRange(
+                Version.from_parts(1, 0, 0), Version.from_parts(2, 0, 0), True
+            ),
+        ),
+        (
+            "~1.0",
+            VersionRange(
+                Version.from_parts(1, 0, 0), Version.from_parts(1, 1, 0), True
+            ),
+        ),
+        (
+            "~1.0.0",
+            VersionRange(
+                Version.from_parts(1, 0, 0), Version.from_parts(1, 1, 0), True
+            ),
+        ),
+        (
+            "~1.2",
+            VersionRange(
+                Version.from_parts(1, 2, 0), Version.from_parts(1, 3, 0), True
+            ),
+        ),
+        (
+            "~1.2.3",
+            VersionRange(
+                Version.from_parts(1, 2, 3), Version.from_parts(1, 3, 0), True
+            ),
+        ),
         (
             "~1.2-beta",
-            VersionRange(Version(1, 2, 0, pre="beta"), Version(1, 3, 0), True),
+            VersionRange(
+                Version.from_parts(1, 2, 0, pre=ReleaseTag("beta")),
+                Version.from_parts(1, 3, 0),
+                True,
+            ),
         ),
-        ("~1.2-b2", VersionRange(Version(1, 2, 0, pre="b2"), Version(1, 3, 0), True)),
-        ("~0.3", VersionRange(Version(0, 3, 0), Version(0, 4, 0), True)),
-        ("~3.5", VersionRange(Version(3, 5, 0), Version(3, 6, 0), True)),
-        ("~=3.5", VersionRange(Version(3, 5, 0), Version(4, 0, 0), True)),  # PEP 440
-        ("~=3.5.3", VersionRange(Version(3, 5, 3), Version(3, 6, 0), True)),  # PEP 440
+        (
+            "~1.2-b2",
+            VersionRange(
+                Version.from_parts(1, 2, 0, pre=ReleaseTag("beta", 2)),
+                Version.from_parts(1, 3, 0),
+                True,
+            ),
+        ),
+        (
+            "~0.3",
+            VersionRange(
+                Version.from_parts(0, 3, 0), Version.from_parts(0, 4, 0), True
+            ),
+        ),
+        (
+            "~3.5",
+            VersionRange(
+                Version.from_parts(3, 5, 0), Version.from_parts(3, 6, 0), True
+            ),
+        ),
+        (
+            "~=3.5",
+            VersionRange(
+                Version.from_parts(3, 5, 0), Version.from_parts(4, 0, 0), True
+            ),
+        ),  # PEP 440
+        (
+            "~=3.5.3",
+            VersionRange(
+                Version.from_parts(3, 5, 3), Version.from_parts(3, 6, 0), True
+            ),
+        ),  # PEP 440
         (
             "~=3.5.3rc1",
-            VersionRange(Version(3, 5, 3, pre="rc1"), Version(3, 6, 0), True),
+            VersionRange(
+                Version.from_parts(3, 5, 3, pre=ReleaseTag("rc", 1)),
+                Version.from_parts(3, 6, 0),
+                True,
+            ),
         ),  # PEP 440
     ],
 )
@@ -81,19 +185,63 @@ def test_parse_constraint_tilde(input, constraint):
 @pytest.mark.parametrize(
     "input,constraint",
     [
-        ("^v1", VersionRange(Version(1, 0, 0), Version(2, 0, 0), True)),
-        ("^0", VersionRange(Version(0, 0, 0), Version(1, 0, 0), True)),
-        ("^0.0", VersionRange(Version(0, 0, 0), Version(0, 1, 0), True)),
-        ("^1.2", VersionRange(Version(1, 2, 0), Version(2, 0, 0), True)),
+        (
+            "^v1",
+            VersionRange(
+                Version.from_parts(1, 0, 0), Version.from_parts(2, 0, 0), True
+            ),
+        ),
+        ("^0", VersionRange(Version.from_parts(0), Version.from_parts(0, 1), True)),
+        (
+            "^0.0",
+            VersionRange(
+                Version.from_parts(0, 0, 0), Version.from_parts(0, 1, 0), True
+            ),
+        ),
+        (
+            "^1.2",
+            VersionRange(
+                Version.from_parts(1, 2, 0), Version.from_parts(2, 0, 0), True
+            ),
+        ),
         (
             "^1.2.3-beta.2",
-            VersionRange(Version(1, 2, 3, pre="beta.2"), Version(2, 0, 0), True),
+            VersionRange(
+                Version.from_parts(1, 2, 3, pre=ReleaseTag("beta", 2)),
+                Version.from_parts(2, 0, 0),
+                True,
+            ),
+        ),
+        (
+            "^1.2.3",
+            VersionRange(
+                Version.from_parts(1, 2, 3), Version.from_parts(2, 0, 0), True
+            ),
+        ),
+        (
+            "^0.2.3",
+            VersionRange(
+                Version.from_parts(0, 2, 3), Version.from_parts(0, 3, 0), True
+            ),
+        ),
+        (
+            "^0.2",
+            VersionRange(
+                Version.from_parts(0, 2, 0), Version.from_parts(0, 3, 0), True
+            ),
+        ),
+        (
+            "^0.2.0",
+            VersionRange(
+                Version.from_parts(0, 2, 0), Version.from_parts(0, 3, 0), True
+            ),
+        ),
+        (
+            "^0.0.3",
+            VersionRange(
+                Version.from_parts(0, 0, 3), Version.from_parts(0, 0, 4), True
+            ),
         ),
-        ("^1.2.3", VersionRange(Version(1, 2, 3), Version(2, 0, 0), True)),
-        ("^0.2.3", VersionRange(Version(0, 2, 3), Version(0, 3, 0), True)),
-        ("^0.2", VersionRange(Version(0, 2, 0), Version(0, 3, 0), True)),
-        ("^0.2.0", VersionRange(Version(0, 2, 0), Version(0, 3, 0), True)),
-        ("^0.0.3", VersionRange(Version(0, 0, 3), Version(0, 0, 4), True)),
     ],
 )
 def test_parse_constraint_caret(input, constraint):
@@ -117,7 +265,10 @@ def test_parse_constraint_caret(input, constraint):
 )
 def test_parse_constraint_multi(input):
     assert parse_constraint(input) == VersionRange(
-        Version(2, 0, 0), Version(3, 0, 0), include_min=False, include_max=True
+        Version.from_parts(2, 0, 0),
+        Version.from_parts(3, 0, 0),
+        include_min=False,
+        include_max=True,
     )
 
 
@@ -127,8 +278,10 @@ def test_parse_constraint_multi(input):
 )
 def test_parse_constraint_multi_wilcard(input):
     assert parse_constraint(input) == VersionUnion(
-        VersionRange(Version(2, 7, 0), Version(3, 0, 0), True, False),
-        VersionRange(Version(3, 2, 0), None, True, False),
+        VersionRange(
+            Version.from_parts(2, 7, 0), Version.from_parts(3, 0, 0), True, False
+        ),
+        VersionRange(Version.from_parts(3, 2, 0), None, True, False),
     )
 
 
diff --git a/tests/semver/test_parse_constraint.py b/tests/semver/test_parse_constraint.py
index 97e80dd1d..a5db226af 100644
--- a/tests/semver/test_parse_constraint.py
+++ b/tests/semver/test_parse_constraint.py
@@ -9,79 +9,166 @@
 @pytest.mark.parametrize(
     "constraint,version",
     [
-        ("~=3.8", VersionRange(min=Version(3, 8), max=Version(4, 0), include_min=True)),
+        (
+            "~=3.8",
+            VersionRange(
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(4, 0),
+                include_min=True,
+            ),
+        ),
         (
             "== 3.8.*",
-            VersionRange(min=Version(3, 8), max=Version(3, 9, 0), include_min=True),
+            VersionRange(
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(3, 9, 0),
+                include_min=True,
+            ),
         ),
         (
             "~= 3.8",
-            VersionRange(min=Version(3, 8), max=Version(4, 0), include_min=True),
+            VersionRange(
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(4, 0),
+                include_min=True,
+            ),
+        ),
+        (
+            "~3.8",
+            VersionRange(
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(3, 9),
+                include_min=True,
+            ),
+        ),
+        (
+            "~ 3.8",
+            VersionRange(
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(3, 9),
+                include_min=True,
+            ),
         ),
-        ("~3.8", VersionRange(min=Version(3, 8), max=Version(3, 9), include_min=True)),
-        ("~ 3.8", VersionRange(min=Version(3, 8), max=Version(3, 9), include_min=True)),
-        (">3.8", VersionRange(min=Version(3, 8))),
-        (">=3.8", VersionRange(min=Version(3, 8), include_min=True)),
-        (">= 3.8", VersionRange(min=Version(3, 8), include_min=True)),
+        (">3.8", VersionRange(min=Version.from_parts(3, 8))),
+        (">=3.8", VersionRange(min=Version.from_parts(3, 8), include_min=True)),
+        (">= 3.8", VersionRange(min=Version.from_parts(3, 8), include_min=True)),
         (
             ">3.8,<=6.5",
-            VersionRange(min=Version(3, 8), max=Version(6, 5), include_max=True),
+            VersionRange(
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(6, 5),
+                include_max=True,
+            ),
         ),
         (
             ">3.8,<= 6.5",
-            VersionRange(min=Version(3, 8), max=Version(6, 5), include_max=True),
+            VersionRange(
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(6, 5),
+                include_max=True,
+            ),
         ),
         (
             "> 3.8,<= 6.5",
-            VersionRange(min=Version(3, 8), max=Version(6, 5), include_max=True),
+            VersionRange(
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(6, 5),
+                include_max=True,
+            ),
         ),
         (
             "> 3.8,<=6.5",
-            VersionRange(min=Version(3, 8), max=Version(6, 5), include_max=True),
+            VersionRange(
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(6, 5),
+                include_max=True,
+            ),
         ),
         (
             ">3.8 ,<=6.5",
-            VersionRange(min=Version(3, 8), max=Version(6, 5), include_max=True),
+            VersionRange(
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(6, 5),
+                include_max=True,
+            ),
         ),
         (
             ">3.8, <=6.5",
-            VersionRange(min=Version(3, 8), max=Version(6, 5), include_max=True),
+            VersionRange(
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(6, 5),
+                include_max=True,
+            ),
         ),
         (
             ">3.8 , <=6.5",
-            VersionRange(min=Version(3, 8), max=Version(6, 5), include_max=True),
+            VersionRange(
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(6, 5),
+                include_max=True,
+            ),
         ),
         (
             "==3.8",
             VersionRange(
-                min=Version(3, 8), max=Version(3, 8), include_min=True, include_max=True
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(3, 8),
+                include_min=True,
+                include_max=True,
             ),
         ),
         (
             "== 3.8",
             VersionRange(
-                min=Version(3, 8), max=Version(3, 8), include_min=True, include_max=True
+                min=Version.from_parts(3, 8),
+                max=Version.from_parts(3, 8),
+                include_min=True,
+                include_max=True,
             ),
         ),
         (
             "~2.7 || ~3.8",
             VersionUnion(
-                VersionRange(min=Version(2, 7), max=Version(2, 8), include_min=True),
-                VersionRange(min=Version(3, 8), max=Version(3, 9), include_min=True),
+                VersionRange(
+                    min=Version.from_parts(2, 7),
+                    max=Version.from_parts(2, 8),
+                    include_min=True,
+                ),
+                VersionRange(
+                    min=Version.from_parts(3, 8),
+                    max=Version.from_parts(3, 9),
+                    include_min=True,
+                ),
             ),
         ),
         (
             "~2.7||~3.8",
             VersionUnion(
-                VersionRange(min=Version(2, 7), max=Version(2, 8), include_min=True),
-                VersionRange(min=Version(3, 8), max=Version(3, 9), include_min=True),
+                VersionRange(
+                    min=Version.from_parts(2, 7),
+                    max=Version.from_parts(2, 8),
+                    include_min=True,
+                ),
+                VersionRange(
+                    min=Version.from_parts(3, 8),
+                    max=Version.from_parts(3, 9),
+                    include_min=True,
+                ),
             ),
         ),
         (
             "~ 2.7||~ 3.8",
             VersionUnion(
-                VersionRange(min=Version(2, 7), max=Version(2, 8), include_min=True),
-                VersionRange(min=Version(3, 8), max=Version(3, 9), include_min=True),
+                VersionRange(
+                    min=Version.from_parts(2, 7),
+                    max=Version.from_parts(2, 8),
+                    include_min=True,
+                ),
+                VersionRange(
+                    min=Version.from_parts(3, 8),
+                    max=Version.from_parts(3, 9),
+                    include_min=True,
+                ),
             ),
         ),
     ],
diff --git a/tests/semver/test_version.py b/tests/semver/test_version.py
index 69bb6a6c1..ad909f33d 100644
--- a/tests/semver/test_version.py
+++ b/tests/semver/test_version.py
@@ -1,64 +1,90 @@
 import pytest
 
 from poetry.core.semver.empty_constraint import EmptyConstraint
-from poetry.core.semver.exceptions import ParseVersionError
 from poetry.core.semver.version import Version
 from poetry.core.semver.version_range import VersionRange
+from poetry.core.version.exceptions import InvalidVersion
+from poetry.core.version.pep440 import ReleaseTag
 
 
 @pytest.mark.parametrize(
-    "input,version",
+    "text,version",
     [
-        ("1.0.0", Version(1, 0, 0)),
-        ("1", Version(1, 0, 0)),
-        ("1.0", Version(1, 0, 0)),
-        ("1b1", Version(1, 0, 0, pre="beta1")),
-        ("1.0b1", Version(1, 0, 0, pre="beta1")),
-        ("1.0.0b1", Version(1, 0, 0, pre="beta1")),
-        ("1.0.0-b1", Version(1, 0, 0, pre="beta1")),
-        ("1.0.0-beta.1", Version(1, 0, 0, pre="beta1")),
-        ("1.0.0+1", Version(1, 0, 0, build="1")),
-        ("1.0.0-1", Version(1, 0, 0, build="1")),
-        ("1.0.0.0", Version(1, 0, 0)),
-        ("1.0.0-post", Version(1, 0, 0)),
-        ("1.0.0-post1", Version(1, 0, 0, build="1")),
-        ("0.6c", Version(0, 6, 0, pre="rc0")),
-        ("0.6pre", Version(0, 6, 0, pre="rc0")),
+        ("1.0.0", Version.from_parts(1, 0, 0)),
+        ("1", Version.from_parts(1, 0, 0)),
+        ("1.0", Version.from_parts(1, 0, 0)),
+        ("1b1", Version.from_parts(1, 0, 0, pre=ReleaseTag("beta", 1))),
+        ("1.0b1", Version.from_parts(1, 0, 0, pre=ReleaseTag("beta", 1))),
+        ("1.0.0b1", Version.from_parts(1, 0, 0, pre=ReleaseTag("beta", 1))),
+        ("1.0.0-b1", Version.from_parts(1, 0, 0, pre=ReleaseTag("beta", 1))),
+        ("1.0.0-beta.1", Version.from_parts(1, 0, 0, pre=ReleaseTag("beta", 1))),
+        ("1.0.0+1", Version.from_parts(1, 0, 0, local=1)),
+        ("1.0.0-1", Version.from_parts(1, 0, 0, post=ReleaseTag("post", 1))),
+        ("1.0.0.0", Version.from_parts(1, 0, 0, extra=0)),
+        ("1.0.0-post", Version.from_parts(1, 0, 0, post=ReleaseTag("post"))),
+        ("1.0.0-post1", Version.from_parts(1, 0, 0, post=ReleaseTag("post", 1))),
+        ("0.6c", Version.from_parts(0, 6, 0, pre=ReleaseTag("rc", 0))),
+        ("0.6pre", Version.from_parts(0, 6, 0, pre=ReleaseTag("preview", 0))),
     ],
 )
-def test_parse_valid(input, version):
-    parsed = Version.parse(input)
+def test_parse_valid(text, version):
+    parsed = Version.parse(text)
 
     assert parsed == version
-    assert parsed.text == input
+    assert parsed.text == text
 
 
 @pytest.mark.parametrize("input", [(None, "example")])
 def test_parse_invalid(input):
-    with pytest.raises(ParseVersionError):
+    with pytest.raises(InvalidVersion):
         Version.parse(input)
 
 
-def test_comparison():
-    versions = [
-        "1.0.0-alpha",
-        "1.0.0-alpha.1",
-        "1.0.0-beta.2",
-        "1.0.0-beta.11",
-        "1.0.0-rc.1",
-        "1.0.0-rc.1+build.1",
-        "1.0.0",
-        "1.0.0+0.3.7",
-        "1.3.7+build",
-        "1.3.7+build.2.b8f12d7",
-        "1.3.7+build.11.e0f985a",
-        "2.0.0",
-        "2.1.0",
-        "2.2.0",
-        "2.11.0",
-        "2.11.1",
-    ]
-
+@pytest.mark.parametrize(
+    "versions",
+    [
+        [
+            "1.0.0-alpha",
+            "1.0.0-alpha.1",
+            "1.0.0-beta.2",
+            "1.0.0-beta.11",
+            "1.0.0-rc.1",
+            "1.0.0-rc.1+build.1",
+            "1.0.0",
+            "1.0.0+0.3.7",
+            "1.3.7+build",
+            "1.3.7+build.2.b8f12d7",
+            "1.3.7+build.11.e0f985a",
+            "2.0.0",
+            "2.1.0",
+            "2.2.0",
+            "2.11.0",
+            "2.11.1",
+        ],
+        # PEP 440 example comparisons
+        [
+            "1.0.dev456",
+            "1.0a1",
+            "1.0a2.dev456",
+            "1.0a12.dev456",
+            "1.0a12",
+            "1.0b1.dev456",
+            "1.0b2",
+            "1.0b2.post345.dev456",
+            "1.0b2.post345",
+            "1.0rc1.dev456",
+            "1.0rc1",
+            "1.0",
+            "1.0+abc.5",
+            "1.0+abc.7",
+            "1.0+5",
+            "1.0.post456.dev34",
+            "1.0.post456",
+            "1.1.dev1",
+        ],
+    ],
+)
+def test_comparison(versions):
     for i in range(len(versions)):
         for j in range(len(versions)):
             a = Version.parse(versions[i])
diff --git a/tests/version/test_requirements.py b/tests/version/test_requirements.py
index 375294641..0a8a9bd87 100644
--- a/tests/version/test_requirements.py
+++ b/tests/version/test_requirements.py
@@ -28,12 +28,12 @@ def assert_requirement(req, name, url=None, extras=None, constraint="*", marker=
         ("name", {"name": "name"}),
         ("foo-bar.quux_baz", {"name": "foo-bar.quux_baz"}),
         ("name>=3", {"name": "name", "constraint": ">=3"}),
-        ("name==1.0.org1", {"name": "name", "constraint": "==1.0.org1"}),
+        ("name==1.0.post1", {"name": "name", "constraint": "==1.0.post1"}),
         (
-            "name>=1.x.y;python_version=='2.6'",
+            "name>=1.2.3;python_version=='2.6'",
             {
                 "name": "name",
-                "constraint": ">=1.x.y",
+                "constraint": ">=1.2.3",
                 "marker": 'python_version == "2.6"',
             },
         ),