Repository: python-poetry/poetry

Issue:
Poetry selecting incorrect version of sub-dependency
<!--
  Hi there! Thank you for discovering and submitting an issue.

  Before you submit this; let's make sure of a few things.
  Please make sure the following boxes are ticked if they are correct.
  If not, please try and fulfill these first.
-->

<!-- Checked checkbox should look like this: [x] -->
- [x] I am on the [latest](https://github.com/sdispater/poetry/releases/latest) Poetry version.
- [x] I have searched the [issues](https://github.com/sdispater/poetry/issues) of this repo and believe that this is not a duplicate.
- [x] If an exception occurs when executing a command, I executed it again in debug mode (`-vvv` option).

<!--
  Once those are done, if you're able to fill in the following list with your information,
  it'd be very helpful to whoever handles the issue.
-->

- **OS version and name**: macOS 10.13.4 High Sierra
- **Poetry version**: 0.11.3

## Issue
<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->
Poetry currently installs celery in a non-functional state by installing the wrong version of one of its dependencies, billiard. billiard's version number isn't parseable by semver so I think that may be the problem? Steps to reproduce:

```toml
[tool.poetry]
name = "deps"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]

[tool.poetry.dependencies]
python = "*"
celery = "^4.2"

[tool.poetry.dev-dependencies]
```

```shell
$ poetry lock -vvv
Virtualenv deps-py3.6 already exists.
Using virtualenv: /Users/<user>/Library/Caches/pypoetry/virtualenvs/deps-py3.6
Updating dependencies
Resolving dependencies...
   1: fact: deps is 0.1.0
   1: derived: deps
   1: fact: deps depends on celery (^4.2)
   1: selecting deps (0.1.0)
   1: derived: celery (^4.2)
PyPI: 2 packages found for celery >=4.2,<5.0
   1: fact: celery (4.2.1) depends on kombu (>=4.2.0,<5.0)
   1: fact: celery (4.2.1) depends on billiard (>=3.5.0.2,<3.6.0)
   1: fact: celery (4.2.1) depends on pytz (>0.0-dev)
   1: selecting celery (4.2.1)
   1: derived: pytz (>0.0-dev)
   1: derived: billiard (>=3.5.0.2,<3.6.0)
   1: derived: kombu (>=4.2.0,<5.0)
PyPI: 91 packages found for pytz >0.0-dev
   1: selecting pytz (2018.5)
PyPI: 4 packages found for billiard >=3.5.0.2,<3.6.0
   1: selecting billiard (3.5.0.0rc2)
PyPI: No release information found for kombu-3.0.17-20140602, skipping
PyPI: 2 packages found for kombu >=4.2.0,<5.0
   1: fact: kombu (4.2.1) depends on amqp (>=2.1.4,<3.0)
   1: selecting kombu (4.2.1)
   1: derived: amqp (>=2.1.4,<3.0)
PyPI: No release information found for amqp-0.0.1, skipping
PyPI: 7 packages found for amqp >=2.1.4,<3.0
   1: fact: amqp (2.3.2) depends on vine (>=1.1.3)
   1: selecting amqp (2.3.2)
   1: derived: vine (>=1.1.3)
PyPI: No release information found for vine-1.0.0rc1, skipping
PyPI: 2 packages found for vine >=1.1.3
   1: selecting vine (1.1.4)
   1: Version solving took 0.369 seconds.
   1: Tried 1 solutions.


Writing lock file
```

Relevant section of pyproject.lock:

```toml
[[package]]
category = "main"
description = "Python multiprocessing fork with improvements and bugfixes"
name = "billiard"
optional = false
platform = "UNKNOWN"
python-versions = "*"
version = "3.5.0.0rc2"

[[package]]
category = "main"
description = "Distributed Task Queue."
name = "celery"
optional = false
platform = "any"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "4.2.1"

[package.dependencies]
billiard = ">=3.5.0.2,<3.6.0"
kombu = ">=4.2.0,<5.0"
pytz = ">0.0-dev"
```

For some reason it's installing 3.5.0.0rc2 which is outside of the range `>=3.5.0.2,<3.6.0`. Installing billiard directly will also install 3.5.0.0rc2 instead of the latest 3.5.0.4.


Discussion:

Resolving patch:
diff --git a/poetry/semver/patterns.py b/poetry/semver/patterns.py
--- a/poetry/semver/patterns.py
+++ b/poetry/semver/patterns.py
@@ -6,7 +6,9 @@
     "([+-]?([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?"
 )
 
-_COMPLETE_VERSION = "v?(\d+)(?:\.(\d+))?(?:\.(\d+))?{}(?:\+[^\s]+)?".format(MODIFIERS)
+_COMPLETE_VERSION = "v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?{}(?:\+[^\s]+)?".format(
+    MODIFIERS
+)
 
 COMPLETE_VERSION = re.compile("(?i)" + _COMPLETE_VERSION)
 
diff --git a/poetry/semver/version.py b/poetry/semver/version.py
--- a/poetry/semver/version.py
+++ b/poetry/semver/version.py
@@ -20,6 +20,7 @@ def __init__(
         major,  # type: int
         minor=None,  # type: Union[int, None]
         patch=None,  # type: Union[int, None]
+        rest=None,  # type: Union[int, None]
         pre=None,  # type: Union[str, None]
         build=None,  # type: Union[str, None]
         text=None,  # type: Union[str, None]
@@ -44,10 +45,17 @@ def __init__(
             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)]
@@ -57,6 +65,9 @@ def __init__(
                 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)
@@ -93,6 +104,10 @@ def minor(self):  # type: () -> int
     def patch(self):  # type: () -> int
         return self._patch
 
+    @property
+    def rest(self):  # type: () -> int
+        return self._rest
+
     @property
     def prerelease(self):  # type: () -> List[str]
         return self._prerelease
@@ -185,14 +200,15 @@ def parse(cls, text):  # type: (str) -> Version
         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(4)
-        build = match.group(5)
+        pre = match.group(5)
+        build = match.group(6)
 
         if build:
             build = build.lstrip("+")
 
-        return Version(major, minor, patch, pre, build, text)
+        return Version(major, minor, patch, rest, pre, build, text)
 
     def is_any(self):
         return False
@@ -289,9 +305,6 @@ def _normalize_build(self, build):  # type: (str) -> str
         if not build:
             return
 
-        if build == "0":
-            return
-
         if build.startswith("post"):
             build = build.lstrip("post")
 
@@ -339,6 +352,9 @@ def _cmp(self, other):
         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
@@ -380,7 +396,7 @@ def _cmp_lists(self, a, b):  # type: (List, List) -> int
             if a_part == b_part:
                 continue
 
-            # Missing parts come before present ones.
+            # Missing parts come after present ones.
             if a_part is None:
                 return -1
 
@@ -408,6 +424,7 @@ def __eq__(self, other):  # type: (Version) -> bool
             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
         )


