ducktools-pythonfinder 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ducktools/pythonfinder/__init__.py +40 -0
- ducktools/pythonfinder/__main__.py +106 -0
- ducktools/pythonfinder/darwin/__init__.py +19 -0
- ducktools/pythonfinder/details_script.py +58 -0
- ducktools/pythonfinder/linux/__init__.py +53 -0
- ducktools/pythonfinder/linux/pyenv_search.py +80 -0
- ducktools/pythonfinder/shared.py +181 -0
- ducktools/pythonfinder/win32/__init__.py +14 -0
- ducktools/pythonfinder/win32/pyenv_search.py +57 -0
- ducktools/pythonfinder/win32/registry_search.py +110 -0
- ducktools_pythonfinder-0.1.0.dist-info/COPYING +674 -0
- ducktools_pythonfinder-0.1.0.dist-info/COPYING.LESSER +165 -0
- ducktools_pythonfinder-0.1.0.dist-info/METADATA +112 -0
- ducktools_pythonfinder-0.1.0.dist-info/RECORD +16 -0
- ducktools_pythonfinder-0.1.0.dist-info/WHEEL +5 -0
- ducktools_pythonfinder-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ducktools-pythonfinder
|
|
2
|
+
# Copyright (C) 2024 David C Ellis
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU Lesser General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
# Find platform python versions
|
|
18
|
+
|
|
19
|
+
__version__ = "v0.1.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"get_python_installs",
|
|
23
|
+
"list_python_installs",
|
|
24
|
+
"PythonInstall",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
import sys
|
|
28
|
+
from .shared import PythonInstall
|
|
29
|
+
|
|
30
|
+
match sys.platform: # pragma: no cover
|
|
31
|
+
case "win32":
|
|
32
|
+
from .win32 import get_python_installs
|
|
33
|
+
case "darwin":
|
|
34
|
+
from .darwin import get_python_installs
|
|
35
|
+
case _:
|
|
36
|
+
from .linux import get_python_installs
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def list_python_installs():
|
|
40
|
+
return sorted(get_python_installs(), reverse=True, key=lambda x: x.version)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# ducktools-pythonfinder
|
|
2
|
+
# Copyright (C) 2024 David C Ellis
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU Lesser General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from ducktools.lazyimporter import LazyImporter, ModuleImport
|
|
21
|
+
from ducktools.pythonfinder import list_python_installs
|
|
22
|
+
|
|
23
|
+
_laz = LazyImporter([ModuleImport("argparse")])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_args(args):
|
|
27
|
+
parser = _laz.argparse.ArgumentParser(
|
|
28
|
+
prog="ducktools-pythonfinder",
|
|
29
|
+
description="Discover base Python installs",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument("--min", help="Specify minimum Python version")
|
|
32
|
+
parser.add_argument("--max", help="Specify maximum Python version")
|
|
33
|
+
parser.add_argument("--exact", help="Specify exact Python version")
|
|
34
|
+
|
|
35
|
+
vals = parser.parse_args(args)
|
|
36
|
+
|
|
37
|
+
if vals.min:
|
|
38
|
+
min_ver = tuple(int(i) for i in vals.min.split("."))
|
|
39
|
+
else:
|
|
40
|
+
min_ver = None
|
|
41
|
+
|
|
42
|
+
if vals.max:
|
|
43
|
+
max_ver = tuple(int(i) for i in vals.max.split("."))
|
|
44
|
+
else:
|
|
45
|
+
max_ver = None
|
|
46
|
+
|
|
47
|
+
if vals.exact:
|
|
48
|
+
exact = tuple(int(i) for i in vals.exact.split("."))
|
|
49
|
+
else:
|
|
50
|
+
exact = None
|
|
51
|
+
|
|
52
|
+
return min_ver, max_ver, exact
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
if sys.argv[1:]:
|
|
57
|
+
min_ver, max_ver, exact = parse_args(sys.argv[1:])
|
|
58
|
+
else:
|
|
59
|
+
min_ver, max_ver, exact = None, None, None
|
|
60
|
+
|
|
61
|
+
installs = list_python_installs()
|
|
62
|
+
headings = ["Python Version", "Executable Location"]
|
|
63
|
+
max_executable_len = max(
|
|
64
|
+
len(headings[1]), max(len(inst.executable) for inst in installs)
|
|
65
|
+
)
|
|
66
|
+
headings_str = f"| {headings[0]} | {headings[1]:<{max_executable_len}s} |"
|
|
67
|
+
|
|
68
|
+
print("Discoverable Python Installs")
|
|
69
|
+
if sys.platform == "win32":
|
|
70
|
+
print("+ - Listed in the Windows Registry ")
|
|
71
|
+
print("* - This is the active python executable used to call this module")
|
|
72
|
+
print(
|
|
73
|
+
"** - This is the parent python executable of the venv used to call this module"
|
|
74
|
+
)
|
|
75
|
+
print()
|
|
76
|
+
print(headings_str)
|
|
77
|
+
print(f"| {'-' * len(headings[0])} | {'-' * max_executable_len} |")
|
|
78
|
+
for install in installs:
|
|
79
|
+
if min_ver and install.version < min_ver:
|
|
80
|
+
continue
|
|
81
|
+
elif max_ver and install.version > max_ver:
|
|
82
|
+
continue
|
|
83
|
+
elif exact:
|
|
84
|
+
mismatch = False
|
|
85
|
+
for i, val in enumerate(exact):
|
|
86
|
+
if val != install.version[i]:
|
|
87
|
+
mismatch = True
|
|
88
|
+
break
|
|
89
|
+
if mismatch:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
version_str = install.version_str
|
|
93
|
+
if install.executable == sys.executable:
|
|
94
|
+
version_str = f"*{version_str}"
|
|
95
|
+
elif sys.prefix != sys.base_prefix and install.executable.startswith(
|
|
96
|
+
sys.base_prefix
|
|
97
|
+
):
|
|
98
|
+
version_str = f"**{version_str}"
|
|
99
|
+
|
|
100
|
+
if sys.platform == "win32" and install.metadata.get("InWindowsRegistry"):
|
|
101
|
+
version_str = f"+{version_str}"
|
|
102
|
+
|
|
103
|
+
print(f"| {version_str:>14s} | {install.executable:<{max_executable_len}s} |")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# ducktools-pythonfinder
|
|
2
|
+
# Copyright (C) 2024 David C Ellis
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU Lesser General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
"""Currently just copied from linux"""
|
|
18
|
+
|
|
19
|
+
from ..linux import get_python_installs, get_path_pythons, get_pyenv_pythons
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# ducktools-pythonfinder
|
|
2
|
+
# Copyright (C) 2024 David C Ellis
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU Lesser General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
Get the details from a python install as JSON
|
|
19
|
+
"""
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_details():
|
|
24
|
+
try:
|
|
25
|
+
implementation = sys.implementation.name
|
|
26
|
+
except AttributeError: # pragma: no cover
|
|
27
|
+
# Probably Python 2
|
|
28
|
+
import platform
|
|
29
|
+
|
|
30
|
+
implementation = platform.python_implementation().lower()
|
|
31
|
+
metadata = {}
|
|
32
|
+
else:
|
|
33
|
+
if implementation != "cpython": # pragma: no cover
|
|
34
|
+
metadata = {"{}_version".format(implementation): sys.implementation.version}
|
|
35
|
+
else:
|
|
36
|
+
metadata = {}
|
|
37
|
+
|
|
38
|
+
install = dict(
|
|
39
|
+
version=list(sys.version_info),
|
|
40
|
+
executable=sys.executable,
|
|
41
|
+
architecture="64bit" if (sys.maxsize > 2**32) else "32bit",
|
|
42
|
+
implementation=implementation,
|
|
43
|
+
metadata=metadata,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return install
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main():
|
|
50
|
+
import json
|
|
51
|
+
|
|
52
|
+
install = get_details()
|
|
53
|
+
|
|
54
|
+
sys.stdout.write(json.dumps(install))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__": # pragma: no cover
|
|
58
|
+
main()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ducktools-pythonfinder
|
|
2
|
+
# Copyright (C) 2024 David C Ellis
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU Lesser General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import os.path
|
|
19
|
+
import itertools
|
|
20
|
+
from _collections_abc import Iterator
|
|
21
|
+
|
|
22
|
+
from ..shared import PythonInstall, get_folder_pythons
|
|
23
|
+
from .pyenv_search import get_pyenv_pythons
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
PATH_FOLDERS = os.environ.get("PATH").split(":")
|
|
27
|
+
_PYENV_ROOT = os.environ.get("PYENV_ROOT")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_path_pythons() -> Iterator[PythonInstall]:
|
|
31
|
+
exe_names = set()
|
|
32
|
+
|
|
33
|
+
for fld in PATH_FOLDERS:
|
|
34
|
+
# Don't retrieve pyenv installs
|
|
35
|
+
if _PYENV_ROOT and fld.startswith(_PYENV_ROOT):
|
|
36
|
+
continue
|
|
37
|
+
elif not os.path.exists(fld):
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
for install in get_folder_pythons(fld):
|
|
41
|
+
name = os.path.basename(install.executable)
|
|
42
|
+
if name not in exe_names:
|
|
43
|
+
yield install
|
|
44
|
+
exe_names.add(name)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_python_installs():
|
|
48
|
+
listed_pythons = set()
|
|
49
|
+
|
|
50
|
+
for py in itertools.chain(get_pyenv_pythons(), get_path_pythons()):
|
|
51
|
+
if py.executable not in listed_pythons:
|
|
52
|
+
yield py
|
|
53
|
+
listed_pythons.add(py.executable)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# ducktools-pythonfinder
|
|
2
|
+
# Copyright (C) 2024 David C Ellis
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU Lesser General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
Discover python installs that have been created with pyenv
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import os.path
|
|
23
|
+
from _collections_abc import Iterator
|
|
24
|
+
|
|
25
|
+
from ducktools.lazyimporter import LazyImporter, ModuleImport
|
|
26
|
+
|
|
27
|
+
from ..shared import PythonInstall
|
|
28
|
+
from .. import details_script
|
|
29
|
+
|
|
30
|
+
_laz = LazyImporter(
|
|
31
|
+
[
|
|
32
|
+
ModuleImport("re"),
|
|
33
|
+
ModuleImport("subprocess"),
|
|
34
|
+
ModuleImport("json"),
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# pyenv folder names
|
|
39
|
+
PYTHON_VER_RE = r"\d{1,2}\.\d{1,2}\.\d+[a-z]*\d*"
|
|
40
|
+
PYPY_VER_RE = r"^pypy(?P<pyversion>\d{1,2}\.\d+)-(?P<pypyversion>[\d\.]*)$"
|
|
41
|
+
|
|
42
|
+
# 'pypy -V' output matcher
|
|
43
|
+
PYPY_V_OUTPUT = (
|
|
44
|
+
r"(?is)python (?P<python_version>\d+\.\d+\.\d+[a-z]*\d*).*?"
|
|
45
|
+
r"pypy (?P<pypy_version>\d+\.\d+\.\d+[a-z]*\d*).*"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
PYENV_VERSIONS_FOLDER = os.path.join(os.environ.get("PYENV_ROOT", ""), "versions")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_pyenv_pythons(
|
|
52
|
+
versions_folder: str | os.PathLike = PYENV_VERSIONS_FOLDER,
|
|
53
|
+
) -> Iterator[PythonInstall]:
|
|
54
|
+
if not os.path.exists(versions_folder):
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Sorting puts standard python versions before pypy
|
|
58
|
+
# This can lead to much faster returns by potentially yielding
|
|
59
|
+
# the required python version before checking pypy
|
|
60
|
+
|
|
61
|
+
for p in sorted(os.scandir(versions_folder), key=lambda x: x.path):
|
|
62
|
+
executable = os.path.join(p.path, "bin/python")
|
|
63
|
+
|
|
64
|
+
if os.path.exists(executable):
|
|
65
|
+
if _laz.re.fullmatch(PYTHON_VER_RE, p.name):
|
|
66
|
+
yield PythonInstall.from_str(p.name, executable)
|
|
67
|
+
elif _laz.re.fullmatch(PYPY_VER_RE, p.name):
|
|
68
|
+
details_output = _laz.subprocess.run(
|
|
69
|
+
[executable, details_script.__file__],
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
).stdout
|
|
73
|
+
|
|
74
|
+
if details_output:
|
|
75
|
+
try:
|
|
76
|
+
details = _laz.json.loads(details_output)
|
|
77
|
+
except _laz.json.JSONDecodeError:
|
|
78
|
+
pass
|
|
79
|
+
else:
|
|
80
|
+
yield PythonInstall.from_json(**details)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# ducktools-pythonfinder
|
|
2
|
+
# Copyright (C) 2024 David C Ellis
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU Lesser General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
import os
|
|
19
|
+
import os.path
|
|
20
|
+
|
|
21
|
+
from prefab_classes import prefab, attribute
|
|
22
|
+
from ducktools.lazyimporter import LazyImporter, ModuleImport, FromImport
|
|
23
|
+
|
|
24
|
+
from . import details_script
|
|
25
|
+
|
|
26
|
+
_laz = LazyImporter(
|
|
27
|
+
[
|
|
28
|
+
ModuleImport("re"),
|
|
29
|
+
ModuleImport("subprocess"),
|
|
30
|
+
ModuleImport("platform"),
|
|
31
|
+
FromImport("glob", "glob"),
|
|
32
|
+
ModuleImport("json"),
|
|
33
|
+
]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
FULL_PY_VER_RE = r"(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<micro>\d*)(?P<releaselevel>[a-zA-Z]*)(?P<serial>\d*)"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@prefab
|
|
41
|
+
class PythonInstall:
|
|
42
|
+
version: tuple[int, int, int, str, int]
|
|
43
|
+
executable: str
|
|
44
|
+
architecture: str = "64bit"
|
|
45
|
+
implementation: str = "cpython"
|
|
46
|
+
metadata: dict = attribute(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def version_str(self) -> str:
|
|
50
|
+
major, minor, micro, releaselevel, serial = self.version
|
|
51
|
+
|
|
52
|
+
match releaselevel:
|
|
53
|
+
case "alpha":
|
|
54
|
+
releaselevel = "a"
|
|
55
|
+
case "beta":
|
|
56
|
+
releaselevel = "b"
|
|
57
|
+
case "candidate":
|
|
58
|
+
releaselevel = "rc"
|
|
59
|
+
case _:
|
|
60
|
+
releaselevel = ""
|
|
61
|
+
|
|
62
|
+
if serial == 0:
|
|
63
|
+
serial = ""
|
|
64
|
+
else:
|
|
65
|
+
serial = f"{serial}"
|
|
66
|
+
|
|
67
|
+
return f"{major}.{minor}.{micro}{releaselevel}{serial}"
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_str(
|
|
71
|
+
cls,
|
|
72
|
+
version: str,
|
|
73
|
+
executable: str,
|
|
74
|
+
architecture: str = "64bit",
|
|
75
|
+
implementation: str = "cpython",
|
|
76
|
+
metadata: dict | None = None,
|
|
77
|
+
):
|
|
78
|
+
parsed_version = _laz.re.fullmatch(FULL_PY_VER_RE, version)
|
|
79
|
+
|
|
80
|
+
if not parsed_version:
|
|
81
|
+
raise ValueError(f"{version!r} is not a recognised Python version string.")
|
|
82
|
+
|
|
83
|
+
major, minor, micro, releaselevel, serial = parsed_version.groups()
|
|
84
|
+
|
|
85
|
+
match releaselevel:
|
|
86
|
+
case "a":
|
|
87
|
+
releaselevel = "alpha"
|
|
88
|
+
case "b":
|
|
89
|
+
releaselevel = "beta"
|
|
90
|
+
case "rc":
|
|
91
|
+
releaselevel = "candidate"
|
|
92
|
+
case _:
|
|
93
|
+
releaselevel = "final"
|
|
94
|
+
|
|
95
|
+
version_tuple = (
|
|
96
|
+
int(major),
|
|
97
|
+
int(minor),
|
|
98
|
+
int(micro) if micro else 0,
|
|
99
|
+
releaselevel,
|
|
100
|
+
int(serial if serial != "" else 0),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# noinspection PyArgumentList
|
|
104
|
+
return cls(
|
|
105
|
+
version=version_tuple,
|
|
106
|
+
executable=executable,
|
|
107
|
+
architecture=architecture,
|
|
108
|
+
implementation=implementation,
|
|
109
|
+
metadata=metadata,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_json(cls, version, executable, architecture, implementation, metadata):
|
|
114
|
+
if arch_ver := metadata.get(f"{implementation}_version"):
|
|
115
|
+
metadata[f"{implementation}_version"] = tuple(arch_ver)
|
|
116
|
+
|
|
117
|
+
return cls(
|
|
118
|
+
tuple(version), executable, architecture, implementation, metadata # noqa
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def get_pip_version(self) -> str | None:
|
|
122
|
+
"""
|
|
123
|
+
Get the version of pip installed on a python install.
|
|
124
|
+
|
|
125
|
+
:return: None if pip is not found or the command fails
|
|
126
|
+
version number as string otherwise.
|
|
127
|
+
"""
|
|
128
|
+
pip_call = _laz.subprocess.run(
|
|
129
|
+
[self.executable, "-c", "import pip; print(pip.__version__, end='')"],
|
|
130
|
+
text=True,
|
|
131
|
+
capture_output=True,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Pip call failed
|
|
135
|
+
if pip_call.returncode != 0:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
return pip_call.stdout
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _python_exe_regex(basename: str = "python"):
|
|
142
|
+
if sys.platform == "win32":
|
|
143
|
+
return _laz.re.compile(rf"{basename}\d?\.?\d*\.exe")
|
|
144
|
+
else:
|
|
145
|
+
return _laz.re.compile(rf"{basename}\d?\.?\d*")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_install_details(executable: str) -> PythonInstall | None:
|
|
149
|
+
try:
|
|
150
|
+
detail_output = _laz.subprocess.run(
|
|
151
|
+
[executable, details_script.__file__],
|
|
152
|
+
capture_output=True,
|
|
153
|
+
text=True,
|
|
154
|
+
check=True,
|
|
155
|
+
).stdout
|
|
156
|
+
except (_laz.subprocess.CalledProcessError, FileNotFoundError):
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
output = _laz.json.loads(detail_output)
|
|
161
|
+
except _laz.json.JSONDecodeError:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
return PythonInstall.from_json(**output)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_folder_pythons(
|
|
168
|
+
base_folder: str | os.PathLike, basenames: tuple[str] = ("python", "pypy")
|
|
169
|
+
):
|
|
170
|
+
regexes = [_python_exe_regex(name) for name in basenames]
|
|
171
|
+
|
|
172
|
+
with os.scandir(base_folder) as fld:
|
|
173
|
+
for file_path in fld:
|
|
174
|
+
if (
|
|
175
|
+
not file_path.is_symlink()
|
|
176
|
+
and file_path.is_file()
|
|
177
|
+
and any(reg.fullmatch(file_path.name) for reg in regexes)
|
|
178
|
+
):
|
|
179
|
+
install = get_install_details(file_path.path)
|
|
180
|
+
if install:
|
|
181
|
+
yield install
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from _collections_abc import Iterator
|
|
2
|
+
import itertools
|
|
3
|
+
|
|
4
|
+
from ..shared import PythonInstall
|
|
5
|
+
from .pyenv_search import get_pyenv_pythons
|
|
6
|
+
from .registry_search import get_registered_pythons
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_python_installs() -> Iterator[PythonInstall]:
|
|
10
|
+
listed_installs = set()
|
|
11
|
+
for py in itertools.chain(get_registered_pythons(), get_pyenv_pythons()):
|
|
12
|
+
if py.executable not in listed_installs:
|
|
13
|
+
yield py
|
|
14
|
+
listed_installs.add(py.executable)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# ducktools-pythonfinder
|
|
2
|
+
# Copyright (C) 2024 David C Ellis
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU Lesser General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import os.path
|
|
19
|
+
from _collections_abc import Iterator
|
|
20
|
+
|
|
21
|
+
from ..shared import PythonInstall
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
PYENV_VERSIONS_FOLDER = os.path.join(os.environ.get("PYENV_ROOT", ""), "versions")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_pyenv_pythons(
|
|
28
|
+
versions_folder: str | os.PathLike = PYENV_VERSIONS_FOLDER,
|
|
29
|
+
) -> Iterator[PythonInstall]:
|
|
30
|
+
if not os.path.exists(versions_folder):
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
for p in os.scandir(versions_folder):
|
|
34
|
+
executable = os.path.join(p.path, "python.exe")
|
|
35
|
+
|
|
36
|
+
if os.path.exists(executable):
|
|
37
|
+
match p.name.split("-"):
|
|
38
|
+
case (version, arch):
|
|
39
|
+
# win32 in pyenv name means 32 bit python install
|
|
40
|
+
# 'arm' is the only alternative which will be 64bit
|
|
41
|
+
arch = "32bit" if arch == "win32" else "64bit"
|
|
42
|
+
try:
|
|
43
|
+
yield PythonInstall.from_str(
|
|
44
|
+
version, executable, architecture=arch
|
|
45
|
+
)
|
|
46
|
+
except ValueError:
|
|
47
|
+
pass
|
|
48
|
+
case (version,):
|
|
49
|
+
# If no arch given pyenv will be 64 bit
|
|
50
|
+
try:
|
|
51
|
+
yield PythonInstall.from_str(
|
|
52
|
+
version, executable, architecture="64bit"
|
|
53
|
+
)
|
|
54
|
+
except ValueError:
|
|
55
|
+
pass
|
|
56
|
+
case _:
|
|
57
|
+
pass # Skip unrecognised versions
|