From e46cabcb2c555d53b62eb56b029f52bf2c3eee91 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Thu, 5 Apr 2018 17:29:02 +0800 Subject: [PATCH] Fix Python version parser for 2.7.0 Fix #1893. --- pipenv/utils.py | 48 ++++++++++++++++++++++++++++++--------------- tests/test_utils.py | 20 +++++++++++++++++++ 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/pipenv/utils.py b/pipenv/utils.py index de2d4fe3..80db89d4 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import errno import os +import re import hashlib import tempfile import sys @@ -175,29 +176,44 @@ def cleanup_toml(tml): return toml +def parse_python_version(output): + """Parse a Python version output returned by `python --version`. + + Return a dict with three keys: major, minor, and micro. Each value is a + string containing a version part. + + Note: The micro part would be `'0'` if it's missing from the input string. + """ + version_pattern = re.compile(r''' + ^ # Beginning of line. + Python # Literally "Python". + \s # Space. + (?P\d+) # Major = one or more digits. + \. # Dot. + (?P\d+) # Minor = one or more digits. + (?: # Unnamed group for dot-micro. + \. # Dot. + (?P\d+) # Micro = one or more digit. + )? # Micro is optional because pypa/pipenv#1893. + .* # Trailing garbage. + $ # End of line. + ''', re.VERBOSE) + + match = version_pattern.match(output) + if not match: + return None + return match.groupdict(default='0') + + def python_version(path_to_python): if not path_to_python: return None - try: c = delegator.run([path_to_python, '--version'], block=False) except Exception: return None - - output = c.out.strip() or c.err.strip() - - @parse.with_pattern(r'.*') - def allow_empty(text): - return text - - TEMPLATE = 'Python {}.{}.{:d}{:AllowEmpty}' - parsed = parse.parse(TEMPLATE, output, dict(AllowEmpty=allow_empty)) - if parsed: - parsed = parsed.fixed - else: - return None - - return u"{v[0]}.{v[1]}.{v[2]}".format(v=parsed) + version = parse_python_version(c.out.strip() or c.err.strip()) + return u'{major}.{minor}.{micro}'.format(**version) def escape_grouped_arguments(s): diff --git a/tests/test_utils.py b/tests/test_utils.py index 26db37fb..4622e185 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -401,3 +401,23 @@ twine = "*" ) def test_prepare_pip_source_args(self, sources, expected_args): assert pipenv.utils.prepare_pip_source_args(sources, pip_args=None) == expected_args + + @pytest.mark.utils + def test_parse_python_version(self): + ver = pipenv.utils.parse_python_version('Python 3.6.5\n') + assert ver == {'major': '3', 'minor': '6', 'micro': '5'} + + @pytest.mark.utils + def test_parse_python_version_suffix(self): + ver = pipenv.utils.parse_python_version('Python 3.6.5rc1\n') + assert ver == {'major': '3', 'minor': '6', 'micro': '5'} + + @pytest.mark.utils + def test_parse_python_version_270(self): + ver = pipenv.utils.parse_python_version('Python 2.7\n') + assert ver == {'major': '2', 'minor': '7', 'micro': '0'} + + @pytest.mark.utils + def test_parse_python_version_270_garbage(self): + ver = pipenv.utils.parse_python_version('Python 2.7+\n') + assert ver == {'major': '2', 'minor': '7', 'micro': '0'}