Skip to content

Commit acd64cf

Browse files
authored
Merge pull request #58 from DavidCEllis/add_sysconfig_paths
Add sysconfig paths to the data in a PythonInstall
2 parents 8f8bcf3 + 3d89208 commit acd64cf

File tree

4 files changed

+50
-10
lines changed

4 files changed

+50
-10
lines changed

src/ducktools/pythonfinder/__main__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
ModuleImport("argparse"),
3636
ModuleImport("csv"),
3737
ModuleImport("subprocess"),
38+
ModuleImport("sysconfig"),
3839
ModuleImport("platform"),
3940
FromImport(".pythonorg_search", "PythonOrgSearch"),
4041
FromImport("packaging.specifiers", "SpecifierSet"),
@@ -207,7 +208,7 @@ def display_local_installs(
207208
version_str = f"*{version_str}"
208209
elif (
209210
sys.prefix != sys.base_prefix
210-
and os.path.commonpath([install.executable, sys.base_prefix]) == sys.base_prefix
211+
and install.paths.get("stdlib") == _laz.sysconfig.get_path("stdlib")
211212
):
212213
version_str = f"**{version_str}"
213214

@@ -240,7 +241,7 @@ def display_local_installs(
240241
if sys.platform != "win32":
241242
print("[] - This Python install is shadowed by another on Path")
242243
print("* - This is the active Python executable used to call this module")
243-
print("** - This is the parent Python executable of the venv used to call this module")
244+
print("** - This is a parent Python executable of the venv used to call this module")
244245
print()
245246

246247
headings_str = f"| {headings[0]:<{max_version_len}s} | {headings[1]:<{max_executable_len}s} |"

src/ducktools/pythonfinder/details_script.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def version_str_to_tuple(version):
4646
releaselevel = "alpha"
4747
elif releaselevel == "b":
4848
releaselevel = "beta"
49-
elif releaselevel == "rc":
49+
elif releaselevel in {"c", "rc"}:
5050
releaselevel = "candidate"
5151
else:
5252
releaselevel = "final"
@@ -80,22 +80,36 @@ def get_details():
8080
"graalpy_version": version_str_to_tuple(ver)
8181
}
8282
except (NameError, ValueError):
83-
metadata = {"{}_version".format(implementation): sys.implementation.version}
83+
metadata = {"{}_version".format(implementation): sys.implementation.version}
8484
elif implementation != "cpython": # pragma: no cover
85-
metadata = {"{}_version".format(implementation): sys.implementation.version}
85+
if implementation == "micropython":
86+
imp_ver = sys.implementation.version[:3]
87+
else:
88+
imp_ver = sys.implementation.version
89+
90+
metadata = {"{}_version".format(implementation): imp_ver}
8691
else:
8792
metadata = {}
8893
if sys.version_info >= (3, 13):
8994
import sysconfig
9095
freethreaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
9196
metadata["freethreaded"] = freethreaded
9297

98+
# Attempt to get the paths to stdlib etc from sysconfig
99+
try:
100+
import sysconfig # Exists in 2.7 and 3.2 onwards, missing in earlier and micropython
101+
except ImportError:
102+
paths = {}
103+
else:
104+
paths = sysconfig.get_paths()
105+
93106
install = dict(
94107
version=list(sys.version_info),
95108
executable=sys.executable,
96109
architecture="64bit" if (sys.maxsize > 2**32) else "32bit",
97110
implementation=implementation,
98111
metadata=metadata,
112+
paths=paths,
99113
)
100114

101115
return install

src/ducktools/pythonfinder/linux/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@
3737
from .pyenv_search import get_pyenv_pythons, get_pyenv_root
3838

3939

40+
KNOWN_MANAGED_PATHS = {
41+
"/usr/bin": "OS",
42+
"/bin": "OS",
43+
"/usr/sbin": "OS",
44+
"/sbin": "OS",
45+
}
46+
47+
4048
def get_path_pythons(*, finder: DetailFinder | None = None) -> Iterator[PythonInstall]:
4149
exe_names = set()
4250

@@ -46,7 +54,7 @@ def get_path_pythons(*, finder: DetailFinder | None = None) -> Iterator[PythonIn
4654

4755
excluded_folders = [pyenv_root, uv_root]
4856

49-
finder = DetailFinder if finder is None else finder
57+
finder = DetailFinder() if finder is None else finder
5058

5159
for fld in path_folders:
5260
# Don't retrieve pyenv installs
@@ -63,6 +71,9 @@ def get_path_pythons(*, finder: DetailFinder | None = None) -> Iterator[PythonIn
6371
continue
6472

6573
for install in get_folder_pythons(fld, finder=finder):
74+
if manager := KNOWN_MANAGED_PATHS.get(os.path.dirname(install.executable)):
75+
install.managed_by = manager
76+
6677
name = os.path.basename(install.executable)
6778
if name in exe_names:
6879
install.shadowed = True

src/ducktools/pythonfinder/shared.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
CACHE_FOLDER = os.path.join(USER_FOLDER, ".cache", "ducktools", "pythonfinder")
8181

8282

83-
CACHE_VERSION = 1
83+
CACHE_VERSION = 2
8484
DETAILS_CACHE_PATH = os.path.join(CACHE_FOLDER, f"runtime_cache_v{CACHE_VERSION}.json")
8585
INSTALLER_CACHE_PATH = os.path.join(CACHE_FOLDER, "installer_details.json")
8686

@@ -321,6 +321,7 @@ class PythonInstall(Prefab):
321321
implementation: str = "cpython"
322322
managed_by: str | None = None
323323
metadata: dict = attribute(default_factory=dict)
324+
paths: dict[str, str] = attribute(default_factory=dict)
324325
shadowed: bool = attribute(default=False, serialize=False)
325326
_implementation_version: tuple[int, int, int, str, int] | None = attribute(default=None, private=True)
326327

@@ -365,9 +366,13 @@ def from_str(
365366
implementation: str = "cpython",
366367
managed_by: str | None = None,
367368
metadata: dict | None = None,
369+
paths: dict | None = None,
368370
):
369371
version_tuple = version_str_to_tuple(version)
370372

373+
metadata = {} if metadata is None else metadata
374+
paths = {} if paths is None else paths
375+
371376
# noinspection PyArgumentList
372377
return cls(
373378
version=version_tuple,
@@ -376,6 +381,7 @@ def from_str(
376381
implementation=implementation,
377382
managed_by=managed_by,
378383
metadata=metadata,
384+
paths=paths,
379385
)
380386

381387
@classmethod
@@ -386,11 +392,14 @@ def from_json(
386392
architecture,
387393
implementation,
388394
metadata,
395+
paths=None,
389396
managed_by=None,
390397
):
391398
if arch_ver := metadata.get(f"{implementation}_version"):
392399
metadata[f"{implementation}_version"] = tuple(arch_ver)
393400

401+
paths = {} if paths is None else paths
402+
394403
# noinspection PyArgumentList
395404
return cls(
396405
version=tuple(version), # type: ignore
@@ -399,6 +408,7 @@ def from_json(
399408
implementation=implementation,
400409
managed_by=managed_by,
401410
metadata=metadata,
411+
paths=paths,
402412
)
403413

404414
def get_pip_version(self) -> str | None:
@@ -430,14 +440,16 @@ def _python_exe_regex(basename: str = "python"):
430440

431441
def get_folder_pythons(
432442
base_folder: str | os.PathLike,
433-
basenames: tuple[str] = ("python", "pypy"),
443+
basenames: tuple[str, ...] = ("python", "pypy", "micropython"),
434444
finder: DetailFinder | None = None,
435445
managed_by: str | None = None,
436446
):
437447
regexes = [_python_exe_regex(name) for name in basenames]
438448

439449
finder = DetailFinder() if finder is None else finder
440450

451+
base_folder = str(base_folder)
452+
441453
with finder, os.scandir(base_folder) as fld:
442454
for file_path in fld:
443455
try:
@@ -452,11 +464,13 @@ def get_folder_pythons(
452464
p = file_path.path
453465
if file_path.is_symlink():
454466
# Might be a venv - look for pyvenv.cfg in parent
455-
fld = os.path.dirname(p)
456-
if os.path.exists(os.path.join(fld, "../pyvenv.cfg")):
467+
dirname = os.path.dirname(p)
468+
469+
if os.path.exists(os.path.join(dirname, "../pyvenv.cfg")):
457470
continue
458471
else:
459472
p = file_path.path
473+
460474
install = finder.get_install_details(p, managed_by=managed_by)
461475
if install:
462476
yield install

0 commit comments

Comments
 (0)