Fix python path discovery if not called python

- Begin a refactor of `delegator.run` invocation to ensure we capture
  and handle failures with our own exception wrappers
- Additoinally capture output and error logging and command information
  when running in verbose mode (should avoid significant repitition in
  the codebase)
- Refactor `which` and `system_which` to fallback to pythonfinder's
implementation
- Abstract `is_python_command` to identify whether we are looking for
  python, this enables us to rely on `pythonfinder.Finder.find_all_python_versions()`
  to ensure we aren't skipping python versions
- Fixes #2783

Signed-off-by: Dan Ryan <dan@danryan.co>
This commit is contained in:
Dan Ryan
2018-11-30 02:22:59 -05:00
parent d5e446ee7b
commit dcfce70817
6 changed files with 314 additions and 145 deletions
+2
View File
@@ -0,0 +1,2 @@
Fixed an issue which caused errors due to reliance on the system utilities ``which`` and ``where`` which may not always exist on some systems.
- Fixed a bug which caused periodic failures in python discovery when executables named ``python`` were not present on the target ``$PATH``.
+1 -1
View File
@@ -10,7 +10,7 @@ class ScriptEmptyError(ValueError):
def _quote_if_contains(value, pattern):
if next(re.finditer(pattern, value), None):
if next(iter(re.finditer(pattern, value)), None):
return '"{0}"'.format(re.sub(r'(\\*)"', r'\1\1\\"', value))
return value
+119 -76
View File
@@ -34,7 +34,8 @@ from .utils import (
escape_cmd, escape_grouped_arguments, find_windows_executable,
get_canonical_names, is_pinned, is_pypi_url, is_required_version, is_star,
is_valid_url, parse_indexes, pep423_name, prepare_pip_source_args,
proper_case, python_version, venv_resolve_deps
proper_case, python_version, venv_resolve_deps, run_command,
is_python_command, find_python
)
@@ -86,16 +87,19 @@ def which(command, location=None, allow_global=False):
location = os.environ.get("VIRTUAL_ENV", None)
if not (location and os.path.exists(location)) and not allow_global:
raise RuntimeError("location not created nor specified")
version_str = "python{0}".format(".".join([str(v) for v in sys.version_info[:2]]))
is_python = command in ("python", os.path.basename(sys.executable), version_str)
if not allow_global:
if os.name == "nt":
p = find_windows_executable(os.path.join(location, "Scripts"), command)
else:
p = os.path.join(location, "bin", command)
else:
if command == "python":
if is_python:
p = sys.executable
if not os.path.exists(p):
if command == "python":
if is_python:
p = sys.executable or system_which("python")
else:
p = system_which(command)
@@ -323,26 +327,18 @@ def find_a_system_python(line):
* Search for "python" and "pythonX.Y" executables in PATH to find a match.
* Nothing fits, return None.
"""
if not line:
return None
if os.path.isabs(line):
return line
from .vendor.pythonfinder import Finder
finder = Finder(system=False, global_search=True)
if not line:
return next(iter(finder.find_all_python_versions()), None)
# Use the windows finder executable
if (line.startswith("py ") or line.startswith("py.exe ")) and os.name == "nt":
line = line.split(" ", 1)[1].lstrip("-")
elif line.startswith("py"):
python_entry = finder.which(line)
if python_entry:
return python_entry.path.as_posix()
return None
python_entry = finder.find_python_version(line)
if not python_entry:
python_entry = finder.which("python{0}".format(line))
if python_entry:
return python_entry.path.as_posix()
return None
python_entry = find_python(finder, line)
return python_entry
def ensure_python(three=None, python=None):
@@ -1433,8 +1429,8 @@ def pip_install(
if "--hash" not in f.read():
ignore_hashes = True
else:
ignore_hashes = True
install_reqs = requirement.as_line(as_list=True, include_hashes=not ignore_hashes)
ignore_hashes = True if not requirement.hashes else False
install_reqs = requirement.as_line(as_list=True)
if not requirement.markers:
install_reqs = [escape_cmd(r) for r in install_reqs]
elif len(install_reqs) > 1:
@@ -1507,18 +1503,59 @@ def pip_download(package_name):
return c
def fallback_which(command, location=None, allow_global=False, system=False):
"""
A fallback implementation of the `which` utility command that relies exclusively on
searching the path for commands.
:param str command: The command to search for, optional
:param str location: The search location to prioritize (prepend to path), defaults to None
:param bool allow_global: Whether to search the global path, defaults to False
:param bool system: Whether to use the system python instead of pipenv's python, defaults to False
:raises ValueError: Raised if no command is provided
:raises TypeError: Raised if the command provided is not a string
:return: A path to the discovered command location
:rtype: str
"""
from .vendor.pythonfinder import Finder
if not command:
raise ValueError("fallback_which: Must provide a command to search for...")
if not isinstance(command, six.string_types):
raise TypeError("Provided command must be a string, received {0!r}".format(command))
global_search = system or allow_global
finder = Finder(system=False, global_search=global_search, path=location)
if is_python_command(command):
result = find_python(finder, command)
if result:
return result
result = finder.which(command)
if result:
return result.path.as_posix()
return ""
def which_pip(allow_global=False):
"""Returns the location of virtualenv-installed pip."""
location = None
if "VIRTUAL_ENV" in os.environ:
location = os.environ["VIRTUAL_ENV"]
if allow_global:
if "VIRTUAL_ENV" in os.environ:
return which("pip", location=os.environ["VIRTUAL_ENV"])
if location:
pip = which("pip", location=location)
if pip:
return pip
for p in ("pip", "pip3", "pip2"):
where = system_which(p)
if where:
return where
return which("pip")
pip = which("pip")
if not pip:
pip = fallback_which("pip", allow_global=allow_global, location=location)
return pip
def system_which(command, mult=False):
@@ -1528,6 +1565,7 @@ def system_which(command, mult=False):
vistir.compat.fs_str(k): vistir.compat.fs_str(val)
for k, val in os.environ.items()
}
result = None
try:
c = delegator.run("{0} {1}".format(_which, command))
try:
@@ -1542,21 +1580,20 @@ def system_which(command, mult=False):
)
assert c.return_code == 0
except AssertionError:
return None if not mult else []
result = fallback_which(command, allow_global=True)
except TypeError:
from .vendor.pythonfinder import Finder
finder = Finder()
result = finder.which(command)
if result:
return result.path.as_posix()
return
if not result:
result = fallback_which(command, allow_global=True)
else:
result = c.out.strip() or c.err.strip()
if mult:
return result.split("\n")
if not result:
result = next(iter([c.out, c.err]), "").split("\n")
result = next(iter(result)) if not mult else result
return result
if not result:
result = fallback_which(command, allow_global=True)
result = [result] if mult else result
return result
else:
return result.split("\n")[0]
def format_help(help):
@@ -2173,6 +2210,7 @@ def do_uninstall(
p for normalized, p in selected_pkg_map.items()
if normalized in (used_packages - bad_pkgs)
]
pip_path = None
for normalized, package_name in selected_pkg_map.items():
click.echo(
crayons.white(
@@ -2182,12 +2220,10 @@ def do_uninstall(
# Uninstall the package.
if package_name in packages_to_remove:
with project.environment.activated():
cmd = "{0} uninstall {1} -y".format(
escape_grouped_arguments(which_pip(allow_global=system)), package_name,
)
if environments.is_verbose():
click.echo("$ {0}".format(cmd))
c = delegator.run(cmd)
if pip_path is None:
pip_path = which_pip(allow_global=system)
cmd = [pip_path, "uninstall", package_name, "-y"]
c = run_command(cmd)
click.echo(crayons.blue(c.out))
if c.return_code != 0:
failure = True
@@ -2441,6 +2477,7 @@ def do_check(
args=None,
pypi_mirror=None,
):
from pipenv.vendor.vistir.compat import JSONDecodeError
if not system:
# Ensure that virtualenv is available.
ensure_project(
@@ -2471,18 +2508,27 @@ def do_check(
sys.exit(1)
else:
sys.exit(0)
click.echo(crayons.normal(fix_utf8("Checking PEP 508 requirements…"), bold=True))
if system:
python = system_which("python")
else:
python = which("python")
# Run the PEP 508 checker in the virtualenv.
c = delegator.run(
'"{0}" {1}'.format(
python, escape_grouped_arguments(pep508checker.__file__.rstrip("cdo"))
)
click.echo(crayons.normal(decode_for_output("Checking PEP 508 requirements…"), bold=True))
pep508checker_path = pep508checker.__file__.rstrip("cdo")
safety_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "patched", "safety.zip"
)
results = simplejson.loads(c.out)
if not system:
python = which("python")
else:
python = system_which("python")
_cmd = [python,]
# Run the PEP 508 checker in the virtualenv.
cmd = _cmd + [pep508checker_path]
c = run_command(cmd)
try:
results = simplejson.loads(c.out.strip())
except JSONDecodeError:
click.echo("{0}\n{1}".format(
crayons.white(decode_for_output("Failed parsing pep508 results: "), bold=True),
c.out.strip()
))
sys.exit(1)
# Load the pipfile.
p = pipfile.Pipfile.load(project.pipfile_location)
failed = False
@@ -2507,13 +2553,9 @@ def do_check(
sys.exit(1)
else:
click.echo(crayons.green("Passed!"))
click.echo(crayons.normal(fix_utf8("Checking installed package safety…"), bold=True))
path = pep508checker.__file__.rstrip("cdo")
path = os.sep.join(__file__.split(os.sep)[:-1] + ["patched", "safety.zip"])
if not system:
python = which("python")
else:
python = system_which("python")
click.echo(crayons.normal(
decode_for_output("Checking installed package safety…"), bold=True)
)
if ignore:
ignored = "--ignore {0}".format(" --ignore ".join(ignore))
click.echo(
@@ -2524,17 +2566,13 @@ def do_check(
)
else:
ignored = ""
c = delegator.run(
'"{0}" {1} check --json --key={2} {3}'.format(
python, escape_grouped_arguments(path), PIPENV_PYUP_API_KEY, ignored
)
)
key = "--key={0}".format(PIPENV_PYUP_API_KEY)
cmd = _cmd + [safety_path, "check", "--json", key, ignored]
c = run_command(cmd)
try:
results = simplejson.loads(c.out)
except ValueError:
click.echo("An error occurred:", err=True)
click.echo(c.err if len(c.err) > 0 else c.out, err=True)
sys.exit(1)
except (ValueError, JSONDecodeError):
raise exceptions.JSONParseError(c.out, c.err)
for (package, resolved, installed, description, vuln) in results:
click.echo(
"{0}: {1} {2} resolved ({3} installed)!".format(
@@ -2553,7 +2591,9 @@ def do_check(
def do_graph(bare=False, json=False, json_tree=False, reverse=False):
from pipenv.vendor.vistir.compat import JSONDecodeError
import pipdeptree
pipdeptree_path = pipdeptree.__file__.rstrip("cdo")
try:
python_path = which("python")
except AttributeError:
@@ -2618,11 +2658,9 @@ def do_graph(bare=False, json=False, json_tree=False, reverse=False):
err=True,
)
sys.exit(1)
cmd = '"{0}" {1} {2} -l'.format(
python_path, escape_grouped_arguments(pipdeptree.__file__.rstrip("cdo")), flag
)
cmd_args = [python_path, pipdeptree_path, flag, "-l"]
c = run_command(cmd_args)
# Run dep-tree.
c = delegator.run(cmd)
if not bare:
if json:
data = []
@@ -2644,9 +2682,14 @@ def do_graph(bare=False, json=False, json_tree=False, reverse=False):
obj["dependencies"] = traverse(obj["dependencies"])
return obj
data = traverse(simplejson.loads(c.out))
click.echo(simplejson.dumps(data, indent=4))
sys.exit(0)
try:
parsed = simplejson.loads(c.out.strip())
except JSONDecodeError:
raise exceptions.JSONParseError(c.out, c.err)
else:
data = traverse(parsed)
click.echo(simplejson.dumps(data, indent=4))
sys.exit(0)
else:
for line in c.out.strip().split("\n"):
# Ignore bad packages as top level.
@@ -2755,8 +2798,8 @@ def do_clean(
)
)
# Uninstall the package.
cmd_str = Script.parse(cmd + [apparent_bad_package]).cmdify()
c = delegator.run(cmd_str, block=True)
cmd = [which_pip(), "uninstall", apparent_bad_package, "-y"]
c = run_command(cmd)
if c.return_code != 0:
failure = True
sys.exit(int(failure))
+69 -20
View File
@@ -9,7 +9,7 @@ from traceback import format_exception, format_tb
import six
from . import environments
from ._compat import fix_utf8
from ._compat import decode_for_output
from .patched import crayons
from .vendor.click._compat import get_text_stderr
from .vendor.click.exceptions import (
@@ -36,7 +36,7 @@ def handle_exception(exc_type, exception, traceback, hook=sys.excepthook):
line = "[pipenv.exceptions.{0!s}]: {1}".format(
exception.__class__.__name__, line
)
click_echo(fix_utf8(line), err=True)
click_echo(decode_for_output(line), err=True)
exception.show()
@@ -64,8 +64,57 @@ class PipenvException(ClickException):
extra = "[pipenv.exceptions.{0!s}]: {1}".format(
self.__class__.__name__, extra
)
extra = decode_for_output(extra, file)
click_echo(extra, file=file)
click_echo(fix_utf8("{0}".format(self.message)), file=file)
click_echo(decode_for_output("{0}".format(self.message), file), file=file)
class PipenvCmdError(PipenvException):
def __init__(self, cmd, out="", err="", exit_code=1):
self.cmd = cmd
self.out = out
self.err = err
self.exit_code = exit_code
message = "Error running command: {0}".format(cmd)
PipenvException.__init__(self, message)
def show(self, file=None):
if file is None:
file = get_text_stderr()
click_echo("{0} {1}".format(
crayons.red("Error running command: "),
crayons.white(decode_for_output("$ {0}".format(self.cmd), file), bold=True)
), err=True)
if self.out:
click_echo("{0} {1}".format(
crayons.white("OUTPUT: "),
decode_for_output(self.out, file)
), err=True)
if self.err:
click_echo("{0} {1}".format(
crayons.white("STDERR: "),
decode_for_output(self.err, file)
), err=True)
class JSONParseError(PipenvException):
def __init__(self, contents="", error_text=""):
self.error_text = error_text
PipenvException.__init__(self, contents)
def show(self, file=None):
if file is None:
file = get_text_stderr()
message = "{0}\n{1}".format(
crayons.white("Failed parsing JSON results:", bold=True),
decode_for_output(self.message.strip(), file)
)
click_echo(self.message, err=True)
if self.error_text:
click_echo("{0} {1}".format(
crayons.white("ERROR TEXT:", bold=True),
decode_for_output(self.error_text, file)
), err=True)
class PipenvUsageError(UsageError):
@@ -78,7 +127,7 @@ class PipenvUsageError(UsageError):
message = formatted_message.format(msg_prefix, crayons.white(message, bold=True))
self.message = message
extra = kwargs.pop("extra", [])
UsageError.__init__(self, fix_utf8(message), ctx)
UsageError.__init__(self, decode_for_output(message), ctx)
self.extra = extra
def show(self, file=None):
@@ -93,7 +142,7 @@ class PipenvUsageError(UsageError):
for extra in self.extra:
if color:
extra = getattr(crayons, color, "blue")(extra)
click_echo(fix_utf8(extra), file=file)
click_echo(decode_for_output(extra, file), file=file)
hint = ''
if (self.cmd is not None and
self.cmd.get_help_option(self.ctx) is not None):
@@ -117,7 +166,7 @@ class PipenvFileError(FileError):
crayons.white("{0} not found!".format(filename), bold=True),
message
)
FileError.__init__(self, filename=filename, hint=fix_utf8(message), **kwargs)
FileError.__init__(self, filename=filename, hint=decode_for_output(message), **kwargs)
self.extra = extra
def show(self, file=None):
@@ -127,7 +176,7 @@ class PipenvFileError(FileError):
if isinstance(self.extra, six.string_types):
self.extra = [self.extra,]
for extra in self.extra:
click_echo(fix_utf8(extra), file=file)
click_echo(decode_for_output(extra, file), file=file)
click_echo(self.message, file=file)
@@ -137,10 +186,10 @@ class PipfileNotFound(PipenvFileError):
message = ("{0} {1}".format(
crayons.red("Aborting!", bold=True),
crayons.white("Please ensure that the file exists and is located in your"
" project root directory.", bold=True)
" project root directory.", bold=True)
)
)
super(PipfileNotFound, self).__init__(filename, message=fix_utf8(message), extra=extra, **kwargs)
super(PipfileNotFound, self).__init__(filename, message=decode_for_output(message), extra=extra, **kwargs)
class LockfileNotFound(PipenvFileError):
@@ -151,7 +200,7 @@ class LockfileNotFound(PipenvFileError):
crayons.red("$ pipenv lock", bold=True),
crayons.white("before you can continue.", bold=True)
)
super(LockfileNotFound, self).__init__(filename, message=fix_utf8(message), extra=extra, **kwargs)
super(LockfileNotFound, self).__init__(filename, message=decode_for_output(message), extra=extra, **kwargs)
class DeployException(PipenvUsageError):
@@ -159,13 +208,13 @@ class DeployException(PipenvUsageError):
if not message:
message = crayons.normal("Aborting deploy", bold=True)
extra = kwargs.pop("extra", [])
PipenvUsageError.__init__(self, message=fix_utf8(message), extra=extra, **kwargs)
PipenvUsageError.__init__(self, message=decode_for_output(message), extra=extra, **kwargs)
class PipenvOptionsError(PipenvUsageError):
def __init__(self, option_name, message=None, ctx=None, **kwargs):
extra = kwargs.pop("extra", [])
PipenvUsageError.__init__(self, message=fix_utf8(message), ctx=ctx, **kwargs)
PipenvUsageError.__init__(self, message=decode_for_output(message), ctx=ctx, **kwargs)
self.extra = extra
self.option_name = option_name
@@ -192,7 +241,7 @@ class PipfileException(PipenvFileError):
hint = "{0} {1}".format(crayons.red("ERROR (PACKAGE NOT INSTALLED):"), hint)
filename = project.pipfile_location
extra = kwargs.pop("extra", [])
PipenvFileError.__init__(self, filename, fix_utf8(hint), extra=extra, **kwargs)
PipenvFileError.__init__(self, filename, decode_for_output(hint), extra=extra, **kwargs)
class SetupException(PipenvException):
@@ -208,7 +257,7 @@ class VirtualenvException(PipenvException):
"There was an unexpected error while activating your virtualenv. "
"Continuing anyway..."
)
PipenvException.__init__(self, fix_utf8(message), **kwargs)
PipenvException.__init__(self, decode_for_output(message), **kwargs)
class VirtualenvActivationException(VirtualenvException):
@@ -219,7 +268,7 @@ class VirtualenvActivationException(VirtualenvException):
"not activated. Continuing anyway…"
)
self.message = message
VirtualenvException.__init__(self, fix_utf8(message), **kwargs)
VirtualenvException.__init__(self, decode_for_output(message), **kwargs)
class VirtualenvCreationException(VirtualenvException):
@@ -227,7 +276,7 @@ class VirtualenvCreationException(VirtualenvException):
if not message:
message = "Failed to create virtual environment."
self.message = message
VirtualenvException.__init__(self, fix_utf8(message), **kwargs)
VirtualenvException.__init__(self, decode_for_output(message), **kwargs)
class UninstallError(PipenvException):
@@ -243,7 +292,7 @@ class UninstallError(PipenvException):
crayons.yellow(str(package), bold=True)
)
self.exit_code = return_code
PipenvException.__init__(self, message=fix_utf8(message), extra=extra)
PipenvException.__init__(self, message=decode_for_output(message), extra=extra)
self.extra = extra
@@ -260,7 +309,7 @@ class InstallError(PipenvException):
crayons.yellow("Package installation failed...")
)
extra = kwargs.pop("extra", [])
PipenvException.__init__(self, message=fix_utf8(message), extra=extra, **kwargs)
PipenvException.__init__(self, message=decode_for_output(message), extra=extra, **kwargs)
class CacheError(PipenvException):
@@ -271,7 +320,7 @@ class CacheError(PipenvException):
crayons.white(path),
crayons.white('Consider trying "pipenv lock --clear" to clear the cache.')
)
super(PipenvException, self).__init__(message=fix_utf8(message))
PipenvException.__init__(self, message=decode_for_output(message))
class ResolutionFailure(PipenvException):
@@ -304,4 +353,4 @@ class ResolutionFailure(PipenvException):
"See PEP440 for more information."
)
)
super(ResolutionFailure, self).__init__(fix_utf8(message), extra=extra)
super(ResolutionFailure, self).__init__(decode_for_output(message), extra=extra)
+100 -48
View File
@@ -30,7 +30,7 @@ import crayons
import parse
from . import environments
from .exceptions import PipenvUsageError
from .exceptions import PipenvUsageError, PipenvCmdError
from .pep508checker import lookup
from .vendor.urllib3 import util as urllib3_util
@@ -119,6 +119,45 @@ def convert_toml_outline_tables(parsed):
return parsed
def run_command(cmd, *args, **kwargs):
"""
Take an input command and run it, handling exceptions and error codes and returning
its stdout and stderr.
:param cmd: The list of command and arguments.
:type cmd: list
:returns: A 2-tuple of the output and error from the command
:rtype: Tuple[str, str]
:raises: exceptions.PipenvCmdError
"""
from pipenv.vendor import delegator
from ._compat import decode_output
from .cmdparse import Script
if isinstance(cmd, (six.string_types, list, tuple)):
cmd = Script.parse(cmd)
if not isinstance(cmd, Script):
raise TypeError("Command input must be a string, list or tuple")
if "env" not in kwargs:
kwargs["env"] = os.environ.copy()
try:
cmd_string = cmd.cmdify()
except TypeError:
click_echo("Error turning command into string: {0}".format(cmd), err=True)
sys.exit(1)
if environments.is_verbose():
click_echo("Running command: $ {0}".format(cmd_string, err=True))
c = delegator.run(cmd_string, *args, **kwargs)
c.block()
if environments.is_verbose():
click_echo("Command output: {0}".format(
crayons.blue(decode_output(c.out))
), err=True)
if not c.ok:
raise PipenvCmdError(cmd_string, c.out, c.err, c.return_code)
return c
def parse_python_version(output):
"""Parse a Python version output returned by `python --version`.
@@ -782,6 +821,7 @@ def create_spinner(text, nospin=None, spinner_name=None):
) as sp:
yield sp
def resolve(cmd, sp):
import delegator
from .cmdparse import Script
@@ -1225,53 +1265,6 @@ def proper_case(package_name):
return good_name
def split_section(input_file, section_suffix, test_function):
"""
Split a pipfile or a lockfile section out by section name and test function
:param dict input_file: A dictionary containing either a pipfile or lockfile
:param str section_suffix: A string of the name of the section
:param func test_function: A test function to test against the value in the key/value pair
>>> split_section(my_lockfile, 'vcs', is_vcs)
{
'default': {
"six": {
"hashes": [
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb",
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9"
],
"version": "==1.11.0"
}
},
'default-vcs': {
"e1839a8": {
"editable": true,
"path": "."
}
}
}
"""
pipfile_sections = ("packages", "dev-packages")
lockfile_sections = ("default", "develop")
if any(section in input_file for section in pipfile_sections):
sections = pipfile_sections
elif any(section in input_file for section in lockfile_sections):
sections = lockfile_sections
else:
# return the original file if we can't find any pipfile or lockfile sections
return input_file
for section in sections:
split_dict = {}
entries = input_file.get(section, {})
for k in list(entries.keys()):
if test_function(entries.get(k)):
split_dict[k] = entries.pop(k)
input_file["-".join([section, section_suffix])] = split_dict
return input_file
def get_windows_path(*args):
"""Sanitize a path for windows environments
@@ -1854,3 +1847,62 @@ def get_pipenv_dist(pkg="pipenv", pipenv_site=None):
pipenv_site = os.path.dirname(pipenv_libdir)
pipenv_dist, _ = find_site_path(pkg, site_dir=pipenv_site)
return pipenv_dist
def find_python(finder, line=None):
"""
Given a `pythonfinder.Finder` instance and an optional line, find a corresponding python
:param finder: A :class:`pythonfinder.Finder` instance to use for searching
:type finder: :class:pythonfinder.Finder`
:param str line: A version, path, name, or nothing, defaults to None
:return: A path to python
:rtype: str
"""
if not finder:
from pipenv.vendor.pythonfinder import Finder
finder = Finder(global_search=True)
if not line:
result = next(iter(finder.find_all_python_versions()), None)
elif line and line[0].isnumeric() or re.match(r'[\d\.]+', line):
result = finder.find_python_version(line)
else:
result = finder.find_python_version(name=line)
if not result:
result = finder.which(line)
if not result and not line.startswith("python"):
line = "python{0}".format(line)
result = find_python(finder, line)
if not result:
result = next(iter(finder.find_all_python_versions()), None)
if result:
if not isinstance(result, six.string_types):
return result.path.as_posix()
return result
return
def is_python_command(line):
"""
Given an input, checks whether the input is a request for python or notself.
This can be a version, a python runtime name, or a generic 'python' or 'pythonX.Y'
:param str line: A potential request to find python
:returns: Whether the line is a python lookup
:rtype: bool
"""
if not isinstance(line, six.string_types):
raise TypeError("Not a valid command to check: {0!r}".format(line))
from pipenv.vendor.pythonfinder.utils import PYTHON_IMPLEMENTATIONS
is_version = re.match(r'[\d\.]+', line)
if line.startswith("python") or is_version or \
any(line.startswith(v) for v in PYTHON_IMPLEMENTATIONS):
return True
# we are less sure about this but we can guess
if line.startswith("py"):
return True
return False
+23
View File
@@ -237,6 +237,29 @@ class TestUtils:
assert os.path.exists(output)
os.remove(output)
@pytest.mark.utils
@pytest.mark.parametrize('line, expected', [
("python", True),
("python3.7", True),
("python2.7", True),
("python2", True),
("python3", True),
("pypy3", True),
("anaconda3-5.3.0", True),
("which", False),
("vim", False),
("miniconda", True),
("micropython", True),
("ironpython", True),
("jython3.5", True),
("2", True),
("2.7", True),
("3.7", True),
("3", True)
])
def test_is_python_command(self, line, expected):
assert pipenv.utils.is_python_command(line) == expected
@pytest.mark.utils
def test_new_line_end_of_toml_file(this):
# toml file that needs clean up