1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5from __future__ import absolute_import
6
7import os
8import subprocess
9import sys
10
11from distutils.version import (
12    StrictVersion,
13)
14
15
16def iter_modules_in_path(*paths):
17    paths = [os.path.abspath(os.path.normcase(p)) + os.sep
18             for p in paths]
19    for name, module in sys.modules.items():
20        if not hasattr(module, '__file__'):
21            continue
22
23        path = module.__file__
24
25        if path.endswith('.pyc'):
26            path = path[:-1]
27        path = os.path.abspath(os.path.normcase(path))
28
29        if any(path.startswith(p) for p in paths):
30            yield path
31
32
33def python_executable_version(exe):
34    """Determine the version of a Python executable by invoking it.
35
36    May raise ``subprocess.CalledProcessError`` or ``ValueError`` on failure.
37    """
38    program = "import sys; print('.'.join(map(str, sys.version_info[0:3])))"
39    out = subprocess.check_output([exe, '-c', program]).rstrip()
40    return StrictVersion(out)
41
42
43def find_python3_executable(min_version='3.5.0'):
44    """Find a Python 3 executable.
45
46    Returns a tuple containing the the path to an executable binary and a
47    version tuple. Both tuple entries will be None if a Python executable
48    could not be resolved.
49    """
50    import which
51
52    if not min_version.startswith('3.'):
53        raise ValueError('min_version expected a 3.x string, got %s' %
54                         min_version)
55
56    min_version = StrictVersion(min_version)
57
58    if sys.version_info.major >= 3:
59        our_version = StrictVersion('%s.%s.%s' % (sys.version_info[0:3]))
60
61        if our_version >= min_version:
62            # This will potentially return a virtualenv Python. It's probably
63            # OK for now...
64            return sys.executable, our_version.version
65
66        # Else fall back to finding another binary.
67
68    # https://www.python.org/dev/peps/pep-0394/ defines how the Python binary
69    # should be named. `python3` should refer to some Python 3. `python` may
70    # refer to a Python 2 or 3. `pythonX.Y` may exist.
71    #
72    # Since `python` is ambiguous and `python3` should always exist, we
73    # ignore `python` here. We instead look for the preferred `python3` first
74    # and fall back to `pythonX.Y` if it isn't found or doesn't meet our
75    # version requirements.
76    names = ['python3']
77
78    # Look for `python3.Y` down to our minimum version.
79    for minor in range(9, min_version.version[1] - 1, -1):
80        names.append('python3.%d' % minor)
81
82    for name in names:
83        try:
84            exe = which.which(name)
85        except which.WhichError:
86            continue
87
88        # We always verify we can invoke the executable and its version is
89        # sane.
90        try:
91            version = python_executable_version(exe)
92        except (subprocess.CalledProcessError, ValueError):
93            continue
94
95        if version >= min_version:
96            return exe, version.version
97
98    return None, None
99