Test patch:
diff --git a/tests/semver/test_main.py b/tests/semver/test_main.py
--- a/tests/semver/test_main.py
+++ b/tests/semver/test_main.py
@@ -22,7 +22,7 @@
         ("=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, "b5")),
+        ("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
     ],
@@ -59,8 +59,11 @@ def test_parse_constraint_wildcard(input, constraint):
         ("~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)),
-        ("~1.2-beta", VersionRange(Version(1, 2, 0, "beta"), Version(1, 3, 0), True)),
-        ("~1.2-b2", VersionRange(Version(1, 2, 0, "b2"), Version(1, 3, 0), True)),
+        (
+            "~1.2-beta",
+            VersionRange(Version(1, 2, 0, pre="beta"), Version(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
@@ -80,7 +83,7 @@ def test_parse_constraint_tilde(input, constraint):
         ("^1.2", VersionRange(Version(1, 2, 0), Version(2, 0, 0), True)),
         (
             "^1.2.3-beta.2",
-            VersionRange(Version(1, 2, 3, "beta.2"), Version(2, 0, 0), True),
+            VersionRange(Version(1, 2, 3, pre="beta.2"), Version(2, 0, 0), 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)),
@@ -173,3 +176,21 @@ def test_parse_constraints_negative_wildcard(input, constraint):
 )
 def test_constraints_keep_version_precision(input, expected):
     assert str(parse_constraint(input)) == expected
+
+
+@pytest.mark.parametrize(
+    "unsorted, sorted_",
+    [
+        (["1.0.3", "1.0.2", "1.0.1"], ["1.0.1", "1.0.2", "1.0.3"]),
+        (["1.0.0.2", "1.0.0.0rc2"], ["1.0.0.0rc2", "1.0.0.2"]),
+        (["1.0.0.0", "1.0.0.0rc2"], ["1.0.0.0rc2", "1.0.0.0"]),
+        (["1.0.0.0.0", "1.0.0.0rc2"], ["1.0.0.0rc2", "1.0.0.0.0"]),
+        (["1.0.0rc2", "1.0.0rc1"], ["1.0.0rc1", "1.0.0rc2"]),
+        (["1.0.0rc2", "1.0.0b1"], ["1.0.0b1", "1.0.0rc2"]),
+    ],
+)
+def test_versions_are_sortable(unsorted, sorted_):
+    unsorted = [parse_constraint(u) for u in unsorted]
+    sorted_ = [parse_constraint(s) for s in sorted_]
+
+    assert sorted(unsorted) == sorted_
diff --git a/tests/semver/test_version.py b/tests/semver/test_version.py
--- a/tests/semver/test_version.py
+++ b/tests/semver/test_version.py
@@ -11,18 +11,18 @@
         ("1.0.0", Version(1, 0, 0)),
         ("1", Version(1, 0, 0)),
         ("1.0", Version(1, 0, 0)),
-        ("1b1", Version(1, 0, 0, "beta1")),
-        ("1.0b1", Version(1, 0, 0, "beta1")),
-        ("1.0.0b1", Version(1, 0, 0, "beta1")),
-        ("1.0.0-b1", Version(1, 0, 0, "beta1")),
-        ("1.0.0-beta.1", Version(1, 0, 0, "beta1")),
-        ("1.0.0+1", Version(1, 0, 0, None, "1")),
-        ("1.0.0-1", Version(1, 0, 0, None, "1")),
+        ("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, None, "1")),
-        ("0.6c", Version(0, 6, 0, "rc0")),
-        ("0.6pre", Version(0, 6, 0, "rc0")),
+        ("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")),
     ],
 )
 def test_parse_valid(input, version):