From 284a69ca55f046cd5ac4b877672c8fc950fc754b Mon Sep 17 00:00:00 2001 From: gabriel pettier Date: Sat, 30 May 2015 12:24:27 +0200 Subject: [PATCH 1/4] needs testing, should fix #201 using pypa implementation of PEP440 https://github.com/pypa/packaging/blob/master/packaging/version.py --- buildozer/libs/_structures.py | 78 +++++++ buildozer/libs/version.py | 401 ++++++++++++++++++++++++++++++++++ buildozer/targets/android.py | 4 +- 3 files changed, 481 insertions(+), 2 deletions(-) create mode 100644 buildozer/libs/_structures.py create mode 100644 buildozer/libs/version.py diff --git a/buildozer/libs/_structures.py b/buildozer/libs/_structures.py new file mode 100644 index 000000000..0ae9bb52a --- /dev/null +++ b/buildozer/libs/_structures.py @@ -0,0 +1,78 @@ +# Copyright 2014 Donald Stufft +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import absolute_import, division, print_function + + +class Infinity(object): + + def __repr__(self): + return "Infinity" + + def __hash__(self): + return hash(repr(self)) + + def __lt__(self, other): + return False + + def __le__(self, other): + return False + + def __eq__(self, other): + return isinstance(other, self.__class__) + + def __ne__(self, other): + return not isinstance(other, self.__class__) + + def __gt__(self, other): + return True + + def __ge__(self, other): + return True + + def __neg__(self): + return NegativeInfinity + +Infinity = Infinity() + + +class NegativeInfinity(object): + + def __repr__(self): + return "-Infinity" + + def __hash__(self): + return hash(repr(self)) + + def __lt__(self, other): + return True + + def __le__(self, other): + return True + + def __eq__(self, other): + return isinstance(other, self.__class__) + + def __ne__(self, other): + return not isinstance(other, self.__class__) + + def __gt__(self, other): + return False + + def __ge__(self, other): + return False + + def __neg__(self): + return Infinity + +NegativeInfinity = NegativeInfinity() diff --git a/buildozer/libs/version.py b/buildozer/libs/version.py new file mode 100644 index 000000000..cf8afb16d --- /dev/null +++ b/buildozer/libs/version.py @@ -0,0 +1,401 @@ +# Copyright 2014 Donald Stufft +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import absolute_import, division, print_function + +import collections +import itertools +import re + +from ._structures import Infinity + + +__all__ = [ + "parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN" +] + + +_Version = collections.namedtuple( + "_Version", + ["epoch", "release", "dev", "pre", "post", "local"], +) + + +def parse(version): + """ + Parse the given version string and return either a :class:`Version` object + or a :class:`LegacyVersion` object depending on if the given version is + a valid PEP 440 version or a legacy version. + """ + try: + return Version(version) + except InvalidVersion: + return LegacyVersion(version) + + +class InvalidVersion(ValueError): + """ + An invalid version was found, users should refer to PEP 440. + """ + + +class _BaseVersion(object): + + def __hash__(self): + return hash(self._key) + + def __lt__(self, other): + return self._compare(other, lambda s, o: s < o) + + def __le__(self, other): + return self._compare(other, lambda s, o: s <= o) + + def __eq__(self, other): + return self._compare(other, lambda s, o: s == o) + + def __ge__(self, other): + return self._compare(other, lambda s, o: s >= o) + + def __gt__(self, other): + return self._compare(other, lambda s, o: s > o) + + def __ne__(self, other): + return self._compare(other, lambda s, o: s != o) + + def _compare(self, other, method): + if not isinstance(other, _BaseVersion): + return NotImplemented + + return method(self._key, other._key) + + +class LegacyVersion(_BaseVersion): + + def __init__(self, version): + self._version = str(version) + self._key = _legacy_cmpkey(self._version) + + def __str__(self): + return self._version + + def __repr__(self): + return "".format(repr(str(self))) + + @property + def public(self): + return self._version + + @property + def base_version(self): + return self._version + + @property + def local(self): + return None + + @property + def is_prerelease(self): + return False + + @property + def is_postrelease(self): + 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): + 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): + # 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 + +# Deliberately not anchored to the start and end of the string, to make it +# easier for 3rd party code to reuse +VERSION_PATTERN = 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
+"""
+
+
+class Version(_BaseVersion):
+
+    _regex = re.compile(
+        r"^\s*" + VERSION_PATTERN + r"\s*$",
+        re.VERBOSE | re.IGNORECASE,
+    )
+
+    def __init__(self, version):
+        # Validate the version and parse it into pieces
+        match = self._regex.search(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):
+        return "".format(repr(str(self)))
+
+    def __str__(self):
+        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):
+        return str(self).split("+", 1)[0]
+
+    @property
+    def base_version(self):
+        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):
+        version_string = str(self)
+        if "+" in version_string:
+            return version_string.split("+", 1)[1]
+
+    @property
+    def is_prerelease(self):
+        return bool(self._version.dev or self._version.pre)
+
+    @property
+    def is_postrelease(self):
+        return bool(self._version.post)
+
+
+def _parse_letter_version(letter, number):
+    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"
+
+        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):
+    """
+    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, release, pre, post, dev, local):
+    # 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(
+            itertools.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/buildozer/targets/android.py b/buildozer/targets/android.py
index 3b35b7d18..895c93d80 100644
--- a/buildozer/targets/android.py
+++ b/buildozer/targets/android.py
@@ -29,6 +29,7 @@
 from shutil import copyfile
 from glob import glob
 
+from buildozer.libs.version import parse
 
 
 class TargetAndroid(Target):
@@ -315,8 +316,7 @@ def _android_update_sdk(self, packages):
             child.sendline('y')
 
     def _process_version_string(self, version_string):
-        version = [int(i) for i in version_string.split(".")]
-        return version
+        return parse(version_string)
 
     def _build_package_string(self, package_name, version_list):
         version_string = '.'.join([str(ver) for ver in version_list])

From c19575a7d500ae373af944692243d47f028f4e32 Mon Sep 17 00:00:00 2001
From: gabriel pettier 
Date: Sat, 30 May 2015 12:34:30 +0200
Subject: [PATCH 2/4] completly remove self._process_version_string, since
 parse() is simpler

---
 buildozer/targets/android.py | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/buildozer/targets/android.py b/buildozer/targets/android.py
index 895c93d80..6b3492d93 100644
--- a/buildozer/targets/android.py
+++ b/buildozer/targets/android.py
@@ -315,9 +315,6 @@ def _android_update_sdk(self, packages):
                 break
             child.sendline('y')
 
-    def _process_version_string(self, version_string):
-        return parse(version_string)
-
     def _build_package_string(self, package_name, version_list):
         version_string = '.'.join([str(ver) for ver in version_list])
         return '{}-{}'.format(package_name, version_string)
@@ -330,7 +327,7 @@ def _read_version_subdir(self, *args):
             return [0]
         for v in os.listdir(join(*args)):
             try:
-                versions.append(self._process_version_string(v))
+                versions.append(parse(v))
             except:
                 pass
         if not versions:
@@ -346,7 +343,7 @@ def _find_latest_package(self, packages, key):
             if not p.startswith(key):
                 continue
             version_string = p.split(key)[-1]
-            version = self._process_version_string(version_string)
+            version = parse(version_string)
             package_versions.append(version)
         if not package_versions:
             return

From e4a4d2ad1b75dc76261a2600ca9e31decdf8606b Mon Sep 17 00:00:00 2001
From: gabriel pettier 
Date: Sat, 30 May 2015 12:43:41 +0200
Subject: [PATCH 3/4] stop messing with version completly, don't split or join

---
 buildozer/targets/android.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/buildozer/targets/android.py b/buildozer/targets/android.py
index 6b3492d93..f2a6a87ac 100644
--- a/buildozer/targets/android.py
+++ b/buildozer/targets/android.py
@@ -315,9 +315,8 @@ def _android_update_sdk(self, packages):
                 break
             child.sendline('y')
 
-    def _build_package_string(self, package_name, version_list):
-        version_string = '.'.join([str(ver) for ver in version_list])
-        return '{}-{}'.format(package_name, version_string)
+    def _build_package_string(self, package_name, version):
+        return '{}-{}'.format(package_name, version)
 
     def _read_version_subdir(self, *args):
         versions = []

From 12964f488f3a2f0d8624c18734a68398b06908f5 Mon Sep 17 00:00:00 2001
From: gabriel pettier 
Date: Sat, 30 May 2015 12:35:24 +0200
Subject: [PATCH 4/4] use max() instead of sort() + [-1]

---
 buildozer/targets/android.py | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/buildozer/targets/android.py b/buildozer/targets/android.py
index f2a6a87ac..91abe5ceb 100644
--- a/buildozer/targets/android.py
+++ b/buildozer/targets/android.py
@@ -333,8 +333,7 @@ def _read_version_subdir(self, *args):
             self.buildozer.error(
                 'Unable to find the latest version for {}'.format(join(*args)))
             return [0]
-        versions.sort()
-        return versions[-1]
+        return max(versions)
 
     def _find_latest_package(self, packages, key):
         package_versions = []
@@ -346,8 +345,7 @@ def _find_latest_package(self, packages, key):
             package_versions.append(version)
         if not package_versions:
             return
-        package_versions.sort()
-        return package_versions[-1]
+        return max(package_versions)
 
     def _install_android_packages(self):